@eifi1/ui-kit 0.16.1 → 0.18.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 +34 -9
- package/dist/chart.d.ts +10 -1
- package/dist/components/account-chips.d.ts +153 -0
- package/dist/components/account-chips.js +135 -0
- package/dist/components/account-chips.js.map +1 -0
- package/dist/components/account-settings-labels.d.ts +10 -2
- package/dist/components/account-settings-labels.js +3 -0
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/amount-input.d.ts +18 -1
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -1
- package/dist/components/calculator.d.ts +10 -1
- package/dist/components/chip.d.ts +25 -1
- package/dist/components/chip.js +143 -6
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +42 -1
- package/dist/components/confirm-dialog.js +36 -5
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +10 -1
- package/dist/components/danger-confirm.d.ts +55 -4
- package/dist/components/danger-confirm.js +67 -30
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +93 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -1
- package/dist/components/file-button.d.ts +10 -1
- package/dist/components/file-dropzone.d.ts +10 -1
- package/dist/components/form-actions.d.ts +10 -1
- package/dist/components/form-actions.js +35 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +30 -6
- package/dist/components/line-items.js +77 -33
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +13 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +10 -1
- package/dist/components/number-field.d.ts +11 -2
- package/dist/components/number-field.js +23 -22
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +10 -1
- package/dist/components/numpad-sheet.d.ts +10 -1
- package/dist/components/passkeys-setting.d.ts +27 -1
- package/dist/components/passkeys-setting.js +31 -5
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +25 -1
- package/dist/components/progress-bar.js +21 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.d.ts +90 -0
- package/dist/components/reauth-dialog.js +158 -0
- package/dist/components/reauth-dialog.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -1
- package/dist/components/server-wake.d.ts +58 -0
- package/dist/components/server-wake.js +58 -0
- package/dist/components/server-wake.js.map +1 -0
- package/dist/components/settings-fields.d.ts +10 -1
- package/dist/components/share-card.d.ts +61 -0
- package/dist/components/share-card.js +350 -0
- package/dist/components/share-card.js.map +1 -0
- package/dist/components/signed-amount.d.ts +12 -3
- package/dist/components/signed-amount.js +6 -4
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/status-dot.d.ts +2 -0
- package/dist/components/text-link.d.ts +10 -1
- package/dist/components/time-input.d.ts +10 -1
- package/dist/components/toast.d.ts +10 -1
- package/dist/components/toast.js +42 -2
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toggle-group.js +5 -2
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +10 -1
- package/dist/components/ui.js +67 -30
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/components/user-avatar.d.ts +2 -0
- package/dist/components/write-lock.d.ts +68 -0
- package/dist/components/write-lock.js +33 -0
- package/dist/components/write-lock.js.map +1 -0
- package/dist/{data-table-CfVXtCeG.d.ts → data-table-3mi2R3so.d.ts} +59 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +10 -1
- package/dist/hooks/use-overlay-history.js +16 -0
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +10 -1
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +78 -0
- package/dist/i18n/{locales/de.js → german.js} +59 -9
- package/dist/i18n/german.js.map +1 -0
- package/dist/i18n/kit-labels.d.ts +10 -1
- package/dist/i18n/kit-labels.js +5 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +23 -8
- package/dist/i18n/locales/de-CH.js +7 -3
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/{de.d.ts → en.d.ts} +24 -10
- package/dist/i18n/locales/en.js +95 -0
- package/dist/i18n/locales/en.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +10 -1
- package/dist/i18n/locales/es.js +51 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -1
- package/dist/i18n/locales/fr.js +51 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -1
- package/dist/i18n/locales/hu.js +54 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -1
- package/dist/i18n/locales/it.js +51 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -1
- package/dist/i18n/locales/zh.js +50 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +3 -3
- package/dist/i18n/swiss.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.js +33 -1
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-DE--LmCk.d.ts → kit-labels-CPbsjlXr.d.ts} +282 -10
- package/dist/lib/server-wake.d.ts +130 -0
- package/dist/lib/server-wake.js +121 -0
- package/dist/lib/server-wake.js.map +1 -0
- package/dist/rhf/fields.d.ts +10 -1
- package/dist/rhf/form.d.ts +10 -1
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +10 -1
- package/dist/search/global-search.d.ts +24 -4
- package/dist/search/global-search.js +31 -3
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/shell/app-shell.d.ts +10 -1
- package/dist/shell/top-bar-brand.d.ts +10 -1
- package/dist/shell/topbar-action-menu.d.ts +2 -0
- package/dist/shell.d.ts +10 -1
- package/dist/wizard/stepper-nav.d.ts +10 -1
- package/dist/wizard.d.ts +10 -1
- package/package.json +1 -1
- package/src/components/account-chips.tsx +302 -0
- package/src/components/account-settings-labels.ts +16 -2
- package/src/components/amount-input.tsx +8 -0
- package/src/components/chip.tsx +239 -6
- package/src/components/confirm-dialog.tsx +82 -7
- package/src/components/danger-confirm.tsx +118 -33
- package/src/components/data-table.tsx +185 -3
- package/src/components/form-actions.tsx +93 -4
- package/src/components/line-items.tsx +116 -40
- package/src/components/list.tsx +13 -1
- package/src/components/number-field.tsx +32 -26
- package/src/components/passkeys-setting.tsx +68 -7
- package/src/components/progress-bar.tsx +40 -4
- package/src/components/reauth-dialog.tsx +273 -0
- package/src/components/server-wake.tsx +124 -0
- package/src/components/share-card.tsx +615 -0
- package/src/components/signed-amount.tsx +16 -5
- package/src/components/toast.tsx +72 -2
- package/src/components/toggle-group.tsx +5 -2
- package/src/components/ui.tsx +145 -34
- package/src/components/write-lock.tsx +96 -0
- package/src/hooks/use-overlay-history.ts +51 -0
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/{locales/de.ts → german.ts} +68 -13
- package/src/i18n/kit-labels.tsx +22 -2
- package/src/i18n/locales/de-CH.ts +17 -8
- package/src/i18n/locales/en.ts +116 -0
- package/src/i18n/locales/es.ts +51 -0
- package/src/i18n/locales/fr.ts +52 -0
- package/src/i18n/locales/hu.ts +55 -0
- package/src/i18n/locales/it.ts +51 -0
- package/src/i18n/locales/zh.ts +50 -0
- package/src/i18n/swiss.ts +3 -3
- package/src/index.ts +23 -1
- package/src/lib/server-wake.ts +270 -0
- package/src/rhf/line-items.tsx +2 -1
- package/src/search/global-search.tsx +59 -6
- package/dist/i18n/locales/de-CH-informal.d.ts +0 -61
- package/dist/i18n/locales/de-CH-informal.js +0 -8
- package/dist/i18n/locales/de-CH-informal.js.map +0 -1
- package/dist/i18n/locales/de-informal.d.ts +0 -82
- package/dist/i18n/locales/de-informal.js +0 -37
- package/dist/i18n/locales/de-informal.js.map +0 -1
- package/dist/i18n/locales/de.js.map +0 -1
- package/src/i18n/locales/de-CH-informal.ts +0 -10
- package/src/i18n/locales/de-informal.ts +0 -61
|
@@ -53,8 +53,10 @@ export interface LineItemCellContext<T> {
|
|
|
53
53
|
* so the list draws no header for it. */
|
|
54
54
|
export type LineItemsFieldLabels = "aria" | "floating";
|
|
55
55
|
|
|
56
|
-
/** The colour of a {@link LineItemsSummary}'s value
|
|
57
|
-
|
|
56
|
+
/** The colour of a {@link LineItemsSummary}'s value — a verdict (`success` /
|
|
57
|
+
* `danger`), quiet (`muted`), or the money pair (`income` / `expense`, the colours
|
|
58
|
+
* `SignedAmount` draws a figure in) for a remainder that reads as the row it settles. */
|
|
59
|
+
export type LineItemsSummaryTone = "neutral" | "success" | "danger" | "muted" | "income" | "expense";
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* A line under the lines that says where they stand — a split's remainder ("Left to
|
|
@@ -65,13 +67,28 @@ export interface LineItemsSummary {
|
|
|
65
67
|
label: ReactNode;
|
|
66
68
|
value?: ReactNode;
|
|
67
69
|
/** The value's colour. Default `neutral`. The label stays neutral: the colour is a
|
|
68
|
-
* second signal, never the only one — the value and label carry the meaning.
|
|
70
|
+
* second signal, never the only one — the value and label carry the meaning.
|
|
71
|
+
* With no `value` the line is a state on its own ("Waiting for the total"), and
|
|
72
|
+
* the tone colours the label instead — `muted` quiets the whole line. */
|
|
69
73
|
tone?: LineItemsSummaryTone;
|
|
74
|
+
/** Beside the value: a chip or badge that names the state ("Balanced" next to a
|
|
75
|
+
* journal entry's zero difference). */
|
|
76
|
+
status?: ReactNode;
|
|
70
77
|
/** A small button at the end of the line. Disabled along with the list's
|
|
71
|
-
* `disabled`.
|
|
72
|
-
action
|
|
78
|
+
* `disabled`. `variant`: `secondary` (default) is a boxed button; `link` a muted
|
|
79
|
+
* text link, for an action that is an aside rather than the way to settle it. */
|
|
80
|
+
action?: { label: ReactNode; onClick: () => void; disabled?: boolean; variant?: "secondary" | "link" };
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
/** Where a row's remove button sits. `row` (default): stacked, a row of the card to
|
|
84
|
+
* itself (paired, the whole width); wide, its own track. `inline`: stacked, beside
|
|
85
|
+
* the row's last field ("category + amount" over "memo + delete"); wide, unchanged. */
|
|
86
|
+
export type LineItemsRemovePlacement = "row" | "inline";
|
|
87
|
+
|
|
88
|
+
/** How the remove button sits in the height of a wide row: at its top, centred, or at
|
|
89
|
+
* its foot. */
|
|
90
|
+
export type LineItemsRemoveAlign = "start" | "center" | "end";
|
|
91
|
+
|
|
75
92
|
export interface LineItemsColumn<T> {
|
|
76
93
|
/** Stable id: the React key, and the key into `totals`. */
|
|
77
94
|
key: string;
|
|
@@ -143,6 +160,14 @@ export interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
143
160
|
* them up (a column's `narrowSpan={2}` takes a whole row), once the pane is wide
|
|
144
161
|
* enough for two fields (18rem); narrower still, one. Default `1`. */
|
|
145
162
|
narrowColumns?: 1 | 2;
|
|
163
|
+
/** Stacked, where the remove button goes: a row of its own (`row`, default) or
|
|
164
|
+
* beside the last field (`inline`). See {@link LineItemsRemovePlacement}. */
|
|
165
|
+
removePlacement?: LineItemsRemovePlacement;
|
|
166
|
+
/** On the wide grid, the remove button's place in the row's height. Default `start`
|
|
167
|
+
* — or `center` when a column floats its label (`fieldLabels="floating"`): a
|
|
168
|
+
* floating field is taller than the button, and centred is level with its control.
|
|
169
|
+
* A field that shows a message under itself grows the row; pin `start` then. */
|
|
170
|
+
removeAlign?: LineItemsRemoveAlign;
|
|
146
171
|
/** Disables add and remove (the fields are the caller's to disable). */
|
|
147
172
|
disabled?: boolean;
|
|
148
173
|
labels?: Partial<LineItemsLabels>;
|
|
@@ -154,6 +179,14 @@ const SUMMARY_TONE: Record<LineItemsSummaryTone, string> = {
|
|
|
154
179
|
success: "text-[var(--success)]",
|
|
155
180
|
danger: "text-[var(--danger)]",
|
|
156
181
|
muted: "text-[var(--text-muted)]",
|
|
182
|
+
// SignedAmount's money palette (toneTextClass "income" / "expense").
|
|
183
|
+
income: "text-[var(--money-income)]",
|
|
184
|
+
expense: "text-[var(--money-expense)]",
|
|
185
|
+
};
|
|
186
|
+
const REMOVE_ALIGN: Record<LineItemsRemoveAlign, string> = {
|
|
187
|
+
start: "@lg:self-start",
|
|
188
|
+
center: "@lg:self-center",
|
|
189
|
+
end: "@lg:self-end",
|
|
157
190
|
};
|
|
158
191
|
const FOCUSABLE =
|
|
159
192
|
"input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])";
|
|
@@ -217,6 +250,8 @@ export function LineItems<T>({
|
|
|
217
250
|
summary,
|
|
218
251
|
fieldLabels = "aria",
|
|
219
252
|
narrowColumns = 1,
|
|
253
|
+
removePlacement = "row",
|
|
254
|
+
removeAlign: removeAlignProp,
|
|
220
255
|
disabled = false,
|
|
221
256
|
labels: labelsProp,
|
|
222
257
|
className,
|
|
@@ -292,6 +327,9 @@ export function LineItems<T>({
|
|
|
292
327
|
// its own label, a row of headers would say each name twice.
|
|
293
328
|
const showHeader = columns.some((c) => !floats(c));
|
|
294
329
|
const pairs = narrowColumns === 2;
|
|
330
|
+
const removeAlign = removeAlignProp ?? (columns.some(floats) ? "center" : "start");
|
|
331
|
+
const inline = removePlacement === "inline" && Boolean(onRemove) && columns.length > 0;
|
|
332
|
+
const lastColumn = columns[columns.length - 1];
|
|
295
333
|
|
|
296
334
|
const headerCell = (column: LineItemsColumn<T>) => (
|
|
297
335
|
<span
|
|
@@ -305,7 +343,43 @@ export function LineItems<T>({
|
|
|
305
343
|
</span>
|
|
306
344
|
);
|
|
307
345
|
|
|
346
|
+
const removeCell = (index: number, row: number, isConfirming: boolean) => (
|
|
347
|
+
<div
|
|
348
|
+
data-line-items-remove-cell=""
|
|
349
|
+
className={cn(
|
|
350
|
+
"flex justify-end @lg:justify-center",
|
|
351
|
+
REMOVE_ALIGN[removeAlign],
|
|
352
|
+
// Paired, the remove button keeps a row of its own at the card's end rather
|
|
353
|
+
// than squeezing in beside the last field — unless asked to go `inline`.
|
|
354
|
+
inline ? "shrink-0" : pairs && "@2xs:col-span-2 @lg:col-span-1",
|
|
355
|
+
)}
|
|
356
|
+
>
|
|
357
|
+
<IconButton
|
|
358
|
+
type="button"
|
|
359
|
+
data-line-items-remove=""
|
|
360
|
+
variant={isConfirming ? "danger" : "ghost"}
|
|
361
|
+
tone={isConfirming ? undefined : "danger"}
|
|
362
|
+
label={isConfirming ? labels.confirmRemove(row) : labels.remove(row)}
|
|
363
|
+
disabled={!canRemove}
|
|
364
|
+
onClick={() => void removeAt(index)}
|
|
365
|
+
onBlur={() => {
|
|
366
|
+
if (isConfirming) setConfirming(null);
|
|
367
|
+
}}
|
|
368
|
+
onKeyDown={(e) => {
|
|
369
|
+
if (e.key === "Escape" && isConfirming) {
|
|
370
|
+
e.stopPropagation();
|
|
371
|
+
setConfirming(null);
|
|
372
|
+
}
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
<Trash2 />
|
|
376
|
+
</IconButton>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
|
|
308
380
|
const showTotals = totals !== undefined;
|
|
381
|
+
const summaryTone = summary?.tone ?? "neutral";
|
|
382
|
+
const summaryHasValue = hasContent(summary?.value);
|
|
309
383
|
const labelColumn = showTotals ? columns.find((c) => !hasContent(totals[c.key]))?.key : undefined;
|
|
310
384
|
|
|
311
385
|
return (
|
|
@@ -355,13 +429,15 @@ export function LineItems<T>({
|
|
|
355
429
|
>
|
|
356
430
|
{columns.map((column) => {
|
|
357
431
|
const floating = floats(column);
|
|
358
|
-
|
|
432
|
+
const isLast = inline && column === lastColumn;
|
|
433
|
+
const span = pairs && column.narrowSpan === 2 && "@2xs:col-span-2 @lg:col-span-1";
|
|
434
|
+
const cell = (
|
|
359
435
|
<div
|
|
360
436
|
key={column.key}
|
|
361
437
|
className={cn(
|
|
362
438
|
"min-w-0 space-y-1 @lg:space-y-0",
|
|
363
439
|
// The wide grid places every cell in its own track again.
|
|
364
|
-
|
|
440
|
+
isLast ? "flex-1" : span,
|
|
365
441
|
)}
|
|
366
442
|
>
|
|
367
443
|
{!floating && (
|
|
@@ -377,33 +453,23 @@ export function LineItems<T>({
|
|
|
377
453
|
})}
|
|
378
454
|
</div>
|
|
379
455
|
);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
label={isConfirming ? labels.confirmRemove(row) : labels.remove(row)}
|
|
391
|
-
disabled={!canRemove}
|
|
392
|
-
onClick={() => void removeAt(index)}
|
|
393
|
-
onBlur={() => {
|
|
394
|
-
if (isConfirming) setConfirming(null);
|
|
395
|
-
}}
|
|
396
|
-
onKeyDown={(e) => {
|
|
397
|
-
if (e.key === "Escape" && isConfirming) {
|
|
398
|
-
e.stopPropagation();
|
|
399
|
-
setConfirming(null);
|
|
400
|
-
}
|
|
401
|
-
}}
|
|
456
|
+
if (!isLast) return cell;
|
|
457
|
+
// Inline, the last field and the remove button share one cell of the
|
|
458
|
+
// card; on the wide grid the wrapper dissolves (`contents`) and each is a
|
|
459
|
+
// track of its own again. Level with the field's control: its foot under
|
|
460
|
+
// a header copy, its middle when it floats its own label.
|
|
461
|
+
return (
|
|
462
|
+
<div
|
|
463
|
+
key={column.key}
|
|
464
|
+
data-line-items-inline=""
|
|
465
|
+
className={cn("flex min-w-0 gap-2 @lg:contents", floating ? "items-center" : "items-end", span)}
|
|
402
466
|
>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
467
|
+
{cell}
|
|
468
|
+
{removeCell(index, row, isConfirming)}
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
{onRemove && !inline && removeCell(index, row, isConfirming)}
|
|
407
473
|
</div>
|
|
408
474
|
);
|
|
409
475
|
})}
|
|
@@ -454,19 +520,29 @@ export function LineItems<T>({
|
|
|
454
520
|
data-line-items-summary=""
|
|
455
521
|
className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--text-primary)]"
|
|
456
522
|
>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
523
|
+
{/* With no value the line is a state of its own, and the label takes the
|
|
524
|
+
tone — the only place left to carry it. */}
|
|
525
|
+
<span
|
|
526
|
+
data-tone={summaryHasValue ? undefined : summaryTone}
|
|
527
|
+
className={cn("min-w-0", !summaryHasValue && SUMMARY_TONE[summaryTone])}
|
|
528
|
+
>
|
|
529
|
+
{summary.label}
|
|
530
|
+
</span>
|
|
531
|
+
{summaryHasValue && (
|
|
532
|
+
<span data-tone={summaryTone} className={cn("font-semibold tabular-nums", SUMMARY_TONE[summaryTone])}>
|
|
463
533
|
{summary.value}
|
|
464
534
|
</span>
|
|
465
535
|
)}
|
|
536
|
+
{hasContent(summary.status) && (
|
|
537
|
+
<span data-line-items-summary-status="" className="inline-flex">
|
|
538
|
+
{summary.status}
|
|
539
|
+
</span>
|
|
540
|
+
)}
|
|
466
541
|
{summary.action && (
|
|
467
542
|
<Button
|
|
468
543
|
type="button"
|
|
469
|
-
variant="secondary"
|
|
544
|
+
variant={summary.action.variant === "link" ? "link" : "secondary"}
|
|
545
|
+
tone={summary.action.variant === "link" ? "muted" : undefined}
|
|
470
546
|
size="sm"
|
|
471
547
|
className="ms-auto"
|
|
472
548
|
disabled={disabled || summary.action.disabled}
|
package/src/components/list.tsx
CHANGED
|
@@ -160,7 +160,12 @@ interface ListItemBaseProps {
|
|
|
160
160
|
* never operated — no buttons, links or inputs (invalid HTML in a button, and folded
|
|
161
161
|
* into its name). Its text joins the row's accessible name, so give a bar that only
|
|
162
162
|
* repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,
|
|
163
|
-
* full width of the
|
|
163
|
+
* full width of the TEXT COLUMN only — the column the title and subtitle are in, not
|
|
164
|
+
* the whole row: it starts after the {@link icon} / {@link leading} and stops where
|
|
165
|
+
* {@link trailing} begins, so a meter under a row with a trailing figure is as wide as
|
|
166
|
+
* the title, not run on under the figure (keksdose's budget rows). For a bar across
|
|
167
|
+
* the full row, put it in {@link children} (outside the target) or leave `trailing`
|
|
168
|
+
* off and show the figure in `meta`. Strictly a button holds phrasing content only; the
|
|
164
169
|
* kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine
|
|
165
170
|
* there but an HTML validator flags.
|
|
166
171
|
*/
|
|
@@ -280,6 +285,13 @@ interface ListItemBaseProps {
|
|
|
280
285
|
* the list's child and keeps both. The `<li>` becomes a column and the row takes the
|
|
281
286
|
* full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches
|
|
282
287
|
* the row across the list.
|
|
288
|
+
*
|
|
289
|
+
* `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto
|
|
290
|
+
* its child with `cloneElement` — a save guard that injects `disabled` while a form is
|
|
291
|
+
* dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the
|
|
292
|
+
* button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,
|
|
293
|
+
* from the same state the guard reads, and let the wrapper only explain it:
|
|
294
|
+
* `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label="Save first">{row}</Tooltip>} />`.
|
|
283
295
|
*/
|
|
284
296
|
renderRow?: (row: ReactElement) => ReactNode;
|
|
285
297
|
/** Reaches the main target — the element with the row's name and action. */
|
|
@@ -154,26 +154,27 @@ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
|
154
154
|
* digit grouping — "1.000" would read as a thousand in German and as one in English,
|
|
155
155
|
* and a field that parses its own display back must not have that ambiguity.
|
|
156
156
|
*/
|
|
157
|
-
export function NumberField({
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
157
|
+
export function NumberField(props: NumberFieldProps) {
|
|
158
|
+
const {
|
|
159
|
+
value,
|
|
160
|
+
onCommit,
|
|
161
|
+
onValueChange,
|
|
162
|
+
digits,
|
|
163
|
+
min,
|
|
164
|
+
max,
|
|
165
|
+
nullable = false,
|
|
166
|
+
unit,
|
|
167
|
+
unitPlacement = "suffix",
|
|
168
|
+
locale: localeProp,
|
|
169
|
+
label,
|
|
170
|
+
invalid,
|
|
171
|
+
error,
|
|
172
|
+
id,
|
|
173
|
+
"aria-describedby": ariaDescribedBy,
|
|
174
|
+
"aria-invalid": ariaInvalid,
|
|
175
|
+
"aria-required": ariaRequired,
|
|
176
|
+
...rest
|
|
177
|
+
} = props;
|
|
177
178
|
const locale = useKitLocale(localeProp);
|
|
178
179
|
const mark = decimalMark(locale);
|
|
179
180
|
const localize = (text: string) => (mark === "," ? text.replace(/\./g, ",") : text);
|
|
@@ -292,15 +293,20 @@ export function NumberField({
|
|
|
292
293
|
invalid={invalid || hasError || ariaInvalid === true || ariaInvalid === "true"}
|
|
293
294
|
/>
|
|
294
295
|
);
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
296
|
+
// Input's FieldGroup, restated: no wrapper for a field that never takes `error`, so
|
|
297
|
+
// the prop changed nothing about the DOM a flex row lays out — and a STEADY one for a
|
|
298
|
+
// field that does, message or not. Wrapping only while a message showed swapped the
|
|
299
|
+
// element above the <input>, which remounted it: focus, caret and the half-typed
|
|
300
|
+
// draft's keystrokes were lost the moment the message came or went.
|
|
301
|
+
if (!hasError && !("error" in props)) return field;
|
|
298
302
|
return (
|
|
299
303
|
<div>
|
|
300
304
|
{field}
|
|
301
|
-
|
|
302
|
-
{
|
|
303
|
-
|
|
305
|
+
{hasError && (
|
|
306
|
+
<p id={errorId} className={FIELD_ERROR_CLASS}>
|
|
307
|
+
{error}
|
|
308
|
+
</p>
|
|
309
|
+
)}
|
|
304
310
|
</div>
|
|
305
311
|
);
|
|
306
312
|
}
|
|
@@ -42,6 +42,32 @@ export interface PasskeysSettingProps<Id extends PasskeyId = PasskeyId> extends
|
|
|
42
42
|
* the user cancelling the OS prompt) keeps what was typed.
|
|
43
43
|
*/
|
|
44
44
|
onAdd: (name: string) => void | Promise<unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* A step the app runs BEFORE `onAdd` (0.18.0) — re-asking for the current password,
|
|
47
|
+
* a policy notice, anything that may stop the add. Called with the same trimmed name;
|
|
48
|
+
* resolve `true` to go on to `onAdd`, `false` to stop quietly (nothing is said, the
|
|
49
|
+
* typed name stays). A rejection, or a throw, stops the same way: the app reports
|
|
50
|
+
* its own errors. While it runs the add button is disabled and busy, so a second
|
|
51
|
+
* click cannot start a second check.
|
|
52
|
+
*
|
|
53
|
+
* Kurvenschmiede asked for the password in its own dialog by handing `onAdd` a
|
|
54
|
+
* promise it kept open across that dialog (passkeys-card.tsx) — which still works.
|
|
55
|
+
* This is the documented place for it: open the dialog here, resolve with whether it
|
|
56
|
+
* was confirmed, and keep what it produced (the server's registration options) for
|
|
57
|
+
* `onAdd` in a ref. The ceremony then starts from the dialog's click rather than the
|
|
58
|
+
* card's, which is the user activation `navigator.credentials.create` wants.
|
|
59
|
+
*
|
|
60
|
+
* Left out, `onAdd` is called synchronously in the click, exactly as before.
|
|
61
|
+
*/
|
|
62
|
+
beforeAdd?: (name: string) => boolean | Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* What a passkey does to the password, which picks the line under the title (0.18.0).
|
|
65
|
+
* `"instead"` (default): it replaces it — `labels.description`, kastlan's promise.
|
|
66
|
+
* `"alongside"`: the password stays and the passkey is a second way in —
|
|
67
|
+
* `labels.descriptionAlongside`. Kurvenschmiede overrode `description` for exactly
|
|
68
|
+
* this; an app that sets `mode` needs no override.
|
|
69
|
+
*/
|
|
70
|
+
mode?: "instead" | "alongside";
|
|
45
71
|
/** The ceremony is running. The add button says so and does not start a second one. */
|
|
46
72
|
adding?: boolean;
|
|
47
73
|
/** Offer "Rename" on each row. Return a promise to leave edit mode only on success. */
|
|
@@ -92,6 +118,8 @@ export function PasskeysSetting<Id extends PasskeyId = PasskeyId>({
|
|
|
92
118
|
passkeys,
|
|
93
119
|
loading,
|
|
94
120
|
onAdd,
|
|
121
|
+
beforeAdd,
|
|
122
|
+
mode = "instead",
|
|
95
123
|
adding = false,
|
|
96
124
|
onRename,
|
|
97
125
|
onDelete,
|
|
@@ -108,6 +136,10 @@ export function PasskeysSetting<Id extends PasskeyId = PasskeyId>({
|
|
|
108
136
|
const labels = useAccountSettingsLabels("passkeys", labelsProp);
|
|
109
137
|
const locale = useKitLocale(localeProp);
|
|
110
138
|
const [name, setName] = useState("");
|
|
139
|
+
// `beforeAdd` is running. State for the button, and a ref for the guard: two clicks
|
|
140
|
+
// in one frame both read the state from before either set it.
|
|
141
|
+
const [checking, setChecking] = useState(false);
|
|
142
|
+
const checkingRef = useRef(false);
|
|
111
143
|
|
|
112
144
|
const date = (value: PasskeyItem["createdAt"]): string | null => {
|
|
113
145
|
if (value === null || value === undefined || value === "") return null;
|
|
@@ -117,13 +149,40 @@ export function PasskeysSetting<Id extends PasskeyId = PasskeyId>({
|
|
|
117
149
|
};
|
|
118
150
|
|
|
119
151
|
const add = () => {
|
|
120
|
-
if (adding) return;
|
|
152
|
+
if (adding || checkingRef.current) return;
|
|
121
153
|
const typed = name;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
154
|
+
const value = typed.trim();
|
|
155
|
+
const send = () =>
|
|
156
|
+
Promise.resolve(onAdd(value))
|
|
157
|
+
// Clear only what was sent: the field stays editable while the OS prompt is up.
|
|
158
|
+
.then(() => setName((current) => (current === typed ? "" : current)));
|
|
159
|
+
if (!beforeAdd) {
|
|
160
|
+
send().catch(() => {});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const done = () => {
|
|
164
|
+
checkingRef.current = false;
|
|
165
|
+
setChecking(false);
|
|
166
|
+
};
|
|
167
|
+
checkingRef.current = true;
|
|
168
|
+
setChecking(true);
|
|
169
|
+
let gate: Promise<boolean>;
|
|
170
|
+
// Called in the click, not a microtask later, so a step that itself needs the
|
|
171
|
+
// user's activation (a popup, the clipboard) still has it; a throw is a "no".
|
|
172
|
+
try {
|
|
173
|
+
gate = Promise.resolve(beforeAdd(value));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
gate = Promise.reject(error);
|
|
176
|
+
}
|
|
177
|
+
gate
|
|
178
|
+
.then((go) => {
|
|
179
|
+
done();
|
|
180
|
+
// Strictly `true`: an app whose step resolves `undefined` meant nothing.
|
|
181
|
+
if (go === true) return send();
|
|
182
|
+
})
|
|
183
|
+
.catch(done);
|
|
126
184
|
};
|
|
185
|
+
const busy = adding || checking;
|
|
127
186
|
|
|
128
187
|
const isLoading = loading || passkeys === undefined;
|
|
129
188
|
const items = passkeys ?? [];
|
|
@@ -171,7 +230,7 @@ export function PasskeysSetting<Id extends PasskeyId = PasskeyId>({
|
|
|
171
230
|
}}
|
|
172
231
|
/>
|
|
173
232
|
)}
|
|
174
|
-
<Button onClick={add} disabled={
|
|
233
|
+
<Button onClick={add} disabled={busy} aria-busy={busy || undefined}>
|
|
175
234
|
<KeyRound className="size-4" aria-hidden />
|
|
176
235
|
{adding ? labels.adding : labels.add}
|
|
177
236
|
</Button>
|
|
@@ -186,7 +245,9 @@ export function PasskeysSetting<Id extends PasskeyId = PasskeyId>({
|
|
|
186
245
|
<div>
|
|
187
246
|
<div className="text-sm font-medium">{labels.title}</div>
|
|
188
247
|
<div className="text-xs text-[var(--text-muted)]">
|
|
189
|
-
{
|
|
248
|
+
{mode === "alongside"
|
|
249
|
+
? labels.descriptionAlongside
|
|
250
|
+
: labels.description}
|
|
190
251
|
</div>
|
|
191
252
|
</div>
|
|
192
253
|
{body}
|
|
@@ -2,6 +2,8 @@ import { useId } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
5
|
+
import { toneTextClass } from "./signed-amount";
|
|
6
|
+
import type { TextTone } from "./signed-amount";
|
|
5
7
|
|
|
6
8
|
/** The words a progress bar adds of its own. */
|
|
7
9
|
export interface ProgressBarLabels {
|
|
@@ -169,6 +171,18 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
169
171
|
* `legendValue={(seg, share) => <>{share} <Amount … /></>}`.
|
|
170
172
|
*/
|
|
171
173
|
legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;
|
|
174
|
+
/**
|
|
175
|
+
* The colour of each legend row's figure, as a {@link TextTone} — `toneTextClass`'s
|
|
176
|
+
* palette, the money pair included. Default `"muted"`: the legend explains the bar and
|
|
177
|
+
* should not outshout the headline above it. One tone for every row, or a function
|
|
178
|
+
* per row. A tone and not the segment's own `tone`: keksdose's tax card fills its
|
|
179
|
+
* three parts with one bucket's certainty ramp (`className`), while the FIGURES carry
|
|
180
|
+
* the bucket's money direction — `legendTone="expense"` for the tax owed — which it
|
|
181
|
+
* could only say before through `legendValue` and an app-side `Amount`. A function
|
|
182
|
+
* returning `undefined` keeps that row muted. Applies to `legendValue`'s node too
|
|
183
|
+
* (it inherits the colour unless it sets its own).
|
|
184
|
+
*/
|
|
185
|
+
legendTone?: TextTone | ((segment: ProgressBarSegment, index: number) => TextTone | undefined);
|
|
172
186
|
/** A muted line under the bar — the plan's reset date, "3 of 5 seats in use". It
|
|
173
187
|
* describes the bar (`aria-describedby`). */
|
|
174
188
|
hint?: ReactNode;
|
|
@@ -180,6 +194,16 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
180
194
|
* `tone="danger"` if it should read as over. Ignored for `segments` and `max={null}`.
|
|
181
195
|
*/
|
|
182
196
|
overage?: boolean | ((over: number) => ReactNode);
|
|
197
|
+
/**
|
|
198
|
+
* Tag every figure the bar PRINTS `data-private`, so the host's demo-mode rule blurs
|
|
199
|
+
* it — the attribute `StatTile sensitive` and `Tooltip redact` carry: the `showValue`
|
|
200
|
+
* text, each `legend` row's value (yours from `legendValue` too), the unlimited
|
|
201
|
+
* figure and the `overage` line. The track, the swatches and the part names stay
|
|
202
|
+
* readable — a blurred bar is no picture at all. Before this an app with a demo-mode
|
|
203
|
+
* blur had to pass `legendValue` only to wrap the figure in a tagged span (keksdose).
|
|
204
|
+
* The `aria-valuetext` is not touched: a screen reader is not a screen-share.
|
|
205
|
+
*/
|
|
206
|
+
sensitive?: boolean;
|
|
183
207
|
labels?: Partial<ProgressBarLabels>;
|
|
184
208
|
}
|
|
185
209
|
|
|
@@ -214,8 +238,10 @@ export function ProgressBar({
|
|
|
214
238
|
segments,
|
|
215
239
|
legend = false,
|
|
216
240
|
legendValue,
|
|
241
|
+
legendTone = "muted",
|
|
217
242
|
hint,
|
|
218
243
|
overage,
|
|
244
|
+
sensitive = false,
|
|
219
245
|
labels,
|
|
220
246
|
className,
|
|
221
247
|
as = "div",
|
|
@@ -233,6 +259,7 @@ export function ProgressBar({
|
|
|
233
259
|
const text = useKitLabels("progressBar", DEFAULT_PROGRESS_BAR_LABELS, labels);
|
|
234
260
|
const kitLocale = useKitLocale(locale);
|
|
235
261
|
const labelId = useId();
|
|
262
|
+
const priv = sensitive ? "" : undefined;
|
|
236
263
|
const hintId = useId();
|
|
237
264
|
const hintNode =
|
|
238
265
|
hint != null ? (
|
|
@@ -258,7 +285,7 @@ export function ProgressBar({
|
|
|
258
285
|
{label}
|
|
259
286
|
</span>
|
|
260
287
|
)}
|
|
261
|
-
<span data-part="unlimited" className="ms-auto shrink-0 tabular-nums text-[var(--text-muted)]">
|
|
288
|
+
<span data-part="unlimited" data-private={priv} className="ms-auto shrink-0 tabular-nums text-[var(--text-muted)]">
|
|
262
289
|
{shown !== undefined && `${shown} · `}
|
|
263
290
|
{text.unlimited}
|
|
264
291
|
</span>
|
|
@@ -320,7 +347,7 @@ export function ProgressBar({
|
|
|
320
347
|
overage && !stacked && value !== undefined && Number.isFinite(value) && value > max ? value - max : 0;
|
|
321
348
|
const overageNode =
|
|
322
349
|
over > 0 ? (
|
|
323
|
-
<p id={`${hintId}-over`} data-part="overage" className="mt-1 text-xs text-[var(--danger)]">
|
|
350
|
+
<p id={`${hintId}-over`} data-part="overage" data-private={priv} className="mt-1 text-xs text-[var(--danger)]">
|
|
324
351
|
{typeof overage === "function"
|
|
325
352
|
? overage(over)
|
|
326
353
|
: text.overLimit(new Intl.NumberFormat(kitLocale).format(over))}
|
|
@@ -344,7 +371,7 @@ export function ProgressBar({
|
|
|
344
371
|
{showValue && valueText !== undefined && (
|
|
345
372
|
// Hidden: the same words are the bar's aria-valuetext, and reading them
|
|
346
373
|
// twice in a row is noise.
|
|
347
|
-
<span aria-hidden className="ms-auto shrink-0 tabular-nums text-[var(--text-muted)]">
|
|
374
|
+
<span aria-hidden data-part="value" data-private={priv} className="ms-auto shrink-0 tabular-nums text-[var(--text-muted)]">
|
|
348
375
|
{valueText}
|
|
349
376
|
</span>
|
|
350
377
|
)}
|
|
@@ -426,7 +453,16 @@ export function ProgressBar({
|
|
|
426
453
|
{seg.label != null && (
|
|
427
454
|
<span className="min-w-0 flex-1 truncate text-[var(--text-secondary)]">{seg.label}</span>
|
|
428
455
|
)}
|
|
429
|
-
<span
|
|
456
|
+
<span
|
|
457
|
+
data-part="legend-value"
|
|
458
|
+
data-private={priv}
|
|
459
|
+
className={cn(
|
|
460
|
+
"ms-auto shrink-0 text-xs tabular-nums",
|
|
461
|
+
toneTextClass(
|
|
462
|
+
(typeof legendTone === "function" ? legendTone(seg, i) : legendTone) ?? "muted",
|
|
463
|
+
),
|
|
464
|
+
)}
|
|
465
|
+
>
|
|
430
466
|
{legendValue ? legendValue(seg, formatPart(seg.value), i) : formatPart(seg.value)}
|
|
431
467
|
</span>
|
|
432
468
|
</li>
|