@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/date-picker.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ComponentType, ReactNode, RefObject } from \"react\";\nimport { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { addDaysIso, parseIsoDate } from \"../lib/dates\";\nimport {\n DEFAULT_DATE_PICKER_LABELS,\n useKitLabels,\n useKitLocale,\n type DatePickerLabels,\n} from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { Button, FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport { MiniCalendar, type MiniCalendarProps } from \"./mini-calendar\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * What both pickers share — and, through `ComponentPropsWithoutRef<\"div\">`, everything a\n * `<div>` takes, so a `data-tour` anchor, a test id or an `aria-describedby` reaches the\n * field's root element instead of being dropped (audit §api-design).\n *\n * `onChange` is omitted from those attributes: the DOM event of that name carries a\n * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own\n * below.\n */\ninterface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** BCP 47 tag for the trigger's date and the calendar. Falls back to\n * `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n placeholder?: string;\n /** Inclusive ISO bounds for selectable days. */\n min?: string;\n max?: string;\n /** Show an X to clear the value (emits `\"\"`). */\n clearable?: boolean;\n /** Accessible name of the clear button. Wins over `datePicker.clear` from\n * `<UiKitProvider labels>`; kept as a prop because it predates the provider. */\n clearLabel?: string;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n className?: string;\n /** Intl options for the trigger's formatted date (default: locale short date). */\n formatOptions?: Intl.DateTimeFormatOptions;\n /** Render the trigger's text for an ISO date yourself; wins over `formatOptions`.\n * For a host whose date rendering is more than one `Intl` call can say — Keksdose\n * puts the weekday's name in the UI language beside digits ordered by a separate\n * format preference, two locales in one string. `locale` still drives the calendar. */\n formatValue?: (iso: string) => string;\n /** Passed through to the {@link MiniCalendar} this opens. Its month arrows are\n * icon-only, so their `aria-label` is the only name they have — and the package\n * ships English defaults, which a translating host has to be able to replace.\n * Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */\n calendarLabels?: MiniCalendarProps[\"labels\"];\n}\n\n/** Format an ISO date for the trigger. `lib/dates`' `formatIsoDate` insists on a\n * locale, and here there may be none — no prop and no provider — which `Intl` reads\n * as \"the runtime's default\", as every other kit formatter does. */\nfunction formatDate(iso: string, locale: string | undefined, options?: Intl.DateTimeFormatOptions) {\n return parseIsoDate(iso)?.toLocaleDateString(locale, options) ?? \"\";\n}\n\n/**\n * The field-shaped button that opens the calendar.\n *\n * A component of its own rather than JSX inline in `Popover`'s `trigger` render prop,\n * because handing focus back to it is an effect and a render prop is not somewhere a\n * hook can go.\n */\nfunction DateFieldTrigger({\n open,\n toggle,\n triggerRef,\n triggerText,\n hasValue,\n labelledBy,\n valueId,\n padded,\n disabled,\n invalid,\n panelId,\n aria,\n}: {\n /** The caller's naming/description attributes, routed here from the field. */\n aria: TriggerAria;\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n /** The panel's id — `role=\"combobox\"` requires `aria-controls`. */\n panelId: string;\n triggerText: string;\n hasValue: boolean;\n /** ids of the hidden label twin + the value span, in the order they are spoken. */\n labelledBy?: string;\n valueId: string;\n padded: boolean;\n disabled?: boolean;\n invalid?: boolean;\n}) {\n const wasOpen = useRef(false);\n useEffect(() => {\n const justClosed = wasOpen.current && !open;\n wasOpen.current = open;\n if (!justClosed) return;\n // Only when the panel still HAD focus. The day the user was standing on has just\n // been unmounted with the panel, which drops focus to <body> and leaves a\n // keyboard user back at the top of the document with no idea where they were.\n // But an outside click closes this panel too, and that click has already given\n // focus to whatever it landed on — dragging it back here would be worse than the\n // bug, so a focus that is somewhere real is left alone.\n const active = document.activeElement;\n if (!active || active === document.body) triggerRef.current?.focus();\n // Here rather than through `useFocusTrap`'s `restoreFocus`, and not only because\n // `Popover` runs no trap to carry it: that option restores to whatever held focus\n // when the panel opened, and on Safari — and on Firefox on macOS — clicking a\n // button does not focus it, so \"whatever held focus\" is the page. The requirement\n // is this trigger, every time, however the panel was opened.\n }, [open, triggerRef]);\n\n return (\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n onClick={toggle}\n // The name has to carry the VALUE. A bare `aria-label={label}` — which is what\n // this was — replaces the button's text outright, so the one field in the form\n // whose entire job is to show a date announced \"Due date\" and stopped there,\n // and a screen-reader user could not read back what they had picked without\n // opening the calendar and hunting for the selected day.\n //\n // Two ids rather than one composed string (`common.fieldValue`): \"label: value\"\n // is a sentence, and a name reference is spoken as the two texts in order with\n // no punctuation to translate at all. The hidden twin of the FieldLabel lives\n // in DateField, next to the label it copies.\n id={aria.id}\n aria-labelledby={aria[\"aria-label\"] && !aria[\"aria-labelledby\"] ? undefined : labelledBy}\n aria-label={aria[\"aria-label\"]}\n aria-describedby={aria[\"aria-describedby\"]}\n // `role=\"combobox\"` on a button that opens a calendar is the APG date-picker\n // shape, and it is what makes the next two lines legal: `button` supports\n // neither `aria-expanded` nor `aria-invalid`, so the previous markup set an\n // invalid state that announced nothing (ESLint's `role-supports-aria-props`).\n //\n // `aria-haspopup=\"dialog\"` is now honest — it was not when this comment first\n // said so. `Popover` gained a real `role=\"dialog\"` in the same wave, so the\n // trigger's promise and the panel's role finally agree.\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n aria-controls={panelId}\n aria-expanded={open}\n aria-invalid={invalid || aria[\"aria-invalid\"] === true || aria[\"aria-invalid\"] === \"true\" || undefined}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n padded && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n invalid && FIELD_INVALID,\n )}\n >\n <span id={valueId} className={cn(\"truncate\", !hasValue && \"text-[var(--text-placeholder)]\")}>\n {/* `|| \" \"` (a NON-BREAKING space) — triggerText is \"\" when there is no value and no\n placeholder was passed. An empty span has no line box, so the\n trigger collapses to its padding and sits shorter than every\n other field beside it.\n\n It has to be a NON-BREAKING space. This read `|| \" \"` first, and\n that fix did nothing at all: an ordinary space is collapsible\n white space, and white space at the start and end of a line is\n removed — so the span rendered with height 0 and the trigger\n stayed 22px against the 42px of the fields either side of it\n (Keksdose live #294, measured in the browser: `span.textContent`\n WAS `\" \"` and `getBoundingClientRect().height` was 0). U+00A0 is\n not collapsible, so it holds the line. */}\n {triggerText || \" \"}\n </span>\n </button>\n );\n}\n\n/** A field-styled trigger that opens a portalled MiniCalendar. Shared by the\n * single- and range-date pickers below. */\nfunction DateField({\n triggerText,\n hasValue,\n label,\n disabled,\n clearable,\n clearLabel,\n panelLabel,\n className,\n width,\n onClear,\n invalid,\n children,\n ...rest\n}: Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> & {\n triggerText: string;\n hasValue: boolean;\n label?: ReactNode;\n disabled?: boolean;\n clearable?: boolean;\n /** Already resolved by the picker (prop > provider > English). */\n clearLabel: string;\n /** The popover panel's accessible name, resolved likewise. */\n panelLabel: string;\n invalid?: boolean;\n className?: string;\n /** Popover panel width; omit for the default (a bare calendar). */\n width?: number;\n onClear: () => void;\n children: (close: () => void) => ReactNode;\n}) {\n const showClear = Boolean(clearable && hasValue && !disabled);\n const [aria, wrapperRest] = splitTriggerAria(rest);\n // The panel is portalled to <body> and leaves the subtree whose `dir` it inherited,\n // so a calendar in an RTL form opened LTR — the grid's columns, its arrow keys and\n // its chevrons all the wrong way round. Read at the moment of opening (the only way\n // the popover opens is through `toggle`) and put back on the panel.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const id = useId();\n const labelId = `${id}-label`;\n const valueId = `${id}-value`;\n const panelId = `${id}-panel`;\n // Only a STRING label can be reused as a name; a ReactNode may be a whole row with\n // its own interactive \"?\" hint in it, and naming a button with that reads the hint\n // out as part of the field's name.\n const named = typeof label === \"string\";\n return (\n // The caller's attributes land here, on the field's own box — the trigger inside is\n // named by `aria-labelledby` and must keep the id pair it is given.\n <div {...wrapperRest} ref={rootRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names\n nothing on its own — this hidden twin is what the trigger is named by.\n `aria-hidden` because a name reference reads a hidden element deliberately\n (accname §4.3.1), and without it the label would be announced twice: once as\n loose text before the button, once inside the button's own name.\n `sr-only-fixed` rather than `sr-only` — see the note on the class in\n tokens.css. */}\n {named && (\n <span id={labelId} aria-hidden className=\"sr-only-fixed\">\n {label}\n </span>\n )}\n <Popover\n width={width}\n panelId={panelId}\n // Named for what it is. Unnamed, it fell back to `popover.panel`, and a date\n // field announced its calendar as \"Popover\" — in English, in every language.\n labels={{ panel: panelLabel }}\n dir={dir}\n trigger={({ open, toggle, ref }) => (\n <DateFieldTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n triggerText={triggerText}\n hasValue={hasValue}\n // ALWAYS a name reference, never `undefined`. A `<button>` takes its\n // accessible name from its contents, so an unlabelled trigger used to\n // announce its own date text for free. `role=\"combobox\"` does not —\n // content is excluded from name computation for that role — so leaving\n // this undefined made an unlabelled date field announce nothing at all.\n // Named: \"label, value\". Unnamed: the value alone, which is what the\n // button was saying before.\n //\n // A caller's own reference comes first. And when the caller gives the field\n // an `id` (so an external `<label htmlFor>` can point at it), the trigger\n // lists ITSELF first: a self-reference in `aria-labelledby` is resolved\n // from the element's native label (accname 2B → 2D), so \"Due date\" from\n // that <label> is spoken before the value, instead of being overridden.\n labelledBy={[\n aria[\"aria-labelledby\"],\n named ? labelId : !aria[\"aria-labelledby\"] && aria.id ? aria.id : undefined,\n valueId,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria={aria}\n panelId={panelId}\n valueId={valueId}\n padded={label !== undefined}\n disabled={disabled}\n invalid={invalid}\n />\n )}\n >\n {(close) => children(close)}\n </Popover>\n {showClear ? (\n <button\n type=\"button\"\n aria-label={clearLabel}\n onClick={onClear}\n className=\"absolute end-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <Calendar\n aria-hidden\n className=\"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\"\n />\n )}\n </div>\n );\n}\n\n/** One ‹ / › day-step button, sized to sit flush beside the field. */\nfunction StepButton({\n icon: Icon,\n label,\n disabled,\n onClick,\n mirror,\n className,\n}: {\n icon: ComponentType<{ className?: string }>;\n label: string;\n disabled: boolean;\n onClick: () => void;\n /** Flip the icon in RTL — for the ‹ › day steps, whose \"previous\" points to the\n * reading start. Not for the today icon, which has no direction. */\n mirror?: boolean;\n /** The button's corners in the joined group (see DatePicker). */\n className?: string;\n}) {\n return (\n <Tooltip label={label} portal>\n <button\n type=\"button\"\n aria-label={label}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n FIELD_BASE,\n // FIELD_BASE is `block w-full` for text inputs; a step button is neither, and\n // px-3 would make it wider than it needs to be. twMerge lets these win.\n \"relative flex h-full w-10 shrink-0 items-center justify-center px-0 text-[var(--text-muted)]\",\n \"hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\",\n // Raised while hovered or focused, so the shared border and the focus ring are\n // drawn on top of the neighbour they overlap by a pixel.\n \"hover:z-10 focus-visible:z-10\",\n // Dimmed ICON, not a dimmed box: at 40% opacity the whole button — border\n // included — faded out of the group and read as a broken, detached control.\n \"disabled:cursor-not-allowed disabled:text-[var(--text-placeholder)] disabled:opacity-60 disabled:hover:bg-[var(--bg-surface)]\",\n className,\n )}\n >\n <Icon className={cn(\"size-4\", mirror && \"rtl:-scale-x-100\")} />\n </button>\n </Tooltip>\n );\n}\n\nexport interface DatePickerProps extends DatePickerBaseProps {\n /** ISO \"YYYY-MM-DD\", or \"\" for empty. */\n value: string;\n onChange: (iso: string) => void;\n /**\n * Flank the field with ‹ › buttons that move the value one day. For fields whose\n * typical edit is a day or two — opening a calendar to go \"yesterday\" is three\n * interactions for what should be one.\n *\n * Off by default: on a filter or a far-away date the buttons are dead weight, and\n * they cost ~5rem of width that a narrow layout may not have.\n */\n step?: boolean;\n /** Accessible names for the step buttons. Optional: they resolve from\n * `datePicker.previousDay` / `nextDay` in `<UiKitProvider labels>`, then from the\n * English defaults. This prop wins over both, for the one field that says it\n * differently. */\n stepLabels?: { prev: string; next: string };\n /**\n * Today's date as ISO \"YYYY-MM-DD\". Adds a jump-to-today button beside the field,\n * disabled once the value already is that day.\n *\n * Passed in rather than read from a clock here: a component that decides what \"today\"\n * is cannot be tested at a fixed date, and the consumer already has one definition of\n * it. The button is always RENDERED — going flat instead of disappearing — because a\n * control that comes and goes with the value reflows whatever sits beside it, which\n * is the report this exists to answer.\n */\n today?: string;\n /** Accessible name for the today button; wins over `datePicker.today`. */\n todayLabel?: string;\n}\n\n/**\n * The picker's own label props, folded into one `datePicker` override so they sit at\n * the top of the same precedence chain as everything else: prop > provider > English.\n * They predate the provider and stay public API, which is why they are not simply\n * replaced by a `labels` prop.\n */\nfunction useDatePickerLabels(\n clearLabel: string | undefined,\n stepLabels: { prev: string; next: string } | undefined,\n todayLabel: string | undefined,\n): DatePickerLabels {\n const prev = stepLabels?.prev;\n const next = stepLabels?.next;\n const fromProps = useMemo(() => {\n const out: Partial<DatePickerLabels> = {};\n if (clearLabel !== undefined) out.clear = clearLabel;\n if (prev !== undefined) out.previousDay = prev;\n if (next !== undefined) out.nextDay = next;\n if (todayLabel !== undefined) out.today = todayLabel;\n return out;\n }, [clearLabel, prev, next, todayLabel]);\n return useKitLabels(\"datePicker\", DEFAULT_DATE_PICKER_LABELS, fromProps);\n}\n\n/** Single-date picker: a field showing the formatted date, opening a calendar. */\nexport function DatePicker({\n value,\n onChange,\n locale: localeProp,\n placeholder,\n min,\n max,\n formatOptions,\n formatValue,\n step,\n stepLabels,\n today,\n todayLabel,\n className,\n calendarLabels,\n label,\n clearable,\n clearLabel,\n disabled,\n invalid,\n ...rest\n}: DatePickerProps) {\n const locale = useKitLocale(localeProp);\n const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);\n const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));\n // With a step or a today button beside it the field is no longer the outermost\n // element — the flex row at the bottom is, and a caller's attributes belong on\n // whichever of the two is actually the root. Naming the shared props rather than\n // letting `...rest` carry them is what makes that choice possible.\n const wrapped = Boolean(step || today);\n const [triggerAria, wrapperRest] = splitTriggerAria(rest);\n const field = (\n <DateField\n // With step/today buttons the flex row is the root and takes the caller's props —\n // except the ones that name the field, which still belong on its trigger.\n {...(wrapped ? triggerAria : rest)}\n label={label}\n clearable={clearable}\n clearLabel={text.clear}\n panelLabel={text.panel}\n disabled={disabled}\n invalid={invalid}\n className={step ? \"min-w-0 flex-1\" : className}\n hasValue={Boolean(value)}\n triggerText={value ? render(value) : (placeholder ?? \"\")}\n onClear={() => onChange(\"\")}\n >\n {(close) => (\n <MiniCalendar\n mode=\"single\"\n focusOnOpen\n from={value}\n to={value}\n locale={locale}\n min={min}\n max={max}\n labels={calendarLabels}\n onSelect={(iso) => {\n onChange(iso);\n close();\n }}\n />\n )}\n </DateField>\n );\n if (!wrapped) return field;\n\n // An empty field has nothing to step from, so both buttons are dead until a date\n // is picked. Bounds are compared as strings: \"YYYY-MM-DD\" sorts chronologically.\n const target = (days: number) => (value ? addDaysIso(value, days) : \"\");\n const outOfBounds = (iso: string) => Boolean((min && iso < min) || (max && iso > max));\n const blocked = (days: number) => {\n const next = target(days);\n if (!next || disabled) return true;\n return outOfBounds(next);\n };\n return (\n // ONE joined control, not three boxes in a row: the buttons and the field share\n // their borders (each piece overlaps the previous by a pixel) and only the outer\n // ends are rounded — the button-group shape, so ‹ date › reads as one field with\n // controls rather than a field with loose buttons parked beside it, which is what\n // the `gap-1` version looked like.\n //\n // items-stretch, not items-center: the buttons match the field's height, which\n // varies with whether it carries a floating label. The field's trigger is nested\n // inside DateField, so its inner corners are squared from here.\n <div\n {...wrapperRest}\n className={cn(\n \"flex items-stretch [&>*:not(:first-child)]:-ms-px\",\n step && \"[&_[role=combobox]]:rounded-s-none\",\n (step || today) && \"[&_[role=combobox]]:rounded-e-none\",\n \"[&_[role=combobox]:focus-visible]:relative [&_[role=combobox]:focus-visible]:z-10\",\n className,\n )}\n >\n {step && (\n <StepButton\n icon={ChevronLeft}\n mirror\n label={text.previousDay}\n disabled={blocked(-1)}\n onClick={() => onChange(target(-1))}\n className=\"rounded-e-none\"\n />\n )}\n {field}\n {step && (\n <StepButton\n icon={ChevronRight}\n mirror\n label={text.nextDay}\n disabled={blocked(1)}\n onClick={() => onChange(target(1))}\n className={cn(\"rounded-s-none\", today && \"rounded-e-none\")}\n />\n )}\n {today && (\n <StepButton\n icon={CalendarClock}\n label={text.today}\n disabled={Boolean(disabled) || value === today || outOfBounds(today)}\n onClick={() => onChange(today)}\n className=\"rounded-s-none\"\n />\n )}\n </div>\n );\n}\n\nexport interface DateRangePickerPreset {\n /**\n * The preset's identity, independent of its dates — `\"last_3_months\"`, not two ISO\n * strings. Optional for the 0.x callers that pass anonymous pairs.\n *\n * keksdose's report range field (report-range-field.tsx, \"Why this is app-level\")\n * is why it exists: its URL stores `?preset=last_3_months` and recomputes the dates\n * from today on every read, so a preset that is only its dates froze \"last 3 months\"\n * at whatever day the panel rendered, and two presets that happen to cover the same\n * days (this week and today, on a Monday) could not be told apart. With an `id` the\n * choice is reported to `onChange` and shown as selected for as long as the range\n * still matches it. Recompute `from`/`to` on every render (`calendarMonthPresets()`,\n * `dateRangePresets()`) and pass the key as the id.\n */\n id?: string;\n label: ReactNode;\n from: string;\n to: string;\n}\n\n/**\n * When the picker hands its range to `onChange`.\n *\n * - `\"immediate\"` (the default, and all of 0.7): a preset click commits and closes,\n * and each calendar click is reported as it happens — the half-made `(from, \"\")`\n * included. The right shape for quick ranges on a filter, where one click is one\n * answer.\n * - `\"apply\"`: the panel works on a DRAFT. A preset ARMS the calendar instead of\n * committing, days can then be nudged (\"last 3 months, but end it last Friday\"),\n * and nothing reaches `onChange` until Apply — which stays disabled until both ends\n * exist. Cancel, Escape and an outside click discard the draft, and every open\n * starts again from the committed value. keksdose's report range asked for exactly\n * this: each commit there re-runs a whole page of charts, so an intermediate state\n * must never escape the panel.\n */\nexport type DateRangeCommit = \"immediate\" | \"apply\";\n\nexport interface DateRangePickerProps extends DatePickerBaseProps {\n from: string;\n to: string;\n /** The committed range. `presetId` is the chosen preset's `id` when a preset\n * produced it (and the range was not nudged since), `undefined` for days picked by\n * hand. A 0.x two-argument handler simply ignores it. */\n onChange: (from: string, to: string, presetId?: string) => void;\n /** Separator between the two formatted dates in the trigger (default \" – \"). */\n separator?: string;\n /** Optional named ranges rendered as a start-side column (last month / YTD / …). */\n presets?: DateRangePickerPreset[];\n /**\n * The active preset's `id`, controlled — e.g. straight from keksdose's `?preset=`.\n * `null` (or an id no preset has, such as `\"custom\"`) says \"no preset\": nothing in\n * the column is marked even if a preset happens to cover the same days. A preset is\n * marked only while its dates still equal `from`/`to`.\n *\n * Leave it `undefined` and the picker remembers the last preset it committed\n * itself; with no memory at all it falls back to the first preset whose dates match.\n */\n preset?: string | null;\n /** See {@link DateRangeCommit}. Default `\"immediate\"`. */\n commit?: DateRangeCommit;\n}\n\ninterface RangeDraft {\n from: string;\n to: string;\n presetId: string | undefined;\n}\n\n/** Which preset (by index) the column marks, or -1. See `DateRangePickerProps.preset`. */\nfunction markedPreset(\n presets: readonly DateRangePickerPreset[],\n from: string,\n to: string,\n activeId: string | null | undefined,\n controlled: boolean,\n): number {\n const matches = (p: DateRangePickerPreset) => p.from === from && p.to === to;\n if (activeId != null) {\n const byId = presets.findIndex((p) => p.id === activeId && matches(p));\n if (byId !== -1 || controlled) return byId;\n } else if (controlled) {\n return -1;\n }\n return presets.findIndex(matches);\n}\n\n/**\n * The preset column: a named group of toggle buttons, `aria-pressed` on the marked one.\n * Pressed-state rather than colour alone, because \"which range is this\" is the one\n * thing a screen-reader user opening the panel needs to hear.\n */\nfunction PresetColumn({\n presets,\n marked,\n label,\n onPick,\n}: {\n presets: readonly DateRangePickerPreset[];\n marked: number;\n label: string;\n onPick: (p: DateRangePickerPreset) => void;\n}) {\n return (\n // `border-e`/`pe`, not `-r`: the preset column is on the START side, and the rule\n // between it and the calendar has to follow it in a right-to-left UI.\n <div\n role=\"group\"\n aria-label={label}\n className=\"flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2\"\n >\n {presets.map((p, i) => {\n const selected = i === marked;\n return (\n <button\n key={p.id ?? i}\n type=\"button\"\n aria-pressed={selected}\n data-preset={p.id}\n onClick={() => onPick(p)}\n className={cn(\n \"rounded px-2 py-1.5 text-start text-xs\",\n selected\n ? \"bg-[var(--bg-active)] font-medium text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n )}\n >\n {p.label}\n </button>\n );\n })}\n </div>\n );\n}\n\n/**\n * The panel's body. A component of its own so `commit=\"apply\"` can keep its draft in\n * state that is born when the panel opens: `Popover` unmounts its children on close,\n * so the draft is re-seeded from the committed value on every open, and an abandoned\n * half-selection cannot survive into the next one.\n */\nfunction RangePanel({\n from,\n to,\n presets,\n activeId,\n controlled,\n commit,\n close,\n commitRange,\n calendarProps,\n labels,\n}: {\n from: string;\n to: string;\n presets: readonly DateRangePickerPreset[] | undefined;\n activeId: string | null | undefined;\n controlled: boolean;\n commit: DateRangeCommit;\n close: () => void;\n commitRange: (from: string, to: string, presetId: string | undefined) => void;\n calendarProps: Pick<MiniCalendarProps, \"locale\" | \"min\" | \"max\" | \"labels\">;\n labels: DatePickerLabels;\n}) {\n const drafting = commit === \"apply\";\n const [draft, setDraft] = useState<RangeDraft>({\n from,\n to,\n presetId: activeId ?? undefined,\n });\n // What the panel SHOWS: the draft while drafting, the committed value otherwise.\n const shown = drafting ? draft : { from, to, presetId: activeId ?? undefined };\n const marked = presets\n ? markedPreset(presets, shown.from, shown.to, drafting ? draft.presetId : activeId, drafting || controlled)\n : -1;\n const complete = Boolean(draft.from && draft.to);\n\n const calendar = (\n <MiniCalendar\n {...calendarProps}\n focusOnOpen\n from={shown.from}\n to={shown.to}\n onSelect={(f, t) => {\n if (drafting) {\n setDraft({ from: f, to: t, presetId: undefined });\n return;\n }\n commitRange(f, t, undefined);\n // Two-click range: only dismiss once both ends are chosen.\n if (f && t) close();\n }}\n />\n );\n\n const body =\n presets && presets.length > 0 ? (\n <div className=\"flex gap-3\">\n <PresetColumn\n presets={presets}\n marked={marked}\n label={labels.presets}\n onPick={(p) => {\n if (drafting) {\n setDraft({ from: p.from, to: p.to, presetId: p.id });\n return;\n }\n commitRange(p.from, p.to, p.id);\n close();\n }}\n />\n {/* `flex-1`: the calendar takes the rest of the panel. Content-sized it was\n seven tiny cells beside a wide preset column. */}\n <div className=\"min-w-0 flex-1\">{calendar}</div>\n </div>\n ) : (\n calendar\n );\n\n if (!drafting) return body;\n return (\n <div className=\"flex flex-col gap-2\">\n {body}\n <div className=\"flex items-center justify-end gap-2 border-t border-[var(--border)] pt-2\">\n <Button type=\"button\" variant=\"ghost\" onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"button\"\n // A half-made `(from, \"\")` would commit a window with no end — a blank\n // report, one click into a two-click gesture.\n disabled={!complete}\n onClick={() => {\n if (!complete) return;\n commitRange(draft.from, draft.to, draft.presetId);\n close();\n }}\n >\n {labels.apply}\n </Button>\n </div>\n </div>\n );\n}\n\n/** Two-date range picker: click a start then an end; closes once both are set.\n * With `presets`, a column of named ranges is shown beside the calendar; with\n * `commit=\"apply\"` the panel drafts and commits on Apply (see {@link DateRangeCommit}). */\nexport function DateRangePicker({\n from,\n to,\n onChange,\n locale: localeProp,\n placeholder,\n min,\n max,\n formatOptions,\n formatValue,\n separator = \" – \",\n presets,\n preset,\n commit = \"immediate\",\n calendarLabels,\n label,\n clearable,\n clearLabel,\n disabled,\n invalid,\n ...rest\n}: DateRangePickerProps) {\n const locale = useKitLocale(localeProp);\n const text = useDatePickerLabels(clearLabel, undefined, undefined);\n const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));\n // The preset this picker committed itself, for a caller that does not control\n // `preset`. Cleared by a hand-picked range or a clear.\n const [ownPreset, setOwnPreset] = useState<string | undefined>(undefined);\n const controlled = preset !== undefined;\n const activeId = controlled ? preset : ownPreset;\n const a = from ? render(from) : \"\";\n const b = to ? render(to) : \"\";\n const triggerText = from\n ? to\n ? `${a}${separator}${b}`\n : `${a}${separator}…`\n : (placeholder ?? \"\");\n const hasPresets = Boolean(presets && presets.length > 0);\n const commitRange = (f: string, t: string, presetId: string | undefined) => {\n setOwnPreset(presetId);\n // Two arguments when no preset is involved — exactly the 0.7 call, so a caller's\n // `toHaveBeenCalledWith(from, to)` and a variadic handler see no difference.\n if (presetId === undefined) onChange(f, t);\n else onChange(f, t, presetId);\n };\n return (\n <DateField\n {...rest}\n label={label}\n clearable={clearable}\n clearLabel={text.clear}\n panelLabel={text.rangePanel}\n disabled={disabled}\n invalid={invalid}\n // Widen so the preset column sits beside the calendar (default otherwise).\n width={hasPresets ? 440 : undefined}\n hasValue={Boolean(from || to)}\n triggerText={triggerText}\n onClear={() => commitRange(\"\", \"\", undefined)}\n >\n {(close) => (\n <RangePanel\n from={from}\n to={to}\n presets={presets}\n activeId={activeId}\n controlled={controlled}\n commit={commit}\n close={close}\n commitRange={commitRange}\n calendarProps={{ locale, min, max, labels: calendarLabels }}\n labels={text}\n />\n )}\n </DateField>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/date-picker.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ComponentType, ReactNode, RefObject } from \"react\";\nimport { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { addDaysIso, parseIsoDate } from \"../lib/dates\";\nimport {\n DEFAULT_DATE_PICKER_LABELS,\n DEFAULT_PICKER_SHEET_LABELS,\n useKitLabels,\n useKitLocale,\n type DatePickerLabels,\n} from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { Button, FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID, PHONE_QUERY } from \"./ui\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { MiniCalendar, type MiniCalendarProps } from \"./mini-calendar\";\nimport { Popover } from \"./popover\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * What both pickers share — and, through `ComponentPropsWithoutRef<\"div\">`, everything a\n * `<div>` takes, so a `data-tour` anchor, a test id or an `aria-describedby` reaches the\n * field's root element instead of being dropped (audit §api-design).\n *\n * `onChange` is omitted from those attributes: the DOM event of that name carries a\n * `FormEvent`, while both pickers answer with ISO date strings. Each declares its own\n * below.\n */\ninterface DatePickerBaseProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** BCP 47 tag for the trigger's date and the calendar. Falls back to\n * `<UiKitProvider locale>`, then to the runtime's default. */\n locale?: string;\n /** Embedded top-boundary label, matching the native Input/Select fields. */\n label?: ReactNode;\n placeholder?: string;\n /** Inclusive ISO bounds for selectable days. */\n min?: string;\n max?: string;\n /** Show an X to clear the value (emits `\"\"`). */\n clearable?: boolean;\n /** Accessible name of the clear button. Wins over `datePicker.clear` from\n * `<UiKitProvider labels>`; kept as a prop because it predates the provider. */\n clearLabel?: string;\n disabled?: boolean;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n className?: string;\n /** Intl options for the trigger's formatted date (default: locale short date). */\n formatOptions?: Intl.DateTimeFormatOptions;\n /** Render the trigger's text for an ISO date yourself; wins over `formatOptions`.\n * For a host whose date rendering is more than one `Intl` call can say — Keksdose\n * puts the weekday's name in the UI language beside digits ordered by a separate\n * format preference, two locales in one string. `locale` still drives the calendar. */\n formatValue?: (iso: string) => string;\n /** Passed through to the {@link MiniCalendar} this opens. Its month arrows are\n * icon-only, so their `aria-label` is the only name they have — and the package\n * ships English defaults, which a translating host has to be able to replace.\n * Usually unnecessary now: the calendar reads `miniCalendar` from the provider. */\n calendarLabels?: MiniCalendarProps[\"labels\"];\n}\n\n/** Format an ISO date for the trigger. `lib/dates`' `formatIsoDate` insists on a\n * locale, and here there may be none — no prop and no provider — which `Intl` reads\n * as \"the runtime's default\", as every other kit formatter does. */\nfunction formatDate(iso: string, locale: string | undefined, options?: Intl.DateTimeFormatOptions) {\n return parseIsoDate(iso)?.toLocaleDateString(locale, options) ?? \"\";\n}\n\n/**\n * Everything the kit's own trigger button carries, handed to a custom one\n * ({@link DateRangePickerProps.renderTrigger}) to spread onto its `<button>`. Spread\n * it WHOLE: the ref is how focus comes back when the panel closes, the id pair and\n * `aria-labelledby` are the 0.5.1 naming (an external `<label htmlFor>`, a form\n * library's `aria-describedby`), and `role`/`aria-haspopup`/`aria-controls`/\n * `aria-expanded` are what tell a screen reader this opens a calendar and whether it\n * is open. `className` is the kit's field look (42px with a label, `pe-9` for the\n * glyph the field draws at its end) — merge yours after it with `cn`.\n */\nexport interface DateTriggerAttributes {\n ref: RefObject<HTMLButtonElement | null>;\n type: \"button\";\n id?: string;\n role: \"combobox\";\n \"aria-haspopup\": \"dialog\";\n \"aria-controls\": string;\n \"aria-expanded\": boolean;\n \"aria-labelledby\"?: string;\n \"aria-label\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: true;\n disabled?: boolean;\n onClick: () => void;\n className: string;\n}\n\n/** A custom trigger, as `DateField` calls it; each picker adapts it to its own props. */\ntype TriggerRenderer = (attrs: DateTriggerAttributes, valueId: string) => ReactNode;\n\n/** A caller's trigger, as an element of its own: the attributes carry the trigger's\n * ref, and a ref is handed to a child as a prop, not passed to a function in the\n * middle of the parent's render. */\nfunction CustomTrigger({\n render,\n attrs,\n valueId,\n}: {\n render: TriggerRenderer;\n attrs: DateTriggerAttributes;\n valueId: string;\n}) {\n return render(attrs, valueId);\n}\n\n/**\n * The field-shaped button that opens the calendar.\n *\n * A component of its own rather than JSX inline in `Popover`'s `trigger` render prop,\n * because handing focus back to it is an effect and a render prop is not somewhere a\n * hook can go.\n */\nfunction DateFieldTrigger({\n open,\n toggle,\n triggerRef,\n triggerText,\n hasValue,\n labelledBy,\n valueId,\n padded,\n disabled,\n invalid,\n panelId,\n aria,\n renderTrigger,\n}: {\n /** The caller's naming/description attributes, routed here from the field. */\n aria: TriggerAria;\n /** Draw the button yourself, with these attributes — see {@link DateTriggerAttributes}. */\n renderTrigger?: TriggerRenderer;\n open: boolean;\n toggle: () => void;\n triggerRef: RefObject<HTMLButtonElement | null>;\n /** The panel's id — `role=\"combobox\"` requires `aria-controls`. */\n panelId: string;\n triggerText: string;\n hasValue: boolean;\n /** ids of the hidden label twin + the value span, in the order they are spoken. */\n labelledBy?: string;\n valueId: string;\n padded: boolean;\n disabled?: boolean;\n invalid?: boolean;\n}) {\n const wasOpen = useRef(false);\n useEffect(() => {\n const justClosed = wasOpen.current && !open;\n wasOpen.current = open;\n if (!justClosed) return;\n // Only when the panel still HAD focus. The day the user was standing on has just\n // been unmounted with the panel, which drops focus to <body> and leaves a\n // keyboard user back at the top of the document with no idea where they were.\n // But an outside click closes this panel too, and that click has already given\n // focus to whatever it landed on — dragging it back here would be worse than the\n // bug, so a focus that is somewhere real is left alone.\n const active = document.activeElement;\n if (!active || active === document.body) triggerRef.current?.focus();\n // Here rather than through `useFocusTrap`'s `restoreFocus`, and not only because\n // `Popover` runs no trap to carry it: that option restores to whatever held focus\n // when the panel opened, and on Safari — and on Firefox on macOS — clicking a\n // button does not focus it, so \"whatever held focus\" is the page. The requirement\n // is this trigger, every time, however the panel was opened.\n }, [open, triggerRef]);\n\n // One attribute object, worn either by the kit's own button below or by a caller's\n // (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the\n // naming and state this one announces.\n const attrs: DateTriggerAttributes = {\n ref: triggerRef,\n type: \"button\",\n disabled,\n onClick: toggle,\n // The name has to carry the VALUE. A bare `aria-label={label}` — which is what\n // this was — replaces the button's text outright, so the one field in the form\n // whose entire job is to show a date announced \"Due date\" and stopped there,\n // and a screen-reader user could not read back what they had picked without\n // opening the calendar and hunting for the selected day.\n //\n // Two ids rather than one composed string (`common.fieldValue`): \"label: value\"\n // is a sentence, and a name reference is spoken as the two texts in order with\n // no punctuation to translate at all. The hidden twin of the FieldLabel lives\n // in DateField, next to the label it copies.\n id: aria.id,\n \"aria-labelledby\": aria[\"aria-label\"] && !aria[\"aria-labelledby\"] ? undefined : labelledBy,\n \"aria-label\": aria[\"aria-label\"],\n \"aria-describedby\": aria[\"aria-describedby\"],\n // `role=\"combobox\"` on a button that opens a calendar is the APG date-picker\n // shape, and it is what makes the next two lines legal: `button` supports\n // neither `aria-expanded` nor `aria-invalid`, so the previous markup set an\n // invalid state that announced nothing (ESLint's `role-supports-aria-props`).\n //\n // `aria-haspopup=\"dialog\"` is now honest — it was not when this comment first\n // said so. `Popover` gained a real `role=\"dialog\"` in the same wave, so the\n // trigger's promise and the panel's role finally agree. The phone sheet is a\n // `role=\"dialog\"` too, and carries the same id.\n role: \"combobox\",\n \"aria-haspopup\": \"dialog\",\n \"aria-controls\": panelId,\n \"aria-expanded\": open,\n \"aria-invalid\":\n invalid || aria[\"aria-invalid\"] === true || aria[\"aria-invalid\"] === \"true\" || undefined,\n className: cn(\n FIELD_TRIGGER,\n \"pe-9\",\n padded && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n invalid && FIELD_INVALID,\n ),\n };\n if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;\n\n return (\n <button {...attrs}>\n <span id={valueId} className={cn(\"truncate\", !hasValue && \"text-[var(--text-placeholder)]\")}>\n {/* `|| \" \"` (a NON-BREAKING space) — triggerText is \"\" when there is no value and no\n placeholder was passed. An empty span has no line box, so the\n trigger collapses to its padding and sits shorter than every\n other field beside it.\n\n It has to be a NON-BREAKING space. This read `|| \" \"` first, and\n that fix did nothing at all: an ordinary space is collapsible\n white space, and white space at the start and end of a line is\n removed — so the span rendered with height 0 and the trigger\n stayed 22px against the 42px of the fields either side of it\n (Keksdose live #294, measured in the browser: `span.textContent`\n WAS `\" \"` and `getBoundingClientRect().height` was 0). U+00A0 is\n not collapsible, so it holds the line. */}\n {triggerText || \" \"}\n </span>\n </button>\n );\n}\n\n/**\n * How a panel's body and its action row are put on screen — the popover stacks them,\n * the phone sheet pins the actions in its footer. Handed to the panel rather than\n * applied around it because the actions belong to the panel's own draft state\n * (`RangePanel`), while the frame they sit in belongs to the field.\n */\ntype PanelFrame = (body: ReactNode, actions?: ReactNode) => ReactNode;\n\nconst popoverFrame: PanelFrame = (body, actions) =>\n actions ? (\n <div className=\"flex flex-col gap-2\">\n {body}\n <div className=\"flex items-center justify-end gap-2 border-t border-[var(--border)] pt-2\">{actions}</div>\n </div>\n ) : (\n body\n );\n\n/** A field-styled trigger that opens a portalled MiniCalendar. Shared by the\n * single- and range-date pickers below. */\nfunction DateField({\n triggerText,\n hasValue,\n label,\n disabled,\n clearable,\n clearLabel,\n panelLabel,\n className,\n width,\n onClear,\n invalid,\n sheet = false,\n sheetBackCloses = true,\n renderTrigger,\n children,\n ...rest\n}: Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> & {\n triggerText: string;\n hasValue: boolean;\n label?: ReactNode;\n disabled?: boolean;\n clearable?: boolean;\n /** Already resolved by the picker (prop > provider > English). */\n clearLabel: string;\n /** The popover panel's accessible name, resolved likewise. */\n panelLabel: string;\n invalid?: boolean;\n className?: string;\n /** Popover panel width; omit for the default (a bare calendar). */\n width?: number;\n onClear: () => void;\n /** Open as a full-screen sheet instead of the popover — the picker decides when\n * (below {@link PHONE_QUERY}). */\n sheet?: boolean;\n /** The sheet's `FullBleedDialog backCloses`. */\n sheetBackCloses?: boolean;\n renderTrigger?: TriggerRenderer;\n children: (close: () => void, frame: PanelFrame) => ReactNode;\n}) {\n const showClear = Boolean(clearable && hasValue && !disabled);\n const [aria, wrapperRest] = splitTriggerAria(rest);\n // The panel is portalled to <body> and leaves the subtree whose `dir` it inherited,\n // so a calendar in an RTL form opened LTR — the grid's columns, its arrow keys and\n // its chevrons all the wrong way round. Read at the moment of opening (the only way\n // the popover opens is through `toggle`) and put back on the panel.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const id = useId();\n const labelId = `${id}-label`;\n const valueId = `${id}-value`;\n const panelId = `${id}-panel`;\n // Only a STRING label can be reused as a name; a ReactNode may be a whole row with\n // its own interactive \"?\" hint in it, and naming a button with that reads the hint\n // out as part of the field's name.\n const named = typeof label === \"string\";\n // The sheet's open flag. Only the phone branch owns one: on desktop `Popover` owns\n // it, so the two can never disagree about what is on screen.\n const [sheetOpen, setSheetOpen] = useState(false);\n const sheetTriggerRef = useRef<HTMLButtonElement>(null);\n const closeSheet = () => setSheetOpen(false);\n const sheetLabels = useKitLabels(\"pickerSheet\", DEFAULT_PICKER_SHEET_LABELS);\n const sheetFrame: PanelFrame = (body, actions) => (\n <FullBleedDialog\n open\n onClose={closeSheet}\n // The same id the popover panel wears, so the trigger's `aria-controls` holds in\n // both presentations; named the way the popover is.\n id={panelId}\n aria-label={panelLabel}\n header={panelLabel}\n // `pickerSheet.close`: the one \"close this full-screen picker\" string the kit\n // already translates, rather than a second key saying the same thing.\n closeLabel={sheetLabels.close}\n backCloses={sheetBackCloses}\n dir={dir}\n // Portalled to <body>, the sheet is not inside the field's wrapper, so a\n // document-level \"outside click\" listener — a Popover or dropdown this field\n // sits in — would read a tap on a day as outside and unmount the cell before\n // its click (Keksdose dev#477, the fix `PickerSheet` carries).\n onMouseDown={(e) => e.stopPropagation()}\n footer={actions}\n >\n {body}\n </FullBleedDialog>\n );\n const fieldTrigger = ({\n open,\n toggle,\n ref,\n }: {\n open: boolean;\n toggle: () => void;\n ref: RefObject<HTMLButtonElement | null>;\n }) => (\n <DateFieldTrigger\n open={open}\n toggle={() => {\n setDir(dirOf(rootRef.current));\n toggle();\n }}\n triggerRef={ref}\n triggerText={triggerText}\n hasValue={hasValue}\n // ALWAYS a name reference, never `undefined`. A `<button>` takes its\n // accessible name from its contents, so an unlabelled trigger used to\n // announce its own date text for free. `role=\"combobox\"` does not —\n // content is excluded from name computation for that role — so leaving\n // this undefined made an unlabelled date field announce nothing at all.\n // Named: \"label, value\". Unnamed: the value alone, which is what the\n // button was saying before.\n //\n // A caller's own reference comes first. And when the caller gives the field\n // an `id` (so an external `<label htmlFor>` can point at it), the trigger\n // lists ITSELF first: a self-reference in `aria-labelledby` is resolved\n // from the element's native label (accname 2B → 2D), so \"Due date\" from\n // that <label> is spoken before the value, instead of being overridden.\n labelledBy={[\n aria[\"aria-labelledby\"],\n named ? labelId : !aria[\"aria-labelledby\"] && aria.id ? aria.id : undefined,\n valueId,\n ]\n .filter(Boolean)\n .join(\" \")}\n aria={aria}\n panelId={panelId}\n valueId={valueId}\n padded={label !== undefined}\n disabled={disabled}\n invalid={invalid}\n renderTrigger={renderTrigger}\n />\n );\n return (\n // The caller's attributes land here, on the field's own box — the trigger inside is\n // named by `aria-labelledby` and must keep the id pair it is given.\n <div {...wrapperRest} ref={rootRef} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n {/* The visible FieldLabel is a plain span, not a `<label htmlFor>`, so it names\n nothing on its own — this hidden twin is what the trigger is named by.\n `aria-hidden` because a name reference reads a hidden element deliberately\n (accname §4.3.1), and without it the label would be announced twice: once as\n loose text before the button, once inside the button's own name.\n `sr-only-fixed` rather than `sr-only` — see the note on the class in\n tokens.css. */}\n {named && (\n <span id={labelId} aria-hidden className=\"sr-only-fixed\">\n {label}\n </span>\n )}\n {sheet ? (\n <>\n {fieldTrigger({ open: sheetOpen, toggle: () => setSheetOpen((v) => !v), ref: sheetTriggerRef })}\n {/* Mounted only while open, like the popover's children: the panel's draft is\n born with it, so every open starts again from the committed value. */}\n {sheetOpen && children(closeSheet, sheetFrame)}\n </>\n ) : (\n <Popover\n width={width}\n panelId={panelId}\n // Named for what it is. Unnamed, it fell back to `popover.panel`, and a date\n // field announced its calendar as \"Popover\" — in English, in every language.\n labels={{ panel: panelLabel }}\n dir={dir}\n trigger={fieldTrigger}\n >\n {(close) => children(close, popoverFrame)}\n </Popover>\n )}\n {showClear ? (\n <button\n type=\"button\"\n aria-label={clearLabel}\n onClick={onClear}\n className=\"absolute end-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n ) : (\n <Calendar\n aria-hidden\n className=\"pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]\"\n />\n )}\n </div>\n );\n}\n\n/** One ‹ / › day-step button, sized to sit flush beside the field. */\nfunction StepButton({\n icon: Icon,\n label,\n disabled,\n onClick,\n mirror,\n className,\n}: {\n icon: ComponentType<{ className?: string }>;\n label: string;\n disabled: boolean;\n onClick: () => void;\n /** Flip the icon in RTL — for the ‹ › day steps, whose \"previous\" points to the\n * reading start. Not for the today icon, which has no direction. */\n mirror?: boolean;\n /** The button's corners in the joined group (see DatePicker). */\n className?: string;\n}) {\n return (\n <Tooltip label={label} portal>\n <button\n type=\"button\"\n aria-label={label}\n disabled={disabled}\n onClick={onClick}\n className={cn(\n FIELD_BASE,\n // FIELD_BASE is `block w-full` for text inputs; a step button is neither, and\n // px-3 would make it wider than it needs to be. twMerge lets these win.\n \"relative flex h-full w-10 shrink-0 items-center justify-center px-0 text-[var(--text-muted)]\",\n \"hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\",\n // Raised while hovered or focused, so the shared border and the focus ring are\n // drawn on top of the neighbour they overlap by a pixel.\n \"hover:z-10 focus-visible:z-10\",\n // Dimmed ICON, not a dimmed box: at 40% opacity the whole button — border\n // included — faded out of the group and read as a broken, detached control.\n \"disabled:cursor-not-allowed disabled:text-[var(--text-placeholder)] disabled:opacity-60 disabled:hover:bg-[var(--bg-surface)]\",\n className,\n )}\n >\n <Icon className={cn(\"size-4\", mirror && \"rtl:-scale-x-100\")} />\n </button>\n </Tooltip>\n );\n}\n\nexport interface DatePickerProps extends DatePickerBaseProps {\n /** ISO \"YYYY-MM-DD\", or \"\" for empty. */\n value: string;\n onChange: (iso: string) => void;\n /**\n * Flank the field with ‹ › buttons that move the value one day. For fields whose\n * typical edit is a day or two — opening a calendar to go \"yesterday\" is three\n * interactions for what should be one.\n *\n * Off by default: on a filter or a far-away date the buttons are dead weight, and\n * they cost ~5rem of width that a narrow layout may not have.\n */\n step?: boolean;\n /** Accessible names for the step buttons. Optional: they resolve from\n * `datePicker.previousDay` / `nextDay` in `<UiKitProvider labels>`, then from the\n * English defaults. This prop wins over both, for the one field that says it\n * differently. */\n stepLabels?: { prev: string; next: string };\n /**\n * Today's date as ISO \"YYYY-MM-DD\". Adds a jump-to-today button beside the field,\n * disabled once the value already is that day.\n *\n * Passed in rather than read from a clock here: a component that decides what \"today\"\n * is cannot be tested at a fixed date, and the consumer already has one definition of\n * it. The button is always RENDERED — going flat instead of disappearing — because a\n * control that comes and goes with the value reflows whatever sits beside it, which\n * is the report this exists to answer.\n */\n today?: string;\n /** Accessible name for the today button; wins over `datePicker.today`. */\n todayLabel?: string;\n}\n\n/**\n * The picker's own label props, folded into one `datePicker` override so they sit at\n * the top of the same precedence chain as everything else: prop > provider > English.\n * They predate the provider and stay public API, which is why they are not simply\n * replaced by a `labels` prop.\n */\nfunction useDatePickerLabels(\n clearLabel: string | undefined,\n stepLabels: { prev: string; next: string } | undefined,\n todayLabel: string | undefined,\n): DatePickerLabels {\n const prev = stepLabels?.prev;\n const next = stepLabels?.next;\n const fromProps = useMemo(() => {\n const out: Partial<DatePickerLabels> = {};\n if (clearLabel !== undefined) out.clear = clearLabel;\n if (prev !== undefined) out.previousDay = prev;\n if (next !== undefined) out.nextDay = next;\n if (todayLabel !== undefined) out.today = todayLabel;\n return out;\n }, [clearLabel, prev, next, todayLabel]);\n return useKitLabels(\"datePicker\", DEFAULT_DATE_PICKER_LABELS, fromProps);\n}\n\n/** Single-date picker: a field showing the formatted date, opening a calendar. */\nexport function DatePicker({\n value,\n onChange,\n locale: localeProp,\n placeholder,\n min,\n max,\n formatOptions,\n formatValue,\n step,\n stepLabels,\n today,\n todayLabel,\n className,\n calendarLabels,\n label,\n clearable,\n clearLabel,\n disabled,\n invalid,\n ...rest\n}: DatePickerProps) {\n const locale = useKitLocale(localeProp);\n const text = useDatePickerLabels(clearLabel, stepLabels, todayLabel);\n const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));\n // With a step or a today button beside it the field is no longer the outermost\n // element — the flex row at the bottom is, and a caller's attributes belong on\n // whichever of the two is actually the root. Naming the shared props rather than\n // letting `...rest` carry them is what makes that choice possible.\n const wrapped = Boolean(step || today);\n const [triggerAria, wrapperRest] = splitTriggerAria(rest);\n const field = (\n <DateField\n // With step/today buttons the flex row is the root and takes the caller's props —\n // except the ones that name the field, which still belong on its trigger.\n {...(wrapped ? triggerAria : rest)}\n label={label}\n clearable={clearable}\n clearLabel={text.clear}\n panelLabel={text.panel}\n disabled={disabled}\n invalid={invalid}\n className={step ? \"min-w-0 flex-1\" : className}\n hasValue={Boolean(value)}\n triggerText={value ? render(value) : (placeholder ?? \"\")}\n onClear={() => onChange(\"\")}\n >\n {(close) => (\n <MiniCalendar\n mode=\"single\"\n focusOnOpen\n from={value}\n to={value}\n locale={locale}\n min={min}\n max={max}\n labels={calendarLabels}\n onSelect={(iso) => {\n onChange(iso);\n close();\n }}\n />\n )}\n </DateField>\n );\n if (!wrapped) return field;\n\n // An empty field has nothing to step from, so both buttons are dead until a date\n // is picked. Bounds are compared as strings: \"YYYY-MM-DD\" sorts chronologically.\n const target = (days: number) => (value ? addDaysIso(value, days) : \"\");\n const outOfBounds = (iso: string) => Boolean((min && iso < min) || (max && iso > max));\n const blocked = (days: number) => {\n const next = target(days);\n if (!next || disabled) return true;\n return outOfBounds(next);\n };\n return (\n // ONE joined control, not three boxes in a row: the buttons and the field share\n // their borders (each piece overlaps the previous by a pixel) and only the outer\n // ends are rounded — the button-group shape, so ‹ date › reads as one field with\n // controls rather than a field with loose buttons parked beside it, which is what\n // the `gap-1` version looked like.\n //\n // items-stretch, not items-center: the buttons match the field's height, which\n // varies with whether it carries a floating label. The field's trigger is nested\n // inside DateField, so its inner corners are squared from here.\n <div\n {...wrapperRest}\n className={cn(\n \"flex items-stretch [&>*:not(:first-child)]:-ms-px\",\n step && \"[&_[role=combobox]]:rounded-s-none\",\n (step || today) && \"[&_[role=combobox]]:rounded-e-none\",\n \"[&_[role=combobox]:focus-visible]:relative [&_[role=combobox]:focus-visible]:z-10\",\n className,\n )}\n >\n {step && (\n <StepButton\n icon={ChevronLeft}\n mirror\n label={text.previousDay}\n disabled={blocked(-1)}\n onClick={() => onChange(target(-1))}\n className=\"rounded-e-none\"\n />\n )}\n {field}\n {step && (\n <StepButton\n icon={ChevronRight}\n mirror\n label={text.nextDay}\n disabled={blocked(1)}\n onClick={() => onChange(target(1))}\n className={cn(\"rounded-s-none\", today && \"rounded-e-none\")}\n />\n )}\n {today && (\n <StepButton\n icon={CalendarClock}\n label={text.today}\n disabled={Boolean(disabled) || value === today || outOfBounds(today)}\n onClick={() => onChange(today)}\n className=\"rounded-s-none\"\n />\n )}\n </div>\n );\n}\n\nexport interface DateRangePickerPreset {\n /**\n * The preset's identity, independent of its dates — `\"last_3_months\"`, not two ISO\n * strings. Optional for the 0.x callers that pass anonymous pairs.\n *\n * keksdose's report range field (report-range-field.tsx, \"Why this is app-level\")\n * is why it exists: its URL stores `?preset=last_3_months` and recomputes the dates\n * from today on every read, so a preset that is only its dates froze \"last 3 months\"\n * at whatever day the panel rendered, and two presets that happen to cover the same\n * days (this week and today, on a Monday) could not be told apart. With an `id` the\n * choice is reported to `onChange` and shown as selected for as long as the range\n * still matches it. Recompute `from`/`to` on every render (`calendarMonthPresets()`,\n * `dateRangePresets()`) and pass the key as the id.\n */\n id?: string;\n label: ReactNode;\n from: string;\n to: string;\n}\n\n/**\n * When the picker hands its range to `onChange`.\n *\n * - `\"immediate\"` (the default, and all of 0.7): a preset click commits and closes,\n * and each calendar click is reported as it happens — the half-made `(from, \"\")`\n * included. The right shape for quick ranges on a filter, where one click is one\n * answer.\n * - `\"apply\"`: the panel works on a DRAFT. A preset ARMS the calendar instead of\n * committing, days can then be nudged (\"last 3 months, but end it last Friday\"),\n * and nothing reaches `onChange` until Apply — which stays disabled until both ends\n * exist. Cancel, Escape and an outside click discard the draft, and every open\n * starts again from the committed value. keksdose's report range asked for exactly\n * this: each commit there re-runs a whole page of charts, so an intermediate state\n * must never escape the panel.\n */\nexport type DateRangeCommit = \"immediate\" | \"apply\";\n\nexport interface DateRangePickerProps extends DatePickerBaseProps {\n from: string;\n to: string;\n /** The committed range. `presetId` is the chosen preset's `id` when a preset\n * produced it (and the range was not nudged since), `undefined` for days picked by\n * hand. A 0.x two-argument handler simply ignores it. */\n onChange: (from: string, to: string, presetId?: string) => void;\n /** Separator between the two formatted dates in the trigger (default \" – \"). */\n separator?: string;\n /** Optional named ranges rendered as a start-side column (last month / YTD / …). */\n presets?: DateRangePickerPreset[];\n /**\n * The active preset's `id`, controlled — e.g. straight from keksdose's `?preset=`.\n * `null` (or an id no preset has, such as `\"custom\"`) says \"no preset\": nothing in\n * the column is marked even if a preset happens to cover the same days. A preset is\n * marked only while its dates still equal `from`/`to`.\n *\n * Leave it `undefined` and the picker remembers the last preset it committed\n * itself; with no memory at all it falls back to the first preset whose dates match.\n */\n preset?: string | null;\n /** See {@link DateRangeCommit}. Default `\"immediate\"`. */\n commit?: DateRangeCommit;\n /**\n * Draw the trigger yourself. keksdose's report range field\n * (reports/report-range-field.tsx, \"Why this is app-level\") is why: its trigger\n * NAMES the active preset (\"Last 3 months\", the window as a muted suffix) where the\n * kit's shows two dates, sits inside a Tooltip, and is squared off on one side so the\n * granularity control can join it flush at the field's 42px.\n *\n * Spread `props.triggerProps` onto a `<button>` whole and put `props.valueProps` on\n * the element that shows the value — see {@link DateTriggerAttributes} for what each\n * part is for. The field around it (label, hidden name twin, clear/calendar glyph,\n * popover or phone sheet) is unchanged.\n */\n renderTrigger?: (props: DateRangeTriggerRenderProps) => ReactNode;\n /**\n * Below {@link PHONE_QUERY} the panel opens as a full-screen sheet (`FullBleedDialog`)\n * with presets in a grid above the calendar and Apply pinned in the footer, instead\n * of the 440px popover that hung off a 360px screen (keksdose's report range field,\n * which hand-rolled exactly this). This is its `backCloses`: on by default, so Back\n * dismisses the sheet; off for a caller whose commit rewrites the URL with\n * `replaceState` in the same tick — keksdose's `?preset/from/to` — where the router\n * would overwrite the sheet's history marker.\n */\n sheetBackCloses?: boolean;\n}\n\n/** What {@link DateRangePickerProps.renderTrigger} is called with. */\nexport interface DateRangeTriggerRenderProps {\n /** Spread onto the `<button>`, whole. Merge your class after `triggerProps.className`. */\n triggerProps: DateTriggerAttributes;\n /** Put on the element showing the value: the trigger is named \"label, value\" by\n * reference to this id. Without it the name is the label alone. */\n valueProps: { id: string };\n open: boolean;\n from: string;\n to: string;\n /** The preset the column marks for the committed range, if any — the one to name. */\n preset: DateRangePickerPreset | undefined;\n /** What the kit's own trigger would show: the formatted range, or the placeholder. */\n text: string;\n}\n\ninterface RangeDraft {\n from: string;\n to: string;\n presetId: string | undefined;\n}\n\n/** Which preset (by index) the column marks, or -1. See `DateRangePickerProps.preset`. */\nfunction markedPreset(\n presets: readonly DateRangePickerPreset[],\n from: string,\n to: string,\n activeId: string | null | undefined,\n controlled: boolean,\n): number {\n const matches = (p: DateRangePickerPreset) => p.from === from && p.to === to;\n if (activeId != null) {\n const byId = presets.findIndex((p) => p.id === activeId && matches(p));\n if (byId !== -1 || controlled) return byId;\n } else if (controlled) {\n return -1;\n }\n return presets.findIndex(matches);\n}\n\n/**\n * The preset column: a named group of toggle buttons, `aria-pressed` on the marked one.\n * Pressed-state rather than colour alone, because \"which range is this\" is the one\n * thing a screen-reader user opening the panel needs to hear.\n */\nfunction PresetColumn({\n presets,\n marked,\n label,\n onPick,\n sheet,\n}: {\n presets: readonly DateRangePickerPreset[];\n marked: number;\n label: string;\n onPick: (p: DateRangePickerPreset) => void;\n /** The phone sheet's shape: a two-column grid ABOVE the calendar with 44px rows —\n * a 128px column beside a month grid leaves the grid ~200px on a 360px screen. */\n sheet?: boolean;\n}) {\n return (\n // `border-e`/`pe`, not `-r`: the preset column is on the START side, and the rule\n // between it and the calendar has to follow it in a right-to-left UI.\n <div\n role=\"group\"\n aria-label={label}\n className={\n sheet\n ? \"grid grid-cols-2 gap-0.5\"\n : \"flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2\"\n }\n >\n {presets.map((p, i) => {\n const selected = i === marked;\n return (\n <button\n key={p.id ?? i}\n type=\"button\"\n aria-pressed={selected}\n data-preset={p.id}\n onClick={() => onPick(p)}\n className={cn(\n \"rounded px-2 text-start\",\n // 44px on a phone: the kit's touch-target size (`SHEET_ROW_CLASS`).\n sheet ? \"min-h-11 py-2 text-sm\" : \"py-1.5 text-xs\",\n selected\n ? \"bg-[var(--bg-active)] font-medium text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n )}\n >\n {p.label}\n </button>\n );\n })}\n </div>\n );\n}\n\n/**\n * The panel's body. A component of its own so `commit=\"apply\"` can keep its draft in\n * state that is born when the panel opens: `Popover` unmounts its children on close,\n * so the draft is re-seeded from the committed value on every open, and an abandoned\n * half-selection cannot survive into the next one.\n */\nfunction RangePanel({\n from,\n to,\n presets,\n activeId,\n controlled,\n commit,\n close,\n frame,\n sheet,\n commitRange,\n calendarProps,\n labels,\n}: {\n from: string;\n to: string;\n presets: readonly DateRangePickerPreset[] | undefined;\n activeId: string | null | undefined;\n controlled: boolean;\n commit: DateRangeCommit;\n close: () => void;\n /** Where the body and the Apply row go — see `PanelFrame`. */\n frame: PanelFrame;\n sheet: boolean;\n commitRange: (from: string, to: string, presetId: string | undefined) => void;\n calendarProps: Pick<MiniCalendarProps, \"locale\" | \"min\" | \"max\" | \"labels\">;\n labels: DatePickerLabels;\n}) {\n const drafting = commit === \"apply\";\n const [draft, setDraft] = useState<RangeDraft>({\n from,\n to,\n presetId: activeId ?? undefined,\n });\n // What the panel SHOWS: the draft while drafting, the committed value otherwise.\n const shown = drafting ? draft : { from, to, presetId: activeId ?? undefined };\n const marked = presets\n ? markedPreset(presets, shown.from, shown.to, drafting ? draft.presetId : activeId, drafting || controlled)\n : -1;\n const complete = Boolean(draft.from && draft.to);\n\n const calendar = (\n <MiniCalendar\n {...calendarProps}\n focusOnOpen\n from={shown.from}\n to={shown.to}\n onSelect={(f, t) => {\n if (drafting) {\n setDraft({ from: f, to: t, presetId: undefined });\n return;\n }\n commitRange(f, t, undefined);\n // Two-click range: only dismiss once both ends are chosen.\n if (f && t) close();\n }}\n />\n );\n\n const body =\n presets && presets.length > 0 ? (\n <div className={sheet ? \"flex flex-col gap-3\" : \"flex gap-3\"}>\n <PresetColumn\n sheet={sheet}\n presets={presets}\n marked={marked}\n label={labels.presets}\n onPick={(p) => {\n if (drafting) {\n setDraft({ from: p.from, to: p.to, presetId: p.id });\n return;\n }\n commitRange(p.from, p.to, p.id);\n close();\n }}\n />\n {/* `flex-1`: the calendar takes the rest of the panel. Content-sized it was\n seven tiny cells beside a wide preset column. */}\n <div className=\"min-w-0 flex-1\">{calendar}</div>\n </div>\n ) : (\n calendar\n );\n\n if (!drafting) return frame(body);\n return frame(\n body,\n <>\n {/* `flex-1` in the sheet: two thumb-wide halves of the footer, not two small\n buttons in its corner. */}\n <Button type=\"button\" variant=\"ghost\" className={sheet ? \"flex-1\" : undefined} onClick={close}>\n {labels.cancel}\n </Button>\n <Button\n type=\"button\"\n className={sheet ? \"flex-1\" : undefined}\n // A half-made `(from, \"\")` would commit a window with no end — a blank\n // report, one click into a two-click gesture.\n disabled={!complete}\n onClick={() => {\n if (!complete) return;\n commitRange(draft.from, draft.to, draft.presetId);\n close();\n }}\n >\n {labels.apply}\n </Button>\n </>,\n );\n}\n\n/** Two-date range picker: click a start then an end; closes once both are set.\n * With `presets`, a column of named ranges is shown beside the calendar; with\n * `commit=\"apply\"` the panel drafts and commits on Apply (see {@link DateRangeCommit}). */\nexport function DateRangePicker({\n from,\n to,\n onChange,\n locale: localeProp,\n placeholder,\n min,\n max,\n formatOptions,\n formatValue,\n separator = \" – \",\n presets,\n preset,\n commit = \"immediate\",\n renderTrigger,\n sheetBackCloses,\n calendarLabels,\n label,\n clearable,\n clearLabel,\n disabled,\n invalid,\n ...rest\n}: DateRangePickerProps) {\n const locale = useKitLocale(localeProp);\n const text = useDatePickerLabels(clearLabel, undefined, undefined);\n const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));\n // The preset this picker committed itself, for a caller that does not control\n // `preset`. Cleared by a hand-picked range or a clear.\n const [ownPreset, setOwnPreset] = useState<string | undefined>(undefined);\n const controlled = preset !== undefined;\n const activeId = controlled ? preset : ownPreset;\n const a = from ? render(from) : \"\";\n const b = to ? render(to) : \"\";\n const triggerText = from\n ? to\n ? `${a}${separator}${b}`\n : `${a}${separator}…`\n : (placeholder ?? \"\");\n const hasPresets = Boolean(presets && presets.length > 0);\n const sheet = useMediaQuery(PHONE_QUERY, false);\n const commitRange = (f: string, t: string, presetId: string | undefined) => {\n setOwnPreset(presetId);\n // Two arguments when no preset is involved — exactly the 0.7 call, so a caller's\n // `toHaveBeenCalledWith(from, to)` and a variadic handler see no difference.\n if (presetId === undefined) onChange(f, t);\n else onChange(f, t, presetId);\n };\n return (\n <DateField\n {...rest}\n label={label}\n clearable={clearable}\n clearLabel={text.clear}\n panelLabel={text.rangePanel}\n disabled={disabled}\n invalid={invalid}\n // Widen so the preset column sits beside the calendar (default otherwise).\n width={hasPresets ? 440 : undefined}\n hasValue={Boolean(from || to)}\n triggerText={triggerText}\n onClear={() => commitRange(\"\", \"\", undefined)}\n sheet={sheet}\n sheetBackCloses={sheetBackCloses}\n renderTrigger={\n renderTrigger &&\n ((triggerProps, valueId) =>\n renderTrigger({\n triggerProps,\n valueProps: { id: valueId },\n open: triggerProps[\"aria-expanded\"],\n from,\n to,\n preset: presets?.[markedPreset(presets, from, to, activeId, controlled)],\n text: triggerText,\n }))\n }\n >\n {(close, frame) => (\n <RangePanel\n from={from}\n to={to}\n presets={presets}\n activeId={activeId}\n controlled={controlled}\n commit={commit}\n close={close}\n frame={frame}\n sheet={sheet}\n commitRange={commitRange}\n calendarProps={{ locale, min, max, labels: calendarLabels }}\n labels={text}\n />\n )}\n </DateField>\n );\n}\n"],"mappings":"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","names":[]}
@@ -80,8 +80,17 @@ interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "title">
80
80
  * settings or analysis page. `bare` draws nothing: a leading chevron that turns
81
81
  * down, for an inline "Show 3 hidden accounts" inside something that already has
82
82
  * its own surface.
83
+ *
84
+ * `menu` is a row of a `HoverMenu`: the top bar's menu-item look
85
+ * (`TOPBAR_MENU_ITEM_CLASS` — full-width, `px-3 py-2`, regular weight, square, the
86
+ * hover wash with the text colour left alone), an INSET focus ring so the menu's
87
+ * clipping edge cannot cut it off, the chevron at the end (`chevronPosition`
88
+ * defaults to `end` here) and a body with no padding of its own, so the sub-list's
89
+ * rows sit flush like the rows around them. keksdose's account menu opens its
90
+ * language sub-list this way, and on `bare` it took four header overrides and a
91
+ * body one to get there (account-menu.tsx).
83
92
  */
84
- variant?: "card" | "bare";
93
+ variant?: "card" | "bare" | "menu";
85
94
  /**
86
95
  * Where a `bare` disclosure draws its chevron. `start` (default) is the leading
87
96
  * chevron that turns from the reading direction to down. `end` puts it at the far
@@ -89,7 +98,7 @@ interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "title">
89
98
  * account menu, whose language sub-list reads "Language 🇩🇪 ⌄" with the chevron
90
99
  * after the flag, like every other menu row with a sub-list. With `trailing`, the
91
100
  * chevron follows it, as on a card. A `card` always has it at the end, so this is
92
- * ignored there.
101
+ * ignored there. A `menu` defaults to `end`, like the menu rows around it.
93
102
  */
94
103
  chevronPosition?: "start" | "end";
95
104
  /**
@@ -50,7 +50,7 @@ function Disclosure({
50
50
  defaultOpen = false,
51
51
  onOpenChange,
52
52
  variant = "card",
53
- chevronPosition = "start",
53
+ chevronPosition,
54
54
  headingAs: Heading,
55
55
  keepMounted,
56
56
  disabled,
@@ -67,7 +67,8 @@ function Disclosure({
67
67
  const open = controlled ?? own;
68
68
  const bodyId = useId();
69
69
  const card = variant === "card";
70
- const chevronAtEnd = card || chevronPosition === "end";
70
+ const menu = variant === "menu";
71
+ const chevronAtEnd = card || (chevronPosition ?? (menu ? "end" : "start")) === "end";
71
72
  const triggerOnly = controls !== void 0;
72
73
  const joined = open && !triggerOnly;
73
74
  const hasTrailing = trailing !== void 0 && trailing !== null && trailing !== false;
@@ -96,13 +97,18 @@ function Disclosure({
96
97
  cn(
97
98
  "min-w-0 flex-1 after:absolute after:inset-0 after:content-['']",
98
99
  "focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-[var(--brand)]",
99
- card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : "after:rounded-sm"
100
+ card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : !menu && "after:rounded-sm"
100
101
  )
101
102
  ) : "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
102
103
  card ? cn(
103
104
  "items-center justify-between rounded-lg p-4",
104
105
  hasTrailing ? "pe-0" : "hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
105
106
  joined && "rounded-b-none"
107
+ ) : menu ? (
108
+ // TOPBAR_MENU_ITEM_CLASS's row, spelled out rather than imported: a
109
+ // component does not reach up into the shell. A test holds the two to
110
+ // the same classes.
111
+ "items-center justify-between gap-3 px-3 py-2 text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] focus-visible:ring-inset"
106
112
  ) : "items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
107
113
  headerClassName
108
114
  ),
@@ -140,7 +146,7 @@ function Disclosure({
140
146
  ]
141
147
  }
142
148
  ) : header,
143
- !triggerOnly && /* @__PURE__ */ jsx(Collapse, { id: bodyId, open, keepMounted, children: /* @__PURE__ */ jsx("div", { className: cn(card ? "space-y-3 px-4 pb-4" : "space-y-2 pt-2", bodyClassName), children }) })
149
+ !triggerOnly && /* @__PURE__ */ jsx(Collapse, { id: bodyId, open, keepMounted, children: /* @__PURE__ */ jsx("div", { className: cn(card ? "space-y-3 px-4 pb-4" : !menu && "space-y-2 pt-2", bodyClassName), children }) })
144
150
  ]
145
151
  }
146
152
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/disclosure.tsx"],"sourcesContent":["import { useEffect, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\n\n/** How long the fold takes, in ms. The same number as the `duration-200` below and as\n * the unmount timer, because they are the same movement. */\nconst COLLAPSE_MS = 200;\n\n/** Read at the moment of closing, like `useCloseTransition` does: the setting can\n * change under a long-lived page, and jsdom/SSR have no `matchMedia` — where \"no\n * animation\" is also the only correct answer, since nothing is painting. */\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window === \"undefined\" ||\n typeof window.matchMedia !== \"function\" ||\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n\n/**\n * `extends` the div's props so an `id` (what a trigger's `aria-controls` points at), a\n * `data-tour` anchor or a test id reaches the element that folds.\n */\nexport interface CollapseProps extends ComponentPropsWithoutRef<\"div\"> {\n open: boolean;\n /**\n * Keep the children mounted while shut (hidden and `inert`) instead of unmounting\n * them once the fold has finished closing.\n *\n * Off by default, and that default is a behaviour contract rather than a\n * performance note: Lenkbank's disclosure bodies FETCH, and a body that only exists\n * while open is what stops a closed card asking — without every caller carrying its\n * own `enabled: open`. On for a body whose state must outlive a close (a half-typed\n * form) or that must be found by the browser's find-in-page.\n */\n keepMounted?: boolean;\n children: ReactNode;\n}\n\n/**\n * The fold on its own: content that opens and shuts in place, animated to a height\n * nobody measured.\n *\n * THE TECHNIQUE is `AppShell`'s sidebar group's, lifted out so there is one copy of it.\n * A `grid-template-rows` transition from `0fr` to `1fr` is the one way to animate to a\n * content-sized height without measuring it — so a chart that resizes inside it, or a\n * translated line that wraps, is still exactly as tall as it needs. `visibility` rides\n * the same transition, so a shut body leaves the accessibility tree only once it has\n * finished closing; `inert` takes it out of the tab order immediately.\n *\n * THE CHILDREN are unmounted once closed (see {@link CollapseProps.keepMounted}), but\n * only once the track has finished closing: the track is what animates, so the\n * children have to survive the movement that hides them. Under reduced motion they\n * go at once — there is no movement to wait for — and the opening half is silenced by\n * `motion-reduce:transition-none`, since tokens.css only silences the overlay\n * animations by name.\n *\n * Padding and margins belong on a child: on this element, or the clipping one inside\n * it, they would hold the row open by that much.\n */\nexport function Collapse({ open, keepMounted = false, children, className, style, ...rest }: CollapseProps) {\n // Whether the children are still on their way out. Adjusted DURING render when\n // `open` flips (React's \"storing information from previous renders\"), so a close\n // under reduced motion unmounts in the same render that shut it — a test, and a\n // screen reader, see the body go on the click — and a re-open mid-fold simply\n // cancels the linger.\n const [lingering, setLingering] = useState(false);\n const [prevOpen, setPrevOpen] = useState(open);\n if (open !== prevOpen) {\n setPrevOpen(open);\n setLingering(!open && !prefersReducedMotion());\n }\n useEffect(() => {\n if (!lingering) return;\n const timer = setTimeout(() => setLingering(false), COLLAPSE_MS);\n return () => clearTimeout(timer);\n }, [lingering]);\n\n const mounted = open || lingering || keepMounted;\n\n return (\n <div\n {...rest}\n inert={!open}\n className={cn(\n \"grid transition-[grid-template-rows,visibility] duration-200 ease-out motion-reduce:transition-none\",\n className,\n )}\n // Inline rather than `grid-rows-[…]` classes: a caller's `className` must not be\n // able to pin the track open, and the two values ARE the state.\n style={{ ...style, gridTemplateRows: open ? \"1fr\" : \"0fr\", visibility: open ? \"visible\" : \"hidden\" }}\n >\n <div\n className={cn(\n \"min-h-0 overflow-hidden transition-opacity duration-200 ease-out motion-reduce:transition-none\",\n open ? \"opacity-100\" : \"opacity-0\",\n )}\n >\n {mounted && children}\n </div>\n </div>\n );\n}\n\n/**\n * What {@link DisclosureProps.triggerProps} may put on the header button: its own\n * attributes and handlers, and any `data-*`. Not the ones the disclosure owns —\n * `aria-expanded`, `aria-controls`, `disabled`, `onClick`, `type` and the content — nor\n * `className`, which is {@link DisclosureProps.headerClassName}.\n */\nexport type DisclosureTriggerProps = Omit<\n ComponentPropsWithoutRef<\"button\">,\n \"type\" | \"aria-expanded\" | \"aria-controls\" | \"disabled\" | \"onClick\" | \"children\" | \"className\"\n> & { [data: `data-${string}`]: string | number | boolean | undefined };\n\n/**\n * `title` is omitted from the div's own props because this component already owns the\n * name: here it is the header's content (and a ReactNode), not the browser's tooltip.\n * Everything else reaches the outer element.\n */\nexport interface DisclosureProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n /** What the header says. A node, so a status chip or a count can sit in it. */\n title: ReactNode;\n /** The smaller line under the title — what is inside, for a reader deciding whether\n * to open it. */\n hint?: ReactNode;\n /**\n * Who decides whether this one is open. Left out, the disclosure decides for itself,\n * which is what one standing on its own wants. Passed, the caller does — a set of\n * which one is open at a time, or an open state kept in the URL so the view can be\n * linked to. There is no accordion component: \"one open at a time\" is a few lines in\n * the caller and it usually owns a URL parameter, which is app state.\n */\n open?: boolean;\n /** Whether an UNCONTROLLED disclosure starts open. Ignored when `open` is passed. */\n defaultOpen?: boolean;\n /** Called with the next state on every toggle, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `card` (default) draws the kit's `Card` surface round header and body, with the\n * chevron trailing the header and turning over — the \"section that opens\" of a\n * settings or analysis page. `bare` draws nothing: a leading chevron that turns\n * down, for an inline \"Show 3 hidden accounts\" inside something that already has\n * its own surface.\n */\n variant?: \"card\" | \"bare\";\n /**\n * Where a `bare` disclosure draws its chevron. `start` (default) is the leading\n * chevron that turns from the reading direction to down. `end` puts it at the far\n * end of the row, pointing down and turning up as the card's does — keksdose's\n * account menu, whose language sub-list reads \"Language 🇩🇪 ⌄\" with the chevron\n * after the flag, like every other menu row with a sub-list. With `trailing`, the\n * chevron follows it, as on a card. A `card` always has it at the end, so this is\n * ignored there.\n */\n chevronPosition?: \"start\" | \"end\";\n /**\n * Wrap the header button in a heading of this level. The WAI-ARIA disclosure pattern\n * puts the button INSIDE the heading when the disclosure titles a section, so the\n * page's heading outline still lists it. Off by default: an inline \"show more\" is\n * not a section.\n */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n /** See {@link CollapseProps.keepMounted}. */\n keepMounted?: boolean;\n disabled?: boolean;\n /** Extra classes for the header button. */\n headerClassName?: string;\n /**\n * Attributes for the header BUTTON — the outer props go to the wrapping div — such as\n * an `aria-label`, an `id` or a `data-*` test hook. A function of the open state\n * when they depend on it: keksdose's budget table names each group's toggle\n * \"Expand group Food\" / \"Collapse group Food\", because the visible title is only the\n * group's name:\n *\n * ```tsx\n * triggerProps={(open) => ({ \"aria-label\": t(open ? \"collapseGroup\" : \"expandGroup\", { name }) })}\n * ```\n *\n * An `aria-label` REPLACES the title as the button's name, so it must still contain\n * the title's words (WCAG 2.5.3, label in name). See {@link DisclosureTriggerProps}\n * for what the disclosure keeps for itself.\n */\n triggerProps?: DisclosureTriggerProps | ((open: boolean) => DisclosureTriggerProps);\n /** Extra classes for the body's wrapper — where its padding and spacing live. */\n bodyClassName?: string;\n /**\n * Content at the far end of the header row, beside the title: a count, a date, a\n * status chip, an action button. It is a SIBLING of the header button, never inside\n * it — a button may not contain another interactive element, and a count inside it\n * would also be read as part of the button's name. The header button still spans\n * the row under it (a stretched hit area), so clicking the empty space or the\n * card's chevron still toggles, while whatever sits in `trailing` gets its own\n * clicks. With `trailing`, the card's chevron moves after it, to the row's end.\n */\n trailing?: ReactNode;\n /**\n * Trigger-only mode: the id of an element the CALLER renders elsewhere — the hidden\n * rows of a table, a panel in another column — which this header shows and hides.\n * The header's `aria-controls` points at it and the disclosure renders no body of\n * its own (`children`, `bodyClassName` and `keepMounted` are ignored). Pair it with\n * `open` / `onOpenChange`: the caller owns the state, since the caller renders what\n * it governs.\n */\n controls?: string;\n /** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls}). */\n children?: ReactNode;\n}\n\n/**\n * A section that opens in place: a header button with `aria-expanded` and\n * `aria-controls`, and a {@link Collapse} under it.\n *\n * Both apps had written this by hand — Lenkbank as a shared `CollapsibleCard` with ten\n * importers, Keksdose three separate times (accounts, twice; the support panel) — and\n * the copies had drifted: none of Keksdose's animated, and all three swapped a\n * `ChevronRight` for a `ChevronDown` rather than turning one. Here the chevron is one\n * icon that rotates, so the change of state is one element moving rather than two\n * trading places.\n *\n * The body is unmounted while shut; see {@link CollapseProps.keepMounted} for why that\n * is the default and when to opt out.\n */\nexport function Disclosure({\n title,\n hint,\n open: controlled,\n defaultOpen = false,\n onOpenChange,\n variant = \"card\",\n chevronPosition = \"start\",\n headingAs: Heading,\n keepMounted,\n disabled,\n headerClassName,\n triggerProps,\n bodyClassName,\n trailing,\n controls,\n className,\n children,\n ...rest\n}: DisclosureProps) {\n const [own, setOwn] = useState(defaultOpen);\n const open = controlled ?? own;\n const bodyId = useId();\n const card = variant === \"card\";\n // The card's chevron trails always; a bare one only when asked to.\n const chevronAtEnd = card || chevronPosition === \"end\";\n const triggerOnly = controls !== undefined;\n // The card's header squares its lower corners only when a body opens under it.\n const joined = open && !triggerOnly;\n const hasTrailing = trailing !== undefined && trailing !== null && trailing !== false;\n\n const toggle = () => {\n const next = !open;\n if (controlled === undefined) setOwn(next);\n onOpenChange?.(next);\n };\n\n const extraTriggerProps = typeof triggerProps === \"function\" ? triggerProps(open) : triggerProps;\n\n const button = (\n <button\n {...extraTriggerProps}\n type=\"button\"\n aria-expanded={open}\n aria-controls={triggerOnly ? controls : bodyId}\n disabled={disabled}\n onClick={toggle}\n data-disclosure-trigger=\"\"\n className={cn(\n \"flex w-full gap-2 text-start outline-none disabled:cursor-not-allowed disabled:opacity-50\",\n hasTrailing\n ? // Stretched over the whole header row (the row is `relative`), so the space\n // round `trailing` and the card's chevron still toggle; the ring and the\n // card's hover paint on the stretched area / the row, not the text box.\n cn(\n \"min-w-0 flex-1 after:absolute after:inset-0 after:content-['']\",\n \"focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-[var(--brand)]\",\n card ? cn(\"after:rounded-lg\", joined && \"after:rounded-b-none\") : \"after:rounded-sm\",\n )\n : \"focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n card\n ? cn(\n \"items-center justify-between rounded-lg p-4\",\n hasTrailing ? \"pe-0\" : \"hover:bg-[var(--bg-hover)] focus-visible:ring-inset\",\n joined && \"rounded-b-none\",\n )\n : \"items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]\",\n headerClassName,\n )}\n >\n {!chevronAtEnd && <Chevron open={open} leading />}\n {/* `flex-1`: the title takes the row, so whatever a caller puts at its end sits\n at the header's far edge. */}\n <span className=\"min-w-0 flex-1\">\n <span className={cn(\"block\", card && \"text-sm font-semibold text-[var(--text-primary)]\")}>{title}</span>\n {hint !== undefined && (\n <span className=\"mt-0.5 block text-xs font-normal text-[var(--text-muted)]\">{hint}</span>\n )}\n </span>\n {chevronAtEnd && !hasTrailing && <Chevron open={open} small={!card} />}\n </button>\n );\n\n // Preflight already makes h1–h6 inherit size and weight, so the heading adds\n // structure and nothing visible.\n const header = Heading ? <Heading className={hasTrailing ? \"min-w-0 flex-1\" : undefined}>{button}</Heading> : button;\n\n return (\n <div\n {...rest}\n className={cn(card && \"rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm\", className)}\n >\n {hasTrailing ? (\n <div\n className={cn(\n \"relative flex items-center gap-2\",\n card &&\n cn(\n \"rounded-lg pe-4 has-[[data-disclosure-trigger]:enabled:hover]:bg-[var(--bg-hover)]\",\n joined && \"rounded-b-none\",\n ),\n )}\n >\n {header}\n {/* Positioned, so it paints over the stretched button and takes its own\n clicks; the chevron is not, so a click on it lands on the button. */}\n <div className=\"relative flex shrink-0 items-center gap-2\">{trailing}</div>\n {chevronAtEnd && <Chevron open={open} small={!card} />}\n </div>\n ) : (\n header\n )}\n {!triggerOnly && (\n <Collapse id={bodyId} open={open} keepMounted={keepMounted}>\n <div className={cn(card ? \"space-y-3 px-4 pb-4\" : \"space-y-2 pt-2\", bodyClassName)}>{children}</div>\n </Collapse>\n )}\n </div>\n );\n}\n\n/**\n * One icon for both variants, turned rather than swapped. `leading` (bare) points it\n * along the reading direction while shut — right in LTR, left in RTL — and down when\n * open; trailing (card) points down while shut and up when open, which is the card\n * header's convention. Rotation, not a mirrored glyph, so RTL needs one opposite angle\n * and no `scale` composing with the turn.\n */\nfunction Chevron({\n open,\n leading = false,\n small = leading,\n}: {\n open: boolean;\n leading?: boolean;\n /** The bare variant's size, which a bare chevron keeps at either end of its row. */\n small?: boolean;\n}) {\n return (\n <ChevronDown\n aria-hidden\n className={cn(\n \"shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none\",\n small ? \"size-3.5\" : \"size-4\",\n leading ? !open && \"-rotate-90 rtl:rotate-90\" : open && \"rotate-180\",\n )}\n />\n );\n}\n"],"mappings":";AA8FM,cA4MA,YA5MA;AA9FN,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,mBAAmB;AAE5B,SAAS,UAAU;AAInB,MAAM,cAAc;AAKpB,SAAS,uBAAgC;AACvC,SACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kCAAkC,EAAE;AAE1D;AA2CO,SAAS,SAAS,EAAE,MAAM,cAAc,OAAO,UAAU,WAAW,OAAO,GAAG,KAAK,GAAkB;AAM1G,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,IAAI;AAC7C,MAAI,SAAS,UAAU;AACrB,gBAAY,IAAI;AAChB,iBAAa,CAAC,QAAQ,CAAC,qBAAqB,CAAC;AAAA,EAC/C;AACA,YAAU,MAAM;AACd,QAAI,CAAC,UAAW;AAChB,UAAM,QAAQ,WAAW,MAAM,aAAa,KAAK,GAAG,WAAW;AAC/D,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,UAAU,QAAQ,aAAa;AAErC,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO,CAAC;AAAA,MACR,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAGA,OAAO,EAAE,GAAG,OAAO,kBAAkB,OAAO,QAAQ,OAAO,YAAY,OAAO,YAAY,SAAS;AAAA,MAEnG;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,OAAO,gBAAgB;AAAA,UACzB;AAAA,UAEC,qBAAW;AAAA;AAAA,MACd;AAAA;AAAA,EACF;AAEJ;AAyHO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,WAAW;AAC1C,QAAM,OAAO,cAAc;AAC3B,QAAM,SAAS,MAAM;AACrB,QAAM,OAAO,YAAY;AAEzB,QAAM,eAAe,QAAQ,oBAAoB;AACjD,QAAM,cAAc,aAAa;AAEjC,QAAM,SAAS,QAAQ,CAAC;AACxB,QAAM,cAAc,aAAa,UAAa,aAAa,QAAQ,aAAa;AAEhF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,QAAI,eAAe,OAAW,QAAO,IAAI;AACzC,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,oBAAoB,OAAO,iBAAiB,aAAa,aAAa,IAAI,IAAI;AAEpF,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,iBAAe;AAAA,MACf,iBAAe,cAAc,WAAW;AAAA,MACxC;AAAA,MACA,SAAS;AAAA,MACT,2BAAwB;AAAA,MACxB,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA;AAAA;AAAA,UAII;AAAA,YACE;AAAA,YACA;AAAA,YACA,OAAO,GAAG,oBAAoB,UAAU,sBAAsB,IAAI;AAAA,UACpE;AAAA,YACA;AAAA,QACJ,OACI;AAAA,UACE;AAAA,UACA,cAAc,SAAS;AAAA,UACvB,UAAU;AAAA,QACZ,IACA;AAAA,QACJ;AAAA,MACF;AAAA,MAEC;AAAA,SAAC,gBAAgB,oBAAC,WAAQ,MAAY,SAAO,MAAC;AAAA,QAG/C,qBAAC,UAAK,WAAU,kBACd;AAAA,8BAAC,UAAK,WAAW,GAAG,SAAS,QAAQ,kDAAkD,GAAI,iBAAM;AAAA,UAChG,SAAS,UACR,oBAAC,UAAK,WAAU,6DAA6D,gBAAK;AAAA,WAEtF;AAAA,QACC,gBAAgB,CAAC,eAAe,oBAAC,WAAQ,MAAY,OAAO,CAAC,MAAM;AAAA;AAAA;AAAA,EACtE;AAKF,QAAM,SAAS,UAAU,oBAAC,WAAQ,WAAW,cAAc,mBAAmB,QAAY,kBAAO,IAAa;AAE9G,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,WAAW,GAAG,QAAQ,6EAA6E,SAAS;AAAA,MAE3G;AAAA,sBACC;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,QACE;AAAA,gBACE;AAAA,gBACA,UAAU;AAAA,cACZ;AAAA,YACJ;AAAA,YAEC;AAAA;AAAA,cAGD,oBAAC,SAAI,WAAU,6CAA6C,oBAAS;AAAA,cACpE,gBAAgB,oBAAC,WAAQ,MAAY,OAAO,CAAC,MAAM;AAAA;AAAA;AAAA,QACtD,IAEA;AAAA,QAED,CAAC,eACA,oBAAC,YAAS,IAAI,QAAQ,MAAY,aAChC,8BAAC,SAAI,WAAW,GAAG,OAAO,wBAAwB,kBAAkB,aAAa,GAAI,UAAS,GAChG;AAAA;AAAA;AAAA,EAEJ;AAEJ;AASA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AACV,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,WAAW;AAAA,QACT;AAAA,QACA,QAAQ,aAAa;AAAA,QACrB,UAAU,CAAC,QAAQ,6BAA6B,QAAQ;AAAA,MAC1D;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/disclosure.tsx"],"sourcesContent":["import { useEffect, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { ChevronDown } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\n\n/** How long the fold takes, in ms. The same number as the `duration-200` below and as\n * the unmount timer, because they are the same movement. */\nconst COLLAPSE_MS = 200;\n\n/** Read at the moment of closing, like `useCloseTransition` does: the setting can\n * change under a long-lived page, and jsdom/SSR have no `matchMedia` — where \"no\n * animation\" is also the only correct answer, since nothing is painting. */\nfunction prefersReducedMotion(): boolean {\n return (\n typeof window === \"undefined\" ||\n typeof window.matchMedia !== \"function\" ||\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n\n/**\n * `extends` the div's props so an `id` (what a trigger's `aria-controls` points at), a\n * `data-tour` anchor or a test id reaches the element that folds.\n */\nexport interface CollapseProps extends ComponentPropsWithoutRef<\"div\"> {\n open: boolean;\n /**\n * Keep the children mounted while shut (hidden and `inert`) instead of unmounting\n * them once the fold has finished closing.\n *\n * Off by default, and that default is a behaviour contract rather than a\n * performance note: Lenkbank's disclosure bodies FETCH, and a body that only exists\n * while open is what stops a closed card asking — without every caller carrying its\n * own `enabled: open`. On for a body whose state must outlive a close (a half-typed\n * form) or that must be found by the browser's find-in-page.\n */\n keepMounted?: boolean;\n children: ReactNode;\n}\n\n/**\n * The fold on its own: content that opens and shuts in place, animated to a height\n * nobody measured.\n *\n * THE TECHNIQUE is `AppShell`'s sidebar group's, lifted out so there is one copy of it.\n * A `grid-template-rows` transition from `0fr` to `1fr` is the one way to animate to a\n * content-sized height without measuring it — so a chart that resizes inside it, or a\n * translated line that wraps, is still exactly as tall as it needs. `visibility` rides\n * the same transition, so a shut body leaves the accessibility tree only once it has\n * finished closing; `inert` takes it out of the tab order immediately.\n *\n * THE CHILDREN are unmounted once closed (see {@link CollapseProps.keepMounted}), but\n * only once the track has finished closing: the track is what animates, so the\n * children have to survive the movement that hides them. Under reduced motion they\n * go at once — there is no movement to wait for — and the opening half is silenced by\n * `motion-reduce:transition-none`, since tokens.css only silences the overlay\n * animations by name.\n *\n * Padding and margins belong on a child: on this element, or the clipping one inside\n * it, they would hold the row open by that much.\n */\nexport function Collapse({ open, keepMounted = false, children, className, style, ...rest }: CollapseProps) {\n // Whether the children are still on their way out. Adjusted DURING render when\n // `open` flips (React's \"storing information from previous renders\"), so a close\n // under reduced motion unmounts in the same render that shut it — a test, and a\n // screen reader, see the body go on the click — and a re-open mid-fold simply\n // cancels the linger.\n const [lingering, setLingering] = useState(false);\n const [prevOpen, setPrevOpen] = useState(open);\n if (open !== prevOpen) {\n setPrevOpen(open);\n setLingering(!open && !prefersReducedMotion());\n }\n useEffect(() => {\n if (!lingering) return;\n const timer = setTimeout(() => setLingering(false), COLLAPSE_MS);\n return () => clearTimeout(timer);\n }, [lingering]);\n\n const mounted = open || lingering || keepMounted;\n\n return (\n <div\n {...rest}\n inert={!open}\n className={cn(\n \"grid transition-[grid-template-rows,visibility] duration-200 ease-out motion-reduce:transition-none\",\n className,\n )}\n // Inline rather than `grid-rows-[…]` classes: a caller's `className` must not be\n // able to pin the track open, and the two values ARE the state.\n style={{ ...style, gridTemplateRows: open ? \"1fr\" : \"0fr\", visibility: open ? \"visible\" : \"hidden\" }}\n >\n <div\n className={cn(\n \"min-h-0 overflow-hidden transition-opacity duration-200 ease-out motion-reduce:transition-none\",\n open ? \"opacity-100\" : \"opacity-0\",\n )}\n >\n {mounted && children}\n </div>\n </div>\n );\n}\n\n/**\n * What {@link DisclosureProps.triggerProps} may put on the header button: its own\n * attributes and handlers, and any `data-*`. Not the ones the disclosure owns —\n * `aria-expanded`, `aria-controls`, `disabled`, `onClick`, `type` and the content — nor\n * `className`, which is {@link DisclosureProps.headerClassName}.\n */\nexport type DisclosureTriggerProps = Omit<\n ComponentPropsWithoutRef<\"button\">,\n \"type\" | \"aria-expanded\" | \"aria-controls\" | \"disabled\" | \"onClick\" | \"children\" | \"className\"\n> & { [data: `data-${string}`]: string | number | boolean | undefined };\n\n/**\n * `title` is omitted from the div's own props because this component already owns the\n * name: here it is the header's content (and a ReactNode), not the browser's tooltip.\n * Everything else reaches the outer element.\n */\nexport interface DisclosureProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\"> {\n /** What the header says. A node, so a status chip or a count can sit in it. */\n title: ReactNode;\n /** The smaller line under the title — what is inside, for a reader deciding whether\n * to open it. */\n hint?: ReactNode;\n /**\n * Who decides whether this one is open. Left out, the disclosure decides for itself,\n * which is what one standing on its own wants. Passed, the caller does — a set of\n * which one is open at a time, or an open state kept in the URL so the view can be\n * linked to. There is no accordion component: \"one open at a time\" is a few lines in\n * the caller and it usually owns a URL parameter, which is app state.\n */\n open?: boolean;\n /** Whether an UNCONTROLLED disclosure starts open. Ignored when `open` is passed. */\n defaultOpen?: boolean;\n /** Called with the next state on every toggle, controlled or not. */\n onOpenChange?: (open: boolean) => void;\n /**\n * `card` (default) draws the kit's `Card` surface round header and body, with the\n * chevron trailing the header and turning over — the \"section that opens\" of a\n * settings or analysis page. `bare` draws nothing: a leading chevron that turns\n * down, for an inline \"Show 3 hidden accounts\" inside something that already has\n * its own surface.\n *\n * `menu` is a row of a `HoverMenu`: the top bar's menu-item look\n * (`TOPBAR_MENU_ITEM_CLASS` — full-width, `px-3 py-2`, regular weight, square, the\n * hover wash with the text colour left alone), an INSET focus ring so the menu's\n * clipping edge cannot cut it off, the chevron at the end (`chevronPosition`\n * defaults to `end` here) and a body with no padding of its own, so the sub-list's\n * rows sit flush like the rows around them. keksdose's account menu opens its\n * language sub-list this way, and on `bare` it took four header overrides and a\n * body one to get there (account-menu.tsx).\n */\n variant?: \"card\" | \"bare\" | \"menu\";\n /**\n * Where a `bare` disclosure draws its chevron. `start` (default) is the leading\n * chevron that turns from the reading direction to down. `end` puts it at the far\n * end of the row, pointing down and turning up as the card's does — keksdose's\n * account menu, whose language sub-list reads \"Language 🇩🇪 ⌄\" with the chevron\n * after the flag, like every other menu row with a sub-list. With `trailing`, the\n * chevron follows it, as on a card. A `card` always has it at the end, so this is\n * ignored there. A `menu` defaults to `end`, like the menu rows around it.\n */\n chevronPosition?: \"start\" | \"end\";\n /**\n * Wrap the header button in a heading of this level. The WAI-ARIA disclosure pattern\n * puts the button INSIDE the heading when the disclosure titles a section, so the\n * page's heading outline still lists it. Off by default: an inline \"show more\" is\n * not a section.\n */\n headingAs?: \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n /** See {@link CollapseProps.keepMounted}. */\n keepMounted?: boolean;\n disabled?: boolean;\n /** Extra classes for the header button. */\n headerClassName?: string;\n /**\n * Attributes for the header BUTTON — the outer props go to the wrapping div — such as\n * an `aria-label`, an `id` or a `data-*` test hook. A function of the open state\n * when they depend on it: keksdose's budget table names each group's toggle\n * \"Expand group Food\" / \"Collapse group Food\", because the visible title is only the\n * group's name:\n *\n * ```tsx\n * triggerProps={(open) => ({ \"aria-label\": t(open ? \"collapseGroup\" : \"expandGroup\", { name }) })}\n * ```\n *\n * An `aria-label` REPLACES the title as the button's name, so it must still contain\n * the title's words (WCAG 2.5.3, label in name). See {@link DisclosureTriggerProps}\n * for what the disclosure keeps for itself.\n */\n triggerProps?: DisclosureTriggerProps | ((open: boolean) => DisclosureTriggerProps);\n /** Extra classes for the body's wrapper — where its padding and spacing live. */\n bodyClassName?: string;\n /**\n * Content at the far end of the header row, beside the title: a count, a date, a\n * status chip, an action button. It is a SIBLING of the header button, never inside\n * it — a button may not contain another interactive element, and a count inside it\n * would also be read as part of the button's name. The header button still spans\n * the row under it (a stretched hit area), so clicking the empty space or the\n * card's chevron still toggles, while whatever sits in `trailing` gets its own\n * clicks. With `trailing`, the card's chevron moves after it, to the row's end.\n */\n trailing?: ReactNode;\n /**\n * Trigger-only mode: the id of an element the CALLER renders elsewhere — the hidden\n * rows of a table, a panel in another column — which this header shows and hides.\n * The header's `aria-controls` points at it and the disclosure renders no body of\n * its own (`children`, `bodyClassName` and `keepMounted` are ignored). Pair it with\n * `open` / `onOpenChange`: the caller owns the state, since the caller renders what\n * it governs.\n */\n controls?: string;\n /** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls}). */\n children?: ReactNode;\n}\n\n/**\n * A section that opens in place: a header button with `aria-expanded` and\n * `aria-controls`, and a {@link Collapse} under it.\n *\n * Both apps had written this by hand — Lenkbank as a shared `CollapsibleCard` with ten\n * importers, Keksdose three separate times (accounts, twice; the support panel) — and\n * the copies had drifted: none of Keksdose's animated, and all three swapped a\n * `ChevronRight` for a `ChevronDown` rather than turning one. Here the chevron is one\n * icon that rotates, so the change of state is one element moving rather than two\n * trading places.\n *\n * The body is unmounted while shut; see {@link CollapseProps.keepMounted} for why that\n * is the default and when to opt out.\n */\nexport function Disclosure({\n title,\n hint,\n open: controlled,\n defaultOpen = false,\n onOpenChange,\n variant = \"card\",\n chevronPosition,\n headingAs: Heading,\n keepMounted,\n disabled,\n headerClassName,\n triggerProps,\n bodyClassName,\n trailing,\n controls,\n className,\n children,\n ...rest\n}: DisclosureProps) {\n const [own, setOwn] = useState(defaultOpen);\n const open = controlled ?? own;\n const bodyId = useId();\n const card = variant === \"card\";\n const menu = variant === \"menu\";\n // The card's chevron trails always; a menu row's unless asked otherwise; a bare\n // one only when asked to.\n const chevronAtEnd = card || (chevronPosition ?? (menu ? \"end\" : \"start\")) === \"end\";\n const triggerOnly = controls !== undefined;\n // The card's header squares its lower corners only when a body opens under it.\n const joined = open && !triggerOnly;\n const hasTrailing = trailing !== undefined && trailing !== null && trailing !== false;\n\n const toggle = () => {\n const next = !open;\n if (controlled === undefined) setOwn(next);\n onOpenChange?.(next);\n };\n\n const extraTriggerProps = typeof triggerProps === \"function\" ? triggerProps(open) : triggerProps;\n\n const button = (\n <button\n {...extraTriggerProps}\n type=\"button\"\n aria-expanded={open}\n aria-controls={triggerOnly ? controls : bodyId}\n disabled={disabled}\n onClick={toggle}\n data-disclosure-trigger=\"\"\n className={cn(\n \"flex w-full gap-2 text-start outline-none disabled:cursor-not-allowed disabled:opacity-50\",\n hasTrailing\n ? // Stretched over the whole header row (the row is `relative`), so the space\n // round `trailing` and the card's chevron still toggle; the ring and the\n // card's hover paint on the stretched area / the row, not the text box.\n cn(\n \"min-w-0 flex-1 after:absolute after:inset-0 after:content-['']\",\n \"focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-[var(--brand)]\",\n card ? cn(\"after:rounded-lg\", joined && \"after:rounded-b-none\") : !menu && \"after:rounded-sm\",\n )\n : \"focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n card\n ? cn(\n \"items-center justify-between rounded-lg p-4\",\n hasTrailing ? \"pe-0\" : \"hover:bg-[var(--bg-hover)] focus-visible:ring-inset\",\n joined && \"rounded-b-none\",\n )\n : menu\n ? // TOPBAR_MENU_ITEM_CLASS's row, spelled out rather than imported: a\n // component does not reach up into the shell. A test holds the two to\n // the same classes.\n \"items-center justify-between gap-3 px-3 py-2 text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] focus-visible:ring-inset\"\n : \"items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]\",\n headerClassName,\n )}\n >\n {!chevronAtEnd && <Chevron open={open} leading />}\n {/* `flex-1`: the title takes the row, so whatever a caller puts at its end sits\n at the header's far edge. */}\n <span className=\"min-w-0 flex-1\">\n <span className={cn(\"block\", card && \"text-sm font-semibold text-[var(--text-primary)]\")}>{title}</span>\n {hint !== undefined && (\n <span className=\"mt-0.5 block text-xs font-normal text-[var(--text-muted)]\">{hint}</span>\n )}\n </span>\n {chevronAtEnd && !hasTrailing && <Chevron open={open} small={!card} />}\n </button>\n );\n\n // Preflight already makes h1–h6 inherit size and weight, so the heading adds\n // structure and nothing visible.\n const header = Heading ? <Heading className={hasTrailing ? \"min-w-0 flex-1\" : undefined}>{button}</Heading> : button;\n\n return (\n <div\n {...rest}\n className={cn(card && \"rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm\", className)}\n >\n {hasTrailing ? (\n <div\n className={cn(\n \"relative flex items-center gap-2\",\n card &&\n cn(\n \"rounded-lg pe-4 has-[[data-disclosure-trigger]:enabled:hover]:bg-[var(--bg-hover)]\",\n joined && \"rounded-b-none\",\n ),\n )}\n >\n {header}\n {/* Positioned, so it paints over the stretched button and takes its own\n clicks; the chevron is not, so a click on it lands on the button. */}\n <div className=\"relative flex shrink-0 items-center gap-2\">{trailing}</div>\n {chevronAtEnd && <Chevron open={open} small={!card} />}\n </div>\n ) : (\n header\n )}\n {!triggerOnly && (\n <Collapse id={bodyId} open={open} keepMounted={keepMounted}>\n <div className={cn(card ? \"space-y-3 px-4 pb-4\" : !menu && \"space-y-2 pt-2\", bodyClassName)}>{children}</div>\n </Collapse>\n )}\n </div>\n );\n}\n\n/**\n * One icon for both variants, turned rather than swapped. `leading` (bare) points it\n * along the reading direction while shut — right in LTR, left in RTL — and down when\n * open; trailing (card) points down while shut and up when open, which is the card\n * header's convention. Rotation, not a mirrored glyph, so RTL needs one opposite angle\n * and no `scale` composing with the turn.\n */\nfunction Chevron({\n open,\n leading = false,\n small = leading,\n}: {\n open: boolean;\n leading?: boolean;\n /** The bare variant's size, which a bare chevron keeps at either end of its row. */\n small?: boolean;\n}) {\n return (\n <ChevronDown\n aria-hidden\n className={cn(\n \"shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none\",\n small ? \"size-3.5\" : \"size-4\",\n leading ? !open && \"-rotate-90 rtl:rotate-90\" : open && \"rotate-180\",\n )}\n />\n );\n}\n"],"mappings":";AA8FM,cA4NA,YA5NA;AA9FN,SAAS,WAAW,OAAO,gBAAgB;AAE3C,SAAS,mBAAmB;AAE5B,SAAS,UAAU;AAInB,MAAM,cAAc;AAKpB,SAAS,uBAAgC;AACvC,SACE,OAAO,WAAW,eAClB,OAAO,OAAO,eAAe,cAC7B,OAAO,WAAW,kCAAkC,EAAE;AAE1D;AA2CO,SAAS,SAAS,EAAE,MAAM,cAAc,OAAO,UAAU,WAAW,OAAO,GAAG,KAAK,GAAkB;AAM1G,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,IAAI;AAC7C,MAAI,SAAS,UAAU;AACrB,gBAAY,IAAI;AAChB,iBAAa,CAAC,QAAQ,CAAC,qBAAqB,CAAC;AAAA,EAC/C;AACA,YAAU,MAAM;AACd,QAAI,CAAC,UAAW;AAChB,UAAM,QAAQ,WAAW,MAAM,aAAa,KAAK,GAAG,WAAW;AAC/D,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,UAAU,QAAQ,aAAa;AAErC,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO,CAAC;AAAA,MACR,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAGA,OAAO,EAAE,GAAG,OAAO,kBAAkB,OAAO,QAAQ,OAAO,YAAY,OAAO,YAAY,SAAS;AAAA,MAEnG;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,OAAO,gBAAgB;AAAA,UACzB;AAAA,UAEC,qBAAW;AAAA;AAAA,MACd;AAAA;AAAA,EACF;AAEJ;AAkIO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,cAAc;AAAA,EACd;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,WAAW;AAC1C,QAAM,OAAO,cAAc;AAC3B,QAAM,SAAS,MAAM;AACrB,QAAM,OAAO,YAAY;AACzB,QAAM,OAAO,YAAY;AAGzB,QAAM,eAAe,SAAS,oBAAoB,OAAO,QAAQ,cAAc;AAC/E,QAAM,cAAc,aAAa;AAEjC,QAAM,SAAS,QAAQ,CAAC;AACxB,QAAM,cAAc,aAAa,UAAa,aAAa,QAAQ,aAAa;AAEhF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,QAAI,eAAe,OAAW,QAAO,IAAI;AACzC,mBAAe,IAAI;AAAA,EACrB;AAEA,QAAM,oBAAoB,OAAO,iBAAiB,aAAa,aAAa,IAAI,IAAI;AAEpF,QAAM,SACJ;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,iBAAe;AAAA,MACf,iBAAe,cAAc,WAAW;AAAA,MACxC;AAAA,MACA,SAAS;AAAA,MACT,2BAAwB;AAAA,MACxB,WAAW;AAAA,QACT;AAAA,QACA;AAAA;AAAA;AAAA;AAAA,UAII;AAAA,YACE;AAAA,YACA;AAAA,YACA,OAAO,GAAG,oBAAoB,UAAU,sBAAsB,IAAI,CAAC,QAAQ;AAAA,UAC7E;AAAA,YACA;AAAA,QACJ,OACI;AAAA,UACE;AAAA,UACA,cAAc,SAAS;AAAA,UACvB,UAAU;AAAA,QACZ,IACA;AAAA;AAAA;AAAA;AAAA,UAIE;AAAA,YACA;AAAA,QACN;AAAA,MACF;AAAA,MAEC;AAAA,SAAC,gBAAgB,oBAAC,WAAQ,MAAY,SAAO,MAAC;AAAA,QAG/C,qBAAC,UAAK,WAAU,kBACd;AAAA,8BAAC,UAAK,WAAW,GAAG,SAAS,QAAQ,kDAAkD,GAAI,iBAAM;AAAA,UAChG,SAAS,UACR,oBAAC,UAAK,WAAU,6DAA6D,gBAAK;AAAA,WAEtF;AAAA,QACC,gBAAgB,CAAC,eAAe,oBAAC,WAAQ,MAAY,OAAO,CAAC,MAAM;AAAA;AAAA;AAAA,EACtE;AAKF,QAAM,SAAS,UAAU,oBAAC,WAAQ,WAAW,cAAc,mBAAmB,QAAY,kBAAO,IAAa;AAE9G,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,WAAW,GAAG,QAAQ,6EAA6E,SAAS;AAAA,MAE3G;AAAA,sBACC;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,QACE;AAAA,gBACE;AAAA,gBACA,UAAU;AAAA,cACZ;AAAA,YACJ;AAAA,YAEC;AAAA;AAAA,cAGD,oBAAC,SAAI,WAAU,6CAA6C,oBAAS;AAAA,cACpE,gBAAgB,oBAAC,WAAQ,MAAY,OAAO,CAAC,MAAM;AAAA;AAAA;AAAA,QACtD,IAEA;AAAA,QAED,CAAC,eACA,oBAAC,YAAS,IAAI,QAAQ,MAAY,aAChC,8BAAC,SAAI,WAAW,GAAG,OAAO,wBAAwB,CAAC,QAAQ,kBAAkB,aAAa,GAAI,UAAS,GACzG;AAAA;AAAA;AAAA,EAEJ;AAEJ;AASA,SAAS,QAAQ;AAAA,EACf;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AACV,GAKG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,WAAW;AAAA,QACT;AAAA,QACA,QAAQ,aAAa;AAAA,QACrB,UAAU,CAAC,QAAQ,6BAA6B,QAAQ;AAAA,MAC1D;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -20,6 +20,30 @@ interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButton
20
20
  * clears the transactions page's own action group this way (`calc(1rem + 4rem)`).
21
21
  */
22
22
  offset?: string;
23
+ /**
24
+ * Whether the button carries the browser's own tooltip (`title`, the `label` unless a
25
+ * `title` is passed). Default `true`, so an icon-only button keeps a hover hint for a
26
+ * mouse user with no other way to learn what it is. `false` renders no `title` at all:
27
+ * keksdose has ONE tooltip, the kit's `Tooltip` (dev#523), and its source scan fails a
28
+ * native `title=` anywhere — so a flag, not `title=""`, which the scan would still
29
+ * see. An empty `title` renders no attribute either, for the caller who has no scan.
30
+ *
31
+ * Not flipped to off by default: a FAB wrapped in nothing would lose the only hover
32
+ * hint it has, silently, in every app already on it. And the kit cannot wrap itself
33
+ * in its `Tooltip` — the bubble's anchor is a `relative inline-flex` span, and the
34
+ * button is `fixed` and portalled — so the one-tooltip caller wraps it.
35
+ */
36
+ nativeTitle?: boolean;
37
+ /**
38
+ * Make it a toggle. `true`/`false` set `aria-pressed` and swap the solid brand disc for
39
+ * a surface disc — the glyph in brand on the quiet brand fill when on, the secondary
40
+ * text colour when off — the "on" look of `IconButton`'s `pressed` and of a selected
41
+ * Chip. Left out, it is the ordinary action FAB. For keksdose's corner filter toggles
42
+ * (feedback-page's "awaiting only", the /transactions pending and upcoming toggles),
43
+ * which paint `text-[var(--brand)]` on a surface button by hand. Keep `label` the same
44
+ * in both states; `aria-pressed` already says which one it is in.
45
+ */
46
+ pressed?: boolean;
23
47
  ref?: Ref<HTMLButtonElement>;
24
48
  }
25
49
  /**
@@ -39,7 +63,7 @@ interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButton
39
63
  * Portalled, because `position: fixed` inside any transformed ancestor (an animated
40
64
  * page transition) is fixed to that ancestor, not to the screen.
41
65
  */
42
- declare function FloatingActionButton({ label, icon, corner, offset, className, style, type, ...rest }: FloatingActionButtonProps): react.JSX.Element;
66
+ declare function FloatingActionButton({ label, icon, corner, offset, nativeTitle, pressed, className, style, type, title, ...rest }: FloatingActionButtonProps): react.JSX.Element;
43
67
  interface FloatingPanelProps {
44
68
  /** The panel's heading, and its accessible name. */
45
69
  title: ReactNode;
@@ -57,6 +81,9 @@ interface FloatingPanelProps {
57
81
  /** The FAB's distance above the nav — see {@link FloatingActionButtonProps.offset}.
58
82
  * From `md` up the card sits above the FAB, so it moves with it. */
59
83
  offset?: string;
84
+ /** The FAB's native `title` — see {@link FloatingActionButtonProps.nativeTitle}.
85
+ * `false` for keksdose's assistant launcher, under its one-tooltip rule (dev#523). */
86
+ fabNativeTitle?: boolean;
60
87
  /** Default: `floatingPanel.close` from the {@link UiKitProvider}, else "Close". */
61
88
  closeLabel?: string;
62
89
  /**
@@ -102,6 +129,6 @@ interface FloatingPanelProps {
102
129
  * panel unmounts when closed; an app that must keep a draft across a close keeps it in
103
130
  * its own state, where it also survives a reload.
104
131
  */
105
- declare function FloatingPanel({ title, fabLabel, fabIcon, open: openProp, defaultOpen, onOpenChange, corner, offset, closeLabel, initialFocus, className, bodyClassName, children, }: FloatingPanelProps): react.JSX.Element;
132
+ declare function FloatingPanel({ title, fabLabel, fabIcon, open: openProp, defaultOpen, onOpenChange, corner, offset, fabNativeTitle, closeLabel, initialFocus, className, bodyClassName, children, }: FloatingPanelProps): react.JSX.Element;
106
133
 
107
134
  export { DEFAULT_FLOATING_PANEL_LABELS, FloatingActionButton, type FloatingActionButtonProps, type FloatingCorner, FloatingPanel, type FloatingPanelLabels, type FloatingPanelProps };
@@ -30,12 +30,17 @@ function FloatingActionButton({
30
30
  icon,
31
31
  corner = "bottom-end",
32
32
  offset = "1rem",
33
+ nativeTitle = true,
34
+ pressed,
33
35
  className,
34
36
  style,
35
37
  type = "button",
38
+ title,
36
39
  ...rest
37
40
  }) {
38
41
  const [marker, dir] = usePortalDir();
42
+ const toggle = pressed !== void 0;
43
+ const nativeTip = nativeTitle && title !== "" ? title ?? label : void 0;
39
44
  return /* @__PURE__ */ jsxs(Fragment, { children: [
40
45
  /* @__PURE__ */ jsx("span", { ref: marker, hidden: true }),
41
46
  typeof document !== "undefined" && createPortal(
@@ -46,11 +51,18 @@ function FloatingActionButton({
46
51
  type,
47
52
  dir,
48
53
  "aria-label": label,
49
- title: rest.title ?? label,
54
+ "aria-pressed": pressed ?? rest["aria-pressed"],
55
+ title: nativeTip,
50
56
  style: { ...cornerStyle(corner, offset), ...style },
51
57
  className: cn(
52
58
  "fixed z-40 inline-flex size-12 items-center justify-center rounded-full",
53
- "bg-[var(--brand)] text-[var(--brand-contrast)] shadow-lg transition-colors hover:bg-[var(--brand-hover)]",
59
+ "shadow-lg transition-colors",
60
+ !toggle && "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]",
61
+ // A toggle sits on the page's surface — a solid brand disc would read as
62
+ // "on" in both states — and says "on" in the brand family.
63
+ toggle && "border border-[var(--border)]",
64
+ pressed === false && "bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
65
+ pressed === true && "border-[var(--brand)] bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]",
54
66
  "outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2",
55
67
  "[&_svg]:size-6",
56
68
  className
@@ -71,6 +83,7 @@ function FloatingPanel({
71
83
  onOpenChange,
72
84
  corner = "bottom-end",
73
85
  offset = "1rem",
86
+ fabNativeTitle,
74
87
  closeLabel,
75
88
  initialFocus,
76
89
  className,
@@ -129,6 +142,7 @@ function FloatingPanel({
129
142
  icon: fabIcon,
130
143
  corner,
131
144
  offset,
145
+ nativeTitle: fabNativeTitle,
132
146
  "aria-expanded": open,
133
147
  "aria-controls": open ? panelId : void 0,
134
148
  onClick: () => {