@eifi1/ui-kit 0.6.1 → 0.7.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.
- package/README.md +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +36 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +9 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +31 -7
- package/dist/components/ui.js +26 -18
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +43 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +43 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +43 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +43 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +43 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +43 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +43 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +133 -84
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +74 -30
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +200 -129
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useMemo, useRef } from "react";
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Calendar, CalendarClock, ChevronLeft, ChevronRight, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { dirOf } from "../lib/direction.js";
|
|
6
7
|
import { addDaysIso, parseIsoDate } from "../lib/dates.js";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_DATE_PICKER_LABELS,
|
|
@@ -57,7 +58,7 @@ function DateFieldTrigger({
|
|
|
57
58
|
"aria-invalid": invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || void 0,
|
|
58
59
|
className: cn(
|
|
59
60
|
FIELD_TRIGGER,
|
|
60
|
-
"
|
|
61
|
+
"pe-9",
|
|
61
62
|
padded && FIELD_FLOATING_PAD,
|
|
62
63
|
disabled && "cursor-not-allowed opacity-50",
|
|
63
64
|
invalid && FIELD_INVALID
|
|
@@ -83,6 +84,8 @@ function DateField({
|
|
|
83
84
|
}) {
|
|
84
85
|
const showClear = Boolean(clearable && hasValue && !disabled);
|
|
85
86
|
const [aria, wrapperRest] = splitTriggerAria(rest);
|
|
87
|
+
const rootRef = useRef(null);
|
|
88
|
+
const [dir, setDir] = useState("ltr");
|
|
86
89
|
const id = useId();
|
|
87
90
|
const labelId = `${id}-label`;
|
|
88
91
|
const valueId = `${id}-value`;
|
|
@@ -91,7 +94,7 @@ function DateField({
|
|
|
91
94
|
return (
|
|
92
95
|
// The caller's attributes land here, on the field's own box — the trigger inside is
|
|
93
96
|
// named by `aria-labelledby` and must keep the id pair it is given.
|
|
94
|
-
/* @__PURE__ */ jsxs("div", { ...wrapperRest, className: cn("relative", className), children: [
|
|
97
|
+
/* @__PURE__ */ jsxs("div", { ...wrapperRest, ref: rootRef, className: cn("relative", className), children: [
|
|
95
98
|
label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
|
|
96
99
|
named && /* @__PURE__ */ jsx("span", { id: labelId, "aria-hidden": true, className: "sr-only-fixed", children: label }),
|
|
97
100
|
/* @__PURE__ */ jsx(
|
|
@@ -100,11 +103,15 @@ function DateField({
|
|
|
100
103
|
width,
|
|
101
104
|
panelId,
|
|
102
105
|
labels: { panel: panelLabel },
|
|
106
|
+
dir,
|
|
103
107
|
trigger: ({ open, toggle, ref }) => /* @__PURE__ */ jsx(
|
|
104
108
|
DateFieldTrigger,
|
|
105
109
|
{
|
|
106
110
|
open,
|
|
107
|
-
toggle
|
|
111
|
+
toggle: () => {
|
|
112
|
+
setDir(dirOf(rootRef.current));
|
|
113
|
+
toggle();
|
|
114
|
+
},
|
|
108
115
|
triggerRef: ref,
|
|
109
116
|
triggerText,
|
|
110
117
|
hasValue,
|
|
@@ -130,14 +137,14 @@ function DateField({
|
|
|
130
137
|
type: "button",
|
|
131
138
|
"aria-label": clearLabel,
|
|
132
139
|
onClick: onClear,
|
|
133
|
-
className: "absolute
|
|
140
|
+
className: "absolute end-1.5 top-1/2 -translate-y-1/2 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
|
|
134
141
|
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
135
142
|
}
|
|
136
143
|
) : /* @__PURE__ */ jsx(
|
|
137
144
|
Calendar,
|
|
138
145
|
{
|
|
139
146
|
"aria-hidden": true,
|
|
140
|
-
className: "pointer-events-none absolute
|
|
147
|
+
className: "pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-[var(--text-placeholder)]"
|
|
141
148
|
}
|
|
142
149
|
)
|
|
143
150
|
] })
|
|
@@ -148,6 +155,7 @@ function StepButton({
|
|
|
148
155
|
label,
|
|
149
156
|
disabled,
|
|
150
157
|
onClick,
|
|
158
|
+
mirror,
|
|
151
159
|
className
|
|
152
160
|
}) {
|
|
153
161
|
return /* @__PURE__ */ jsx(Tooltip, { label, portal: true, children: /* @__PURE__ */ jsx(
|
|
@@ -171,7 +179,7 @@ function StepButton({
|
|
|
171
179
|
"disabled:cursor-not-allowed disabled:text-[var(--text-placeholder)] disabled:opacity-60 disabled:hover:bg-[var(--bg-surface)]",
|
|
172
180
|
className
|
|
173
181
|
),
|
|
174
|
-
children: /* @__PURE__ */ jsx(Icon, { className: "size-4" })
|
|
182
|
+
children: /* @__PURE__ */ jsx(Icon, { className: cn("size-4", mirror && "rtl:-scale-x-100") })
|
|
175
183
|
}
|
|
176
184
|
) });
|
|
177
185
|
}
|
|
@@ -282,6 +290,7 @@ function DatePicker({
|
|
|
282
290
|
StepButton,
|
|
283
291
|
{
|
|
284
292
|
icon: ChevronLeft,
|
|
293
|
+
mirror: true,
|
|
285
294
|
label: text.previousDay,
|
|
286
295
|
disabled: blocked(-1),
|
|
287
296
|
onClick: () => onChange(target(-1)),
|
|
@@ -293,6 +302,7 @@ function DatePicker({
|
|
|
293
302
|
StepButton,
|
|
294
303
|
{
|
|
295
304
|
icon: ChevronRight,
|
|
305
|
+
mirror: true,
|
|
296
306
|
label: text.nextDay,
|
|
297
307
|
disabled: blocked(1),
|
|
298
308
|
onClick: () => onChange(target(1)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/date-picker.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef } 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 { 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 { 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 \"pr-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 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} 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 trigger={({ open, toggle, ref }) => (\n <DateFieldTrigger\n open={open}\n toggle={toggle}\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 right-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 right-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 className,\n}: {\n icon: ComponentType<{ className?: string }>;\n label: string;\n disabled: boolean;\n onClick: () => void;\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=\"size-4\" />\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 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 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 label: ReactNode;\n from: string;\n to: string;\n}\n\nexport interface DateRangePickerProps extends DatePickerBaseProps {\n from: string;\n to: string;\n onChange: (from: string, to: string) => void;\n /** Separator between the two formatted dates in the trigger (default \" – \"). */\n separator?: string;\n /** Optional named ranges rendered as a left column (last month / YTD / …). */\n presets?: DateRangePickerPreset[];\n}\n\n/** Two-date range picker: click a start then an end; closes once both are set.\n * With `presets`, a left column of named ranges is shown beside the calendar. */\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 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 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 calendar = (close: () => void) => (\n <MiniCalendar\n focusOnOpen\n from={from}\n to={to}\n locale={locale}\n min={min}\n max={max}\n labels={calendarLabels}\n onSelect={(f, t) => {\n onChange(f, t);\n // Two-click range: only dismiss once both ends are chosen.\n if (f && t) close();\n }}\n />\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={() => onChange(\"\", \"\")}\n >\n {(close) =>\n hasPresets ? (\n <div className=\"flex gap-3\">\n {/* `border-e`/`pe`, not `-r`: the preset column is on the START side, and the\n rule between it and the calendar has to follow it in a right-to-left UI. */}\n <div className=\"flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2\">\n {presets!.map((p, i) => {\n const selected = p.from === from && p.to === to;\n return (\n <button\n key={i}\n type=\"button\"\n onClick={() => {\n onChange(p.from, p.to);\n close();\n }}\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 {/* `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(close)}</div>\n </div>\n ) : (\n calendar(close)\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 useKitLabels,\n useKitLocale,\n type DatePickerLabels,\n} from \"../i18n/kit-labels\";\nimport { splitTriggerAria } from \"./trigger-aria\";\nimport type { TriggerAria } from \"./trigger-aria\";\nimport { 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 label: ReactNode;\n from: string;\n to: string;\n}\n\nexport interface DateRangePickerProps extends DatePickerBaseProps {\n from: string;\n to: string;\n onChange: (from: string, to: string) => void;\n /** Separator between the two formatted dates in the trigger (default \" – \"). */\n separator?: string;\n /** Optional named ranges rendered as a left column (last month / YTD / …). */\n presets?: DateRangePickerPreset[];\n}\n\n/** Two-date range picker: click a start then an end; closes once both are set.\n * With `presets`, a left column of named ranges is shown beside the calendar. */\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 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 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 calendar = (close: () => void) => (\n <MiniCalendar\n focusOnOpen\n from={from}\n to={to}\n locale={locale}\n min={min}\n max={max}\n labels={calendarLabels}\n onSelect={(f, t) => {\n onChange(f, t);\n // Two-click range: only dismiss once both ends are chosen.\n if (f && t) close();\n }}\n />\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={() => onChange(\"\", \"\")}\n >\n {(close) =>\n hasPresets ? (\n <div className=\"flex gap-3\">\n {/* `border-e`/`pe`, not `-r`: the preset column is on the START side, and the\n rule between it and the calendar has to follow it in a right-to-left UI. */}\n <div className=\"flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2\">\n {presets!.map((p, i) => {\n const selected = p.from === from && p.to === to;\n return (\n <button\n key={i}\n type=\"button\"\n onClick={() => {\n onChange(p.from, p.to);\n close();\n }}\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 {/* `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(close)}</div>\n </div>\n ) : (\n calendar(close)\n )\n }\n </DateField>\n );\n}\n"],"mappings":";AAsKM,cAwEF,YAxEE;AAtKN,SAAS,WAAW,OAAO,SAAS,QAAQ,gBAAgB;AAE5D,SAAS,UAAU,eAAe,aAAa,cAAc,SAAS;AACtE,SAAS,UAAU;AACnB,SAAS,aAA6B;AACtC,SAAS,YAAY,oBAAoB;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,wBAAwB;AAEjC,SAAS,YAAY,YAAY,eAAe,oBAAoB,qBAAqB;AACzF,SAAS,oBAA4C;AACrD,SAAS,eAAe;AACxB,SAAS,eAAe;AA+CxB,SAAS,WAAW,KAAa,QAA4B,SAAsC;AACjG,SAAO,aAAa,GAAG,GAAG,mBAAmB,QAAQ,OAAO,KAAK;AACnE;AASA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAgBG;AACD,QAAM,UAAU,OAAO,KAAK;AAC5B,YAAU,MAAM;AACd,UAAM,aAAa,QAAQ,WAAW,CAAC;AACvC,YAAQ,UAAU;AAClB,QAAI,CAAC,WAAY;AAOjB,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,UAAU,WAAW,SAAS,KAAM,YAAW,SAAS,MAAM;AAAA,EAMrE,GAAG,CAAC,MAAM,UAAU,CAAC;AAErB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,MAAK;AAAA,MACL;AAAA,MACA,SAAS;AAAA,MAWT,IAAI,KAAK;AAAA,MACT,mBAAiB,KAAK,YAAY,KAAK,CAAC,KAAK,iBAAiB,IAAI,SAAY;AAAA,MAC9E,cAAY,KAAK,YAAY;AAAA,MAC7B,oBAAkB,KAAK,kBAAkB;AAAA,MASzC,MAAK;AAAA,MACL,iBAAc;AAAA,MACd,iBAAe;AAAA,MACf,iBAAe;AAAA,MACf,gBAAc,WAAW,KAAK,cAAc,MAAM,QAAQ,KAAK,cAAc,MAAM,UAAU;AAAA,MAC7F,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,WAAW;AAAA,MACb;AAAA,MAEA,8BAAC,UAAK,IAAI,SAAS,WAAW,GAAG,YAAY,CAAC,YAAY,gCAAgC,GAcvF,yBAAe,QAClB;AAAA;AAAA,EACF;AAEJ;AAIA,SAAS,UAAU;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAgBG;AACD,QAAM,YAAY,QAAQ,aAAa,YAAY,CAAC,QAAQ;AAC5D,QAAM,CAAC,MAAM,WAAW,IAAI,iBAAiB,IAAI;AAKjD,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,CAAC,KAAK,MAAM,IAAI,SAAoB,KAAK;AAC/C,QAAM,KAAK,MAAM;AACjB,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,UAAU,GAAG,EAAE;AACrB,QAAM,UAAU,GAAG,EAAE;AAIrB,QAAM,QAAQ,OAAO,UAAU;AAC/B;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAK,GAAG,aAAa,KAAK,SAAS,WAAW,GAAG,YAAY,SAAS,GACpE;AAAA,gBAAU,UAAa,oBAAC,cAAY,iBAAM;AAAA,MAQ1C,SACC,oBAAC,UAAK,IAAI,SAAS,eAAW,MAAC,WAAU,iBACtC,iBACH;AAAA,MAEF;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UAGA,QAAQ,EAAE,OAAO,WAAW;AAAA,UAC5B;AAAA,UACA,SAAS,CAAC,EAAE,MAAM,QAAQ,IAAI,MAC5B;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,QAAQ,MAAM;AACZ,uBAAO,MAAM,QAAQ,OAAO,CAAC;AAC7B,uBAAO;AAAA,cACT;AAAA,cACA,YAAY;AAAA,cACZ;AAAA,cACA;AAAA,cAcA,YAAY;AAAA,gBACV,KAAK,iBAAiB;AAAA,gBACtB,QAAQ,UAAU,CAAC,KAAK,iBAAiB,KAAK,KAAK,KAAK,KAAK,KAAK;AAAA,gBAClE;AAAA,cACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AAAA,cACX;AAAA,cACA;AAAA,cACA;AAAA,cACA,QAAQ,UAAU;AAAA,cAClB;AAAA,cACA;AAAA;AAAA,UACF;AAAA,UAGD,WAAC,UAAU,SAAS,KAAK;AAAA;AAAA,MAC5B;AAAA,MACC,YACC;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAY;AAAA,UACZ,SAAS;AAAA,UACT,WAAU;AAAA,UAEV,8BAAC,KAAE,WAAU,UAAS;AAAA;AAAA,MACxB,IAEA;AAAA,QAAC;AAAA;AAAA,UACC,eAAW;AAAA,UACX,WAAU;AAAA;AAAA,MACZ;AAAA,OAEJ;AAAA;AAEJ;AAGA,SAAS,WAAW;AAAA,EAClB,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUG;AACD,SACE,oBAAC,WAAQ,OAAc,QAAM,MAC3B;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA,WAAW;AAAA,QACT;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA;AAAA;AAAA,QAGA;AAAA;AAAA;AAAA,QAGA;AAAA,QACA;AAAA,MACF;AAAA,MAEA,8BAAC,QAAK,WAAW,GAAG,UAAU,UAAU,kBAAkB,GAAG;AAAA;AAAA,EAC/D,GACF;AAEJ;AAyCA,SAAS,oBACP,YACA,YACA,YACkB;AAClB,QAAM,OAAO,YAAY;AACzB,QAAM,OAAO,YAAY;AACzB,QAAM,YAAY,QAAQ,MAAM;AAC9B,UAAM,MAAiC,CAAC;AACxC,QAAI,eAAe,OAAW,KAAI,QAAQ;AAC1C,QAAI,SAAS,OAAW,KAAI,cAAc;AAC1C,QAAI,SAAS,OAAW,KAAI,UAAU;AACtC,QAAI,eAAe,OAAW,KAAI,QAAQ;AAC1C,WAAO;AAAA,EACT,GAAG,CAAC,YAAY,MAAM,MAAM,UAAU,CAAC;AACvC,SAAO,aAAa,cAAc,4BAA4B,SAAS;AACzE;AAGO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,OAAO,oBAAoB,YAAY,YAAY,UAAU;AACnE,QAAM,SAAS,gBAAgB,CAAC,QAAgB,WAAW,KAAK,QAAQ,aAAa;AAKrF,QAAM,UAAU,QAAQ,QAAQ,KAAK;AACrC,QAAM,CAAC,aAAa,WAAW,IAAI,iBAAiB,IAAI;AACxD,QAAM,QACJ;AAAA,IAAC;AAAA;AAAA,MAGE,GAAI,UAAU,cAAc;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,MACA,WAAW,OAAO,mBAAmB;AAAA,MACrC,UAAU,QAAQ,KAAK;AAAA,MACvB,aAAa,QAAQ,OAAO,KAAK,IAAK,eAAe;AAAA,MACrD,SAAS,MAAM,SAAS,EAAE;AAAA,MAEzB,WAAC,UACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,aAAW;AAAA,UACX,MAAM;AAAA,UACN,IAAI;AAAA,UACJ;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR,UAAU,CAAC,QAAQ;AACjB,qBAAS,GAAG;AACZ,kBAAM;AAAA,UACR;AAAA;AAAA,MACF;AAAA;AAAA,EAEJ;AAEF,MAAI,CAAC,QAAS,QAAO;AAIrB,QAAM,SAAS,CAAC,SAAkB,QAAQ,WAAW,OAAO,IAAI,IAAI;AACpE,QAAM,cAAc,CAAC,QAAgB,QAAS,OAAO,MAAM,OAAS,OAAO,MAAM,GAAI;AACrF,QAAM,UAAU,CAAC,SAAiB;AAChC,UAAM,OAAO,OAAO,IAAI;AACxB,QAAI,CAAC,QAAQ,SAAU,QAAO;AAC9B,WAAO,YAAY,IAAI;AAAA,EACzB;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAUE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ,WAAW;AAAA,UACT;AAAA,UACA,QAAQ;AAAA,WACP,QAAQ,UAAU;AAAA,UACnB;AAAA,UACA;AAAA,QACF;AAAA,QAEC;AAAA,kBACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN,QAAM;AAAA,cACN,OAAO,KAAK;AAAA,cACZ,UAAU,QAAQ,EAAE;AAAA,cACpB,SAAS,MAAM,SAAS,OAAO,EAAE,CAAC;AAAA,cAClC,WAAU;AAAA;AAAA,UACZ;AAAA,UAED;AAAA,UACA,QACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN,QAAM;AAAA,cACN,OAAO,KAAK;AAAA,cACZ,UAAU,QAAQ,CAAC;AAAA,cACnB,SAAS,MAAM,SAAS,OAAO,CAAC,CAAC;AAAA,cACjC,WAAW,GAAG,kBAAkB,SAAS,gBAAgB;AAAA;AAAA,UAC3D;AAAA,UAED,SACC;AAAA,YAAC;AAAA;AAAA,cACC,MAAM;AAAA,cACN,OAAO,KAAK;AAAA,cACZ,UAAU,QAAQ,QAAQ,KAAK,UAAU,SAAS,YAAY,KAAK;AAAA,cACnE,SAAS,MAAM,SAAS,KAAK;AAAA,cAC7B,WAAU;AAAA;AAAA,UACZ;AAAA;AAAA;AAAA,IAEJ;AAAA;AAEJ;AAoBO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAyB;AACvB,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,OAAO,oBAAoB,YAAY,QAAW,MAAS;AACjE,QAAM,SAAS,gBAAgB,CAAC,QAAgB,WAAW,KAAK,QAAQ,aAAa;AACrF,QAAM,IAAI,OAAO,OAAO,IAAI,IAAI;AAChC,QAAM,IAAI,KAAK,OAAO,EAAE,IAAI;AAC5B,QAAM,cAAc,OAChB,KACE,GAAG,CAAC,GAAG,SAAS,GAAG,CAAC,KACpB,GAAG,CAAC,GAAG,SAAS,WACjB,eAAe;AACpB,QAAM,aAAa,QAAQ,WAAW,QAAQ,SAAS,CAAC;AACxD,QAAM,WAAW,CAAC,UAChB;AAAA,IAAC;AAAA;AAAA,MACC,aAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,UAAU,CAAC,GAAG,MAAM;AAClB,iBAAS,GAAG,CAAC;AAEb,YAAI,KAAK,EAAG,OAAM;AAAA,MACpB;AAAA;AAAA,EACF;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA,YAAY,KAAK;AAAA,MACjB,YAAY,KAAK;AAAA,MACjB;AAAA,MACA;AAAA,MAEA,OAAO,aAAa,MAAM;AAAA,MAC1B,UAAU,QAAQ,QAAQ,EAAE;AAAA,MAC5B;AAAA,MACA,SAAS,MAAM,SAAS,IAAI,EAAE;AAAA,MAE7B,WAAC,UACA,aACE,qBAAC,SAAI,WAAU,cAGb;AAAA,4BAAC,SAAI,WAAU,4EACZ,kBAAS,IAAI,CAAC,GAAG,MAAM;AACtB,gBAAM,WAAW,EAAE,SAAS,QAAQ,EAAE,OAAO;AAC7C,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,SAAS,MAAM;AACb,yBAAS,EAAE,MAAM,EAAE,EAAE;AACrB,sBAAM;AAAA,cACR;AAAA,cACA,WAAW;AAAA,gBACT;AAAA,gBACA,WACI,iEACA;AAAA,cACN;AAAA,cAEC,YAAE;AAAA;AAAA,YAbE;AAAA,UAcP;AAAA,QAEJ,CAAC,GACH;AAAA,QAGA,oBAAC,SAAI,WAAU,kBAAkB,mBAAS,KAAK,GAAE;AAAA,SACnD,IAEA,SAAS,KAAK;AAAA;AAAA,EAGpB;AAEJ;","names":[]}
|
|
@@ -35,6 +35,15 @@ interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "children" |
|
|
|
35
35
|
* calling `onClose` directly unmounts it at once.
|
|
36
36
|
*/
|
|
37
37
|
actions?: ReactNode | ((close: () => void) => ReactNode);
|
|
38
|
+
/**
|
|
39
|
+
* Controls that belong to the HEADER rather than to the actions row — an "Edit"
|
|
40
|
+
* toggle, a status badge with a menu, a "Copy link" (kastlan). Rendered beside the
|
|
41
|
+
* title, before the optional X; on a phone, where title and controls do not fit on
|
|
42
|
+
* one line, they wrap under the title and the X keeps its corner.
|
|
43
|
+
*
|
|
44
|
+
* A function receives the animated close, as `actions` does.
|
|
45
|
+
*/
|
|
46
|
+
headerActions?: ReactNode | ((close: () => void) => ReactNode);
|
|
38
47
|
/**
|
|
39
48
|
* Show an X in the header. Off by default: a centred dialog has a backdrop and
|
|
40
49
|
* Escape, and a form dialog has a Cancel. On for a dialog that commits as it goes
|
|
@@ -45,8 +54,24 @@ interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "children" |
|
|
|
45
54
|
closeLabel?: string;
|
|
46
55
|
/** Extra classes for the scrolling body (default spacing `space-y-3`). */
|
|
47
56
|
bodyClassName?: string;
|
|
48
|
-
/**
|
|
49
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Extra classes for the header row (default padding `px-4 pt-4 pb-3`), merged last —
|
|
59
|
+
* `px-3` for a phone sheet whose body runs at `px-3`, so heading and fields share
|
|
60
|
+
* one gutter.
|
|
61
|
+
*/
|
|
62
|
+
headerClassName?: string;
|
|
63
|
+
/**
|
|
64
|
+
* A rule under the header, the twin of the one over the actions row — for a tall or
|
|
65
|
+
* full-screen body that scrolls under a header which stays. The body then starts a
|
|
66
|
+
* step below the rule instead of flush against it.
|
|
67
|
+
*/
|
|
68
|
+
headerDivider?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The body. Optional: a dialog whose title, description and actions are the whole
|
|
71
|
+
* of it (a "you have unsynced changes" question) leaves it out, and the frame then
|
|
72
|
+
* renders no body at all rather than an empty padded one.
|
|
73
|
+
*/
|
|
74
|
+
children?: ReactNode;
|
|
50
75
|
}
|
|
51
76
|
/**
|
|
52
77
|
* A {@link Modal} with the frame every caller was writing by hand: a heading, an
|
|
@@ -79,6 +104,6 @@ interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "children" |
|
|
|
79
104
|
* does not pop a phone's keyboard; an `autoFocus` in the body overrides that from the
|
|
80
105
|
* caller's side, and should be a decision rather than a habit.
|
|
81
106
|
*/
|
|
82
|
-
declare function DialogFrame({ title, description, headingAs: Heading, actions, closeButton, closeLabel, bodyClassName, className, children, "aria-describedby": describedBy, ...modal }: DialogFrameProps): react.JSX.Element;
|
|
107
|
+
declare function DialogFrame({ title, description, headingAs: Heading, actions, headerActions, closeButton, closeLabel, bodyClassName, headerClassName, headerDivider, className, children, "aria-describedby": describedBy, ...modal }: DialogFrameProps): react.JSX.Element;
|
|
83
108
|
|
|
84
109
|
export { DEFAULT_DIALOG_FRAME_LABELS, DialogFrame, type DialogFrameLabels, type DialogFrameProps };
|
|
@@ -11,9 +11,12 @@ function DialogFrame({
|
|
|
11
11
|
description,
|
|
12
12
|
headingAs: Heading = "h2",
|
|
13
13
|
actions,
|
|
14
|
+
headerActions,
|
|
14
15
|
closeButton = false,
|
|
15
16
|
closeLabel,
|
|
16
17
|
bodyClassName,
|
|
18
|
+
headerClassName,
|
|
19
|
+
headerDivider = false,
|
|
17
20
|
className,
|
|
18
21
|
children,
|
|
19
22
|
"aria-describedby": describedBy,
|
|
@@ -22,6 +25,7 @@ function DialogFrame({
|
|
|
22
25
|
const titleId = useId();
|
|
23
26
|
const descriptionId = useId();
|
|
24
27
|
const hasDescription = description !== void 0 && description !== null;
|
|
28
|
+
const hasBody = children !== void 0 && children !== null && children !== false;
|
|
25
29
|
return /* @__PURE__ */ jsxs(
|
|
26
30
|
Modal,
|
|
27
31
|
{
|
|
@@ -30,26 +34,41 @@ function DialogFrame({
|
|
|
30
34
|
"aria-describedby": [hasDescription ? descriptionId : void 0, describedBy].filter(Boolean).join(" ") || void 0,
|
|
31
35
|
className: cn("flex flex-col overflow-hidden p-0", className),
|
|
32
36
|
children: [
|
|
33
|
-
/* @__PURE__ */ jsxs(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/* @__PURE__ */ jsxs(
|
|
38
|
+
"div",
|
|
39
|
+
{
|
|
40
|
+
className: cn(
|
|
41
|
+
// `last:pb-4`: a frame with neither body nor actions closes on its header.
|
|
42
|
+
"flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3 last:pb-4",
|
|
43
|
+
headerDivider && "border-b border-[var(--border)]",
|
|
44
|
+
headerClassName
|
|
45
|
+
),
|
|
46
|
+
children: [
|
|
47
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-start justify-between gap-x-3 gap-y-2", children: [
|
|
48
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 grow basis-48", children: [
|
|
49
|
+
/* @__PURE__ */ jsx(Heading, { id: titleId, className: "text-lg font-semibold leading-snug text-[var(--text-primary)]", children: title }),
|
|
50
|
+
hasDescription && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "mt-0.5 text-sm text-[var(--text-muted)]", children: description })
|
|
51
|
+
] }),
|
|
52
|
+
headerActions !== void 0 && headerActions !== null && headerActions !== false && /* @__PURE__ */ jsx(FrameHeaderActions, { actions: headerActions, onClose: modal.onClose })
|
|
53
|
+
] }),
|
|
54
|
+
closeButton && /* @__PURE__ */ jsx(FrameClose, { label: closeLabel, onClose: modal.onClose })
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
hasBody && /* @__PURE__ */ jsx(
|
|
41
59
|
"div",
|
|
42
60
|
{
|
|
43
61
|
className: cn(
|
|
44
62
|
// `min-h-0` is what lets a flex child shrink below its content and scroll;
|
|
45
63
|
// `last:pb-4` closes a frame that has no actions row under it.
|
|
46
64
|
"min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4",
|
|
65
|
+
headerDivider && "pt-3",
|
|
47
66
|
bodyClassName
|
|
48
67
|
),
|
|
49
68
|
children
|
|
50
69
|
}
|
|
51
70
|
),
|
|
52
|
-
actions !== void 0 && actions !== null && /* @__PURE__ */ jsx(FrameActions, { actions, onClose: modal.onClose })
|
|
71
|
+
actions !== void 0 && actions !== null && /* @__PURE__ */ jsx(FrameActions, { actions, onClose: modal.onClose, divider: hasBody })
|
|
53
72
|
]
|
|
54
73
|
}
|
|
55
74
|
);
|
|
@@ -68,15 +87,32 @@ function FrameClose({ label, onClose }) {
|
|
|
68
87
|
}
|
|
69
88
|
);
|
|
70
89
|
}
|
|
71
|
-
function
|
|
90
|
+
function FrameHeaderActions({
|
|
72
91
|
actions,
|
|
73
92
|
onClose
|
|
93
|
+
}) {
|
|
94
|
+
const close = useContext(ModalCloseContext) ?? onClose;
|
|
95
|
+
return /* @__PURE__ */ jsx("div", { "data-dialog-header-actions": "", className: "flex shrink-0 flex-wrap items-center gap-2", children: typeof actions === "function" ? actions(close) : actions });
|
|
96
|
+
}
|
|
97
|
+
function FrameActions({
|
|
98
|
+
actions,
|
|
99
|
+
onClose,
|
|
100
|
+
divider
|
|
74
101
|
}) {
|
|
75
102
|
const close = useContext(ModalCloseContext) ?? onClose;
|
|
76
103
|
return (
|
|
77
104
|
// The border marks where the scrolling stops; `flex-wrap` keeps three long
|
|
78
105
|
// translated labels on a phone from pushing the row wider than the sheet.
|
|
79
|
-
/* @__PURE__ */ jsx(
|
|
106
|
+
/* @__PURE__ */ jsx(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
className: cn(
|
|
110
|
+
"flex shrink-0 flex-wrap items-center justify-end gap-2 px-4 py-3",
|
|
111
|
+
divider && "border-t border-[var(--border)]"
|
|
112
|
+
),
|
|
113
|
+
children: typeof actions === "function" ? actions(close) : actions
|
|
114
|
+
}
|
|
115
|
+
)
|
|
80
116
|
);
|
|
81
117
|
}
|
|
82
118
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/dialog-frame.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { Modal, ModalCloseContext } from \"./modal\";\nimport type { ModalProps } from \"./modal\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface DialogFrameLabels {\n /** Accessible name of the header's X, when {@link DialogFrameProps.closeButton} shows it. */\n close: string;\n}\n\nexport const DEFAULT_DIALOG_FRAME_LABELS: DialogFrameLabels = { close: \"Close\" };\n\n/**\n * `ModalProps` minus the three this component owns: the NAME (`labelledBy` — the\n * heading's id is generated inside and never reaches the caller), the content\n * (`children` is the body here, not the whole panel) and `title`, which is the dialog's\n * HEADING and a ReactNode rather than the browser's tooltip string — the collision\n * `PickerSheet` met first. Everything else, `size`, `draggable`, `fullBleed`,\n * `onKeyDown`, a `data-tour` anchor, still reaches the `Modal`.\n */\nexport interface DialogFrameProps extends Omit<ModalProps, \"labelledBy\" | \"children\" | \"title\"> {\n /** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */\n title: ReactNode;\n /** The smaller line under the heading; wired to `aria-describedby`. */\n description?: ReactNode;\n /**\n * The heading's level. `h2` by default, which is what every dialog in both apps\n * writes; a prop for a page that nests its demos under a real heading.\n */\n headingAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n /**\n * The row under the body — buttons, in the caller's own order and variants (the two\n * apps disagree about the cancel button's variant, so the frame has no opinion on\n * it). Stays put while the body scrolls.\n *\n * A FUNCTION receives the panel's animated close: `(close) => <Button\n * onClick={close}>Cancel</Button>` lowers the panel the way Escape does, where\n * calling `onClose` directly unmounts it at once.\n */\n actions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Show an X in the header. Off by default: a centred dialog has a backdrop and\n * Escape, and a form dialog has a Cancel. On for a dialog that commits as it goes\n * and has no actions row, where the X is the only visible way out.\n */\n closeButton?: boolean;\n /** Default: `dialogFrame.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n /** Extra classes for the scrolling body (default spacing `space-y-3`). */\n bodyClassName?: string;\n /** The body. */\n children: ReactNode;\n}\n\n/**\n * A {@link Modal} with the frame every caller was writing by hand: a heading, an\n * optional description, an optional X, a body that scrolls, and an actions row that\n * does not.\n *\n * 34 dialogs across the two apps framed themselves — a heading with an id invented per\n * file (and spelt three ways), four visible type sizes for one thing, ten spellings of\n * one right-aligned button row — and the package's own feedback dialog shipped with no\n * accessible name at all. This makes the name unforgettable: the heading's id comes\n * from `useId()` and goes straight to `Modal`'s `labelledBy`, so a framed dialog cannot\n * announce as just \"dialog\", and two open instances cannot share an id.\n *\n * ## It wraps, it does not change `Modal`\n *\n * `Modal` keeps `labelledBy` and every existing caller compiles untouched. What the\n * frame changes is inside the panel: the panel becomes a flex column with no padding\n * of its own, and only the BODY scrolls. The panel's `max-h-full` is still the outer\n * bound, so a tall form keeps its heading and its Save button on screen instead of\n * scrolling them away with the fields. A caller's own `className` still wins (it is\n * tailwind-merged last), which is how a full-screen phone sheet is spelt:\n * `fullBleed className=\"h-[100dvh] max-w-full rounded-none md:h-auto md:rounded-lg\"`.\n *\n * ## What it is not\n *\n * Not `FullBleedDialog`: that is the phone's full-screen editor with its own `open`,\n * Back handling and a required X, and it already draws a frame of its own. And not a\n * form: submit handling, a pending label and close-on-success stay the caller's.\n * Focus lands on the panel, as `Modal` decides — not on the first field, so opening\n * does not pop a phone's keyboard; an `autoFocus` in the body overrides that from the\n * caller's side, and should be a decision rather than a habit.\n */\nexport function DialogFrame({\n title,\n description,\n headingAs: Heading = \"h2\",\n actions,\n closeButton = false,\n closeLabel,\n bodyClassName,\n className,\n children,\n \"aria-describedby\": describedBy,\n ...modal\n}: DialogFrameProps) {\n const titleId = useId();\n const descriptionId = useId();\n const hasDescription = description !== undefined && description !== null;\n\n return (\n <Modal\n {...modal}\n labelledBy={titleId}\n // The caller's own description (a warning inside the body, say) is ADDED to the\n // frame's, not traded for it: both are the dialog's.\n aria-describedby={[hasDescription ? descriptionId : undefined, describedBy].filter(Boolean).join(\" \") || undefined}\n // `overflow-hidden` replaces the panel's own `overflow-y-auto` (tailwind-merge\n // treats them as one group), `p-0` its `p-4`: the scroller and the padding move\n // to the body, which is the only part that should move.\n className={cn(\"flex flex-col overflow-hidden p-0\", className)}\n >\n <div className=\"flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3\">\n <div className=\"min-w-0\">\n <Heading id={titleId} className=\"text-lg font-semibold leading-snug text-[var(--text-primary)]\">\n {title}\n </Heading>\n {hasDescription && (\n <p id={descriptionId} className=\"mt-0.5 text-sm text-[var(--text-muted)]\">\n {description}\n </p>\n )}\n </div>\n {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}\n </div>\n <div\n className={cn(\n // `min-h-0` is what lets a flex child shrink below its content and scroll;\n // `last:pb-4` closes a frame that has no actions row under it.\n \"min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4\",\n bodyClassName,\n )}\n >\n {children}\n </div>\n {actions !== undefined && actions !== null && <FrameActions actions={actions} onClose={modal.onClose} />}\n </Modal>\n );\n}\n\n/** The X. A component of its own so it can read the panel's animated close, which\n * only exists INSIDE the `Modal` (the frame's own body runs outside it). */\nfunction FrameClose({ label, onClose }: { label?: string; onClose: () => void }) {\n const close = useContext(ModalCloseContext) ?? onClose;\n const labels = useKitLabels(\"dialogFrame\", DEFAULT_DIALOG_FRAME_LABELS, label === undefined ? undefined : { close: label });\n return (\n <button\n type=\"button\"\n onClick={close}\n aria-label={labels.close}\n className=\"-me-1.5 -mt-1 shrink-0 rounded p-1.5 text-[var(--text-muted)] outline-none hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)] focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <X aria-hidden className=\"size-5\" />\n </button>\n );\n}\n\nfunction FrameActions({\n actions,\n onClose,\n}: {\n actions: NonNullable<DialogFrameProps[\"actions\"]>;\n onClose: () => void;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n // The border marks where the scrolling stops; `flex-wrap` keeps three long\n // translated labels on a phone from pushing the row wider than the sheet.\n <div className=\"flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-[var(--border)] px-4 py-3\">\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n"],"mappings":";AAuHQ,SACE,KADF;AAvHR,SAAS,YAAY,aAAa;AAElC,SAAS,SAAS;AAElB,SAAS,UAAU;AACnB,SAAS,OAAO,yBAAyB;AAEzC,SAAS,oBAAoB;AAOtB,MAAM,8BAAiD,EAAE,OAAO,QAAQ;AA2ExE,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,WAAW,UAAU;AAAA,EACrB;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,GAAG;AACL,GAAqB;AACnB,QAAM,UAAU,MAAM;AACtB,QAAM,gBAAgB,MAAM;AAC5B,QAAM,iBAAiB,gBAAgB,UAAa,gBAAgB;AAEpE,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,YAAY;AAAA,MAGZ,oBAAkB,CAAC,iBAAiB,gBAAgB,QAAW,WAAW,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAAA,MAIzG,WAAW,GAAG,qCAAqC,SAAS;AAAA,MAE5D;AAAA,6BAAC,SAAI,WAAU,kEACb;AAAA,+BAAC,SAAI,WAAU,WACb;AAAA,gCAAC,WAAQ,IAAI,SAAS,WAAU,iEAC7B,iBACH;AAAA,YACC,kBACC,oBAAC,OAAE,IAAI,eAAe,WAAU,2CAC7B,uBACH;AAAA,aAEJ;AAAA,UACC,eAAe,oBAAC,cAAW,OAAO,YAAY,SAAS,MAAM,SAAS;AAAA,WACzE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,WAAW;AAAA;AAAA;AAAA,cAGT;AAAA,cACA;AAAA,YACF;AAAA,YAEC;AAAA;AAAA,QACH;AAAA,QACC,YAAY,UAAa,YAAY,QAAQ,oBAAC,gBAAa,SAAkB,SAAS,MAAM,SAAS;AAAA;AAAA;AAAA,EACxG;AAEJ;AAIA,SAAS,WAAW,EAAE,OAAO,QAAQ,GAA4C;AAC/E,QAAM,QAAQ,WAAW,iBAAiB,KAAK;AAC/C,QAAM,SAAS,aAAa,eAAe,6BAA6B,UAAU,SAAY,SAAY,EAAE,OAAO,MAAM,CAAC;AAC1H,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,cAAY,OAAO;AAAA,MACnB,WAAU;AAAA,MAEV,8BAAC,KAAE,eAAW,MAAC,WAAU,UAAS;AAAA;AAAA,EACpC;AAEJ;AAEA,SAAS,aAAa;AAAA,EACpB;AAAA,EACA;AACF,GAGG;AACD,QAAM,QAAQ,WAAW,iBAAiB,KAAK;AAC/C;AAAA;AAAA;AAAA,IAGE,oBAAC,SAAI,WAAU,oGACZ,iBAAO,YAAY,aAAa,QAAQ,KAAK,IAAI,SACpD;AAAA;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/dialog-frame.tsx"],"sourcesContent":["import { useContext, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { Modal, ModalCloseContext } from \"./modal\";\nimport type { ModalProps } from \"./modal\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface DialogFrameLabels {\n /** Accessible name of the header's X, when {@link DialogFrameProps.closeButton} shows it. */\n close: string;\n}\n\nexport const DEFAULT_DIALOG_FRAME_LABELS: DialogFrameLabels = { close: \"Close\" };\n\n/**\n * `ModalProps` minus the three this component owns: the NAME (`labelledBy` — the\n * heading's id is generated inside and never reaches the caller), the content\n * (`children` is the body here, not the whole panel) and `title`, which is the dialog's\n * HEADING and a ReactNode rather than the browser's tooltip string — the collision\n * `PickerSheet` met first. Everything else, `size`, `draggable`, `fullBleed`,\n * `onKeyDown`, a `data-tour` anchor, still reaches the `Modal`.\n */\nexport interface DialogFrameProps extends Omit<ModalProps, \"labelledBy\" | \"children\" | \"title\"> {\n /** The heading, and therefore the dialog's accessible name (`aria-labelledby`). */\n title: ReactNode;\n /** The smaller line under the heading; wired to `aria-describedby`. */\n description?: ReactNode;\n /**\n * The heading's level. `h2` by default, which is what every dialog in both apps\n * writes; a prop for a page that nests its demos under a real heading.\n */\n headingAs?: \"h1\" | \"h2\" | \"h3\" | \"h4\";\n /**\n * The row under the body — buttons, in the caller's own order and variants (the two\n * apps disagree about the cancel button's variant, so the frame has no opinion on\n * it). Stays put while the body scrolls.\n *\n * A FUNCTION receives the panel's animated close: `(close) => <Button\n * onClick={close}>Cancel</Button>` lowers the panel the way Escape does, where\n * calling `onClose` directly unmounts it at once.\n */\n actions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Controls that belong to the HEADER rather than to the actions row — an \"Edit\"\n * toggle, a status badge with a menu, a \"Copy link\" (kastlan). Rendered beside the\n * title, before the optional X; on a phone, where title and controls do not fit on\n * one line, they wrap under the title and the X keeps its corner.\n *\n * A function receives the animated close, as `actions` does.\n */\n headerActions?: ReactNode | ((close: () => void) => ReactNode);\n /**\n * Show an X in the header. Off by default: a centred dialog has a backdrop and\n * Escape, and a form dialog has a Cancel. On for a dialog that commits as it goes\n * and has no actions row, where the X is the only visible way out.\n */\n closeButton?: boolean;\n /** Default: `dialogFrame.close` from the {@link UiKitProvider}, else \"Close\". */\n closeLabel?: string;\n /** Extra classes for the scrolling body (default spacing `space-y-3`). */\n bodyClassName?: string;\n /**\n * Extra classes for the header row (default padding `px-4 pt-4 pb-3`), merged last —\n * `px-3` for a phone sheet whose body runs at `px-3`, so heading and fields share\n * one gutter.\n */\n headerClassName?: string;\n /**\n * A rule under the header, the twin of the one over the actions row — for a tall or\n * full-screen body that scrolls under a header which stays. The body then starts a\n * step below the rule instead of flush against it.\n */\n headerDivider?: boolean;\n /**\n * The body. Optional: a dialog whose title, description and actions are the whole\n * of it (a \"you have unsynced changes\" question) leaves it out, and the frame then\n * renders no body at all rather than an empty padded one.\n */\n children?: ReactNode;\n}\n\n/**\n * A {@link Modal} with the frame every caller was writing by hand: a heading, an\n * optional description, an optional X, a body that scrolls, and an actions row that\n * does not.\n *\n * 34 dialogs across the two apps framed themselves — a heading with an id invented per\n * file (and spelt three ways), four visible type sizes for one thing, ten spellings of\n * one right-aligned button row — and the package's own feedback dialog shipped with no\n * accessible name at all. This makes the name unforgettable: the heading's id comes\n * from `useId()` and goes straight to `Modal`'s `labelledBy`, so a framed dialog cannot\n * announce as just \"dialog\", and two open instances cannot share an id.\n *\n * ## It wraps, it does not change `Modal`\n *\n * `Modal` keeps `labelledBy` and every existing caller compiles untouched. What the\n * frame changes is inside the panel: the panel becomes a flex column with no padding\n * of its own, and only the BODY scrolls. The panel's `max-h-full` is still the outer\n * bound, so a tall form keeps its heading and its Save button on screen instead of\n * scrolling them away with the fields. A caller's own `className` still wins (it is\n * tailwind-merged last), which is how a full-screen phone sheet is spelt:\n * `fullBleed className=\"h-[100dvh] max-w-full rounded-none md:h-auto md:rounded-lg\"`.\n *\n * ## What it is not\n *\n * Not `FullBleedDialog`: that is the phone's full-screen editor with its own `open`,\n * Back handling and a required X, and it already draws a frame of its own. And not a\n * form: submit handling, a pending label and close-on-success stay the caller's.\n * Focus lands on the panel, as `Modal` decides — not on the first field, so opening\n * does not pop a phone's keyboard; an `autoFocus` in the body overrides that from the\n * caller's side, and should be a decision rather than a habit.\n */\nexport function DialogFrame({\n title,\n description,\n headingAs: Heading = \"h2\",\n actions,\n headerActions,\n closeButton = false,\n closeLabel,\n bodyClassName,\n headerClassName,\n headerDivider = false,\n className,\n children,\n \"aria-describedby\": describedBy,\n ...modal\n}: DialogFrameProps) {\n const titleId = useId();\n const descriptionId = useId();\n const hasDescription = description !== undefined && description !== null;\n // `false` too, so `{cond && <Body/>}` with a false `cond` means \"no body\".\n const hasBody = children !== undefined && children !== null && children !== false;\n\n return (\n <Modal\n {...modal}\n labelledBy={titleId}\n // The caller's own description (a warning inside the body, say) is ADDED to the\n // frame's, not traded for it: both are the dialog's.\n aria-describedby={[hasDescription ? descriptionId : undefined, describedBy].filter(Boolean).join(\" \") || undefined}\n // `overflow-hidden` replaces the panel's own `overflow-y-auto` (tailwind-merge\n // treats them as one group), `p-0` its `p-4`: the scroller and the padding move\n // to the body, which is the only part that should move.\n className={cn(\"flex flex-col overflow-hidden p-0\", className)}\n >\n <div\n className={cn(\n // `last:pb-4`: a frame with neither body nor actions closes on its header.\n \"flex shrink-0 items-start justify-between gap-2 px-4 pt-4 pb-3 last:pb-4\",\n headerDivider && \"border-b border-[var(--border)]\",\n headerClassName,\n )}\n >\n {/* Title and header actions share a wrapping row of their own, so it is THEY\n that wrap on a phone — the actions drop under a title that needs its 12rem —\n while the X stays outside it, pinned to the top-end corner. */}\n <div className=\"flex min-w-0 flex-1 flex-wrap items-start justify-between gap-x-3 gap-y-2\">\n <div className=\"min-w-0 grow basis-48\">\n <Heading id={titleId} className=\"text-lg font-semibold leading-snug text-[var(--text-primary)]\">\n {title}\n </Heading>\n {hasDescription && (\n <p id={descriptionId} className=\"mt-0.5 text-sm text-[var(--text-muted)]\">\n {description}\n </p>\n )}\n </div>\n {headerActions !== undefined && headerActions !== null && headerActions !== false && (\n <FrameHeaderActions actions={headerActions} onClose={modal.onClose} />\n )}\n </div>\n {closeButton && <FrameClose label={closeLabel} onClose={modal.onClose} />}\n </div>\n {hasBody && (\n <div\n className={cn(\n // `min-h-0` is what lets a flex child shrink below its content and scroll;\n // `last:pb-4` closes a frame that has no actions row under it.\n \"min-h-0 flex-1 space-y-3 overflow-y-auto overscroll-contain px-4 pb-3 last:pb-4\",\n headerDivider && \"pt-3\",\n bodyClassName,\n )}\n >\n {children}\n </div>\n )}\n {actions !== undefined && actions !== null && (\n <FrameActions actions={actions} onClose={modal.onClose} divider={hasBody} />\n )}\n </Modal>\n );\n}\n\n/** The X. A component of its own so it can read the panel's animated close, which\n * only exists INSIDE the `Modal` (the frame's own body runs outside it). */\nfunction FrameClose({ label, onClose }: { label?: string; onClose: () => void }) {\n const close = useContext(ModalCloseContext) ?? onClose;\n const labels = useKitLabels(\"dialogFrame\", DEFAULT_DIALOG_FRAME_LABELS, label === undefined ? undefined : { close: label });\n return (\n <button\n type=\"button\"\n onClick={close}\n aria-label={labels.close}\n className=\"-me-1.5 -mt-1 shrink-0 rounded p-1.5 text-[var(--text-muted)] outline-none hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)] focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n <X aria-hidden className=\"size-5\" />\n </button>\n );\n}\n\n/** The header's own controls; a component for the same reason as {@link FrameClose}. */\nfunction FrameHeaderActions({\n actions,\n onClose,\n}: {\n actions: NonNullable<DialogFrameProps[\"headerActions\"]>;\n onClose: () => void;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n <div data-dialog-header-actions=\"\" className=\"flex shrink-0 flex-wrap items-center gap-2\">\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n\nfunction FrameActions({\n actions,\n onClose,\n divider,\n}: {\n actions: NonNullable<DialogFrameProps[\"actions\"]>;\n onClose: () => void;\n /** Off when there is no body: nothing scrolls, so there is no edge to mark, and a\n * rule straight under the description would cut the question from its answers. */\n divider: boolean;\n}) {\n const close = useContext(ModalCloseContext) ?? onClose;\n return (\n // The border marks where the scrolling stops; `flex-wrap` keeps three long\n // translated labels on a phone from pushing the row wider than the sheet.\n <div\n className={cn(\n \"flex shrink-0 flex-wrap items-center justify-end gap-2 px-4 py-3\",\n divider && \"border-t border-[var(--border)]\",\n )}\n >\n {typeof actions === \"function\" ? actions(close) : actions}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -87,7 +87,27 @@ interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "title">
|
|
|
87
87
|
headerClassName?: string;
|
|
88
88
|
/** Extra classes for the body's wrapper — where its padding and spacing live. */
|
|
89
89
|
bodyClassName?: string;
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Content at the far end of the header row, beside the title: a count, a date, a
|
|
92
|
+
* status chip, an action button. It is a SIBLING of the header button, never inside
|
|
93
|
+
* it — a button may not contain another interactive element, and a count inside it
|
|
94
|
+
* would also be read as part of the button's name. The header button still spans
|
|
95
|
+
* the row under it (a stretched hit area), so clicking the empty space or the
|
|
96
|
+
* card's chevron still toggles, while whatever sits in `trailing` gets its own
|
|
97
|
+
* clicks. With `trailing`, the card's chevron moves after it, to the row's end.
|
|
98
|
+
*/
|
|
99
|
+
trailing?: ReactNode;
|
|
100
|
+
/**
|
|
101
|
+
* Trigger-only mode: the id of an element the CALLER renders elsewhere — the hidden
|
|
102
|
+
* rows of a table, a panel in another column — which this header shows and hides.
|
|
103
|
+
* The header's `aria-controls` points at it and the disclosure renders no body of
|
|
104
|
+
* its own (`children`, `bodyClassName` and `keepMounted` are ignored). Pair it with
|
|
105
|
+
* `open` / `onOpenChange`: the caller owns the state, since the caller renders what
|
|
106
|
+
* it governs.
|
|
107
|
+
*/
|
|
108
|
+
controls?: string;
|
|
109
|
+
/** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls}). */
|
|
110
|
+
children?: ReactNode;
|
|
91
111
|
}
|
|
92
112
|
/**
|
|
93
113
|
* A section that opens in place: a header button with `aria-expanded` and
|
|
@@ -103,6 +123,6 @@ interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "title">
|
|
|
103
123
|
* The body is unmounted while shut; see {@link CollapseProps.keepMounted} for why that
|
|
104
124
|
* is the default and when to opt out.
|
|
105
125
|
*/
|
|
106
|
-
declare function Disclosure({ title, hint, open: controlled, defaultOpen, onOpenChange, variant, headingAs: Heading, keepMounted, disabled, headerClassName, bodyClassName, className, children, ...rest }: DisclosureProps): react.JSX.Element;
|
|
126
|
+
declare function Disclosure({ title, hint, open: controlled, defaultOpen, onOpenChange, variant, headingAs: Heading, keepMounted, disabled, headerClassName, bodyClassName, trailing, controls, className, children, ...rest }: DisclosureProps): react.JSX.Element;
|
|
107
127
|
|
|
108
128
|
export { Collapse, type CollapseProps, Disclosure, type DisclosureProps };
|
|
@@ -55,6 +55,8 @@ function Disclosure({
|
|
|
55
55
|
disabled,
|
|
56
56
|
headerClassName,
|
|
57
57
|
bodyClassName,
|
|
58
|
+
trailing,
|
|
59
|
+
controls,
|
|
58
60
|
className,
|
|
59
61
|
children,
|
|
60
62
|
...rest
|
|
@@ -63,6 +65,9 @@ function Disclosure({
|
|
|
63
65
|
const open = controlled ?? own;
|
|
64
66
|
const bodyId = useId();
|
|
65
67
|
const card = variant === "card";
|
|
68
|
+
const triggerOnly = controls !== void 0;
|
|
69
|
+
const joined = open && !triggerOnly;
|
|
70
|
+
const hasTrailing = trailing !== void 0 && trailing !== null && trailing !== false;
|
|
66
71
|
const toggle = () => {
|
|
67
72
|
const next = !open;
|
|
68
73
|
if (controlled === void 0) setOwn(next);
|
|
@@ -73,36 +78,64 @@ function Disclosure({
|
|
|
73
78
|
{
|
|
74
79
|
type: "button",
|
|
75
80
|
"aria-expanded": open,
|
|
76
|
-
"aria-controls": bodyId,
|
|
81
|
+
"aria-controls": triggerOnly ? controls : bodyId,
|
|
77
82
|
disabled,
|
|
78
83
|
onClick: toggle,
|
|
84
|
+
"data-disclosure-trigger": "",
|
|
79
85
|
className: cn(
|
|
80
86
|
"flex w-full gap-2 text-start outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
81
|
-
|
|
87
|
+
hasTrailing ? (
|
|
88
|
+
// Stretched over the whole header row (the row is `relative`), so the space
|
|
89
|
+
// round `trailing` and the card's chevron still toggle; the ring and the
|
|
90
|
+
// card's hover paint on the stretched area / the row, not the text box.
|
|
91
|
+
cn(
|
|
92
|
+
"min-w-0 flex-1 after:absolute after:inset-0 after:content-['']",
|
|
93
|
+
"focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-[var(--brand)]",
|
|
94
|
+
card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : "after:rounded-sm"
|
|
95
|
+
)
|
|
96
|
+
) : "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
82
97
|
card ? cn(
|
|
83
|
-
"items-center justify-between rounded-lg p-4
|
|
84
|
-
|
|
98
|
+
"items-center justify-between rounded-lg p-4",
|
|
99
|
+
hasTrailing ? "pe-0" : "hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
|
|
100
|
+
joined && "rounded-b-none"
|
|
85
101
|
) : "items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
|
|
86
102
|
headerClassName
|
|
87
103
|
),
|
|
88
104
|
children: [
|
|
89
105
|
!card && /* @__PURE__ */ jsx(Chevron, { open, leading: true }),
|
|
90
|
-
/* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
|
|
106
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
91
107
|
/* @__PURE__ */ jsx("span", { className: cn("block", card && "text-sm font-semibold text-[var(--text-primary)]"), children: title }),
|
|
92
108
|
hint !== void 0 && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-xs font-normal text-[var(--text-muted)]", children: hint })
|
|
93
109
|
] }),
|
|
94
|
-
card && /* @__PURE__ */ jsx(Chevron, { open })
|
|
110
|
+
card && !hasTrailing && /* @__PURE__ */ jsx(Chevron, { open })
|
|
95
111
|
]
|
|
96
112
|
}
|
|
97
113
|
);
|
|
114
|
+
const header = Heading ? /* @__PURE__ */ jsx(Heading, { className: hasTrailing ? "min-w-0 flex-1" : void 0, children: button }) : button;
|
|
98
115
|
return /* @__PURE__ */ jsxs(
|
|
99
116
|
"div",
|
|
100
117
|
{
|
|
101
118
|
...rest,
|
|
102
119
|
className: cn(card && "rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm", className),
|
|
103
120
|
children: [
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
hasTrailing ? /* @__PURE__ */ jsxs(
|
|
122
|
+
"div",
|
|
123
|
+
{
|
|
124
|
+
className: cn(
|
|
125
|
+
"relative flex items-center gap-2",
|
|
126
|
+
card && cn(
|
|
127
|
+
"rounded-lg pe-4 has-[[data-disclosure-trigger]:enabled:hover]:bg-[var(--bg-hover)]",
|
|
128
|
+
joined && "rounded-b-none"
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
children: [
|
|
132
|
+
header,
|
|
133
|
+
/* @__PURE__ */ jsx("div", { className: "relative flex shrink-0 items-center gap-2", children: trailing }),
|
|
134
|
+
card && /* @__PURE__ */ jsx(Chevron, { open })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
) : header,
|
|
138
|
+
!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 }) })
|
|
106
139
|
]
|
|
107
140
|
}
|
|
108
141
|
);
|