@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
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
2
|
+
import { Ref, ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How typed text is compared with the text it has to repeat — shared by
|
|
6
|
+
* `DangerConfirm`'s `phraseMatch` and `useConfirm`'s `typedMatch`.
|
|
7
|
+
*
|
|
8
|
+
* - `"trim"` ignores spaces around the typed text — a phone keyboard's autocomplete
|
|
9
|
+
* likes to add one. Case counts: "delete" is not "DELETE".
|
|
10
|
+
* - `"exact"` compares character for character, spaces included.
|
|
11
|
+
* - `"caseless"` trims AND ignores case: for an e-mail address, whose domain is
|
|
12
|
+
* case-insensitive and whose local part every real mail server treats so. Both apps'
|
|
13
|
+
* "type the account's address" rules (keksdose's `TYPE_EMAIL`, Kurvenschmiede's
|
|
14
|
+
* `TypedConfirm`) compared `trim().toLowerCase()`, and a capital the user's keyboard
|
|
15
|
+
* put at the start must not make the right row look like the wrong one.
|
|
16
|
+
*/
|
|
17
|
+
type TypedMatch = "trim" | "exact" | "caseless";
|
|
18
|
+
/** Whether `typed` repeats `target` under `mode` — see {@link TypedMatch}. Exported for
|
|
19
|
+
* a caller that draws its own field (a dialog that also picks a recipient) and wants
|
|
20
|
+
* the same rule as the kit's. */
|
|
21
|
+
declare function typedMatches(typed: string, target: string, mode?: TypedMatch): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Busy-while-a-promise-runs, for an action that MAY return one: `run(result, onResolved)`
|
|
24
|
+
* marks the caller pending until `result` settles, calls `onResolved` if it fulfilled,
|
|
25
|
+
* and only clears the flag if it rejected — the caller shows why and the user retries.
|
|
26
|
+
* Nothing runs after unmount. Shared by `DangerConfirm` and `ReauthDialog`, whose
|
|
27
|
+
* contract is the same: resolve = done, reject = stay put.
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
declare function usePromisePending(): {
|
|
32
|
+
pending: boolean;
|
|
33
|
+
run: (result: unknown, onResolved: () => void) => boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The "prove it is you" field: a password input the browser's password manager fills
|
|
37
|
+
* with the CURRENT password (`autocomplete="current-password"`), never offers to
|
|
38
|
+
* generate a new one for. `readOnly` rather than `disabled` while busy: disabling the
|
|
39
|
+
* field that has focus (Enter was pressed in it) drops focus to <body>.
|
|
40
|
+
*
|
|
41
|
+
* @internal DangerConfirm's field. ReauthDialog spells it out with the field's own
|
|
42
|
+
* `error`, which keeps the `<input>` mounted while its message comes and goes.
|
|
43
|
+
*/
|
|
44
|
+
declare function CurrentPasswordInput({ inputRef, label, value, onValueChange, busy, invalid, describedBy, }: {
|
|
45
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
46
|
+
label: string;
|
|
47
|
+
value: string;
|
|
48
|
+
onValueChange: (value: string) => void;
|
|
49
|
+
busy: boolean;
|
|
50
|
+
invalid?: boolean;
|
|
51
|
+
describedBy?: string;
|
|
52
|
+
}): react.JSX.Element;
|
|
4
53
|
/**
|
|
5
54
|
* Every string the tile renders — the `dangerConfirm` namespace of
|
|
6
55
|
* `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and
|
|
@@ -59,9 +108,11 @@ interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">, "onCh
|
|
|
59
108
|
* How the typed text is compared with `phrase`. `"trim"` (default) ignores spaces
|
|
60
109
|
* around it — a phone keyboard's autocomplete likes to add one. `"exact"` compares
|
|
61
110
|
* character for character, spaces included, for an app whose contract is "type
|
|
62
|
-
* exactly this".
|
|
111
|
+
* exactly this". `"caseless"` (0.18) also ignores case — for a phrase that is an
|
|
112
|
+
* e-mail address (keksdose's "type the user's address" admin rule). See
|
|
113
|
+
* {@link TypedMatch}.
|
|
63
114
|
*/
|
|
64
|
-
phraseMatch?:
|
|
115
|
+
phraseMatch?: TypedMatch;
|
|
65
116
|
/** The warning above the fields. Defaults to `labels.prompt`. */
|
|
66
117
|
prompt?: ReactNode;
|
|
67
118
|
/** `"danger"` (default) for what cannot be undone; `"warning"` for what can, at a
|
|
@@ -112,4 +163,4 @@ interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<"div">, "onCh
|
|
|
112
163
|
*/
|
|
113
164
|
declare function DangerConfirm({ onConfirm, requirePassword, phrase, phraseMatch, prompt, tone, armLabel, confirmLabel, busy: busyProp, disabled, lockedReason, armed: armedProp, onArmedChange, labels: labelsProp, className, ...rest }: DangerConfirmProps): react.JSX.Element;
|
|
114
165
|
|
|
115
|
-
export { DEFAULT_DANGER_CONFIRM_LABELS, DangerConfirm, type DangerConfirmLabels, type DangerConfirmProps };
|
|
166
|
+
export { CurrentPasswordInput, DEFAULT_DANGER_CONFIRM_LABELS, DangerConfirm, type DangerConfirmLabels, type DangerConfirmProps, type TypedMatch, typedMatches, usePromisePending };
|
|
@@ -1,10 +1,65 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/cn.js";
|
|
5
5
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
6
6
|
import { Button, Input, Label, Spinner } from "./ui.js";
|
|
7
7
|
import { Tooltip } from "./tooltip.js";
|
|
8
|
+
function typedMatches(typed, target, mode = "trim") {
|
|
9
|
+
if (mode === "exact") return typed === target;
|
|
10
|
+
if (mode === "trim") return typed.trim() === target;
|
|
11
|
+
return typed.trim().toLowerCase() === target.trim().toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function usePromisePending() {
|
|
14
|
+
const [pending, setPending] = useState(false);
|
|
15
|
+
const mounted = useRef(true);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
mounted.current = true;
|
|
18
|
+
return () => {
|
|
19
|
+
mounted.current = false;
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
const run = useCallback((result, onResolved) => {
|
|
23
|
+
if (!result || typeof result.then !== "function") return false;
|
|
24
|
+
setPending(true);
|
|
25
|
+
result.then(
|
|
26
|
+
() => {
|
|
27
|
+
if (!mounted.current) return;
|
|
28
|
+
setPending(false);
|
|
29
|
+
onResolved();
|
|
30
|
+
},
|
|
31
|
+
() => {
|
|
32
|
+
if (mounted.current) setPending(false);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
return true;
|
|
36
|
+
}, []);
|
|
37
|
+
return { pending, run };
|
|
38
|
+
}
|
|
39
|
+
function CurrentPasswordInput({
|
|
40
|
+
inputRef,
|
|
41
|
+
label,
|
|
42
|
+
value,
|
|
43
|
+
onValueChange,
|
|
44
|
+
busy,
|
|
45
|
+
invalid,
|
|
46
|
+
describedBy
|
|
47
|
+
}) {
|
|
48
|
+
return /* @__PURE__ */ jsx(
|
|
49
|
+
Input,
|
|
50
|
+
{
|
|
51
|
+
ref: inputRef,
|
|
52
|
+
type: "password",
|
|
53
|
+
autoComplete: "current-password",
|
|
54
|
+
label,
|
|
55
|
+
value,
|
|
56
|
+
readOnly: busy,
|
|
57
|
+
invalid,
|
|
58
|
+
"aria-describedby": describedBy,
|
|
59
|
+
onChange: (e) => onValueChange(e.target.value)
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
8
63
|
const DEFAULT_DANGER_CONFIRM_LABELS = {
|
|
9
64
|
arm: "Delete\u2026",
|
|
10
65
|
confirm: "Delete",
|
|
@@ -36,7 +91,7 @@ function DangerConfirm({
|
|
|
36
91
|
const armed = armedProp ?? armedState;
|
|
37
92
|
const [password, setPassword] = useState("");
|
|
38
93
|
const [typed, setTyped] = useState("");
|
|
39
|
-
const
|
|
94
|
+
const { pending, run } = usePromisePending();
|
|
40
95
|
const busy = Boolean(busyProp) || pending;
|
|
41
96
|
const promptId = useId();
|
|
42
97
|
const reasonId = useId();
|
|
@@ -45,13 +100,6 @@ function DangerConfirm({
|
|
|
45
100
|
const cancelId = useId();
|
|
46
101
|
const firstFieldRef = useRef(null);
|
|
47
102
|
const moveFocus = useRef(false);
|
|
48
|
-
const mounted = useRef(true);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
mounted.current = true;
|
|
51
|
-
return () => {
|
|
52
|
-
mounted.current = false;
|
|
53
|
-
};
|
|
54
|
-
}, []);
|
|
55
103
|
const [wasArmed, setWasArmed] = useState(armed);
|
|
56
104
|
if (wasArmed !== armed) {
|
|
57
105
|
setWasArmed(armed);
|
|
@@ -81,24 +129,12 @@ function DangerConfirm({
|
|
|
81
129
|
};
|
|
82
130
|
const locked = lockedReason !== void 0 && lockedReason !== null && lockedReason !== false && lockedReason !== "";
|
|
83
131
|
const passwordOk = !requirePassword || password !== "";
|
|
84
|
-
const phraseOk = phrase === void 0 || (
|
|
132
|
+
const phraseOk = phrase === void 0 || typedMatches(typed, phrase, phraseMatch);
|
|
85
133
|
const canConfirm = passwordOk && phraseOk && !busy;
|
|
86
134
|
const submit = (e) => {
|
|
87
135
|
e.preventDefault();
|
|
88
136
|
if (!canConfirm) return;
|
|
89
|
-
|
|
90
|
-
if (!result || typeof result.then !== "function") return;
|
|
91
|
-
setPending(true);
|
|
92
|
-
result.then(
|
|
93
|
-
() => {
|
|
94
|
-
if (!mounted.current) return;
|
|
95
|
-
setPending(false);
|
|
96
|
-
setArmed(false);
|
|
97
|
-
},
|
|
98
|
-
() => {
|
|
99
|
-
if (mounted.current) setPending(false);
|
|
100
|
-
}
|
|
101
|
-
);
|
|
137
|
+
run(onConfirm(requirePassword ? password : void 0), () => setArmed(false));
|
|
102
138
|
};
|
|
103
139
|
const phrasePlaceholder = phrase === void 0 || labels.phrasePlaceholder === void 0 ? void 0 : typeof labels.phrasePlaceholder === "function" ? labels.phrasePlaceholder(phrase) : labels.phrasePlaceholder;
|
|
104
140
|
const phraseLabel = phrase === void 0 ? "" : typeof labels.phrase === "function" ? labels.phrase(phrase) : labels.phrase;
|
|
@@ -162,15 +198,13 @@ function DangerConfirm({
|
|
|
162
198
|
] })
|
|
163
199
|
) : /* @__PURE__ */ jsx(Input, { label: phraseLabel, ...phraseFieldProps })),
|
|
164
200
|
requirePassword && /* @__PURE__ */ jsx(
|
|
165
|
-
|
|
201
|
+
CurrentPasswordInput,
|
|
166
202
|
{
|
|
167
|
-
|
|
168
|
-
type: "password",
|
|
169
|
-
autoComplete: "current-password",
|
|
203
|
+
inputRef: phrase === void 0 ? firstFieldRef : void 0,
|
|
170
204
|
label: labels.password,
|
|
171
205
|
value: password,
|
|
172
|
-
|
|
173
|
-
|
|
206
|
+
busy,
|
|
207
|
+
onValueChange: setPassword
|
|
174
208
|
}
|
|
175
209
|
),
|
|
176
210
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
@@ -194,7 +228,10 @@ function DangerConfirm({
|
|
|
194
228
|
) });
|
|
195
229
|
}
|
|
196
230
|
export {
|
|
231
|
+
CurrentPasswordInput,
|
|
197
232
|
DEFAULT_DANGER_CONFIRM_LABELS,
|
|
198
|
-
DangerConfirm
|
|
233
|
+
DangerConfirm,
|
|
234
|
+
typedMatches,
|
|
235
|
+
usePromisePending
|
|
199
236
|
};
|
|
200
237
|
//# sourceMappingURL=danger-confirm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/danger-confirm.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, FormEvent, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, Input, Label, Spinner } from \"./ui\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * Every string the tile renders — the `dangerConfirm` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and\n * confirm buttons are usually worded per action (\"Delete budget\"), which is what the\n * `armLabel` / `confirmLabel` props are for; these are the fallbacks.\n */\nexport interface DangerConfirmLabels {\n /** The button that arms the tile. */\n arm: string;\n /** The button that runs the action once the guards are satisfied. */\n confirm: string;\n cancel: string;\n /** The warning above the fields, when no `prompt` is given. */\n prompt: string;\n /** Label of the password field (`requirePassword`). */\n password: string;\n /** Label of the type-to-confirm field. A FUNCTION of the phrase by default, like\n * every message that carries a value: where the phrase sits in the sentence moves\n * with the language. A plain STRING is taken as the finished label — for an app\n * whose catalogue already words it (\"Type DELETE to confirm\") and would otherwise\n * wrap it as `() => label`. */\n phrase: string | ((phrase: string) => string);\n /** Placeholder of the type-to-confirm field — a string, or a function of the\n * phrase (`(p) => p` echoes it). Optional and unset by default: the field keeps its\n * floating label, as before. Given, the label moves ABOVE the field (a floating\n * label occupies the empty field, which is exactly where a placeholder shows) and\n * the placeholder fills the field. An empty string means none. */\n phrasePlaceholder?: string | ((phrase: string) => string);\n}\n\n/** `satisfies` rather than a type annotation, so `DEFAULT_DANGER_CONFIRM_LABELS.phrase`\n * stays callable for a caller that composes its own label from it. */\nexport const DEFAULT_DANGER_CONFIRM_LABELS = {\n arm: \"Delete…\",\n confirm: \"Delete\",\n cancel: \"Cancel\",\n prompt: \"This cannot be undone.\",\n password: \"Password\",\n phrase: (phrase: string) => `Type “${phrase}” to confirm`,\n} satisfies DangerConfirmLabels;\n\nexport interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /**\n * Runs the action, with the entered password when `requirePassword` is on.\n *\n * Return a promise and the tile manages itself: busy until it settles, disarmed\n * (fields wiped) when it resolves, still armed when it rejects — so the user can\n * correct a wrong password and retry. The rejection is not swallowed for you to\n * miss: handle it (and show why) in the caller, as with any mutation.\n */\n onConfirm: (password?: string) => void | Promise<unknown>;\n /** Show a password field; confirm stays disabled until it is filled. */\n requirePassword?: boolean;\n /** Show a \"type <phrase> to confirm\" field; confirm stays disabled until the field\n * matches (case-sensitive; surrounding spaces ignored unless `phraseMatch=\"exact\"`). */\n phrase?: string;\n /**\n * How the typed text is compared with `phrase`. `\"trim\"` (default) ignores spaces\n * around it — a phone keyboard's autocomplete likes to add one. `\"exact\"` compares\n * character for character, spaces included, for an app whose contract is \"type\n * exactly this\".\n */\n phraseMatch?: \"trim\" | \"exact\";\n /** The warning above the fields. Defaults to `labels.prompt`. */\n prompt?: ReactNode;\n /** `\"danger\"` (default) for what cannot be undone; `\"warning\"` for what can, at a\n * cost (loading demo data over your own). Colours the prompt and the confirm. */\n tone?: \"danger\" | \"warning\";\n /** Visible text of the arm button; defaults to `labels.arm`. */\n armLabel?: ReactNode;\n /** Visible text of the confirm button; defaults to `labels.confirm`. */\n confirmLabel?: ReactNode;\n /** The action is running: confirm shows a spinner and nothing can be pressed. For a\n * caller that tracks the mutation itself (a `useMutation`'s `isPending`); a\n * promise returned from `onConfirm` does the same on its own. */\n busy?: boolean;\n /** The arm button is disabled. */\n disabled?: boolean;\n /**\n * Why the action is not available — a read-only demo, a write lock, a missing\n * permission. Disables the arm button like `disabled`, and SAYS so: the sentence is\n * shown under the button and attached to it with `aria-describedby`, and the button\n * stays focusable (`aria-disabled`) so a keyboard user can land on it and hear why.\n *\n * On the ARM button rather than the confirm: arming asks for a password, and asking\n * for a password for a write that can never land is the worse of the two.\n */\n lockedReason?: ReactNode;\n /** Controlled armed state. The parent can then collapse the tile from a mutation's\n * own `onSuccess` without returning a promise. */\n armed?: boolean;\n /** Called with the next armed state — on arm, cancel, and a resolved `onConfirm`. */\n onArmedChange?: (armed: boolean) => void;\n /** User-facing strings; see {@link DangerConfirmLabels}. */\n labels?: Partial<DangerConfirmLabels>;\n}\n\n/**\n * An \"arm → confirm\" tile for destructive actions: one button, which expands into a\n * warning, an optional password field, an optional type-to-confirm field and a\n * confirm that stays disabled until every guard is satisfied.\n *\n * Keksdose hand-rolled it three times (load demo data, wipe everything, reset a\n * budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific\n * part was its write-lock hook, which is `lockedReason` here.\n *\n * The fields are a `<form>`, so Enter confirms once the guards allow it. They are\n * wiped every time the tile disarms — a password must not sit in a collapsed tile.\n * Arming moves focus to the first field (or Cancel, when there is none — never to the\n * destructive button itself), and disarming moves it back to the arm button, so the\n * keyboard user is never left on an element that just vanished.\n */\nexport function DangerConfirm({\n onConfirm,\n requirePassword,\n phrase,\n phraseMatch = \"trim\",\n prompt,\n tone = \"danger\",\n armLabel,\n confirmLabel,\n busy: busyProp,\n disabled,\n lockedReason,\n armed: armedProp,\n onArmedChange,\n labels: labelsProp,\n className,\n ...rest\n}: DangerConfirmProps) {\n const labels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);\n const [armedState, setArmedState] = useState(false);\n const armed = armedProp ?? armedState;\n const [password, setPassword] = useState(\"\");\n const [typed, setTyped] = useState(\"\");\n const [pending, setPending] = useState(false);\n const busy = Boolean(busyProp) || pending;\n // `readOnly` rather than `disabled` on the fields while busy: disabling the field\n // that has focus (Enter was pressed in it) drops focus to <body>.\n const promptId = useId();\n const reasonId = useId();\n const phraseFieldId = useId();\n\n // The kit's Button takes no ref, so the two buttons focus is moved to are found by id.\n const armId = useId();\n const cancelId = useId();\n const firstFieldRef = useRef<HTMLInputElement>(null);\n // Focus moves only after a transition — never on mount, so a tile that renders armed\n // (controlled) does not take the page's focus merely by existing.\n const moveFocus = useRef(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // A disarm from anywhere (cancel, a resolved confirm, the parent) wipes the fields.\n // During render, like NumberField's draft, so no frame shows a collapsed tile that\n // still holds a password.\n const [wasArmed, setWasArmed] = useState(armed);\n if (wasArmed !== armed) {\n setWasArmed(armed);\n if (!armed) {\n setPassword(\"\");\n setTyped(\"\");\n }\n }\n\n const prevArmed = useRef(armed);\n useEffect(() => {\n const changed = prevArmed.current !== armed;\n prevArmed.current = armed;\n const byUser = moveFocus.current;\n moveFocus.current = false;\n if (!changed) return;\n if (armed) {\n if (byUser) (firstFieldRef.current ?? document.getElementById(cancelId))?.focus();\n return;\n }\n // A controlled parent collapsing the tile from its own `onSuccess` did not go\n // through `setArmed`; if focus went down with the form, bring it back too.\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (byUser || lost) document.getElementById(armId)?.focus();\n }, [armed, armId, cancelId]);\n\n const setArmed = (next: boolean) => {\n moveFocus.current = true;\n if (armedProp === undefined) setArmedState(next);\n onArmedChange?.(next);\n };\n\n const locked = lockedReason !== undefined && lockedReason !== null && lockedReason !== false && lockedReason !== \"\";\n const passwordOk = !requirePassword || password !== \"\";\n const phraseOk = phrase === undefined || (phraseMatch === \"exact\" ? typed : typed.trim()) === phrase;\n const canConfirm = passwordOk && phraseOk && !busy;\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (!canConfirm) return;\n const result = onConfirm(requirePassword ? password : undefined);\n if (!result || typeof (result as Promise<unknown>).then !== \"function\") return;\n setPending(true);\n (result as Promise<unknown>).then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n setArmed(false);\n },\n () => {\n // Stays armed, fields kept: the caller shows why, the user retries.\n if (mounted.current) setPending(false);\n },\n );\n };\n\n const phrasePlaceholder =\n phrase === undefined || labels.phrasePlaceholder === undefined\n ? undefined\n : typeof labels.phrasePlaceholder === \"function\"\n ? labels.phrasePlaceholder(phrase)\n : labels.phrasePlaceholder;\n\n const phraseLabel =\n phrase === undefined ? \"\" : typeof labels.phrase === \"function\" ? labels.phrase(phrase) : labels.phrase;\n const phraseFieldProps = {\n ref: firstFieldRef,\n value: typed,\n autoComplete: \"off\",\n autoCapitalize: \"off\",\n spellCheck: false,\n readOnly: busy,\n onChange: (e: ChangeEvent<HTMLInputElement>) => setTyped(e.target.value),\n };\n\n const toneText = tone === \"warning\" ? \"text-[var(--warning)]\" : \"text-[var(--danger)]\";\n\n if (!armed) {\n const arm = (\n <Button\n id={armId}\n type=\"button\"\n variant={tone === \"warning\" ? \"secondary\" : \"danger\"}\n disabled={disabled}\n aria-disabled={locked || undefined}\n aria-describedby={locked ? reasonId : undefined}\n onClick={() => {\n if (!locked) setArmed(true);\n }}\n className=\"aria-disabled:cursor-not-allowed aria-disabled:opacity-50\"\n >\n {armLabel ?? labels.arm}\n </Button>\n );\n return (\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {locked ? (\n // The reason on the button itself too, where the pointer that tried it is —\n // the line below can sit out of view in a long settings card. Portalled, so\n // a card that scrolls neither clips it nor grows by it.\n //\n // The button in a FRAGMENT on purpose: Tooltip clones an element child to\n // add the bubble to its `aria-describedby`, and the button is already\n // described by the visible line below — the same sentence twice, read out\n // on every focus. A fragment is left as it is, so the bubble stays visual.\n <Tooltip label={lockedReason} portal>\n <>{arm}</>\n </Tooltip>\n ) : (\n arm\n )}\n {locked && (\n <p id={reasonId} className=\"text-xs text-[var(--text-muted)]\">\n {lockedReason}\n </p>\n )}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"space-y-2\", className)}>\n <form\n className=\"space-y-2\"\n aria-describedby={promptId}\n aria-busy={busy || undefined}\n onSubmit={submit}\n noValidate\n >\n <p id={promptId} className={cn(\"text-xs font-medium\", toneText)}>\n {prompt ?? labels.prompt}\n </p>\n {phrase !== undefined &&\n (phrasePlaceholder ? (\n // A static label above, so the placeholder has the empty field to itself.\n <div className=\"space-y-1.5\">\n <Label htmlFor={phraseFieldId}>{phraseLabel}</Label>\n <Input id={phraseFieldId} placeholder={phrasePlaceholder} {...phraseFieldProps} />\n </div>\n ) : (\n <Input label={phraseLabel} {...phraseFieldProps} />\n ))}\n {requirePassword && (\n <Input\n // The phrase field takes the ref when both are there — it comes first.\n ref={phrase === undefined ? firstFieldRef : undefined}\n type=\"password\"\n autoComplete=\"current-password\"\n label={labels.password}\n value={password}\n readOnly={busy}\n onChange={(e) => setPassword(e.target.value)}\n />\n )}\n <div className=\"flex flex-wrap gap-2\">\n <Button id={cancelId} type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => setArmed(false)}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n variant={tone === \"warning\" ? \"primary\" : \"danger\"}\n disabled={!canConfirm}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {confirmLabel ?? labels.confirm}\n </Button>\n </div>\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/danger-confirm.tsx"],"sourcesContent":["import { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, FormEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, Input, Label, Spinner } from \"./ui\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * How typed text is compared with the text it has to repeat — shared by\n * `DangerConfirm`'s `phraseMatch` and `useConfirm`'s `typedMatch`.\n *\n * - `\"trim\"` ignores spaces around the typed text — a phone keyboard's autocomplete\n * likes to add one. Case counts: \"delete\" is not \"DELETE\".\n * - `\"exact\"` compares character for character, spaces included.\n * - `\"caseless\"` trims AND ignores case: for an e-mail address, whose domain is\n * case-insensitive and whose local part every real mail server treats so. Both apps'\n * \"type the account's address\" rules (keksdose's `TYPE_EMAIL`, Kurvenschmiede's\n * `TypedConfirm`) compared `trim().toLowerCase()`, and a capital the user's keyboard\n * put at the start must not make the right row look like the wrong one.\n */\nexport type TypedMatch = \"trim\" | \"exact\" | \"caseless\";\n\n/** Whether `typed` repeats `target` under `mode` — see {@link TypedMatch}. Exported for\n * a caller that draws its own field (a dialog that also picks a recipient) and wants\n * the same rule as the kit's. */\nexport function typedMatches(typed: string, target: string, mode: TypedMatch = \"trim\"): boolean {\n if (mode === \"exact\") return typed === target;\n if (mode === \"trim\") return typed.trim() === target;\n // `toLowerCase`, not `toLocaleLowerCase`: the comparison must not change with the\n // reader's locale (a Turkish dotless i would otherwise fail an ASCII address).\n return typed.trim().toLowerCase() === target.trim().toLowerCase();\n}\n\n/**\n * Busy-while-a-promise-runs, for an action that MAY return one: `run(result, onResolved)`\n * marks the caller pending until `result` settles, calls `onResolved` if it fulfilled,\n * and only clears the flag if it rejected — the caller shows why and the user retries.\n * Nothing runs after unmount. Shared by `DangerConfirm` and `ReauthDialog`, whose\n * contract is the same: resolve = done, reject = stay put.\n *\n * @internal\n */\nexport function usePromisePending() {\n const [pending, setPending] = useState(false);\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const run = useCallback((result: unknown, onResolved: () => void): boolean => {\n if (!result || typeof (result as Promise<unknown>).then !== \"function\") return false;\n setPending(true);\n (result as Promise<unknown>).then(\n () => {\n if (!mounted.current) return;\n setPending(false);\n onResolved();\n },\n () => {\n if (mounted.current) setPending(false);\n },\n );\n return true;\n }, []);\n return { pending, run };\n}\n\n/**\n * The \"prove it is you\" field: a password input the browser's password manager fills\n * with the CURRENT password (`autocomplete=\"current-password\"`), never offers to\n * generate a new one for. `readOnly` rather than `disabled` while busy: disabling the\n * field that has focus (Enter was pressed in it) drops focus to <body>.\n *\n * @internal DangerConfirm's field. ReauthDialog spells it out with the field's own\n * `error`, which keeps the `<input>` mounted while its message comes and goes.\n */\nexport function CurrentPasswordInput({\n inputRef,\n label,\n value,\n onValueChange,\n busy,\n invalid,\n describedBy,\n}: {\n inputRef?: Ref<HTMLInputElement>;\n label: string;\n value: string;\n onValueChange: (value: string) => void;\n busy: boolean;\n invalid?: boolean;\n describedBy?: string;\n}) {\n return (\n <Input\n ref={inputRef}\n type=\"password\"\n autoComplete=\"current-password\"\n label={label}\n value={value}\n readOnly={busy}\n invalid={invalid}\n aria-describedby={describedBy}\n onChange={(e) => onValueChange(e.target.value)}\n />\n );\n}\n\n/**\n * Every string the tile renders — the `dangerConfirm` namespace of\n * `<UiKitProvider labels>`, overridable per instance through `labels`. The arm and\n * confirm buttons are usually worded per action (\"Delete budget\"), which is what the\n * `armLabel` / `confirmLabel` props are for; these are the fallbacks.\n */\nexport interface DangerConfirmLabels {\n /** The button that arms the tile. */\n arm: string;\n /** The button that runs the action once the guards are satisfied. */\n confirm: string;\n cancel: string;\n /** The warning above the fields, when no `prompt` is given. */\n prompt: string;\n /** Label of the password field (`requirePassword`). */\n password: string;\n /** Label of the type-to-confirm field. A FUNCTION of the phrase by default, like\n * every message that carries a value: where the phrase sits in the sentence moves\n * with the language. A plain STRING is taken as the finished label — for an app\n * whose catalogue already words it (\"Type DELETE to confirm\") and would otherwise\n * wrap it as `() => label`. */\n phrase: string | ((phrase: string) => string);\n /** Placeholder of the type-to-confirm field — a string, or a function of the\n * phrase (`(p) => p` echoes it). Optional and unset by default: the field keeps its\n * floating label, as before. Given, the label moves ABOVE the field (a floating\n * label occupies the empty field, which is exactly where a placeholder shows) and\n * the placeholder fills the field. An empty string means none. */\n phrasePlaceholder?: string | ((phrase: string) => string);\n}\n\n/** `satisfies` rather than a type annotation, so `DEFAULT_DANGER_CONFIRM_LABELS.phrase`\n * stays callable for a caller that composes its own label from it. */\nexport const DEFAULT_DANGER_CONFIRM_LABELS = {\n arm: \"Delete…\",\n confirm: \"Delete\",\n cancel: \"Cancel\",\n prompt: \"This cannot be undone.\",\n password: \"Password\",\n phrase: (phrase: string) => `Type “${phrase}” to confirm`,\n} satisfies DangerConfirmLabels;\n\nexport interface DangerConfirmProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /**\n * Runs the action, with the entered password when `requirePassword` is on.\n *\n * Return a promise and the tile manages itself: busy until it settles, disarmed\n * (fields wiped) when it resolves, still armed when it rejects — so the user can\n * correct a wrong password and retry. The rejection is not swallowed for you to\n * miss: handle it (and show why) in the caller, as with any mutation.\n */\n onConfirm: (password?: string) => void | Promise<unknown>;\n /** Show a password field; confirm stays disabled until it is filled. */\n requirePassword?: boolean;\n /** Show a \"type <phrase> to confirm\" field; confirm stays disabled until the field\n * matches (case-sensitive; surrounding spaces ignored unless `phraseMatch=\"exact\"`). */\n phrase?: string;\n /**\n * How the typed text is compared with `phrase`. `\"trim\"` (default) ignores spaces\n * around it — a phone keyboard's autocomplete likes to add one. `\"exact\"` compares\n * character for character, spaces included, for an app whose contract is \"type\n * exactly this\". `\"caseless\"` (0.18) also ignores case — for a phrase that is an\n * e-mail address (keksdose's \"type the user's address\" admin rule). See\n * {@link TypedMatch}.\n */\n phraseMatch?: TypedMatch;\n /** The warning above the fields. Defaults to `labels.prompt`. */\n prompt?: ReactNode;\n /** `\"danger\"` (default) for what cannot be undone; `\"warning\"` for what can, at a\n * cost (loading demo data over your own). Colours the prompt and the confirm. */\n tone?: \"danger\" | \"warning\";\n /** Visible text of the arm button; defaults to `labels.arm`. */\n armLabel?: ReactNode;\n /** Visible text of the confirm button; defaults to `labels.confirm`. */\n confirmLabel?: ReactNode;\n /** The action is running: confirm shows a spinner and nothing can be pressed. For a\n * caller that tracks the mutation itself (a `useMutation`'s `isPending`); a\n * promise returned from `onConfirm` does the same on its own. */\n busy?: boolean;\n /** The arm button is disabled. */\n disabled?: boolean;\n /**\n * Why the action is not available — a read-only demo, a write lock, a missing\n * permission. Disables the arm button like `disabled`, and SAYS so: the sentence is\n * shown under the button and attached to it with `aria-describedby`, and the button\n * stays focusable (`aria-disabled`) so a keyboard user can land on it and hear why.\n *\n * On the ARM button rather than the confirm: arming asks for a password, and asking\n * for a password for a write that can never land is the worse of the two.\n */\n lockedReason?: ReactNode;\n /** Controlled armed state. The parent can then collapse the tile from a mutation's\n * own `onSuccess` without returning a promise. */\n armed?: boolean;\n /** Called with the next armed state — on arm, cancel, and a resolved `onConfirm`. */\n onArmedChange?: (armed: boolean) => void;\n /** User-facing strings; see {@link DangerConfirmLabels}. */\n labels?: Partial<DangerConfirmLabels>;\n}\n\n/**\n * An \"arm → confirm\" tile for destructive actions: one button, which expands into a\n * warning, an optional password field, an optional type-to-confirm field and a\n * confirm that stays disabled until every guard is satisfied.\n *\n * Keksdose hand-rolled it three times (load demo data, wipe everything, reset a\n * budget) and then as `shared/components/danger-confirm.tsx`; the only app-specific\n * part was its write-lock hook, which is `lockedReason` here.\n *\n * The fields are a `<form>`, so Enter confirms once the guards allow it. They are\n * wiped every time the tile disarms — a password must not sit in a collapsed tile.\n * Arming moves focus to the first field (or Cancel, when there is none — never to the\n * destructive button itself), and disarming moves it back to the arm button, so the\n * keyboard user is never left on an element that just vanished.\n */\nexport function DangerConfirm({\n onConfirm,\n requirePassword,\n phrase,\n phraseMatch = \"trim\",\n prompt,\n tone = \"danger\",\n armLabel,\n confirmLabel,\n busy: busyProp,\n disabled,\n lockedReason,\n armed: armedProp,\n onArmedChange,\n labels: labelsProp,\n className,\n ...rest\n}: DangerConfirmProps) {\n const labels = useKitLabels(\"dangerConfirm\", DEFAULT_DANGER_CONFIRM_LABELS, labelsProp);\n const [armedState, setArmedState] = useState(false);\n const armed = armedProp ?? armedState;\n const [password, setPassword] = useState(\"\");\n const [typed, setTyped] = useState(\"\");\n const { pending, run } = usePromisePending();\n const busy = Boolean(busyProp) || pending;\n // `readOnly` rather than `disabled` on the fields while busy: disabling the field\n // that has focus (Enter was pressed in it) drops focus to <body>.\n const promptId = useId();\n const reasonId = useId();\n const phraseFieldId = useId();\n\n // The kit's Button takes no ref, so the two buttons focus is moved to are found by id.\n const armId = useId();\n const cancelId = useId();\n const firstFieldRef = useRef<HTMLInputElement>(null);\n // Focus moves only after a transition — never on mount, so a tile that renders armed\n // (controlled) does not take the page's focus merely by existing.\n const moveFocus = useRef(false);\n\n // A disarm from anywhere (cancel, a resolved confirm, the parent) wipes the fields.\n // During render, like NumberField's draft, so no frame shows a collapsed tile that\n // still holds a password.\n const [wasArmed, setWasArmed] = useState(armed);\n if (wasArmed !== armed) {\n setWasArmed(armed);\n if (!armed) {\n setPassword(\"\");\n setTyped(\"\");\n }\n }\n\n const prevArmed = useRef(armed);\n useEffect(() => {\n const changed = prevArmed.current !== armed;\n prevArmed.current = armed;\n const byUser = moveFocus.current;\n moveFocus.current = false;\n if (!changed) return;\n if (armed) {\n if (byUser) (firstFieldRef.current ?? document.getElementById(cancelId))?.focus();\n return;\n }\n // A controlled parent collapsing the tile from its own `onSuccess` did not go\n // through `setArmed`; if focus went down with the form, bring it back too.\n const lost = document.activeElement === null || document.activeElement === document.body;\n if (byUser || lost) document.getElementById(armId)?.focus();\n }, [armed, armId, cancelId]);\n\n const setArmed = (next: boolean) => {\n moveFocus.current = true;\n if (armedProp === undefined) setArmedState(next);\n onArmedChange?.(next);\n };\n\n const locked = lockedReason !== undefined && lockedReason !== null && lockedReason !== false && lockedReason !== \"\";\n const passwordOk = !requirePassword || password !== \"\";\n const phraseOk = phrase === undefined || typedMatches(typed, phrase, phraseMatch);\n const canConfirm = passwordOk && phraseOk && !busy;\n\n const submit = (e: FormEvent) => {\n e.preventDefault();\n if (!canConfirm) return;\n // Disarms when it resolves; a rejection leaves it armed, fields kept: the caller\n // shows why, the user retries.\n run(onConfirm(requirePassword ? password : undefined), () => setArmed(false));\n };\n\n const phrasePlaceholder =\n phrase === undefined || labels.phrasePlaceholder === undefined\n ? undefined\n : typeof labels.phrasePlaceholder === \"function\"\n ? labels.phrasePlaceholder(phrase)\n : labels.phrasePlaceholder;\n\n const phraseLabel =\n phrase === undefined ? \"\" : typeof labels.phrase === \"function\" ? labels.phrase(phrase) : labels.phrase;\n const phraseFieldProps = {\n ref: firstFieldRef,\n value: typed,\n autoComplete: \"off\",\n autoCapitalize: \"off\",\n spellCheck: false,\n readOnly: busy,\n onChange: (e: ChangeEvent<HTMLInputElement>) => setTyped(e.target.value),\n };\n\n const toneText = tone === \"warning\" ? \"text-[var(--warning)]\" : \"text-[var(--danger)]\";\n\n if (!armed) {\n const arm = (\n <Button\n id={armId}\n type=\"button\"\n variant={tone === \"warning\" ? \"secondary\" : \"danger\"}\n disabled={disabled}\n aria-disabled={locked || undefined}\n aria-describedby={locked ? reasonId : undefined}\n onClick={() => {\n if (!locked) setArmed(true);\n }}\n className=\"aria-disabled:cursor-not-allowed aria-disabled:opacity-50\"\n >\n {armLabel ?? labels.arm}\n </Button>\n );\n return (\n <div {...rest} className={cn(\"space-y-1\", className)}>\n {locked ? (\n // The reason on the button itself too, where the pointer that tried it is —\n // the line below can sit out of view in a long settings card. Portalled, so\n // a card that scrolls neither clips it nor grows by it.\n //\n // The button in a FRAGMENT on purpose: Tooltip clones an element child to\n // add the bubble to its `aria-describedby`, and the button is already\n // described by the visible line below — the same sentence twice, read out\n // on every focus. A fragment is left as it is, so the bubble stays visual.\n <Tooltip label={lockedReason} portal>\n <>{arm}</>\n </Tooltip>\n ) : (\n arm\n )}\n {locked && (\n <p id={reasonId} className=\"text-xs text-[var(--text-muted)]\">\n {lockedReason}\n </p>\n )}\n </div>\n );\n }\n\n return (\n <div {...rest} className={cn(\"space-y-2\", className)}>\n <form\n className=\"space-y-2\"\n aria-describedby={promptId}\n aria-busy={busy || undefined}\n onSubmit={submit}\n noValidate\n >\n <p id={promptId} className={cn(\"text-xs font-medium\", toneText)}>\n {prompt ?? labels.prompt}\n </p>\n {phrase !== undefined &&\n (phrasePlaceholder ? (\n // A static label above, so the placeholder has the empty field to itself.\n <div className=\"space-y-1.5\">\n <Label htmlFor={phraseFieldId}>{phraseLabel}</Label>\n <Input id={phraseFieldId} placeholder={phrasePlaceholder} {...phraseFieldProps} />\n </div>\n ) : (\n <Input label={phraseLabel} {...phraseFieldProps} />\n ))}\n {requirePassword && (\n <CurrentPasswordInput\n // The phrase field takes the ref when both are there — it comes first.\n inputRef={phrase === undefined ? firstFieldRef : undefined}\n label={labels.password}\n value={password}\n busy={busy}\n onValueChange={setPassword}\n />\n )}\n <div className=\"flex flex-wrap gap-2\">\n <Button id={cancelId} type=\"button\" variant=\"ghost\" disabled={busy} onClick={() => setArmed(false)}>\n {labels.cancel}\n </Button>\n <Button\n type=\"submit\"\n variant={tone === \"warning\" ? \"primary\" : \"danger\"}\n disabled={!canConfirm}\n aria-busy={busy || undefined}\n >\n {busy && <Spinner label={null} className=\"h-4 w-4\" />}\n {confirmLabel ?? labels.confirm}\n </Button>\n </div>\n </form>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
import { DataTableLabels } from './data-table-labels.js';
|
|
4
|
-
import { b as DataTableColumn } from '../data-table-
|
|
4
|
+
import { b as DataTableColumn } from '../data-table-3mi2R3so.js';
|
|
5
5
|
import './data-table-sort.js';
|
|
6
6
|
import './swipeable-row.js';
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { b as DataTableColumn, f as FilterValue } from '../data-table-
|
|
2
|
+
import { b as DataTableColumn, f as FilterValue } from '../data-table-3mi2R3so.js';
|
|
3
3
|
import { DataTableLabels } from './data-table-labels.js';
|
|
4
4
|
import './data-table-sort.js';
|
|
5
5
|
import './swipeable-row.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { C as ColumnFilter, f as FilterValue, j as dateFilter, k as decodeFilterValue, l as decodeFilterValueOfType, m as defaultFilterState, n as encodeFilterValue, o as filterHref, p as isFilterActive, q as numberFilter, s as resolveFilter, t as rowMatches, u as selectFilter, v as textFilter } from '../data-table-
|
|
1
|
+
export { C as ColumnFilter, f as FilterValue, j as dateFilter, k as decodeFilterValue, l as decodeFilterValueOfType, m as defaultFilterState, n as encodeFilterValue, o as filterHref, p as isFilterActive, q as numberFilter, s as resolveFilter, t as rowMatches, u as selectFilter, v as textFilter } from '../data-table-3mi2R3so.js';
|
|
2
2
|
import 'react';
|
|
3
3
|
import './data-table-sort.js';
|
|
4
4
|
import './swipeable-row.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'react';
|
|
2
|
-
export { D as DataTable, x as DataTableCellProps, a as DataTableChrome, b as DataTableColumn, c as DataTableDensity, y as DataTableHeadProps, d as DataTableProps, e as DataTableRowAction, F as FilterState, M as MobileSwipeActions, S as ServerPagination } from '../data-table-
|
|
2
|
+
export { D as DataTable, x as DataTableCellProps, a as DataTableChrome, b as DataTableColumn, c as DataTableDensity, y as DataTableHeadProps, d as DataTableProps, e as DataTableRowAction, F as FilterState, M as MobileSwipeActions, S as ServerPagination } from '../data-table-3mi2R3so.js';
|
|
3
3
|
export { SortState } from './data-table-sort.js';
|
|
4
4
|
import './swipeable-row.js';
|
|
5
5
|
import './data-table-labels.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createElement } from "react";
|
|
4
|
-
import { Fragment as Fragment2, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Fragment as Fragment2, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import {
|
|
7
7
|
ArrowDown,
|
|
@@ -87,6 +87,7 @@ function logicalAlign(className) {
|
|
|
87
87
|
)
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
|
+
const FOOTER_CELL = "sticky bottom-0 z-10 bg-[var(--bg-surface-2)] shadow-[inset_0_2px_0_var(--border-strong)]";
|
|
90
91
|
const END_ALIGN = /(^|\s)text-end(?=\s|$)/;
|
|
91
92
|
function cleanAttrs(attrs) {
|
|
92
93
|
if (!attrs) return void 0;
|
|
@@ -180,6 +181,7 @@ function DataTable({
|
|
|
180
181
|
locale: localeProp,
|
|
181
182
|
storageKeyPrefix = DEFAULT_PERSIST_PREFIX,
|
|
182
183
|
mobileExpandAsDialog = false,
|
|
184
|
+
mobileDialogBackCloses = true,
|
|
183
185
|
mobileGroupBy,
|
|
184
186
|
mobileGroupLabel,
|
|
185
187
|
mobileCard,
|
|
@@ -194,7 +196,9 @@ function DataTable({
|
|
|
194
196
|
frame = true,
|
|
195
197
|
className,
|
|
196
198
|
rowActions,
|
|
197
|
-
toolbar
|
|
199
|
+
toolbar,
|
|
200
|
+
footerLabel,
|
|
201
|
+
footer: footerProp
|
|
198
202
|
}) {
|
|
199
203
|
const compact = density === "compact";
|
|
200
204
|
const minimal = chrome === "minimal";
|
|
@@ -435,7 +439,7 @@ function DataTable({
|
|
|
435
439
|
if (!table) return [th];
|
|
436
440
|
return [
|
|
437
441
|
th,
|
|
438
|
-
...Array.from(table.querySelectorAll("tbody [data-col]")).filter(
|
|
442
|
+
...Array.from(table.querySelectorAll("tbody [data-col], tfoot [data-col]")).filter(
|
|
439
443
|
(el) => el.dataset.col === key
|
|
440
444
|
)
|
|
441
445
|
];
|
|
@@ -517,6 +521,11 @@ function DataTable({
|
|
|
517
521
|
const visibleCount = visibleColumns.length;
|
|
518
522
|
const totalCount = columns.length;
|
|
519
523
|
const totalColSpan = visibleCount + (selection ? 1 : 0);
|
|
524
|
+
const hasFooterColumns = columns.some((c) => c.footer);
|
|
525
|
+
const showFooter = footerProp !== false && hasFooterColumns && sorted.length > 0;
|
|
526
|
+
const footerLabelCol = showFooter ? visibleColumns.find((c) => !c.footer && c.key !== ROW_ACTIONS_KEY) ?? visibleColumns[0] : void 0;
|
|
527
|
+
const footerValue = (col) => col.footer?.(sorted, slice);
|
|
528
|
+
const footerHeadingId = useId();
|
|
520
529
|
const columnsCountLabel = rowActions ? labels.columnsCount(visibleCount - 1, totalCount - 1) : labels.columnsCount(visibleCount, totalCount);
|
|
521
530
|
const isMdUp = useMediaQuery("(min-width: 768px)", true);
|
|
522
531
|
const mobileColumns = visibleColumns.filter((c) => !c.mobileHidden);
|
|
@@ -782,6 +791,34 @@ function DataTable({
|
|
|
782
791
|
]
|
|
783
792
|
}
|
|
784
793
|
),
|
|
794
|
+
showFooter && mobileColumns.some((c) => c.footer) && /* @__PURE__ */ jsxs(
|
|
795
|
+
"section",
|
|
796
|
+
{
|
|
797
|
+
"data-table-footer": "",
|
|
798
|
+
"aria-labelledby": footerLabel != null ? footerHeadingId : void 0,
|
|
799
|
+
className: cn(
|
|
800
|
+
"border-t-2 border-[var(--border-strong)] bg-[var(--bg-surface-2)]",
|
|
801
|
+
compact ? "px-3 py-2" : "px-4 py-3",
|
|
802
|
+
isLoading && "opacity-60 transition-opacity"
|
|
803
|
+
),
|
|
804
|
+
children: [
|
|
805
|
+
footerLabel != null && /* @__PURE__ */ jsx("div", { id: footerHeadingId, className: cn("font-semibold", compact ? "mb-1 text-sm" : "mb-2"), children: footerLabel }),
|
|
806
|
+
/* @__PURE__ */ jsx(
|
|
807
|
+
"dl",
|
|
808
|
+
{
|
|
809
|
+
className: cn(
|
|
810
|
+
"grid grid-cols-[auto_1fr] gap-x-3",
|
|
811
|
+
compact ? "gap-y-0.5 text-xs" : "gap-y-1 text-sm"
|
|
812
|
+
),
|
|
813
|
+
children: mobileColumns.filter((c) => c.footer).map((col) => /* @__PURE__ */ jsxs(Fragment2, { children: [
|
|
814
|
+
/* @__PURE__ */ jsx("dt", { className: "self-center text-xs uppercase tracking-wide text-[var(--text-muted)]", children: col.header }),
|
|
815
|
+
/* @__PURE__ */ jsx("dd", { className: "min-w-0 self-center font-semibold text-[var(--text-primary)]", children: footerValue(col) })
|
|
816
|
+
] }, col.key))
|
|
817
|
+
}
|
|
818
|
+
)
|
|
819
|
+
]
|
|
820
|
+
}
|
|
821
|
+
),
|
|
785
822
|
isServer && /* @__PURE__ */ jsx(
|
|
786
823
|
Pagination,
|
|
787
824
|
{
|
|
@@ -803,6 +840,7 @@ function DataTable({
|
|
|
803
840
|
onClose: closeDialog,
|
|
804
841
|
closeLabel: labels.close,
|
|
805
842
|
header: mobileDialogRow ? mobilePrimaryCol?.cell(mobileDialogRow) : null,
|
|
843
|
+
backCloses: mobileDialogBackCloses,
|
|
806
844
|
children: mobileDialogContent
|
|
807
845
|
}
|
|
808
846
|
)
|
|
@@ -1102,6 +1140,58 @@ function DataTable({
|
|
|
1102
1140
|
) })
|
|
1103
1141
|
]
|
|
1104
1142
|
}
|
|
1143
|
+
),
|
|
1144
|
+
showFooter && // Sticky to the scroller's bottom edge for the same reason the header is
|
|
1145
|
+
// sticky to its top: the body scrolls inside a bounded wrapper, and a
|
|
1146
|
+
// total that scrolls away with the last row is a total nobody sees on a
|
|
1147
|
+
// 25-row page. Each cell carries its own background and its own `sticky`,
|
|
1148
|
+
// as the header's do — `position: sticky` on a `<tfoot>` is not reliable.
|
|
1149
|
+
/* @__PURE__ */ jsx(
|
|
1150
|
+
"tfoot",
|
|
1151
|
+
{
|
|
1152
|
+
"data-table-footer": "",
|
|
1153
|
+
className: cn(
|
|
1154
|
+
"font-semibold text-[var(--text-primary)]",
|
|
1155
|
+
isLoading && "opacity-60 transition-opacity"
|
|
1156
|
+
),
|
|
1157
|
+
children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
1158
|
+
selection && /* @__PURE__ */ jsx(
|
|
1159
|
+
"td",
|
|
1160
|
+
{
|
|
1161
|
+
className: cn(
|
|
1162
|
+
FOOTER_CELL,
|
|
1163
|
+
compact ? "w-8 px-2 py-1" : "w-10 px-3 py-2"
|
|
1164
|
+
)
|
|
1165
|
+
}
|
|
1166
|
+
),
|
|
1167
|
+
visibleColumns.map((col) => {
|
|
1168
|
+
const width = widths[col.key];
|
|
1169
|
+
const pinned = width ? { width, minWidth: width, maxWidth: width } : void 0;
|
|
1170
|
+
const isLabel = col === footerLabelCol;
|
|
1171
|
+
const Cell = isLabel ? "th" : "td";
|
|
1172
|
+
return /* @__PURE__ */ jsx(
|
|
1173
|
+
Cell,
|
|
1174
|
+
{
|
|
1175
|
+
"data-col": col.key,
|
|
1176
|
+
scope: isLabel ? "row" : void 0,
|
|
1177
|
+
style: pinned,
|
|
1178
|
+
className: cn(
|
|
1179
|
+
FOOTER_CELL,
|
|
1180
|
+
"align-top",
|
|
1181
|
+
compact ? "px-2 py-1" : "px-3 py-2",
|
|
1182
|
+
width && "overflow-hidden text-ellipsis",
|
|
1183
|
+
// A `<th>` centres by default; the label reads from the start
|
|
1184
|
+
// like the cells above it, unless the column says otherwise.
|
|
1185
|
+
isLabel && "text-start",
|
|
1186
|
+
logicalAlign(col.className)
|
|
1187
|
+
),
|
|
1188
|
+
children: col.footer ? footerValue(col) : isLabel ? footerLabel : null
|
|
1189
|
+
},
|
|
1190
|
+
col.key
|
|
1191
|
+
);
|
|
1192
|
+
})
|
|
1193
|
+
] })
|
|
1194
|
+
}
|
|
1105
1195
|
)
|
|
1106
1196
|
]
|
|
1107
1197
|
}
|