@eifi1/ui-kit 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/amount-input.d.ts +6 -0
- package/dist/components/amount-input.js +35 -5
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/error-boundary.js +14 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +7 -3
- package/dist/components/numpad-sheet.js +4 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/text.js.map +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -1
- package/dist/rhf/fields.js.map +1 -1
- package/package.json +1 -1
- package/src/components/amount-input.tsx +73 -7
- package/src/components/error-boundary.tsx +17 -1
- package/src/components/numpad-sheet.tsx +13 -4
- package/src/components/text.tsx +2 -1
- package/src/lib/format.ts +3 -1
- package/src/rhf/fields.tsx +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/numpad-sheet.tsx"],"sourcesContent":["import { createPortal } from \"react-dom\";\nimport { Delete } from \"lucide-react\";\nimport { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useEscapeKey } from \"../hooks/use-dismiss\";\nimport { evaluateExpression, formatResult, sanitizeLive } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/**\n * A full calculator keypad rendered as a bottom sheet — the mobile counterpart to\n * the desktop {@link CalculatorButton} popover, and the replacement for the old\n * under-the-field operator bar (`MathKeys`, feedback #334).\n *\n * A native app (YNAB) can swap the system keyboard for its own keypad; a PWA\n * cannot — but it CAN suppress the OS keyboard with `inputMode=\"none\"` on the\n * host field and paint its own keypad in the freed space. So the host input keeps\n * real focus/caret/selection + the existing value/commit contract, while this\n * sheet drives it: digits + operators + a live `= …` preview, evaluated through\n * the shared {@link evaluateExpression} engine. Desktop is untouched (it keeps the\n * native keyboard and the calculator popover); the host gates this to mobile.\n *\n * Buttons commit through the host's `onChange`/blur, so the sheet is stateless —\n * the field text is the single source of truth. `onPointerDown` preventDefault on\n * the root keeps the host input focused (caret stays, keyboard stays hidden) as\n * the user taps keys; the buttons' `onClick` still fires.\n */\n/** Names for the pad itself and its four non-digit keys. The first four are\n * screen-reader names; `done` is the one string here that is READ OFF THE SCREEN —\n * the brand-filled primary button — and it was a hardcoded English literal while\n * its three neighbours each took an override, so a German phone showed \"Done\"\n * among German labels with no prop able to change it. */\nexport interface NumberPadSheetLabels extends Partial<CalculatorLabels> {\n /** The pad's own name — the `calculator.panel` key under its older name here. */\n pad?: string;\n backspace?: string;\n clear?: string;\n equals?: string;\n /** Visible text on the primary key, not an `aria-label`. */\n done?: string;\n}\n\ntype PadKey =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: PadKeyName }\n | { kind: \"back\" };\n\n/** The {@link CalculatorLabels} key naming a non-digit key; digits are their glyph. */\ntype PadKeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\n// Insert chars that survive `sanitizeLive` (it keeps `× ÷` and ASCII `+ - * /`\n// but strips the unicode minus `−`, so minus inserts an ASCII \"-\" while showing\n// the nicer glyph). The evaluator normalises `× ÷` back to `* /`.\nconst PAD_KEYS: PadKey[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"-\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\n/** Every key's shape — including the thing this pad used to delete without replacing.\n *\n * `focus:outline-none` alone is not a style choice, it is the removal of the only\n * signal a keyboard user has; on a 4×4 grid of identical tiles it leaves no way at all\n * to tell which key Enter is about to press (the audit's §a11y, and the same defect as\n * the dropdown search box).\n *\n * `focus-visible` and `ring-inset`, for the two reasons {@link ToggleGroup} already\n * writes down. This is a touch control first, and a plain `focus:` ring paints itself\n * on every TAP, because a tap focuses the button. And the keys sit 6px apart, which is\n * thinner than an outward ring plus the neighbour it would spill onto — inset keeps the\n * ring inside the key it describes.\n *\n * --brand rather than --border-strong: the operator keys are filled with --border, and\n * a ring in the colour of the thing it surrounds is not an indicator. */\nconst PAD_BTN =\n \"flex h-14 items-center justify-center rounded-lg text-lg font-medium transition-transform select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)] active:scale-[0.97]\";\nconst PAD_DIGIT = \"bg-[var(--bg-surface-2)] text-[var(--text-primary)] active:bg-[var(--border)]\";\nconst PAD_ACCENT = \"bg-[var(--border)] text-[var(--text-primary)] active:bg-[var(--bg-surface-2)]\";\n\nexport function NumberPadSheet({\n value,\n onChange,\n onDone,\n label,\n labels,\n decimalMark = \".\",\n}: {\n value: string;\n /** Fired with the raw (sanitised) field text on every key — same contract as\n * the host field's own `onChange`. */\n onChange: (value: string) => void;\n /** The glyph on the decimal key — \",\" where the host shows a comma (AmountInput\n * in fr-CH, kastlan 40). The key still inserts a dot; the host's text is dot-form\n * underneath and `sanitizeLive` reads either. */\n decimalMark?: \".\" | \",\";\n /** Fired on \"Done\": the host blurs the input, which commits (evaluates) and\n * unmounts the sheet via its existing blur handler. */\n onDone: () => void;\n /** Optional field label, echoed in the sheet header so the user still knows\n * which field they're editing when the sheet covers it. */\n label?: ReactNode;\n /** Names for the pad itself and its non-digit keys — screen-reader names, except\n * `done`, which is the visible text on the primary key. Merged over `calculator`\n * from `<UiKitProvider labels>`: this is the same keypad as `CalculatorButton`'s,\n * on a phone, and one translation covers both. Digits need no name — their glyph\n * IS the name; the operators do (see {@link CalculatorLabels.plus}). */\n labels?: NumberPadSheetLabels;\n}) {\n // `pad` is this sheet's older spelling of `panel`, so it is folded in as that.\n const fromProps = useMemo(() => {\n if (!labels) return undefined;\n const { pad, ...rest } = labels;\n return pad === undefined ? rest : { ...rest, panel: pad };\n }, [labels]);\n const text = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n\n // NO `useBodyScrollLock`, and that is the point of the control (Keksdose live\n // #317: *\"Background not scrollable when the amount input calculator field is\n // open\"*).\n //\n // This sheet is a KEYBOARD, not a dialog. It is opened by focusing a field and\n // exists only because a PWA cannot swap the system keyboard for its own — so it\n // suppresses the OS keyboard with `inputMode=\"none\"` and paints itself in the\n // freed space. An OS keyboard does not freeze the page behind it; it takes the\n // bottom of the screen and leaves you free to scroll what is left, which is how\n // you reach the field you are typing into when the keys cover it.\n //\n // Locking here made the page unreachable at exactly the moment it matters most:\n // correcting a receipt total against the running line sum, where the figure you\n // are comparing against sits below the keypad. The other four holders of that\n // hook are all modal — `Modal`, `PickerSheet`, `FullBleedDialog`, the DataTable\n // row dialog — and they stay locked, because for them the page behind is not\n // part of the task.\n //\n // The pad still does not scroll ITSELF away under a stray drag: the root's\n // `onPointerDown` preventDefault (below) keeps the host input focused and eats\n // the gesture on the sheet, so a touch that starts on a key is not a page scroll.\n\n // ── NO focus trap either, and for a harder reason than the scroll lock ────────\n //\n // Wave 3 put `useFocusTrap` on every overlay in this package. This one is the\n // exception, and it is not an oversight to be tidied up later: a trap here would\n // DELETE THE COMPONENT IT WAS PROTECTING. Both hosts render the pad with\n // `showNumpad = isMobile && !disabled && focused`, where `focused` is the host\n // input's own focus state. Moving focus into the pad blurs that input, the host\n // sets `focused` false, and the pad unmounts — on mount, before a single key can be\n // pressed. The blur also COMMITS, so the trap would evaluate a half-typed\n // expression on the way out.\n //\n // That is the contract, not an accident of it. This sheet exists because a PWA\n // cannot swap the system keyboard for its own: the host keeps real focus, caret and\n // selection while `inputMode=\"none\"` suppresses the OS keys, and the root's\n // `onPointerDown` preventDefault below is there for the same reason. The pad is\n // `role=\"group\"`, not a dialog. A keyboard does not trap focus; it is what you type\n // WITH.\n //\n // What was genuinely missing is a way OUT that is not a tap. `useEscapeKey` and not\n // a handler on the sheet, because the sheet never has the focus a keydown would\n // bubble from — the host does, and the host is not this component. Escape does what\n // the Done key does rather than cancelling, because committing on blur is the\n // host's contract and a cancel would have to be the host's to offer.\n //\n // The limit, since it is better written down than discovered: inside a `Modal` the\n // dialog's own Escape handler sits on the focus path and stops the event before a\n // document listener sees it, so there the press closes the dialog — which it did\n // before this line existed too, and which takes the pad with it either way.\n useEscapeKey(onDone);\n\n const result = evaluateExpression(value);\n const preview = result !== null && formatResult(result) !== value.trim() ? `= ${formatResult(result)}` : \"\";\n\n const insert = (ch: string) => onChange(sanitizeLive(value + ch));\n const backspace = () => onChange(value.slice(0, -1));\n const clearAll = () => onChange(\"\");\n const equals = () => {\n if (result !== null) onChange(formatResult(result));\n };\n\n const sheet = (\n <div\n role=\"group\"\n aria-label={text.panel}\n // Keep the host input focused when tapping the pad: preventDefault on\n // pointerdown blocks the focus/blur, while the buttons' click still fires.\n onPointerDown={(e) => e.preventDefault()}\n className=\"fixed inset-x-0 bottom-0 z-50 border-t border-[var(--border)] bg-[var(--bg-surface)] px-2 pt-2 shadow-[0_-4px_24px_rgba(0,0,0,0.12)]\"\n style={{ paddingBottom: \"max(0.5rem, env(safe-area-inset-bottom))\" }}\n >\n {/* Header display: which field + the running expression and live result, so\n the value stays visible even when the sheet covers the field. */}\n <div className=\"flex items-end justify-between gap-3 px-2 pb-2\">\n {label != null && (\n <span className=\"truncate pb-1 text-xs font-medium text-[var(--money-neutral)]\">{label}</span>\n )}\n {/* At the logical end, opposite the label; the figures themselves stay\n left-to-right and right-aligned, like the desktop calculator's. */}\n <div className=\"ms-auto min-w-0\">\n <div dir=\"ltr\" className=\"truncate text-right font-mono text-lg leading-tight text-[var(--text-primary)]\">{value || \"0\"}</div>\n <div dir=\"ltr\" className=\"h-4 text-right font-mono text-xs text-[var(--money-neutral)]\">{preview}</div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-4 gap-1.5\">\n {PAD_KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={text.backspace}\n onClick={backspace}\n className={cn(PAD_BTN, PAD_ACCENT)}\n >\n <Delete className=\"size-5\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? text[key.name] : key.label}\n onClick={() => insert(key.ins)}\n className={cn(PAD_BTN, key.accent ? PAD_ACCENT : PAD_DIGIT)}\n >\n {key.kind === \"ins\" && key.name === \"decimal\" ? decimalMark : key.label}\n </button>\n ),\n )}\n </div>\n\n <div className=\"mt-1.5 grid grid-cols-4 gap-1.5\">\n <button type=\"button\" aria-label={text.clear} onClick={clearAll} className={cn(PAD_BTN, PAD_ACCENT)}>\n C\n </button>\n <button type=\"button\" aria-label={text.equals} onClick={equals} className={cn(PAD_BTN, PAD_ACCENT)}>\n =\n </button>\n <button\n type=\"button\"\n onClick={onDone}\n // The one key whose own fill is --brand, so it rings in the colour that fill\n // was chosen to be legible against instead (tailwind-merge keeps the later\n // ring colour). A --brand ring here would be present in the DOM and invisible\n // on screen, which is the defect this change exists to fix.\n className={cn(\n PAD_BTN,\n \"col-span-2 bg-[var(--brand)] text-[var(--brand-contrast)] focus-visible:ring-[var(--brand-contrast)] active:bg-[var(--brand-hover)]\",\n )}\n >\n {text.done}\n </button>\n </div>\n </div>\n );\n\n return createPortal(sheet, document.body);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/numpad-sheet.tsx"],"sourcesContent":["import { createPortal } from \"react-dom\";\nimport { Delete } from \"lucide-react\";\nimport { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useEscapeKey } from \"../hooks/use-dismiss\";\nimport { evaluateExpression, formatResult, sanitizeLive } from \"../lib/calc\";\nimport {\n DEFAULT_CALCULATOR_LABELS,\n useKitLabels,\n type CalculatorLabels,\n} from \"../i18n/kit-labels\";\n\n/**\n * A full calculator keypad rendered as a bottom sheet — the mobile counterpart to\n * the desktop {@link CalculatorButton} popover, and the replacement for the old\n * under-the-field operator bar (`MathKeys`, feedback #334).\n *\n * A native app (YNAB) can swap the system keyboard for its own keypad; a PWA\n * cannot — but it CAN suppress the OS keyboard with `inputMode=\"none\"` on the\n * host field and paint its own keypad in the freed space. So the host input keeps\n * real focus/caret/selection + the existing value/commit contract, while this\n * sheet drives it: digits + operators + a live `= …` preview, evaluated through\n * the shared {@link evaluateExpression} engine. Desktop is untouched (it keeps the\n * native keyboard and the calculator popover); the host gates this to mobile.\n *\n * Buttons commit through the host's `onChange`/blur, so the sheet is stateless —\n * the field text is the single source of truth. `onPointerDown` preventDefault on\n * the root keeps the host input focused (caret stays, keyboard stays hidden) as\n * the user taps keys; the buttons' `onClick` still fires.\n */\n/** Names for the pad itself and its four non-digit keys. The first four are\n * screen-reader names; `done` is the one string here that is READ OFF THE SCREEN —\n * the brand-filled primary button — and it was a hardcoded English literal while\n * its three neighbours each took an override, so a German phone showed \"Done\"\n * among German labels with no prop able to change it. */\nexport interface NumberPadSheetLabels extends Partial<CalculatorLabels> {\n /** The pad's own name — the `calculator.panel` key under its older name here. */\n pad?: string;\n backspace?: string;\n clear?: string;\n equals?: string;\n /** Visible text on the primary key, not an `aria-label`. */\n done?: string;\n}\n\ntype PadKey =\n | { kind: \"ins\"; label: string; ins: string; accent?: boolean; name?: PadKeyName }\n | { kind: \"back\" };\n\n/** The {@link CalculatorLabels} key naming a non-digit key; digits are their glyph. */\ntype PadKeyName = \"plus\" | \"minus\" | \"times\" | \"divide\" | \"decimal\";\n\n// Insert chars that survive `sanitizeLive` (it keeps `× ÷` and ASCII `+ - * /`\n// but strips the unicode minus `−`, so minus inserts an ASCII \"-\" while showing\n// the nicer glyph). The evaluator normalises `× ÷` back to `* /`.\nconst PAD_KEYS: PadKey[] = [\n { kind: \"ins\", label: \"7\", ins: \"7\" },\n { kind: \"ins\", label: \"8\", ins: \"8\" },\n { kind: \"ins\", label: \"9\", ins: \"9\" },\n { kind: \"ins\", label: \"÷\", ins: \"÷\", accent: true, name: \"divide\" },\n { kind: \"ins\", label: \"4\", ins: \"4\" },\n { kind: \"ins\", label: \"5\", ins: \"5\" },\n { kind: \"ins\", label: \"6\", ins: \"6\" },\n { kind: \"ins\", label: \"×\", ins: \"×\", accent: true, name: \"times\" },\n { kind: \"ins\", label: \"1\", ins: \"1\" },\n { kind: \"ins\", label: \"2\", ins: \"2\" },\n { kind: \"ins\", label: \"3\", ins: \"3\" },\n { kind: \"ins\", label: \"−\", ins: \"-\", accent: true, name: \"minus\" },\n { kind: \"ins\", label: \".\", ins: \".\", name: \"decimal\" },\n { kind: \"ins\", label: \"0\", ins: \"0\" },\n { kind: \"back\" },\n { kind: \"ins\", label: \"+\", ins: \"+\", accent: true, name: \"plus\" },\n];\n\n/** Every key's shape — including the thing this pad used to delete without replacing.\n *\n * `focus:outline-none` alone is not a style choice, it is the removal of the only\n * signal a keyboard user has; on a 4×4 grid of identical tiles it leaves no way at all\n * to tell which key Enter is about to press (the audit's §a11y, and the same defect as\n * the dropdown search box).\n *\n * `focus-visible` and `ring-inset`, for the two reasons {@link ToggleGroup} already\n * writes down. This is a touch control first, and a plain `focus:` ring paints itself\n * on every TAP, because a tap focuses the button. And the keys sit 6px apart, which is\n * thinner than an outward ring plus the neighbour it would spill onto — inset keeps the\n * ring inside the key it describes.\n *\n * --brand rather than --border-strong: the operator keys are filled with --border, and\n * a ring in the colour of the thing it surrounds is not an indicator. */\nconst PAD_BTN =\n \"flex h-14 items-center justify-center rounded-lg text-lg font-medium transition-transform select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)] active:scale-[0.97]\";\nconst PAD_DIGIT = \"bg-[var(--bg-surface-2)] text-[var(--text-primary)] active:bg-[var(--border)]\";\nconst PAD_ACCENT = \"bg-[var(--border)] text-[var(--text-primary)] active:bg-[var(--bg-surface-2)]\";\n\nexport function NumberPadSheet({\n value,\n onChange,\n onDone,\n label,\n labels,\n decimalMark = \".\",\n onResult,\n}: {\n value: string;\n /** Fired with the raw (sanitised) field text on every key — same contract as\n * the host field's own `onChange`. */\n onChange: (value: string) => void;\n /** The glyph on the decimal key — \",\" where the host shows a comma (AmountInput\n * in fr-CH, kastlan 40). With \",\" the key inserts a comma and the text is handed\n * back unsanitized, for the host to read by its locale (keksdose G1). */\n decimalMark?: \".\" | \",\";\n /** \"=\": the evaluated result, dot-decimal. Default `onChange`. A host that reads\n * typed text by the locale's marks takes it apart from keystrokes, since a result\n * like \"1.234\" is a decimal and never a grouped thousand. */\n onResult?: (value: string) => void;\n /** Fired on \"Done\": the host blurs the input, which commits (evaluates) and\n * unmounts the sheet via its existing blur handler. */\n onDone: () => void;\n /** Optional field label, echoed in the sheet header so the user still knows\n * which field they're editing when the sheet covers it. */\n label?: ReactNode;\n /** Names for the pad itself and its non-digit keys — screen-reader names, except\n * `done`, which is the visible text on the primary key. Merged over `calculator`\n * from `<UiKitProvider labels>`: this is the same keypad as `CalculatorButton`'s,\n * on a phone, and one translation covers both. Digits need no name — their glyph\n * IS the name; the operators do (see {@link CalculatorLabels.plus}). */\n labels?: NumberPadSheetLabels;\n}) {\n // `pad` is this sheet's older spelling of `panel`, so it is folded in as that.\n const fromProps = useMemo(() => {\n if (!labels) return undefined;\n const { pad, ...rest } = labels;\n return pad === undefined ? rest : { ...rest, panel: pad };\n }, [labels]);\n const text = useKitLabels(\"calculator\", DEFAULT_CALCULATOR_LABELS, fromProps);\n\n // NO `useBodyScrollLock`, and that is the point of the control (Keksdose live\n // #317: *\"Background not scrollable when the amount input calculator field is\n // open\"*).\n //\n // This sheet is a KEYBOARD, not a dialog. It is opened by focusing a field and\n // exists only because a PWA cannot swap the system keyboard for its own — so it\n // suppresses the OS keyboard with `inputMode=\"none\"` and paints itself in the\n // freed space. An OS keyboard does not freeze the page behind it; it takes the\n // bottom of the screen and leaves you free to scroll what is left, which is how\n // you reach the field you are typing into when the keys cover it.\n //\n // Locking here made the page unreachable at exactly the moment it matters most:\n // correcting a receipt total against the running line sum, where the figure you\n // are comparing against sits below the keypad. The other four holders of that\n // hook are all modal — `Modal`, `PickerSheet`, `FullBleedDialog`, the DataTable\n // row dialog — and they stay locked, because for them the page behind is not\n // part of the task.\n //\n // The pad still does not scroll ITSELF away under a stray drag: the root's\n // `onPointerDown` preventDefault (below) keeps the host input focused and eats\n // the gesture on the sheet, so a touch that starts on a key is not a page scroll.\n\n // ── NO focus trap either, and for a harder reason than the scroll lock ────────\n //\n // Wave 3 put `useFocusTrap` on every overlay in this package. This one is the\n // exception, and it is not an oversight to be tidied up later: a trap here would\n // DELETE THE COMPONENT IT WAS PROTECTING. Both hosts render the pad with\n // `showNumpad = isMobile && !disabled && focused`, where `focused` is the host\n // input's own focus state. Moving focus into the pad blurs that input, the host\n // sets `focused` false, and the pad unmounts — on mount, before a single key can be\n // pressed. The blur also COMMITS, so the trap would evaluate a half-typed\n // expression on the way out.\n //\n // That is the contract, not an accident of it. This sheet exists because a PWA\n // cannot swap the system keyboard for its own: the host keeps real focus, caret and\n // selection while `inputMode=\"none\"` suppresses the OS keys, and the root's\n // `onPointerDown` preventDefault below is there for the same reason. The pad is\n // `role=\"group\"`, not a dialog. A keyboard does not trap focus; it is what you type\n // WITH.\n //\n // What was genuinely missing is a way OUT that is not a tap. `useEscapeKey` and not\n // a handler on the sheet, because the sheet never has the focus a keydown would\n // bubble from — the host does, and the host is not this component. Escape does what\n // the Done key does rather than cancelling, because committing on blur is the\n // host's contract and a cancel would have to be the host's to offer.\n //\n // The limit, since it is better written down than discovered: inside a `Modal` the\n // dialog's own Escape handler sits on the focus path and stops the event before a\n // document listener sees it, so there the press closes the dialog — which it did\n // before this line existed too, and which takes the pad with it either way.\n useEscapeKey(onDone);\n\n const result = evaluateExpression(value);\n const preview = result !== null && formatResult(result) !== value.trim() ? `= ${formatResult(result)}` : \"\";\n\n // With a \",\" mark the host keeps the typist's own text (AmountInput, keksdose G1):\n // the key inserts the comma it shows, and the host, not this pad, decides which mark\n // is the decimal one — sanitizing here would fold a grouping \".\" into a decimal.\n const insert = (ch: string) =>\n onChange(decimalMark === \",\" ? value + (ch === \".\" ? \",\" : ch) : sanitizeLive(value + ch));\n const backspace = () => onChange(value.slice(0, -1));\n const clearAll = () => onChange(\"\");\n const equals = () => {\n if (result !== null) (onResult ?? onChange)(formatResult(result));\n };\n\n const sheet = (\n <div\n role=\"group\"\n aria-label={text.panel}\n // Keep the host input focused when tapping the pad: preventDefault on\n // pointerdown blocks the focus/blur, while the buttons' click still fires.\n onPointerDown={(e) => e.preventDefault()}\n className=\"fixed inset-x-0 bottom-0 z-50 border-t border-[var(--border)] bg-[var(--bg-surface)] px-2 pt-2 shadow-[0_-4px_24px_rgba(0,0,0,0.12)]\"\n style={{ paddingBottom: \"max(0.5rem, env(safe-area-inset-bottom))\" }}\n >\n {/* Header display: which field + the running expression and live result, so\n the value stays visible even when the sheet covers the field. */}\n <div className=\"flex items-end justify-between gap-3 px-2 pb-2\">\n {label != null && (\n <span className=\"truncate pb-1 text-xs font-medium text-[var(--money-neutral)]\">{label}</span>\n )}\n {/* At the logical end, opposite the label; the figures themselves stay\n left-to-right and right-aligned, like the desktop calculator's. */}\n <div className=\"ms-auto min-w-0\">\n <div dir=\"ltr\" className=\"truncate text-right font-mono text-lg leading-tight text-[var(--text-primary)]\">{value || \"0\"}</div>\n <div dir=\"ltr\" className=\"h-4 text-right font-mono text-xs text-[var(--money-neutral)]\">{preview}</div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-4 gap-1.5\">\n {PAD_KEYS.map((key, i) =>\n key.kind === \"back\" ? (\n <button\n key=\"back\"\n type=\"button\"\n aria-label={text.backspace}\n onClick={backspace}\n className={cn(PAD_BTN, PAD_ACCENT)}\n >\n <Delete className=\"size-5\" />\n </button>\n ) : (\n <button\n key={`${key.ins}-${i}`}\n type=\"button\"\n aria-label={key.name ? text[key.name] : key.label}\n onClick={() => insert(key.ins)}\n className={cn(PAD_BTN, key.accent ? PAD_ACCENT : PAD_DIGIT)}\n >\n {key.kind === \"ins\" && key.name === \"decimal\" ? decimalMark : key.label}\n </button>\n ),\n )}\n </div>\n\n <div className=\"mt-1.5 grid grid-cols-4 gap-1.5\">\n <button type=\"button\" aria-label={text.clear} onClick={clearAll} className={cn(PAD_BTN, PAD_ACCENT)}>\n C\n </button>\n <button type=\"button\" aria-label={text.equals} onClick={equals} className={cn(PAD_BTN, PAD_ACCENT)}>\n =\n </button>\n <button\n type=\"button\"\n onClick={onDone}\n // The one key whose own fill is --brand, so it rings in the colour that fill\n // was chosen to be legible against instead (tailwind-merge keeps the later\n // ring colour). A --brand ring here would be present in the DOM and invisible\n // on screen, which is the defect this change exists to fix.\n className={cn(\n PAD_BTN,\n \"col-span-2 bg-[var(--brand)] text-[var(--brand-contrast)] focus-visible:ring-[var(--brand-contrast)] active:bg-[var(--brand-hover)]\",\n )}\n >\n {text.done}\n </button>\n </div>\n </div>\n );\n\n return createPortal(sheet, document.body);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/text.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/* ── SectionLabel ─────────────────────────────────────────────────────────── */\n\n/**\n * `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;\n * `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings\n * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a\n * dense panel (a legend, a menu) or above a block of page content, and one size looked\n * wrong in the other place.\n *\n * `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels\n * its figures (\"Assigned\", \"Activity\", \"Upcoming\", \"Available\" —\n * budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line\n * (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a\n * figure that size, 12px competed with it. Named `md` rather than slotted in order\n * because `sm` was already the default and renaming it would move every caller.\n *\n * `md` is also the heading over a CHART COLUMN (lenkbank P9): the small label centred\n * over each of a pair of facing charts, or over each column of a small-multiples grid.\n * There the label names a plot, not a section of the page, so `sm` (12px, the page's\n * section heading) out-ranks the card title above it, and `xs` (10px, a legend's\n * title) sinks below the axis ticks it sits beside. Rule of thumb: `xs` inside a\n * control (legend, menu), `md` over a figure or a plot, `sm` over a block of page\n * content.\n */\nexport type SectionLabelSize = \"xs\" | \"md\" | \"sm\";\n\n/**\n * The small, uppercase, tracked, muted label — as a class string, per size.\n *\n * Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a\n * caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better\n * reached as a string than wrapped in a component that only takes a `className`. The\n * uppercase is CSS, so a screen reader reads the text as written: write it in normal\n * case.\n */\nexport const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n md: \"text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n sm: \"text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n};\n\n/**\n * `plain` (default): the label alone, as above.\n *\n * `band` (0.13, keksdose F5): the label on a full-width `surface-2` bar with a bottom\n * border — the day-group header of keksdose's mobile transaction list\n * (mobile-transaction-list.tsx:171), which separates one day's rows from the next.\n * Its type is kept as keksdose drew it — `font-medium` in the SECONDARY text colour,\n * not the plain label's semibold muted — so adopting the kit does not restyle a live\n * list: the bar already sets the label apart, and needs no extra weight. The padding (`px-4 py-2`) lines the text up with a\n * `ListItem density=\"comfortable\"` row below it; override it with `className` for a\n * list at another density. A sticky header is the caller's: add `sticky top-0`.\n */\nexport type SectionLabelVariant = \"plain\" | \"band\";\n\n/** The band's own type and box, per size — spelled out rather than merged over\n * {@link SECTION_LABEL_CLASS}, so the result does not hang on tailwind-merge telling\n * a font-size `text
|
|
1
|
+
{"version":3,"sources":["../../src/components/text.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/* ── SectionLabel ─────────────────────────────────────────────────────────── */\n\n/**\n * `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;\n * `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings\n * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a\n * dense panel (a legend, a menu) or above a block of page content, and one size looked\n * wrong in the other place.\n *\n * `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels\n * its figures (\"Assigned\", \"Activity\", \"Upcoming\", \"Available\" —\n * budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line\n * (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a\n * figure that size, 12px competed with it. Named `md` rather than slotted in order\n * because `sm` was already the default and renaming it would move every caller.\n *\n * `md` is also the heading over a CHART COLUMN (lenkbank P9): the small label centred\n * over each of a pair of facing charts, or over each column of a small-multiples grid.\n * There the label names a plot, not a section of the page, so `sm` (12px, the page's\n * section heading) out-ranks the card title above it, and `xs` (10px, a legend's\n * title) sinks below the axis ticks it sits beside. Rule of thumb: `xs` inside a\n * control (legend, menu), `md` over a figure or a plot, `sm` over a block of page\n * content.\n */\nexport type SectionLabelSize = \"xs\" | \"md\" | \"sm\";\n\n/**\n * The small, uppercase, tracked, muted label — as a class string, per size.\n *\n * Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a\n * caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better\n * reached as a string than wrapped in a component that only takes a `className`. The\n * uppercase is CSS, so a screen reader reads the text as written: write it in normal\n * case.\n */\nexport const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n md: \"text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n sm: \"text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n};\n\n/**\n * `plain` (default): the label alone, as above.\n *\n * `band` (0.13, keksdose F5): the label on a full-width `surface-2` bar with a bottom\n * border — the day-group header of keksdose's mobile transaction list\n * (mobile-transaction-list.tsx:171), which separates one day's rows from the next.\n * Its type is kept as keksdose drew it — `font-medium` in the SECONDARY text colour,\n * not the plain label's semibold muted — so adopting the kit does not restyle a live\n * list: the bar already sets the label apart, and needs no extra weight. The padding (`px-4 py-2`) lines the text up with a\n * `ListItem density=\"comfortable\"` row below it; override it with `className` for a\n * list at another density. A sticky header is the caller's: add `sticky top-0`.\n */\nexport type SectionLabelVariant = \"plain\" | \"band\";\n\n/** The band's own type and box, per size — spelled out rather than merged over\n * {@link SECTION_LABEL_CLASS}, so the result does not hang on tailwind-merge telling\n * a font-size arbitrary `text-` value from a colour one. (Not spelt out as class\n * names here: Tailwind scans comments, and a literal one broke the showcase CSS.) */\nconst SECTION_LABEL_BAND_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-[10px]\",\n md: \"text-[11px]\",\n sm: \"text-xs\",\n};\nconst BAND =\n \"block border-b border-[var(--border)] bg-[var(--bg-surface-2)] px-4 py-2 font-medium uppercase tracking-wide text-[var(--text-secondary)]\";\n\ntype SectionLabelElement = \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"span\" | \"div\" | \"legend\";\n\nexport interface SectionLabelProps extends ComponentPropsWithoutRef<\"h3\"> {\n /**\n * The element — which is the heading LEVEL, and so is the caller's to pick: the kit\n * cannot know where in the outline the label sits. Default `h3`, which is what\n * lenkbank's control page writes under its `h2` card title. Pass `span` or `p` for a\n * label that is not a heading at all (a legend group's title).\n */\n as?: SectionLabelElement;\n size?: SectionLabelSize;\n /** `band` draws the label on a `surface-2` bar with a bottom border — a list's group\n * header. See {@link SectionLabelVariant}. */\n variant?: SectionLabelVariant;\n children: ReactNode;\n}\n\n/**\n * A section's small uppercase label. lenkbank spells\n * `text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400`\n * on every `<h3>` of its control page and more; keksdose's budget switcher writes the\n * 10px version on a menu row. One component, the token colour, and the level chosen\n * where the outline is known.\n */\nexport function SectionLabel({\n as = \"h3\",\n size = \"sm\",\n variant = \"plain\",\n className,\n children,\n ...rest\n}: SectionLabelProps) {\n const Tag = as as ElementType;\n const type = variant === \"band\" ? cn(SECTION_LABEL_BAND_CLASS[size], BAND) : SECTION_LABEL_CLASS[size];\n return (\n <Tag {...rest} className={cn(type, className)}>\n {children}\n </Tag>\n );\n}\n\n/* ── Caption ──────────────────────────────────────────────────────────────── */\n\n/**\n * The caption type as a class string: 11px, snug, muted. lenkbank's `CAPTION`\n * (shared/ui/caption.ts:23) — written out 31 times before it was a constant — as a\n * token colour rather than `slate-500 dark:slate-400`, so it follows the palette.\n * Exported for the same reason lenkbank's is a string: every caller positions it\n * differently (`mt-1`, `self-end pb-2`, `border-t`), and what must be one thing is the\n * type, not the box.\n */\nexport const CAPTION_CLASS = \"text-[11px] leading-snug text-[var(--text-muted)]\";\n\nexport interface CaptionProps extends ComponentPropsWithoutRef<\"p\"> {\n /** Default `p`; `span` for a caption inside a line of other content. */\n as?: \"p\" | \"span\" | \"div\";\n children: ReactNode;\n}\n\n/**\n * The small grey sentence under a field, a table or a button — prose ABOUT the thing\n * above it. Not `FieldHint`, which hides behind a field label's \"?\" so a paragraph does\n * not make one field taller than its neighbour (lenkbank feedback #27).\n */\nexport function Caption({ as = \"p\", className, children, ...rest }: CaptionProps) {\n const Tag = as as ElementType;\n return (\n <Tag {...rest} className={cn(CAPTION_CLASS, className)}>\n {children}\n </Tag>\n );\n}\n"],"mappings":";AAyGI;AAxGJ,SAAS,UAAU;AAqCZ,MAAM,sBAAwD;AAAA,EACnE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAoBA,MAAM,2BAA6D;AAAA,EACjE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AACA,MAAM,OACJ;AA0BK,SAAS,aAAa;AAAA,EAC3B,KAAK;AAAA,EACL,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,MAAM;AACZ,QAAM,OAAO,YAAY,SAAS,GAAG,yBAAyB,IAAI,GAAG,IAAI,IAAI,oBAAoB,IAAI;AACrG,SACE,oBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,MAAM,SAAS,GACzC,UACH;AAEJ;AAYO,MAAM,gBAAgB;AAatB,SAAS,QAAQ,EAAE,KAAK,KAAK,WAAW,UAAU,GAAG,KAAK,GAAiB;AAChF,QAAM,MAAM;AACZ,SACE,oBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,eAAe,SAAS,GAClD,UACH;AAEJ;","names":[]}
|
package/dist/lib/format.js
CHANGED
|
@@ -31,7 +31,7 @@ function numberOptions(o) {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
function isMissing(value) {
|
|
34
|
-
return value === null || value === void 0 || Number.
|
|
34
|
+
return value === null || value === void 0 || !Number.isFinite(value);
|
|
35
35
|
}
|
|
36
36
|
function formatNumber(value, options = {}) {
|
|
37
37
|
if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
package/dist/lib/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as\n * `empty` (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or\n * \"Invalid Date\".\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\n/** The options every number formatter shares (money and percent carry their own unit). */\nexport interface FormatNumberBaseOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n}\n\n/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */\nexport interface FormatNumberOptions extends FormatNumberBaseOptions {\n /**\n * A unit printed after the figure, joined by a narrow no-break space (U+202F):\n * \"3.400 N\", \"12 kg\", \"4,5 m²\". lenkbank (P8) prints forces, masses and lengths in\n * every table and summary, and each call site glued `${n} N` together with a plain\n * space — which wraps the \"N\" onto the next line in a narrow column and reads loose\n * beside the digit grouping. The narrow no-break space is the SI brochure's and the\n * Swiss typesetting rule's separator between value and unit, and it never breaks.\n *\n * Free text, not `Intl`'s `style: \"unit\"`: that one only knows the ECMA-402\n * sanctioned units (no \"N\", no \"kN\", no \"m³/h\"), so an app would still be gluing.\n * A missing value prints `empty` ALONE — \"— N\" says there is a newton-shaped\n * nothing, which is noise in a column that already heads its unit.\n */\n unit?: string;\n}\n\n/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */\nconst UNIT_SEPARATOR = \"\\u202F\";\n\nexport interface FormatMoneyOptions extends FormatNumberBaseOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberBaseOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\nfunction isMissing(value: number | null | undefined): value is null | undefined {\n return value === null || value === undefined || Number.isNaN(value);\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\" — with `unit`, \"3.400 N\". */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const figure = numberFormatter(options.locale, numberOptions(options)).format(value);\n return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(\n value: number | null | undefined,\n currency: string,\n options: FormatMoneyOptions = {},\n): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n return numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n }).format(value);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? value / 100 : value;\n return numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n }).format(ratio);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. */\n timeZone?: string;\n}\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\nfunction dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (\n value: number | null | undefined,\n currency: string,\n options?: WithoutLocale<FormatMoneyOptions>,\n ) => string;\n formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),\n }),\n [kitLocale],\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as\n * `empty` (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or\n * \"Invalid Date\".\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\n/** The options every number formatter shares (money and percent carry their own unit). */\nexport interface FormatNumberBaseOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n}\n\n/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */\nexport interface FormatNumberOptions extends FormatNumberBaseOptions {\n /**\n * A unit printed after the figure, joined by a narrow no-break space (U+202F):\n * \"3.400 N\", \"12 kg\", \"4,5 m²\". lenkbank (P8) prints forces, masses and lengths in\n * every table and summary, and each call site glued `${n} N` together with a plain\n * space — which wraps the \"N\" onto the next line in a narrow column and reads loose\n * beside the digit grouping. The narrow no-break space is the SI brochure's and the\n * Swiss typesetting rule's separator between value and unit, and it never breaks.\n *\n * Free text, not `Intl`'s `style: \"unit\"`: that one only knows the ECMA-402\n * sanctioned units (no \"N\", no \"kN\", no \"m³/h\"), so an app would still be gluing.\n * A missing value prints `empty` ALONE — \"— N\" says there is a newton-shaped\n * nothing, which is noise in a column that already heads its unit.\n */\n unit?: string;\n}\n\n/** Narrow no-break space (U+202F): between a figure and its {@link FormatNumberOptions.unit}. */\nconst UNIT_SEPARATOR = \"\\u202F\";\n\nexport interface FormatMoneyOptions extends FormatNumberBaseOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberBaseOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\nfunction isMissing(value: number | null | undefined): value is null | undefined {\n // ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing\n // figure, and \"∞\" printed in a money or measurement column reads like data.\n return value === null || value === undefined || !Number.isFinite(value);\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\" — with `unit`, \"3.400 N\". */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const figure = numberFormatter(options.locale, numberOptions(options)).format(value);\n return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(\n value: number | null | undefined,\n currency: string,\n options: FormatMoneyOptions = {},\n): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n return numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n }).format(value);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? value / 100 : value;\n return numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n }).format(ratio);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. */\n timeZone?: string;\n}\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\nfunction dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (\n value: number | null | undefined,\n currency: string,\n options?: WithoutLocale<FormatMoneyOptions>,\n ) => string;\n formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),\n }),\n [kitLocale],\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/rhf/fields.d.ts
CHANGED
|
@@ -209,7 +209,9 @@ type RhfMoneyFieldProps<TFieldValues extends FieldValues = FieldValues, TName ex
|
|
|
209
209
|
/** What an emptied field stores when `valueAs` is `"number"`. Default `null`. */
|
|
210
210
|
emptyValue?: null | "";
|
|
211
211
|
/** Decimals the amount settles to on blur, Enter or a calculator result. Default:
|
|
212
|
-
* the currency's minor unit (CHF 2, JPY 0).
|
|
212
|
+
* the currency's minor unit (CHF 2, JPY 0). ⚠️ A unit price or a rate needs its
|
|
213
|
+
* column's scale here (`digits={4}` for 1.789 €/l), or it is stored rounded to
|
|
214
|
+
* cents without a word (keksdose G2). See {@link AmountInput}'s `digits`. */
|
|
213
215
|
digits?: number;
|
|
214
216
|
/** Clamp the settled amount (a release capped at the deposit's balance). Validate
|
|
215
217
|
* with `rules` as well if a clamp needs explaining. */
|
package/dist/rhf/fields.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rhf/fields.tsx"],"sourcesContent":["/**\n * Bound fields: one line per field of a react-hook-form form. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <Form {...form}>\n * <RhfTextField name=\"name\" label=\"Name\" hint=\"As on the lease\" required\n * rules={{ required: \"Enter a name\" }} />\n * <RhfMoneyField name=\"rent\" label=\"Net rent\" currency=\"CHF\" />\n * <RhfDateField name=\"start\" label=\"Start\" />\n * <RhfSelect name=\"type\" label=\"Type\" options={TYPES} />\n * <RhfCombobox name=\"tenantId\" label=\"Tenant\" options={tenants} clearable />\n * <RhfCheckbox name=\"isDefault\" label=\"Default account\" />\n * </Form>\n * ```\n *\n * Each folds the `FormField → FormItem → FormLabel → FormControl → field →\n * FormDescription → FormMessage` scaffold (see ./form) into one element, the way\n * kastlan's shared/components/form/fields.tsx does for its forms — with what that file\n * left out:\n *\n * - **The red border.** Every field gets `invalid` from the field state, so an errored\n * field PAINTS as well as announces. kastlan's number, money, date and entity fields\n * passed only `aria-invalid` through `FormControl`, or nothing, and `Input`, `Select`\n * and `Textarea` paint only from the prop (see the note in ./form).\n * - **Focus on error.** `field.ref` reaches the focusable element — the element itself\n * where the kit field forwards a ref, a focus handle on it where it does not (the\n * number field, the pickers) — so `handleSubmit` and `trigger(…, { shouldFocus })`\n * land the caret on the first field in error.\n * - **`control` is optional**: under `<Form {...form}>` the fields find the form\n * through react-hook-form's context. Pass it for the field's value types\n * (`control={form.control}` checks `name` against the form's values).\n *\n * The common props: `name`, `control`, `label` (a {@link FormLabel} above the field;\n * omitted, none — then give the field an `aria-label`), `hint` (standing advice under\n * the field, attached to it with `aria-describedby`), `required` (the label's mark,\n * and `aria-required` where the control takes it — validation is `rules`' or the\n * resolver's), `rules`, `disabled` (the control only — the value is still submitted;\n * `excludeWhenDisabled` for react-hook-form's drop-the-value semantics), and\n * `className` for the item's box.\n *\n * {@link RhfField} is the shell they are all built on, for a control the kit does not\n * ship (an address autocomplete): it takes a `render` and wires the rest.\n */\nimport { useLayoutEffect, useRef, useState, type ReactElement, type ReactNode } from \"react\";\nimport type {\n ControllerFieldState,\n ControllerProps,\n ControllerRenderProps,\n FieldPath,\n FieldValues,\n} from \"react-hook-form\";\nimport { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from \"./form\";\nimport { Input, Select, Textarea, type InputProps, type SelectProps, type TextareaProps } from \"../components/ui\";\nimport { NumberField, type NumberFieldProps } from \"../components/number-field\";\nimport { AmountInput } from \"../components/amount-input\";\nimport { DatePicker, type DatePickerProps } from \"../components/date-picker\";\nimport { Checkbox, type CheckboxProps } from \"../components/checkbox\";\nimport { EntityCombobox, type EntityComboboxProps } from \"../components/entity-combobox\";\nimport { Combobox, type ComboboxProps } from \"../components/combobox\";\nimport type { ComboClearValue } from \"../components/combobox-core\";\nimport { cn } from \"../lib/cn\";\n\n// ── the shell ────────────────────────────────────────────────────────────────\n\n/** What every bound field takes. */\nexport interface RhfFieldBaseProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> {\n name: TName;\n /** `form.control`. Optional under `<Form {...form}>`; pass it to type-check `name`. */\n control?: ControllerProps<TFieldValues, TName, TTransformed>[\"control\"];\n /** react-hook-form's `rules` (`required`, `min`, `validate` …). */\n rules?: ControllerProps<TFieldValues, TName, TTransformed>[\"rules\"];\n /** A {@link FormLabel} above the field. Omitted: no label — name the control with\n * an `aria-label` instead. */\n label?: ReactNode;\n /** Standing advice under the field (\"As printed on the card\"), attached to the\n * control with `aria-describedby`. */\n hint?: ReactNode;\n /** Draws the label's required mark and sets `aria-required` where the control\n * takes it. Whether the field IS required is `rules`' or the resolver's to say. */\n required?: boolean;\n /** Disables the control and KEEPS the value: a field locked to a preset (a lease's\n * preselected unit, a fixed period) still submits it, and `rules` still run.\n * react-hook-form's own Controller `disabled` drops the value from `handleSubmit`'s\n * data instead, which a locked preset must never do (kastlan, 0.12). To get that,\n * add `excludeWhenDisabled`. A form-wide `useForm({ disabled })` still disables\n * every field the RHF way. */\n disabled?: boolean;\n /** With `disabled`: disable through react-hook-form, so the value is left out of\n * the submitted data and validation skips it, like a disabled native input in a\n * plain form post. Default `false`. */\n excludeWhenDisabled?: boolean;\n /** Classes for the item's box (a `grid gap-1`). */\n className?: string;\n}\n\n/** What {@link RhfField}'s `render` receives. */\nexport interface RhfFieldRenderContext<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> {\n field: ControllerRenderProps<TFieldValues, TName>;\n fieldState: ControllerFieldState;\n /** The field has an error: pass it to the control's `invalid` so it paints. */\n invalid: boolean;\n /** The error's message, if any. */\n error: string | undefined;\n /** The control's id, which the label points at. `FormControl` sets it on the\n * rendered element already; this is for a control that routes it elsewhere. */\n id: string;\n /** The label's id, for a control that is named with `aria-labelledby`. */\n labelId: string;\n /** The ids of the rendered hint and message, if any. */\n describedBy: string | undefined;\n}\n\nexport interface RhfFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends RhfFieldBaseProps<TFieldValues, TName, TTransformed> {\n /** The control. Wrapped in {@link FormControl} (its `id`, `aria-describedby` and\n * `aria-invalid`) unless `asControl` is false. */\n render: (ctx: RhfFieldRenderContext<TFieldValues, TName>) => ReactElement;\n /** `false`: render the control as is, for one that wires its own ids from the\n * context. Default `true`. */\n asControl?: boolean;\n /** `false`: no {@link FormMessage}, for a control that renders its own error (its\n * `error` prop). Default `true`. */\n message?: boolean;\n /** `false`: no {@link FormLabel}, for a control that carries its label itself (a\n * checkbox). Default `true`. */\n showLabel?: boolean;\n}\n\nfunction hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/** The label, with an id the control can be named by. Inside the item, so it can read\n * the item's id. */\nfunction RhfLabel({ required, children }: { required?: boolean; children: ReactNode }) {\n const { id } = useFormField();\n return (\n <FormLabel id={`${id}-label`} required={required}>\n {children}\n </FormLabel>\n );\n}\n\nfunction ControlSlot<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({\n field,\n fieldState,\n render,\n asControl,\n}: {\n field: ControllerRenderProps<TFieldValues, TName>;\n fieldState: ControllerFieldState;\n render: RhfFieldProps<TFieldValues, TName>[\"render\"];\n asControl: boolean;\n}) {\n const { id, formItemId, describedBy } = useFormField();\n const element = render({\n field,\n fieldState,\n invalid: fieldState.invalid,\n error: fieldState.error?.message,\n id: formItemId,\n labelId: `${id}-label`,\n describedBy,\n });\n return asControl ? <FormControl>{element}</FormControl> : element;\n}\n\n/**\n * The shell every bound field is built on: label, control, hint and message around\n * a `render` of your own.\n *\n * ```tsx\n * <RhfField name=\"address\" label=\"Address\" render={({ field, invalid }) => (\n * <AddressAutocomplete value={field.value ?? \"\"} onChange={field.onChange} invalid={invalid} />\n * )} />\n * ```\n */\nexport function RhfField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n render,\n asControl = true,\n message = true,\n showLabel = true,\n}: RhfFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <FormField\n control={control}\n name={name}\n rules={rules}\n disabled={excludeWhenDisabled ? disabled : undefined}\n render={({ field, fieldState }) => (\n <FormItem className={className}>\n {showLabel && hasContent(label) && <RhfLabel required={required}>{label}</RhfLabel>}\n <ControlSlot\n field={disabled && !field.disabled ? { ...field, disabled: true } : field}\n fieldState={fieldState}\n render={render}\n asControl={asControl}\n />\n {hasContent(hint) && <FormDescription>{hint}</FormDescription>}\n {message && <FormMessage />}\n </FormItem>\n )}\n />\n );\n}\n\n/**\n * Hands react-hook-form a focus handle for a control that forwards no ref: its\n * `shouldFocusError` calls `focus()` on whatever `field.ref` was given. `find` runs at\n * focus time, so it sees the element as it is then.\n */\nfunction useFocusHandle(ref: ControllerRenderProps[\"ref\"], find: () => HTMLElement | null | undefined) {\n const findRef = useRef(find);\n useLayoutEffect(() => {\n findRef.current = find;\n });\n useLayoutEffect(() => {\n ref({ focus: () => findRef.current()?.focus() });\n }, [ref]);\n}\n\n// ── text ─────────────────────────────────────────────────────────────────────\n\ntype OwnInputProps = Omit<\n InputProps,\n \"name\" | \"value\" | \"defaultValue\" | \"onChange\" | \"onBlur\" | \"ref\" | \"label\" | \"error\" | \"invalid\" | \"className\" | \"disabled\" | \"required\" | \"inputClassName\"\n> & {\n /**\n * Classes for the `<input>` — `className` is the item's box (kastlan 42: a\n * `font-mono` IBAN, a `tabular-nums` reference number). {@link RhfTextarea} and\n * {@link RhfNumberField} had it; this one only reached `Input`'s prop of the same\n * name by accident of the rest-spread, undocumented and one refactor from being\n * dropped. Declared, it is part of the contract the three share.\n */\n inputClassName?: string;\n};\n\nexport type RhfTextFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnInputProps;\n\n/** A text {@link Input} — `type` for email, tel, password, url. Stores the string. */\nexport function RhfTextField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n ...inputProps\n}: RhfTextFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid }) => (\n <Input\n aria-required={required || undefined}\n {...inputProps}\n name={field.name}\n ref={field.ref}\n value={(field.value ?? \"\") as string}\n onChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n />\n )}\n />\n );\n}\n\ntype OwnTextareaProps = Omit<\n TextareaProps,\n \"name\" | \"value\" | \"defaultValue\" | \"onChange\" | \"onBlur\" | \"ref\" | \"label\" | \"error\" | \"invalid\" | \"className\" | \"disabled\" | \"required\"\n> & {\n /** Classes for the `<textarea>`. */\n inputClassName?: string;\n};\n\nexport type RhfTextareaProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextareaProps;\n\n/** A multi-line {@link Textarea}; takes `rows`. Stores the string. */\nexport function RhfTextarea<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n inputClassName,\n ...areaProps\n}: RhfTextareaProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid }) => (\n <Textarea\n aria-required={required || undefined}\n {...areaProps}\n className={inputClassName}\n name={field.name}\n ref={field.ref}\n value={(field.value ?? \"\") as string}\n onChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n />\n )}\n />\n );\n}\n\n// ── numbers ──────────────────────────────────────────────────────────────────\n\n/** A form value → the number field's `number | null`. Defaults hold `\"\"`, `null`, a\n * number, or a numeric string. */\nfunction toNumber(value: unknown): number | null {\n if (value === null || value === undefined || value === \"\") return null;\n const n = typeof value === \"number\" ? value : Number(String(value).replace(\",\", \".\"));\n return Number.isFinite(n) ? n : null;\n}\n\nexport type RhfNumberFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<\n NumberFieldProps,\n | \"digits\"\n | \"min\"\n | \"max\"\n | \"step\"\n | \"calculator\"\n | \"unit\"\n | \"unitPlacement\"\n | \"placeholder\"\n | \"inputClassName\"\n | \"locale\"\n | \"ariaLabel\"\n | \"autoFocus\"\n > & {\n /** What an emptied field stores. Default `null`; `\"\"` for a schema written\n * against a native number input (`z.coerce.number()` over `\"\"`). */\n emptyValue?: null | \"\";\n /** `false`: an emptied field snaps back to its last number instead of storing\n * `emptyValue`. Default `true`. */\n nullable?: boolean;\n };\n\n/**\n * The kit's {@link NumberField}: a parsed, rounded (`digits`) and clamped (`min` /\n * `max`) number, committed on blur or Enter in the locale's decimal mark. Stores a\n * number, or `emptyValue` when emptied.\n */\nexport function RhfNumberField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n emptyValue = null,\n nullable = true,\n ...numberProps\n}: RhfNumberFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid, id }) => (\n // FormControl clones this element with `id` / `aria-describedby` /\n // `aria-invalid`, which NumberControl hands on to the field.\n <NumberControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n id={id}\n emptyValue={emptyValue}\n nullable={nullable}\n numberProps={numberProps}\n />\n )}\n />\n );\n}\n\nfunction NumberControl({\n field,\n invalid,\n required,\n id,\n emptyValue,\n nullable,\n numberProps,\n ...aria\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n id: string;\n emptyValue: null | \"\";\n nullable: boolean;\n numberProps: Omit<RhfNumberFieldProps, keyof RhfFieldBaseProps | \"emptyValue\" | \"nullable\">;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: NumberFieldProps[\"aria-invalid\"];\n}) {\n // NumberField forwards no ref; it routes `id` to its <input>, where the handle\n // finds it.\n useFocusHandle(field.ref, () => document.getElementById(id));\n return (\n <NumberField\n {...numberProps}\n aria-describedby={aria[\"aria-describedby\"]}\n aria-invalid={aria[\"aria-invalid\"]}\n aria-required={required || undefined}\n id={id}\n value={toNumber(field.value)}\n onCommit={(n) => {\n field.onChange(n ?? emptyValue);\n field.onBlur();\n }}\n nullable={nullable}\n disabled={field.disabled}\n invalid={invalid}\n />\n );\n}\n\nexport type RhfIntegerFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfNumberFieldProps<TFieldValues, TName, TTransformed>;\n\n/**\n * {@link RhfNumberField} for a whole number — a floor, a room count, a year, a\n * notice period in months. Every such call site in kastlan (item 41) spelled out the\n * same three props, `digits={0} calculator={false} emptyValue=\"\"`, and a preset\n * written thirty times is one that is eventually written wrong once:\n *\n * - `digits={0}`: rounds on commit, so \"2.5\" rooms settles to 3 instead of reaching\n * an `Integer` column and failing server-side;\n * - `calculator={false}`: a count is typed, not worked out — the trigger is noise\n * beside a two-digit field;\n * - `emptyValue=\"\"`: kastlan's integer schemas are `z.coerce.number()` over `\"\"`,\n * written against the native number inputs these fields replaced.\n *\n * Each is only a default: pass the prop to override it (`emptyValue={null}` for a\n * nullable column). Everything else is {@link RhfNumberField}'s.\n */\nexport function RhfIntegerField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>(props: RhfIntegerFieldProps<TFieldValues, TName, TTransformed>) {\n // `??` / `=== undefined` rather than a spread over the defaults, so a caller\n // forwarding its own optional prop (`digits={props.digits}`) still gets the preset,\n // while an explicit `emptyValue={null}` keeps its null.\n return (\n <RhfNumberField\n {...props}\n digits={props.digits ?? 0}\n calculator={props.calculator ?? false}\n emptyValue={props.emptyValue === undefined ? \"\" : props.emptyValue}\n />\n );\n}\n\n// ── money ────────────────────────────────────────────────────────────────────\n\nexport type RhfMoneyFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & {\n placeholder?: string;\n /** The currency chip's code (\"CHF\"). */\n currency?: string;\n /** Makes the chip a currency picker. Bind it to another field with\n * `useController`, or to state. */\n onCurrencyChange?: (code: string) => void;\n /** Colours the figure for the direction of money. */\n tone?: \"neutral\" | \"outflow\" | \"inflow\";\n align?: \"start\" | \"center\";\n ariaLabel?: string;\n autoFocus?: boolean;\n /**\n * What the form stores: `\"number\"` (default) — `12.5`, or `emptyValue` while the\n * field is empty — or `\"string\"`, the field's own text (\"12.50\") for a schema that\n * parses it itself. A half-typed sum (\"12+\") leaves the stored number as it was\n * until it resolves.\n */\n valueAs?: \"number\" | \"string\";\n /** What an emptied field stores when `valueAs` is `\"number\"`. Default `null`. */\n emptyValue?: null | \"\";\n /** Decimals the amount settles to on blur, Enter or a calculator result. Default:\n * the currency's minor unit (CHF 2, JPY 0). See {@link AmountInput}'s `digits`. */\n digits?: number;\n /** Clamp the settled amount (a release capped at the deposit's balance). Validate\n * with `rules` as well if a clamp needs explaining. */\n min?: number;\n max?: number;\n};\n\n/** The amount a text reads as, or `undefined` for a draft that is not one yet. */\nfunction parseAmount(text: string): number | null | undefined {\n const trimmed = text.trim();\n if (trimmed === \"\") return null;\n if (!/^-?\\d*[.,]?\\d*$/.test(trimmed)) return undefined;\n const n = Number(trimmed.replace(\",\", \".\"));\n return Number.isFinite(n) ? n : undefined;\n}\n\nfunction MoneyControl({\n field,\n invalid,\n required,\n id,\n describedBy,\n props,\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n id: string;\n describedBy: string | undefined;\n props: Omit<RhfMoneyFieldProps, keyof RhfFieldBaseProps>;\n}) {\n const { valueAs = \"number\", emptyValue = null, ariaLabel, ...amountProps } = props;\n const value: unknown = field.value;\n const external = value === null || value === undefined ? \"\" : String(value);\n // The text being typed. A stored NUMBER cannot hold \"12.\" or \"12+5\", so the draft\n // lives here and is re-read from the form only when the form's value moves away\n // from what the draft already says (a reset, a setValue).\n const [draft, setDraft] = useState(external);\n const [seen, setSeen] = useState<unknown>(value);\n if (seen !== value) {\n setSeen(value);\n const current = valueAs === \"string\" ? draft : parseAmount(draft);\n const same = valueAs === \"string\" ? current === external : current === toNumber(value);\n if (!same) setDraft(external);\n }\n const input = useRef<HTMLInputElement | null>(null);\n // AmountInput reports no blur, and react-hook-form needs one to mark the field\n // touched: listen on its <input>.\n const onBlur = field.onBlur;\n useLayoutEffect(() => {\n const el = input.current;\n if (!el) return;\n el.addEventListener(\"blur\", onBlur);\n return () => el.removeEventListener(\"blur\", onBlur);\n }, [onBlur]);\n return (\n <div>\n <AmountInput\n {...amountProps}\n ariaLabel={ariaLabel}\n id={id}\n aria-describedby={describedBy}\n aria-required={required || undefined}\n ref={(el) => {\n input.current = el;\n field.ref(el);\n }}\n value={draft}\n disabled={field.disabled}\n invalid={invalid}\n onChange={(text) => {\n setDraft(text);\n if (valueAs === \"string\") {\n field.onChange(text);\n return;\n }\n const parsed = parseAmount(text);\n if (parsed === undefined) return;\n field.onChange(parsed ?? emptyValue);\n }}\n />\n </div>\n );\n}\n\n/**\n * The kit's {@link AmountInput}: a money figure with its currency chip and the\n * calculator (\"1200+80\" resolves on blur or Enter). Stores a number by default — see\n * `valueAs`.\n */\nexport function RhfMoneyField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n ...amountProps\n}: RhfMoneyFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n asControl={false}\n render={({ field, invalid, id, describedBy }) => (\n <MoneyControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n id={id}\n describedBy={describedBy}\n props={amountProps}\n />\n )}\n />\n );\n}\n\n// ── date ─────────────────────────────────────────────────────────────────────\n\nexport type RhfDateFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Omit<DatePickerProps, \"value\" | \"onChange\" | \"label\" | \"invalid\" | \"disabled\" | \"className\" | \"id\"> & {\n /** Classes for the picker. Default `w-full`. */\n inputClassName?: string;\n /** What a cleared date stores. Default `\"\"`, the empty ISO string; `null` for a\n * nullable schema. */\n emptyValue?: \"\" | null;\n };\n\nfunction DateControl({\n field,\n invalid,\n required,\n id,\n emptyValue,\n inputClassName,\n pickerProps,\n ...aria\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n id: string;\n emptyValue: \"\" | null;\n inputClassName?: string;\n pickerProps: Omit<DatePickerProps, \"value\" | \"onChange\" | \"label\" | \"invalid\" | \"disabled\" | \"className\" | \"id\">;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: DatePickerProps[\"aria-invalid\"];\n}) {\n useFocusHandle(field.ref, () => document.getElementById(id));\n const value: unknown = field.value;\n return (\n <DatePicker\n {...pickerProps}\n {...aria}\n aria-required={required || undefined}\n id={id}\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(iso) => {\n field.onChange(iso === \"\" ? emptyValue : iso);\n field.onBlur();\n }}\n disabled={field.disabled}\n invalid={invalid}\n className={inputClassName ?? \"w-full\"}\n />\n );\n}\n\n/** The kit's {@link DatePicker}, holding an ISO `\"YYYY-MM-DD\"` string. */\nexport function RhfDateField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n inputClassName,\n emptyValue = \"\",\n ...pickerProps\n}: RhfDateFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid, id }) => (\n // FormControl clones this element with `aria-describedby` / `aria-invalid`,\n // which DateControl hands on to the picker's trigger.\n <DateControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n id={id}\n emptyValue={emptyValue}\n inputClassName={inputClassName}\n pickerProps={pickerProps}\n />\n )}\n />\n );\n}\n\n// ── select ───────────────────────────────────────────────────────────────────\n\nexport interface RhfSelectOption {\n value: string | number;\n label: ReactNode;\n disabled?: boolean;\n}\n\nexport type RhfSelectProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<SelectProps, \"size\" | \"autoFocus\" | \"aria-label\"> & {\n /** The options, as data. Or pass `<option>` children. */\n options?: readonly RhfSelectOption[];\n children?: ReactNode;\n /** A first, empty option (\"Choose…\"), selected while the value is empty. */\n placeholder?: string;\n /** Store `Number(value)` rather than the option's string. An empty choice still\n * stores `\"\"`. */\n valueAsNumber?: boolean;\n /** Classes for the select. Default `w-full`. */\n selectClassName?: string;\n };\n\n/** A native {@link Select}, from `options` or `<option>` children. */\nexport function RhfSelect<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n options,\n children,\n placeholder,\n valueAsNumber,\n selectClassName,\n ...selectProps\n}: RhfSelectProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid }) => (\n <Select\n aria-required={required || undefined}\n {...selectProps}\n className={cn(\"w-full\", selectClassName)}\n name={field.name}\n ref={field.ref}\n value={(field.value ?? \"\") as string | number}\n onChange={(e) => {\n const raw = e.target.value;\n field.onChange(valueAsNumber && raw !== \"\" ? Number(raw) : raw);\n }}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n >\n {placeholder !== undefined && <option value=\"\">{placeholder}</option>}\n {options\n ? options.map((o) => (\n <option key={String(o.value)} value={o.value} disabled={o.disabled}>\n {o.label}\n </option>\n ))\n : children}\n </Select>\n )}\n />\n );\n}\n\n// ── checkbox ─────────────────────────────────────────────────────────────────\n\nexport type RhfCheckboxProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<CheckboxProps, \"inputClassName\" | \"autoFocus\" | \"aria-label\">;\n\n/**\n * The kit's {@link Checkbox}, label beside the box and `hint` under it (the\n * checkbox's own `description`). Stores a boolean. `required` is the native one here\n * — see Checkbox — so a `<form>` refuses to submit it unticked.\n */\nexport function RhfCheckbox<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n ...boxProps\n}: RhfCheckboxProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, required, disabled, excludeWhenDisabled, className }}\n showLabel={false}\n render={({ field, invalid }) => (\n <Checkbox\n {...boxProps}\n ref={field.ref}\n name={field.name}\n checked={Boolean(field.value)}\n onCheckedChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n required={required}\n invalid={invalid}\n label={label}\n description={hint}\n />\n )}\n />\n );\n}\n\n// ── comboboxes ───────────────────────────────────────────────────────────────\n\nexport type RhfComboboxProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n V extends string | number = string | number,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<\n EntityComboboxProps<V>,\n | \"options\"\n | \"loadOptions\"\n | \"loading\"\n | \"placeholder\"\n | \"searchPlaceholder\"\n | \"emptyLabel\"\n | \"clearable\"\n | \"clearLabel\"\n | \"onCreate\"\n | \"createLabel\"\n | \"filter\"\n | \"minChars\"\n | \"debounceMs\"\n > & {\n /** What a clear stores — the picker's `clearValue`. Default `null`; `\"\"` for a\n * schema that spells \"no choice\" as an empty string. */\n clearValue?: ComboClearValue;\n /** Classes for the picker. */\n comboClassName?: string;\n };\n\nfunction EntityControl<V extends string | number>({\n field,\n invalid,\n required,\n error,\n id,\n describedBy,\n clearValue,\n comboClassName,\n comboProps,\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n error: string | undefined;\n id: string;\n describedBy: string | undefined;\n clearValue: ComboClearValue;\n comboClassName?: string;\n comboProps: Omit<RhfComboboxProps<FieldValues, string, V>, keyof RhfFieldBaseProps | \"clearValue\" | \"comboClassName\">;\n}) {\n const box = useRef<HTMLDivElement>(null);\n // EntityCombobox puts `id` and the ARIA below on its trigger, so the label's\n // `htmlFor` names it, the hint describes it and the focus handle finds it by id.\n useFocusHandle(field.ref, () => document.getElementById(id));\n // EntityCombobox reports no blur: focus leaving the picker marks the field touched.\n const onBlur = field.onBlur;\n useLayoutEffect(() => {\n const el = box.current;\n if (!el) return;\n el.addEventListener(\"focusout\", onBlur);\n return () => el.removeEventListener(\"focusout\", onBlur);\n }, [onBlur]);\n const value: unknown = field.value;\n return (\n <div ref={box}>\n <EntityCombobox<V, ComboClearValue>\n {...comboProps}\n className={comboClassName}\n id={id}\n aria-describedby={describedBy}\n aria-required={required || undefined}\n value={(value ?? null) as V | ComboClearValue | null}\n clearValue={clearValue}\n onChange={(v) => field.onChange(v)}\n disabled={field.disabled}\n invalid={invalid}\n // Its own message, which it merges into the trigger's aria-describedby after\n // the hint.\n error={error}\n />\n </div>\n );\n}\n\n/**\n * The kit's {@link EntityCombobox}: an id-keyed pick from `options` or `loadOptions`.\n * Stores the option's `value`, or `clearValue` on a clear (only offered with\n * `clearable`).\n */\nexport function RhfCombobox<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n V extends string | number = string | number,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n clearValue = null,\n comboClassName,\n ...comboProps\n}: RhfComboboxProps<TFieldValues, TName, V, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n asControl={false}\n message={false}\n render={({ field, invalid, error, id, describedBy }) => (\n <EntityControl<V>\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n error={error}\n id={id}\n describedBy={describedBy}\n clearValue={clearValue}\n comboClassName={comboClassName}\n comboProps={comboProps}\n />\n )}\n />\n );\n}\n\nexport type RhfTextComboboxProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<\n ComboboxProps,\n | \"options\"\n | \"placeholder\"\n | \"groupBy\"\n | \"maxSuggestions\"\n | \"searchPlaceholder\"\n | \"createLabel\"\n | \"optionAdornment\"\n | \"autoFocus\"\n | \"aria-label\"\n > & {\n /** Classes for the combobox. */\n comboClassName?: string;\n };\n\n/**\n * The kit's free-text {@link Combobox}: type anything, or pick one of `options`.\n * Stores the string.\n */\nexport function RhfTextCombobox<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n comboClassName,\n ...comboProps\n}: RhfTextComboboxProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n asControl={false}\n message={false}\n render={({ field, invalid, error, id, describedBy }) => (\n <TextComboControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n error={error}\n id={id}\n describedBy={describedBy}\n className={comboClassName}\n comboProps={comboProps}\n />\n )}\n />\n );\n}\n\nfunction TextComboControl({\n field,\n invalid,\n required,\n error,\n id,\n describedBy,\n className,\n comboProps,\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n error: string | undefined;\n id: string;\n describedBy: string | undefined;\n className?: string;\n comboProps: Omit<RhfTextComboboxProps, keyof RhfFieldBaseProps | \"comboClassName\">;\n}) {\n // The combobox routes `id` and its ARIA to its <input>, so the label's `htmlFor`\n // names it, the hint describes it and the focus handle finds it; the error is its\n // own `error` prop, which it merges in after the hint.\n useFocusHandle(field.ref, () => document.getElementById(id));\n const value: unknown = field.value;\n return (\n <Combobox\n {...comboProps}\n id={id}\n aria-describedby={describedBy}\n aria-required={required || undefined}\n className={className}\n value={typeof value === \"string\" ? value : value == null ? \"\" : String(value)}\n onChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n error={error}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/rhf/fields.tsx"],"sourcesContent":["/**\n * Bound fields: one line per field of a react-hook-form form. `@eifi1/ui-kit/rhf`.\n *\n * ```tsx\n * <Form {...form}>\n * <RhfTextField name=\"name\" label=\"Name\" hint=\"As on the lease\" required\n * rules={{ required: \"Enter a name\" }} />\n * <RhfMoneyField name=\"rent\" label=\"Net rent\" currency=\"CHF\" />\n * <RhfDateField name=\"start\" label=\"Start\" />\n * <RhfSelect name=\"type\" label=\"Type\" options={TYPES} />\n * <RhfCombobox name=\"tenantId\" label=\"Tenant\" options={tenants} clearable />\n * <RhfCheckbox name=\"isDefault\" label=\"Default account\" />\n * </Form>\n * ```\n *\n * Each folds the `FormField → FormItem → FormLabel → FormControl → field →\n * FormDescription → FormMessage` scaffold (see ./form) into one element, the way\n * kastlan's shared/components/form/fields.tsx does for its forms — with what that file\n * left out:\n *\n * - **The red border.** Every field gets `invalid` from the field state, so an errored\n * field PAINTS as well as announces. kastlan's number, money, date and entity fields\n * passed only `aria-invalid` through `FormControl`, or nothing, and `Input`, `Select`\n * and `Textarea` paint only from the prop (see the note in ./form).\n * - **Focus on error.** `field.ref` reaches the focusable element — the element itself\n * where the kit field forwards a ref, a focus handle on it where it does not (the\n * number field, the pickers) — so `handleSubmit` and `trigger(…, { shouldFocus })`\n * land the caret on the first field in error.\n * - **`control` is optional**: under `<Form {...form}>` the fields find the form\n * through react-hook-form's context. Pass it for the field's value types\n * (`control={form.control}` checks `name` against the form's values).\n *\n * The common props: `name`, `control`, `label` (a {@link FormLabel} above the field;\n * omitted, none — then give the field an `aria-label`), `hint` (standing advice under\n * the field, attached to it with `aria-describedby`), `required` (the label's mark,\n * and `aria-required` where the control takes it — validation is `rules`' or the\n * resolver's), `rules`, `disabled` (the control only — the value is still submitted;\n * `excludeWhenDisabled` for react-hook-form's drop-the-value semantics), and\n * `className` for the item's box.\n *\n * {@link RhfField} is the shell they are all built on, for a control the kit does not\n * ship (an address autocomplete): it takes a `render` and wires the rest.\n */\nimport { useLayoutEffect, useRef, useState, type ReactElement, type ReactNode } from \"react\";\nimport type {\n ControllerFieldState,\n ControllerProps,\n ControllerRenderProps,\n FieldPath,\n FieldValues,\n} from \"react-hook-form\";\nimport { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField } from \"./form\";\nimport { Input, Select, Textarea, type InputProps, type SelectProps, type TextareaProps } from \"../components/ui\";\nimport { NumberField, type NumberFieldProps } from \"../components/number-field\";\nimport { AmountInput } from \"../components/amount-input\";\nimport { DatePicker, type DatePickerProps } from \"../components/date-picker\";\nimport { Checkbox, type CheckboxProps } from \"../components/checkbox\";\nimport { EntityCombobox, type EntityComboboxProps } from \"../components/entity-combobox\";\nimport { Combobox, type ComboboxProps } from \"../components/combobox\";\nimport type { ComboClearValue } from \"../components/combobox-core\";\nimport { cn } from \"../lib/cn\";\n\n// ── the shell ────────────────────────────────────────────────────────────────\n\n/** What every bound field takes. */\nexport interface RhfFieldBaseProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> {\n name: TName;\n /** `form.control`. Optional under `<Form {...form}>`; pass it to type-check `name`. */\n control?: ControllerProps<TFieldValues, TName, TTransformed>[\"control\"];\n /** react-hook-form's `rules` (`required`, `min`, `validate` …). */\n rules?: ControllerProps<TFieldValues, TName, TTransformed>[\"rules\"];\n /** A {@link FormLabel} above the field. Omitted: no label — name the control with\n * an `aria-label` instead. */\n label?: ReactNode;\n /** Standing advice under the field (\"As printed on the card\"), attached to the\n * control with `aria-describedby`. */\n hint?: ReactNode;\n /** Draws the label's required mark and sets `aria-required` where the control\n * takes it. Whether the field IS required is `rules`' or the resolver's to say. */\n required?: boolean;\n /** Disables the control and KEEPS the value: a field locked to a preset (a lease's\n * preselected unit, a fixed period) still submits it, and `rules` still run.\n * react-hook-form's own Controller `disabled` drops the value from `handleSubmit`'s\n * data instead, which a locked preset must never do (kastlan, 0.12). To get that,\n * add `excludeWhenDisabled`. A form-wide `useForm({ disabled })` still disables\n * every field the RHF way. */\n disabled?: boolean;\n /** With `disabled`: disable through react-hook-form, so the value is left out of\n * the submitted data and validation skips it, like a disabled native input in a\n * plain form post. Default `false`. */\n excludeWhenDisabled?: boolean;\n /** Classes for the item's box (a `grid gap-1`). */\n className?: string;\n}\n\n/** What {@link RhfField}'s `render` receives. */\nexport interface RhfFieldRenderContext<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> {\n field: ControllerRenderProps<TFieldValues, TName>;\n fieldState: ControllerFieldState;\n /** The field has an error: pass it to the control's `invalid` so it paints. */\n invalid: boolean;\n /** The error's message, if any. */\n error: string | undefined;\n /** The control's id, which the label points at. `FormControl` sets it on the\n * rendered element already; this is for a control that routes it elsewhere. */\n id: string;\n /** The label's id, for a control that is named with `aria-labelledby`. */\n labelId: string;\n /** The ids of the rendered hint and message, if any. */\n describedBy: string | undefined;\n}\n\nexport interface RhfFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> extends RhfFieldBaseProps<TFieldValues, TName, TTransformed> {\n /** The control. Wrapped in {@link FormControl} (its `id`, `aria-describedby` and\n * `aria-invalid`) unless `asControl` is false. */\n render: (ctx: RhfFieldRenderContext<TFieldValues, TName>) => ReactElement;\n /** `false`: render the control as is, for one that wires its own ids from the\n * context. Default `true`. */\n asControl?: boolean;\n /** `false`: no {@link FormMessage}, for a control that renders its own error (its\n * `error` prop). Default `true`. */\n message?: boolean;\n /** `false`: no {@link FormLabel}, for a control that carries its label itself (a\n * checkbox). Default `true`. */\n showLabel?: boolean;\n}\n\nfunction hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/** The label, with an id the control can be named by. Inside the item, so it can read\n * the item's id. */\nfunction RhfLabel({ required, children }: { required?: boolean; children: ReactNode }) {\n const { id } = useFormField();\n return (\n <FormLabel id={`${id}-label`} required={required}>\n {children}\n </FormLabel>\n );\n}\n\nfunction ControlSlot<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({\n field,\n fieldState,\n render,\n asControl,\n}: {\n field: ControllerRenderProps<TFieldValues, TName>;\n fieldState: ControllerFieldState;\n render: RhfFieldProps<TFieldValues, TName>[\"render\"];\n asControl: boolean;\n}) {\n const { id, formItemId, describedBy } = useFormField();\n const element = render({\n field,\n fieldState,\n invalid: fieldState.invalid,\n error: fieldState.error?.message,\n id: formItemId,\n labelId: `${id}-label`,\n describedBy,\n });\n return asControl ? <FormControl>{element}</FormControl> : element;\n}\n\n/**\n * The shell every bound field is built on: label, control, hint and message around\n * a `render` of your own.\n *\n * ```tsx\n * <RhfField name=\"address\" label=\"Address\" render={({ field, invalid }) => (\n * <AddressAutocomplete value={field.value ?? \"\"} onChange={field.onChange} invalid={invalid} />\n * )} />\n * ```\n */\nexport function RhfField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n render,\n asControl = true,\n message = true,\n showLabel = true,\n}: RhfFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <FormField\n control={control}\n name={name}\n rules={rules}\n disabled={excludeWhenDisabled ? disabled : undefined}\n render={({ field, fieldState }) => (\n <FormItem className={className}>\n {showLabel && hasContent(label) && <RhfLabel required={required}>{label}</RhfLabel>}\n <ControlSlot\n field={disabled && !field.disabled ? { ...field, disabled: true } : field}\n fieldState={fieldState}\n render={render}\n asControl={asControl}\n />\n {hasContent(hint) && <FormDescription>{hint}</FormDescription>}\n {message && <FormMessage />}\n </FormItem>\n )}\n />\n );\n}\n\n/**\n * Hands react-hook-form a focus handle for a control that forwards no ref: its\n * `shouldFocusError` calls `focus()` on whatever `field.ref` was given. `find` runs at\n * focus time, so it sees the element as it is then.\n */\nfunction useFocusHandle(ref: ControllerRenderProps[\"ref\"], find: () => HTMLElement | null | undefined) {\n const findRef = useRef(find);\n useLayoutEffect(() => {\n findRef.current = find;\n });\n useLayoutEffect(() => {\n ref({ focus: () => findRef.current()?.focus() });\n }, [ref]);\n}\n\n// ── text ─────────────────────────────────────────────────────────────────────\n\ntype OwnInputProps = Omit<\n InputProps,\n \"name\" | \"value\" | \"defaultValue\" | \"onChange\" | \"onBlur\" | \"ref\" | \"label\" | \"error\" | \"invalid\" | \"className\" | \"disabled\" | \"required\" | \"inputClassName\"\n> & {\n /**\n * Classes for the `<input>` — `className` is the item's box (kastlan 42: a\n * `font-mono` IBAN, a `tabular-nums` reference number). {@link RhfTextarea} and\n * {@link RhfNumberField} had it; this one only reached `Input`'s prop of the same\n * name by accident of the rest-spread, undocumented and one refactor from being\n * dropped. Declared, it is part of the contract the three share.\n */\n inputClassName?: string;\n};\n\nexport type RhfTextFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnInputProps;\n\n/** A text {@link Input} — `type` for email, tel, password, url. Stores the string. */\nexport function RhfTextField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n ...inputProps\n}: RhfTextFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid }) => (\n <Input\n aria-required={required || undefined}\n {...inputProps}\n name={field.name}\n ref={field.ref}\n value={(field.value ?? \"\") as string}\n onChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n />\n )}\n />\n );\n}\n\ntype OwnTextareaProps = Omit<\n TextareaProps,\n \"name\" | \"value\" | \"defaultValue\" | \"onChange\" | \"onBlur\" | \"ref\" | \"label\" | \"error\" | \"invalid\" | \"className\" | \"disabled\" | \"required\"\n> & {\n /** Classes for the `<textarea>`. */\n inputClassName?: string;\n};\n\nexport type RhfTextareaProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & OwnTextareaProps;\n\n/** A multi-line {@link Textarea}; takes `rows`. Stores the string. */\nexport function RhfTextarea<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n inputClassName,\n ...areaProps\n}: RhfTextareaProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid }) => (\n <Textarea\n aria-required={required || undefined}\n {...areaProps}\n className={inputClassName}\n name={field.name}\n ref={field.ref}\n value={(field.value ?? \"\") as string}\n onChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n />\n )}\n />\n );\n}\n\n// ── numbers ──────────────────────────────────────────────────────────────────\n\n/** A form value → the number field's `number | null`. Defaults hold `\"\"`, `null`, a\n * number, or a numeric string. */\nfunction toNumber(value: unknown): number | null {\n if (value === null || value === undefined || value === \"\") return null;\n const n = typeof value === \"number\" ? value : Number(String(value).replace(\",\", \".\"));\n return Number.isFinite(n) ? n : null;\n}\n\nexport type RhfNumberFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<\n NumberFieldProps,\n | \"digits\"\n | \"min\"\n | \"max\"\n | \"step\"\n | \"calculator\"\n | \"unit\"\n | \"unitPlacement\"\n | \"placeholder\"\n | \"inputClassName\"\n | \"locale\"\n | \"ariaLabel\"\n | \"autoFocus\"\n > & {\n /** What an emptied field stores. Default `null`; `\"\"` for a schema written\n * against a native number input (`z.coerce.number()` over `\"\"`). */\n emptyValue?: null | \"\";\n /** `false`: an emptied field snaps back to its last number instead of storing\n * `emptyValue`. Default `true`. */\n nullable?: boolean;\n };\n\n/**\n * The kit's {@link NumberField}: a parsed, rounded (`digits`) and clamped (`min` /\n * `max`) number, committed on blur or Enter in the locale's decimal mark. Stores a\n * number, or `emptyValue` when emptied.\n */\nexport function RhfNumberField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n emptyValue = null,\n nullable = true,\n ...numberProps\n}: RhfNumberFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid, id }) => (\n // FormControl clones this element with `id` / `aria-describedby` /\n // `aria-invalid`, which NumberControl hands on to the field.\n <NumberControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n id={id}\n emptyValue={emptyValue}\n nullable={nullable}\n numberProps={numberProps}\n />\n )}\n />\n );\n}\n\nfunction NumberControl({\n field,\n invalid,\n required,\n id,\n emptyValue,\n nullable,\n numberProps,\n ...aria\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n id: string;\n emptyValue: null | \"\";\n nullable: boolean;\n numberProps: Omit<RhfNumberFieldProps, keyof RhfFieldBaseProps | \"emptyValue\" | \"nullable\">;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: NumberFieldProps[\"aria-invalid\"];\n}) {\n // NumberField forwards no ref; it routes `id` to its <input>, where the handle\n // finds it.\n useFocusHandle(field.ref, () => document.getElementById(id));\n return (\n <NumberField\n {...numberProps}\n aria-describedby={aria[\"aria-describedby\"]}\n aria-invalid={aria[\"aria-invalid\"]}\n aria-required={required || undefined}\n id={id}\n value={toNumber(field.value)}\n onCommit={(n) => {\n field.onChange(n ?? emptyValue);\n field.onBlur();\n }}\n nullable={nullable}\n disabled={field.disabled}\n invalid={invalid}\n />\n );\n}\n\nexport type RhfIntegerFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfNumberFieldProps<TFieldValues, TName, TTransformed>;\n\n/**\n * {@link RhfNumberField} for a whole number — a floor, a room count, a year, a\n * notice period in months. Every such call site in kastlan (item 41) spelled out the\n * same three props, `digits={0} calculator={false} emptyValue=\"\"`, and a preset\n * written thirty times is one that is eventually written wrong once:\n *\n * - `digits={0}`: rounds on commit, so \"2.5\" rooms settles to 3 instead of reaching\n * an `Integer` column and failing server-side;\n * - `calculator={false}`: a count is typed, not worked out — the trigger is noise\n * beside a two-digit field;\n * - `emptyValue=\"\"`: kastlan's integer schemas are `z.coerce.number()` over `\"\"`,\n * written against the native number inputs these fields replaced.\n *\n * Each is only a default: pass the prop to override it (`emptyValue={null}` for a\n * nullable column). Everything else is {@link RhfNumberField}'s.\n */\nexport function RhfIntegerField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>(props: RhfIntegerFieldProps<TFieldValues, TName, TTransformed>) {\n // `??` / `=== undefined` rather than a spread over the defaults, so a caller\n // forwarding its own optional prop (`digits={props.digits}`) still gets the preset,\n // while an explicit `emptyValue={null}` keeps its null.\n return (\n <RhfNumberField\n {...props}\n digits={props.digits ?? 0}\n calculator={props.calculator ?? false}\n emptyValue={props.emptyValue === undefined ? \"\" : props.emptyValue}\n />\n );\n}\n\n// ── money ────────────────────────────────────────────────────────────────────\n\nexport type RhfMoneyFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> & {\n placeholder?: string;\n /** The currency chip's code (\"CHF\"). */\n currency?: string;\n /** Makes the chip a currency picker. Bind it to another field with\n * `useController`, or to state. */\n onCurrencyChange?: (code: string) => void;\n /** Colours the figure for the direction of money. */\n tone?: \"neutral\" | \"outflow\" | \"inflow\";\n align?: \"start\" | \"center\";\n ariaLabel?: string;\n autoFocus?: boolean;\n /**\n * What the form stores: `\"number\"` (default) — `12.5`, or `emptyValue` while the\n * field is empty — or `\"string\"`, the field's own text (\"12.50\") for a schema that\n * parses it itself. A half-typed sum (\"12+\") leaves the stored number as it was\n * until it resolves.\n */\n valueAs?: \"number\" | \"string\";\n /** What an emptied field stores when `valueAs` is `\"number\"`. Default `null`. */\n emptyValue?: null | \"\";\n /** Decimals the amount settles to on blur, Enter or a calculator result. Default:\n * the currency's minor unit (CHF 2, JPY 0). ⚠️ A unit price or a rate needs its\n * column's scale here (`digits={4}` for 1.789 €/l), or it is stored rounded to\n * cents without a word (keksdose G2). See {@link AmountInput}'s `digits`. */\n digits?: number;\n /** Clamp the settled amount (a release capped at the deposit's balance). Validate\n * with `rules` as well if a clamp needs explaining. */\n min?: number;\n max?: number;\n};\n\n/** The amount a text reads as, or `undefined` for a draft that is not one yet. */\nfunction parseAmount(text: string): number | null | undefined {\n const trimmed = text.trim();\n if (trimmed === \"\") return null;\n if (!/^-?\\d*[.,]?\\d*$/.test(trimmed)) return undefined;\n const n = Number(trimmed.replace(\",\", \".\"));\n return Number.isFinite(n) ? n : undefined;\n}\n\nfunction MoneyControl({\n field,\n invalid,\n required,\n id,\n describedBy,\n props,\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n id: string;\n describedBy: string | undefined;\n props: Omit<RhfMoneyFieldProps, keyof RhfFieldBaseProps>;\n}) {\n const { valueAs = \"number\", emptyValue = null, ariaLabel, ...amountProps } = props;\n const value: unknown = field.value;\n const external = value === null || value === undefined ? \"\" : String(value);\n // The text being typed. A stored NUMBER cannot hold \"12.\" or \"12+5\", so the draft\n // lives here and is re-read from the form only when the form's value moves away\n // from what the draft already says (a reset, a setValue).\n const [draft, setDraft] = useState(external);\n const [seen, setSeen] = useState<unknown>(value);\n if (seen !== value) {\n setSeen(value);\n const current = valueAs === \"string\" ? draft : parseAmount(draft);\n const same = valueAs === \"string\" ? current === external : current === toNumber(value);\n if (!same) setDraft(external);\n }\n const input = useRef<HTMLInputElement | null>(null);\n // AmountInput reports no blur, and react-hook-form needs one to mark the field\n // touched: listen on its <input>.\n const onBlur = field.onBlur;\n useLayoutEffect(() => {\n const el = input.current;\n if (!el) return;\n el.addEventListener(\"blur\", onBlur);\n return () => el.removeEventListener(\"blur\", onBlur);\n }, [onBlur]);\n return (\n <div>\n <AmountInput\n {...amountProps}\n ariaLabel={ariaLabel}\n id={id}\n aria-describedby={describedBy}\n aria-required={required || undefined}\n ref={(el) => {\n input.current = el;\n field.ref(el);\n }}\n value={draft}\n disabled={field.disabled}\n invalid={invalid}\n onChange={(text) => {\n setDraft(text);\n if (valueAs === \"string\") {\n field.onChange(text);\n return;\n }\n const parsed = parseAmount(text);\n if (parsed === undefined) return;\n field.onChange(parsed ?? emptyValue);\n }}\n />\n </div>\n );\n}\n\n/**\n * The kit's {@link AmountInput}: a money figure with its currency chip and the\n * calculator (\"1200+80\" resolves on blur or Enter). Stores a number by default — see\n * `valueAs`.\n */\nexport function RhfMoneyField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n ...amountProps\n}: RhfMoneyFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n asControl={false}\n render={({ field, invalid, id, describedBy }) => (\n <MoneyControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n id={id}\n describedBy={describedBy}\n props={amountProps}\n />\n )}\n />\n );\n}\n\n// ── date ─────────────────────────────────────────────────────────────────────\n\nexport type RhfDateFieldProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Omit<DatePickerProps, \"value\" | \"onChange\" | \"label\" | \"invalid\" | \"disabled\" | \"className\" | \"id\"> & {\n /** Classes for the picker. Default `w-full`. */\n inputClassName?: string;\n /** What a cleared date stores. Default `\"\"`, the empty ISO string; `null` for a\n * nullable schema. */\n emptyValue?: \"\" | null;\n };\n\nfunction DateControl({\n field,\n invalid,\n required,\n id,\n emptyValue,\n inputClassName,\n pickerProps,\n ...aria\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n id: string;\n emptyValue: \"\" | null;\n inputClassName?: string;\n pickerProps: Omit<DatePickerProps, \"value\" | \"onChange\" | \"label\" | \"invalid\" | \"disabled\" | \"className\" | \"id\">;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: DatePickerProps[\"aria-invalid\"];\n}) {\n useFocusHandle(field.ref, () => document.getElementById(id));\n const value: unknown = field.value;\n return (\n <DatePicker\n {...pickerProps}\n {...aria}\n aria-required={required || undefined}\n id={id}\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(iso) => {\n field.onChange(iso === \"\" ? emptyValue : iso);\n field.onBlur();\n }}\n disabled={field.disabled}\n invalid={invalid}\n className={inputClassName ?? \"w-full\"}\n />\n );\n}\n\n/** The kit's {@link DatePicker}, holding an ISO `\"YYYY-MM-DD\"` string. */\nexport function RhfDateField<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n inputClassName,\n emptyValue = \"\",\n ...pickerProps\n}: RhfDateFieldProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid, id }) => (\n // FormControl clones this element with `aria-describedby` / `aria-invalid`,\n // which DateControl hands on to the picker's trigger.\n <DateControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n id={id}\n emptyValue={emptyValue}\n inputClassName={inputClassName}\n pickerProps={pickerProps}\n />\n )}\n />\n );\n}\n\n// ── select ───────────────────────────────────────────────────────────────────\n\nexport interface RhfSelectOption {\n value: string | number;\n label: ReactNode;\n disabled?: boolean;\n}\n\nexport type RhfSelectProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<SelectProps, \"size\" | \"autoFocus\" | \"aria-label\"> & {\n /** The options, as data. Or pass `<option>` children. */\n options?: readonly RhfSelectOption[];\n children?: ReactNode;\n /** A first, empty option (\"Choose…\"), selected while the value is empty. */\n placeholder?: string;\n /** Store `Number(value)` rather than the option's string. An empty choice still\n * stores `\"\"`. */\n valueAsNumber?: boolean;\n /** Classes for the select. Default `w-full`. */\n selectClassName?: string;\n };\n\n/** A native {@link Select}, from `options` or `<option>` children. */\nexport function RhfSelect<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n options,\n children,\n placeholder,\n valueAsNumber,\n selectClassName,\n ...selectProps\n}: RhfSelectProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n render={({ field, invalid }) => (\n <Select\n aria-required={required || undefined}\n {...selectProps}\n className={cn(\"w-full\", selectClassName)}\n name={field.name}\n ref={field.ref}\n value={(field.value ?? \"\") as string | number}\n onChange={(e) => {\n const raw = e.target.value;\n field.onChange(valueAsNumber && raw !== \"\" ? Number(raw) : raw);\n }}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n >\n {placeholder !== undefined && <option value=\"\">{placeholder}</option>}\n {options\n ? options.map((o) => (\n <option key={String(o.value)} value={o.value} disabled={o.disabled}>\n {o.label}\n </option>\n ))\n : children}\n </Select>\n )}\n />\n );\n}\n\n// ── checkbox ─────────────────────────────────────────────────────────────────\n\nexport type RhfCheckboxProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<CheckboxProps, \"inputClassName\" | \"autoFocus\" | \"aria-label\">;\n\n/**\n * The kit's {@link Checkbox}, label beside the box and `hint` under it (the\n * checkbox's own `description`). Stores a boolean. `required` is the native one here\n * — see Checkbox — so a `<form>` refuses to submit it unticked.\n */\nexport function RhfCheckbox<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n ...boxProps\n}: RhfCheckboxProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, required, disabled, excludeWhenDisabled, className }}\n showLabel={false}\n render={({ field, invalid }) => (\n <Checkbox\n {...boxProps}\n ref={field.ref}\n name={field.name}\n checked={Boolean(field.value)}\n onCheckedChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n required={required}\n invalid={invalid}\n label={label}\n description={hint}\n />\n )}\n />\n );\n}\n\n// ── comboboxes ───────────────────────────────────────────────────────────────\n\nexport type RhfComboboxProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n V extends string | number = string | number,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<\n EntityComboboxProps<V>,\n | \"options\"\n | \"loadOptions\"\n | \"loading\"\n | \"placeholder\"\n | \"searchPlaceholder\"\n | \"emptyLabel\"\n | \"clearable\"\n | \"clearLabel\"\n | \"onCreate\"\n | \"createLabel\"\n | \"filter\"\n | \"minChars\"\n | \"debounceMs\"\n > & {\n /** What a clear stores — the picker's `clearValue`. Default `null`; `\"\"` for a\n * schema that spells \"no choice\" as an empty string. */\n clearValue?: ComboClearValue;\n /** Classes for the picker. */\n comboClassName?: string;\n };\n\nfunction EntityControl<V extends string | number>({\n field,\n invalid,\n required,\n error,\n id,\n describedBy,\n clearValue,\n comboClassName,\n comboProps,\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n error: string | undefined;\n id: string;\n describedBy: string | undefined;\n clearValue: ComboClearValue;\n comboClassName?: string;\n comboProps: Omit<RhfComboboxProps<FieldValues, string, V>, keyof RhfFieldBaseProps | \"clearValue\" | \"comboClassName\">;\n}) {\n const box = useRef<HTMLDivElement>(null);\n // EntityCombobox puts `id` and the ARIA below on its trigger, so the label's\n // `htmlFor` names it, the hint describes it and the focus handle finds it by id.\n useFocusHandle(field.ref, () => document.getElementById(id));\n // EntityCombobox reports no blur: focus leaving the picker marks the field touched.\n const onBlur = field.onBlur;\n useLayoutEffect(() => {\n const el = box.current;\n if (!el) return;\n el.addEventListener(\"focusout\", onBlur);\n return () => el.removeEventListener(\"focusout\", onBlur);\n }, [onBlur]);\n const value: unknown = field.value;\n return (\n <div ref={box}>\n <EntityCombobox<V, ComboClearValue>\n {...comboProps}\n className={comboClassName}\n id={id}\n aria-describedby={describedBy}\n aria-required={required || undefined}\n value={(value ?? null) as V | ComboClearValue | null}\n clearValue={clearValue}\n onChange={(v) => field.onChange(v)}\n disabled={field.disabled}\n invalid={invalid}\n // Its own message, which it merges into the trigger's aria-describedby after\n // the hint.\n error={error}\n />\n </div>\n );\n}\n\n/**\n * The kit's {@link EntityCombobox}: an id-keyed pick from `options` or `loadOptions`.\n * Stores the option's `value`, or `clearValue` on a clear (only offered with\n * `clearable`).\n */\nexport function RhfCombobox<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n V extends string | number = string | number,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n clearValue = null,\n comboClassName,\n ...comboProps\n}: RhfComboboxProps<TFieldValues, TName, V, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n asControl={false}\n message={false}\n render={({ field, invalid, error, id, describedBy }) => (\n <EntityControl<V>\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n error={error}\n id={id}\n describedBy={describedBy}\n clearValue={clearValue}\n comboClassName={comboClassName}\n comboProps={comboProps}\n />\n )}\n />\n );\n}\n\nexport type RhfTextComboboxProps<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n> = RhfFieldBaseProps<TFieldValues, TName, TTransformed> &\n Pick<\n ComboboxProps,\n | \"options\"\n | \"placeholder\"\n | \"groupBy\"\n | \"maxSuggestions\"\n | \"searchPlaceholder\"\n | \"createLabel\"\n | \"optionAdornment\"\n | \"autoFocus\"\n | \"aria-label\"\n > & {\n /** Classes for the combobox. */\n comboClassName?: string;\n };\n\n/**\n * The kit's free-text {@link Combobox}: type anything, or pick one of `options`.\n * Stores the string.\n */\nexport function RhfTextCombobox<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n TTransformed = TFieldValues,\n>({\n name,\n control,\n rules,\n label,\n hint,\n required,\n disabled,\n excludeWhenDisabled,\n className,\n comboClassName,\n ...comboProps\n}: RhfTextComboboxProps<TFieldValues, TName, TTransformed>) {\n return (\n <RhfField\n {...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}\n asControl={false}\n message={false}\n render={({ field, invalid, error, id, describedBy }) => (\n <TextComboControl\n field={field as unknown as ControllerRenderProps}\n invalid={invalid}\n required={required}\n error={error}\n id={id}\n describedBy={describedBy}\n className={comboClassName}\n comboProps={comboProps}\n />\n )}\n />\n );\n}\n\nfunction TextComboControl({\n field,\n invalid,\n required,\n error,\n id,\n describedBy,\n className,\n comboProps,\n}: {\n field: ControllerRenderProps;\n invalid: boolean;\n required?: boolean;\n error: string | undefined;\n id: string;\n describedBy: string | undefined;\n className?: string;\n comboProps: Omit<RhfTextComboboxProps, keyof RhfFieldBaseProps | \"comboClassName\">;\n}) {\n // The combobox routes `id` and its ARIA to its <input>, so the label's `htmlFor`\n // names it, the hint describes it and the focus handle finds it; the error is its\n // own `error` prop, which it merges in after the hint.\n useFocusHandle(field.ref, () => document.getElementById(id));\n const value: unknown = field.value;\n return (\n <Combobox\n {...comboProps}\n id={id}\n aria-describedby={describedBy}\n aria-required={required || undefined}\n className={className}\n value={typeof value === \"string\" ? value : value == null ? \"\" : String(value)}\n onChange={field.onChange}\n onBlur={field.onBlur}\n disabled={field.disabled}\n invalid={invalid}\n error={error}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eifi1/ui-kit",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
|
|
6
6
|
"_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
|