@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
|
@@ -6,7 +6,7 @@ import './series-chart-labels.js';
|
|
|
6
6
|
import './toggle-legend.js';
|
|
7
7
|
import './series-chart-ticks.js';
|
|
8
8
|
import './series-chart-budget.js';
|
|
9
|
-
import '../kit-labels-
|
|
9
|
+
import '../kit-labels-CPbsjlXr.js';
|
|
10
10
|
import './data-table-labels.js';
|
|
11
11
|
import './mini-calendar.js';
|
|
12
12
|
import './calendar-heatmap.js';
|
|
@@ -56,6 +56,15 @@ import './authed-image.js';
|
|
|
56
56
|
import '../hooks/use-authed-src.js';
|
|
57
57
|
import './image-grid.js';
|
|
58
58
|
import './lightbox.js';
|
|
59
|
+
import './write-lock.js';
|
|
60
|
+
import './account-chips.js';
|
|
61
|
+
import '../lib/format.js';
|
|
62
|
+
import '../data-table-3mi2R3so.js';
|
|
63
|
+
import './data-table-sort.js';
|
|
64
|
+
import './swipeable-row.js';
|
|
65
|
+
import './reauth-dialog.js';
|
|
66
|
+
import './server-wake.js';
|
|
67
|
+
import '../lib/server-wake.js';
|
|
59
68
|
|
|
60
69
|
/** Which chart of a facing pair. Physical, like the axes: charts do not mirror in RTL. */
|
|
61
70
|
type FacingSide = "left" | "right";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import 'react';
|
|
2
|
-
export {
|
|
2
|
+
export { P as DEFAULT_FILE_PICKER_LABELS, ae as FileButton, af as FileButtonProps, bu as FilePickHandler, ah as FilePickerLabels, ai as FileRejection, aj as FileRejectionReason, ak as FileScreenOptions, bf as UseFilePickerOptions, bg as UseFilePickerReturn, bv as formatAccept, bw as judgePick, bj as matchesAccept, bx as screenFiles, by as summariseRejections, bm as useFilePicker } from '../kit-labels-CPbsjlXr.js';
|
|
3
3
|
import './data-table-labels.js';
|
|
4
4
|
import './mini-calendar.js';
|
|
5
5
|
import './calendar-heatmap.js';
|
|
@@ -50,3 +50,12 @@ import './authed-image.js';
|
|
|
50
50
|
import '../hooks/use-authed-src.js';
|
|
51
51
|
import './image-grid.js';
|
|
52
52
|
import './lightbox.js';
|
|
53
|
+
import './write-lock.js';
|
|
54
|
+
import './account-chips.js';
|
|
55
|
+
import '../lib/format.js';
|
|
56
|
+
import '../data-table-3mi2R3so.js';
|
|
57
|
+
import './data-table-sort.js';
|
|
58
|
+
import './swipeable-row.js';
|
|
59
|
+
import './reauth-dialog.js';
|
|
60
|
+
import './server-wake.js';
|
|
61
|
+
import '../lib/server-wake.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { ai as FileRejection, bu as FilePickHandler, ah as FilePickerLabels } from '../kit-labels-CPbsjlXr.js';
|
|
4
4
|
import './data-table-labels.js';
|
|
5
5
|
import './mini-calendar.js';
|
|
6
6
|
import './calendar-heatmap.js';
|
|
@@ -51,6 +51,15 @@ import './authed-image.js';
|
|
|
51
51
|
import '../hooks/use-authed-src.js';
|
|
52
52
|
import './image-grid.js';
|
|
53
53
|
import './lightbox.js';
|
|
54
|
+
import './write-lock.js';
|
|
55
|
+
import './account-chips.js';
|
|
56
|
+
import '../lib/format.js';
|
|
57
|
+
import '../data-table-3mi2R3so.js';
|
|
58
|
+
import './data-table-sort.js';
|
|
59
|
+
import './swipeable-row.js';
|
|
60
|
+
import './reauth-dialog.js';
|
|
61
|
+
import './server-wake.js';
|
|
62
|
+
import '../lib/server-wake.js';
|
|
54
63
|
|
|
55
64
|
/**
|
|
56
65
|
* Where a refused file's message goes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import 'lucide-react';
|
|
3
|
-
export {
|
|
3
|
+
export { Q as DEFAULT_FORM_ACTIONS_LABELS, an as FormActions, ao as FormActionsAlign, ap as FormActionsDestructive, aq as FormActionsLabels, ar as FormActionsPlacement, as as FormActionsProps, at as FormActionsStickyWithin, au as FormActionsSubmitProps, aO as ResponsiveFormActionsPlacement } from '../kit-labels-CPbsjlXr.js';
|
|
4
4
|
import './data-table-labels.js';
|
|
5
5
|
import './mini-calendar.js';
|
|
6
6
|
import './calendar-heatmap.js';
|
|
@@ -50,3 +50,12 @@ import './authed-image.js';
|
|
|
50
50
|
import '../hooks/use-authed-src.js';
|
|
51
51
|
import './image-grid.js';
|
|
52
52
|
import './lightbox.js';
|
|
53
|
+
import './write-lock.js';
|
|
54
|
+
import './account-chips.js';
|
|
55
|
+
import '../lib/format.js';
|
|
56
|
+
import '../data-table-3mi2R3so.js';
|
|
57
|
+
import './data-table-sort.js';
|
|
58
|
+
import './swipeable-row.js';
|
|
59
|
+
import './reauth-dialog.js';
|
|
60
|
+
import './server-wake.js';
|
|
61
|
+
import '../lib/server-wake.js';
|
|
@@ -3,11 +3,35 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { isValidElement } from "react";
|
|
4
4
|
import { cn } from "../lib/cn.js";
|
|
5
5
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
6
|
+
import { useMediaQuery } from "../hooks/use-media-query.js";
|
|
6
7
|
import { Button, Spinner } from "./ui.js";
|
|
7
8
|
const DEFAULT_FORM_ACTIONS_LABELS = {
|
|
8
9
|
save: "Save",
|
|
9
10
|
cancel: "Cancel"
|
|
10
11
|
};
|
|
12
|
+
const BREAKPOINTS = [
|
|
13
|
+
["xl", "(min-width: 1280px)"],
|
|
14
|
+
["lg", "(min-width: 1024px)"],
|
|
15
|
+
["md", "(min-width: 768px)"],
|
|
16
|
+
["sm", "(min-width: 640px)"]
|
|
17
|
+
];
|
|
18
|
+
function useResolvedPlacement(placement) {
|
|
19
|
+
const matches = {
|
|
20
|
+
xl: useMediaQuery(BREAKPOINTS[0][1], false),
|
|
21
|
+
lg: useMediaQuery(BREAKPOINTS[1][1], false),
|
|
22
|
+
md: useMediaQuery(BREAKPOINTS[2][1], false),
|
|
23
|
+
sm: useMediaQuery(BREAKPOINTS[3][1], false)
|
|
24
|
+
};
|
|
25
|
+
if (typeof placement === "string") return placement;
|
|
26
|
+
for (const [key] of BREAKPOINTS) {
|
|
27
|
+
const p = placement[key];
|
|
28
|
+
if (matches[key] && p !== void 0) return p;
|
|
29
|
+
}
|
|
30
|
+
return placement.base;
|
|
31
|
+
}
|
|
32
|
+
function cssLength(value) {
|
|
33
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
34
|
+
}
|
|
11
35
|
const ALIGN_CLASS = {
|
|
12
36
|
start: "justify-start",
|
|
13
37
|
center: "justify-center",
|
|
@@ -33,12 +57,14 @@ function FormActions({
|
|
|
33
57
|
pending = false,
|
|
34
58
|
submitDisabled = false,
|
|
35
59
|
submitDisabledReason,
|
|
60
|
+
commit = false,
|
|
36
61
|
submitVariant = "brand",
|
|
37
62
|
destructive,
|
|
38
63
|
start: startSlot,
|
|
39
64
|
align,
|
|
40
|
-
placement = "inline",
|
|
65
|
+
placement: placementProp = "inline",
|
|
41
66
|
stickyWithin = "viewport",
|
|
67
|
+
bleed,
|
|
42
68
|
form,
|
|
43
69
|
children,
|
|
44
70
|
className,
|
|
@@ -46,6 +72,8 @@ function FormActions({
|
|
|
46
72
|
...rest
|
|
47
73
|
}) {
|
|
48
74
|
const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
|
|
75
|
+
const placement = useResolvedPlacement(placementProp);
|
|
76
|
+
const bleedLength = bleed === void 0 || bleed === "" ? void 0 : cssLength(bleed);
|
|
49
77
|
const hasDestructive = destructive !== void 0 && destructive !== null && destructive !== false;
|
|
50
78
|
const hasStart = startSlot !== void 0 && startSlot !== null && startSlot !== false;
|
|
51
79
|
const justify = align ?? (hasDestructive || hasStart ? "between" : "end");
|
|
@@ -58,6 +86,7 @@ function FormActions({
|
|
|
58
86
|
onClick: destructive.onClick,
|
|
59
87
|
disabled: destructive.disabled,
|
|
60
88
|
disabledReason: destructive.disabledReason,
|
|
89
|
+
commit,
|
|
61
90
|
children: destructive.label
|
|
62
91
|
}
|
|
63
92
|
) : destructive;
|
|
@@ -78,6 +107,10 @@ function FormActions({
|
|
|
78
107
|
// container the nav is not underneath: `--app-nav-h` is the document's,
|
|
79
108
|
// and would hold the row that far above the container's own edge.
|
|
80
109
|
bottom: stickyWithin === "container" ? 0 : "max(0px, calc(var(--app-nav-h, 0px) - 1px))",
|
|
110
|
+
...bleedLength !== void 0 && {
|
|
111
|
+
marginInline: `calc(-1 * ${bleedLength})`,
|
|
112
|
+
paddingInline: bleedLength
|
|
113
|
+
},
|
|
81
114
|
...style
|
|
82
115
|
} : style,
|
|
83
116
|
className: cn("flex flex-wrap items-center gap-2", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className),
|
|
@@ -99,6 +132,7 @@ function FormActions({
|
|
|
99
132
|
variant: submitVariant,
|
|
100
133
|
disabled: pending || submitDisabled,
|
|
101
134
|
disabledReason: pending ? void 0 : submitDisabledReason,
|
|
135
|
+
commit: commit && !pending,
|
|
102
136
|
"aria-busy": pending || void 0,
|
|
103
137
|
children: [
|
|
104
138
|
pending ? /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-4" }) : SubmitIcon && /* @__PURE__ */ jsx(SubmitIcon, { "aria-hidden": true, className: "size-4 shrink-0" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/form-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, Spinner, type ButtonProps, type ButtonVariant } from \"./ui\";\n\n/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */\nexport interface FormActionsLabels {\n save: string;\n cancel: string;\n}\n\nexport const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {\n save: \"Save\",\n cancel: \"Cancel\",\n};\n\nexport type FormActionsAlign = \"start\" | \"center\" | \"end\" | \"between\";\n\n/**\n * Where the row sits:\n * - `inline` (default): under the form's last field, `pt-4` above it.\n * - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface\n * colour with a rule above, clear of the phone's home indicator — for a long form\n * whose Save should not be a scroll away.\n * - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,\n * FullBleedDialog's `footer`), which already pads and rules it.\n */\nexport type FormActionsPlacement = \"inline\" | \"sticky\" | \"dialog\";\n\n/**\n * What a `sticky` row sticks to — see {@link FormActionsProps.stickyWithin}.\n * - `viewport` (default): the page's (or AppShell pane's) bottom edge, lifted onto\n * the phone's bottom nav by `--app-nav-h`.\n * - `container`: the bottom of whatever scroll container it is in — a dialog body, a\n * side pane, a panel with `overflow-y-auto` — at `bottom: 0`, with no nav offset.\n */\nexport type FormActionsStickyWithin = \"viewport\" | \"container\";\n\n/**\n * Extra attributes for the save button — an `id`, `data-*` hooks for tests or\n * analytics, an `aria-describedby`. The props FormActions drives itself (type, click,\n * disabled, variant, form, children, the busy state) are not among them.\n */\nexport type FormActionsSubmitProps = Omit<\n ButtonProps,\n \"type\" | \"onClick\" | \"disabled\" | \"disabledReason\" | \"variant\" | \"form\" | \"children\" | \"aria-busy\"\n> & { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** The start-side action of {@link FormActionsProps.destructive}, as data. */\nexport interface FormActionsDestructive {\n label: ReactNode;\n onClick: () => void;\n disabled?: boolean;\n /** See Button's `disabledReason`. */\n disabledReason?: ReactNode;\n}\n\nconst ALIGN_CLASS: Record<FormActionsAlign, string> = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n};\n\nconst PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {\n inline: \"pt-4\",\n sticky: \"sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3\",\n dialog: \"\",\n};\n\nexport interface FormActionsProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** Called by Cancel. Omitted: no Cancel button (a form that only saves). */\n onCancel?: () => void;\n /**\n * Given: the save button is `type=\"button\"` and calls this — for a dialog or a row\n * that saves from a handler. Omitted: it is `type=\"submit\"` and submits the\n * enclosing `<form>` (or the one named by {@link form}).\n */\n onSubmit?: () => void;\n /** The save button's text. Default `form.save` from the provider, else \"Save\". */\n submitLabel?: ReactNode;\n /** The cancel button's text. Default `form.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /** The save button's text while `pending` (\"Saving…\"). Default: `submitLabel`. */\n pendingLabel?: ReactNode;\n /**\n * A Lucide icon before the save button's text. While `pending` the spinner takes\n * ITS place rather than being added in front of it — an icon written into\n * `submitLabel` itself stays, so the button would show the spinner AND the glyph,\n * two statuses side by side (keksdose's crop Apply needed a `pendingLabel` copy of\n * its text only to drop the check mark).\n */\n submitIcon?: LucideIcon;\n /** Attributes passed through to the save button: `id`, `data-*`, `aria-*`, a\n * `className`. See {@link FormActionsSubmitProps}. */\n submitProps?: FormActionsSubmitProps;\n /**\n * The save is running: a spinner in the save button, `aria-busy` on it, and the\n * button disabled, so a second click cannot send the form twice. Cancel stays\n * usable. The spinner goes in front of the text, or in place of {@link submitIcon}.\n */\n pending?: boolean;\n /** Disable save for a reason of the form's own (an unbalanced entry). */\n submitDisabled?: boolean;\n /** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */\n submitDisabledReason?: ReactNode;\n /** The save button's variant. Default `brand`; `danger` for a save that destroys\n * (kastlan's `destructive` flag on its own FormActions). */\n submitVariant?: ButtonVariant;\n /**\n * A destructive action at the START of the row, apart from Save and Cancel — the\n * \"Delete\" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger\n * button, or an element of your own (a {@link DangerConfirm}, say). With it the row\n * is `between`-aligned unless `align` says otherwise.\n */\n destructive?: ReactNode | FormActionsDestructive;\n /**\n * Something neutral at the START of the row — a caption (\"Last saved 2 min ago\",\n * \"Drag the corners to crop\"), a \"View activity\" link. After {@link destructive}\n * when both are given; like it, makes the row `between`-aligned unless `align` says\n * otherwise. `children` are for further ACTIONS, beside Cancel.\n */\n start?: ReactNode;\n /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */\n align?: FormActionsAlign;\n /** See {@link FormActionsPlacement}. Default `inline`. */\n placement?: FormActionsPlacement;\n /**\n * With `placement=\"sticky\"`: what the row sticks to. Default `viewport`.\n *\n * CSS sticky always sticks to the NEAREST scroll container, so the default row\n * already holds inside a scrolling pane — but it offsets itself by `--app-nav-h`,\n * which AppShell publishes on the document: inside a pane or a portalled dialog\n * body on a phone it then floats a nav's height above that container's bottom,\n * with the content scrolling through the gap. `container` drops that offset.\n *\n * What neither value can fix, because it is the layout, not the row:\n * - sticky moves only within its PARENT box. Inside a DataTable expansion row the\n * parent is the expansion cell, so the row pins to the table's own scroller\n * (`overflow-auto`, bounded by `maxBodyHeight`) and only while the cell spans\n * that scroller's bottom edge — not to the page.\n * - any `overflow` other than `visible`/`clip` between the row and the scroller\n * you mean (an `overflow-x-auto` wrapper, an `overflow-hidden` card) becomes the\n * scroll container; if THAT box does not scroll, the row never sticks.\n * - the offset is resolved against the scroller's content box, so a scroller with\n * bottom padding leaves that strip uncovered; give the row a negative `bottom`\n * through `style` and the same amount back as padding.\n */\n stickyWithin?: FormActionsStickyWithin;\n /** The `id` of the `<form>` the save button submits, when the row is rendered\n * outside it (a dialog's footer slot). */\n form?: string;\n /** Extra actions, placed before Cancel. */\n children?: ReactNode;\n}\n\nfunction isDestructiveData(value: unknown): value is FormActionsDestructive {\n return typeof value === \"object\" && value !== null && !isValidElement(value) && \"onClick\" in value;\n}\n\n/**\n * The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's\n * `FormActions`, with the labels from the provider, a pending state and a place for\n * the destructive action.\n *\n * ```tsx\n * <form onSubmit={form.handleSubmit(save)}>\n * …\n * <FormActions onCancel={close} pending={mutation.isPending} />\n * </form>\n * ```\n *\n * The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`\n * so the button's name stays its text; `aria-busy` is what says it is working.\n */\nexport function FormActions({\n onCancel,\n onSubmit,\n submitLabel,\n cancelLabel,\n pendingLabel,\n submitIcon: SubmitIcon,\n submitProps,\n pending = false,\n submitDisabled = false,\n submitDisabledReason,\n submitVariant = \"brand\",\n destructive,\n start: startSlot,\n align,\n placement = \"inline\",\n stickyWithin = \"viewport\",\n form,\n children,\n className,\n style,\n ...rest\n}: FormActionsProps) {\n const labels = useKitLabels(\"form\", DEFAULT_FORM_ACTIONS_LABELS);\n const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;\n const hasStart = startSlot !== undefined && startSlot !== null && startSlot !== false;\n const justify = align ?? (hasDestructive || hasStart ? \"between\" : \"end\");\n const destructiveNode = !hasDestructive ? null : isDestructiveData(destructive) ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n tone=\"danger\"\n onClick={destructive.onClick}\n disabled={destructive.disabled}\n disabledReason={destructive.disabledReason}\n >\n {destructive.label}\n </Button>\n ) : (\n destructive\n );\n const saveLabel = submitLabel ?? labels.save;\n\n return (\n <div\n data-slot=\"form-actions\"\n data-placement={placement}\n data-sticky-within={placement === \"sticky\" ? stickyWithin : undefined}\n {...rest}\n style={\n placement === \"sticky\"\n ? {\n paddingBottom: \"max(0.75rem, env(safe-area-inset-bottom))\",\n // On the phone's bottom nav, not behind it (keksdose live #361): `bottom-0`\n // pinned the row under AppShell's nav, which covers the page's last ~56px.\n // `--app-nav-h` is the nav's measured height (0px from md up, and outside an\n // AppShell); the 1px overlaps the two top borders into one line. In a\n // container the nav is not underneath: `--app-nav-h` is the document's,\n // and would hold the row that far above the container's own edge.\n bottom: stickyWithin === \"container\" ? 0 : \"max(0px, calc(var(--app-nav-h, 0px) - 1px))\",\n ...style,\n }\n : style\n }\n className={cn(\"flex flex-wrap items-center gap-2\", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}\n >\n {(hasDestructive || hasStart) && (\n <div className=\"me-auto flex min-w-0 flex-wrap items-center gap-2\">\n {destructiveNode}\n {hasStart && startSlot}\n </div>\n )}\n <div className=\"flex flex-wrap items-center gap-2\">\n {children}\n {onCancel && (\n <Button type=\"button\" variant=\"secondary\" onClick={onCancel}>\n {cancelLabel ?? labels.cancel}\n </Button>\n )}\n <Button\n {...submitProps}\n type={onSubmit ? \"button\" : \"submit\"}\n form={form}\n onClick={onSubmit}\n variant={submitVariant}\n disabled={pending || submitDisabled}\n disabledReason={pending ? undefined : submitDisabledReason}\n aria-busy={pending || undefined}\n >\n {pending ? (\n <Spinner label={null} className=\"size-4\" />\n ) : (\n SubmitIcon && <SubmitIcon aria-hidden className=\"size-4 shrink-0\" />\n )}\n {pending && pendingLabel !== undefined ? pendingLabel : saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AA6MI,cAuCI,YAvCJ;AA7MJ,SAAS,sBAAsB;AAG/B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,QAAQ,eAAqD;AAQ/D,MAAM,8BAAiD;AAAA,EAC5D,MAAM;AAAA,EACN,QAAQ;AACV;AA2CA,MAAM,cAAgD;AAAA,EACpD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,SAAS;AACX;AAEA,MAAM,kBAAwD;AAAA,EAC5D,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACV;AAwFA,SAAS,kBAAkB,OAAiD;AAC1E,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,eAAe,KAAK,KAAK,aAAa;AAC/F;AAiBO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,YAAY;AAAA,EACZ,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,SAAS,aAAa,QAAQ,2BAA2B;AAC/D,QAAM,iBAAiB,gBAAgB,UAAa,gBAAgB,QAAQ,gBAAgB;AAC5F,QAAM,WAAW,cAAc,UAAa,cAAc,QAAQ,cAAc;AAChF,QAAM,UAAU,UAAU,kBAAkB,WAAW,YAAY;AACnE,QAAM,kBAAkB,CAAC,iBAAiB,OAAO,kBAAkB,WAAW,IAC5E;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,SAAS,YAAY;AAAA,MACrB,UAAU,YAAY;AAAA,MACtB,gBAAgB,YAAY;AAAA,MAE3B,sBAAY;AAAA;AAAA,EACf,IAEA;AAEF,QAAM,YAAY,eAAe,OAAO;AAExC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,kBAAgB;AAAA,MAChB,sBAAoB,cAAc,WAAW,eAAe;AAAA,MAC3D,GAAG;AAAA,MACJ,OACE,cAAc,WACV;AAAA,QACE,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOf,QAAQ,iBAAiB,cAAc,IAAI;AAAA,QAC3C,GAAG;AAAA,MACL,IACA;AAAA,MAEN,WAAW,GAAG,qCAAqC,YAAY,OAAO,GAAG,gBAAgB,SAAS,GAAG,SAAS;AAAA,MAE5G;AAAA,2BAAkB,aAClB,qBAAC,SAAI,WAAU,qDACZ;AAAA;AAAA,UACA,YAAY;AAAA,WACf;AAAA,QAEF,qBAAC,SAAI,WAAU,qCACZ;AAAA;AAAA,UACA,YACC,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,SAAS,UAChD,yBAAe,OAAO,QACzB;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACE,GAAG;AAAA,cACJ,MAAM,WAAW,WAAW;AAAA,cAC5B;AAAA,cACA,SAAS;AAAA,cACT,SAAS;AAAA,cACT,UAAU,WAAW;AAAA,cACrB,gBAAgB,UAAU,SAAY;AAAA,cACtC,aAAW,WAAW;AAAA,cAErB;AAAA,0BACC,oBAAC,WAAQ,OAAO,MAAM,WAAU,UAAS,IAEzC,cAAc,oBAAC,cAAW,eAAW,MAAC,WAAU,mBAAkB;AAAA,gBAEnE,WAAW,iBAAiB,SAAY,eAAe;AAAA;AAAA;AAAA,UAC1D;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/form-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { Button, Spinner, type ButtonProps, type ButtonVariant } from \"./ui\";\n\n/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */\nexport interface FormActionsLabels {\n save: string;\n cancel: string;\n}\n\nexport const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {\n save: \"Save\",\n cancel: \"Cancel\",\n};\n\nexport type FormActionsAlign = \"start\" | \"center\" | \"end\" | \"between\";\n\n/**\n * Where the row sits:\n * - `inline` (default): under the form's last field, `pt-4` above it.\n * - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface\n * colour with a rule above, clear of the phone's home indicator — for a long form\n * whose Save should not be a scroll away.\n * - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,\n * FullBleedDialog's `footer`), which already pads and rules it.\n */\nexport type FormActionsPlacement = \"inline\" | \"sticky\" | \"dialog\";\n\n/**\n * A placement per breakpoint, mobile first — `{ base: \"sticky\", md: \"inline\" }` is a\n * Save row stuck to the bottom of a phone's long form and in the flow under the last\n * field from 768px up, where the form fits. The breakpoints are Tailwind's (sm 640,\n * md 768, lg 1024, xl 1280px), as `BulkActionBar`'s `variant` breakpoints are, and\n * resolved the same way, in JS: the sticky row is positioned by inline style, which a\n * `md:` class cannot reach. keksdose switched `placement` on its own media query at\n * every long form for want of this. The breakpoints are the VIEWPORT's, not the\n * container's: a form in a narrow pane on a wide screen resolves to `md`.\n */\nexport interface ResponsiveFormActionsPlacement {\n base: FormActionsPlacement;\n sm?: FormActionsPlacement;\n md?: FormActionsPlacement;\n lg?: FormActionsPlacement;\n xl?: FormActionsPlacement;\n}\n\nconst BREAKPOINTS = [\n [\"xl\", \"(min-width: 1280px)\"],\n [\"lg\", \"(min-width: 1024px)\"],\n [\"md\", \"(min-width: 768px)\"],\n [\"sm\", \"(min-width: 640px)\"],\n] as const;\n\n/** The placement in force: the widest breakpoint that matches AND names one, else\n * `base`. The queries are subscribed unconditionally (hooks cannot be skipped); a\n * plain string ignores them. Without `matchMedia` (SSR, tests) none match, so the row\n * renders its `base`, the phone's. The same resolution as BulkActionBar's variant. */\nfunction useResolvedPlacement(\n placement: FormActionsPlacement | ResponsiveFormActionsPlacement,\n): FormActionsPlacement {\n const matches = {\n xl: useMediaQuery(BREAKPOINTS[0][1], false),\n lg: useMediaQuery(BREAKPOINTS[1][1], false),\n md: useMediaQuery(BREAKPOINTS[2][1], false),\n sm: useMediaQuery(BREAKPOINTS[3][1], false),\n };\n if (typeof placement === \"string\") return placement;\n for (const [key] of BREAKPOINTS) {\n const p = placement[key];\n if (matches[key] && p !== undefined) return p;\n }\n return placement.base;\n}\n\n/** A CSS length: a number is pixels. */\nfunction cssLength(value: string | number): string {\n return typeof value === \"number\" ? `${value}px` : value;\n}\n\n/**\n * What a `sticky` row sticks to — see {@link FormActionsProps.stickyWithin}.\n * - `viewport` (default): the page's (or AppShell pane's) bottom edge, lifted onto\n * the phone's bottom nav by `--app-nav-h`.\n * - `container`: the bottom of whatever scroll container it is in — a dialog body, a\n * side pane, a panel with `overflow-y-auto` — at `bottom: 0`, with no nav offset.\n */\nexport type FormActionsStickyWithin = \"viewport\" | \"container\";\n\n/**\n * Extra attributes for the save button — an `id`, `data-*` hooks for tests or\n * analytics, an `aria-describedby`. The props FormActions drives itself (type, click,\n * disabled, variant, form, children, the busy state) are not among them.\n */\nexport type FormActionsSubmitProps = Omit<\n ButtonProps,\n \"type\" | \"onClick\" | \"disabled\" | \"disabledReason\" | \"variant\" | \"form\" | \"children\" | \"aria-busy\" | \"commit\"\n> & { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/** The start-side action of {@link FormActionsProps.destructive}, as data. */\nexport interface FormActionsDestructive {\n label: ReactNode;\n onClick: () => void;\n disabled?: boolean;\n /** See Button's `disabledReason`. */\n disabledReason?: ReactNode;\n}\n\nconst ALIGN_CLASS: Record<FormActionsAlign, string> = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n};\n\nconst PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {\n inline: \"pt-4\",\n sticky: \"sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3\",\n dialog: \"\",\n};\n\nexport interface FormActionsProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** Called by Cancel. Omitted: no Cancel button (a form that only saves). */\n onCancel?: () => void;\n /**\n * Given: the save button is `type=\"button\"` and calls this — for a dialog or a row\n * that saves from a handler. Omitted: it is `type=\"submit\"` and submits the\n * enclosing `<form>` (or the one named by {@link form}).\n */\n onSubmit?: () => void;\n /** The save button's text. Default `form.save` from the provider, else \"Save\". */\n submitLabel?: ReactNode;\n /** The cancel button's text. Default `form.cancel` from the provider, else \"Cancel\". */\n cancelLabel?: ReactNode;\n /** The save button's text while `pending` (\"Saving…\"). Default: `submitLabel`. */\n pendingLabel?: ReactNode;\n /**\n * A Lucide icon before the save button's text. While `pending` the spinner takes\n * ITS place rather than being added in front of it — an icon written into\n * `submitLabel` itself stays, so the button would show the spinner AND the glyph,\n * two statuses side by side (keksdose's crop Apply needed a `pendingLabel` copy of\n * its text only to drop the check mark).\n */\n submitIcon?: LucideIcon;\n /** Attributes passed through to the save button: `id`, `data-*`, `aria-*`, a\n * `className`. See {@link FormActionsSubmitProps}. */\n submitProps?: FormActionsSubmitProps;\n /**\n * The save is running: a spinner in the save button, `aria-busy` on it, and the\n * button disabled, so a second click cannot send the form twice. Cancel stays\n * usable. The spinner goes in front of the text, or in place of {@link submitIcon}.\n */\n pending?: boolean;\n /** Disable save for a reason of the form's own (an unbalanced entry). */\n submitDisabled?: boolean;\n /** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */\n submitDisabledReason?: ReactNode;\n /**\n * Save is a commit: under a locked {@link WriteLockProvider} it is disabled with the\n * lock's reason (over `submitDisabledReason`), focusable, the reason in its tooltip —\n * Button's `commit`. The `{ label, onClick }` form of {@link destructive} is a\n * commit too and is locked with it; Cancel and `children` are not (closing a form\n * writes nothing). No provider, or an unlocked one: no effect. Default `false`.\n *\n * keksdose wrote `submitDisabled={lock.locked || …}` and\n * `submitDisabledReason={lock.locked ? lock.reason : undefined}` at each form; this is\n * that pair, read from the provider.\n */\n commit?: boolean;\n /** The save button's variant. Default `brand`; `danger` for a save that destroys\n * (kastlan's `destructive` flag on its own FormActions). */\n submitVariant?: ButtonVariant;\n /**\n * A destructive action at the START of the row, apart from Save and Cancel — the\n * \"Delete\" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger\n * button, or an element of your own (a {@link DangerConfirm}, say). With it the row\n * is `between`-aligned unless `align` says otherwise.\n */\n destructive?: ReactNode | FormActionsDestructive;\n /**\n * Something neutral at the START of the row — a caption (\"Last saved 2 min ago\",\n * \"Drag the corners to crop\"), a \"View activity\" link. After {@link destructive}\n * when both are given; like it, makes the row `between`-aligned unless `align` says\n * otherwise. `children` are for further ACTIONS, beside Cancel.\n */\n start?: ReactNode;\n /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */\n align?: FormActionsAlign;\n /** See {@link FormActionsPlacement}, or one per breakpoint — see\n * {@link ResponsiveFormActionsPlacement}. Default `inline`. */\n placement?: FormActionsPlacement | ResponsiveFormActionsPlacement;\n /**\n * While the row is `sticky`: pull it out over its container's inline padding by this\n * much (a CSS length, a number in px) and give the same back as padding — so the\n * rule above it and its surface run edge to edge, while the buttons stay on the\n * content's line. Without it a sticky row in a padded card or pane is a strip inset\n * from both sides, with the content scrolling past it in the gutters (keksdose wrote\n * `@max-md:-mx-3 @max-md:px-3` by hand). Pass the container's padding (`\"0.75rem\"`,\n * `12`, `\"var(--pane-px)\"`). Applied only while the resolved placement is sticky, so\n * `placement={{ base: \"sticky\", md: \"inline\" }}` bleeds on the phone alone. The margin\n * is `calc(-1 * X)`; jsdom folds that to `calc(-X)`, so match it loosely in tests.\n */\n bleed?: string | number;\n /**\n * With `placement=\"sticky\"`: what the row sticks to. Default `viewport`.\n *\n * CSS sticky always sticks to the NEAREST scroll container, so the default row\n * already holds inside a scrolling pane — but it offsets itself by `--app-nav-h`,\n * which AppShell publishes on the document: inside a pane or a portalled dialog\n * body on a phone it then floats a nav's height above that container's bottom,\n * with the content scrolling through the gap. `container` drops that offset.\n *\n * What neither value can fix, because it is the layout, not the row:\n * - sticky moves only within its PARENT box. Inside a DataTable expansion row the\n * parent is the expansion cell, so the row pins to the table's own scroller\n * (`overflow-auto`, bounded by `maxBodyHeight`) and only while the cell spans\n * that scroller's bottom edge — not to the page.\n * - any `overflow` other than `visible`/`clip` between the row and the scroller\n * you mean (an `overflow-x-auto` wrapper, an `overflow-hidden` card) becomes the\n * scroll container; if THAT box does not scroll, the row never sticks.\n * - the offset is resolved against the scroller's content box, so a scroller with\n * bottom padding leaves that strip uncovered; give the row a negative `bottom`\n * through `style` and the same amount back as padding.\n */\n stickyWithin?: FormActionsStickyWithin;\n /** The `id` of the `<form>` the save button submits, when the row is rendered\n * outside it (a dialog's footer slot). */\n form?: string;\n /** Extra actions, placed before Cancel. */\n children?: ReactNode;\n}\n\nfunction isDestructiveData(value: unknown): value is FormActionsDestructive {\n return typeof value === \"object\" && value !== null && !isValidElement(value) && \"onClick\" in value;\n}\n\n/**\n * The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's\n * `FormActions`, with the labels from the provider, a pending state and a place for\n * the destructive action.\n *\n * ```tsx\n * <form onSubmit={form.handleSubmit(save)}>\n * …\n * <FormActions onCancel={close} pending={mutation.isPending} />\n * </form>\n * ```\n *\n * The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`\n * so the button's name stays its text; `aria-busy` is what says it is working.\n */\nexport function FormActions({\n onCancel,\n onSubmit,\n submitLabel,\n cancelLabel,\n pendingLabel,\n submitIcon: SubmitIcon,\n submitProps,\n pending = false,\n submitDisabled = false,\n submitDisabledReason,\n commit = false,\n submitVariant = \"brand\",\n destructive,\n start: startSlot,\n align,\n placement: placementProp = \"inline\",\n stickyWithin = \"viewport\",\n bleed,\n form,\n children,\n className,\n style,\n ...rest\n}: FormActionsProps) {\n const labels = useKitLabels(\"form\", DEFAULT_FORM_ACTIONS_LABELS);\n const placement = useResolvedPlacement(placementProp);\n const bleedLength = bleed === undefined || bleed === \"\" ? undefined : cssLength(bleed);\n const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;\n const hasStart = startSlot !== undefined && startSlot !== null && startSlot !== false;\n const justify = align ?? (hasDestructive || hasStart ? \"between\" : \"end\");\n const destructiveNode = !hasDestructive ? null : isDestructiveData(destructive) ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n tone=\"danger\"\n onClick={destructive.onClick}\n disabled={destructive.disabled}\n disabledReason={destructive.disabledReason}\n commit={commit}\n >\n {destructive.label}\n </Button>\n ) : (\n destructive\n );\n const saveLabel = submitLabel ?? labels.save;\n\n return (\n <div\n data-slot=\"form-actions\"\n data-placement={placement}\n data-sticky-within={placement === \"sticky\" ? stickyWithin : undefined}\n {...rest}\n style={\n placement === \"sticky\"\n ? {\n paddingBottom: \"max(0.75rem, env(safe-area-inset-bottom))\",\n // On the phone's bottom nav, not behind it (keksdose live #361): `bottom-0`\n // pinned the row under AppShell's nav, which covers the page's last ~56px.\n // `--app-nav-h` is the nav's measured height (0px from md up, and outside an\n // AppShell); the 1px overlaps the two top borders into one line. In a\n // container the nav is not underneath: `--app-nav-h` is the document's,\n // and would hold the row that far above the container's own edge.\n bottom: stickyWithin === \"container\" ? 0 : \"max(0px, calc(var(--app-nav-h, 0px) - 1px))\",\n ...(bleedLength !== undefined && {\n marginInline: `calc(-1 * ${bleedLength})`,\n paddingInline: bleedLength,\n }),\n ...style,\n }\n : style\n }\n className={cn(\"flex flex-wrap items-center gap-2\", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}\n >\n {(hasDestructive || hasStart) && (\n <div className=\"me-auto flex min-w-0 flex-wrap items-center gap-2\">\n {destructiveNode}\n {hasStart && startSlot}\n </div>\n )}\n <div className=\"flex flex-wrap items-center gap-2\">\n {children}\n {onCancel && (\n <Button type=\"button\" variant=\"secondary\" onClick={onCancel}>\n {cancelLabel ?? labels.cancel}\n </Button>\n )}\n <Button\n {...submitProps}\n type={onSubmit ? \"button\" : \"submit\"}\n form={form}\n onClick={onSubmit}\n variant={submitVariant}\n disabled={pending || submitDisabled}\n disabledReason={pending ? undefined : submitDisabledReason}\n // Not while pending: the save already left, and the lock's look over the\n // spinner would say it had not.\n commit={commit && !pending}\n aria-busy={pending || undefined}\n >\n {pending ? (\n <Spinner label={null} className=\"size-4\" />\n ) : (\n SubmitIcon && <SubmitIcon aria-hidden className=\"size-4 shrink-0\" />\n )}\n {pending && pendingLabel !== undefined ? pendingLabel : saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -38,8 +38,10 @@ interface LineItemCellContext<T> {
|
|
|
38
38
|
* `floating`: the field shows its own floating label ({@link LineItemCellContext.fieldLabel}),
|
|
39
39
|
* so the list draws no header for it. */
|
|
40
40
|
type LineItemsFieldLabels = "aria" | "floating";
|
|
41
|
-
/** The colour of a {@link LineItemsSummary}'s value
|
|
42
|
-
|
|
41
|
+
/** The colour of a {@link LineItemsSummary}'s value — a verdict (`success` /
|
|
42
|
+
* `danger`), quiet (`muted`), or the money pair (`income` / `expense`, the colours
|
|
43
|
+
* `SignedAmount` draws a figure in) for a remainder that reads as the row it settles. */
|
|
44
|
+
type LineItemsSummaryTone = "neutral" | "success" | "danger" | "muted" | "income" | "expense";
|
|
43
45
|
/**
|
|
44
46
|
* A line under the lines that says where they stand — a split's remainder ("Left to
|
|
45
47
|
* assign CHF 0.00", green at zero, red otherwise, muted while the total is not known
|
|
@@ -49,16 +51,30 @@ interface LineItemsSummary {
|
|
|
49
51
|
label: ReactNode;
|
|
50
52
|
value?: ReactNode;
|
|
51
53
|
/** The value's colour. Default `neutral`. The label stays neutral: the colour is a
|
|
52
|
-
* second signal, never the only one — the value and label carry the meaning.
|
|
54
|
+
* second signal, never the only one — the value and label carry the meaning.
|
|
55
|
+
* With no `value` the line is a state on its own ("Waiting for the total"), and
|
|
56
|
+
* the tone colours the label instead — `muted` quiets the whole line. */
|
|
53
57
|
tone?: LineItemsSummaryTone;
|
|
58
|
+
/** Beside the value: a chip or badge that names the state ("Balanced" next to a
|
|
59
|
+
* journal entry's zero difference). */
|
|
60
|
+
status?: ReactNode;
|
|
54
61
|
/** A small button at the end of the line. Disabled along with the list's
|
|
55
|
-
* `disabled`.
|
|
62
|
+
* `disabled`. `variant`: `secondary` (default) is a boxed button; `link` a muted
|
|
63
|
+
* text link, for an action that is an aside rather than the way to settle it. */
|
|
56
64
|
action?: {
|
|
57
65
|
label: ReactNode;
|
|
58
66
|
onClick: () => void;
|
|
59
67
|
disabled?: boolean;
|
|
68
|
+
variant?: "secondary" | "link";
|
|
60
69
|
};
|
|
61
70
|
}
|
|
71
|
+
/** Where a row's remove button sits. `row` (default): stacked, a row of the card to
|
|
72
|
+
* itself (paired, the whole width); wide, its own track. `inline`: stacked, beside
|
|
73
|
+
* the row's last field ("category + amount" over "memo + delete"); wide, unchanged. */
|
|
74
|
+
type LineItemsRemovePlacement = "row" | "inline";
|
|
75
|
+
/** How the remove button sits in the height of a wide row: at its top, centred, or at
|
|
76
|
+
* its foot. */
|
|
77
|
+
type LineItemsRemoveAlign = "start" | "center" | "end";
|
|
62
78
|
interface LineItemsColumn<T> {
|
|
63
79
|
/** Stable id: the React key, and the key into `totals`. */
|
|
64
80
|
key: string;
|
|
@@ -129,6 +145,14 @@ interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "child
|
|
|
129
145
|
* them up (a column's `narrowSpan={2}` takes a whole row), once the pane is wide
|
|
130
146
|
* enough for two fields (18rem); narrower still, one. Default `1`. */
|
|
131
147
|
narrowColumns?: 1 | 2;
|
|
148
|
+
/** Stacked, where the remove button goes: a row of its own (`row`, default) or
|
|
149
|
+
* beside the last field (`inline`). See {@link LineItemsRemovePlacement}. */
|
|
150
|
+
removePlacement?: LineItemsRemovePlacement;
|
|
151
|
+
/** On the wide grid, the remove button's place in the row's height. Default `start`
|
|
152
|
+
* — or `center` when a column floats its label (`fieldLabels="floating"`): a
|
|
153
|
+
* floating field is taller than the button, and centred is level with its control.
|
|
154
|
+
* A field that shows a message under itself grows the row; pin `start` then. */
|
|
155
|
+
removeAlign?: LineItemsRemoveAlign;
|
|
132
156
|
/** Disables add and remove (the fields are the caller's to disable). */
|
|
133
157
|
disabled?: boolean;
|
|
134
158
|
labels?: Partial<LineItemsLabels>;
|
|
@@ -160,6 +184,6 @@ interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "child
|
|
|
160
184
|
* the row that took its place (else the add button). Ctrl/⌘+Enter in any row adds a
|
|
161
185
|
* row.
|
|
162
186
|
*/
|
|
163
|
-
declare function LineItems<T>({ items, columns, getKey, onAdd, onRemove, minItems, maxItems, confirmRemove, addLabel, rowLabel, totals, totalsLabel, empty, error, summary, fieldLabels, narrowColumns, disabled, labels: labelsProp, className, style, ...rest }: LineItemsProps<T>): react.JSX.Element;
|
|
187
|
+
declare function LineItems<T>({ items, columns, getKey, onAdd, onRemove, minItems, maxItems, confirmRemove, addLabel, rowLabel, totals, totalsLabel, empty, error, summary, fieldLabels, narrowColumns, removePlacement, removeAlign: removeAlignProp, disabled, labels: labelsProp, className, style, ...rest }: LineItemsProps<T>): react.JSX.Element;
|
|
164
188
|
|
|
165
|
-
export { DEFAULT_LINE_ITEMS_LABELS, type LineItemCellContext, LineItems, type LineItemsColumn, type LineItemsFieldLabels, type LineItemsLabels, type LineItemsProps, type LineItemsSummary, type LineItemsSummaryTone };
|
|
189
|
+
export { DEFAULT_LINE_ITEMS_LABELS, type LineItemCellContext, LineItems, type LineItemsColumn, type LineItemsFieldLabels, type LineItemsLabels, type LineItemsProps, type LineItemsRemoveAlign, type LineItemsRemovePlacement, type LineItemsSummary, type LineItemsSummaryTone };
|
|
@@ -18,7 +18,15 @@ const SUMMARY_TONE = {
|
|
|
18
18
|
neutral: "text-[var(--text-primary)]",
|
|
19
19
|
success: "text-[var(--success)]",
|
|
20
20
|
danger: "text-[var(--danger)]",
|
|
21
|
-
muted: "text-[var(--text-muted)]"
|
|
21
|
+
muted: "text-[var(--text-muted)]",
|
|
22
|
+
// SignedAmount's money palette (toneTextClass "income" / "expense").
|
|
23
|
+
income: "text-[var(--money-income)]",
|
|
24
|
+
expense: "text-[var(--money-expense)]"
|
|
25
|
+
};
|
|
26
|
+
const REMOVE_ALIGN = {
|
|
27
|
+
start: "@lg:self-start",
|
|
28
|
+
center: "@lg:self-center",
|
|
29
|
+
end: "@lg:self-end"
|
|
22
30
|
};
|
|
23
31
|
const FOCUSABLE = "input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])";
|
|
24
32
|
function defaultKey(item, index) {
|
|
@@ -49,6 +57,8 @@ function LineItems({
|
|
|
49
57
|
summary,
|
|
50
58
|
fieldLabels = "aria",
|
|
51
59
|
narrowColumns = 1,
|
|
60
|
+
removePlacement = "row",
|
|
61
|
+
removeAlign: removeAlignProp,
|
|
52
62
|
disabled = false,
|
|
53
63
|
labels: labelsProp,
|
|
54
64
|
className,
|
|
@@ -108,6 +118,9 @@ function LineItems({
|
|
|
108
118
|
const floats = (column) => (column.fieldLabels ?? fieldLabels) === "floating";
|
|
109
119
|
const showHeader = columns.some((c) => !floats(c));
|
|
110
120
|
const pairs = narrowColumns === 2;
|
|
121
|
+
const removeAlign = removeAlignProp ?? (columns.some(floats) ? "center" : "start");
|
|
122
|
+
const inline = removePlacement === "inline" && Boolean(onRemove) && columns.length > 0;
|
|
123
|
+
const lastColumn = columns[columns.length - 1];
|
|
111
124
|
const headerCell = (column) => /* @__PURE__ */ jsx(
|
|
112
125
|
"span",
|
|
113
126
|
{
|
|
@@ -119,7 +132,44 @@ function LineItems({
|
|
|
119
132
|
},
|
|
120
133
|
column.key
|
|
121
134
|
);
|
|
135
|
+
const removeCell = (index, row, isConfirming) => /* @__PURE__ */ jsx(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
"data-line-items-remove-cell": "",
|
|
139
|
+
className: cn(
|
|
140
|
+
"flex justify-end @lg:justify-center",
|
|
141
|
+
REMOVE_ALIGN[removeAlign],
|
|
142
|
+
// Paired, the remove button keeps a row of its own at the card's end rather
|
|
143
|
+
// than squeezing in beside the last field — unless asked to go `inline`.
|
|
144
|
+
inline ? "shrink-0" : pairs && "@2xs:col-span-2 @lg:col-span-1"
|
|
145
|
+
),
|
|
146
|
+
children: /* @__PURE__ */ jsx(
|
|
147
|
+
IconButton,
|
|
148
|
+
{
|
|
149
|
+
type: "button",
|
|
150
|
+
"data-line-items-remove": "",
|
|
151
|
+
variant: isConfirming ? "danger" : "ghost",
|
|
152
|
+
tone: isConfirming ? void 0 : "danger",
|
|
153
|
+
label: isConfirming ? labels.confirmRemove(row) : labels.remove(row),
|
|
154
|
+
disabled: !canRemove,
|
|
155
|
+
onClick: () => void removeAt(index),
|
|
156
|
+
onBlur: () => {
|
|
157
|
+
if (isConfirming) setConfirming(null);
|
|
158
|
+
},
|
|
159
|
+
onKeyDown: (e) => {
|
|
160
|
+
if (e.key === "Escape" && isConfirming) {
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
setConfirming(null);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
children: /* @__PURE__ */ jsx(Trash2, {})
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
);
|
|
122
170
|
const showTotals = totals !== void 0;
|
|
171
|
+
const summaryTone = summary?.tone ?? "neutral";
|
|
172
|
+
const summaryHasValue = hasContent(summary?.value);
|
|
123
173
|
const labelColumn = showTotals ? columns.find((c) => !hasContent(totals[c.key]))?.key : void 0;
|
|
124
174
|
return /* @__PURE__ */ jsxs(
|
|
125
175
|
"div",
|
|
@@ -162,13 +212,15 @@ function LineItems({
|
|
|
162
212
|
children: [
|
|
163
213
|
columns.map((column) => {
|
|
164
214
|
const floating = floats(column);
|
|
165
|
-
|
|
215
|
+
const isLast = inline && column === lastColumn;
|
|
216
|
+
const span = pairs && column.narrowSpan === 2 && "@2xs:col-span-2 @lg:col-span-1";
|
|
217
|
+
const cell = /* @__PURE__ */ jsxs(
|
|
166
218
|
"div",
|
|
167
219
|
{
|
|
168
220
|
className: cn(
|
|
169
221
|
"min-w-0 space-y-1 @lg:space-y-0",
|
|
170
222
|
// The wide grid places every cell in its own track again.
|
|
171
|
-
|
|
223
|
+
isLast ? "flex-1" : span
|
|
172
224
|
),
|
|
173
225
|
children: [
|
|
174
226
|
!floating && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "block text-xs font-medium text-[var(--text-muted)] @lg:hidden", children: column.header }),
|
|
@@ -182,31 +234,21 @@ function LineItems({
|
|
|
182
234
|
},
|
|
183
235
|
column.key
|
|
184
236
|
);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
label: isConfirming ? labels.confirmRemove(row) : labels.remove(row),
|
|
196
|
-
disabled: !canRemove,
|
|
197
|
-
onClick: () => void removeAt(index),
|
|
198
|
-
onBlur: () => {
|
|
199
|
-
if (isConfirming) setConfirming(null);
|
|
200
|
-
},
|
|
201
|
-
onKeyDown: (e) => {
|
|
202
|
-
if (e.key === "Escape" && isConfirming) {
|
|
203
|
-
e.stopPropagation();
|
|
204
|
-
setConfirming(null);
|
|
205
|
-
}
|
|
237
|
+
if (!isLast) return cell;
|
|
238
|
+
return /* @__PURE__ */ jsxs(
|
|
239
|
+
"div",
|
|
240
|
+
{
|
|
241
|
+
"data-line-items-inline": "",
|
|
242
|
+
className: cn("flex min-w-0 gap-2 @lg:contents", floating ? "items-center" : "items-end", span),
|
|
243
|
+
children: [
|
|
244
|
+
cell,
|
|
245
|
+
removeCell(index, row, isConfirming)
|
|
246
|
+
]
|
|
206
247
|
},
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
248
|
+
column.key
|
|
249
|
+
);
|
|
250
|
+
}),
|
|
251
|
+
onRemove && !inline && removeCell(index, row, isConfirming)
|
|
210
252
|
]
|
|
211
253
|
},
|
|
212
254
|
getKey(item, index)
|
|
@@ -253,20 +295,22 @@ function LineItems({
|
|
|
253
295
|
"data-line-items-summary": "",
|
|
254
296
|
className: "flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--text-primary)]",
|
|
255
297
|
children: [
|
|
256
|
-
/* @__PURE__ */ jsx(
|
|
257
|
-
hasContent(summary.value) && /* @__PURE__ */ jsx(
|
|
298
|
+
/* @__PURE__ */ jsx(
|
|
258
299
|
"span",
|
|
259
300
|
{
|
|
260
|
-
"data-tone":
|
|
261
|
-
className: cn("
|
|
262
|
-
children: summary.
|
|
301
|
+
"data-tone": summaryHasValue ? void 0 : summaryTone,
|
|
302
|
+
className: cn("min-w-0", !summaryHasValue && SUMMARY_TONE[summaryTone]),
|
|
303
|
+
children: summary.label
|
|
263
304
|
}
|
|
264
305
|
),
|
|
306
|
+
summaryHasValue && /* @__PURE__ */ jsx("span", { "data-tone": summaryTone, className: cn("font-semibold tabular-nums", SUMMARY_TONE[summaryTone]), children: summary.value }),
|
|
307
|
+
hasContent(summary.status) && /* @__PURE__ */ jsx("span", { "data-line-items-summary-status": "", className: "inline-flex", children: summary.status }),
|
|
265
308
|
summary.action && /* @__PURE__ */ jsx(
|
|
266
309
|
Button,
|
|
267
310
|
{
|
|
268
311
|
type: "button",
|
|
269
|
-
variant: "secondary",
|
|
312
|
+
variant: summary.action.variant === "link" ? "link" : "secondary",
|
|
313
|
+
tone: summary.action.variant === "link" ? "muted" : void 0,
|
|
270
314
|
size: "sm",
|
|
271
315
|
className: "ms-auto",
|
|
272
316
|
disabled: disabled || summary.action.disabled,
|