@eifi1/ui-kit 0.8.0 → 0.9.0

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.
Files changed (107) hide show
  1. package/README.md +4 -0
  2. package/dist/components/amount-input.d.ts +2 -0
  3. package/dist/components/calculator.d.ts +2 -0
  4. package/dist/components/chip.d.ts +27 -5
  5. package/dist/components/chip.js +6 -2
  6. package/dist/components/chip.js.map +1 -1
  7. package/dist/components/data-table-filter-popover.d.ts +1 -1
  8. package/dist/components/data-table-filters.d.ts +1 -1
  9. package/dist/components/data-table.d.ts +1 -1
  10. package/dist/components/data-table.js +122 -91
  11. package/dist/components/data-table.js.map +1 -1
  12. package/dist/components/date-picker.d.ts +69 -3
  13. package/dist/components/date-picker.js +152 -65
  14. package/dist/components/date-picker.js.map +1 -1
  15. package/dist/components/disclosure.d.ts +11 -2
  16. package/dist/components/disclosure.js +10 -4
  17. package/dist/components/disclosure.js.map +1 -1
  18. package/dist/components/floating-panel.d.ts +29 -2
  19. package/dist/components/floating-panel.js +16 -2
  20. package/dist/components/floating-panel.js.map +1 -1
  21. package/dist/components/full-bleed-dialog.js +3 -1
  22. package/dist/components/full-bleed-dialog.js.map +1 -1
  23. package/dist/components/number-field.d.ts +2 -0
  24. package/dist/components/number-input.d.ts +2 -0
  25. package/dist/components/numpad-sheet.d.ts +2 -0
  26. package/dist/components/progress-bar.d.ts +16 -2
  27. package/dist/components/progress-bar.js +4 -2
  28. package/dist/components/progress-bar.js.map +1 -1
  29. package/dist/components/treemap.js +4 -1
  30. package/dist/components/treemap.js.map +1 -1
  31. package/dist/components/ui.d.ts +44 -14
  32. package/dist/components/ui.js +28 -22
  33. package/dist/components/ui.js.map +1 -1
  34. package/dist/components/use-table-state.d.ts +1 -1
  35. package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
  36. package/dist/data-table.d.ts +1 -1
  37. package/dist/hooks/use-overlay-history.js +84 -4
  38. package/dist/hooks/use-overlay-history.js.map +1 -1
  39. package/dist/i18n/defaults.d.ts +2 -0
  40. package/dist/i18n/defaults.js +2 -0
  41. package/dist/i18n/defaults.js.map +1 -1
  42. package/dist/i18n/kit-labels.d.ts +3 -0
  43. package/dist/i18n/kit-labels.js.map +1 -1
  44. package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
  45. package/dist/i18n/locales/de-CH.d.ts +2 -0
  46. package/dist/i18n/locales/de-informal.d.ts +2 -0
  47. package/dist/i18n/locales/de.d.ts +2 -0
  48. package/dist/i18n/locales/de.js +10 -0
  49. package/dist/i18n/locales/de.js.map +1 -1
  50. package/dist/i18n/locales/es.d.ts +2 -0
  51. package/dist/i18n/locales/es.js +10 -0
  52. package/dist/i18n/locales/es.js.map +1 -1
  53. package/dist/i18n/locales/fr.d.ts +2 -0
  54. package/dist/i18n/locales/fr.js +10 -0
  55. package/dist/i18n/locales/fr.js.map +1 -1
  56. package/dist/i18n/locales/hu.d.ts +2 -0
  57. package/dist/i18n/locales/hu.js +10 -0
  58. package/dist/i18n/locales/hu.js.map +1 -1
  59. package/dist/i18n/locales/it.d.ts +2 -0
  60. package/dist/i18n/locales/it.js +10 -0
  61. package/dist/i18n/locales/it.js.map +1 -1
  62. package/dist/i18n/locales/zh.d.ts +2 -0
  63. package/dist/i18n/locales/zh.js +10 -0
  64. package/dist/i18n/locales/zh.js.map +1 -1
  65. package/dist/index.d.ts +5 -3
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/search/command-palette.d.ts +49 -1
  69. package/dist/search/command-palette.js +132 -29
  70. package/dist/search/command-palette.js.map +1 -1
  71. package/dist/search/global-search.d.ts +141 -0
  72. package/dist/search/global-search.js +277 -0
  73. package/dist/search/global-search.js.map +1 -0
  74. package/dist/search/search-index.d.ts +105 -0
  75. package/dist/search/search-index.js +142 -0
  76. package/dist/search/search-index.js.map +1 -0
  77. package/dist/search.d.ts +2 -0
  78. package/dist/search.js +2 -0
  79. package/dist/search.js.map +1 -1
  80. package/dist/wizard/stepper-nav.d.ts +10 -1
  81. package/dist/wizard/stepper-nav.js +2 -1
  82. package/dist/wizard/stepper-nav.js.map +1 -1
  83. package/package.json +12 -3
  84. package/src/components/chip.tsx +36 -6
  85. package/src/components/data-table.tsx +81 -5
  86. package/src/components/date-picker.tsx +328 -117
  87. package/src/components/disclosure.tsx +24 -8
  88. package/src/components/floating-panel.tsx +46 -2
  89. package/src/components/full-bleed-dialog.tsx +7 -2
  90. package/src/components/progress-bar.tsx +27 -3
  91. package/src/components/treemap.tsx +3 -0
  92. package/src/components/ui.tsx +95 -37
  93. package/src/hooks/use-overlay-history.ts +181 -4
  94. package/src/i18n/defaults.ts +2 -0
  95. package/src/i18n/kit-labels.tsx +2 -0
  96. package/src/i18n/locales/de.ts +10 -0
  97. package/src/i18n/locales/es.ts +10 -0
  98. package/src/i18n/locales/fr.ts +10 -0
  99. package/src/i18n/locales/hu.ts +10 -0
  100. package/src/i18n/locales/it.ts +10 -0
  101. package/src/i18n/locales/zh.ts +10 -0
  102. package/src/index.ts +4 -0
  103. package/src/search/command-palette.tsx +200 -27
  104. package/src/search/global-search.tsx +484 -0
  105. package/src/search/search-index.ts +295 -0
  106. package/src/search.ts +2 -0
  107. package/src/wizard/stepper-nav.tsx +11 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ui.tsx"],"sourcesContent":["import { forwardRef, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from \"lucide-react\";\nimport type { ButtonHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactNode, Ref, SelectHTMLAttributes, TextareaHTMLAttributes } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { Tooltip, type TooltipSide } from \"./tooltip\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\" | \"brand\" | \"link\";\n\n// Shared base ring for every button-styled element. Kept as a named const so the\n// <Button> component and the {@link buttonClasses} helper draw from one source and\n// can never drift apart.\nconst BUTTON_BASE =\n \"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed\";\n\nexport type ButtonSize = \"sm\" | \"md\";\n\n// Box geometry per size, split out of the base so the two cannot be merged into one\n// string that a size then has to fight. `md` is the pre-0.8.0 look, unchanged. `sm` is\n// the compact secondary action keksdose repeats by hand as `px-2 py-1 text-xs` (and\n// `px-2 py-0.5 text-xs`) on the buttons in a toolbar or a table header; one rung is\n// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for\n// a bigger text button (IconButton's `lg` is a touch target, not a text size). The\n// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n md: \"gap-2 px-3 py-2 text-sm\",\n sm: \"gap-1.5 px-2 py-1 text-xs\",\n};\n\n// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.\n// Actions default to a warm bordered look (primary = filled warm chip, secondary =\n// outline); `brand` stays the solid accent for the rare strong CTA; `danger` takes the\n// semantic `--danger` family (destructive semantics), so a consumer can re-point the\n// destructive hue instead of inheriting a hard-coded red. Focus rings follow each\n// variant's own accent: brand for the four neutral ones, danger for `danger`.\nconst buttonVariantClasses: Record<ButtonVariant, string> = {\n primary:\n \"border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus:ring-[var(--brand)]\",\n secondary:\n \"border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]\",\n ghost:\n \"bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]\",\n danger:\n \"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border)]\",\n brand:\n \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]\",\n // A text link that is still a `<button>` — keksdose's six hand-rolled\n // `<button className=\"text-brand underline\">` sites (\"Resend code\", \"Show all\",\n // \"Undo\" in a toast…), which act rather than navigate and so must not be anchors.\n // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but\n // the base's `focus:ring-2` stays: those copies had `outline-none` and no ring, so\n // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that\n // ring hugging the word instead of drawing a pill round it.\n link:\n \"rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus:ring-[var(--brand)]\",\n};\n\n/** The second argument of {@link buttonClasses} in its options form. */\nexport interface ButtonClassesOptions {\n /** See {@link ButtonProps.size}. */\n size?: ButtonSize;\n className?: string;\n}\n\n/**\n * Button classes for the rare case where the styling must land on a non-`<button>`\n * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix\n * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real\n * button works, prefer `<Button>`. Draws from the same base, size and variant maps as\n * `<Button>`, so the two stay in lockstep.\n *\n * The second argument is either the extra classes (the pre-0.8.0 form) or\n * `{ size, className }` — `buttonClasses(\"secondary\", { size: \"sm\" })` for keksdose's\n * compact toolbar links.\n */\nexport function buttonClasses(\n variant: ButtonVariant = \"primary\",\n classNameOrOptions?: string | ButtonClassesOptions,\n): string {\n const { size = \"md\", className } =\n typeof classNameOrOptions === \"object\" ? classNameOrOptions : { className: classNameOrOptions };\n return cn(BUTTON_BASE, BUTTON_SIZES[size], buttonVariantClasses[variant], className);\n}\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n /** `md` (default) is the page's action button. `sm` is the compact one — 12px text\n * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a\n * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand\n * there). Every variant takes either size. */\n size?: ButtonSize;\n /** In a flex row next to a taller labelled field, fill the field's height so the\n * two line up. No effect outside a flex row. */\n stretch?: boolean;\n /** The `<button>` element. React 19 passes `ref` to a function component as an\n * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —\n * declared here only because `ButtonHTMLAttributes` does not carry it. */\n ref?: Ref<HTMLButtonElement>;\n}\n\nexport function Button({ variant = \"primary\", size = \"md\", stretch, className, ...rest }: ButtonProps) {\n return (\n <button\n {...rest}\n className={cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n // In a flex row next to a taller labelled field, `stretch` makes the button\n // fill the field's height so the two line up (self-stretch overrides the row's\n // align-items). No effect outside a flex row / when it's already the tallest.\n stretch && \"self-stretch\",\n buttonVariantClasses[variant],\n className,\n )}\n />\n );\n}\n\n// Canonical square icon-only button. <Button> carries TEXT geometry — px-3 py-2\n// gap-2 — so rendering a bare glyph through it gives a small icon in a wide,\n// text-shaped box. This fixes a square box instead and, crucially, forces the\n// child icon to a size set by the BOX (20px at md/sm) via `[&_svg]:size-*` so a\n// caller cannot under-size it. Use it wherever an action is a bare icon\n// (edit/delete/tools) so they all match the top-bar icon buttons and can never\n// drift apart again.\n//\n// Draws its colours from the same `buttonVariantClasses` map as <Button>, so the\n// two re-skin together with the palette.\nconst ICON_BUTTON_BASE =\n \"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed\";\n\nexport type IconButtonSize = \"lg\" | \"md\" | \"sm\" | \"xs\" | \"2xs\";\n\n// Box and glyph together, so a 24px chip action cannot end up holding a 20px icon\n// that touches its edges. The two small steps are lenkbank's list-row (28px) and\n// chip (24px) actions, which it had hand-rolled beside the kit's 32/36px ones.\nconst ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {\n // The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's\n // bulk-action bars write `size-11` by hand over an `md` button to get it on phones.\n lg: \"size-11 [&_svg]:size-5\",\n md: \"size-9 [&_svg]:size-5\",\n sm: \"size-8 [&_svg]:size-5\",\n xs: \"size-7 rounded [&_svg]:size-4\",\n \"2xs\": \"size-6 rounded [&_svg]:size-3.5\",\n};\n\n// A tone re-colours the glyph without changing what the variant draws around it.\n// `muted` and `danger` both sit quiet at rest — an action in every row of a list\n// must not shout from every row — and `danger` answers the pointer in the\n// destructive family, so the red arrives only on the one row you are about to\n// act on.\nconst ICON_BUTTON_TONES = {\n default: \"\",\n muted:\n \"text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]\",\n danger:\n \"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]\",\n // Amber AT REST, unlike `danger`: a warning icon button is the one on the row that\n // needs attention (keksdose's \"needs review\" flag on a transaction, the stale-rate\n // marker), not an action repeated down a list — quiet grey would hide the very\n // thing it is there to point out.\n warning:\n \"text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]\",\n // Sky at rest, for the same reason as `warning`: keksdose's reconcile action on an\n // account row (accounts-page:867) is the one on the row to notice, and it is\n // informational rather than a problem, so it takes the `--info` family.\n info: \"text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]\",\n} as const;\n\n// A disabled button must not answer the pointer. The hover classes above are plain\n// `hover:` (so a caller's `className=\"hover:…\"` still replaces them through\n// tailwind-merge), which means they fire on a disabled button too — the old grey\n// icon lit up on hover while refusing the click. Rather than rewrite every hover as\n// `enabled:hover:` (which never matches the `<a>` that `buttonClasses` also styles,\n// and would out-rank a caller's plain `hover:` override), each variant pins its\n// RESTING look under `disabled:hover:`, which out-ranks any `hover:` by specificity\n// and only ever matches a disabled button.\nconst ICON_BUTTON_DISABLED_REST: Record<ButtonVariant | \"overlay\", string> = {\n primary: \"disabled:hover:bg-[var(--bg-surface-2)]\",\n secondary: \"disabled:hover:bg-transparent\",\n ghost: \"disabled:hover:bg-transparent\",\n danger: \"disabled:hover:bg-[var(--danger)]\",\n brand: \"disabled:hover:bg-[var(--brand)]\",\n link: \"disabled:hover:bg-transparent disabled:hover:no-underline\",\n overlay: \"disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]\",\n};\n\n// The tones that change the glyph on hover pin their resting glyph the same way.\nconst ICON_BUTTON_TONES_DISABLED_REST: Partial<Record<keyof typeof ICON_BUTTON_TONES, string>> = {\n muted: \"disabled:hover:text-[var(--text-placeholder)]\",\n danger: \"disabled:hover:text-[var(--text-placeholder)]\",\n};\n\n// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the\n// \"selected\" look of a Chip or a SegmentedControl option, so an on toggle reads as on\n// beside them. After the tone, so a pressed `muted` button is brand, not grey.\nconst ICON_BUTTON_PRESSED =\n \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-[var(--brand)] disabled:hover:bg-[var(--brand-bg)] disabled:hover:text-[var(--brand)]\";\n\n// `variant=\"overlay\"`: a round, translucent disc for a control that sits ON a photo\n// (keksdose's receipt-scan preview: close, rotate, retake over the camera image).\n// The inverse pair, not the surface one: what has to hold is the contrast between the\n// disc and its glyph, whatever the picture underneath is, and `--bg-inverse` /\n// `--text-inverse` are the one token pair defined as each other's opposite in both\n// themes. The disc is the inverse at 60% (`color-mix`, so it stays a token a palette\n// can re-point); the blur keeps a busy background from breaking the glyph's edge.\nconst ICON_BUTTON_OVERLAY =\n \"rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus:ring-[var(--text-inverse)]\";\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Any {@link ButtonVariant}, or `overlay` — a round translucent disc for use over\n * an image (see `ICON_BUTTON_OVERLAY`). */\n variant?: ButtonVariant | \"overlay\";\n /** Box size: lg = 44px (a phone's touch target), md = 36px (matches the top bar),\n * sm = 32px — all three with a 20px icon;\n * xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px\n * icon (an action on a chip or a tab). */\n size?: IconButtonSize;\n /** Glyph colour over the variant. `muted`: placeholder grey, full text colour on\n * hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a\n * remove/delete that repeats down a list. `warning`: amber at rest — a flag that\n * wants attention. `info`: sky at rest — a notice-worthy but harmless action\n * (keksdose's reconcile). Default: the variant's own colours. */\n tone?: keyof typeof ICON_BUTTON_TONES;\n /**\n * Make it a toggle button. `true` sets `aria-pressed=\"true\"` and draws the \"on\" look\n * (brand glyph on the quiet brand fill); `false` sets `aria-pressed=\"false\"` with the\n * normal look, so a screen reader still hears a toggle that is off. Left out, it is\n * an ordinary button with no `aria-pressed` — or whatever `aria-pressed` the caller\n * passes. For keksdose's budget share toggle (budgets-page:291), which paints its own\n * brand colour over a ghost button today. Keep the `aria-label` the same in both\n * states (\"Share budget\"): the pressed state already says whether it is on.\n */\n pressed?: boolean;\n /** Keep the click (and the Enter/Space that produces it) from reaching an\n * ancestor's handler — for an action inside a clickable table row or card.\n *\n * **Not a licence to put this inside another `<button>`.** HTML forbids any\n * interactive content, and any element with a `tabindex`, inside a button — so\n * the `<span role=\"button\" tabIndex={0}>` workaround is invalid too — and ARIA\n * makes a button's children presentational, so a screen reader flattens the\n * inner one into the outer one's name and it cannot be reached at all. Make the\n * row's main action a button that fills the row and put this one BESIDE it, on\n * top:\n *\n * ```tsx\n * <div className=\"relative\">\n * <button className=\"w-full pe-16 …\" onClick={open}>…row…</button>\n * <div className=\"absolute inset-y-0 end-2 flex items-center gap-0.5\">\n * <IconButton size=\"xs\" tone=\"danger\" aria-label=\"Delete\" onClick={remove}>\n * <Trash2 />\n * </IconButton>\n * </div>\n * </div>\n * ```\n *\n * The pair look exactly like the nested version, click exactly like it, and\n * are two tab stops a screen reader can tell apart. */\n stopPropagation?: boolean;\n}\n\nexport const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(\n { variant = \"ghost\", size = \"md\", tone = \"default\", pressed, stopPropagation, className, onClick, onKeyDown, ...rest },\n ref,\n) {\n return (\n <button\n ref={ref}\n {...rest}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={(e) => {\n if (stopPropagation) e.stopPropagation();\n onClick?.(e);\n }}\n onKeyDown={(e) => {\n // A button turns Enter/Space into a click of its own, but the KEY still\n // bubbles — and a clickable row listening for Enter would act on it too.\n if (stopPropagation && (e.key === \"Enter\" || e.key === \" \")) e.stopPropagation();\n onKeyDown?.(e);\n }}\n className={cn(\n ICON_BUTTON_BASE,\n ICON_BUTTON_SIZES[size],\n // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.\n variant === \"overlay\" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],\n ICON_BUTTON_DISABLED_REST[variant],\n ICON_BUTTON_TONES[tone],\n ICON_BUTTON_TONES_DISABLED_REST[tone],\n pressed && ICON_BUTTON_PRESSED,\n className,\n )}\n />\n );\n});\nIconButton.displayName = \"IconButton\";\n\nexport const FIELD_BASE =\n \"block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-3 py-2 text-sm text-[var(--text-primary)] shadow-sm placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:ring-[var(--brand)] \" +\n // A field the user cannot change has to LOOK settled. Without this, `disabled`\n // dimmed the floating label and nothing else — FIELD_BASE's own\n // `text-[var(--text-primary)]` overrides the browser's grey — so a read-only value\n // sat there at full body-text strength, indistinguishable from one you could retype\n // (Keksdose dev#455/#474).\n \"disabled:cursor-default disabled:bg-[var(--bg-surface-2)] disabled:text-[var(--text-muted)] \" +\n // Same for a field that takes focus but refuses keys: `readOnly` is not\n // `disabled`, and a value that cannot be edited should not claim it can be. Opt\n // OUT with {@link FIELD_WRITABLE_LOOK} in the one case where `readOnly` does not\n // mean that — see the two-factor code field, which uses it to block an autofill\n // and drops it the moment you focus the field.\n //\n // ⚠️ The ATTRIBUTE, never the `:read-only` pseudo-class (Keksdose dev#477).\n // `:read-only` does not mean \"was marked readonly\" — per Selectors 4 it matches\n // everything that is not `:read-write`, and only editable inputs/textareas and\n // contenteditable elements are `:read-write`. So `read-only:` matched every\n // `<select>` in the app and every field-styled `<button>` built on this base —\n // the native selects, the date trigger, the entity/currency/multi pickers — and\n // painted them all in the \"you may not edit this\" grey. On one transaction form\n // that made three different-looking families out of one field style: plain\n // inputs, grey selects, grey trigger buttons.\n \"[&[readonly]]:bg-[var(--bg-surface-2)] [&[readonly]]:text-[var(--text-muted)]\";\n\n/** Cancels FIELD_BASE's read-only treatment for a field that is `readOnly` for a\n * reason other than \"you may not edit this\". Attribute-scoped for the same reason\n * FIELD_BASE is — the two have to cancel on the identical selector or twMerge\n * cannot make the later one win. */\nexport const FIELD_WRITABLE_LOOK =\n \"[&[readonly]]:bg-[var(--bg-surface)] [&[readonly]]:text-[var(--text-primary)]\";\n\n// Extra top padding leaves room for a label that floats INSIDE the field (the\n// \"filled\" pattern) — the label sits in the top strip, the value below it. Used by\n// every labelled field (native + custom-dropdown triggers). twMerge lets pt/pb win\n// over FIELD_BASE's py-2.\nexport const FIELD_FLOATING_PAD = \"pt-4 pb-1\";\n\n// Error/required highlight for a field that is missing a value — a `--danger`\n// border and matching focus ring so the control itself shows what's wrong, not just\n// a note beside it (feedback #235). Layered after FIELD_BASE so twMerge wins.\n//\n// The `ring-1` is not decoration; it is what makes the highlight SURVIVE display\n// scaling (Keksdose live #295 — *\"Account select boundary. It is not highlighted on\n// the sides.\"*). At 125%, the browser's usual setting on a 2560px screen, a 1px CSS\n// border is 1.25 device pixels: the horizontal edges land on whole rows and paint\n// solid, while one of the two VERTICAL edges lands across a pixel boundary and is\n// spread over two columns at partial coverage. Measured on the receipt's required\n// account field, dark theme, back when this was a hard-coded rose (full =\n// rgb(208,30,78)): left/top/bottom all 208, right 160 then 115 — the side that is\n// supposed to shout, at 55–77% of the others. A border plus a ring is 2 CSS px, so\n// whatever the fraction there is always one fully covered device pixel on every side\n// (re-measured: worst side 241). It is the GEOMETRY that carries that, not the hue,\n// so the fix survives a consumer re-pointing `--danger-border`.\n//\n// A ring rather than `border-2`: a box-shadow adds no layout, so an invalid field\n// stays exactly the size of a valid one and nothing beside it moves when the value\n// arrives.\nexport const FIELD_INVALID =\n \"border-[var(--danger-border)] ring-1 ring-[var(--danger-border)] focus:border-[var(--danger)] focus:ring-[var(--danger)]\";\n\nexport const FLOATING_INPUT_CLASS = cn(FIELD_BASE, FIELD_FLOATING_PAD, \"peer placeholder:text-transparent\");\n\n/** The phone breakpoint the display treatment below keys off — the same one the\n * numpad sheet uses, kept in one place so the two can't drift apart. */\nexport const PHONE_QUERY = \"(max-width: 767px)\";\n\n/**\n * The PHONE display treatment (Keksdose feedback #176, carried across the entry\n * forms by #179): the field chrome removed so the ONE input a form is actually\n * about reads as the thing itself, not as another boxed row in a stack.\n *\n * Every control that takes `variant=\"display\"` — {@link Input}, `NumberInput`,\n * `AmountInput` — means exactly the same thing by it: the treatment applies below\n * {@link PHONE_QUERY} and the field is untouched above it, so a caller never has\n * to ask the viewport, and a form can't end up half-treated across breakpoints.\n *\n * What stays, deliberately:\n * - A hairline baseline. With the box gone something still has to say \"you can\n * type here\"; it takes the brand colour on focus, where a bordered field would\n * light its whole outline. Each edge is set exactly once (`border-x-0\n * border-t-0 border-b`) so the rule can't hinge on utility order.\n * - The muted placeholder colour, because a display field is usually empty at\n * the moment it matters most and has nothing else to show.\n * - The label, moved to `sr-only` rather than dropped: display type is legible\n * to the eye, not to a screen reader.\n *\n * Size and weight are NOT here — an amount wants display type, a subject line\n * wants a heading — so each control adds its own on top.\n */\nexport const FIELD_DISPLAY =\n \"block w-full border-x-0 border-t-0 border-b border-[var(--border)] bg-transparent px-0 pt-0 pb-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:outline-none focus:ring-0 disabled:opacity-60\";\n\n// A field-styled button trigger for the custom dropdown controls (MultiSelect,\n// CurrencySelect) — the field look (border/bg) as a flex row for the value + chevron,\n// so they don't hand-copy the field classes. Add FIELD_FLOATING_PAD only when the\n// trigger carries a label (unlabelled triggers stay normal height to match buttons /\n// adjacent controls). Pair the labelled case with a static FieldLabel.\n// `relative` so the trigger can host an absolutely-centred FieldChevron / clear\n// button the way the native Select does.\nexport const FIELD_TRIGGER = cn(\n FIELD_BASE,\n \"relative flex items-center justify-between gap-2 text-start hover:bg-[var(--bg-hover)]\",\n);\n\n/** An alias rather than an interface: the chevron adds nothing of its own to an\n * `<svg>`'s props, and an interface declaring no members is the same type wearing a\n * name that suggests otherwise. */\nexport type FieldChevronProps = Omit<ComponentPropsWithoutRef<\"svg\">, \"children\">;\n\n/** The dropdown chevron, shared by the native {@link Select} and every custom\n * {@link FIELD_TRIGGER} control.\n *\n * Absolutely positioned and centred on the FIELD box. As an ordinary flex child\n * it centres on the *content* box instead, and `FIELD_FLOATING_PAD` (pt-4 pb-1)\n * pushes that box's midline down — so a labelled currency/multi-select chevron\n * sat visibly lower than the native select's right beside it (feedback #400).\n * Pair it with `pe-9` on the trigger so the value can't run underneath. At the\n * inline END, not the right: a right-to-left form reads the value from the right,\n * and a chevron parked on top of its first word hid exactly the part that says\n * what was picked. */\nexport function FieldChevron({ className, ...rest }: FieldChevronProps) {\n return (\n <ChevronDown\n {...rest}\n // After the spread: the chevron is decoration beside a control that already has\n // a name, and an `aria-hidden` a caller could switch off by accident is a second\n // announcement of the same field.\n aria-hidden\n className={cn(\n \"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n className,\n )}\n />\n );\n}\n\n// Animated label that starts centred (as a placeholder) in an empty field and\n// floats up INSIDE the top strip on focus or once the field has a value. Sits on\n// the field's own surface, so no background chip and nothing to mismatch the card.\nexport const FLOATING_LABEL_CLASS = cn(\n \"pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all\",\n \"max-w-[calc(100%-1.5rem)] truncate\",\n \"peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]\",\n \"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]\",\n \"peer-disabled:opacity-50\",\n);\n\n// The animated label again, as a ROW that the label and its \"?\" share — same\n// placement, same float, same type, but with the type on the row so the label\n// INHERITS it. It has to be that way round: `peer-focus:` compiles to a sibling\n// selector, so the classes have to sit on the element that is actually a sibling\n// of the input, and a label nested inside a wrapper is not one.\n//\n// Wider end clearance than the label alone takes (`3rem` rather than `1.5rem`),\n// because the controls that carry an animated label are the ones with something\n// at the end edge of the field — NumberInput's calculator is the case this was\n// written for. The label truncates a little sooner; the alternative was the \"?\"\n// sitting on top of a button (steering-design feedback #48).\nconst FLOATING_ROW_CLASS = cn(\n \"pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all\",\n \"max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]\",\n \"peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]\",\n \"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]\",\n \"peer-disabled:opacity-50\",\n);\n\n// The TYPE of the small static label, without any placement. Split out so the\n// label and anything sharing its line (see `hint` on {@link FloatingField}) are\n// laid out by one flex row instead of by two absolute offsets guessing at the\n// same baseline — which is what put dev#468's \"?\" three pixels above the word it\n// belongs to.\nconst STATIC_LABEL_TYPE =\n \"text-[11px] leading-tight text-[var(--text-muted)] peer-disabled:opacity-50\";\n\n// A field that always has a value (select / dropdown trigger) keeps the label\n// permanently in the floated position — small, in the top strip, value below.\nexport const FLOATING_LABEL_STATIC = cn(\n \"pointer-events-none absolute start-3 top-1\",\n STATIC_LABEL_TYPE,\n \"max-w-[calc(100%-1.5rem)] truncate\",\n);\n\n/**\n * The one place that assembles a labelled field: a `relative` wrapper around the\n * control (`children`) plus a floating label inside the top strip. Pass `staticLabel`\n * for controls that always have a value (selects); omit it for free-text fields whose\n * label animates from centred→up. Used by Input/Select/Textarea/NumberInput; the\n * dropdown controls that need a ref + menu keep their own wrapper but the same label\n * (via {@link FieldLabel}) and trigger ({@link FIELD_TRIGGER}) styles.\n */\nexport interface FloatingFieldProps extends ComponentPropsWithoutRef<\"div\"> {\n htmlFor?: string;\n label?: ReactNode;\n staticLabel?: boolean;\n /** Keep the label in the accessibility tree but out of the layout — what\n * {@link FIELD_DISPLAY} needs, since a floating label inside a field with no\n * field left would have nothing to float in. */\n srOnlyLabel?: boolean;\n /** Something interactive that belongs to the LABEL rather than to the value —\n * in practice a {@link FieldHint} \"?\" (dev#468). It is rendered in a flex row\n * with the label, so it is centred on the label's line by the layout instead\n * of by a hand-tuned `top-…`, and it can never drift when the type changes.\n *\n * It rides an ANIMATED label too, and the two cases place it differently on\n * purpose. A static label has a field-wide strip to itself, so the hint sits\n * at the far end of it and a long label truncates into the gap. An animated\n * one belongs to a control with something at the end edge of the field — a\n * calculator, a stepper — so the hint follows the label instead, and it is\n * the label that gives way (steering-design feedback #48). */\n hint?: ReactNode;\n children: ReactNode;\n}\n\nexport function FloatingField({\n className,\n htmlFor,\n label,\n staticLabel,\n srOnlyLabel,\n hint,\n children,\n ...rest\n}: FloatingFieldProps) {\n const withHint = hint !== undefined && !srOnlyLabel;\n const atEnd = withHint && staticLabel;\n const labelEl = label !== undefined && (\n <label\n htmlFor={htmlFor}\n className={\n srOnlyLabel\n ? \"sr-only\"\n : atEnd\n ? cn(\"pointer-events-none min-w-0 truncate\", STATIC_LABEL_TYPE)\n : withHint\n ? \"pointer-events-none min-w-0 truncate\"\n : staticLabel\n ? FLOATING_LABEL_STATIC\n : FLOATING_LABEL_CLASS\n }\n >\n {label}\n </label>\n );\n return (\n // `relative` is the whole contract of this wrapper — the floating label and every\n // control that hangs off it (a reveal toggle, a chevron) are positioned against\n // this box — so it is merged through `cn` after the spread rather than left where\n // a caller's stray `className` or `style` could unset it.\n <div {...rest} className={cn(\"relative\", className)}>\n {children}\n {withHint ? (\n // Static: `inset-x-3` rather than `start-3`, so a long label truncates at\n // the field's own end padding instead of running under the chevron.\n // Animated: the row floats with the label and is only as wide as it needs\n // to be. Either way the hint keeps its width (`shrink-0`) and the label\n // is the one that gives way.\n <div\n className={\n atEnd\n ? \"pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1\"\n : FLOATING_ROW_CLASS\n }\n >\n {labelEl}\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>\n </div>\n ) : (\n labelEl\n )}\n </div>\n );\n}\n\n/**\n * The \"?\" that explains a field, on the field's own label line (dev#468).\n *\n * Pass it to a labelled {@link Select} / {@link Input} as `hint`. It exists as a\n * component rather than as a snippet each form repeats because the previous\n * version was exactly that snippet — an absolutely-positioned button whose\n * `top-1.5` was one guess at where an 11px label sits — and the reporter's\n * follow-up was *\"question mark is not centered. Is it part of the hoc?\n * positioning problems seem quite frequently.\"* It was not part of the HOC. Now\n * it is, and there is one place left where the answer can be wrong.\n *\n * A `<button>` rather than a bare icon: hover alone puts the explanation out of\n * reach of a keyboard and of every touch device, and the tooltip shows on focus\n * too. The text is also its accessible name, so a screen reader gets it without\n * the bubble ever opening.\n */\nexport interface FieldHintProps extends Omit<ComponentPropsWithoutRef<\"button\">, \"children\"> {\n /** The explanation. It is both the tooltip's text and, by default, the button's\n * accessible name, so a screen reader gets it without the bubble ever opening. */\n label: string;\n /** Default `\"start\"`: before the hint in the reading direction (the left in LTR, the\n * right in RTL). `left` / `right` stay physical. */\n side?: TooltipSide;\n}\n\nexport function FieldHint({\n label,\n side = \"start\",\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: FieldHintProps) {\n return (\n <Tooltip label={label} side={side} portal>\n <button\n {...rest}\n // `type` after the spread, not before. These render inside forms — that is the\n // only place a field has a label line — and a hint that defaulted to `submit`\n // because a caller spread a props object at it would save the form on a click\n // meant to explain a field.\n type=\"button\"\n // The explanation names the button unless the caller says otherwise; passing\n // `aria-label` is how you shorten it for a screen reader without shortening\n // what the bubble shows.\n aria-label={ariaLabel ?? label}\n // Nothing to activate: the tooltip opens on hover and on focus, and a\n // click that did something as well would be a second, undiscoverable\n // behaviour on the same target.\n onClick={(e) => e.preventDefault()}\n className={cn(\n \"flex text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)]\",\n className,\n )}\n >\n <HelpCircle className=\"size-3.5\" />\n </button>\n </Tooltip>\n );\n}\n\n/**\n * The floating label for a custom-dropdown trigger (a `<span>`, since the trigger is a\n * button not a labelable input). Same placement as {@link FloatingField}'s static label,\n * so every labelled field lines up. Render inside a `relative` wrapper, before the trigger.\n */\nexport interface FieldLabelProps extends ComponentPropsWithoutRef<\"span\"> {\n children: ReactNode;\n}\n\nexport function FieldLabel({ children, className, ...rest }: FieldLabelProps) {\n return (\n <span {...rest} className={cn(FLOATING_LABEL_STATIC, \"z-10\", className)}>\n {children}\n </span>\n );\n}\n\nexport interface LabelProps extends ComponentPropsWithoutRef<\"label\"> {\n /** Draw the required mark after the text. The mark is `aria-hidden`: what a\n * screen reader hears is the control's own `required` / `aria-required`, which\n * the caller still sets — a \"star\" read out as the last word of every name is\n * noise, and a control that is not marked required is not required however its\n * label looks. */\n required?: boolean;\n /** `sm` for a toolbar or a dense filter row (12px), `md` otherwise (14px). */\n size?: \"sm\" | \"md\";\n /** Dim the label with its control. A label ABOVE its field is not the field's\n * `peer`, so `peer-disabled:` cannot reach it the way it reaches a floating one. */\n disabled?: boolean;\n}\n\n/**\n * The label ABOVE a field, for the places a floating label does not fit: a control\n * the kit did not render (a third-party picker, a range slider, a group of radios),\n * a filter bar whose fields are unlabelled {@link Select}s, a form that sets labels\n * above its fields throughout.\n *\n * A new component rather than a mode of {@link FieldLabel}, because the two share\n * nothing but the word. `FieldLabel` is an absolutely positioned `<span>` inside a\n * trigger's `relative` box and has to stay exactly that for every dropdown that\n * already lines up with it; this is a real `<label>` in normal flow, with `htmlFor`,\n * that a click focuses the control through. Spacing below it belongs to the caller's\n * layout (`space-y-1.5` on the pair, typically), as it would for any block.\n */\nexport function Label({ required, size = \"md\", disabled, className, children, ...rest }: LabelProps) {\n return (\n <label\n {...rest}\n className={cn(\n \"inline-flex items-center gap-0.5 font-medium leading-none text-[var(--text-primary)]\",\n size === \"sm\" ? \"text-xs\" : \"text-sm\",\n disabled && \"cursor-default opacity-50\",\n className,\n )}\n >\n {children}\n {required && (\n <span aria-hidden className=\"text-[var(--danger)]\">\n *\n </span>\n )}\n </label>\n );\n}\n\n/**\n * The message under a field that is wrong, and the wiring that attaches it.\n *\n * `invalid` paints the field ({@link FIELD_INVALID}) and sets `aria-invalid`, and\n * there it stopped: a field that announces \"invalid\" and nothing else has told a\n * screen-reader user only that they are stuck. The message was always on screen —\n * a sibling `<p>` beside the field — and never in the accessibility tree, because\n * attaching it needs an id on the message and an `aria-describedby` on the control,\n * and no caller was going to mint one by hand for every field on a form.\n *\n * Two rules the wiring has to keep:\n *\n * - **Merge, never replace.** A field may already point at a hint (\"at least twelve\n * characters\"). Overwriting that reference to say the field is wrong trades one\n * half of the answer for the other — and it keeps the half the user has already\n * read. The hint stays first: it is the standing advice, the error is the news.\n * - **`invalid` keeps working alone**, for the forms whose message lives somewhere\n * else entirely (a summary at the top of a dialog). There is nothing to point at\n * then, and a dangling id describes the field as nothing at all.\n *\n * Deliberately NOT `role=\"alert\"`. `aria-describedby` is read when focus reaches the\n * control, which is where a field's own error is wanted; an alert would also interrupt\n * whatever is being read at the time, on every keystroke of a form that re-validates\n * as you type.\n */\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nfunction useFieldError(\n error: ReactNode,\n invalid: boolean | undefined,\n describedBy: string | undefined,\n /** The caller's own `aria-invalid`. A form library sets THIS (see `@eifi1/ui-kit/rhf`\n * `FormControl`), not our `invalid` prop — and a field that announces invalid should\n * look it too, as NumberField and the pickers already did. */\n ariaInvalid?: unknown,\n) {\n const errorId = useId();\n // `null`, `false` and `\"\"` are what a caller's `touched && errors.iban` evaluates to\n // on the happy path. None of them is a message, and pointing the control at one\n // would describe it with an empty node.\n const hasError = error !== undefined && error !== null && error !== false && error !== \"\";\n return {\n /** A field carrying a message that says what is wrong with it IS wrong. */\n isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === \"true\",\n describedBy: hasError ? (describedBy ? `${describedBy} ${errorId}` : errorId) : describedBy,\n errorEl: hasError ? (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n ) : null,\n };\n}\n\n/**\n * A field and its message as one box.\n *\n * The message cannot go INSIDE the field's own `relative` box. The password reveal\n * toggle is `inset-y-0` and the select chevron is `top-1/2`, so both centre on\n * whatever that box contains — put two lines of message in it and the chevron drifts\n * down out of the field, between the value and the text. So the field keeps its box\n * and this wraps the pair.\n *\n * It renders nothing of its own when there is no message, which is what keeps `error`\n * additive: a field without one is exactly the DOM it was before the prop existed,\n * down to the bare `<input>` an unlabelled {@link Input} drops straight into a\n * caller's flex row. For the same reason `className` is NOT moved out here — it goes\n * on the field, as it always has, so adding a message cannot silently change what\n * that prop styles.\n */\nfunction FieldGroup({ errorEl, children }: { errorEl: ReactNode; children: ReactNode }) {\n if (errorEl === null) return <>{children}</>;\n return (\n <div>\n {children}\n {errorEl}\n </div>\n );\n}\n\n/** The two names the password reveal toggle can wear. See {@link Input}. */\nexport interface PasswordRevealLabels {\n /** While the value is hidden — activating the toggle will show it. */\n show: string;\n /** While the value is shown. */\n hide: string;\n}\n\nexport const DEFAULT_PASSWORD_REVEAL_LABELS: PasswordRevealLabels = {\n show: \"Show password\",\n hide: \"Hide password\",\n};\n\n/** Caller's labels over the English defaults — the same shape as\n * `resolveDataTableLabels`, so a consumer translates every kit string one way. */\nexport function resolvePasswordRevealLabels(\n partial?: Partial<PasswordRevealLabels>,\n): PasswordRevealLabels {\n if (!partial) return DEFAULT_PASSWORD_REVEAL_LABELS;\n return { ...DEFAULT_PASSWORD_REVEAL_LABELS, ...partial };\n}\n\n// Native date/time inputs only reveal the calendar via the tiny trailing icon;\n// open the picker on a click anywhere in the field instead (feedback #224).\nconst PICKER_TYPES = new Set([\"date\", \"datetime-local\", \"month\", \"time\", \"week\"]);\n\nexport interface InputProps extends InputHTMLAttributes<HTMLInputElement> {\n label?: ReactNode;\n /** Classes for the `<input>` itself, as distinct from `className`, which\n * styles the field WRAPPER once a `label` turns this into a FloatingField.\n * Without it a labelled Input had no way to reach its own element — so\n * `tabular-nums` on a numeric text field, which NumberInput has supported\n * all along through the identically named prop, was simply unavailable. */\n inputClassName?: string;\n /** {@link FIELD_DISPLAY} — on a phone, drop the chrome and set the value as a\n * heading. For the one field a form is about (a feedback subject, an account\n * name), never for a stack of them. Labelled fields only: the label is what\n * the placeholder falls back to once it goes `sr-only`. */\n variant?: \"field\" | \"display\";\n /** The field is required and unanswered, or holds something that cannot be\n * saved — {@link FIELD_INVALID}, the same rose border/ring `Select` has worn\n * since feedback #235.\n *\n * It exists here because writing `aria-invalid` by hand did NOT do this. The\n * attribute spreads onto the element and nothing styles it — there is no\n * `[aria-invalid]` rule in this package or in either consumer's stylesheet —\n * so three Keksdose dialogs flagged a mismatched passphrase to a screen reader\n * and painted the field exactly as if it were fine. Setting the prop sets the\n * attribute too, so the two can no longer be spelled separately. */\n invalid?: boolean;\n /** What is wrong with the value, in the caller's own words (\"That IBAN has 21\n * digits\"). Rendered under the field, pointed at by the control's\n * `aria-describedby` — MERGED with any the caller already passed — and implies\n * `invalid`, so the field paints as well as announces. `invalid` alone still\n * covers the case where the message lives elsewhere. See {@link useFieldError}. */\n error?: ReactNode;\n /** Names for the password reveal toggle, English by default — it is the one\n * string this component renders on its own behalf, and a German form was\n * reading it out in English. See {@link PasswordRevealLabels}. */\n passwordLabels?: Partial<PasswordRevealLabels>;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n className,\n inputClassName,\n label,\n id,\n placeholder,\n type,\n variant = \"field\",\n invalid,\n error,\n passwordLabels,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const fieldId = id ?? generated;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n rest[\"aria-describedby\"],\n rest[\"aria-invalid\"],\n );\n const asDisplay = useMediaQuery(PHONE_QUERY, false) && variant === \"display\";\n // Password fields get a reveal toggle so users can check what they typed.\n const isPassword = type === \"password\";\n const [revealed, setRevealed] = useState(false);\n const effectiveType = isPassword && revealed ? \"text\" : type;\n const handleClick = PICKER_TYPES.has(type ?? \"\")\n ? (e: MouseEvent<HTMLInputElement>) => {\n rest.onClick?.(e);\n try {\n // showPicker throws if unsupported or not user-activated — a click is\n // a valid activation, so this is safe; guard for older browsers.\n (e.currentTarget as HTMLInputElement & { showPicker?: () => void }).showPicker?.();\n } catch {\n /* ignore */\n }\n }\n : rest.onClick;\n const passwordText = useKitLabels(\"passwordReveal\", DEFAULT_PASSWORD_REVEAL_LABELS, passwordLabels);\n const revealToggle = isPassword ? (\n <button\n type=\"button\"\n // `tabIndex={-1}` sat here, which made this a painted, clickable control that\n // Tab stepped straight over — on the one field whose value cannot be checked by\n // looking at it. There is no mouse-only case for a reveal toggle; the people who\n // cannot see what they typed are exactly who it is for. It is a tab stop now,\n // and it needs a focus ring of its own, because FIELD_BASE's ring belongs to the\n // input underneath and stays put while focus moves onto the button on top of it.\n onClick={() => setRevealed((v) => !v)}\n // …and since it is a tab stop, a field the user may not edit must not hand the\n // keyboard a control that shows what is in it.\n disabled={rest.disabled}\n aria-label={revealed ? passwordText.hide : passwordText.show}\n aria-pressed={revealed}\n className=\"absolute inset-y-0 end-0 flex items-center rounded-e-md px-2.5 text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--brand)] disabled:cursor-default disabled:opacity-50\"\n >\n {revealed ? <EyeOff className=\"size-4\" /> : <Eye className=\"size-4\" />}\n </button>\n ) : null;\n if (label === undefined) {\n if (!isPassword) {\n return (\n <FieldGroup errorEl={errorEl}>\n <input\n ref={ref}\n id={id}\n type={type}\n placeholder={placeholder}\n {...rest}\n // AFTER the spread, so the prop wins — but OR-ed with whatever the spread\n // carried, or setting `invalid` would have quietly deleted a caller's own\n // `aria-invalid`. The prop is the one that also paints; a bare attribute\n // still announces, which is all it ever did.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n // Likewise merged rather than replaced — see {@link useFieldError}.\n aria-describedby={describedBy}\n onClick={handleClick}\n className={cn(FIELD_BASE, className, inputClassName, isInvalid && FIELD_INVALID)}\n />\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <div className={cn(\"relative\", className)}>\n <input\n ref={ref}\n id={id}\n type={effectiveType}\n placeholder={placeholder}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_BASE, \"pe-9\", inputClassName, isInvalid && FIELD_INVALID)}\n />\n {revealToggle}\n </div>\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <FloatingField className={className} htmlFor={fieldId} label={label} srOnlyLabel={asDisplay}>\n <input\n ref={ref}\n id={fieldId}\n type={effectiveType}\n // A labelled field's placeholder is normally a single space, feeding the\n // floating label's peer-placeholder-shown trick. With the label sr-only\n // there is no float left to drive, and an empty borderless line would say\n // nothing at all — so the label text becomes the placeholder.\n placeholder={asDisplay ? (placeholder ?? (typeof label === \"string\" ? label : \" \")) : \" \"}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n onClick={handleClick}\n className={cn(\n asDisplay\n ? cn(FIELD_DISPLAY, \"text-xl font-semibold leading-snug\")\n : FLOATING_INPUT_CLASS,\n isPassword && \"pe-9\",\n inputClassName,\n isInvalid && FIELD_INVALID,\n )}\n />\n {revealToggle}\n </FloatingField>\n </FieldGroup>\n );\n});\nInput.displayName = \"Input\";\n\nexport interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, \"size\"> {\n label?: ReactNode;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`. */\n error?: ReactNode;\n /** A {@link FieldHint} for the label line — see {@link FloatingField}. */\n hint?: ReactNode;\n /**\n * `\"sm\"`: a 28px, 12px-type select for a toolbar or a table header, where the\n * 36px field stands a head taller than the buttons beside it. `\"md\"` (default)\n * is the field every form uses. Unlabelled selects only — a floating label needs\n * the tall box to float in, so a labelled Select ignores `\"sm\"`.\n *\n * A NUMBER is still the native attribute (the rows of a list box) and is passed\n * straight through, so the one HTML meaning of `size` keeps working. Above 1 (as\n * with `multiple`) the browser draws a list box, and the Select drops its chevron\n * and the end padding reserved for it.\n */\n size?: \"sm\" | \"md\" | number;\n /** Classes for the `<select>` itself. `className` styles the WRAPPER — the box\n * the chevron is positioned against — so it could set a width and nothing\n * else; this is the way to the element. See {@link Input}'s `inputClassName`. */\n selectClassName?: string;\n}\n\n// The compact select: the field's colours, a toolbar button's height. `py-0` and a\n// fixed height rather than a smaller padding, so the box is 28px whatever line\n// height the caller's type brings with it.\nconst SELECT_SM = \"h-7 py-0 ps-2 pe-7 text-xs\";\n\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(\n { className, label, id, children, invalid, error, hint, size, selectClassName, ...rest },\n ref,\n) {\n const generated = useId();\n // Only a number reaches the DOM; the two words are this component's own.\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // A numeric size above 1 (or `multiple`) makes the browser draw a LIST BOX — all\n // the rows in the field, no menu to drop. The dropdown dress did not fit it: the\n // chevron was centred on a box several rows tall, sat on top of the rows' text\n // at the end edge, and `pe-9` clipped every option nine units early. A list box\n // gets the plain field instead — its own native scroll, no chevron, even padding.\n const listBox = (nativeSize !== undefined && nativeSize > 1) || Boolean(rest.multiple);\n const small = size === \"sm\" && label === undefined;\n const fieldId = id ?? generated;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n rest[\"aria-describedby\"],\n rest[\"aria-invalid\"],\n );\n // Custom chevron (native arrow hidden via appearance-none) so it sits a touch\n // in from the end border and matches both themes — feedback #223. A DISABLED\n // select has no menu to drop, so it drops the chevron too: the arrow is the one\n // thing on the control that promises a choice (Keksdose dev#474, where the\n // account type became read-only and still looked exactly like a picker).\n const chevron =\n rest.disabled || listBox ? null : (\n <FieldChevron className={small ? \"end-1.5 size-3.5\" : undefined} />\n );\n // `pe-9` is the room the chevron takes; a list box has none to make room for.\n const dress = listBox ? \"overflow-y-auto\" : \"appearance-none pe-9\";\n if (label === undefined) {\n return (\n <FieldGroup errorEl={errorEl}>\n <div className={cn(\"relative\", className)}>\n <select\n ref={ref}\n // `id` is destructured out of the props to feed `fieldId`, and this branch\n // never put it back — so an UNLABELLED Select swallowed it and the\n // consumer's own `<label for=\"…\">` pointed at nothing. The control stayed\n // in the tab order with no accessible name at all: reachable, and silent\n // when it was reached. Input and Textarea both forward it here; this is\n // the third one doing the same thing.\n id={id}\n size={nativeSize}\n {...rest}\n // OR-ed with the spread for the reason spelled out on Input's copy: this\n // branch wrote `invalid || undefined`, so passing `aria-invalid` by hand\n // to a Select — which is what a caller does when the validity is\n // `\"grammar\"` or `\"spelling\"`, or when the paint is not wanted — had the\n // attribute silently dropped. Input has never done that.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n dress,\n small && SELECT_SM,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n {chevron}\n </div>\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <FloatingField className={className} htmlFor={fieldId} label={label} staticLabel hint={hint}>\n <select\n ref={ref}\n id={fieldId}\n size={nativeSize}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n // The floated label takes the top strip of a list box too, so its first\n // row starts under the label rather than behind it.\n listBox ? \"pt-5 pb-1\" : FIELD_FLOATING_PAD,\n \"peer\",\n dress,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n {chevron}\n </FloatingField>\n </FieldGroup>\n );\n});\nSelect.displayName = \"Select\";\n\nexport interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n label?: ReactNode;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`. */\n error?: ReactNode;\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n { className, label, id, placeholder, invalid, error, ...rest },\n ref,\n) {\n const generated = useId();\n const fieldId = id ?? generated;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n rest[\"aria-describedby\"],\n rest[\"aria-invalid\"],\n );\n if (label === undefined) {\n return (\n <FieldGroup errorEl={errorEl}>\n <textarea\n ref={ref}\n id={id}\n placeholder={placeholder}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_BASE, className, isInvalid && FIELD_INVALID)}\n />\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <FloatingField className={className} htmlFor={fieldId} label={label}>\n <textarea\n ref={ref}\n id={fieldId}\n placeholder=\" \"\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(FLOATING_INPUT_CLASS, isInvalid && FIELD_INVALID)}\n />\n </FloatingField>\n </FieldGroup>\n );\n});\nTextarea.displayName = \"Textarea\";\n\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n children: ReactNode;\n /**\n * When true, drop the card chrome (border, rounded corners, shadow) on mobile so the\n * card spans edge-to-edge. Chrome reappears at the `md` breakpoint. Use for primary\n * content cards on data pages; leave off for centered dialog/panel cards.\n */\n flush?: boolean;\n /**\n * `inset`: a panel INSIDE a card rather than a card on the page — the raised\n * `--bg-surface-2`, a small radius, `p-3` of its own and no border or shadow, since\n * it is already sitting on the card that has them. Lenkbank's results blocks and\n * corner panels write this by hand (`rounded-md bg-surface-2 p-3`) under a Card of\n * their own. Unlike the default card it carries its padding, because every one of\n * those copies wanted the same one; a caller's `p-*` still wins. `flush` is ignored:\n * an inset panel never runs edge to edge.\n */\n variant?: \"default\" | \"inset\";\n}\n\nexport function Card({ className, children, flush, variant = \"default\", ...rest }: CardProps) {\n return (\n <div\n {...rest}\n className={cn(\n variant === \"inset\"\n ? \"rounded-md bg-[var(--bg-surface-2)] p-3\"\n : cn(\n // Surface + border are theme tokens so the palette switcher (feedback\n // #307) can re-skin every card; a caller's own bg-*/border-* override\n // still wins via tailwind-merge.\n \"bg-[var(--bg-surface)]\",\n flush\n ? \"border-y border-[var(--border)] md:rounded-lg md:border md:shadow-sm\"\n : \"rounded-lg border border-[var(--border)] shadow-sm\",\n ),\n className,\n )}\n >\n {children}\n </div>\n );\n}\n\n// Composed shadcn-style Card sub-parts. `Card` stays padding-less (callers set\n// their own padding via className), so these own the padding/rhythm. Token-driven\n// so they re-skin with the palette. Use CardHeader → CardTitle/CardDescription\n// (+ optional CardAction, top-right) → CardContent → CardFooter.\n/** The sub-parts add nothing to a `<div>`'s props — they are the SAME element with a\n * `data-slot` and a padding rhythm — so each name is an alias rather than an empty\n * interface pretending to be more. They exist so a consumer's own wrapper can say\n * `CardHeaderProps` instead of `ComponentProps<typeof CardHeader>`. */\nexport type CardHeaderProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardTitleProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardDescriptionProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardActionProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardContentProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardFooterProps = ComponentPropsWithoutRef<\"div\">;\n\nexport function CardHeader({ className, ...props }: CardHeaderProps) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n // Grid (not flex) so CardAction can occupy a top-right column; with no\n // action it collapses to one column and title/description stack.\n \"grid auto-rows-min items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function CardTitle({ className, ...props }: CardTitleProps) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"font-semibold leading-none\", className)}\n {...props}\n />\n );\n}\n\nexport function CardDescription({ className, ...props }: CardDescriptionProps) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-sm text-[var(--money-neutral)]\", className)}\n {...props}\n />\n );\n}\n\nexport function CardAction({ className, ...props }: CardActionProps) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\"col-start-2 row-start-1 row-span-2 self-start justify-self-end\", className)}\n {...props}\n />\n );\n}\n\nexport function CardContent({ className, ...props }: CardContentProps) {\n // `last:pb-6`, not a plain `pb-6`: CardHeader owns the top padding and CardFooter\n // owns the bottom one, so a card WITHOUT a footer had nothing closing it off and\n // its last field sat flush against the card edge (kastlan feedback: the language\n // input touching the card bottom on /profile). Scoping to `:last-child` fixes that\n // case and leaves a footered card's rhythm exactly as it was.\n return <div data-slot=\"card-content\" className={cn(\"px-6 last:pb-6\", className)} {...props} />;\n}\n\nexport function CardFooter({ className, ...props }: CardFooterProps) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6 pb-6\", className)}\n {...props}\n />\n );\n}\n\n/** A `<span>`'s props plus the words a reader hears — see {@link CardHeaderProps}.\n * The ring is drawn with a border, so there is nothing inside it to put children in. */\nexport interface SpinnerProps extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n /**\n * What the spinner means, for a screen reader. Default: `common.loading` from the\n * {@link UiKitProvider}, else \"Loading…\".\n *\n * `null` makes it DECORATIVE — no role, no text, hidden from assistive tech. Use it\n * whenever words are already there: beside visible \"Loading…\" text (otherwise it is\n * announced twice), inside a labelled button (otherwise its text joins the button's\n * name), or inside a live region of the app's own (otherwise that region and this\n * one both announce).\n */\n label?: string | null;\n}\n\n/**\n * A spinning ring that also SAYS it is loading.\n *\n * It used to be a bordered span and nothing else, so a reader met no element at all\n * where a sighted user saw the page working — or, beside an emptied list, met the\n * empty list and concluded there was nothing there. `role=\"status\"` makes it a polite\n * live region, and the text inside is what that region announces. Text rather than\n * `aria-label`: a live region's announcement is its CONTENT, and several readers\n * ignore a name on one.\n *\n * `relative` so the `sr-only` text has a local containing block (see\n * sr-only-containment.test). Where the words are already on screen, pass\n * `label={null}`: announcing is right for a spinner standing alone and wrong for one\n * beside text, inside a button, or inside the app's own live region.\n */\nexport function Spinner({ className, label, ...rest }: SpinnerProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, {\n loading: label ?? undefined,\n });\n const ring = cn(\n \"relative inline-block h-5 w-5 animate-spin rounded-full border-2 border-[var(--border)] border-t-[var(--text-primary)]\",\n className,\n );\n if (label === null) return <span aria-hidden {...rest} className={ring} />;\n return (\n <span role=\"status\" {...rest} className={ring}>\n {/* The trailing space is a separator for the case the caller forgot `label={null}`\n inside a button: a name is the concatenation of its text, and without it the\n spinner's word ran straight into the button's — \"Loadingconfirm\". */}\n <span className=\"sr-only\">{common.loading} </span>\n </span>\n );\n}\n\nexport interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** The box renders `title` and `hint` in its own two-line rhythm, which is what makes\n * every empty state in three apps look like the same thing — so there is no\n * `children` slot to put arbitrary content in. The two slots below are the only\n * other things an empty state has turned out to need, and each has a fixed place. */\n title: string;\n hint?: string;\n /** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's\n * offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and\n * muted, so five call sites cannot pick five sizes; hidden from assistive tech,\n * because the title already says what it shows. */\n icon?: ReactNode;\n /** What to do about it, BELOW the hint: keksdose's query-state cards (Retry), its\n * error boundary (Retry / Reload / Go home — pass all three in a fragment, they\n * wrap and centre as one row), kastlan's \"Create a rule\". Buttons or links the\n * caller renders; the box only places them. */\n action?: ReactNode;\n}\n\nexport function EmptyState({ title, hint, icon, action, className, ...rest }: EmptyStateProps) {\n return (\n <div\n {...rest}\n className={cn(\n \"flex flex-col items-center justify-center rounded-lg border border-dashed border-[var(--border-strong)] bg-[var(--bg-surface-2)] px-4 py-10 text-center text-sm text-[var(--text-muted)]\",\n className,\n )}\n >\n {icon != null && (\n <div aria-hidden className=\"mb-3 text-[var(--text-placeholder)] [&_svg]:size-8\">\n {icon}\n </div>\n )}\n <div className=\"font-medium text-[var(--text-secondary)]\">{title}</div>\n {hint && <div className=\"mt-1 text-xs\">{hint}</div>}\n {action != null && (\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">{action}</div>\n )}\n </div>\n );\n}\n\n/** One tab of a {@link Tabs} strip. */\nexport interface TabItem<T extends string> {\n id: T;\n /** A ReactNode so a tab can pair an icon with text. */\n label: ReactNode;\n /** Optional trailing node (e.g. a count pill). */\n badge?: ReactNode;\n /** Marks a tab that IS a route — see {@link TabsProps}. */\n href?: string;\n /** Leading decoration: an icon, or the colour swatches of the lines this tab\n * draws in a chart above the strip. Hidden from assistive tech — it repeats\n * what the label says, or it says something the label must say instead. */\n icon?: ReactNode;\n /** A second, smaller line under the label — the one fact that tells two tabs\n * with similar labels apart (\"80 km/h\", \"Level 3\"). Part of the tab's name. */\n detail?: ReactNode;\n /** A tab that exists as a slot with nothing in it yet, drawn with a dashed\n * outline so a gap in a sequence looks like a gap. Visual only: say \"empty\" in\n * the label or `detail` if a screen reader needs to know. */\n empty?: boolean;\n /** `false` keeps this one tab when the strip has `onRemove` — the one sheet a\n * workbook cannot be without. Default: removable. */\n removable?: boolean;\n /** Plain-text name for the labels the strip composes about this tab (\"Remove\n * …\"), for when `label` is not a string. Default: `label` if it is a string,\n * else the id. */\n name?: string;\n}\n\n/** The strings the strip renders on its own behalf — only when it can add or\n * remove tabs. */\nexport interface TabsLabels {\n /** The \"add\" button's visible text when the caller gives no `addLabel`. */\n add: string;\n /** Accessible name of a tab's × — \"Remove <tab>\". */\n remove: (tab: string) => string;\n}\n\nexport const DEFAULT_TABS_LABELS: TabsLabels = {\n add: \"Add tab\",\n remove: (tab) => `Remove ${tab}`,\n};\n\n/**\n * The strip's own props sit on a `<div>`: the tablist IS the root element, so anything\n * a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an\n * `aria-describedby` — lands there. `onChange` is omitted from the div's props because\n * this component's `onChange` hands over the chosen TAB ID, not a DOM event, and\n * `children` because the strip renders `tabs` — a caller who wants a PANEL wires it up\n * through `panelId`, which is the whole point of that prop.\n */\nexport interface TabsProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n /** `label` is a ReactNode so tabs can pair an icon with text; `badge` is an\n * optional trailing node (e.g. a count pill). `href` marks a tab that IS a route:\n * it renders as an anchor so it can be middle-/⌘-clicked into a new tab (Keksdose\n * feedback #451), while a plain click still goes through `onChange` and stays\n * client-side. Tabs that only flip local state leave it unset — a link to a URL\n * that does not select the tab would be worse than no link. */\n tabs: TabItem<T>[];\n active: T;\n onChange: (id: T) => void;\n className?: string;\n /** Let the strip WRAP onto as many rows as it needs below `md`, instead of\n * scrolling sideways — for a strip carrying more tabs than fit on a phone row.\n * Keksdose live #262: ten reports on a 406px screen showed about three, and the\n * round-2 answer (a <select> below `md`) cost a tap to open and a tap to choose.\n * *\"I would rather keep the tabs but have multirow tabs depending on the screen\n * size, I will not loose the function to see all reports at once and navigate by\n * single click rather by double click.\"*\n *\n * The active MARKER has to change with the layout, which is why this cannot be\n * done from a call site with a `className`: the default marker is a `border-b-2`\n * underline riding the container's own bottom rule, and a tab sitting in a row\n * that does not touch that rule cannot wear it — every row but the last would\n * show a stray line floating mid-strip, and `-mb-px` would pull each chip a\n * pixel into the row beneath it. A wrapped strip marks the active tab with a\n * filled brand chip instead, which is also the only thing findable at a glance\n * among ten same-weight labels on three rows.\n *\n * From `md` up NOTHING changes: same single-line underline strip, same paddings,\n * same colours as every other strip in the app.\n *\n * Opt-in on purpose. The three-tab strips (invoices, statements, bank imports)\n * already fit a phone row, and turning those into chips would be an unrequested\n * redesign of three other pages. */\n wrap?: boolean;\n /**\n * Accessible name for the `role=\"tablist\"` container. Every tab carries its own\n * text, so this names the GROUP, not the tabs; leave it unset where a visible\n * heading immediately above already does that job.\n *\n * @deprecated Pass `aria-label` instead. This kit had three spellings for one idea\n * — `ariaLabel`, `aria-label` and this `label` — and settled on the DOM one, which\n * is also the one that arrives for free now that the strip spreads its rest props.\n * `label` still works and is unchanged; it names the strip only when `aria-label`\n * is absent.\n */\n label?: string;\n /**\n * `id` of the element the caller renders the open tab's content into — the missing\n * half of `role=\"tab\"`. A tab that controls nothing is a tab in name only: a screen\n * reader announces \"tab, 2 of 5\" and then has no way to take the user to what it\n * opened, and no way back.\n *\n * **This component does not render the panel, on purpose.** `Tabs` is the STRIP; the\n * content lives wherever the caller put it — three Keksdose pages render it in a\n * sibling `<Card>`, one renders it through a router outlet, and a tab can BE a route\n * (`href`), in which case the panel is a whole page this component never sees.\n * Wrapping `children` here would mean either moving that content into the strip's\n * subtree — a layout change on every page that uses tabs — or shipping a wrapper\n * that only some callers could use. So the two halves are joined by an id instead.\n *\n * Wire the other end yourself:\n *\n * ```tsx\n * <Tabs tabs={tabs} active={active} onChange={setActive} panelId=\"report-panel\" />\n * <div id=\"report-panel\" role=\"tabpanel\" aria-labelledby=\"report-panel-tab\" tabIndex={-1}>\n * ```\n *\n * `aria-controls` goes on the OPEN tab only, and `${panelId}-tab` is its id — the\n * closed tabs' panels are not in the document, and a dangling `aria-controls`\n * describes a tab as opening something that is not there.\n */\n panelId?: string;\n /**\n * Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab\n * (only on the open one with `removeOn=\"active\"`).\n *\n * The × is on the open tab only — a row of tabs each carrying one is a row of\n * things to hit by accident, and the tab you can remove should be the one you\n * are looking at — but its room is reserved on every removable tab, so opening a\n * tab never widens it and never pushes the rest of the strip along (lenkbank\n * feedback #72). It is named \"Remove <tab>\" and is not a tab stop of its own: the\n * strip stays one stop, and the keyboard path is Delete (announced through\n * `aria-keyshortcuts`), which removes whichever tab has focus (see `removeOn`).\n *\n * The strip only ASKS: it calls this, and the tab goes when the caller drops it\n * from `tabs` — after a confirmation, a request, whatever it needs. When it does,\n * focus that was on the removed tab (or its ×) moves to the open tab if the\n * removed one was open, else to its neighbour, instead of falling to the page.\n */\n onRemove?: (id: T) => void;\n /**\n * Which tabs the Delete key removes.\n *\n * - `\"every\"` (default): any focused removable tab — the ARIA pattern's reading.\n * - `\"active\"`: only the OPEN tab, the same one that wears the ×. For a strip whose\n * remove is immediate and unconfirmed (lenkbank's sheet tabs delete a measurement\n * sheet at once), so arrowing across the strip and pressing Delete cannot take\n * out a sheet the user is not looking at. Delete on a closed tab does nothing,\n * and only the open tab announces the shortcut (`aria-keyshortcuts`).\n *\n * Either way the × is drawn on the open tab only and its room stays reserved on\n * every removable tab, so the strip never relayouts when the selection moves.\n */\n removeOn?: \"every\" | \"active\";\n /** Renders an \"add\" button after the last tab, outside the tablist — it is an\n * action, not a tab, and a tablist may own only tabs. */\n onAdd?: () => void;\n /** The add button's text. Say what is added — \"Add loop\", \"Add report\" says\n * more than \"Add\". Default: `tabs.add` from the {@link UiKitProvider}. */\n addLabel?: string;\n /** Overrides for this strip's strings. See {@link TabsLabels}. */\n labels?: Partial<TabsLabels>;\n /** An add or a remove is in flight: the × and the add button are disabled and\n * Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */\n busy?: boolean;\n /**\n * `\"vertical\"`: the same strip as a SIDE NAV — one item per row, the open one\n * filled, ↑/↓ walking it (Home/End as before) and `aria-orientation=\"vertical\"` on\n * the tablist. For keksdose's admin page and settings page, which each hand-built\n * the identical sidebar with none of this strip's keyboard, roving tab stop or\n * routed-`href` handling. Everything else — `href` tabs, `badge`, `panelId`,\n * `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the\n * badge moves to the row's end.\n *\n * **On a phone ({@link PHONE_QUERY}) it becomes the horizontal strip**, `wrap` and\n * all, rather than staying a column. A side nav only works beside its content; on\n * a phone it has to go ABOVE it, and eight full-width rows there push the panel\n * the user picked below the fold on every visit — the two keksdose pages both\n * collapsed theirs into a scrolling row by hand for exactly that reason. The\n * orientation is switched in JavaScript, not with `md:` classes, so the\n * `aria-orientation` a screen reader hears and the arrow keys that work always\n * match the layout on screen. The caller's own two-column layout has to stack at\n * the same breakpoint (`md:grid-cols-[14rem_1fr]`).\n */\n orientation?: \"horizontal\" | \"vertical\";\n}\n\n// The two shapes are written out as whole strings rather than as one base plus a\n// pile of `md:` overrides. The unwrapped pair is character-for-character what the\n// three strips that do not opt in get, so those cannot drift; the wrapped pair is\n// authored from scratch, so no unprefixed utility has to be beaten by its own `md:`\n// twin through tailwind-merge. The only ordering this relies on is Tailwind's own:\n// unprefixed utilities are emitted first, then `md:` — so from 768px up the wrapped\n// strip resolves to exactly the paint of the default one. Both themes come free now\n// that the colours are tokens: there is no `md:dark:` tier left to keep in step.\nconst TABLIST_CLASSES =\n \"flex gap-1 overflow-x-auto overflow-y-hidden border-b border-[var(--border)]\";\n// The side nav. A column of full-width rows with the open one FILLED rather than\n// underlined: an underline under one row of a list reads as a separator, not as a\n// selection. `--bg-active` is the kit's own selected-row fill (the sidebar's), so the\n// admin page's nav and the app's own sidebar mark \"you are here\" the same way.\nconst TABLIST_VERTICAL_CLASSES = \"flex flex-col gap-0.5\";\nconst TAB_VERTICAL_CLASSES =\n \"flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]\";\nconst TAB_VERTICAL_ACTIVE_CLASSES = \"bg-[var(--bg-active)] text-[var(--text-primary)]\";\nconst TAB_VERTICAL_INACTIVE_CLASSES =\n \"text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\";\n\nconst TABLIST_WRAP_CLASSES =\n \"flex flex-wrap gap-1.5 md:flex-nowrap md:gap-1 md:overflow-x-auto md:overflow-y-hidden md:border-b md:border-[var(--border)]\";\n\nconst TAB_CLASSES =\n \"whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]\";\nconst TAB_ACTIVE_CLASSES = \"border-[var(--text-primary)] text-[var(--text-primary)]\";\nconst TAB_INACTIVE_CLASSES =\n \"border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:border-[var(--border-strong)]\";\n\n// Below `md`, a chip: `py-2` on `text-xs` is exactly 32px tall, and ten German\n// report labels then land in three rows on a 406px screen (four at `text-sm`). The\n// active fill is the app's own selected pair, `--brand` / `--brand-contrast`: the\n// palette store writes both halves of it together for every preset and both themes,\n// so the contrast holds everywhere (5.1:1 at its worst preset) without a hard-coded\n// colour. Inactive chips take the raised card surface with full-strength body text —\n// all ten have to stay readable; it is the FILL, not the text weight, that says\n// which one is open.\nconst TAB_WRAP_CLASSES =\n \"whitespace-nowrap rounded-md px-2.5 py-2 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] md:rounded-none md:border-b-2 md:-mb-px md:bg-transparent md:px-3 md:py-2 md:text-sm\";\nconst TAB_WRAP_ACTIVE_CLASSES =\n \"bg-[var(--brand)] text-[var(--brand-contrast)] md:border-[var(--text-primary)] md:text-[var(--text-primary)]\";\nconst TAB_WRAP_INACTIVE_CLASSES =\n \"bg-[var(--bg-surface)] text-[var(--text-primary)] md:border-transparent md:text-[var(--text-muted)] md:hover:border-[var(--border-strong)] md:hover:text-[var(--text-secondary)]\";\n\n// The add/remove chrome. The × reserves its gutter on every removable tab (see\n// `onRemove`); `md:pe-8` is spelled out because the wrapped chip's `md:px-3` would\n// otherwise hand the gutter back from 768px up.\nconst TAB_REMOVABLE_CLASSES = \"pe-8 md:pe-8\";\nconst TAB_EMPTY_CLASSES = \"-mx-1.5 rounded border border-dashed border-[var(--border-strong)] px-1.5\";\nconst TAB_ADD_CLASSES =\n \"inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4\";\nconst TAB_ADD_WRAP_CLASSES = \"px-2.5 text-xs md:px-3 md:text-sm\";\n\nexport function Tabs<T extends string>({\n tabs,\n active,\n onChange,\n className,\n wrap = false,\n label,\n panelId,\n onRemove,\n removeOn = \"every\",\n onAdd,\n addLabel,\n labels,\n busy = false,\n orientation = \"horizontal\",\n \"aria-label\": ariaLabel,\n ...rest\n}: TabsProps<T>) {\n const text = useKitLabels(\"tabs\", DEFAULT_TABS_LABELS, labels);\n // See `orientation`: a vertical strip is the horizontal one on a phone.\n const phone = useMediaQuery(PHONE_QUERY, false);\n const vertical = orientation === \"vertical\" && !phone;\n const stripRef = useRef<HTMLDivElement>(null);\n const isRemovable = (tab: TabItem<T>) => onRemove !== undefined && tab.removable !== false;\n // Whether Delete reaches this tab: every removable tab, or only the open one.\n const deletesOnKey = (tab: TabItem<T>) =>\n isRemovable(tab) && (removeOn === \"every\" || tab.id === active);\n const nameOf = (tab: TabItem<T>) =>\n tab.name ?? (typeof tab.label === \"string\" ? tab.label : tab.id);\n\n // Where focus goes once a removal the strip asked for has happened. The caller\n // owns `tabs` and may take its time (a confirmation, a request), so this waits\n // for the id to actually leave the list rather than guessing at the moment.\n const pendingFocus = useRef<{ removed: T; wasActive: boolean; neighbour?: T } | null>(null);\n const requestRemove = (id: T) => {\n const index = tabs.findIndex((t) => t.id === id);\n pendingFocus.current = {\n removed: id,\n wasActive: id === active,\n neighbour: (tabs[index + 1] ?? tabs[index - 1])?.id,\n };\n onRemove?.(id);\n };\n useLayoutEffect(() => {\n const pending = pendingFocus.current;\n if (!pending || tabs.some((t) => t.id === pending.removed)) return;\n pendingFocus.current = null;\n const strip = stripRef.current;\n if (!strip) return;\n // Only focus that fell out of the strip with the removed tab is ours to place.\n // If the user has moved on — into the panel, into a dialog — leave it there.\n const focused = document.activeElement;\n if (focused && focused !== document.body && !strip.contains(focused)) return;\n // The removed tab was open: focus follows the caller's new selection, which is\n // also the strip's tab stop. It was not: its neighbour, as the ARIA pattern has it.\n const target =\n !pending.wasActive && tabs.some((t) => t.id === pending.neighbour)\n ? pending.neighbour\n : active;\n const index = tabs.findIndex((t) => t.id === target);\n strip.querySelectorAll<HTMLElement>('[role=\"tab\"]')[index]?.focus();\n }, [tabs, active]);\n\n // Arrow keys walk the strip in DOM order (ARIA tabs pattern), which is what keeps\n // a WRAPPED strip navigable: the rows flow in DOM order too, so Right off the end\n // of row one lands on the first chip of row two rather than nowhere. Home/End jump\n // to the ends. Focus only — activation stays on click/Enter/Space, because a tab\n // here can be a real route and moving focus must not navigate.\n //\n // On the TABS, not on the tablist. The container carried it, which left a `<div>`\n // wearing an interactive role and a key handler while being unfocusable —\n // `jsx-a11y/interactive-supports-focus`, and the only instance of it left in `src/`.\n // The fix that rule wants is `tabIndex` on the div; the fix the pattern wants is\n // the handler on the elements that are natively focusable and actually receive the\n // keystroke. Those are the tabs, so the warning goes away by being right rather\n // than by being satisfied.\n const onTabKeyDown = (e: KeyboardEvent<HTMLElement>, tab: TabItem<T>) => {\n // Delete removes the FOCUSED tab (the ARIA pattern's optional key for deletable\n // tabs) — the keyboard's way to the × that is not a tab stop. `busy` swallows it\n // rather than letting it fall through to the page. With `removeOn=\"active\"` only\n // the open tab answers; a closed one lets the key pass untouched.\n if (e.key === \"Delete\" && deletesOnKey(tab)) {\n e.preventDefault();\n if (!busy) requestRemove(tab.id);\n return;\n }\n // Along the reading direction: the strip is a flex row, so in RTL the NEXT tab sits\n // to the left, and ArrowLeft has to reach it. A vertical strip answers ↑/↓ instead\n // — and ONLY those, as the pattern has it: ←/→ are left to the page, where a side\n // nav's neighbour (the panel) may well want them.\n const step: 1 | -1 | 0 = vertical\n ? e.key === \"ArrowDown\"\n ? 1\n : e.key === \"ArrowUp\"\n ? -1\n : 0\n : horizontalStep(e.key, e.currentTarget);\n if (step === 0 && e.key !== \"Home\" && e.key !== \"End\") return;\n const strip = e.currentTarget.closest('[role=\"tablist\"]');\n if (!strip) return;\n const items = Array.from(strip.querySelectorAll<HTMLElement>('[role=\"tab\"]'));\n const from = items.indexOf(e.currentTarget);\n if (from === -1) return;\n e.preventDefault();\n const to =\n e.key === \"Home\"\n ? 0\n : e.key === \"End\"\n ? items.length - 1\n : (from + step + items.length) % items.length;\n items[to]?.focus();\n };\n const tablist = (\n <div\n // Before the role and the name: a caller's arbitrary attribute is welcome on the\n // strip, but a `role` or an `aria-label` arriving through a spread props object\n // must not be able to unmake the tablist the tabs below are registered against.\n {...rest}\n ref={stripRef}\n role=\"tablist\"\n // The DOM spelling wins over the deprecated `label`; see {@link TabsProps}.\n aria-label={ariaLabel ?? label}\n // Only when vertical: `horizontal` is the tablist's implicit value, and the\n // attribute on every existing strip would be noise in every snapshot of them.\n aria-orientation={vertical ? \"vertical\" : undefined}\n // With an add button the strip gains an outer box, and `className` goes there\n // — it is the box a caller's margin or width is meant for.\n className={cn(\n vertical ? TABLIST_VERTICAL_CLASSES : wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,\n onAdd ? \"min-w-0\" : className,\n )}\n >\n {tabs.map((tab) => {\n const isActive = tab.id === active;\n const removable = isRemovable(tab);\n // Written once and worn by either tag below, so a routed tab and a\n // state-only tab stay indistinguishable to the eye and to a screen reader.\n const shared = {\n role: \"tab\" as const,\n \"aria-selected\": isActive,\n // Roving tabindex: the whole strip is ONE stop in the page's tab order, and\n // it is the open tab. Ten report tabs otherwise cost ten Tab presses to step\n // over on the way to the table below them (live #262's strip is the extreme\n // case, but every strip in the app paid it).\n //\n // The stop follows the SELECTION, not the focus. Arrowing is a look around —\n // activation here is manual, because a tab can be a real route — so re-\n // pointing the stop at a report the user only arrowed past would hand the\n // strip back on the next visit in a state they never chose. The cost is that\n // Tab out and back returns to the open tab rather than to the last one\n // looked at; the ARIA pattern allows either, and this one needs no second\n // piece of state shadowing `active`.\n tabIndex: isActive ? 0 : -1,\n // Only the open tab: see `panelId`.\n \"aria-controls\": isActive ? panelId : undefined,\n id: isActive && panelId ? `${panelId}-tab` : undefined,\n \"aria-keyshortcuts\": deletesOnKey(tab) ? \"Delete\" : undefined,\n onKeyDown: (e: KeyboardEvent<HTMLElement>) => onTabKeyDown(e, tab),\n className: vertical\n ? cn(\n TAB_VERTICAL_CLASSES,\n isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,\n removable && \"pe-8\",\n )\n : cn(\n wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,\n wrap\n ? isActive\n ? TAB_WRAP_ACTIVE_CLASSES\n : TAB_WRAP_INACTIVE_CLASSES\n : isActive\n ? TAB_ACTIVE_CLASSES\n : TAB_INACTIVE_CLASSES,\n removable && TAB_REMOVABLE_CLASSES,\n ),\n };\n const inner = (\n <span\n className={cn(\n // In a side nav the row is the full width, so the label takes it and a\n // badge lands at the row's end, where a column of counts lines up.\n vertical ? \"flex min-w-0 flex-1 items-center gap-1.5\" : \"inline-flex items-center gap-1.5\",\n tab.empty && TAB_EMPTY_CLASSES,\n )}\n >\n {tab.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 items-center gap-0.5\">\n {tab.icon}\n </span>\n )}\n {tab.detail != null ? (\n <span className=\"flex flex-col items-start text-start\">\n <span>{tab.label}</span>\n {/* A separator for the NAME only — whitespace between flex items is\n not drawn, and without it the two lines ran together into one\n word for a screen reader (\"80 km/hloop 2\"). */}{\" \"}\n <span className=\"text-[11px] font-normal leading-tight text-[var(--text-muted)]\">\n {tab.detail}\n </span>\n </span>\n ) : (\n tab.label\n )}\n {tab.badge != null ? (\n vertical ? <span className=\"ms-auto flex shrink-0 items-center\">{tab.badge}</span> : tab.badge\n ) : null}\n </span>\n );\n const element = tab.href ? (\n <a\n key={tab.id}\n {...shared}\n href={tab.href}\n draggable={false}\n onClick={(e) => {\n // Middle-, ⌘/Ctrl-, Shift- and Alt-click belong to the browser\n // (feedback #451); only the plain left click is ours to cancel and\n // route through `onChange`, which keeps the switch client-side.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n onChange(tab.id);\n }}\n // A <button> tab activated on Space; an anchor does not, so becoming a\n // link (feedback #451) would have quietly cost keyboard users the tab\n // strip. Enter is deliberately untouched — it is the anchor's own\n // default and already arrives at `onChange` through the click handler\n // above, so handling it here too would switch tabs twice. Same three\n // lines the mobile row card carries, for the same reason; the rejected\n // alternative — keeping a button and faking the middle click with\n // window.open — is what the whole item is moving away from.\n //\n // Composed with the strip's arrow-key walk rather than replacing it: this\n // prop overrides the one in `shared`, and an anchor tab that lost the\n // arrows would be a strip that is navigable only where it is not a link.\n onKeyDown={(e) => {\n onTabKeyDown(e, tab);\n if (e.defaultPrevented || e.key !== \" \") return;\n e.preventDefault();\n onChange(tab.id);\n }}\n >\n {inner}\n </a>\n ) : (\n <button key={tab.id} {...shared} onClick={() => onChange(tab.id)}>\n {inner}\n </button>\n );\n if (!removable) return element;\n return (\n // The × cannot go INSIDE the tab: a button in a button is invalid HTML, and\n // a tab's children are presentational, so a reader would flatten it into the\n // tab's name. It sits beside the tab, over the gutter the tab reserves.\n <div key={tab.id} className={cn(\"relative flex\", !vertical && \"shrink-0\")}>\n {element}\n {isActive && (\n <IconButton\n size=\"2xs\"\n tone=\"danger\"\n // Not a tab stop: the strip is one, and Delete is the keyboard's path.\n // Still a named button, so a screen reader's cursor and a touch\n // reader's swipe both reach it.\n tabIndex={-1}\n type=\"button\"\n aria-label={text.remove(nameOf(tab))}\n disabled={busy}\n onClick={() => requestRemove(tab.id)}\n className={cn(\n \"absolute inset-y-0 end-1 my-auto\",\n // On a wrapped strip's filled chip the quiet grey would vanish into\n // the brand fill below `md`.\n wrap && \"text-[var(--brand-contrast)] md:text-[var(--text-placeholder)]\",\n )}\n >\n <X />\n </IconButton>\n )}\n </div>\n );\n })}\n </div>\n );\n if (!onAdd) return tablist;\n if (vertical) {\n // Under the column, not beside it, and with no rule to carry on: a side nav has none.\n return (\n <div className={cn(\"flex flex-col gap-0.5\", className)}>\n {tablist}\n <button type=\"button\" onClick={onAdd} disabled={busy} className={TAB_ADD_CLASSES}>\n <Plus aria-hidden />\n {addLabel ?? text.add}\n </button>\n </div>\n );\n }\n return (\n <div\n className={cn(\n wrap ? \"flex flex-wrap items-end gap-1.5 md:flex-nowrap md:gap-0\" : \"flex items-end\",\n className,\n )}\n >\n {tablist}\n {/* Outside the tablist: an action, not a tab — and a tablist may own only\n tabs. The rule under the strip carries on beneath it. */}\n <div\n className={cn(\n \"flex flex-1 items-center\",\n wrap\n ? \"md:self-stretch md:border-b md:border-[var(--border)]\"\n : \"self-stretch border-b border-[var(--border)]\",\n )}\n >\n <button\n type=\"button\"\n onClick={onAdd}\n disabled={busy}\n className={cn(TAB_ADD_CLASSES, wrap && TAB_ADD_WRAP_CLASSES)}\n >\n <Plus aria-hidden />\n {addLabel ?? text.add}\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["IconButton","Input","Select","Textarea"]}
1
+ {"version":3,"sources":["../../src/components/ui.tsx"],"sourcesContent":["import { forwardRef, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from \"lucide-react\";\nimport type { ButtonHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactNode, Ref, SelectHTMLAttributes, TextareaHTMLAttributes } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { Tooltip, type TooltipSide } from \"./tooltip\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\" | \"brand\" | \"link\";\n\n// Shared base ring for every button-styled element. Kept as a named const so the\n// <Button> component and the {@link buttonClasses} helper draw from one source and\n// can never drift apart.\nconst BUTTON_BASE =\n \"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed\";\n\nexport type ButtonSize = \"sm\" | \"md\";\n\n// Box geometry per size, split out of the base so the two cannot be merged into one\n// string that a size then has to fight. `md` is the pre-0.8.0 look, unchanged. `sm` is\n// the compact secondary action keksdose repeats by hand as `px-2 py-1 text-xs` (and\n// `px-2 py-0.5 text-xs`) on the buttons in a toolbar or a table header; one rung is\n// enough, so the two spellings meet at `py-1`. There is no `lg`: no app has asked for\n// a bigger text button (IconButton's `lg` is a touch target, not a text size). The\n// variant map comes AFTER the size, so `link`'s `p-0` still wins at either size.\nconst BUTTON_SIZES: Record<ButtonSize, string> = {\n md: \"gap-2 px-3 py-2 text-sm\",\n sm: \"gap-1.5 px-2 py-1 text-xs\",\n};\n\n// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.\n// Actions default to a warm bordered look (primary = filled warm chip, secondary =\n// outline); `brand` stays the solid accent for the rare strong CTA; `danger` takes the\n// semantic `--danger` family (destructive semantics), so a consumer can re-point the\n// destructive hue instead of inheriting a hard-coded red. Focus rings follow each\n// variant's own accent: brand for the four neutral ones, danger for `danger`.\nconst buttonVariantClasses: Record<ButtonVariant, string> = {\n primary:\n \"border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus:ring-[var(--brand)]\",\n secondary:\n \"border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]\",\n ghost:\n \"bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]\",\n danger:\n \"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border)]\",\n brand:\n \"bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]\",\n // A text link that is still a `<button>` — keksdose's six hand-rolled\n // `<button className=\"text-brand underline\">` sites (\"Resend code\", \"Show all\",\n // \"Undo\" in a toast…), which act rather than navigate and so must not be anchors.\n // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but\n // the base's `focus:ring-2` stays: those copies had `outline-none` and no ring, so\n // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that\n // ring hugging the word instead of drawing a pill round it.\n link:\n \"rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus:ring-[var(--brand)]\",\n};\n\n/** The second argument of {@link buttonClasses} in its options form. */\nexport interface ButtonClassesOptions {\n /** See {@link ButtonProps.size}. */\n size?: ButtonSize;\n className?: string;\n}\n\n/**\n * Button classes for the rare case where the styling must land on a non-`<button>`\n * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix\n * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real\n * button works, prefer `<Button>`. Draws from the same base, size and variant maps as\n * `<Button>`, so the two stay in lockstep.\n *\n * The second argument is either the extra classes (the pre-0.8.0 form) or\n * `{ size, className }` — `buttonClasses(\"secondary\", { size: \"sm\" })` for keksdose's\n * compact toolbar links.\n */\nexport function buttonClasses(\n variant: ButtonVariant = \"primary\",\n classNameOrOptions?: string | ButtonClassesOptions,\n): string {\n const { size = \"md\", className } =\n typeof classNameOrOptions === \"object\" ? classNameOrOptions : { className: classNameOrOptions };\n return cn(BUTTON_BASE, BUTTON_SIZES[size], buttonVariantClasses[variant], className);\n}\n\n/**\n * ⚠️ No default `type`: like a native `<button>`, a `Button` inside a `<form>` SUBMITS\n * it unless you pass `type=\"button\"`. Kept native on purpose — a form's own submit\n * button relies on it, and changing the default would silently stop those forms from\n * submitting. Pass `type=\"button\"` for every other action in a form (keksdose had six\n * link buttons submitting their form).\n */\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n variant?: ButtonVariant;\n /** `md` (default) is the page's action button. `sm` is the compact one — 12px text\n * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a\n * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand\n * there). Every variant takes either size. */\n size?: ButtonSize;\n /** In a flex row next to a taller labelled field, fill the field's height so the\n * two line up. No effect outside a flex row. */\n stretch?: boolean;\n /** The `<button>` element. React 19 passes `ref` to a function component as an\n * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —\n * declared here only because `ButtonHTMLAttributes` does not carry it. */\n ref?: Ref<HTMLButtonElement>;\n}\n\nexport function Button({ variant = \"primary\", size = \"md\", stretch, className, ...rest }: ButtonProps) {\n return (\n <button\n {...rest}\n className={cn(\n BUTTON_BASE,\n BUTTON_SIZES[size],\n // In a flex row next to a taller labelled field, `stretch` makes the button\n // fill the field's height so the two line up (self-stretch overrides the row's\n // align-items). No effect outside a flex row / when it's already the tallest.\n stretch && \"self-stretch\",\n buttonVariantClasses[variant],\n className,\n )}\n />\n );\n}\n\n// Canonical square icon-only button. <Button> carries TEXT geometry — px-3 py-2\n// gap-2 — so rendering a bare glyph through it gives a small icon in a wide,\n// text-shaped box. This fixes a square box instead and, crucially, forces the\n// child icon to a size set by the BOX (20px at md/sm) via `[&_svg]:size-*` so a\n// caller cannot under-size it. Use it wherever an action is a bare icon\n// (edit/delete/tools) so they all match the top-bar icon buttons and can never\n// drift apart again.\n//\n// Draws its colours from the same `buttonVariantClasses` map as <Button>, so the\n// two re-skin together with the palette.\nconst ICON_BUTTON_BASE =\n \"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed\";\n\nexport type IconButtonSize = \"lg\" | \"md\" | \"sm\" | \"xs\" | \"2xs\";\n\n// Box and glyph together, so a 24px chip action cannot end up holding a 20px icon\n// that touches its edges. The two small steps are lenkbank's list-row (28px) and\n// chip (24px) actions, which it had hand-rolled beside the kit's 32/36px ones.\nconst ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {\n // The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's\n // bulk-action bars write `size-11` by hand over an `md` button to get it on phones.\n lg: \"size-11 [&_svg]:size-5\",\n md: \"size-9 [&_svg]:size-5\",\n sm: \"size-8 [&_svg]:size-5\",\n xs: \"size-7 rounded [&_svg]:size-4\",\n \"2xs\": \"size-6 rounded [&_svg]:size-3.5\",\n};\n\n// A tone re-colours the glyph without changing what the variant draws around it.\n// Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):\n// QUIET is placeholder grey until the pointer or focus arrives, then the tone's\n// family; TONED wears the tone's colour at rest. The defaults are the looks each tone\n// had before `quiet` existed: `danger` is quiet — an action in every row of a list\n// must not shout from every row, so the red arrives only on the one row you are about\n// to act on — while `warning` and `info` are toned, because they mark the ONE thing\n// on the row to notice (keksdose's \"needs review\" flag, its reconcile action at\n// accounts-page:867), and quiet grey would hide the very thing they point out.\n// `muted` is quiet by definition and has no toned look; `default` has no tone.\nexport type IconButtonTone = \"default\" | \"muted\" | \"danger\" | \"warning\" | \"info\";\n\ntype ColouredTone = \"danger\" | \"warning\" | \"info\";\n\nconst ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {\n danger: {\n quiet:\n \"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]\",\n toned: \"text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border)]\",\n },\n warning: {\n quiet:\n \"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus:ring-[var(--warning-border)]\",\n toned: \"text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]\",\n },\n info: {\n quiet:\n \"text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]\",\n toned: \"text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]\",\n },\n};\n\nconst ICON_BUTTON_MUTED =\n \"text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]\";\n\n/** Which tones sit quiet at rest when `quiet` is left out. */\nconst QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = { danger: true, warning: false, info: false };\n\nfunction iconButtonToneClass(tone: IconButtonTone, quiet: boolean | undefined): string {\n if (tone === \"default\") return \"\";\n if (tone === \"muted\") return cn(ICON_BUTTON_MUTED, ICON_BUTTON_QUIET_DISABLED_REST);\n const isQuiet = quiet ?? QUIET_BY_DEFAULT[tone];\n const look = ICON_BUTTON_TONES[tone];\n return isQuiet ? cn(look.quiet, ICON_BUTTON_QUIET_DISABLED_REST) : look.toned;\n}\n\n// A disabled button must not answer the pointer. The hover classes above are plain\n// `hover:` (so a caller's `className=\"hover:…\"` still replaces them through\n// tailwind-merge), which means they fire on a disabled button too — the old grey\n// icon lit up on hover while refusing the click. Rather than rewrite every hover as\n// `enabled:hover:` (which never matches the `<a>` that `buttonClasses` also styles,\n// and would out-rank a caller's plain `hover:` override), each variant pins its\n// RESTING look under `disabled:hover:`, which out-ranks any `hover:` by specificity\n// and only ever matches a disabled button.\nconst ICON_BUTTON_DISABLED_REST: Record<ButtonVariant | \"overlay\", string> = {\n primary: \"disabled:hover:bg-[var(--bg-surface-2)]\",\n secondary: \"disabled:hover:bg-transparent\",\n ghost: \"disabled:hover:bg-transparent\",\n danger: \"disabled:hover:bg-[var(--danger)]\",\n brand: \"disabled:hover:bg-[var(--brand)]\",\n link: \"disabled:hover:bg-transparent disabled:hover:no-underline\",\n overlay: \"disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]\",\n};\n\n// The quiet looks change the glyph on hover, so they pin their resting glyph the\n// same way. A toned look keeps its glyph on hover and needs no pin.\nconst ICON_BUTTON_QUIET_DISABLED_REST = \"disabled:hover:text-[var(--text-placeholder)]\";\n\n// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the\n// \"selected\" look of a Chip or a SegmentedControl option, so an on toggle reads as on\n// beside them. After the tone, so a pressed `muted` button is brand, not grey.\nconst ICON_BUTTON_PRESSED =\n \"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-[var(--brand)] disabled:hover:bg-[var(--brand-bg)] disabled:hover:text-[var(--brand)]\";\n\n// `variant=\"overlay\"`: a round, translucent disc for a control that sits ON a photo\n// (keksdose's receipt-scan preview: close, rotate, retake over the camera image).\n// The inverse pair, not the surface one: what has to hold is the contrast between the\n// disc and its glyph, whatever the picture underneath is, and `--bg-inverse` /\n// `--text-inverse` are the one token pair defined as each other's opposite in both\n// themes. The disc is the inverse at 60% (`color-mix`, so it stays a token a palette\n// can re-point); the blur keeps a busy background from breaking the glyph's edge.\nconst ICON_BUTTON_OVERLAY =\n \"rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus:ring-[var(--text-inverse)]\";\n\nexport interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n /** Any {@link ButtonVariant}, or `overlay` — a round translucent disc for use over\n * an image (see `ICON_BUTTON_OVERLAY`). */\n variant?: ButtonVariant | \"overlay\";\n /** Box size: lg = 44px (a phone's touch target), md = 36px (matches the top bar),\n * sm = 32px — all three with a 20px icon;\n * xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px\n * icon (an action on a chip or a tab). */\n size?: IconButtonSize;\n /** Glyph colour over the variant. `muted`: placeholder grey, full text colour on\n * hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a\n * remove/delete that repeats down a list. `warning`: amber at rest — a flag that\n * wants attention. `info`: sky at rest — a notice-worthy but harmless action\n * (keksdose's reconcile). Default: the variant's own colours. See `quiet` for\n * turning a coloured tone's resting look the other way. */\n tone?: IconButtonTone;\n /**\n * Whether a coloured tone (`danger`, `warning`, `info`) waits for the pointer:\n * `true` is placeholder grey at rest and the tone's colour on hover and focus;\n * `false` wears the tone's colour at rest. Left out, each tone keeps its own\n * default — `danger` quiet, `warning` and `info` not. Ignored for `muted` (quiet by\n * definition) and `default` (no tone).\n *\n * `quiet={false}` on `danger` is for a destructive action that stands ALONE, where\n * hover-to-reveal hides it: keksdose's phone bulk bar (mobile-bulk-bar.tsx) has one\n * \"delete all\" and a touch screen that never hovers, so it painted rose by hand.\n * `quiet` on `warning`/`info` is the same switch the other way, for a flag repeated\n * down a list. A boolean over the tone rather than a new tone (`danger-solid`) or an\n * `emphasis` scale: there are exactly two resting looks, every coloured tone has\n * both, and which one fits is a question about the SITE (alone or repeated, touch or\n * pointer), not about the tone — so it is one switch the family shares.\n */\n quiet?: boolean;\n /**\n * Make it a toggle button. `true` sets `aria-pressed=\"true\"` and draws the \"on\" look\n * (brand glyph on the quiet brand fill); `false` sets `aria-pressed=\"false\"` with the\n * normal look, so a screen reader still hears a toggle that is off. Left out, it is\n * an ordinary button with no `aria-pressed` — or whatever `aria-pressed` the caller\n * passes. For keksdose's budget share toggle (budgets-page:291), which paints its own\n * brand colour over a ghost button today. Keep the `aria-label` the same in both\n * states (\"Share budget\"): the pressed state already says whether it is on.\n */\n pressed?: boolean;\n /** Keep the click (and the Enter/Space that produces it) from reaching an\n * ancestor's handler — for an action inside a clickable table row or card.\n *\n * **Not a licence to put this inside another `<button>`.** HTML forbids any\n * interactive content, and any element with a `tabindex`, inside a button — so\n * the `<span role=\"button\" tabIndex={0}>` workaround is invalid too — and ARIA\n * makes a button's children presentational, so a screen reader flattens the\n * inner one into the outer one's name and it cannot be reached at all. Make the\n * row's main action a button that fills the row and put this one BESIDE it, on\n * top:\n *\n * ```tsx\n * <div className=\"relative\">\n * <button className=\"w-full pe-16 …\" onClick={open}>…row…</button>\n * <div className=\"absolute inset-y-0 end-2 flex items-center gap-0.5\">\n * <IconButton size=\"xs\" tone=\"danger\" aria-label=\"Delete\" onClick={remove}>\n * <Trash2 />\n * </IconButton>\n * </div>\n * </div>\n * ```\n *\n * The pair look exactly like the nested version, click exactly like it, and\n * are two tab stops a screen reader can tell apart. */\n stopPropagation?: boolean;\n}\n\nexport const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(\n { variant = \"ghost\", size = \"md\", tone = \"default\", quiet, pressed, stopPropagation, className, onClick, onKeyDown, ...rest },\n ref,\n) {\n return (\n <button\n ref={ref}\n {...rest}\n aria-pressed={pressed ?? rest[\"aria-pressed\"]}\n onClick={(e) => {\n if (stopPropagation) e.stopPropagation();\n onClick?.(e);\n }}\n onKeyDown={(e) => {\n // A button turns Enter/Space into a click of its own, but the KEY still\n // bubbles — and a clickable row listening for Enter would act on it too.\n if (stopPropagation && (e.key === \"Enter\" || e.key === \" \")) e.stopPropagation();\n onKeyDown?.(e);\n }}\n className={cn(\n ICON_BUTTON_BASE,\n ICON_BUTTON_SIZES[size],\n // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.\n variant === \"overlay\" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],\n ICON_BUTTON_DISABLED_REST[variant],\n iconButtonToneClass(tone, quiet),\n pressed && ICON_BUTTON_PRESSED,\n className,\n )}\n />\n );\n});\nIconButton.displayName = \"IconButton\";\n\nexport const FIELD_BASE =\n \"block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-3 py-2 text-sm text-[var(--text-primary)] shadow-sm placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:ring-[var(--brand)] \" +\n // A field the user cannot change has to LOOK settled. Without this, `disabled`\n // dimmed the floating label and nothing else — FIELD_BASE's own\n // `text-[var(--text-primary)]` overrides the browser's grey — so a read-only value\n // sat there at full body-text strength, indistinguishable from one you could retype\n // (Keksdose dev#455/#474).\n \"disabled:cursor-default disabled:bg-[var(--bg-surface-2)] disabled:text-[var(--text-muted)] \" +\n // Same for a field that takes focus but refuses keys: `readOnly` is not\n // `disabled`, and a value that cannot be edited should not claim it can be. Opt\n // OUT with {@link FIELD_WRITABLE_LOOK} in the one case where `readOnly` does not\n // mean that — see the two-factor code field, which uses it to block an autofill\n // and drops it the moment you focus the field.\n //\n // ⚠️ The ATTRIBUTE, never the `:read-only` pseudo-class (Keksdose dev#477).\n // `:read-only` does not mean \"was marked readonly\" — per Selectors 4 it matches\n // everything that is not `:read-write`, and only editable inputs/textareas and\n // contenteditable elements are `:read-write`. So `read-only:` matched every\n // `<select>` in the app and every field-styled `<button>` built on this base —\n // the native selects, the date trigger, the entity/currency/multi pickers — and\n // painted them all in the \"you may not edit this\" grey. On one transaction form\n // that made three different-looking families out of one field style: plain\n // inputs, grey selects, grey trigger buttons.\n \"[&[readonly]]:bg-[var(--bg-surface-2)] [&[readonly]]:text-[var(--text-muted)]\";\n\n/** Cancels FIELD_BASE's read-only treatment for a field that is `readOnly` for a\n * reason other than \"you may not edit this\". Attribute-scoped for the same reason\n * FIELD_BASE is — the two have to cancel on the identical selector or twMerge\n * cannot make the later one win. */\nexport const FIELD_WRITABLE_LOOK =\n \"[&[readonly]]:bg-[var(--bg-surface)] [&[readonly]]:text-[var(--text-primary)]\";\n\n// Extra top padding leaves room for a label that floats INSIDE the field (the\n// \"filled\" pattern) — the label sits in the top strip, the value below it. Used by\n// every labelled field (native + custom-dropdown triggers). twMerge lets pt/pb win\n// over FIELD_BASE's py-2.\nexport const FIELD_FLOATING_PAD = \"pt-4 pb-1\";\n\n// Error/required highlight for a field that is missing a value — a `--danger`\n// border and matching focus ring so the control itself shows what's wrong, not just\n// a note beside it (feedback #235). Layered after FIELD_BASE so twMerge wins.\n//\n// The `ring-1` is not decoration; it is what makes the highlight SURVIVE display\n// scaling (Keksdose live #295 — *\"Account select boundary. It is not highlighted on\n// the sides.\"*). At 125%, the browser's usual setting on a 2560px screen, a 1px CSS\n// border is 1.25 device pixels: the horizontal edges land on whole rows and paint\n// solid, while one of the two VERTICAL edges lands across a pixel boundary and is\n// spread over two columns at partial coverage. Measured on the receipt's required\n// account field, dark theme, back when this was a hard-coded rose (full =\n// rgb(208,30,78)): left/top/bottom all 208, right 160 then 115 — the side that is\n// supposed to shout, at 55–77% of the others. A border plus a ring is 2 CSS px, so\n// whatever the fraction there is always one fully covered device pixel on every side\n// (re-measured: worst side 241). It is the GEOMETRY that carries that, not the hue,\n// so the fix survives a consumer re-pointing `--danger-border`.\n//\n// A ring rather than `border-2`: a box-shadow adds no layout, so an invalid field\n// stays exactly the size of a valid one and nothing beside it moves when the value\n// arrives.\nexport const FIELD_INVALID =\n \"border-[var(--danger-border)] ring-1 ring-[var(--danger-border)] focus:border-[var(--danger)] focus:ring-[var(--danger)]\";\n\nexport const FLOATING_INPUT_CLASS = cn(FIELD_BASE, FIELD_FLOATING_PAD, \"peer placeholder:text-transparent\");\n\n/** The phone breakpoint the display treatment below keys off — the same one the\n * numpad sheet uses, kept in one place so the two can't drift apart. */\nexport const PHONE_QUERY = \"(max-width: 767px)\";\n\n/**\n * The PHONE display treatment (Keksdose feedback #176, carried across the entry\n * forms by #179): the field chrome removed so the ONE input a form is actually\n * about reads as the thing itself, not as another boxed row in a stack.\n *\n * Every control that takes `variant=\"display\"` — {@link Input}, `NumberInput`,\n * `AmountInput` — means exactly the same thing by it: the treatment applies below\n * {@link PHONE_QUERY} and the field is untouched above it, so a caller never has\n * to ask the viewport, and a form can't end up half-treated across breakpoints.\n *\n * What stays, deliberately:\n * - A hairline baseline. With the box gone something still has to say \"you can\n * type here\"; it takes the brand colour on focus, where a bordered field would\n * light its whole outline. Each edge is set exactly once (`border-x-0\n * border-t-0 border-b`) so the rule can't hinge on utility order.\n * - The muted placeholder colour, because a display field is usually empty at\n * the moment it matters most and has nothing else to show.\n * - The label, moved to `sr-only` rather than dropped: display type is legible\n * to the eye, not to a screen reader.\n *\n * Size and weight are NOT here — an amount wants display type, a subject line\n * wants a heading — so each control adds its own on top.\n */\nexport const FIELD_DISPLAY =\n \"block w-full border-x-0 border-t-0 border-b border-[var(--border)] bg-transparent px-0 pt-0 pb-1 text-[var(--text-primary)] shadow-none placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:outline-none focus:ring-0 disabled:opacity-60\";\n\n// A field-styled button trigger for the custom dropdown controls (MultiSelect,\n// CurrencySelect) — the field look (border/bg) as a flex row for the value + chevron,\n// so they don't hand-copy the field classes. Add FIELD_FLOATING_PAD only when the\n// trigger carries a label (unlabelled triggers stay normal height to match buttons /\n// adjacent controls). Pair the labelled case with a static FieldLabel.\n// `relative` so the trigger can host an absolutely-centred FieldChevron / clear\n// button the way the native Select does.\nexport const FIELD_TRIGGER = cn(\n FIELD_BASE,\n \"relative flex items-center justify-between gap-2 text-start hover:bg-[var(--bg-hover)]\",\n);\n\n/** An alias rather than an interface: the chevron adds nothing of its own to an\n * `<svg>`'s props, and an interface declaring no members is the same type wearing a\n * name that suggests otherwise. */\nexport type FieldChevronProps = Omit<ComponentPropsWithoutRef<\"svg\">, \"children\">;\n\n/** The dropdown chevron, shared by the native {@link Select} and every custom\n * {@link FIELD_TRIGGER} control.\n *\n * Absolutely positioned and centred on the FIELD box. As an ordinary flex child\n * it centres on the *content* box instead, and `FIELD_FLOATING_PAD` (pt-4 pb-1)\n * pushes that box's midline down — so a labelled currency/multi-select chevron\n * sat visibly lower than the native select's right beside it (feedback #400).\n * Pair it with `pe-9` on the trigger so the value can't run underneath. At the\n * inline END, not the right: a right-to-left form reads the value from the right,\n * and a chevron parked on top of its first word hid exactly the part that says\n * what was picked. */\nexport function FieldChevron({ className, ...rest }: FieldChevronProps) {\n return (\n <ChevronDown\n {...rest}\n // After the spread: the chevron is decoration beside a control that already has\n // a name, and an `aria-hidden` a caller could switch off by accident is a second\n // announcement of the same field.\n aria-hidden\n className={cn(\n \"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\",\n className,\n )}\n />\n );\n}\n\n// Animated label that starts centred (as a placeholder) in an empty field and\n// floats up INSIDE the top strip on focus or once the field has a value. Sits on\n// the field's own surface, so no background chip and nothing to mismatch the card.\nexport const FLOATING_LABEL_CLASS = cn(\n \"pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all\",\n \"max-w-[calc(100%-1.5rem)] truncate\",\n \"peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]\",\n \"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]\",\n \"peer-disabled:opacity-50\",\n);\n\n// The animated label again, as a ROW that the label and its \"?\" share — same\n// placement, same float, same type, but with the type on the row so the label\n// INHERITS it. It has to be that way round: `peer-focus:` compiles to a sibling\n// selector, so the classes have to sit on the element that is actually a sibling\n// of the input, and a label nested inside a wrapper is not one.\n//\n// Wider end clearance than the label alone takes (`3rem` rather than `1.5rem`),\n// because the controls that carry an animated label are the ones with something\n// at the end edge of the field — NumberInput's calculator is the case this was\n// written for. The label truncates a little sooner; the alternative was the \"?\"\n// sitting on top of a button (steering-design feedback #48).\nconst FLOATING_ROW_CLASS = cn(\n \"pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all\",\n \"max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]\",\n \"peer-focus:top-1 peer-focus:text-[11px] peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]\",\n \"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]\",\n \"peer-disabled:opacity-50\",\n);\n\n// The TYPE of the small static label, without any placement. Split out so the\n// label and anything sharing its line (see `hint` on {@link FloatingField}) are\n// laid out by one flex row instead of by two absolute offsets guessing at the\n// same baseline — which is what put dev#468's \"?\" three pixels above the word it\n// belongs to.\nconst STATIC_LABEL_TYPE =\n \"text-[11px] leading-tight text-[var(--text-muted)] peer-disabled:opacity-50\";\n\n// A field that always has a value (select / dropdown trigger) keeps the label\n// permanently in the floated position — small, in the top strip, value below.\nexport const FLOATING_LABEL_STATIC = cn(\n \"pointer-events-none absolute start-3 top-1\",\n STATIC_LABEL_TYPE,\n \"max-w-[calc(100%-1.5rem)] truncate\",\n);\n\n/**\n * The one place that assembles a labelled field: a `relative` wrapper around the\n * control (`children`) plus a floating label inside the top strip. Pass `staticLabel`\n * for controls that always have a value (selects); omit it for free-text fields whose\n * label animates from centred→up. Used by Input/Select/Textarea/NumberInput; the\n * dropdown controls that need a ref + menu keep their own wrapper but the same label\n * (via {@link FieldLabel}) and trigger ({@link FIELD_TRIGGER}) styles.\n */\nexport interface FloatingFieldProps extends ComponentPropsWithoutRef<\"div\"> {\n htmlFor?: string;\n label?: ReactNode;\n staticLabel?: boolean;\n /** Keep the label in the accessibility tree but out of the layout — what\n * {@link FIELD_DISPLAY} needs, since a floating label inside a field with no\n * field left would have nothing to float in. */\n srOnlyLabel?: boolean;\n /** Something interactive that belongs to the LABEL rather than to the value —\n * in practice a {@link FieldHint} \"?\" (dev#468). It is rendered in a flex row\n * with the label, so it is centred on the label's line by the layout instead\n * of by a hand-tuned `top-…`, and it can never drift when the type changes.\n *\n * It rides an ANIMATED label too, and the two cases place it differently on\n * purpose. A static label has a field-wide strip to itself, so the hint sits\n * at the far end of it and a long label truncates into the gap. An animated\n * one belongs to a control with something at the end edge of the field — a\n * calculator, a stepper — so the hint follows the label instead, and it is\n * the label that gives way (steering-design feedback #48). */\n hint?: ReactNode;\n children: ReactNode;\n}\n\nexport function FloatingField({\n className,\n htmlFor,\n label,\n staticLabel,\n srOnlyLabel,\n hint,\n children,\n ...rest\n}: FloatingFieldProps) {\n const withHint = hint !== undefined && !srOnlyLabel;\n const atEnd = withHint && staticLabel;\n const labelEl = label !== undefined && (\n <label\n htmlFor={htmlFor}\n className={\n srOnlyLabel\n ? \"sr-only\"\n : atEnd\n ? cn(\"pointer-events-none min-w-0 truncate\", STATIC_LABEL_TYPE)\n : withHint\n ? \"pointer-events-none min-w-0 truncate\"\n : staticLabel\n ? FLOATING_LABEL_STATIC\n : FLOATING_LABEL_CLASS\n }\n >\n {label}\n </label>\n );\n return (\n // `relative` is the whole contract of this wrapper — the floating label and every\n // control that hangs off it (a reveal toggle, a chevron) are positioned against\n // this box — so it is merged through `cn` after the spread rather than left where\n // a caller's stray `className` or `style` could unset it.\n <div {...rest} className={cn(\"relative\", className)}>\n {children}\n {withHint ? (\n // Static: `inset-x-3` rather than `start-3`, so a long label truncates at\n // the field's own end padding instead of running under the chevron.\n // Animated: the row floats with the label and is only as wide as it needs\n // to be. Either way the hint keeps its width (`shrink-0`) and the label\n // is the one that gives way.\n <div\n className={\n atEnd\n ? \"pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1\"\n : FLOATING_ROW_CLASS\n }\n >\n {labelEl}\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>\n </div>\n ) : (\n labelEl\n )}\n </div>\n );\n}\n\n/**\n * The \"?\" that explains a field, on the field's own label line (dev#468).\n *\n * Pass it to a labelled {@link Select} / {@link Input} as `hint`. It exists as a\n * component rather than as a snippet each form repeats because the previous\n * version was exactly that snippet — an absolutely-positioned button whose\n * `top-1.5` was one guess at where an 11px label sits — and the reporter's\n * follow-up was *\"question mark is not centered. Is it part of the hoc?\n * positioning problems seem quite frequently.\"* It was not part of the HOC. Now\n * it is, and there is one place left where the answer can be wrong.\n *\n * A `<button>` rather than a bare icon: hover alone puts the explanation out of\n * reach of a keyboard and of every touch device, and the tooltip shows on focus\n * too. The text is also its accessible name, so a screen reader gets it without\n * the bubble ever opening.\n */\nexport interface FieldHintProps extends Omit<ComponentPropsWithoutRef<\"button\">, \"children\"> {\n /** The explanation. It is both the tooltip's text and, by default, the button's\n * accessible name, so a screen reader gets it without the bubble ever opening. */\n label: string;\n /** Default `\"start\"`: before the hint in the reading direction (the left in LTR, the\n * right in RTL). `left` / `right` stay physical. */\n side?: TooltipSide;\n}\n\nexport function FieldHint({\n label,\n side = \"start\",\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: FieldHintProps) {\n return (\n <Tooltip label={label} side={side} portal>\n <button\n {...rest}\n // `type` after the spread, not before. These render inside forms — that is the\n // only place a field has a label line — and a hint that defaulted to `submit`\n // because a caller spread a props object at it would save the form on a click\n // meant to explain a field.\n type=\"button\"\n // The explanation names the button unless the caller says otherwise; passing\n // `aria-label` is how you shorten it for a screen reader without shortening\n // what the bubble shows.\n aria-label={ariaLabel ?? label}\n // Nothing to activate: the tooltip opens on hover and on focus, and a\n // click that did something as well would be a second, undiscoverable\n // behaviour on the same target.\n onClick={(e) => e.preventDefault()}\n className={cn(\n \"flex text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)]\",\n className,\n )}\n >\n <HelpCircle className=\"size-3.5\" />\n </button>\n </Tooltip>\n );\n}\n\n/**\n * The floating label for a custom-dropdown trigger (a `<span>`, since the trigger is a\n * button not a labelable input). Same placement as {@link FloatingField}'s static label,\n * so every labelled field lines up. Render inside a `relative` wrapper, before the trigger.\n */\nexport interface FieldLabelProps extends ComponentPropsWithoutRef<\"span\"> {\n children: ReactNode;\n}\n\nexport function FieldLabel({ children, className, ...rest }: FieldLabelProps) {\n return (\n <span {...rest} className={cn(FLOATING_LABEL_STATIC, \"z-10\", className)}>\n {children}\n </span>\n );\n}\n\nexport interface LabelProps extends ComponentPropsWithoutRef<\"label\"> {\n /** Draw the required mark after the text. The mark is `aria-hidden`: what a\n * screen reader hears is the control's own `required` / `aria-required`, which\n * the caller still sets — a \"star\" read out as the last word of every name is\n * noise, and a control that is not marked required is not required however its\n * label looks. */\n required?: boolean;\n /** `sm` for a toolbar or a dense filter row (12px), `md` otherwise (14px). */\n size?: \"sm\" | \"md\";\n /** Dim the label with its control. A label ABOVE its field is not the field's\n * `peer`, so `peer-disabled:` cannot reach it the way it reaches a floating one. */\n disabled?: boolean;\n}\n\n/**\n * The label ABOVE a field, for the places a floating label does not fit: a control\n * the kit did not render (a third-party picker, a range slider, a group of radios),\n * a filter bar whose fields are unlabelled {@link Select}s, a form that sets labels\n * above its fields throughout.\n *\n * A new component rather than a mode of {@link FieldLabel}, because the two share\n * nothing but the word. `FieldLabel` is an absolutely positioned `<span>` inside a\n * trigger's `relative` box and has to stay exactly that for every dropdown that\n * already lines up with it; this is a real `<label>` in normal flow, with `htmlFor`,\n * that a click focuses the control through. Spacing below it belongs to the caller's\n * layout (`space-y-1.5` on the pair, typically), as it would for any block.\n */\nexport function Label({ required, size = \"md\", disabled, className, children, ...rest }: LabelProps) {\n return (\n <label\n {...rest}\n className={cn(\n \"inline-flex items-center gap-0.5 font-medium leading-none text-[var(--text-primary)]\",\n size === \"sm\" ? \"text-xs\" : \"text-sm\",\n disabled && \"cursor-default opacity-50\",\n className,\n )}\n >\n {children}\n {required && (\n <span aria-hidden className=\"text-[var(--danger)]\">\n *\n </span>\n )}\n </label>\n );\n}\n\n/**\n * The message under a field that is wrong, and the wiring that attaches it.\n *\n * `invalid` paints the field ({@link FIELD_INVALID}) and sets `aria-invalid`, and\n * there it stopped: a field that announces \"invalid\" and nothing else has told a\n * screen-reader user only that they are stuck. The message was always on screen —\n * a sibling `<p>` beside the field — and never in the accessibility tree, because\n * attaching it needs an id on the message and an `aria-describedby` on the control,\n * and no caller was going to mint one by hand for every field on a form.\n *\n * Two rules the wiring has to keep:\n *\n * - **Merge, never replace.** A field may already point at a hint (\"at least twelve\n * characters\"). Overwriting that reference to say the field is wrong trades one\n * half of the answer for the other — and it keeps the half the user has already\n * read. The hint stays first: it is the standing advice, the error is the news.\n * - **`invalid` keeps working alone**, for the forms whose message lives somewhere\n * else entirely (a summary at the top of a dialog). There is nothing to point at\n * then, and a dangling id describes the field as nothing at all.\n *\n * Deliberately NOT `role=\"alert\"`. `aria-describedby` is read when focus reaches the\n * control, which is where a field's own error is wanted; an alert would also interrupt\n * whatever is being read at the time, on every keystroke of a form that re-validates\n * as you type.\n */\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nfunction useFieldError(\n error: ReactNode,\n invalid: boolean | undefined,\n describedBy: string | undefined,\n /** The caller's own `aria-invalid`. A form library sets THIS (see `@eifi1/ui-kit/rhf`\n * `FormControl`), not our `invalid` prop — and a field that announces invalid should\n * look it too, as NumberField and the pickers already did. */\n ariaInvalid?: unknown,\n) {\n const errorId = useId();\n // `null`, `false` and `\"\"` are what a caller's `touched && errors.iban` evaluates to\n // on the happy path. None of them is a message, and pointing the control at one\n // would describe it with an empty node.\n const hasError = error !== undefined && error !== null && error !== false && error !== \"\";\n return {\n /** A field carrying a message that says what is wrong with it IS wrong. */\n isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === \"true\",\n describedBy: hasError ? (describedBy ? `${describedBy} ${errorId}` : errorId) : describedBy,\n errorEl: hasError ? (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n ) : null,\n };\n}\n\n/**\n * A field and its message as one box.\n *\n * The message cannot go INSIDE the field's own `relative` box. The password reveal\n * toggle is `inset-y-0` and the select chevron is `top-1/2`, so both centre on\n * whatever that box contains — put two lines of message in it and the chevron drifts\n * down out of the field, between the value and the text. So the field keeps its box\n * and this wraps the pair.\n *\n * It renders nothing of its own when there is no message, which is what keeps `error`\n * additive: a field without one is exactly the DOM it was before the prop existed,\n * down to the bare `<input>` an unlabelled {@link Input} drops straight into a\n * caller's flex row. For the same reason `className` is NOT moved out here — it goes\n * on the field, as it always has, so adding a message cannot silently change what\n * that prop styles.\n */\nfunction FieldGroup({ errorEl, children }: { errorEl: ReactNode; children: ReactNode }) {\n if (errorEl === null) return <>{children}</>;\n return (\n <div>\n {children}\n {errorEl}\n </div>\n );\n}\n\n/** The two names the password reveal toggle can wear. See {@link Input}. */\nexport interface PasswordRevealLabels {\n /** While the value is hidden — activating the toggle will show it. */\n show: string;\n /** While the value is shown. */\n hide: string;\n}\n\nexport const DEFAULT_PASSWORD_REVEAL_LABELS: PasswordRevealLabels = {\n show: \"Show password\",\n hide: \"Hide password\",\n};\n\n/** Caller's labels over the English defaults — the same shape as\n * `resolveDataTableLabels`, so a consumer translates every kit string one way. */\nexport function resolvePasswordRevealLabels(\n partial?: Partial<PasswordRevealLabels>,\n): PasswordRevealLabels {\n if (!partial) return DEFAULT_PASSWORD_REVEAL_LABELS;\n return { ...DEFAULT_PASSWORD_REVEAL_LABELS, ...partial };\n}\n\n// Native date/time inputs only reveal the calendar via the tiny trailing icon;\n// open the picker on a click anywhere in the field instead (feedback #224).\nconst PICKER_TYPES = new Set([\"date\", \"datetime-local\", \"month\", \"time\", \"week\"]);\n\nexport interface InputProps extends InputHTMLAttributes<HTMLInputElement> {\n label?: ReactNode;\n /** Classes for the `<input>` itself, as distinct from `className`, which\n * styles the field WRAPPER once a `label` turns this into a FloatingField.\n * Without it a labelled Input had no way to reach its own element — so\n * `tabular-nums` on a numeric text field, which NumberInput has supported\n * all along through the identically named prop, was simply unavailable. */\n inputClassName?: string;\n /** {@link FIELD_DISPLAY} — on a phone, drop the chrome and set the value as a\n * heading. For the one field a form is about (a feedback subject, an account\n * name), never for a stack of them. Labelled fields only: the label is what\n * the placeholder falls back to once it goes `sr-only`. */\n variant?: \"field\" | \"display\";\n /** The field is required and unanswered, or holds something that cannot be\n * saved — {@link FIELD_INVALID}, the same rose border/ring `Select` has worn\n * since feedback #235.\n *\n * It exists here because writing `aria-invalid` by hand did NOT do this. The\n * attribute spreads onto the element and nothing styles it — there is no\n * `[aria-invalid]` rule in this package or in either consumer's stylesheet —\n * so three Keksdose dialogs flagged a mismatched passphrase to a screen reader\n * and painted the field exactly as if it were fine. Setting the prop sets the\n * attribute too, so the two can no longer be spelled separately. */\n invalid?: boolean;\n /** What is wrong with the value, in the caller's own words (\"That IBAN has 21\n * digits\"). Rendered under the field, pointed at by the control's\n * `aria-describedby` — MERGED with any the caller already passed — and implies\n * `invalid`, so the field paints as well as announces. `invalid` alone still\n * covers the case where the message lives elsewhere. See {@link useFieldError}. */\n error?: ReactNode;\n /** Names for the password reveal toggle, English by default — it is the one\n * string this component renders on its own behalf, and a German form was\n * reading it out in English. See {@link PasswordRevealLabels}. */\n passwordLabels?: Partial<PasswordRevealLabels>;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n className,\n inputClassName,\n label,\n id,\n placeholder,\n type,\n variant = \"field\",\n invalid,\n error,\n passwordLabels,\n ...rest\n },\n ref,\n) {\n const generated = useId();\n const fieldId = id ?? generated;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n rest[\"aria-describedby\"],\n rest[\"aria-invalid\"],\n );\n const asDisplay = useMediaQuery(PHONE_QUERY, false) && variant === \"display\";\n // Password fields get a reveal toggle so users can check what they typed.\n const isPassword = type === \"password\";\n const [revealed, setRevealed] = useState(false);\n const effectiveType = isPassword && revealed ? \"text\" : type;\n const handleClick = PICKER_TYPES.has(type ?? \"\")\n ? (e: MouseEvent<HTMLInputElement>) => {\n rest.onClick?.(e);\n try {\n // showPicker throws if unsupported or not user-activated — a click is\n // a valid activation, so this is safe; guard for older browsers.\n (e.currentTarget as HTMLInputElement & { showPicker?: () => void }).showPicker?.();\n } catch {\n /* ignore */\n }\n }\n : rest.onClick;\n const passwordText = useKitLabels(\"passwordReveal\", DEFAULT_PASSWORD_REVEAL_LABELS, passwordLabels);\n const revealToggle = isPassword ? (\n <button\n type=\"button\"\n // `tabIndex={-1}` sat here, which made this a painted, clickable control that\n // Tab stepped straight over — on the one field whose value cannot be checked by\n // looking at it. There is no mouse-only case for a reveal toggle; the people who\n // cannot see what they typed are exactly who it is for. It is a tab stop now,\n // and it needs a focus ring of its own, because FIELD_BASE's ring belongs to the\n // input underneath and stays put while focus moves onto the button on top of it.\n onClick={() => setRevealed((v) => !v)}\n // …and since it is a tab stop, a field the user may not edit must not hand the\n // keyboard a control that shows what is in it.\n disabled={rest.disabled}\n aria-label={revealed ? passwordText.hide : passwordText.show}\n aria-pressed={revealed}\n className=\"absolute inset-y-0 end-0 flex items-center rounded-e-md px-2.5 text-[var(--text-placeholder)] transition-colors hover:text-[var(--text-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--brand)] disabled:cursor-default disabled:opacity-50\"\n >\n {revealed ? <EyeOff className=\"size-4\" /> : <Eye className=\"size-4\" />}\n </button>\n ) : null;\n if (label === undefined) {\n if (!isPassword) {\n return (\n <FieldGroup errorEl={errorEl}>\n <input\n ref={ref}\n id={id}\n type={type}\n placeholder={placeholder}\n {...rest}\n // AFTER the spread, so the prop wins — but OR-ed with whatever the spread\n // carried, or setting `invalid` would have quietly deleted a caller's own\n // `aria-invalid`. The prop is the one that also paints; a bare attribute\n // still announces, which is all it ever did.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n // Likewise merged rather than replaced — see {@link useFieldError}.\n aria-describedby={describedBy}\n onClick={handleClick}\n className={cn(FIELD_BASE, className, inputClassName, isInvalid && FIELD_INVALID)}\n />\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <div className={cn(\"relative\", className)}>\n <input\n ref={ref}\n id={id}\n type={effectiveType}\n placeholder={placeholder}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_BASE, \"pe-9\", inputClassName, isInvalid && FIELD_INVALID)}\n />\n {revealToggle}\n </div>\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <FloatingField className={className} htmlFor={fieldId} label={label} srOnlyLabel={asDisplay}>\n <input\n ref={ref}\n id={fieldId}\n type={effectiveType}\n // A labelled field's placeholder is normally a single space, feeding the\n // floating label's peer-placeholder-shown trick. With the label sr-only\n // there is no float left to drive, and an empty borderless line would say\n // nothing at all — so the label text becomes the placeholder.\n placeholder={asDisplay ? (placeholder ?? (typeof label === \"string\" ? label : \" \")) : \" \"}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n onClick={handleClick}\n className={cn(\n asDisplay\n ? cn(FIELD_DISPLAY, \"text-xl font-semibold leading-snug\")\n : FLOATING_INPUT_CLASS,\n isPassword && \"pe-9\",\n inputClassName,\n isInvalid && FIELD_INVALID,\n )}\n />\n {revealToggle}\n </FloatingField>\n </FieldGroup>\n );\n});\nInput.displayName = \"Input\";\n\nexport interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, \"size\"> {\n label?: ReactNode;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`. */\n error?: ReactNode;\n /** A {@link FieldHint} for the label line — see {@link FloatingField}. */\n hint?: ReactNode;\n /**\n * `\"sm\"`: a 28px, 12px-type select for a toolbar or a table header, where the\n * 36px field stands a head taller than the buttons beside it. `\"md\"` (default)\n * is the field every form uses. Unlabelled selects only — a floating label needs\n * the tall box to float in, so a labelled Select ignores `\"sm\"`.\n *\n * A NUMBER is still the native attribute (the rows of a list box) and is passed\n * straight through, so the one HTML meaning of `size` keeps working. Above 1 (as\n * with `multiple`) the browser draws a list box, and the Select drops its chevron\n * and the end padding reserved for it.\n */\n size?: \"sm\" | \"md\" | number;\n /** Classes for the `<select>` itself. `className` styles the WRAPPER — the box\n * the chevron is positioned against — so it could set a width and nothing\n * else; this is the way to the element. See {@link Input}'s `inputClassName`. */\n selectClassName?: string;\n}\n\n// The compact select: the field's colours, a toolbar button's height. `py-0` and a\n// fixed height rather than a smaller padding, so the box is 28px whatever line\n// height the caller's type brings with it.\nconst SELECT_SM = \"h-7 py-0 ps-2 pe-7 text-xs\";\n\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(\n { className, label, id, children, invalid, error, hint, size, selectClassName, ...rest },\n ref,\n) {\n const generated = useId();\n // Only a number reaches the DOM; the two words are this component's own.\n const nativeSize = typeof size === \"number\" ? size : undefined;\n // A numeric size above 1 (or `multiple`) makes the browser draw a LIST BOX — all\n // the rows in the field, no menu to drop. The dropdown dress did not fit it: the\n // chevron was centred on a box several rows tall, sat on top of the rows' text\n // at the end edge, and `pe-9` clipped every option nine units early. A list box\n // gets the plain field instead — its own native scroll, no chevron, even padding.\n const listBox = (nativeSize !== undefined && nativeSize > 1) || Boolean(rest.multiple);\n const small = size === \"sm\" && label === undefined;\n const fieldId = id ?? generated;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n rest[\"aria-describedby\"],\n rest[\"aria-invalid\"],\n );\n // Custom chevron (native arrow hidden via appearance-none) so it sits a touch\n // in from the end border and matches both themes — feedback #223. A DISABLED\n // select has no menu to drop, so it drops the chevron too: the arrow is the one\n // thing on the control that promises a choice (Keksdose dev#474, where the\n // account type became read-only and still looked exactly like a picker).\n const chevron =\n rest.disabled || listBox ? null : (\n <FieldChevron className={small ? \"end-1.5 size-3.5\" : undefined} />\n );\n // `pe-9` is the room the chevron takes; a list box has none to make room for.\n const dress = listBox ? \"overflow-y-auto\" : \"appearance-none pe-9\";\n if (label === undefined) {\n return (\n <FieldGroup errorEl={errorEl}>\n <div className={cn(\"relative\", className)}>\n <select\n ref={ref}\n // `id` is destructured out of the props to feed `fieldId`, and this branch\n // never put it back — so an UNLABELLED Select swallowed it and the\n // consumer's own `<label for=\"…\">` pointed at nothing. The control stayed\n // in the tab order with no accessible name at all: reachable, and silent\n // when it was reached. Input and Textarea both forward it here; this is\n // the third one doing the same thing.\n id={id}\n size={nativeSize}\n {...rest}\n // OR-ed with the spread for the reason spelled out on Input's copy: this\n // branch wrote `invalid || undefined`, so passing `aria-invalid` by hand\n // to a Select — which is what a caller does when the validity is\n // `\"grammar\"` or `\"spelling\"`, or when the paint is not wanted — had the\n // attribute silently dropped. Input has never done that.\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n dress,\n small && SELECT_SM,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n {chevron}\n </div>\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <FloatingField className={className} htmlFor={fieldId} label={label} staticLabel hint={hint}>\n <select\n ref={ref}\n id={fieldId}\n size={nativeSize}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(\n FIELD_BASE,\n // The floated label takes the top strip of a list box too, so its first\n // row starts under the label rather than behind it.\n listBox ? \"pt-5 pb-1\" : FIELD_FLOATING_PAD,\n \"peer\",\n dress,\n selectClassName,\n isInvalid && FIELD_INVALID,\n )}\n >\n {children}\n </select>\n {chevron}\n </FloatingField>\n </FieldGroup>\n );\n});\nSelect.displayName = \"Select\";\n\nexport interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {\n label?: ReactNode;\n /** See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** See {@link Input}'s `error`. */\n error?: ReactNode;\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n { className, label, id, placeholder, invalid, error, ...rest },\n ref,\n) {\n const generated = useId();\n const fieldId = id ?? generated;\n const { isInvalid, describedBy, errorEl } = useFieldError(\n error,\n invalid,\n rest[\"aria-describedby\"],\n rest[\"aria-invalid\"],\n );\n if (label === undefined) {\n return (\n <FieldGroup errorEl={errorEl}>\n <textarea\n ref={ref}\n id={id}\n placeholder={placeholder}\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(FIELD_BASE, className, isInvalid && FIELD_INVALID)}\n />\n </FieldGroup>\n );\n }\n return (\n <FieldGroup errorEl={errorEl}>\n <FloatingField className={className} htmlFor={fieldId} label={label}>\n <textarea\n ref={ref}\n id={fieldId}\n placeholder=\" \"\n {...rest}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={describedBy}\n className={cn(FLOATING_INPUT_CLASS, isInvalid && FIELD_INVALID)}\n />\n </FloatingField>\n </FieldGroup>\n );\n});\nTextarea.displayName = \"Textarea\";\n\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n children: ReactNode;\n /**\n * When true, drop the card chrome (border, rounded corners, shadow) on mobile so the\n * card spans edge-to-edge. Chrome reappears at the `md` breakpoint. Use for primary\n * content cards on data pages; leave off for centered dialog/panel cards.\n */\n flush?: boolean;\n /**\n * `inset`: a panel INSIDE a card rather than a card on the page — the raised\n * `--bg-surface-2`, a small radius, `p-3` of its own and no border or shadow, since\n * it is already sitting on the card that has them. Lenkbank's results blocks and\n * corner panels write this by hand (`rounded-md bg-surface-2 p-3`) under a Card of\n * their own. Unlike the default card it carries its padding, because every one of\n * those copies wanted the same one; a caller's `p-*` still wins. `flush` is ignored:\n * an inset panel never runs edge to edge.\n */\n variant?: \"default\" | \"inset\";\n}\n\nexport function Card({ className, children, flush, variant = \"default\", ...rest }: CardProps) {\n return (\n <div\n {...rest}\n className={cn(\n variant === \"inset\"\n ? \"rounded-md bg-[var(--bg-surface-2)] p-3\"\n : cn(\n // Surface + border are theme tokens so the palette switcher (feedback\n // #307) can re-skin every card; a caller's own bg-*/border-* override\n // still wins via tailwind-merge.\n \"bg-[var(--bg-surface)]\",\n flush\n ? \"border-y border-[var(--border)] md:rounded-lg md:border md:shadow-sm\"\n : \"rounded-lg border border-[var(--border)] shadow-sm\",\n ),\n className,\n )}\n >\n {children}\n </div>\n );\n}\n\n// Composed shadcn-style Card sub-parts. `Card` stays padding-less (callers set\n// their own padding via className), so these own the padding/rhythm. Token-driven\n// so they re-skin with the palette. Use CardHeader → CardTitle/CardDescription\n// (+ optional CardAction, top-right) → CardContent → CardFooter.\n/** The sub-parts add nothing to a `<div>`'s props — they are the SAME element with a\n * `data-slot` and a padding rhythm — so each name is an alias rather than an empty\n * interface pretending to be more. They exist so a consumer's own wrapper can say\n * `CardHeaderProps` instead of `ComponentProps<typeof CardHeader>`. */\nexport type CardHeaderProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardTitleProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardDescriptionProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardActionProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardContentProps = ComponentPropsWithoutRef<\"div\">;\nexport type CardFooterProps = ComponentPropsWithoutRef<\"div\">;\n\nexport function CardHeader({ className, ...props }: CardHeaderProps) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n // Grid (not flex) so CardAction can occupy a top-right column; with no\n // action it collapses to one column and title/description stack.\n \"grid auto-rows-min items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function CardTitle({ className, ...props }: CardTitleProps) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"font-semibold leading-none\", className)}\n {...props}\n />\n );\n}\n\nexport function CardDescription({ className, ...props }: CardDescriptionProps) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-sm text-[var(--money-neutral)]\", className)}\n {...props}\n />\n );\n}\n\nexport function CardAction({ className, ...props }: CardActionProps) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\"col-start-2 row-start-1 row-span-2 self-start justify-self-end\", className)}\n {...props}\n />\n );\n}\n\nexport function CardContent({ className, ...props }: CardContentProps) {\n // `last:pb-6`, not a plain `pb-6`: CardHeader owns the top padding and CardFooter\n // owns the bottom one, so a card WITHOUT a footer had nothing closing it off and\n // its last field sat flush against the card edge (kastlan feedback: the language\n // input touching the card bottom on /profile). Scoping to `:last-child` fixes that\n // case and leaves a footered card's rhythm exactly as it was.\n return <div data-slot=\"card-content\" className={cn(\"px-6 last:pb-6\", className)} {...props} />;\n}\n\nexport function CardFooter({ className, ...props }: CardFooterProps) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6 pb-6\", className)}\n {...props}\n />\n );\n}\n\n/** A `<span>`'s props plus the words a reader hears — see {@link CardHeaderProps}.\n * The ring is drawn with a border, so there is nothing inside it to put children in. */\nexport interface SpinnerProps extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n /**\n * What the spinner means, for a screen reader. Default: `common.loading` from the\n * {@link UiKitProvider}, else \"Loading…\".\n *\n * `null` makes it DECORATIVE — no role, no text, hidden from assistive tech. Use it\n * whenever words are already there: beside visible \"Loading…\" text (otherwise it is\n * announced twice), inside a labelled button (otherwise its text joins the button's\n * name), or inside a live region of the app's own (otherwise that region and this\n * one both announce).\n */\n label?: string | null;\n}\n\n/**\n * A spinning ring that also SAYS it is loading.\n *\n * It used to be a bordered span and nothing else, so a reader met no element at all\n * where a sighted user saw the page working — or, beside an emptied list, met the\n * empty list and concluded there was nothing there. `role=\"status\"` makes it a polite\n * live region, and the text inside is what that region announces. Text rather than\n * `aria-label`: a live region's announcement is its CONTENT, and several readers\n * ignore a name on one.\n *\n * `relative` so the `sr-only` text has a local containing block (see\n * sr-only-containment.test). Where the words are already on screen, pass\n * `label={null}`: announcing is right for a spinner standing alone and wrong for one\n * beside text, inside a button, or inside the app's own live region.\n */\nexport function Spinner({ className, label, ...rest }: SpinnerProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, {\n loading: label ?? undefined,\n });\n const ring = cn(\n \"relative inline-block h-5 w-5 animate-spin rounded-full border-2 border-[var(--border)] border-t-[var(--text-primary)]\",\n className,\n );\n if (label === null) return <span aria-hidden {...rest} className={ring} />;\n return (\n <span role=\"status\" {...rest} className={ring}>\n {/* The trailing space is a separator for the case the caller forgot `label={null}`\n inside a button: a name is the concatenation of its text, and without it the\n spinner's word ran straight into the button's — \"Loadingconfirm\". */}\n <span className=\"sr-only\">{common.loading} </span>\n </span>\n );\n}\n\nexport interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"title\"> {\n /** The box renders `title` and `hint` in its own two-line rhythm, which is what makes\n * every empty state in three apps look like the same thing — so there is no\n * `children` slot to put arbitrary content in. The two slots below are the only\n * other things an empty state has turned out to need, and each has a fixed place. */\n /** Nodes rather than strings, so a title can carry a code, a link or an emphasised\n * word — keksdose's error boundary shows the error's name in `<code>` — while the\n * box still sets the type. */\n title: ReactNode;\n hint?: ReactNode;\n /**\n * Render the title as a heading of this level. Left out it is a `<div>`, as before:\n * most empty states are a message inside a section that already has its heading.\n * keksdose's error boundary IS the page when it trips, and its `<h2>` went missing\n * when it moved onto EmptyState — so the page's heading outline lost the one line\n * that says what happened. Only the element changes; the look is the box's.\n */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n /** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's\n * offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and\n * muted, so five call sites cannot pick five sizes; hidden from assistive tech,\n * because the title already says what it shows. */\n icon?: ReactNode;\n /** What to do about it, BELOW the hint: keksdose's query-state cards (Retry), its\n * error boundary (Retry / Reload / Go home — pass all three in a fragment, they\n * wrap and centre as one row), kastlan's \"Create a rule\". Buttons or links the\n * caller renders; the box only places them. */\n action?: ReactNode;\n}\n\nexport function EmptyState({ title, hint, icon, action, headingAs, className, ...rest }: EmptyStateProps) {\n const Title = headingAs ?? \"div\";\n return (\n <div\n {...rest}\n className={cn(\n \"flex flex-col items-center justify-center rounded-lg border border-dashed border-[var(--border-strong)] bg-[var(--bg-surface-2)] px-4 py-10 text-center text-sm text-[var(--text-muted)]\",\n className,\n )}\n >\n {icon != null && (\n <div aria-hidden className=\"mb-3 text-[var(--text-placeholder)] [&_svg]:size-8\">\n {icon}\n </div>\n )}\n {/* `text-sm`: a heading keeps the box's size, not whatever a stylesheet gives h2. */}\n <Title className=\"text-sm font-medium text-[var(--text-secondary)]\">{title}</Title>\n {hint != null && hint !== false && hint !== \"\" && <div className=\"mt-1 text-xs\">{hint}</div>}\n {action != null && (\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">{action}</div>\n )}\n </div>\n );\n}\n\n/** One tab of a {@link Tabs} strip. */\nexport interface TabItem<T extends string> {\n id: T;\n /** A ReactNode so a tab can pair an icon with text. */\n label: ReactNode;\n /** Optional trailing node (e.g. a count pill). */\n badge?: ReactNode;\n /** Marks a tab that IS a route — see {@link TabsProps}. */\n href?: string;\n /** Leading decoration: an icon, or the colour swatches of the lines this tab\n * draws in a chart above the strip. Hidden from assistive tech — it repeats\n * what the label says, or it says something the label must say instead. */\n icon?: ReactNode;\n /** A second, smaller line under the label — the one fact that tells two tabs\n * with similar labels apart (\"80 km/h\", \"Level 3\"). Part of the tab's name. */\n detail?: ReactNode;\n /** A tab that exists as a slot with nothing in it yet, drawn with a dashed\n * outline so a gap in a sequence looks like a gap. Visual only: say \"empty\" in\n * the label or `detail` if a screen reader needs to know. */\n empty?: boolean;\n /** `false` keeps this one tab when the strip has `onRemove` — the one sheet a\n * workbook cannot be without. Default: removable. */\n removable?: boolean;\n /** Plain-text name for the labels the strip composes about this tab (\"Remove\n * …\"), for when `label` is not a string. Default: `label` if it is a string,\n * else the id. */\n name?: string;\n}\n\n/** The strings the strip renders on its own behalf — only when it can add or\n * remove tabs. */\nexport interface TabsLabels {\n /** The \"add\" button's visible text when the caller gives no `addLabel`. */\n add: string;\n /** Accessible name of a tab's × — \"Remove <tab>\". */\n remove: (tab: string) => string;\n}\n\nexport const DEFAULT_TABS_LABELS: TabsLabels = {\n add: \"Add tab\",\n remove: (tab) => `Remove ${tab}`,\n};\n\n/**\n * The strip's own props sit on a `<div>`: the tablist IS the root element, so anything\n * a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an\n * `aria-describedby` — lands there. `onChange` is omitted from the div's props because\n * this component's `onChange` hands over the chosen TAB ID, not a DOM event, and\n * `children` because the strip renders `tabs` — a caller who wants a PANEL wires it up\n * through `panelId`, which is the whole point of that prop.\n */\nexport interface TabsProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n /** `label` is a ReactNode so tabs can pair an icon with text; `badge` is an\n * optional trailing node (e.g. a count pill). `href` marks a tab that IS a route:\n * it renders as an anchor so it can be middle-/⌘-clicked into a new tab (Keksdose\n * feedback #451), while a plain click still goes through `onChange` and stays\n * client-side. Tabs that only flip local state leave it unset — a link to a URL\n * that does not select the tab would be worse than no link. */\n tabs: TabItem<T>[];\n active: T;\n onChange: (id: T) => void;\n className?: string;\n /** Let the strip WRAP onto as many rows as it needs below `md`, instead of\n * scrolling sideways — for a strip carrying more tabs than fit on a phone row.\n * Keksdose live #262: ten reports on a 406px screen showed about three, and the\n * round-2 answer (a <select> below `md`) cost a tap to open and a tap to choose.\n * *\"I would rather keep the tabs but have multirow tabs depending on the screen\n * size, I will not loose the function to see all reports at once and navigate by\n * single click rather by double click.\"*\n *\n * The active MARKER has to change with the layout, which is why this cannot be\n * done from a call site with a `className`: the default marker is a `border-b-2`\n * underline riding the container's own bottom rule, and a tab sitting in a row\n * that does not touch that rule cannot wear it — every row but the last would\n * show a stray line floating mid-strip, and `-mb-px` would pull each chip a\n * pixel into the row beneath it. A wrapped strip marks the active tab with a\n * filled brand chip instead, which is also the only thing findable at a glance\n * among ten same-weight labels on three rows.\n *\n * From `md` up NOTHING changes: same single-line underline strip, same paddings,\n * same colours as every other strip in the app.\n *\n * Opt-in on purpose. The three-tab strips (invoices, statements, bank imports)\n * already fit a phone row, and turning those into chips would be an unrequested\n * redesign of three other pages. */\n wrap?: boolean;\n /**\n * Accessible name for the `role=\"tablist\"` container. Every tab carries its own\n * text, so this names the GROUP, not the tabs; leave it unset where a visible\n * heading immediately above already does that job.\n *\n * @deprecated Pass `aria-label` instead. This kit had three spellings for one idea\n * — `ariaLabel`, `aria-label` and this `label` — and settled on the DOM one, which\n * is also the one that arrives for free now that the strip spreads its rest props.\n * `label` still works and is unchanged; it names the strip only when `aria-label`\n * is absent.\n */\n label?: string;\n /**\n * `id` of the element the caller renders the open tab's content into — the missing\n * half of `role=\"tab\"`. A tab that controls nothing is a tab in name only: a screen\n * reader announces \"tab, 2 of 5\" and then has no way to take the user to what it\n * opened, and no way back.\n *\n * **This component does not render the panel, on purpose.** `Tabs` is the STRIP; the\n * content lives wherever the caller put it — three Keksdose pages render it in a\n * sibling `<Card>`, one renders it through a router outlet, and a tab can BE a route\n * (`href`), in which case the panel is a whole page this component never sees.\n * Wrapping `children` here would mean either moving that content into the strip's\n * subtree — a layout change on every page that uses tabs — or shipping a wrapper\n * that only some callers could use. So the two halves are joined by an id instead.\n *\n * Wire the other end yourself:\n *\n * ```tsx\n * <Tabs tabs={tabs} active={active} onChange={setActive} panelId=\"report-panel\" />\n * <div id=\"report-panel\" role=\"tabpanel\" aria-labelledby=\"report-panel-tab\" tabIndex={-1}>\n * ```\n *\n * `aria-controls` goes on the OPEN tab only, and `${panelId}-tab` is its id — the\n * closed tabs' panels are not in the document, and a dangling `aria-controls`\n * describes a tab as opening something that is not there.\n */\n panelId?: string;\n /**\n * Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab\n * (only on the open one with `removeOn=\"active\"`).\n *\n * The × is on the open tab only — a row of tabs each carrying one is a row of\n * things to hit by accident, and the tab you can remove should be the one you\n * are looking at — but its room is reserved on every removable tab, so opening a\n * tab never widens it and never pushes the rest of the strip along (lenkbank\n * feedback #72). It is named \"Remove <tab>\" and is not a tab stop of its own: the\n * strip stays one stop, and the keyboard path is Delete (announced through\n * `aria-keyshortcuts`), which removes whichever tab has focus (see `removeOn`).\n *\n * The strip only ASKS: it calls this, and the tab goes when the caller drops it\n * from `tabs` — after a confirmation, a request, whatever it needs. When it does,\n * focus that was on the removed tab (or its ×) moves to the open tab if the\n * removed one was open, else to its neighbour, instead of falling to the page.\n */\n onRemove?: (id: T) => void;\n /**\n * Which tabs the Delete key removes.\n *\n * - `\"every\"` (default): any focused removable tab — the ARIA pattern's reading.\n * - `\"active\"`: only the OPEN tab, the same one that wears the ×. For a strip whose\n * remove is immediate and unconfirmed (lenkbank's sheet tabs delete a measurement\n * sheet at once), so arrowing across the strip and pressing Delete cannot take\n * out a sheet the user is not looking at. Delete on a closed tab does nothing,\n * and only the open tab announces the shortcut (`aria-keyshortcuts`).\n *\n * Either way the × is drawn on the open tab only and its room stays reserved on\n * every removable tab, so the strip never relayouts when the selection moves.\n */\n removeOn?: \"every\" | \"active\";\n /** Renders an \"add\" button after the last tab, outside the tablist — it is an\n * action, not a tab, and a tablist may own only tabs. */\n onAdd?: () => void;\n /** The add button's text. Say what is added — \"Add loop\", \"Add report\" says\n * more than \"Add\". Default: `tabs.add` from the {@link UiKitProvider}. */\n addLabel?: string;\n /** Overrides for this strip's strings. See {@link TabsLabels}. */\n labels?: Partial<TabsLabels>;\n /** An add or a remove is in flight: the × and the add button are disabled and\n * Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */\n busy?: boolean;\n /**\n * `\"vertical\"`: the same strip as a SIDE NAV — one item per row, the open one\n * filled, ↑/↓ walking it (Home/End as before) and `aria-orientation=\"vertical\"` on\n * the tablist. For keksdose's admin page and settings page, which each hand-built\n * the identical sidebar with none of this strip's keyboard, roving tab stop or\n * routed-`href` handling. Everything else — `href` tabs, `badge`, `panelId`,\n * `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the\n * badge moves to the row's end.\n *\n * **On a phone ({@link PHONE_QUERY}) it becomes the horizontal strip**, `wrap` and\n * all, rather than staying a column. A side nav only works beside its content; on\n * a phone it has to go ABOVE it, and eight full-width rows there push the panel\n * the user picked below the fold on every visit — the two keksdose pages both\n * collapsed theirs into a scrolling row by hand for exactly that reason. The\n * orientation is switched in JavaScript, not with `md:` classes, so the\n * `aria-orientation` a screen reader hears and the arrow keys that work always\n * match the layout on screen. The caller's own two-column layout has to stack at\n * the same breakpoint (`md:grid-cols-[14rem_1fr]`).\n */\n orientation?: \"horizontal\" | \"vertical\";\n}\n\n// The two shapes are written out as whole strings rather than as one base plus a\n// pile of `md:` overrides. The unwrapped pair is character-for-character what the\n// three strips that do not opt in get, so those cannot drift; the wrapped pair is\n// authored from scratch, so no unprefixed utility has to be beaten by its own `md:`\n// twin through tailwind-merge. The only ordering this relies on is Tailwind's own:\n// unprefixed utilities are emitted first, then `md:` — so from 768px up the wrapped\n// strip resolves to exactly the paint of the default one. Both themes come free now\n// that the colours are tokens: there is no `md:dark:` tier left to keep in step.\nconst TABLIST_CLASSES =\n \"flex gap-1 overflow-x-auto overflow-y-hidden border-b border-[var(--border)]\";\n// The side nav. A column of full-width rows with the open one FILLED rather than\n// underlined: an underline under one row of a list reads as a separator, not as a\n// selection. `--bg-active` is the kit's own selected-row fill (the sidebar's), so the\n// admin page's nav and the app's own sidebar mark \"you are here\" the same way.\nconst TABLIST_VERTICAL_CLASSES = \"flex flex-col gap-0.5\";\nconst TAB_VERTICAL_CLASSES =\n \"flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]\";\nconst TAB_VERTICAL_ACTIVE_CLASSES = \"bg-[var(--bg-active)] text-[var(--text-primary)]\";\nconst TAB_VERTICAL_INACTIVE_CLASSES =\n \"text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]\";\n\nconst TABLIST_WRAP_CLASSES =\n \"flex flex-wrap gap-1.5 md:flex-nowrap md:gap-1 md:overflow-x-auto md:overflow-y-hidden md:border-b md:border-[var(--border)]\";\n\nconst TAB_CLASSES =\n \"whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]\";\nconst TAB_ACTIVE_CLASSES = \"border-[var(--text-primary)] text-[var(--text-primary)]\";\nconst TAB_INACTIVE_CLASSES =\n \"border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:border-[var(--border-strong)]\";\n\n// Below `md`, a chip: `py-2` on `text-xs` is exactly 32px tall, and ten German\n// report labels then land in three rows on a 406px screen (four at `text-sm`). The\n// active fill is the app's own selected pair, `--brand` / `--brand-contrast`: the\n// palette store writes both halves of it together for every preset and both themes,\n// so the contrast holds everywhere (5.1:1 at its worst preset) without a hard-coded\n// colour. Inactive chips take the raised card surface with full-strength body text —\n// all ten have to stay readable; it is the FILL, not the text weight, that says\n// which one is open.\nconst TAB_WRAP_CLASSES =\n \"whitespace-nowrap rounded-md px-2.5 py-2 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] md:rounded-none md:border-b-2 md:-mb-px md:bg-transparent md:px-3 md:py-2 md:text-sm\";\nconst TAB_WRAP_ACTIVE_CLASSES =\n \"bg-[var(--brand)] text-[var(--brand-contrast)] md:border-[var(--text-primary)] md:text-[var(--text-primary)]\";\nconst TAB_WRAP_INACTIVE_CLASSES =\n \"bg-[var(--bg-surface)] text-[var(--text-primary)] md:border-transparent md:text-[var(--text-muted)] md:hover:border-[var(--border-strong)] md:hover:text-[var(--text-secondary)]\";\n\n// The add/remove chrome. The × reserves its gutter on every removable tab (see\n// `onRemove`); `md:pe-8` is spelled out because the wrapped chip's `md:px-3` would\n// otherwise hand the gutter back from 768px up.\nconst TAB_REMOVABLE_CLASSES = \"pe-8 md:pe-8\";\nconst TAB_EMPTY_CLASSES = \"-mx-1.5 rounded border border-dashed border-[var(--border-strong)] px-1.5\";\nconst TAB_ADD_CLASSES =\n \"inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4\";\nconst TAB_ADD_WRAP_CLASSES = \"px-2.5 text-xs md:px-3 md:text-sm\";\n\nexport function Tabs<T extends string>({\n tabs,\n active,\n onChange,\n className,\n wrap = false,\n label,\n panelId,\n onRemove,\n removeOn = \"every\",\n onAdd,\n addLabel,\n labels,\n busy = false,\n orientation = \"horizontal\",\n \"aria-label\": ariaLabel,\n ...rest\n}: TabsProps<T>) {\n const text = useKitLabels(\"tabs\", DEFAULT_TABS_LABELS, labels);\n // See `orientation`: a vertical strip is the horizontal one on a phone.\n const phone = useMediaQuery(PHONE_QUERY, false);\n const vertical = orientation === \"vertical\" && !phone;\n const stripRef = useRef<HTMLDivElement>(null);\n const isRemovable = (tab: TabItem<T>) => onRemove !== undefined && tab.removable !== false;\n // Whether Delete reaches this tab: every removable tab, or only the open one.\n const deletesOnKey = (tab: TabItem<T>) =>\n isRemovable(tab) && (removeOn === \"every\" || tab.id === active);\n const nameOf = (tab: TabItem<T>) =>\n tab.name ?? (typeof tab.label === \"string\" ? tab.label : tab.id);\n\n // Where focus goes once a removal the strip asked for has happened. The caller\n // owns `tabs` and may take its time (a confirmation, a request), so this waits\n // for the id to actually leave the list rather than guessing at the moment.\n const pendingFocus = useRef<{ removed: T; wasActive: boolean; neighbour?: T } | null>(null);\n const requestRemove = (id: T) => {\n const index = tabs.findIndex((t) => t.id === id);\n pendingFocus.current = {\n removed: id,\n wasActive: id === active,\n neighbour: (tabs[index + 1] ?? tabs[index - 1])?.id,\n };\n onRemove?.(id);\n };\n useLayoutEffect(() => {\n const pending = pendingFocus.current;\n if (!pending || tabs.some((t) => t.id === pending.removed)) return;\n pendingFocus.current = null;\n const strip = stripRef.current;\n if (!strip) return;\n // Only focus that fell out of the strip with the removed tab is ours to place.\n // If the user has moved on — into the panel, into a dialog — leave it there.\n const focused = document.activeElement;\n if (focused && focused !== document.body && !strip.contains(focused)) return;\n // The removed tab was open: focus follows the caller's new selection, which is\n // also the strip's tab stop. It was not: its neighbour, as the ARIA pattern has it.\n const target =\n !pending.wasActive && tabs.some((t) => t.id === pending.neighbour)\n ? pending.neighbour\n : active;\n const index = tabs.findIndex((t) => t.id === target);\n strip.querySelectorAll<HTMLElement>('[role=\"tab\"]')[index]?.focus();\n }, [tabs, active]);\n\n // Arrow keys walk the strip in DOM order (ARIA tabs pattern), which is what keeps\n // a WRAPPED strip navigable: the rows flow in DOM order too, so Right off the end\n // of row one lands on the first chip of row two rather than nowhere. Home/End jump\n // to the ends. Focus only — activation stays on click/Enter/Space, because a tab\n // here can be a real route and moving focus must not navigate.\n //\n // On the TABS, not on the tablist. The container carried it, which left a `<div>`\n // wearing an interactive role and a key handler while being unfocusable —\n // `jsx-a11y/interactive-supports-focus`, and the only instance of it left in `src/`.\n // The fix that rule wants is `tabIndex` on the div; the fix the pattern wants is\n // the handler on the elements that are natively focusable and actually receive the\n // keystroke. Those are the tabs, so the warning goes away by being right rather\n // than by being satisfied.\n const onTabKeyDown = (e: KeyboardEvent<HTMLElement>, tab: TabItem<T>) => {\n // Delete removes the FOCUSED tab (the ARIA pattern's optional key for deletable\n // tabs) — the keyboard's way to the × that is not a tab stop. `busy` swallows it\n // rather than letting it fall through to the page. With `removeOn=\"active\"` only\n // the open tab answers; a closed one lets the key pass untouched.\n if (e.key === \"Delete\" && deletesOnKey(tab)) {\n e.preventDefault();\n if (!busy) requestRemove(tab.id);\n return;\n }\n // Along the reading direction: the strip is a flex row, so in RTL the NEXT tab sits\n // to the left, and ArrowLeft has to reach it. A vertical strip answers ↑/↓ instead\n // — and ONLY those, as the pattern has it: ←/→ are left to the page, where a side\n // nav's neighbour (the panel) may well want them.\n const step: 1 | -1 | 0 = vertical\n ? e.key === \"ArrowDown\"\n ? 1\n : e.key === \"ArrowUp\"\n ? -1\n : 0\n : horizontalStep(e.key, e.currentTarget);\n if (step === 0 && e.key !== \"Home\" && e.key !== \"End\") return;\n const strip = e.currentTarget.closest('[role=\"tablist\"]');\n if (!strip) return;\n const items = Array.from(strip.querySelectorAll<HTMLElement>('[role=\"tab\"]'));\n const from = items.indexOf(e.currentTarget);\n if (from === -1) return;\n e.preventDefault();\n const to =\n e.key === \"Home\"\n ? 0\n : e.key === \"End\"\n ? items.length - 1\n : (from + step + items.length) % items.length;\n items[to]?.focus();\n };\n const tablist = (\n <div\n // Before the role and the name: a caller's arbitrary attribute is welcome on the\n // strip, but a `role` or an `aria-label` arriving through a spread props object\n // must not be able to unmake the tablist the tabs below are registered against.\n {...rest}\n ref={stripRef}\n role=\"tablist\"\n // The DOM spelling wins over the deprecated `label`; see {@link TabsProps}.\n aria-label={ariaLabel ?? label}\n // Only when vertical: `horizontal` is the tablist's implicit value, and the\n // attribute on every existing strip would be noise in every snapshot of them.\n aria-orientation={vertical ? \"vertical\" : undefined}\n // With an add button the strip gains an outer box, and `className` goes there\n // — it is the box a caller's margin or width is meant for.\n className={cn(\n vertical ? TABLIST_VERTICAL_CLASSES : wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,\n onAdd ? \"min-w-0\" : className,\n )}\n >\n {tabs.map((tab) => {\n const isActive = tab.id === active;\n const removable = isRemovable(tab);\n // Written once and worn by either tag below, so a routed tab and a\n // state-only tab stay indistinguishable to the eye and to a screen reader.\n const shared = {\n role: \"tab\" as const,\n \"aria-selected\": isActive,\n // Roving tabindex: the whole strip is ONE stop in the page's tab order, and\n // it is the open tab. Ten report tabs otherwise cost ten Tab presses to step\n // over on the way to the table below them (live #262's strip is the extreme\n // case, but every strip in the app paid it).\n //\n // The stop follows the SELECTION, not the focus. Arrowing is a look around —\n // activation here is manual, because a tab can be a real route — so re-\n // pointing the stop at a report the user only arrowed past would hand the\n // strip back on the next visit in a state they never chose. The cost is that\n // Tab out and back returns to the open tab rather than to the last one\n // looked at; the ARIA pattern allows either, and this one needs no second\n // piece of state shadowing `active`.\n tabIndex: isActive ? 0 : -1,\n // Only the open tab: see `panelId`.\n \"aria-controls\": isActive ? panelId : undefined,\n id: isActive && panelId ? `${panelId}-tab` : undefined,\n \"aria-keyshortcuts\": deletesOnKey(tab) ? \"Delete\" : undefined,\n onKeyDown: (e: KeyboardEvent<HTMLElement>) => onTabKeyDown(e, tab),\n className: vertical\n ? cn(\n TAB_VERTICAL_CLASSES,\n isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,\n removable && \"pe-8\",\n )\n : cn(\n wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,\n wrap\n ? isActive\n ? TAB_WRAP_ACTIVE_CLASSES\n : TAB_WRAP_INACTIVE_CLASSES\n : isActive\n ? TAB_ACTIVE_CLASSES\n : TAB_INACTIVE_CLASSES,\n removable && TAB_REMOVABLE_CLASSES,\n ),\n };\n const inner = (\n <span\n className={cn(\n // In a side nav the row is the full width, so the label takes it and a\n // badge lands at the row's end, where a column of counts lines up.\n vertical ? \"flex min-w-0 flex-1 items-center gap-1.5\" : \"inline-flex items-center gap-1.5\",\n tab.empty && TAB_EMPTY_CLASSES,\n )}\n >\n {tab.icon != null && (\n <span aria-hidden className=\"inline-flex shrink-0 items-center gap-0.5\">\n {tab.icon}\n </span>\n )}\n {tab.detail != null ? (\n <span className=\"flex flex-col items-start text-start\">\n <span>{tab.label}</span>\n {/* A separator for the NAME only — whitespace between flex items is\n not drawn, and without it the two lines ran together into one\n word for a screen reader (\"80 km/hloop 2\"). */}{\" \"}\n <span className=\"text-[11px] font-normal leading-tight text-[var(--text-muted)]\">\n {tab.detail}\n </span>\n </span>\n ) : (\n tab.label\n )}\n {tab.badge != null ? (\n vertical ? <span className=\"ms-auto flex shrink-0 items-center\">{tab.badge}</span> : tab.badge\n ) : null}\n </span>\n );\n const element = tab.href ? (\n <a\n key={tab.id}\n {...shared}\n href={tab.href}\n draggable={false}\n onClick={(e) => {\n // Middle-, ⌘/Ctrl-, Shift- and Alt-click belong to the browser\n // (feedback #451); only the plain left click is ours to cancel and\n // route through `onChange`, which keeps the switch client-side.\n if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n e.preventDefault();\n onChange(tab.id);\n }}\n // A <button> tab activated on Space; an anchor does not, so becoming a\n // link (feedback #451) would have quietly cost keyboard users the tab\n // strip. Enter is deliberately untouched — it is the anchor's own\n // default and already arrives at `onChange` through the click handler\n // above, so handling it here too would switch tabs twice. Same three\n // lines the mobile row card carries, for the same reason; the rejected\n // alternative — keeping a button and faking the middle click with\n // window.open — is what the whole item is moving away from.\n //\n // Composed with the strip's arrow-key walk rather than replacing it: this\n // prop overrides the one in `shared`, and an anchor tab that lost the\n // arrows would be a strip that is navigable only where it is not a link.\n onKeyDown={(e) => {\n onTabKeyDown(e, tab);\n if (e.defaultPrevented || e.key !== \" \") return;\n e.preventDefault();\n onChange(tab.id);\n }}\n >\n {inner}\n </a>\n ) : (\n <button key={tab.id} {...shared} onClick={() => onChange(tab.id)}>\n {inner}\n </button>\n );\n if (!removable) return element;\n return (\n // The × cannot go INSIDE the tab: a button in a button is invalid HTML, and\n // a tab's children are presentational, so a reader would flatten it into the\n // tab's name. It sits beside the tab, over the gutter the tab reserves.\n <div key={tab.id} className={cn(\"relative flex\", !vertical && \"shrink-0\")}>\n {element}\n {isActive && (\n <IconButton\n size=\"2xs\"\n tone=\"danger\"\n // Not a tab stop: the strip is one, and Delete is the keyboard's path.\n // Still a named button, so a screen reader's cursor and a touch\n // reader's swipe both reach it.\n tabIndex={-1}\n type=\"button\"\n aria-label={text.remove(nameOf(tab))}\n disabled={busy}\n onClick={() => requestRemove(tab.id)}\n className={cn(\n \"absolute inset-y-0 end-1 my-auto\",\n // On a wrapped strip's filled chip the quiet grey would vanish into\n // the brand fill below `md`.\n wrap && \"text-[var(--brand-contrast)] md:text-[var(--text-placeholder)]\",\n )}\n >\n <X />\n </IconButton>\n )}\n </div>\n );\n })}\n </div>\n );\n if (!onAdd) return tablist;\n if (vertical) {\n // Under the column, not beside it, and with no rule to carry on: a side nav has none.\n return (\n <div className={cn(\"flex flex-col gap-0.5\", className)}>\n {tablist}\n <button type=\"button\" onClick={onAdd} disabled={busy} className={TAB_ADD_CLASSES}>\n <Plus aria-hidden />\n {addLabel ?? text.add}\n </button>\n </div>\n );\n }\n return (\n <div\n className={cn(\n wrap ? \"flex flex-wrap items-end gap-1.5 md:flex-nowrap md:gap-0\" : \"flex items-end\",\n className,\n )}\n >\n {tablist}\n {/* Outside the tablist: an action, not a tab — and a tablist may own only\n tabs. The rule under the strip carries on beneath it. */}\n <div\n className={cn(\n \"flex flex-1 items-center\",\n wrap\n ? \"md:self-stretch md:border-b md:border-[var(--border)]\"\n : \"self-stretch border-b border-[var(--border)]\",\n )}\n >\n <button\n type=\"button\"\n onClick={onAdd}\n disabled={busy}\n className={cn(TAB_ADD_CLASSES, wrap && TAB_ADD_WRAP_CLASSES)}\n >\n <Plus aria-hidden />\n {addLabel ?? text.add}\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["IconButton","Input","Select","Textarea"]}
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { SortState } from './data-table-sort.js';
3
- import { b as DataTableColumn, F as FilterState } from '../data-table-filters-CF1PXqjQ.js';
3
+ import { b as DataTableColumn, F as FilterState } from '../data-table-filters-Dh9uF_S-.js';
4
4
  import './swipeable-row.js';
5
5
  import './data-table-labels.js';
6
6