@eifi1/ui-kit 0.11.0 → 0.12.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 +57 -32
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +24 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +102 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +23 -2
- package/dist/components/list.js +32 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +15 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group.js +35 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +215 -23
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +156 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +153 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +153 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +153 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +153 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +153 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +12 -1
- package/dist/shell/topbar-action-menu.js +16 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +224 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +65 -16
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +248 -0
- package/src/components/toggle-group.tsx +58 -10
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/ui.tsx +426 -36
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +102 -8
- package/src/i18n/locales/de.ts +153 -0
- package/src/i18n/locales/es.ts +150 -0
- package/src/i18n/locales/fr.ts +153 -0
- package/src/i18n/locales/hu.ts +151 -0
- package/src/i18n/locales/it.ts +151 -0
- package/src/i18n/locales/zh.ts +150 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +21 -0
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { isValidElement } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { Button, Spinner, type ButtonVariant } from "./ui";
|
|
6
|
+
|
|
7
|
+
/** The words {@link FormActions} renders on its own behalf — the `form` namespace. */
|
|
8
|
+
export interface FormActionsLabels {
|
|
9
|
+
save: string;
|
|
10
|
+
cancel: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_FORM_ACTIONS_LABELS: FormActionsLabels = {
|
|
14
|
+
save: "Save",
|
|
15
|
+
cancel: "Cancel",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FormActionsAlign = "start" | "center" | "end" | "between";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where the row sits:
|
|
22
|
+
* - `inline` (default): under the form's last field, `pt-4` above it.
|
|
23
|
+
* - `sticky`: stuck to the bottom of the scrolling page or pane, on the surface
|
|
24
|
+
* colour with a rule above, clear of the phone's home indicator — for a long form
|
|
25
|
+
* whose Save should not be a scroll away.
|
|
26
|
+
* - `dialog`: no spacing of its own, for a dialog's footer slot (DialogFrame,
|
|
27
|
+
* FullBleedDialog's `footer`), which already pads and rules it.
|
|
28
|
+
*/
|
|
29
|
+
export type FormActionsPlacement = "inline" | "sticky" | "dialog";
|
|
30
|
+
|
|
31
|
+
/** The start-side action of {@link FormActionsProps.destructive}, as data. */
|
|
32
|
+
export interface FormActionsDestructive {
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
/** See Button's `disabledReason`. */
|
|
37
|
+
disabledReason?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ALIGN_CLASS: Record<FormActionsAlign, string> = {
|
|
41
|
+
start: "justify-start",
|
|
42
|
+
center: "justify-center",
|
|
43
|
+
end: "justify-end",
|
|
44
|
+
between: "justify-between",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const PLACEMENT_CLASS: Record<FormActionsPlacement, string> = {
|
|
48
|
+
inline: "pt-4",
|
|
49
|
+
sticky: "sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3",
|
|
50
|
+
dialog: "",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface FormActionsProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
54
|
+
/** Called by Cancel. Omitted: no Cancel button (a form that only saves). */
|
|
55
|
+
onCancel?: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Given: the save button is `type="button"` and calls this — for a dialog or a row
|
|
58
|
+
* that saves from a handler. Omitted: it is `type="submit"` and submits the
|
|
59
|
+
* enclosing `<form>` (or the one named by {@link form}).
|
|
60
|
+
*/
|
|
61
|
+
onSubmit?: () => void;
|
|
62
|
+
/** The save button's text. Default `form.save` from the provider, else "Save". */
|
|
63
|
+
submitLabel?: ReactNode;
|
|
64
|
+
/** The cancel button's text. Default `form.cancel` from the provider, else "Cancel". */
|
|
65
|
+
cancelLabel?: ReactNode;
|
|
66
|
+
/** The save button's text while `pending` ("Saving…"). Default: `submitLabel`. */
|
|
67
|
+
pendingLabel?: ReactNode;
|
|
68
|
+
/**
|
|
69
|
+
* The save is running: a spinner in the save button, `aria-busy` on it, and the
|
|
70
|
+
* button disabled, so a second click cannot send the form twice. Cancel stays
|
|
71
|
+
* usable.
|
|
72
|
+
*/
|
|
73
|
+
pending?: boolean;
|
|
74
|
+
/** Disable save for a reason of the form's own (an unbalanced entry). */
|
|
75
|
+
submitDisabled?: boolean;
|
|
76
|
+
/** Why save is disabled — see Button's `disabledReason`. Keeps it focusable. */
|
|
77
|
+
submitDisabledReason?: ReactNode;
|
|
78
|
+
/** The save button's variant. Default `brand`; `danger` for a save that destroys
|
|
79
|
+
* (kastlan's `destructive` flag on its own FormActions). */
|
|
80
|
+
submitVariant?: ButtonVariant;
|
|
81
|
+
/**
|
|
82
|
+
* A destructive action at the START of the row, apart from Save and Cancel — the
|
|
83
|
+
* "Delete" of an edit dialog. Pass `{ label, onClick }` for the kit's quiet danger
|
|
84
|
+
* button, or an element of your own (a {@link DangerConfirm}, say). With it the row
|
|
85
|
+
* is `between`-aligned unless `align` says otherwise.
|
|
86
|
+
*/
|
|
87
|
+
destructive?: ReactNode | FormActionsDestructive;
|
|
88
|
+
/** Horizontal alignment. Default `end` (`between` with a `destructive` action). */
|
|
89
|
+
align?: FormActionsAlign;
|
|
90
|
+
/** See {@link FormActionsPlacement}. Default `inline`. */
|
|
91
|
+
placement?: FormActionsPlacement;
|
|
92
|
+
/** The `id` of the `<form>` the save button submits, when the row is rendered
|
|
93
|
+
* outside it (a dialog's footer slot). */
|
|
94
|
+
form?: string;
|
|
95
|
+
/** Extra actions, placed before Cancel. */
|
|
96
|
+
children?: ReactNode;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isDestructiveData(value: unknown): value is FormActionsDestructive {
|
|
100
|
+
return typeof value === "object" && value !== null && !isValidElement(value) && "onClick" in value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The Save / Cancel row every form, wizard-step form and dialog ends in — kastlan's
|
|
105
|
+
* `FormActions`, with the labels from the provider, a pending state and a place for
|
|
106
|
+
* the destructive action.
|
|
107
|
+
*
|
|
108
|
+
* ```tsx
|
|
109
|
+
* <form onSubmit={form.handleSubmit(save)}>
|
|
110
|
+
* …
|
|
111
|
+
* <FormActions onCancel={close} pending={mutation.isPending} />
|
|
112
|
+
* </form>
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* The spinner is the kit's {@link Spinner}, drawn inside the button with `label={null}`
|
|
116
|
+
* so the button's name stays its text; `aria-busy` is what says it is working.
|
|
117
|
+
*/
|
|
118
|
+
export function FormActions({
|
|
119
|
+
onCancel,
|
|
120
|
+
onSubmit,
|
|
121
|
+
submitLabel,
|
|
122
|
+
cancelLabel,
|
|
123
|
+
pendingLabel,
|
|
124
|
+
pending = false,
|
|
125
|
+
submitDisabled = false,
|
|
126
|
+
submitDisabledReason,
|
|
127
|
+
submitVariant = "brand",
|
|
128
|
+
destructive,
|
|
129
|
+
align,
|
|
130
|
+
placement = "inline",
|
|
131
|
+
form,
|
|
132
|
+
children,
|
|
133
|
+
className,
|
|
134
|
+
style,
|
|
135
|
+
...rest
|
|
136
|
+
}: FormActionsProps) {
|
|
137
|
+
const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
|
|
138
|
+
const hasDestructive = destructive !== undefined && destructive !== null && destructive !== false;
|
|
139
|
+
const justify = align ?? (hasDestructive ? "between" : "end");
|
|
140
|
+
const start = !hasDestructive ? null : isDestructiveData(destructive) ? (
|
|
141
|
+
<Button
|
|
142
|
+
type="button"
|
|
143
|
+
variant="ghost"
|
|
144
|
+
tone="danger"
|
|
145
|
+
onClick={destructive.onClick}
|
|
146
|
+
disabled={destructive.disabled}
|
|
147
|
+
disabledReason={destructive.disabledReason}
|
|
148
|
+
>
|
|
149
|
+
{destructive.label}
|
|
150
|
+
</Button>
|
|
151
|
+
) : (
|
|
152
|
+
destructive
|
|
153
|
+
);
|
|
154
|
+
const saveLabel = submitLabel ?? labels.save;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
data-slot="form-actions"
|
|
159
|
+
data-placement={placement}
|
|
160
|
+
{...rest}
|
|
161
|
+
style={
|
|
162
|
+
placement === "sticky"
|
|
163
|
+
? { paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))", ...style }
|
|
164
|
+
: style
|
|
165
|
+
}
|
|
166
|
+
className={cn("flex flex-wrap items-center gap-2", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}
|
|
167
|
+
>
|
|
168
|
+
{start && <div className="me-auto flex items-center gap-2">{start}</div>}
|
|
169
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
170
|
+
{children}
|
|
171
|
+
{onCancel && (
|
|
172
|
+
<Button type="button" variant="secondary" onClick={onCancel}>
|
|
173
|
+
{cancelLabel ?? labels.cancel}
|
|
174
|
+
</Button>
|
|
175
|
+
)}
|
|
176
|
+
<Button
|
|
177
|
+
type={onSubmit ? "button" : "submit"}
|
|
178
|
+
form={form}
|
|
179
|
+
onClick={onSubmit}
|
|
180
|
+
variant={submitVariant}
|
|
181
|
+
disabled={pending || submitDisabled}
|
|
182
|
+
disabledReason={pending ? undefined : submitDisabledReason}
|
|
183
|
+
aria-busy={pending || undefined}
|
|
184
|
+
>
|
|
185
|
+
{pending && <Spinner label={null} className="size-4" />}
|
|
186
|
+
{pending && pendingLabel !== undefined ? pendingLabel : saveLabel}
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
|
|
3
|
+
import { FileText } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
7
|
+
import type { AuthedFetcher } from "../hooks/use-authed-src";
|
|
8
|
+
import { AuthedImage } from "./authed-image";
|
|
9
|
+
import { Lightbox, imageItemKind } from "./lightbox";
|
|
10
|
+
import type { ImageItem, LightboxLabels } from "./lightbox";
|
|
11
|
+
|
|
12
|
+
export interface ImageGridLabels {
|
|
13
|
+
/** The list's accessible name. */
|
|
14
|
+
list: string;
|
|
15
|
+
/** An open button's name when an item's `alt` is empty: "Image 3 of 7". */
|
|
16
|
+
item: (index: number, count: number) => string;
|
|
17
|
+
/** The name of the group of per-image actions, given the image's name. */
|
|
18
|
+
actions: (name: string) => string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_IMAGE_GRID_LABELS: ImageGridLabels = {
|
|
22
|
+
list: "Images",
|
|
23
|
+
item: (index, count) => `Image ${index} of ${count}`,
|
|
24
|
+
actions: (name) => `Actions for ${name}`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface ImageGridProps extends Omit<ComponentPropsWithoutRef<"ul">, "children"> {
|
|
28
|
+
items: readonly ImageItem[];
|
|
29
|
+
/** The app's authenticated GET — thumbnails and the lightbox fetch through it. Only
|
|
30
|
+
* images are fetched for the grid; a file (PDF) tile shows its icon and name. */
|
|
31
|
+
fetcher?: AuthedFetcher | null;
|
|
32
|
+
/**
|
|
33
|
+
* Per-image actions — a delete, an edit. Rendered BESIDE the open button (on top of
|
|
34
|
+
* the thumbnail's corner), never inside it: a button inside a button is invalid HTML
|
|
35
|
+
* and a screen reader flattens it into the outer one's name, where it cannot be
|
|
36
|
+
* reached. Use `IconButton variant="overlay"` for controls that sit on a photo.
|
|
37
|
+
*/
|
|
38
|
+
renderActions?: (item: ImageItem, index: number) => ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* What a tile's click does. Left out, the grid opens its own {@link Lightbox} on that
|
|
41
|
+
* image. Given, the grid opens nothing — for a caller that keeps the lightbox's
|
|
42
|
+
* index in the URL, or that opens something else.
|
|
43
|
+
*/
|
|
44
|
+
onOpen?: (index: number) => void;
|
|
45
|
+
/**
|
|
46
|
+
* A fixed column count. Left out, the grid is responsive: as many columns of at
|
|
47
|
+
* least {@link minTileSize} px as fit, so the same grid is three across in a card on
|
|
48
|
+
* a phone and eight across on a desktop page — no breakpoints to choose.
|
|
49
|
+
*/
|
|
50
|
+
columns?: number;
|
|
51
|
+
/** The smallest a tile may be in the responsive layout. Default 96 (px). */
|
|
52
|
+
minTileSize?: number;
|
|
53
|
+
/** `square` (default) crops to a square; `video` to 16:9; `auto` keeps each image's
|
|
54
|
+
* own proportions (a floor plan), within a square-ish box. */
|
|
55
|
+
aspect?: "square" | "video" | "auto";
|
|
56
|
+
/** Props for the built-in lightbox (ignored with {@link onOpen}). */
|
|
57
|
+
lightbox?: {
|
|
58
|
+
loop?: boolean;
|
|
59
|
+
zoom?: boolean;
|
|
60
|
+
download?: boolean;
|
|
61
|
+
onDownload?: (item: ImageItem, index: number) => void;
|
|
62
|
+
labels?: Partial<LightboxLabels>;
|
|
63
|
+
};
|
|
64
|
+
labels?: Partial<ImageGridLabels>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ASPECT_CLASS = {
|
|
68
|
+
square: "aspect-square",
|
|
69
|
+
video: "aspect-video",
|
|
70
|
+
auto: "aspect-[4/3]",
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A grid of thumbnails that open in a {@link Lightbox} — kastlan's room and defect
|
|
75
|
+
* photos (room-inspector, unit-rooms-tab) and its floor plans (unit-floor-plan), each
|
|
76
|
+
* of which drew its own `grid grid-cols-N` of `<img>` that nothing could open, with no
|
|
77
|
+
* loading state and nowhere to put a delete.
|
|
78
|
+
*
|
|
79
|
+
* Each tile is a real `<button>` named by the image's alt, with the actions beside it
|
|
80
|
+
* and the caption below it — outside the button, so the name stays the alt and the
|
|
81
|
+
* caption is read once. Placeholders come from {@link AuthedImage}: a skeleton while
|
|
82
|
+
* loading, a "couldn't load" tile on failure.
|
|
83
|
+
*/
|
|
84
|
+
export function ImageGrid({
|
|
85
|
+
items,
|
|
86
|
+
fetcher,
|
|
87
|
+
renderActions,
|
|
88
|
+
onOpen,
|
|
89
|
+
columns,
|
|
90
|
+
minTileSize = 96,
|
|
91
|
+
aspect = "square",
|
|
92
|
+
lightbox,
|
|
93
|
+
labels: labelsProp,
|
|
94
|
+
className,
|
|
95
|
+
style,
|
|
96
|
+
...rest
|
|
97
|
+
}: ImageGridProps) {
|
|
98
|
+
const labels = useKitLabels("imageGrid", DEFAULT_IMAGE_GRID_LABELS, labelsProp);
|
|
99
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
100
|
+
const count = items.length;
|
|
101
|
+
|
|
102
|
+
const gridStyle: CSSProperties = {
|
|
103
|
+
gridTemplateColumns:
|
|
104
|
+
columns !== undefined
|
|
105
|
+
? `repeat(${columns}, minmax(0, 1fr))`
|
|
106
|
+
: `repeat(auto-fill, minmax(min(${minTileSize}px, 100%), 1fr))`,
|
|
107
|
+
...style,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<ul
|
|
113
|
+
aria-label={labels.list}
|
|
114
|
+
{...rest}
|
|
115
|
+
className={cn("grid gap-2", className)}
|
|
116
|
+
style={gridStyle}
|
|
117
|
+
>
|
|
118
|
+
{items.map((item, i) => {
|
|
119
|
+
const name = item.alt || labels.item(i + 1, count);
|
|
120
|
+
const kind = imageItemKind(item);
|
|
121
|
+
const actions = renderActions?.(item, i);
|
|
122
|
+
return (
|
|
123
|
+
<li key={item.key ?? item.src} className="relative min-w-0">
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
aria-label={name}
|
|
127
|
+
onClick={() => (onOpen ? onOpen(i) : setOpenIndex(i))}
|
|
128
|
+
className={cn(
|
|
129
|
+
"block w-full overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-hover)]",
|
|
130
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
131
|
+
ASPECT_CLASS[aspect],
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
{kind === "file" ? (
|
|
135
|
+
<span className="flex h-full w-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-[var(--text-secondary)]">
|
|
136
|
+
<FileText aria-hidden className="size-6 shrink-0 text-[var(--text-muted)]" />
|
|
137
|
+
<span aria-hidden className="line-clamp-2 break-all">{item.fileName ?? item.alt}</span>
|
|
138
|
+
</span>
|
|
139
|
+
) : (
|
|
140
|
+
<AuthedImage
|
|
141
|
+
src={item.thumbnailSrc ?? item.src}
|
|
142
|
+
// The button is named already; the picture inside it is decoration
|
|
143
|
+
// to a reader, or the name would be read twice.
|
|
144
|
+
alt=""
|
|
145
|
+
fetcher={fetcher}
|
|
146
|
+
loading="lazy"
|
|
147
|
+
wrapperClassName="h-full w-full"
|
|
148
|
+
className={cn("h-full w-full", aspect === "auto" ? "object-contain" : "object-cover")}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</button>
|
|
152
|
+
{actions !== undefined && actions !== null && actions !== false && (
|
|
153
|
+
<div
|
|
154
|
+
role="group"
|
|
155
|
+
aria-label={labels.actions(name)}
|
|
156
|
+
className="absolute end-1 top-1 flex gap-1"
|
|
157
|
+
>
|
|
158
|
+
{actions}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
{item.caption !== undefined && item.caption !== null && item.caption !== false && (
|
|
162
|
+
<div className="mt-1 line-clamp-2 text-xs text-[var(--text-secondary)]">{item.caption}</div>
|
|
163
|
+
)}
|
|
164
|
+
</li>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</ul>
|
|
168
|
+
{!onOpen && (
|
|
169
|
+
<Lightbox
|
|
170
|
+
open={openIndex !== null}
|
|
171
|
+
onClose={() => setOpenIndex(null)}
|
|
172
|
+
items={items}
|
|
173
|
+
index={openIndex ?? 0}
|
|
174
|
+
onIndexChange={setOpenIndex}
|
|
175
|
+
fetcher={fetcher}
|
|
176
|
+
{...lightbox}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
}
|