@eifi1/ui-kit 0.7.0 → 0.8.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 +32 -17
- package/dist/chart.d.ts +3 -2
- package/dist/components/alert-banner.d.ts +59 -10
- package/dist/components/alert-banner.js +114 -8
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button-group.js +49 -0
- package/dist/components/button-group.js.map +1 -0
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/chart-zoom.d.ts +63 -14
- package/dist/components/chart-zoom.js +82 -16
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chip.d.ts +67 -2
- package/dist/components/chip.js +73 -8
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +120 -0
- package/dist/components/confirm-dialog.js +96 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/copy-button.d.ts +62 -0
- package/dist/components/copy-button.js +95 -0
- package/dist/components/copy-button.js.map +1 -0
- 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 +5 -1
- package/dist/components/data-table-pagination.js +71 -58
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +28 -5
- package/dist/components/data-table-sort.js +11 -7
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +544 -423
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +51 -5
- package/dist/components/date-picker.js +152 -40
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +60 -0
- package/dist/components/description-list.js +112 -0
- package/dist/components/description-list.js.map +1 -0
- package/dist/components/disclosure.d.ts +37 -2
- package/dist/components/disclosure.js +14 -5
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -0
- package/dist/components/file-button.d.ts +19 -1
- package/dist/components/file-button.js +8 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +56 -9
- package/dist/components/file-dropzone.js +100 -70
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +107 -0
- package/dist/components/floating-panel.js +206 -0
- package/dist/components/floating-panel.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +12 -1
- package/dist/components/full-bleed-dialog.js +26 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/modal.d.ts +6 -2
- package/dist/components/modal.js +2 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +5 -0
- package/dist/components/number-input.d.ts +5 -0
- package/dist/components/numpad-sheet.d.ts +5 -0
- package/dist/components/progress-bar.d.ts +54 -0
- package/dist/components/progress-bar.js +94 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/scroll-area.d.ts +53 -0
- package/dist/components/scroll-area.js +86 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/separator.d.ts +23 -0
- package/dist/components/separator.js +24 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/series-chart-ticks.d.ts +34 -1
- package/dist/components/series-chart-ticks.js +83 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +314 -19
- package/dist/components/series-chart.js +510 -120
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/skeleton.d.ts +36 -0
- package/dist/components/skeleton.js +35 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/stat-tile.d.ts +60 -6
- package/dist/components/stat-tile.js +60 -26
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +83 -0
- package/dist/components/table.js +166 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-legend.d.ts +3 -2
- package/dist/components/toggle-legend.js +1 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/tree-view.d.ts +129 -0
- package/dist/components/tree-view.js +376 -0
- package/dist/components/tree-view.js.map +1 -0
- package/dist/components/treemap.d.ts +9 -2
- package/dist/components/treemap.js +2 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +92 -14
- package/dist/components/ui.js +112 -30
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
- package/dist/data-table.d.ts +2 -2
- package/dist/data-table.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
- package/dist/hooks/use-copy-to-clipboard.js +81 -0
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
- package/dist/hooks/use-debounce.d.ts +43 -0
- package/dist/hooks/use-debounce.js +56 -0
- package/dist/hooks/use-debounce.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +72 -0
- package/dist/hooks/use-file-drop.js +57 -0
- package/dist/hooks/use-file-drop.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +17 -0
- package/dist/i18n/kit-labels.js +6 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
- package/dist/i18n/locales/de-CH-informal.js +8 -0
- package/dist/i18n/locales/de-CH-informal.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +5 -0
- package/dist/i18n/locales/de-CH.js +1 -14
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/de-informal.d.ts +64 -0
- package/dist/i18n/locales/de-informal.js +37 -0
- package/dist/i18n/locales/de-informal.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +5 -0
- package/dist/i18n/locales/de.js +25 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +5 -0
- package/dist/i18n/locales/es.js +25 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +5 -0
- package/dist/i18n/locales/fr.js +25 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +5 -0
- package/dist/i18n/locales/hu.js +25 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +5 -0
- package/dist/i18n/locales/it.js +25 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +5 -0
- package/dist/i18n/locales/zh.js +25 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +13 -0
- package/dist/i18n/swiss.js +19 -0
- package/dist/i18n/swiss.js.map +1 -0
- package/dist/index.d.ts +26 -11
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/dates.d.ts +37 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +15 -1
- package/dist/search/command-palette.js +10 -2
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.js +2 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +35 -2
- package/dist/wizard/stepper-nav.js +54 -22
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +107 -5
- package/dist/wizard/types.js +2 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +141 -21
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.d.ts +10 -2
- package/dist/wizard/wizard-summary.js +3 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +4 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +211 -20
- package/src/components/button-group.tsx +75 -0
- package/src/components/chart-zoom.tsx +158 -20
- package/src/components/chip.tsx +160 -9
- package/src/components/confirm-dialog.tsx +242 -0
- package/src/components/copy-button.tsx +158 -0
- package/src/components/data-table-pagination.tsx +19 -6
- package/src/components/data-table-sort.ts +49 -10
- package/src/components/data-table.tsx +293 -24
- package/src/components/date-picker.tsx +262 -57
- package/src/components/description-list.tsx +174 -0
- package/src/components/disclosure.tsx +58 -5
- package/src/components/file-button.tsx +22 -2
- package/src/components/file-dropzone.tsx +198 -100
- package/src/components/floating-panel.tsx +341 -0
- package/src/components/full-bleed-dialog.tsx +48 -5
- package/src/components/modal.tsx +9 -2
- package/src/components/progress-bar.tsx +163 -0
- package/src/components/scroll-area.tsx +129 -0
- package/src/components/separator.tsx +37 -0
- package/src/components/series-chart-ticks.ts +135 -0
- package/src/components/series-chart.tsx +934 -67
- package/src/components/skeleton.tsx +65 -0
- package/src/components/stat-tile.tsx +160 -27
- package/src/components/table.tsx +263 -0
- package/src/components/toggle-legend.tsx +4 -3
- package/src/components/tooltip.tsx +40 -12
- package/src/components/tree-view.tsx +589 -0
- package/src/components/treemap.tsx +10 -2
- package/src/components/ui.tsx +268 -42
- package/src/data-table.ts +2 -0
- package/src/hooks/use-copy-to-clipboard.ts +122 -0
- package/src/hooks/use-debounce.ts +105 -0
- package/src/hooks/use-file-drop.ts +123 -0
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/kit-labels.tsx +19 -0
- package/src/i18n/locales/de-CH-informal.ts +10 -0
- package/src/i18n/locales/de-CH.ts +1 -16
- package/src/i18n/locales/de-informal.ts +61 -0
- package/src/i18n/locales/de.ts +25 -0
- package/src/i18n/locales/es.ts +25 -0
- package/src/i18n/locales/fr.ts +25 -0
- package/src/i18n/locales/hu.ts +25 -0
- package/src/i18n/locales/it.ts +25 -0
- package/src/i18n/locales/zh.ts +25 -0
- package/src/i18n/swiss.ts +25 -0
- package/src/index.ts +29 -0
- package/src/lib/dates.ts +48 -0
- package/src/search/command-palette.tsx +29 -3
- package/src/shell/app-shell.tsx +2 -2
- package/src/wizard/stepper-nav.tsx +102 -28
- package/src/wizard/types.ts +106 -4
- package/src/wizard/use-wizard.ts +211 -31
- package/src/wizard/wizard-summary.tsx +24 -12
- package/src/wizard.ts +3 -2
package/dist/components/chip.js
CHANGED
|
@@ -44,6 +44,39 @@ const TONE = {
|
|
|
44
44
|
selected: "border-current bg-[var(--bg-active)] text-[var(--money-expense)]"
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
const OUTLINE = {
|
|
48
|
+
neutral: "border-[var(--border-strong)] bg-transparent text-[var(--text-secondary)]",
|
|
49
|
+
brand: "border-[var(--brand)] bg-transparent text-[var(--brand)]",
|
|
50
|
+
danger: "border-[var(--danger-border)] bg-transparent text-[var(--danger)]",
|
|
51
|
+
warning: "border-[var(--warning-border)] bg-transparent text-[var(--warning)]",
|
|
52
|
+
success: "border-[var(--success-border)] bg-transparent text-[var(--success)]",
|
|
53
|
+
info: "border-[var(--info-border)] bg-transparent text-[var(--info)]",
|
|
54
|
+
income: "border-current/60 bg-transparent text-[var(--money-income)]",
|
|
55
|
+
expense: "border-current/60 bg-transparent text-[var(--money-expense)]"
|
|
56
|
+
};
|
|
57
|
+
const SOLID = {
|
|
58
|
+
neutral: "border-transparent bg-[var(--bg-inverse)] text-[var(--text-inverse)]",
|
|
59
|
+
brand: "border-transparent bg-[var(--brand)] text-[var(--brand-contrast)]",
|
|
60
|
+
danger: "border-transparent bg-[var(--danger)] text-[var(--danger-contrast)]",
|
|
61
|
+
warning: "border-transparent bg-[var(--warning)] text-[var(--text-inverse)]",
|
|
62
|
+
success: "border-transparent bg-[var(--success)] text-[var(--text-inverse)]",
|
|
63
|
+
info: "border-transparent bg-[var(--info)] text-[var(--text-inverse)]",
|
|
64
|
+
income: "border-transparent bg-[var(--money-income)] text-[var(--text-inverse)]",
|
|
65
|
+
expense: "border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]"
|
|
66
|
+
};
|
|
67
|
+
const CAPS = {
|
|
68
|
+
sm: "text-[10px] font-semibold uppercase tracking-wider",
|
|
69
|
+
md: "text-[11px] font-semibold uppercase tracking-wider",
|
|
70
|
+
lg: "text-xs font-semibold uppercase tracking-wider"
|
|
71
|
+
};
|
|
72
|
+
function surfaceOf(tone, variant, selected) {
|
|
73
|
+
const palette = TONE[tone];
|
|
74
|
+
if (variant === "solid") {
|
|
75
|
+
return cn(SOLID[tone], selected && "ring-2 ring-[var(--border-strong)] ring-offset-1 ring-offset-[var(--bg-surface)]");
|
|
76
|
+
}
|
|
77
|
+
if (selected) return palette.selected;
|
|
78
|
+
return variant === "outline" ? OUTLINE[tone] : palette.idle;
|
|
79
|
+
}
|
|
47
80
|
const SIZE = {
|
|
48
81
|
sm: {
|
|
49
82
|
body: "gap-1 px-2 py-0.5 text-xs",
|
|
@@ -74,12 +107,17 @@ const Chip = forwardRef(function Chip2({
|
|
|
74
107
|
children,
|
|
75
108
|
tone = "neutral",
|
|
76
109
|
size = "md",
|
|
110
|
+
variant = "soft",
|
|
111
|
+
shape = "pill",
|
|
112
|
+
caps = false,
|
|
77
113
|
icon: Icon,
|
|
78
114
|
selected,
|
|
79
115
|
href,
|
|
116
|
+
renderLink,
|
|
80
117
|
onClick,
|
|
81
118
|
onRemove,
|
|
82
119
|
removeLabel,
|
|
120
|
+
removeDisabled = false,
|
|
83
121
|
disabled = false,
|
|
84
122
|
className,
|
|
85
123
|
tabIndex,
|
|
@@ -90,15 +128,18 @@ const Chip = forwardRef(function Chip2({
|
|
|
90
128
|
}, ref) {
|
|
91
129
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
|
|
92
130
|
const s = SIZE[size];
|
|
93
|
-
const palette = TONE[tone];
|
|
94
131
|
const interactive = !!href || !!onClick;
|
|
132
|
+
const radius = shape === "square" ? size === "sm" ? "rounded" : "rounded-md" : "rounded-full";
|
|
133
|
+
const type = cn(caps && CAPS[size], variant === "solid" && "justify-center tabular-nums");
|
|
95
134
|
const surface = cn(
|
|
96
|
-
|
|
135
|
+
surfaceOf(tone, variant, selected),
|
|
97
136
|
disabled && "cursor-default opacity-50"
|
|
98
137
|
);
|
|
99
138
|
const look = cn(
|
|
100
139
|
CHIP_BASE,
|
|
101
140
|
s.body,
|
|
141
|
+
type,
|
|
142
|
+
radius,
|
|
102
143
|
surface,
|
|
103
144
|
interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
|
|
104
145
|
className
|
|
@@ -117,29 +158,47 @@ const Chip = forwardRef(function Chip2({
|
|
|
117
158
|
e.stopPropagation();
|
|
118
159
|
onRemove();
|
|
119
160
|
},
|
|
120
|
-
disabled,
|
|
161
|
+
disabled: disabled || removeDisabled,
|
|
121
162
|
className: cn(
|
|
122
163
|
// Logical margins: the × sits at the END of the pill, which is the left in RTL.
|
|
123
|
-
"-me-0.5 ms-0.5 shrink-0
|
|
164
|
+
"-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors",
|
|
165
|
+
shape === "square" ? "rounded-sm" : "rounded-full",
|
|
124
166
|
"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
125
|
-
disabled && "pointer-events-none"
|
|
167
|
+
(disabled || removeDisabled) && "pointer-events-none",
|
|
168
|
+
// A disabled chip is already dimmed as a whole; dimming the × again on top
|
|
169
|
+
// would take it to a quarter. `removeDisabled` alone dims just the ×.
|
|
170
|
+
removeDisabled && !disabled && "opacity-40"
|
|
126
171
|
),
|
|
127
172
|
children: /* @__PURE__ */ jsx(X, { className: s.remove, "aria-hidden": true })
|
|
128
173
|
}
|
|
129
174
|
) : null;
|
|
130
|
-
const pill = (inner2) => /* @__PURE__ */ jsxs("span", { className: cn(CHIP_PILL, s.tail, surface, className), children: [
|
|
175
|
+
const pill = (inner2) => /* @__PURE__ */ jsxs("span", { className: cn(CHIP_PILL, s.tail, radius, surface, className), children: [
|
|
131
176
|
inner2,
|
|
132
177
|
remove
|
|
133
178
|
] });
|
|
134
179
|
const inner = cn(
|
|
135
|
-
"inline-flex min-w-0 items-center self-stretch
|
|
180
|
+
"inline-flex min-w-0 items-center self-stretch transition-colors",
|
|
181
|
+
radius,
|
|
136
182
|
CHIP_RING,
|
|
137
183
|
s.split,
|
|
184
|
+
type,
|
|
138
185
|
!disabled && "cursor-pointer hover:bg-current/10",
|
|
139
186
|
disabled && "cursor-default"
|
|
140
187
|
);
|
|
141
188
|
if (href && !disabled) {
|
|
142
|
-
const
|
|
189
|
+
const linkProps = {
|
|
190
|
+
...rest,
|
|
191
|
+
ref,
|
|
192
|
+
href,
|
|
193
|
+
"aria-current": selected ? "true" : void 0,
|
|
194
|
+
className: remove ? inner : look,
|
|
195
|
+
children: body
|
|
196
|
+
};
|
|
197
|
+
const link = renderLink ? (
|
|
198
|
+
// Through a component rather than called here, so the forwarded ref arrives as
|
|
199
|
+
// an ordinary prop of an element and is never handed to a function mid-render.
|
|
200
|
+
/* @__PURE__ */ jsx(RenderedLink, { render: renderLink, ...linkProps })
|
|
201
|
+
) : /* @__PURE__ */ jsx(
|
|
143
202
|
"a",
|
|
144
203
|
{
|
|
145
204
|
ref,
|
|
@@ -185,6 +244,12 @@ const Chip = forwardRef(function Chip2({
|
|
|
185
244
|
}
|
|
186
245
|
);
|
|
187
246
|
});
|
|
247
|
+
function RenderedLink({
|
|
248
|
+
render,
|
|
249
|
+
...props
|
|
250
|
+
}) {
|
|
251
|
+
return render(props);
|
|
252
|
+
}
|
|
188
253
|
const DEFAULT_CHIP_INPUT_LABELS = {
|
|
189
254
|
added: (v) => `${v} added`,
|
|
190
255
|
removed: (v) => `${v} removed`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { forwardRef, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { FIELD_INVALID } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * A chip: a compact pill carrying one value.\n *\n * Three jobs, deliberately one component. A chip can be inert (a tag on a row), a link\n * (a jump to a section), or a toggle (a filter that is on or off) — and the difference is\n * which prop you pass, not which component you pick. Keeping them together is what stops\n * an app growing three near-identical pills that drift apart; it is the shape this kit\n * already uses for `Button`, which is a link when it is given an href.\n *\n * A chip is not a `Button`. It is smaller, it is rounded to a pill, and it reads as a\n * VALUE rather than as an action — which matters because a row of eight buttons says\n * \"choose one of eight things to do\" and a row of eight chips says \"here are eight\n * things\". Using `Button` for both is the mistake this exists to prevent.\n */\n\nexport type ChipTone =\n | \"neutral\"\n | \"brand\"\n | \"danger\"\n | \"warning\"\n | \"success\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px. */\nexport type ChipSize = \"sm\" | \"md\" | \"lg\";\n\nconst TONE: Record<ChipTone, { idle: string; selected: string }> = {\n neutral: {\n idle: \"border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\",\n selected: \"border-[var(--border-strong)] bg-[var(--bg-active)] text-[var(--text-primary)]\",\n },\n brand: {\n idle: \"border-[var(--border)] bg-[var(--brand-bg)] text-[var(--brand-muted)]\",\n selected: \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n },\n danger: {\n idle: \"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)]\",\n selected: \"border-[var(--danger)] bg-[var(--danger)] text-[var(--danger-contrast)]\",\n },\n warning: {\n idle: \"border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n selected: \"border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n },\n success: {\n idle: \"border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success)]\",\n selected: \"border-[var(--success)] bg-[var(--success-bg)] text-[var(--success)]\",\n },\n info: {\n idle: \"border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]\",\n selected: \"border-[var(--info)] bg-[var(--info-bg)] text-[var(--info)]\",\n },\n // The money pair (Keksdose's direction toggle). Only the TEXT and the BORDER carry\n // the tint, on the neutral chip's own surfaces: the money tokens are chosen for text\n // contrast, and a filled amber pill beside an amber figure reads as one smear.\n // `border-current` takes its hue from the text, so the two cannot drift apart.\n income: {\n idle: \"border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-income)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--money-income)]\",\n },\n expense: {\n idle: \"border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-expense)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--money-expense)]\",\n },\n};\n\n/**\n * `body` is the whole pill when the chip is ONE element. When a link or a button also has\n * a dismiss control, the pill is a wrapper holding two siblings (see the note on\n * {@link Chip}): `split` is then the interactive part's own padding — the full start\n * inset, a short end one before the ×; `tail` is the wrapper's end inset after the ×.\n */\nconst SIZE: Record<\n ChipSize,\n { body: string; split: string; tail: string; icon: string; remove: string }\n> = {\n sm: {\n body: \"gap-1 px-2 py-0.5 text-xs\",\n split: \"gap-1 ps-2 pe-1 py-0.5 text-xs\",\n tail: \"pe-1.5\",\n icon: \"size-3\",\n remove: \"size-3\",\n },\n md: {\n body: \"gap-1.5 px-2.5 py-1 text-sm\",\n split: \"gap-1.5 ps-2.5 pe-1.5 py-1 text-sm\",\n tail: \"pe-2\",\n icon: \"size-3.5\",\n remove: \"size-3.5\",\n },\n lg: {\n body: \"min-h-11 gap-2 px-4 py-2 text-sm\",\n split: \"min-h-11 gap-2 ps-4 pe-2 py-2 text-sm\",\n tail: \"min-h-11 pe-3.5\",\n icon: \"size-4\",\n remove: \"size-4\",\n },\n};\n\nconst CHIP_PILL = \"inline-flex max-w-full items-center rounded-full border transition-colors\";\n// `focus-visible`, not `focus`: a chip commonly receives focus programmatically (the\n// ChipInput moves focus onto one after a removal) and a ring that appears on a\n// pointer click reads as a stuck selection.\nconst CHIP_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]\";\nconst CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;\n\ninterface ChipBaseProps {\n children: ReactNode;\n tone?: ChipTone;\n size?: ChipSize;\n icon?: LucideIcon;\n /**\n * Marks the chip as the current one — `aria-current` on a link, `aria-pressed` on a\n * toggle.\n *\n * On an ON/OFF toggle, keep the LABEL the same in both states and let `selected`\n * carry the state. A label that flips with it (\"Skip this month\" / \"Ask again this\n * month\") is announced together with \"pressed\", and then says the opposite of what\n * it does.\n *\n * A control that switches between two NAMED states (outflow ⇄ inflow) is not a\n * toggle but an ACTION button: leave `selected` out, let the visible label and the\n * tone follow the state (\"− Outflow\" / \"+ Inflow\"), and name the action in\n * `aria-label` (\"Direction: outflow — tap for inflow\"). Without `selected` the chip\n * reports no pressed state at all. A fixed label there contradicts the figure beside\n * it — \"+ Outflow\" in green next to an inflow (keksdose #417).\n */\n selected?: boolean;\n /** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */\n onRemove?: () => void;\n /** Accessible name for the dismiss button. Default: `common.remove` from the\n * {@link UiKitProvider}, else \"Remove\". */\n removeLabel?: string;\n disabled?: boolean;\n className?: string;\n /**\n * Roving focus, for a container that manages a row of chips (see {@link ChipInput}).\n * A chip with no `href`/`onClick` is inert and unfocusable by default; give it a\n * `tabIndex` and a `role` and it becomes the thing arrow keys move between.\n */\n tabIndex?: number;\n role?: string;\n onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;\n onFocus?: () => void;\n /** Accessible name, when the visible text is not the whole story. */\n \"aria-label\"?: string;\n /** Any other ARIA attribute — `aria-describedby`, `aria-expanded`, `aria-controls`,\n * `aria-haspopup` — reaches the chip's interactive element. */\n [key: `aria-${string}`]: string | boolean | number | undefined;\n /** A test id, a `data-tour` anchor, or any other data attribute. */\n [key: `data-${string}`]: unknown;\n id?: string;\n /** A native tooltip. For anything a user must read, prefer the kit's `Tooltip`. */\n title?: string;\n}\n\n/**\n * A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were\n * accepted and `onClick` was silently dropped whenever `href` was set; a link that must\n * also run code wants a router `Link`-style `onClick` on the anchor, which is a different\n * component's job.\n */\nexport type ChipProps = ChipBaseProps &\n (\n | {\n /** Renders the chip as a link. Mutually exclusive with `onClick`. */\n href?: string;\n onClick?: never;\n }\n | {\n href?: never;\n /** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives\n * the click, so a chip inside a clickable row can `stopPropagation()`. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n }\n );\n\n/**\n * Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or\n * a link, so when both are present the chip renders a wrapper holding TWO siblings — the\n * interactive body and the dismiss control. The WRAPPER is the visual pill (border, tone,\n * `className`) and the two sit inside it, so the × reads as part of the chip; the body's\n * focus ring is drawn on the body alone, and `ref`, `id` and the ARIA props still reach\n * the body — the interactive element.\n */\nexport const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(\n {\n children,\n tone = \"neutral\",\n size = \"md\",\n icon: Icon,\n selected,\n href,\n onClick,\n onRemove,\n removeLabel,\n disabled = false,\n className,\n tabIndex,\n role,\n onKeyDown,\n onFocus,\n ...rest\n },\n ref,\n) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\n const s = SIZE[size];\n const palette = TONE[tone];\n const interactive = !!href || !!onClick;\n const surface = cn(\n selected ? palette.selected : palette.idle,\n disabled && \"cursor-default opacity-50\",\n );\n const look = cn(\n CHIP_BASE,\n s.body,\n surface,\n interactive && !disabled && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n className,\n );\n\n const body = (\n <>\n {Icon && <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />}\n <span className=\"min-w-0 truncate\">{children}</span>\n </>\n );\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n // The name carries WHAT is being removed, not just \"Remove\" — a screen-reader user\n // tabbing a row of eight dismiss buttons otherwise hears the same word eight times\n // with no way to tell which one they are on.\n // Composed through `common.fieldValue` because the punctuation between the two\n // halves is the language's, not ours. A chip whose content is not plain text\n // has no value to name, and gets the bare verb rather than a dangling colon.\n aria-label={\n typeof children === \"string\" && children\n ? common.fieldValue(common.remove, children)\n : common.remove\n }\n onClick={(e) => {\n // The chip may itself be a link; removing it must not also follow it.\n e.preventDefault();\n e.stopPropagation();\n onRemove();\n }}\n disabled={disabled}\n className={cn(\n // Logical margins: the × sits at the END of the pill, which is the left in RTL.\n \"-me-0.5 ms-0.5 shrink-0 rounded-full p-0.5 transition-colors\",\n \"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n disabled && \"pointer-events-none\",\n )}\n >\n <X className={s.remove} aria-hidden />\n </button>\n ) : null;\n\n // ── The combined shape: one pill, two interactive siblings inside it.\n const pill = (inner: ReactNode) => (\n <span className={cn(CHIP_PILL, s.tail, surface, className)}>\n {inner}\n {remove}\n </span>\n );\n // The body inside a pill: no border or surface of its own (the pill has them), a\n // hover wash in the text's own hue so it works on every tone.\n const inner = cn(\n \"inline-flex min-w-0 items-center self-stretch rounded-full transition-colors\",\n CHIP_RING,\n s.split,\n !disabled && \"cursor-pointer hover:bg-current/10\",\n disabled && \"cursor-default\",\n );\n\n // ── The three shapes.\n if (href && !disabled) {\n const link = (\n <a\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n aria-current={selected ? \"true\" : undefined}\n className={remove ? inner : look}\n {...rest}\n >\n {body}\n </a>\n );\n return remove ? pill(link) : link;\n }\n\n if (onClick) {\n const button = (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n // Only when `selected` is PASSED: a chip with `onClick` and no `selected` is an\n // action button (it does something), not a toggle (it is on or off), and\n // announcing it \"not pressed\" would claim a state it does not have.\n aria-pressed={selected}\n className={remove ? inner : look}\n {...rest}\n >\n {body}\n </button>\n );\n return remove ? pill(button) : button;\n }\n\n return (\n <span\n ref={ref as React.Ref<HTMLSpanElement>}\n className={look}\n tabIndex={tabIndex}\n role={role}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n {...rest}\n >\n {body}\n {remove}\n </span>\n );\n});\n\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\nexport const DEFAULT_CHIP_INPUT_LABELS: ChipInputLabels = {\n added: (v) => `${v} added`,\n removed: (v) => `${v} removed`,\n remove: \"Remove\",\n atLimit: (max) => `Limit of ${max} reached`,\n duplicate: (v) => `${v} is already in the list`,\n};\n\nexport function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels {\n return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;\n}\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field. Without one, name the field with\n * `aria-label`. */\n label?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling the rest of this kit\n * now uses. Still honoured, and still second to `aria-label` when both are given.\n */\n ariaLabel?: string;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: Partial<ChipInputLabels>;\n className?: string;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips, in reading order (mirrored in RTL)\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n ariaLabel,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English — the same order\n // `resolveChipInputLabels` gives outside a provider.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n // The last refusal, shown under the field until the next edit. The live region alone\n // told only a screen-reader user why Enter did nothing; everyone else saw the text\n // simply stay put.\n const [rejected, setRejected] = useState<string | null>(null);\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const rejectedId = `${id}-rejected`;\n const isInvalid = invalid || !!error;\n const describedBy = [error ? errorId : null, rejected ? rejectedId : null]\n .filter(Boolean)\n .join(\" \");\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n setRejected(rejection);\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n setRejected(null);\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n if (e.key === \"Escape\" && draft) {\n e.preventDefault();\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n // ←/→ follow the reading direction, not DOM order: in RTL the previous chip is to\n // the RIGHT, and ArrowLeft moves on towards the input.\n const step = horizontalStep(e.key, e.currentTarget);\n if (step === -1 && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (step === 1) {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-[var(--text-secondary)]\">\n {label}\n </label>\n )}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 shadow-sm\",\n \"focus-within:border-[var(--brand)] focus-within:ring-1 focus-within:ring-[var(--brand)]\",\n disabled && \"cursor-default bg-[var(--bg-surface-2)]\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={inputRef}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n setRejected(null);\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two. Failing that, the DOM spelling wins and\n // the deprecated `ariaLabel` is the fallback.\n aria-label={label ? undefined : (ariaLabelAttr ?? ariaLabel)}\n aria-describedby={describedBy || undefined}\n aria-invalid={isInvalid || !!rejected || undefined}\n className=\"min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default\"\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {error && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n {rejected && (\n <p id={rejectedId} className=\"text-xs text-[var(--danger)]\" data-chip-input-rejected=\"\">\n {rejected}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Chip","inner"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/chip.tsx"],"sourcesContent":["import { forwardRef, useId, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { X } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { FIELD_INVALID } from \"./ui\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\n\n/**\n * A chip: a compact pill carrying one value.\n *\n * Three jobs, deliberately one component. A chip can be inert (a tag on a row), a link\n * (a jump to a section), or a toggle (a filter that is on or off) — and the difference is\n * which prop you pass, not which component you pick. Keeping them together is what stops\n * an app growing three near-identical pills that drift apart; it is the shape this kit\n * already uses for `Button`, which is a link when it is given an href.\n *\n * A chip is not a `Button`. It is smaller, it is rounded to a pill, and it reads as a\n * VALUE rather than as an action — which matters because a row of eight buttons says\n * \"choose one of eight things to do\" and a row of eight chips says \"here are eight\n * things\". Using `Button` for both is the mistake this exists to prevent.\n */\n\nexport type ChipTone =\n | \"neutral\"\n | \"brand\"\n | \"danger\"\n | \"warning\"\n | \"success\"\n | \"info\"\n | \"income\"\n | \"expense\";\n/** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px. */\nexport type ChipSize = \"sm\" | \"md\" | \"lg\";\n/**\n * How much of the tone the chip wears.\n *\n * - `soft` (default): the tinted surface every chip has had.\n * - `outline`: the tone's border and text on NO surface — keksdose's DEV badge in the\n * account menu and kastlan's 27 shadcn `variant=\"outline\"` badges, which sit on\n * coloured rows where a second tint would muddy the row.\n * - `solid`: the tone as a fill under contrasting text — the \"count\" pill (unread\n * messages, open tasks). It centres its content and sets figures tabular, so a\n * one-digit count is a round dot and \"9\" → \"10\" does not jiggle the row.\n */\nexport type ChipVariant = \"soft\" | \"outline\" | \"solid\";\n/** `pill` (default) is fully rounded; `square` has the small radius of a field or a\n * button — for a chip that sits in a table cell or beside square controls. */\nexport type ChipShape = \"pill\" | \"square\";\n\nconst TONE: Record<ChipTone, { idle: string; selected: string }> = {\n neutral: {\n idle: \"border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-secondary)]\",\n selected: \"border-[var(--border-strong)] bg-[var(--bg-active)] text-[var(--text-primary)]\",\n },\n brand: {\n idle: \"border-[var(--border)] bg-[var(--brand-bg)] text-[var(--brand-muted)]\",\n selected: \"border-[var(--brand)] bg-[var(--brand)] text-[var(--brand-contrast)]\",\n },\n danger: {\n idle: \"border-[var(--danger-border)] bg-[var(--danger-bg)] text-[var(--danger)]\",\n selected: \"border-[var(--danger)] bg-[var(--danger)] text-[var(--danger-contrast)]\",\n },\n warning: {\n idle: \"border-[var(--warning-border)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n selected: \"border-[var(--warning)] bg-[var(--warning-bg)] text-[var(--warning)]\",\n },\n success: {\n idle: \"border-[var(--success-border)] bg-[var(--success-bg)] text-[var(--success)]\",\n selected: \"border-[var(--success)] bg-[var(--success-bg)] text-[var(--success)]\",\n },\n info: {\n idle: \"border-[var(--info-border)] bg-[var(--info-bg)] text-[var(--info)]\",\n selected: \"border-[var(--info)] bg-[var(--info-bg)] text-[var(--info)]\",\n },\n // The money pair (Keksdose's direction toggle). Only the TEXT and the BORDER carry\n // the tint, on the neutral chip's own surfaces: the money tokens are chosen for text\n // contrast, and a filled amber pill beside an amber figure reads as one smear.\n // `border-current` takes its hue from the text, so the two cannot drift apart.\n income: {\n idle: \"border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-income)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--money-income)]\",\n },\n expense: {\n idle: \"border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-expense)]\",\n selected: \"border-current bg-[var(--bg-active)] text-[var(--money-expense)]\",\n },\n};\n\n// `outline` idle looks: the tone's border and text, the surface left to whatever the\n// chip sits on. Selected still takes the tone's `selected` look (below), because an\n// outline toggle that stays an outline when on has no visible state.\nconst OUTLINE: Record<ChipTone, string> = {\n neutral: \"border-[var(--border-strong)] bg-transparent text-[var(--text-secondary)]\",\n brand: \"border-[var(--brand)] bg-transparent text-[var(--brand)]\",\n danger: \"border-[var(--danger-border)] bg-transparent text-[var(--danger)]\",\n warning: \"border-[var(--warning-border)] bg-transparent text-[var(--warning)]\",\n success: \"border-[var(--success-border)] bg-transparent text-[var(--success)]\",\n info: \"border-[var(--info-border)] bg-transparent text-[var(--info)]\",\n income: \"border-current/60 bg-transparent text-[var(--money-income)]\",\n expense: \"border-current/60 bg-transparent text-[var(--money-expense)]\",\n};\n\n// `solid`: the fill under its contrast pair. Only brand and danger have a declared\n// `-contrast` token; the other hues are 700s in the light theme and 300s in the dark\n// one, so `--text-inverse` (the surface colour) is the side of the pair that contrasts\n// with them in both — which is what the inverse token is for. The border goes\n// transparent rather than away, so a solid chip is exactly as tall as a soft one.\nconst SOLID: Record<ChipTone, string> = {\n neutral: \"border-transparent bg-[var(--bg-inverse)] text-[var(--text-inverse)]\",\n brand: \"border-transparent bg-[var(--brand)] text-[var(--brand-contrast)]\",\n danger: \"border-transparent bg-[var(--danger)] text-[var(--danger-contrast)]\",\n warning: \"border-transparent bg-[var(--warning)] text-[var(--text-inverse)]\",\n success: \"border-transparent bg-[var(--success)] text-[var(--text-inverse)]\",\n info: \"border-transparent bg-[var(--info)] text-[var(--text-inverse)]\",\n income: \"border-transparent bg-[var(--money-income)] text-[var(--text-inverse)]\",\n expense: \"border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]\",\n};\n\n/** The status-badge type (`caps`): a size step down, heavier and tracked, because\n * capitals at body size shout and capitals untracked run together. Per size, so it\n * replaces the size's own `text-*` through tailwind-merge. */\nconst CAPS: Record<ChipSize, string> = {\n sm: \"text-[10px] font-semibold uppercase tracking-wider\",\n md: \"text-[11px] font-semibold uppercase tracking-wider\",\n lg: \"text-xs font-semibold uppercase tracking-wider\",\n};\n\nfunction surfaceOf(tone: ChipTone, variant: ChipVariant, selected: boolean | undefined): string {\n const palette = TONE[tone];\n if (variant === \"solid\") {\n // A solid chip is already the strongest look there is, so \"on\" is a ring round it.\n return cn(SOLID[tone], selected && \"ring-2 ring-[var(--border-strong)] ring-offset-1 ring-offset-[var(--bg-surface)]\");\n }\n if (selected) return palette.selected;\n return variant === \"outline\" ? OUTLINE[tone] : palette.idle;\n}\n\n/** What {@link ChipProps.renderLink} is handed: everything the chip would have put on\n * its own `<a>`. Spread it onto your router's link, mapping `href` to what the link\n * calls it — `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface ChipLinkProps {\n href: string;\n /** The chip's whole look — keep it, or the pill is a bare link. */\n className: string;\n children: ReactNode;\n /** The chip's forwarded ref. React 19 passes it to a function component as a prop. */\n ref?: Ref<HTMLAnchorElement>;\n \"aria-current\"?: \"true\";\n id?: string;\n title?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * `body` is the whole pill when the chip is ONE element. When a link or a button also has\n * a dismiss control, the pill is a wrapper holding two siblings (see the note on\n * {@link Chip}): `split` is then the interactive part's own padding — the full start\n * inset, a short end one before the ×; `tail` is the wrapper's end inset after the ×.\n */\nconst SIZE: Record<\n ChipSize,\n { body: string; split: string; tail: string; icon: string; remove: string }\n> = {\n sm: {\n body: \"gap-1 px-2 py-0.5 text-xs\",\n split: \"gap-1 ps-2 pe-1 py-0.5 text-xs\",\n tail: \"pe-1.5\",\n icon: \"size-3\",\n remove: \"size-3\",\n },\n md: {\n body: \"gap-1.5 px-2.5 py-1 text-sm\",\n split: \"gap-1.5 ps-2.5 pe-1.5 py-1 text-sm\",\n tail: \"pe-2\",\n icon: \"size-3.5\",\n remove: \"size-3.5\",\n },\n lg: {\n body: \"min-h-11 gap-2 px-4 py-2 text-sm\",\n split: \"min-h-11 gap-2 ps-4 pe-2 py-2 text-sm\",\n tail: \"min-h-11 pe-3.5\",\n icon: \"size-4\",\n remove: \"size-4\",\n },\n};\n\nconst CHIP_PILL = \"inline-flex max-w-full items-center rounded-full border transition-colors\";\n// `focus-visible`, not `focus`: a chip commonly receives focus programmatically (the\n// ChipInput moves focus onto one after a removal) and a ring that appears on a\n// pointer click reads as a stuck selection.\nconst CHIP_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-1 focus-visible:ring-offset-[var(--bg-surface)]\";\nconst CHIP_BASE = `${CHIP_PILL} ${CHIP_RING}`;\n\ninterface ChipBaseProps {\n children: ReactNode;\n tone?: ChipTone;\n size?: ChipSize;\n /** See {@link ChipVariant}. Default `soft`. */\n variant?: ChipVariant;\n /** See {@link ChipShape}. Default `pill`. */\n shape?: ChipShape;\n /**\n * The uppercase, tracked \"status badge\" type — keksdose's ~25 PAID / DRAFT /\n * OVERDUE / ADMIN badges, each spelling `uppercase tracking-wide text-[10px]` by\n * hand. A flag rather than a fourth `variant` because it is TYPE, not surface: a\n * status badge is soft, outlined or solid as the row needs, and a `variant=\"tag\"`\n * would have had to pick one. Screen readers read the text as written, not as\n * drawn, so write it in normal case.\n */\n caps?: boolean;\n icon?: LucideIcon;\n /**\n * Marks the chip as the current one — `aria-current` on a link, `aria-pressed` on a\n * toggle.\n *\n * On an ON/OFF toggle, keep the LABEL the same in both states and let `selected`\n * carry the state. A label that flips with it (\"Skip this month\" / \"Ask again this\n * month\") is announced together with \"pressed\", and then says the opposite of what\n * it does.\n *\n * A control that switches between two NAMED states (outflow ⇄ inflow) is not a\n * toggle but an ACTION button: leave `selected` out, let the visible label and the\n * tone follow the state (\"− Outflow\" / \"+ Inflow\"), and name the action in\n * `aria-label` (\"Direction: outflow — tap for inflow\"). Without `selected` the chip\n * reports no pressed state at all. A fixed label there contradicts the figure beside\n * it — \"+ Outflow\" in green next to an inflow (keksdose #417).\n */\n selected?: boolean;\n /** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */\n onRemove?: () => void;\n /** Accessible name for the dismiss button. Default: `common.remove` from the\n * {@link UiKitProvider}, else \"Remove\". */\n removeLabel?: string;\n /** Disables the × ONLY — the chip itself stays a working link or toggle. For the\n * value that may not be removed right now (the last filter of a required set, a\n * tag the user may read but not edit) where `disabled` would also kill the chip's\n * own action. */\n removeDisabled?: boolean;\n disabled?: boolean;\n className?: string;\n /**\n * Roving focus, for a container that manages a row of chips (see {@link ChipInput}).\n * A chip with no `href`/`onClick` is inert and unfocusable by default; give it a\n * `tabIndex` and a `role` and it becomes the thing arrow keys move between.\n */\n tabIndex?: number;\n role?: string;\n onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;\n onFocus?: () => void;\n /** Accessible name, when the visible text is not the whole story. */\n \"aria-label\"?: string;\n /** Any other ARIA attribute — `aria-describedby`, `aria-expanded`, `aria-controls`,\n * `aria-haspopup` — reaches the chip's interactive element. */\n [key: `aria-${string}`]: string | boolean | number | undefined;\n /** A test id, a `data-tour` anchor, or any other data attribute. */\n [key: `data-${string}`]: unknown;\n id?: string;\n /** A native tooltip. For anything a user must read, prefer the kit's `Tooltip`. */\n title?: string;\n}\n\n/**\n * A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were\n * accepted and `onClick` was silently dropped whenever `href` was set; a link that must\n * also run code wants a router `Link`-style `onClick` on the anchor, which is a different\n * component's job.\n */\nexport type ChipProps = ChipBaseProps &\n (\n | {\n /** Renders the chip as a link. Mutually exclusive with `onClick`. */\n href?: string;\n onClick?: never;\n /**\n * Renders the link for `href` — pass your router's `<Link>` here, since a\n * plain `<a>` reloads a single-page app (keksdose's account-menu admin pill).\n * Default: `<a>`. Ignored without `href`.\n *\n * A render function rather than an `as={Link}`, for three reasons. It is the\n * API `StatTile` already has (`renderLink`), so a consumer learns it once.\n * Router links do not take `href` — react-router's takes `to` — so an `as`\n * would either have to pass props through untyped or teach the kit about one\n * router; here the caller maps `href` onto its own link in one line, fully\n * typed. And it keeps `href` as THE link prop, which is what the `href` /\n * `onClick` exclusion below is typed on: the pill is a link because it has an\n * href, whichever element draws it.\n */\n renderLink?: (props: ChipLinkProps) => ReactElement;\n }\n | {\n href?: never;\n renderLink?: never;\n /** Renders the chip as a toggle button. Mutually exclusive with `href`. Receives\n * the click, so a chip inside a clickable row can `stopPropagation()`. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n }\n );\n\n/**\n * Note on `onRemove` with `href`/`onClick`: a button cannot be nested inside a button or\n * a link, so when both are present the chip renders a wrapper holding TWO siblings — the\n * interactive body and the dismiss control. The WRAPPER is the visual pill (border, tone,\n * `className`) and the two sit inside it, so the × reads as part of the chip; the body's\n * focus ring is drawn on the body alone, and `ref`, `id` and the ARIA props still reach\n * the body — the interactive element.\n */\nexport const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(\n {\n children,\n tone = \"neutral\",\n size = \"md\",\n variant = \"soft\",\n shape = \"pill\",\n caps = false,\n icon: Icon,\n selected,\n href,\n renderLink,\n onClick,\n onRemove,\n removeLabel,\n removeDisabled = false,\n disabled = false,\n className,\n tabIndex,\n role,\n onKeyDown,\n onFocus,\n ...rest\n },\n ref,\n) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS, { remove: removeLabel });\n const s = SIZE[size];\n const interactive = !!href || !!onClick;\n // One radius for every rounded piece — the pill, the body inside a split pill, the ×\n // — so a square chip has no round focus ring inside it.\n const radius = shape === \"square\" ? (size === \"sm\" ? \"rounded\" : \"rounded-md\") : \"rounded-full\";\n const type = cn(caps && CAPS[size], variant === \"solid\" && \"justify-center tabular-nums\");\n const surface = cn(\n surfaceOf(tone, variant, selected),\n disabled && \"cursor-default opacity-50\",\n );\n const look = cn(\n CHIP_BASE,\n s.body,\n type,\n radius,\n surface,\n interactive && !disabled && \"cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110\",\n className,\n );\n\n const body = (\n <>\n {Icon && <Icon className={cn(s.icon, \"shrink-0\")} aria-hidden />}\n <span className=\"min-w-0 truncate\">{children}</span>\n </>\n );\n\n const remove = onRemove ? (\n <button\n type=\"button\"\n // The name carries WHAT is being removed, not just \"Remove\" — a screen-reader user\n // tabbing a row of eight dismiss buttons otherwise hears the same word eight times\n // with no way to tell which one they are on.\n // Composed through `common.fieldValue` because the punctuation between the two\n // halves is the language's, not ours. A chip whose content is not plain text\n // has no value to name, and gets the bare verb rather than a dangling colon.\n aria-label={\n typeof children === \"string\" && children\n ? common.fieldValue(common.remove, children)\n : common.remove\n }\n onClick={(e) => {\n // The chip may itself be a link; removing it must not also follow it.\n e.preventDefault();\n e.stopPropagation();\n onRemove();\n }}\n disabled={disabled || removeDisabled}\n className={cn(\n // Logical margins: the × sits at the END of the pill, which is the left in RTL.\n \"-me-0.5 ms-0.5 shrink-0 p-0.5 transition-colors\",\n shape === \"square\" ? \"rounded-sm\" : \"rounded-full\",\n \"hover:bg-[var(--bg-active)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]\",\n (disabled || removeDisabled) && \"pointer-events-none\",\n // A disabled chip is already dimmed as a whole; dimming the × again on top\n // would take it to a quarter. `removeDisabled` alone dims just the ×.\n removeDisabled && !disabled && \"opacity-40\",\n )}\n >\n <X className={s.remove} aria-hidden />\n </button>\n ) : null;\n\n // ── The combined shape: one pill, two interactive siblings inside it.\n const pill = (inner: ReactNode) => (\n <span className={cn(CHIP_PILL, s.tail, radius, surface, className)}>\n {inner}\n {remove}\n </span>\n );\n // The body inside a pill: no border or surface of its own (the pill has them), a\n // hover wash in the text's own hue so it works on every tone.\n const inner = cn(\n \"inline-flex min-w-0 items-center self-stretch transition-colors\",\n radius,\n CHIP_RING,\n s.split,\n type,\n !disabled && \"cursor-pointer hover:bg-current/10\",\n disabled && \"cursor-default\",\n );\n\n // ── The three shapes.\n if (href && !disabled) {\n const linkProps: ChipLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n \"aria-current\": selected ? \"true\" : undefined,\n className: remove ? inner : look,\n children: body,\n };\n const link = renderLink ? (\n // Through a component rather than called here, so the forwarded ref arrives as\n // an ordinary prop of an element and is never handed to a function mid-render.\n <RenderedLink render={renderLink} {...linkProps} />\n ) : (\n <a\n ref={ref as React.Ref<HTMLAnchorElement>}\n href={href}\n aria-current={selected ? \"true\" : undefined}\n className={remove ? inner : look}\n {...rest}\n >\n {body}\n </a>\n );\n return remove ? pill(link) : link;\n }\n\n if (onClick) {\n const button = (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onClick}\n disabled={disabled}\n // Only when `selected` is PASSED: a chip with `onClick` and no `selected` is an\n // action button (it does something), not a toggle (it is on or off), and\n // announcing it \"not pressed\" would claim a state it does not have.\n aria-pressed={selected}\n className={remove ? inner : look}\n {...rest}\n >\n {body}\n </button>\n );\n return remove ? pill(button) : button;\n }\n\n return (\n <span\n ref={ref as React.Ref<HTMLSpanElement>}\n className={look}\n tabIndex={tabIndex}\n role={role}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n {...rest}\n >\n {body}\n {remove}\n </span>\n );\n});\n\n/** Calls a chip's `renderLink` with the props it was given — see its call site. */\nfunction RenderedLink({\n render,\n ...props\n}: ChipLinkProps & { render: (props: ChipLinkProps) => ReactElement }) {\n return render(props);\n}\n\n/* ── ChipInput ────────────────────────────────────────────────────────────── */\n\nexport interface ChipInputLabels {\n /** Announced after a value is added. Receives the value. */\n added: (value: string) => string;\n /** Announced after a value is removed. Receives the value. */\n removed: (value: string) => string;\n /** Accessible name for each chip's dismiss button. */\n remove: string;\n /** Shown when `max` is reached. Receives the limit. */\n atLimit: (max: number) => string;\n /** Shown when a duplicate is rejected. Receives the value. */\n duplicate: (value: string) => string;\n}\n\nexport const DEFAULT_CHIP_INPUT_LABELS: ChipInputLabels = {\n added: (v) => `${v} added`,\n removed: (v) => `${v} removed`,\n remove: \"Remove\",\n atLimit: (max) => `Limit of ${max} reached`,\n duplicate: (v) => `${v} is already in the list`,\n};\n\nexport function resolveChipInputLabels(labels?: Partial<ChipInputLabels>): ChipInputLabels {\n return labels ? { ...DEFAULT_CHIP_INPUT_LABELS, ...labels } : DEFAULT_CHIP_INPUT_LABELS;\n}\n\n/**\n * `onChange` is the field's own — the whole LIST, not a DOM event — so the wrapper\n * div's `onChange` is omitted rather than shadowed. `placeholder` is likewise this\n * component's, forwarded to the inner `<input>`; the div has none of its own.\n */\nexport interface ChipInputProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"placeholder\" | \"children\"> {\n value: string[];\n onChange: (next: string[]) => void;\n placeholder?: string;\n /** Visible label, rendered above the field. Without one, name the field with\n * `aria-label`. */\n label?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling the rest of this kit\n * now uses. Still honoured, and still second to `aria-label` when both are given.\n */\n ariaLabel?: string;\n /** Reject or rewrite a value on commit. Return a string to reject WITH that message. */\n validate?: (raw: string) => string | null;\n /** Characters that commit the current text, besides Enter. Default: comma. */\n separators?: string[];\n allowDuplicates?: boolean;\n max?: number;\n tone?: ChipTone;\n size?: ChipSize;\n disabled?: boolean;\n invalid?: boolean;\n /** Error text. Wires `aria-describedby` and implies `invalid`. */\n error?: ReactNode;\n labels?: Partial<ChipInputLabels>;\n className?: string;\n}\n\n/**\n * A field whose value is a LIST, entered as chips.\n *\n * The keyboard model is the whole component; the pills are the easy part. Per the WAI\n * pattern for a \"tag\" input:\n *\n * Enter, or any separator commit the typed text\n * Backspace on empty text move focus to the last chip (does NOT delete it)\n * ← → while on a chip move between chips, in reading order (mirrored in RTL)\n * Backspace / Delete remove the focused chip, focus its neighbour\n * Escape clear what is typed but keep the committed chips\n * paste splits on the separators, so a pasted CSV becomes chips\n *\n * The Backspace-on-empty behaviour is the one worth arguing about. Deleting immediately\n * is more common and is a trap: it is a destructive action with no feedback on a key\n * people hit by reflex, and the value it destroys may have taken a while to type. Moving\n * focus first makes the second Backspace a deliberate one, and it is what lets a\n * keyboard user reach a chip at all.\n */\nexport function ChipInput({\n value,\n onChange,\n placeholder,\n label,\n ariaLabel,\n validate,\n separators = [\",\"],\n allowDuplicates = false,\n max,\n tone = \"neutral\",\n size = \"md\",\n disabled = false,\n invalid = false,\n error,\n labels,\n className,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ChipInputProps) {\n // `labels` over the provider's `chipInput` over English — the same order\n // `resolveChipInputLabels` gives outside a provider.\n const text = useKitLabels(\"chipInput\", DEFAULT_CHIP_INPUT_LABELS, labels);\n // A batch of pasted values is announced as one list, joined the way the language\n // joins a list in a sentence (\"a, b and c added\") — hence the long conjunction,\n // where a trigger's summary would use the narrow one.\n const locale = useKitLocale();\n const [draft, setDraft] = useState(\"\");\n const [message, setMessage] = useState(\"\");\n // The last refusal, shown under the field until the next edit. The live region alone\n // told only a screen-reader user why Enter did nothing; everyone else saw the text\n // simply stay put.\n const [rejected, setRejected] = useState<string | null>(null);\n const [focusedChip, setFocusedChip] = useState<number | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const chipRefs = useRef<Array<HTMLElement | null>>([]);\n const id = useId();\n const listId = `${id}-list`;\n const errorId = `${id}-error`;\n const rejectedId = `${id}-rejected`;\n const isInvalid = invalid || !!error;\n const describedBy = [error ? errorId : null, rejected ? rejectedId : null]\n .filter(Boolean)\n .join(\" \");\n\n const focusChip = (index: number | null) => {\n setFocusedChip(index);\n if (index === null) inputRef.current?.focus();\n else chipRefs.current[index]?.focus();\n };\n\n /**\n * Try to add every candidate, as ONE change.\n *\n * Batched rather than looped for a reason that bites immediately: `commit` closes over\n * `value`, so calling it once per pasted item makes every call compute its result from\n * the list as it was BEFORE the paste, and each `onChange` overwrites the last. A\n * pasted \"one,two,three\" left only \"three\". Accumulating locally and emitting once is\n * also the correct behaviour for an undo stack — a paste is one action.\n */\n const commitMany = (raws: string[]): { added: string[]; rejection: string | null } => {\n const next = [...value];\n const added: string[] = [];\n let rejection: string | null = null;\n\n for (const raw of raws) {\n const candidate = raw.trim();\n if (!candidate) continue;\n if (max !== undefined && next.length >= max) {\n rejection ??= text.atLimit(max);\n break;\n }\n if (!allowDuplicates && next.includes(candidate)) {\n rejection ??= text.duplicate(candidate);\n continue;\n }\n const invalidReason = validate?.(candidate);\n if (invalidReason) {\n rejection ??= invalidReason;\n continue;\n }\n next.push(candidate);\n added.push(candidate);\n }\n\n if (added.length) onChange(next);\n // A rejection is the more useful thing to say: \"three added\" is obvious from the\n // chips appearing, whereas \"already in the list\" is the only sign of what did not.\n const addedList = new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" });\n setMessage(rejection ?? (added.length ? text.added(addedList.format(added)) : \"\"));\n setRejected(rejection);\n return { added, rejection };\n };\n\n const commit = (raw: string): boolean => commitMany([raw]).added.length > 0;\n\n const removeAt = (index: number) => {\n const removed = value[index];\n onChange(value.filter((_, i) => i !== index));\n setMessage(text.removed(removed));\n setRejected(null);\n // Focus the neighbour that takes its place, or the input when the list empties —\n // dropping focus to <body> here is how a keyboard user loses the field entirely.\n const next = index >= value.length - 1 ? null : index;\n requestAnimationFrame(() => focusChip(value.length - 1 === 0 ? null : next));\n };\n\n const onInputKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\") {\n // Only when there is something to commit: an empty Enter belongs to the form.\n if (draft.trim()) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n }\n return;\n }\n if (separators.includes(e.key)) {\n e.preventDefault();\n if (commit(draft)) setDraft(\"\");\n return;\n }\n if (e.key === \"Escape\" && draft) {\n e.preventDefault();\n setDraft(\"\");\n return;\n }\n if (e.key === \"Backspace\" && !draft && value.length) {\n e.preventDefault();\n focusChip(value.length - 1);\n }\n };\n\n const onChipKeyDown = (e: KeyboardEvent<HTMLElement>, index: number) => {\n if (e.key === \"Backspace\" || e.key === \"Delete\") {\n e.preventDefault();\n removeAt(index);\n return;\n }\n // ←/→ follow the reading direction, not DOM order: in RTL the previous chip is to\n // the RIGHT, and ArrowLeft moves on towards the input.\n const step = horizontalStep(e.key, e.currentTarget);\n if (step === -1 && index > 0) {\n e.preventDefault();\n focusChip(index - 1);\n return;\n }\n if (step === 1) {\n e.preventDefault();\n focusChip(index === value.length - 1 ? null : index + 1);\n return;\n }\n if (e.key === \"Escape\") {\n e.preventDefault();\n focusChip(null);\n }\n };\n\n return (\n // The rest lands on the OUTER box — the thing a `data-tour` step or a test id\n // wants to point at — while the accessible name below goes on the `<input>`,\n // because that is what a screen-reader user actually lands in. Spreading the two\n // to the same element would have named a decorative wrapper and left the field\n // anonymous.\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {label && (\n <label htmlFor={`${id}-input`} className=\"block text-sm text-[var(--text-secondary)]\">\n {label}\n </label>\n )}\n <div\n // `relative` so the live region below can be `sr-only` without escaping to the\n // initial containing block and inflating the document height.\n className={cn(\n \"relative flex flex-wrap items-center gap-1.5 rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5 shadow-sm\",\n \"focus-within:border-[var(--brand)] focus-within:ring-1 focus-within:ring-[var(--brand)]\",\n disabled && \"cursor-default bg-[var(--bg-surface-2)]\",\n isInvalid && FIELD_INVALID,\n )}\n onClick={() => !disabled && inputRef.current?.focus()}\n >\n {/* `display: contents` on the wrappers so the chips participate in the field's\n own flex wrap rather than forming a nested row. The list semantics are\n restored with explicit roles, because `contents` removes the ul/li boxes from\n the accessibility tree in several engines. */}\n {/* eslint-disable-next-line jsx-a11y/no-redundant-roles -- NOT redundant here: the\n `contents` display below removes the ul and li boxes from the box tree, and\n several engines drop the list semantics with them. The explicit role is what\n puts \"list, 3 items\" back. */}\n <ul id={listId} role=\"list\" className=\"contents\">\n {value.map((v, i) => (\n <li key={`${v}-${i}`} role=\"presentation\" className=\"contents\">\n <Chip\n ref={(el: HTMLElement | null) => {\n chipRefs.current[i] = el;\n }}\n tone={tone}\n size={size}\n disabled={disabled}\n removeLabel={text.remove}\n onRemove={disabled ? undefined : () => removeAt(i)}\n // A ROVING tabIndex: only the focused chip is tabbable, so Tab leaves\n // the field instead of walking through every value in it — the reason a\n // list of twenty tags is otherwise a twenty-stop detour for a keyboard.\n tabIndex={focusedChip === i ? 0 : -1}\n role=\"listitem\"\n aria-label={v}\n onKeyDown={(e) => onChipKeyDown(e, i)}\n onFocus={() => setFocusedChip(i)}\n data-chip-index={i}\n >\n {v}\n </Chip>\n </li>\n ))}\n </ul>\n <input\n id={`${id}-input`}\n ref={inputRef}\n value={draft}\n onChange={(e) => {\n setDraft(e.target.value);\n setMessage(\"\");\n setRejected(null);\n }}\n onKeyDown={onInputKeyDown}\n onBlur={() => {\n // Commit on blur: a value typed and then clicked away from is a value the\n // user meant to add, and losing it silently is the commonest complaint\n // about this pattern.\n if (draft.trim() && commit(draft)) setDraft(\"\");\n }}\n onPaste={(e) => {\n const pasted = e.clipboardData.getData(\"text\");\n const pattern = new RegExp(`[${separators.map((s) => `\\\\${s}`).join(\"\")}\\n\\r]`);\n if (!pattern.test(pasted)) return;\n e.preventDefault();\n if (commitMany(pasted.split(pattern)).added.length) setDraft(\"\");\n }}\n placeholder={value.length ? undefined : placeholder}\n disabled={disabled}\n // A visible `label` is already wired through `htmlFor`, so adding a name\n // here would give the field two. Failing that, the DOM spelling wins and\n // the deprecated `ariaLabel` is the fallback.\n aria-label={label ? undefined : (ariaLabelAttr ?? ariaLabel)}\n aria-describedby={describedBy || undefined}\n aria-invalid={isInvalid || !!rejected || undefined}\n className=\"min-w-[6rem] flex-1 bg-transparent py-0.5 text-sm text-[var(--text-primary)] outline-none placeholder:text-[var(--text-placeholder)] disabled:cursor-default\"\n />\n {/* Additions, removals and rejections move no focus, so nothing would announce\n them. `sr-only-fixed` rather than `sr-only`: this sits inside a consumer's\n markup and must not extend the document height. */}\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {message}\n </span>\n </div>\n {error && (\n <p id={errorId} className=\"text-xs text-[var(--danger)]\">\n {error}\n </p>\n )}\n {rejected && (\n <p id={rejectedId} className=\"text-xs text-[var(--danger)]\" data-chip-input-rejected=\"\">\n {rejected}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["Chip","inner"]}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback
|
|
6
|
+
* text. A call that knows what it is confirming says so per call (`confirmLabel:
|
|
7
|
+
* "Delete budget"`), which is what the dialog is for — these are for the rest.
|
|
8
|
+
*/
|
|
9
|
+
interface ConfirmDialogLabels {
|
|
10
|
+
confirm: string;
|
|
11
|
+
cancel: string;
|
|
12
|
+
}
|
|
13
|
+
declare const DEFAULT_CONFIRM_DIALOG_LABELS: ConfirmDialogLabels;
|
|
14
|
+
type ConfirmTone = "danger" | "warning" | "neutral";
|
|
15
|
+
interface ConfirmOptions {
|
|
16
|
+
/** The question, and the dialog's accessible name. */
|
|
17
|
+
title: ReactNode;
|
|
18
|
+
/** The smaller line under it — the consequence. Wired to `aria-describedby`. */
|
|
19
|
+
body?: ReactNode;
|
|
20
|
+
/** Default: `confirmDialog.confirm` from the provider, else "Confirm". */
|
|
21
|
+
confirmLabel?: ReactNode;
|
|
22
|
+
/** Default: `confirmDialog.cancel` from the provider, else "Cancel". */
|
|
23
|
+
cancelLabel?: ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* `"danger"` for what cannot be undone (a delete): the confirm is the destructive
|
|
26
|
+
* button and focus starts on CANCEL. `"warning"` and `"neutral"` (default) start on
|
|
27
|
+
* the confirm. See {@link ConfirmProvider} for why the two differ.
|
|
28
|
+
*/
|
|
29
|
+
tone?: ConfirmTone;
|
|
30
|
+
}
|
|
31
|
+
type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.
|
|
34
|
+
*
|
|
35
|
+
* The returned function is STABLE for the life of the provider, so it can sit in an
|
|
36
|
+
* effect's or a `useCallback`'s dependency list without re-running it.
|
|
37
|
+
*
|
|
38
|
+
* Throws outside a {@link ConfirmProvider}, rather than falling back to
|
|
39
|
+
* `window.confirm`: a silent fallback is exactly the native dialog the three apps are
|
|
40
|
+
* migrating away from, and it would look like the migration had worked.
|
|
41
|
+
*/
|
|
42
|
+
declare function useConfirm(): ConfirmFn;
|
|
43
|
+
interface ConfirmProviderProps {
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The host for {@link useConfirm}: ONE confirm dialog for the whole app, rendered on
|
|
48
|
+
* {@link DialogFrame}, answering a promise.
|
|
49
|
+
*
|
|
50
|
+
* Asked for by all three apps at once — keksdose has 20+ `window.confirm` calls,
|
|
51
|
+
* kastlan 7, lenkbank 1. The native dialog cannot be translated past its OK/Cancel
|
|
52
|
+
* (those are the BROWSER's language, not the app's), cannot say which button is the
|
|
53
|
+
* destructive one, and on an installed PWA it prints the origin over the question.
|
|
54
|
+
* A promise is what lets each of those call sites change by one `await`:
|
|
55
|
+
*
|
|
56
|
+
* if (!window.confirm(t("budget.delete_confirm"))) return;
|
|
57
|
+
* // becomes
|
|
58
|
+
* if (!(await confirm({ title: t("budget.delete_confirm"), tone: "danger" }))) return;
|
|
59
|
+
*
|
|
60
|
+
* ## Its own provider, not part of `UiKitProvider`
|
|
61
|
+
*
|
|
62
|
+
* `UiKitProvider` renders nothing: it is strings and a locale, it is optional, and it
|
|
63
|
+
* NESTS (an inner one re-labels one table). A dialog host inside it would mount a host
|
|
64
|
+
* per nested provider, and a component under the inner one would open a second dialog
|
|
65
|
+
* layer of its own. So the host is its own component, mounted once — inside the
|
|
66
|
+
* `UiKitProvider` whose strings it should speak, which is also the order the labels
|
|
67
|
+
* need.
|
|
68
|
+
*
|
|
69
|
+
* ## Focus
|
|
70
|
+
*
|
|
71
|
+
* On CANCEL for `tone: "danger"`, on CONFIRM otherwise. A confirm is often answered by
|
|
72
|
+
* a reflexive Enter — the key that pressed the Delete button in the first place may
|
|
73
|
+
* still be down — and for an action that cannot be undone that reflex must land on the
|
|
74
|
+
* harmless answer; `DangerConfirm` makes the same call ("never to the destructive
|
|
75
|
+
* button itself"). For everything else the confirm is the expected answer and Enter
|
|
76
|
+
* accepting it is what `window.confirm` taught users, which the migrating call sites'
|
|
77
|
+
* users already rely on. Focus returns to whatever opened it, through `Modal`'s trap.
|
|
78
|
+
*
|
|
79
|
+
* ## Escape, the backdrop, Back and Cancel all answer `false`
|
|
80
|
+
*
|
|
81
|
+
* Only the confirm button answers `true`. Anything that merely dismisses is a no.
|
|
82
|
+
*
|
|
83
|
+
* ## A second `confirm` while one is open QUEUES
|
|
84
|
+
*
|
|
85
|
+
* It is shown when the first is answered, and each call gets its own answer. The
|
|
86
|
+
* alternative — answering the open one `false` to make room — would answer a question
|
|
87
|
+
* the user never answered: a background prompt ("your session is about to expire")
|
|
88
|
+
* would cancel the delete they were reading. `false` has to mean "the user said no".
|
|
89
|
+
* A double-click cannot queue two of the same: the first click opens the modal
|
|
90
|
+
* synchronously (a discrete event's update is flushed before the next one), so the
|
|
91
|
+
* second lands on the open dialog rather than on the button — at worst on its
|
|
92
|
+
* backdrop, which answers `false`, the safe way to be wrong.
|
|
93
|
+
*
|
|
94
|
+
* Unmounting the provider answers everything still pending `false`, so no caller's
|
|
95
|
+
* `await` hangs forever.
|
|
96
|
+
*
|
|
97
|
+
* ## keksdose's synchronous call (transaction-swipe-plan.ts)
|
|
98
|
+
*
|
|
99
|
+
* `buildTxSwipeActions` is synchronous, but its `window.confirm` sits inside the
|
|
100
|
+
* action THUNK (`confirmed(key, run)` returns `() => { if (confirm(…)) run() }`), and a
|
|
101
|
+
* thunk may be async without its caller knowing. So the plan builder stays
|
|
102
|
+
* synchronous; it takes the confirm as an argument and the thunk awaits it:
|
|
103
|
+
*
|
|
104
|
+
* // mobile-transaction-list.tsx
|
|
105
|
+
* const confirm = useConfirm();
|
|
106
|
+
* buildTxSwipeActions({ tx, …, confirm });
|
|
107
|
+
*
|
|
108
|
+
* // transaction-swipe-plan.ts
|
|
109
|
+
* const confirmed = (messageKey: string, run: () => void) => () => {
|
|
110
|
+
* void confirm({ title: t(messageKey), tone: "danger" }).then((ok) => ok && run());
|
|
111
|
+
* };
|
|
112
|
+
*
|
|
113
|
+
* Its test then passes `confirm: vi.fn(async () => false)` instead of stubbing the
|
|
114
|
+
* global, and asserts after an `await` (the mutation now runs a microtask later). The
|
|
115
|
+
* row snaps back when the swipe commits, before the answer — which is right: the
|
|
116
|
+
* dialog, not the half-swiped row, is where the question now is.
|
|
117
|
+
*/
|
|
118
|
+
declare function ConfirmProvider({ children }: ConfirmProviderProps): react.JSX.Element;
|
|
119
|
+
|
|
120
|
+
export { type ConfirmDialogLabels, type ConfirmFn, type ConfirmOptions, ConfirmProvider, type ConfirmProviderProps, type ConfirmTone, DEFAULT_CONFIRM_DIALOG_LABELS, useConfirm };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { DialogFrame } from "./dialog-frame.js";
|
|
6
|
+
import { Button } from "./ui.js";
|
|
7
|
+
const DEFAULT_CONFIRM_DIALOG_LABELS = {
|
|
8
|
+
confirm: "Confirm",
|
|
9
|
+
cancel: "Cancel"
|
|
10
|
+
};
|
|
11
|
+
const ConfirmContext = createContext(null);
|
|
12
|
+
function useConfirm() {
|
|
13
|
+
const confirm = useContext(ConfirmContext);
|
|
14
|
+
if (!confirm) {
|
|
15
|
+
throw new Error("useConfirm() must be called below a <ConfirmProvider>.");
|
|
16
|
+
}
|
|
17
|
+
return confirm;
|
|
18
|
+
}
|
|
19
|
+
function ConfirmProvider({ children }) {
|
|
20
|
+
const [queue, setQueue] = useState([]);
|
|
21
|
+
const nextId = useRef(0);
|
|
22
|
+
const pending = useRef(/* @__PURE__ */ new Set());
|
|
23
|
+
const confirm = useCallback(
|
|
24
|
+
(options) => new Promise((resolve) => {
|
|
25
|
+
const request = { ...options, id: ++nextId.current, resolve };
|
|
26
|
+
pending.current.add(request);
|
|
27
|
+
setQueue((q) => [...q, request]);
|
|
28
|
+
}),
|
|
29
|
+
[]
|
|
30
|
+
);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const open = pending.current;
|
|
33
|
+
return () => {
|
|
34
|
+
for (const r of open) r.resolve(false);
|
|
35
|
+
open.clear();
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
const answer = useCallback((request, value) => {
|
|
39
|
+
if (!pending.current.delete(request)) return;
|
|
40
|
+
request.resolve(value);
|
|
41
|
+
setQueue((q) => q.filter((r) => r !== request));
|
|
42
|
+
}, []);
|
|
43
|
+
const current = queue[0];
|
|
44
|
+
return /* @__PURE__ */ jsxs(ConfirmContext.Provider, { value: confirm, children: [
|
|
45
|
+
children,
|
|
46
|
+
current && // Keyed per request, so a queued one gets a fresh Modal: the first one's trap
|
|
47
|
+
// hands focus back to the page, and the next one's captures that as its own
|
|
48
|
+
// restore target instead of a button that has just been unmounted.
|
|
49
|
+
/* @__PURE__ */ jsx(ConfirmDialogView, { request: current, onAnswer: answer }, current.id)
|
|
50
|
+
] });
|
|
51
|
+
}
|
|
52
|
+
function ConfirmDialogView({
|
|
53
|
+
request,
|
|
54
|
+
onAnswer
|
|
55
|
+
}) {
|
|
56
|
+
const labels = useKitLabels("confirmDialog", DEFAULT_CONFIRM_DIALOG_LABELS);
|
|
57
|
+
const tone = request.tone ?? "neutral";
|
|
58
|
+
const confirmRef = useRef(null);
|
|
59
|
+
const cancelRef = useRef(null);
|
|
60
|
+
const accepted = useRef(false);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
(tone === "danger" ? cancelRef : confirmRef).current?.focus();
|
|
63
|
+
}, [tone]);
|
|
64
|
+
return /* @__PURE__ */ jsx(
|
|
65
|
+
DialogFrame,
|
|
66
|
+
{
|
|
67
|
+
title: request.title,
|
|
68
|
+
description: request.body,
|
|
69
|
+
onClose: () => onAnswer(request, accepted.current),
|
|
70
|
+
role: "alertdialog",
|
|
71
|
+
"data-confirm-tone": tone,
|
|
72
|
+
actions: (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
73
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelRef, type: "button", variant: "ghost", onClick: close, children: request.cancelLabel ?? labels.cancel }),
|
|
74
|
+
/* @__PURE__ */ jsx(
|
|
75
|
+
Button,
|
|
76
|
+
{
|
|
77
|
+
ref: confirmRef,
|
|
78
|
+
type: "button",
|
|
79
|
+
variant: tone === "danger" ? "danger" : "primary",
|
|
80
|
+
onClick: () => {
|
|
81
|
+
accepted.current = true;
|
|
82
|
+
close();
|
|
83
|
+
},
|
|
84
|
+
children: request.confirmLabel ?? labels.confirm
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
] })
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
ConfirmProvider,
|
|
93
|
+
DEFAULT_CONFIRM_DIALOG_LABELS,
|
|
94
|
+
useConfirm
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=confirm-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/confirm-dialog.tsx"],"sourcesContent":["import { createContext, useCallback, useContext, useEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { DialogFrame } from \"./dialog-frame\";\nimport { Button } from \"./ui\";\n\n/**\n * The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback\n * text. A call that knows what it is confirming says so per call (`confirmLabel:\n * \"Delete budget\"`), which is what the dialog is for — these are for the rest.\n */\nexport interface ConfirmDialogLabels {\n confirm: string;\n cancel: string;\n}\n\nexport const DEFAULT_CONFIRM_DIALOG_LABELS: ConfirmDialogLabels = {\n confirm: \"Confirm\",\n cancel: \"Cancel\",\n};\n\nexport type ConfirmTone = \"danger\" | \"warning\" | \"neutral\";\n\nexport interface ConfirmOptions {\n /** The question, and the dialog's accessible name. */\n title: ReactNode;\n /** The smaller line under it — the consequence. Wired to `aria-describedby`. */\n body?: ReactNode;\n /** Default: `confirmDialog.confirm` from the provider, else \"Confirm\". */\n confirmLabel?: ReactNode;\n /** Default: `confirmDialog.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /**\n * `\"danger\"` for what cannot be undone (a delete): the confirm is the destructive\n * button and focus starts on CANCEL. `\"warning\"` and `\"neutral\"` (default) start on\n * the confirm. See {@link ConfirmProvider} for why the two differ.\n */\n tone?: ConfirmTone;\n}\n\nexport type ConfirmFn = (options: ConfirmOptions) => Promise<boolean>;\n\nconst ConfirmContext = createContext<ConfirmFn | null>(null);\n\n/**\n * `const confirm = useConfirm()` → `if (!(await confirm({ title }))) return`.\n *\n * The returned function is STABLE for the life of the provider, so it can sit in an\n * effect's or a `useCallback`'s dependency list without re-running it.\n *\n * Throws outside a {@link ConfirmProvider}, rather than falling back to\n * `window.confirm`: a silent fallback is exactly the native dialog the three apps are\n * migrating away from, and it would look like the migration had worked.\n */\nexport function useConfirm(): ConfirmFn {\n const confirm = useContext(ConfirmContext);\n if (!confirm) {\n throw new Error(\"useConfirm() must be called below a <ConfirmProvider>.\");\n }\n return confirm;\n}\n\ninterface Request extends ConfirmOptions {\n id: number;\n resolve: (answer: boolean) => void;\n}\n\nexport interface ConfirmProviderProps {\n children: ReactNode;\n}\n\n/**\n * The host for {@link useConfirm}: ONE confirm dialog for the whole app, rendered on\n * {@link DialogFrame}, answering a promise.\n *\n * Asked for by all three apps at once — keksdose has 20+ `window.confirm` calls,\n * kastlan 7, lenkbank 1. The native dialog cannot be translated past its OK/Cancel\n * (those are the BROWSER's language, not the app's), cannot say which button is the\n * destructive one, and on an installed PWA it prints the origin over the question.\n * A promise is what lets each of those call sites change by one `await`:\n *\n * if (!window.confirm(t(\"budget.delete_confirm\"))) return;\n * // becomes\n * if (!(await confirm({ title: t(\"budget.delete_confirm\"), tone: \"danger\" }))) return;\n *\n * ## Its own provider, not part of `UiKitProvider`\n *\n * `UiKitProvider` renders nothing: it is strings and a locale, it is optional, and it\n * NESTS (an inner one re-labels one table). A dialog host inside it would mount a host\n * per nested provider, and a component under the inner one would open a second dialog\n * layer of its own. So the host is its own component, mounted once — inside the\n * `UiKitProvider` whose strings it should speak, which is also the order the labels\n * need.\n *\n * ## Focus\n *\n * On CANCEL for `tone: \"danger\"`, on CONFIRM otherwise. A confirm is often answered by\n * a reflexive Enter — the key that pressed the Delete button in the first place may\n * still be down — and for an action that cannot be undone that reflex must land on the\n * harmless answer; `DangerConfirm` makes the same call (\"never to the destructive\n * button itself\"). For everything else the confirm is the expected answer and Enter\n * accepting it is what `window.confirm` taught users, which the migrating call sites'\n * users already rely on. Focus returns to whatever opened it, through `Modal`'s trap.\n *\n * ## Escape, the backdrop, Back and Cancel all answer `false`\n *\n * Only the confirm button answers `true`. Anything that merely dismisses is a no.\n *\n * ## A second `confirm` while one is open QUEUES\n *\n * It is shown when the first is answered, and each call gets its own answer. The\n * alternative — answering the open one `false` to make room — would answer a question\n * the user never answered: a background prompt (\"your session is about to expire\")\n * would cancel the delete they were reading. `false` has to mean \"the user said no\".\n * A double-click cannot queue two of the same: the first click opens the modal\n * synchronously (a discrete event's update is flushed before the next one), so the\n * second lands on the open dialog rather than on the button — at worst on its\n * backdrop, which answers `false`, the safe way to be wrong.\n *\n * Unmounting the provider answers everything still pending `false`, so no caller's\n * `await` hangs forever.\n *\n * ## keksdose's synchronous call (transaction-swipe-plan.ts)\n *\n * `buildTxSwipeActions` is synchronous, but its `window.confirm` sits inside the\n * action THUNK (`confirmed(key, run)` returns `() => { if (confirm(…)) run() }`), and a\n * thunk may be async without its caller knowing. So the plan builder stays\n * synchronous; it takes the confirm as an argument and the thunk awaits it:\n *\n * // mobile-transaction-list.tsx\n * const confirm = useConfirm();\n * buildTxSwipeActions({ tx, …, confirm });\n *\n * // transaction-swipe-plan.ts\n * const confirmed = (messageKey: string, run: () => void) => () => {\n * void confirm({ title: t(messageKey), tone: \"danger\" }).then((ok) => ok && run());\n * };\n *\n * Its test then passes `confirm: vi.fn(async () => false)` instead of stubbing the\n * global, and asserts after an `await` (the mutation now runs a microtask later). The\n * row snaps back when the swipe commits, before the answer — which is right: the\n * dialog, not the half-swiped row, is where the question now is.\n */\nexport function ConfirmProvider({ children }: ConfirmProviderProps) {\n const [queue, setQueue] = useState<Request[]>([]);\n const nextId = useRef(0);\n // Every unanswered request, including the ones not yet in state — the unmount\n // cleanup below must reach a request made in the same tick it unmounts.\n const pending = useRef(new Set<Request>());\n\n const confirm = useCallback<ConfirmFn>(\n (options) =>\n new Promise<boolean>((resolve) => {\n const request: Request = { ...options, id: ++nextId.current, resolve };\n pending.current.add(request);\n setQueue((q) => [...q, request]);\n }),\n [],\n );\n\n useEffect(() => {\n const open = pending.current;\n return () => {\n for (const r of open) r.resolve(false);\n open.clear();\n };\n }, []);\n\n const answer = useCallback((request: Request, value: boolean) => {\n if (!pending.current.delete(request)) return;\n request.resolve(value);\n setQueue((q) => q.filter((r) => r !== request));\n }, []);\n\n const current = queue[0];\n return (\n <ConfirmContext.Provider value={confirm}>\n {children}\n {current && (\n // Keyed per request, so a queued one gets a fresh Modal: the first one's trap\n // hands focus back to the page, and the next one's captures that as its own\n // restore target instead of a button that has just been unmounted.\n <ConfirmDialogView key={current.id} request={current} onAnswer={answer} />\n )}\n </ConfirmContext.Provider>\n );\n}\n\nfunction ConfirmDialogView({\n request,\n onAnswer,\n}: {\n request: Request;\n onAnswer: (request: Request, value: boolean) => void;\n}) {\n const labels = useKitLabels(\"confirmDialog\", DEFAULT_CONFIRM_DIALOG_LABELS);\n const tone = request.tone ?? \"neutral\";\n const confirmRef = useRef<HTMLButtonElement>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n // Set by the confirm button, read once the panel has finished lowering. Everything\n // else that ends in `onClose` — Escape, the backdrop, Back, Cancel — leaves it false.\n const accepted = useRef(false);\n\n // In THIS component's effect, which runs after `Modal`'s own (a parent's effects run\n // after its children's). `Modal` engages its focus trap on the panel first — and\n // records the page's focused element as the restore target — and only then does\n // focus move to the button. Focusing it from inside the panel would run first and\n // make the button the restore target.\n useEffect(() => {\n (tone === \"danger\" ? cancelRef : confirmRef).current?.focus();\n }, [tone]);\n\n return (\n <DialogFrame\n title={request.title}\n description={request.body}\n onClose={() => onAnswer(request, accepted.current)}\n // A confirmation interrupts to ask a question that must be answered.\n role=\"alertdialog\"\n data-confirm-tone={tone}\n actions={(close) => (\n <>\n <Button ref={cancelRef} type=\"button\" variant=\"ghost\" onClick={close}>\n {request.cancelLabel ?? labels.cancel}\n </Button>\n <Button\n ref={confirmRef}\n type=\"button\"\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n onClick={() => {\n accepted.current = true;\n close();\n }}\n >\n {request.confirmLabel ?? labels.confirm}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":";AAiLI,SA6CI,UAvCA,KANJ;AAjLJ,SAAS,eAAe,aAAa,YAAY,WAAW,QAAQ,gBAAgB;AAGpF,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AAYhB,MAAM,gCAAqD;AAAA,EAChE,SAAS;AAAA,EACT,QAAQ;AACV;AAuBA,MAAM,iBAAiB,cAAgC,IAAI;AAYpD,SAAS,aAAwB;AACtC,QAAM,UAAU,WAAW,cAAc;AACzC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,wDAAwD;AAAA,EAC1E;AACA,SAAO;AACT;AAmFO,SAAS,gBAAgB,EAAE,SAAS,GAAyB;AAClE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAoB,CAAC,CAAC;AAChD,QAAM,SAAS,OAAO,CAAC;AAGvB,QAAM,UAAU,OAAO,oBAAI,IAAa,CAAC;AAEzC,QAAM,UAAU;AAAA,IACd,CAAC,YACC,IAAI,QAAiB,CAAC,YAAY;AAChC,YAAM,UAAmB,EAAE,GAAG,SAAS,IAAI,EAAE,OAAO,SAAS,QAAQ;AACrE,cAAQ,QAAQ,IAAI,OAAO;AAC3B,eAAS,CAAC,MAAM,CAAC,GAAG,GAAG,OAAO,CAAC;AAAA,IACjC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAEA,YAAU,MAAM;AACd,UAAM,OAAO,QAAQ;AACrB,WAAO,MAAM;AACX,iBAAW,KAAK,KAAM,GAAE,QAAQ,KAAK;AACrC,WAAK,MAAM;AAAA,IACb;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,SAAS,YAAY,CAAC,SAAkB,UAAmB;AAC/D,QAAI,CAAC,QAAQ,QAAQ,OAAO,OAAO,EAAG;AACtC,YAAQ,QAAQ,KAAK;AACrB,aAAS,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,MAAM,OAAO,CAAC;AAAA,EAChD,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM,CAAC;AACvB,SACE,qBAAC,eAAe,UAAf,EAAwB,OAAO,SAC7B;AAAA;AAAA,IACA;AAAA;AAAA;AAAA,IAIC,oBAAC,qBAAmC,SAAS,SAAS,UAAU,UAAxC,QAAQ,EAAwC;AAAA,KAE5E;AAEJ;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AACF,GAGG;AACD,QAAM,SAAS,aAAa,iBAAiB,6BAA6B;AAC1E,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,YAAY,OAA0B,IAAI;AAGhD,QAAM,WAAW,OAAO,KAAK;AAO7B,YAAU,MAAM;AACd,KAAC,SAAS,WAAW,YAAY,YAAY,SAAS,MAAM;AAAA,EAC9D,GAAG,CAAC,IAAI,CAAC;AAET,SACE;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,QAAQ;AAAA,MACf,aAAa,QAAQ;AAAA,MACrB,SAAS,MAAM,SAAS,SAAS,SAAS,OAAO;AAAA,MAEjD,MAAK;AAAA,MACL,qBAAmB;AAAA,MACnB,SAAS,CAAC,UACR,iCACE;AAAA,4BAAC,UAAO,KAAK,WAAW,MAAK,UAAS,SAAQ,SAAQ,SAAS,OAC5D,kBAAQ,eAAe,OAAO,QACjC;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAS,SAAS,WAAW,WAAW;AAAA,YACxC,SAAS,MAAM;AACb,uBAAS,UAAU;AACnB,oBAAM;AAAA,YACR;AAAA,YAEC,kBAAQ,gBAAgB,OAAO;AAAA;AAAA,QAClC;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|