Repository Public
1"use client";2import { useState, type ReactNode } from "react";3import { Button, cx, Sheet } from "@/components/ui";45export function Toggle({ checked, onChange, label, hint, disabled }: { checked: boolean; onChange: (v: boolean) => void; label: string; hint?: string; disabled?: boolean }) {6 return (7 <label className={cx("flex items-start gap-3 min-h-[44px] py-1.5 cursor-pointer", disabled && "opacity-60 cursor-not-allowed")}>8 <button type="button" role="switch" aria-checked={checked} aria-label={label} disabled={disabled} onClick={() => onChange(!checked)}9 className={cx("relative mt-0.5 w-[46px] h-[28px] rounded-full shrink-0 transition-colors cursor-pointer", checked ? "bg-emerald" : "bg-line-strong")}>10 <span className={cx("absolute top-[3px] left-[3px] w-[22px] h-[22px] rounded-full bg-surface shadow-e1 transition-transform", checked && "translate-x-[18px]")} />11 </button>12 <span className="min-w-0 pt-0.5"><span className="block font-medium">{label}</span>{hint ? <span className="block hint !mt-0.5">{hint}</span> : null}</span>13 </label>14 );15}1617export function Section({ title, lead, actions, children }: { title: string; lead?: ReactNode; actions?: ReactNode; children: ReactNode }) {18 return (19 <section className="card p-5 sm:p-6">20 <div className="flex flex-wrap items-start justify-between gap-3 mb-4">21 <div className="min-w-0"><h2 className="t-headline">{title}</h2>{lead ? <p className="t-mute text-sm mt-1 max-w-2xl">{lead}</p> : null}</div>22 {actions ? <div className="flex flex-wrap gap-2">{actions}</div> : null}23 </div>24 {children}25 </section>26 );27}2829export function CopyButton({ text, label, copied }: { text: string; label: string; copied: string }) {30 const [done, setDone] = useState(false);31 return (32 <Button small icon={done ? "check" : "link"} onClick={async () => { try { await navigator.clipboard.writeText(text); setDone(true); setTimeout(() => setDone(false), 2000); } catch { /* clipboard refused */ } }}>33 {done ? copied : label}34 </Button>35 );36}3738export function Confirm({ open, onClose, title, children, confirmLabel, cancelLabel, onConfirm, busy, danger = true }: { open: boolean; onClose: () => void; title: string; children: ReactNode; confirmLabel: string; cancelLabel: string; onConfirm: () => void; busy?: boolean; danger?: boolean }) {39 return (40 <Sheet open={open} onClose={onClose} title={title}41 footer={<><Button onClick={onClose}>{cancelLabel}</Button><Button variant={danger ? "danger" : "primary"} busy={busy} onClick={onConfirm}>{confirmLabel}</Button></>}>42 <div className="grid gap-3 text-[15px]">{children}</div>43 </Sheet>44 );45}4647/** Group heading inside a long form. */48export function FormGroup({ title, children }: { title: string; children: ReactNode }) {49 return <fieldset className="grid gap-4 pt-5 mt-5 first:mt-0 first:pt-0 border-t border-line first:border-t-0"><legend className="t-eyebrow float-left w-full mb-1">{title}</legend>{children}</fieldset>;50}5152export const numOrEmpty = (v: string): number | "" => (v === "" ? "" : Number(v));53