@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,53 @@
|
|
|
1
|
+
import 'react';
|
|
2
|
+
export { N as DEFAULT_FORM_ACTIONS_LABELS, aj as FormActions, ak as FormActionsAlign, al as FormActionsDestructive, am as FormActionsLabels, an as FormActionsPlacement, ao as FormActionsProps } from '../kit-labels-v3biUF1L.js';
|
|
3
|
+
import './data-table-labels.js';
|
|
4
|
+
import './mini-calendar.js';
|
|
5
|
+
import './calendar-heatmap.js';
|
|
6
|
+
import './popover.js';
|
|
7
|
+
import './chip.js';
|
|
8
|
+
import 'lucide-react';
|
|
9
|
+
import './field-sync.js';
|
|
10
|
+
import './tooltip.js';
|
|
11
|
+
import '../hooks/use-anchored-rect.js';
|
|
12
|
+
import '../lib/clipping.js';
|
|
13
|
+
import '../wizard/types.js';
|
|
14
|
+
import '../tour/tour.js';
|
|
15
|
+
import '../search/command-palette.js';
|
|
16
|
+
import '../search/global-search.js';
|
|
17
|
+
import '../search/search-index.js';
|
|
18
|
+
import './month-picker.js';
|
|
19
|
+
import './page-contents.js';
|
|
20
|
+
import './series-chart-labels.js';
|
|
21
|
+
import './pie-chart-labels.js';
|
|
22
|
+
import './sparkline.js';
|
|
23
|
+
import './stat-tile.js';
|
|
24
|
+
import './signature-pad.js';
|
|
25
|
+
import './password-strength.js';
|
|
26
|
+
import './danger-confirm.js';
|
|
27
|
+
import './swatch-picker.js';
|
|
28
|
+
import './tile-radio.js';
|
|
29
|
+
import './icon-picker.js';
|
|
30
|
+
import './dialog-frame.js';
|
|
31
|
+
import './modal.js';
|
|
32
|
+
import '../hooks/use-search-param-state.js';
|
|
33
|
+
import './measured-grid.js';
|
|
34
|
+
import '../feedback/feedback-attachment.js';
|
|
35
|
+
import '../feedback/feedback-dialog.js';
|
|
36
|
+
import '../feedback/feedback-thread.js';
|
|
37
|
+
import '../feedback/feedback-inbox.js';
|
|
38
|
+
import './account-settings-labels.js';
|
|
39
|
+
import './confirm-dialog.js';
|
|
40
|
+
import './floating-panel.js';
|
|
41
|
+
import './bulk-action-bar.js';
|
|
42
|
+
import './list.js';
|
|
43
|
+
import './breadcrumbs.js';
|
|
44
|
+
import './toast.js';
|
|
45
|
+
import './description-list.js';
|
|
46
|
+
import './line-items.js';
|
|
47
|
+
import './progress-bar.js';
|
|
48
|
+
import './signed-amount.js';
|
|
49
|
+
import './error-boundary.js';
|
|
50
|
+
import './authed-image.js';
|
|
51
|
+
import '../hooks/use-authed-src.js';
|
|
52
|
+
import './image-grid.js';
|
|
53
|
+
import './lightbox.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
6
|
+
import { Button, Spinner } from "./ui.js";
|
|
7
|
+
const DEFAULT_FORM_ACTIONS_LABELS = {
|
|
8
|
+
save: "Save",
|
|
9
|
+
cancel: "Cancel"
|
|
10
|
+
};
|
|
11
|
+
const ALIGN_CLASS = {
|
|
12
|
+
start: "justify-start",
|
|
13
|
+
center: "justify-center",
|
|
14
|
+
end: "justify-end",
|
|
15
|
+
between: "justify-between"
|
|
16
|
+
};
|
|
17
|
+
const PLACEMENT_CLASS = {
|
|
18
|
+
inline: "pt-4",
|
|
19
|
+
sticky: "sticky bottom-0 z-10 border-t border-[var(--border)] bg-[var(--bg-surface)] pt-3",
|
|
20
|
+
dialog: ""
|
|
21
|
+
};
|
|
22
|
+
function isDestructiveData(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !isValidElement(value) && "onClick" in value;
|
|
24
|
+
}
|
|
25
|
+
function FormActions({
|
|
26
|
+
onCancel,
|
|
27
|
+
onSubmit,
|
|
28
|
+
submitLabel,
|
|
29
|
+
cancelLabel,
|
|
30
|
+
pendingLabel,
|
|
31
|
+
pending = false,
|
|
32
|
+
submitDisabled = false,
|
|
33
|
+
submitDisabledReason,
|
|
34
|
+
submitVariant = "brand",
|
|
35
|
+
destructive,
|
|
36
|
+
align,
|
|
37
|
+
placement = "inline",
|
|
38
|
+
form,
|
|
39
|
+
children,
|
|
40
|
+
className,
|
|
41
|
+
style,
|
|
42
|
+
...rest
|
|
43
|
+
}) {
|
|
44
|
+
const labels = useKitLabels("form", DEFAULT_FORM_ACTIONS_LABELS);
|
|
45
|
+
const hasDestructive = destructive !== void 0 && destructive !== null && destructive !== false;
|
|
46
|
+
const justify = align ?? (hasDestructive ? "between" : "end");
|
|
47
|
+
const start = !hasDestructive ? null : isDestructiveData(destructive) ? /* @__PURE__ */ jsx(
|
|
48
|
+
Button,
|
|
49
|
+
{
|
|
50
|
+
type: "button",
|
|
51
|
+
variant: "ghost",
|
|
52
|
+
tone: "danger",
|
|
53
|
+
onClick: destructive.onClick,
|
|
54
|
+
disabled: destructive.disabled,
|
|
55
|
+
disabledReason: destructive.disabledReason,
|
|
56
|
+
children: destructive.label
|
|
57
|
+
}
|
|
58
|
+
) : destructive;
|
|
59
|
+
const saveLabel = submitLabel ?? labels.save;
|
|
60
|
+
return /* @__PURE__ */ jsxs(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
"data-slot": "form-actions",
|
|
64
|
+
"data-placement": placement,
|
|
65
|
+
...rest,
|
|
66
|
+
style: placement === "sticky" ? { paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))", ...style } : style,
|
|
67
|
+
className: cn("flex flex-wrap items-center gap-2", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className),
|
|
68
|
+
children: [
|
|
69
|
+
start && /* @__PURE__ */ jsx("div", { className: "me-auto flex items-center gap-2", children: start }),
|
|
70
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
71
|
+
children,
|
|
72
|
+
onCancel && /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: onCancel, children: cancelLabel ?? labels.cancel }),
|
|
73
|
+
/* @__PURE__ */ jsxs(
|
|
74
|
+
Button,
|
|
75
|
+
{
|
|
76
|
+
type: onSubmit ? "button" : "submit",
|
|
77
|
+
form,
|
|
78
|
+
onClick: onSubmit,
|
|
79
|
+
variant: submitVariant,
|
|
80
|
+
disabled: pending || submitDisabled,
|
|
81
|
+
disabledReason: pending ? void 0 : submitDisabledReason,
|
|
82
|
+
"aria-busy": pending || void 0,
|
|
83
|
+
children: [
|
|
84
|
+
pending && /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-4" }),
|
|
85
|
+
pending && pendingLabel !== void 0 ? pendingLabel : saveLabel
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
] })
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
DEFAULT_FORM_ACTIONS_LABELS,
|
|
96
|
+
FormActions
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=form-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/form-actions.tsx"],"sourcesContent":["import { isValidElement } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, Spinner, 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/** 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 * 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.\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 /** Horizontal alignment. Default `end` (`between` with a `destructive` action). */\n align?: FormActionsAlign;\n /** See {@link FormActionsPlacement}. Default `inline`. */\n placement?: FormActionsPlacement;\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 pending = false,\n submitDisabled = false,\n submitDisabledReason,\n submitVariant = \"brand\",\n destructive,\n align,\n placement = \"inline\",\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 justify = align ?? (hasDestructive ? \"between\" : \"end\");\n const start = !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 {...rest}\n style={\n placement === \"sticky\"\n ? { paddingBottom: \"max(0.75rem, env(safe-area-inset-bottom))\", ...style }\n : style\n }\n className={cn(\"flex flex-wrap items-center gap-2\", ALIGN_CLASS[justify], PLACEMENT_CLASS[placement], className)}\n >\n {start && <div className=\"me-auto flex items-center gap-2\">{start}</div>}\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 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 && <Spinner label={null} className=\"size-4\" />}\n {pending && pendingLabel !== undefined ? pendingLabel : saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AA4II,cAmCI,YAnCJ;AA5IJ,SAAS,sBAAsB;AAE/B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,QAAQ,eAAmC;AAQ7C,MAAM,8BAAiD;AAAA,EAC5D,MAAM;AAAA,EACN,QAAQ;AACV;AAwBA,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;AAgDA,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,UAAU;AAAA,EACV,iBAAiB;AAAA,EACjB;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;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,UAAU,UAAU,iBAAiB,YAAY;AACvD,QAAM,QAAQ,CAAC,iBAAiB,OAAO,kBAAkB,WAAW,IAClE;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,MACf,GAAG;AAAA,MACJ,OACE,cAAc,WACV,EAAE,eAAe,6CAA6C,GAAG,MAAM,IACvE;AAAA,MAEN,WAAW,GAAG,qCAAqC,YAAY,OAAO,GAAG,gBAAgB,SAAS,GAAG,SAAS;AAAA,MAE7G;AAAA,iBAAS,oBAAC,SAAI,WAAU,mCAAmC,iBAAM;AAAA,QAClE,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,cACC,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,2BAAW,oBAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,gBACpD,WAAW,iBAAiB,SAAY,eAAe;AAAA;AAAA;AAAA,UAC1D;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { AuthedFetcher } from '../hooks/use-authed-src.js';
|
|
4
|
+
import { ImageItem, LightboxLabels } from './lightbox.js';
|
|
5
|
+
|
|
6
|
+
interface ImageGridLabels {
|
|
7
|
+
/** The list's accessible name. */
|
|
8
|
+
list: string;
|
|
9
|
+
/** An open button's name when an item's `alt` is empty: "Image 3 of 7". */
|
|
10
|
+
item: (index: number, count: number) => string;
|
|
11
|
+
/** The name of the group of per-image actions, given the image's name. */
|
|
12
|
+
actions: (name: string) => string;
|
|
13
|
+
}
|
|
14
|
+
declare const DEFAULT_IMAGE_GRID_LABELS: ImageGridLabels;
|
|
15
|
+
interface ImageGridProps extends Omit<ComponentPropsWithoutRef<"ul">, "children"> {
|
|
16
|
+
items: readonly ImageItem[];
|
|
17
|
+
/** The app's authenticated GET — thumbnails and the lightbox fetch through it. Only
|
|
18
|
+
* images are fetched for the grid; a file (PDF) tile shows its icon and name. */
|
|
19
|
+
fetcher?: AuthedFetcher | null;
|
|
20
|
+
/**
|
|
21
|
+
* Per-image actions — a delete, an edit. Rendered BESIDE the open button (on top of
|
|
22
|
+
* the thumbnail's corner), never inside it: a button inside a button is invalid HTML
|
|
23
|
+
* and a screen reader flattens it into the outer one's name, where it cannot be
|
|
24
|
+
* reached. Use `IconButton variant="overlay"` for controls that sit on a photo.
|
|
25
|
+
*/
|
|
26
|
+
renderActions?: (item: ImageItem, index: number) => ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* What a tile's click does. Left out, the grid opens its own {@link Lightbox} on that
|
|
29
|
+
* image. Given, the grid opens nothing — for a caller that keeps the lightbox's
|
|
30
|
+
* index in the URL, or that opens something else.
|
|
31
|
+
*/
|
|
32
|
+
onOpen?: (index: number) => void;
|
|
33
|
+
/**
|
|
34
|
+
* A fixed column count. Left out, the grid is responsive: as many columns of at
|
|
35
|
+
* least {@link minTileSize} px as fit, so the same grid is three across in a card on
|
|
36
|
+
* a phone and eight across on a desktop page — no breakpoints to choose.
|
|
37
|
+
*/
|
|
38
|
+
columns?: number;
|
|
39
|
+
/** The smallest a tile may be in the responsive layout. Default 96 (px). */
|
|
40
|
+
minTileSize?: number;
|
|
41
|
+
/** `square` (default) crops to a square; `video` to 16:9; `auto` keeps each image's
|
|
42
|
+
* own proportions (a floor plan), within a square-ish box. */
|
|
43
|
+
aspect?: "square" | "video" | "auto";
|
|
44
|
+
/** Props for the built-in lightbox (ignored with {@link onOpen}). */
|
|
45
|
+
lightbox?: {
|
|
46
|
+
loop?: boolean;
|
|
47
|
+
zoom?: boolean;
|
|
48
|
+
download?: boolean;
|
|
49
|
+
onDownload?: (item: ImageItem, index: number) => void;
|
|
50
|
+
labels?: Partial<LightboxLabels>;
|
|
51
|
+
};
|
|
52
|
+
labels?: Partial<ImageGridLabels>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A grid of thumbnails that open in a {@link Lightbox} — kastlan's room and defect
|
|
56
|
+
* photos (room-inspector, unit-rooms-tab) and its floor plans (unit-floor-plan), each
|
|
57
|
+
* of which drew its own `grid grid-cols-N` of `<img>` that nothing could open, with no
|
|
58
|
+
* loading state and nowhere to put a delete.
|
|
59
|
+
*
|
|
60
|
+
* Each tile is a real `<button>` named by the image's alt, with the actions beside it
|
|
61
|
+
* and the caption below it — outside the button, so the name stays the alt and the
|
|
62
|
+
* caption is read once. Placeholders come from {@link AuthedImage}: a skeleton while
|
|
63
|
+
* loading, a "couldn't load" tile on failure.
|
|
64
|
+
*/
|
|
65
|
+
declare function ImageGrid({ items, fetcher, renderActions, onOpen, columns, minTileSize, aspect, lightbox, labels: labelsProp, className, style, ...rest }: ImageGridProps): react.JSX.Element;
|
|
66
|
+
|
|
67
|
+
export { DEFAULT_IMAGE_GRID_LABELS, ImageGrid, type ImageGridLabels, type ImageGridProps };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { FileText } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
7
|
+
import { AuthedImage } from "./authed-image.js";
|
|
8
|
+
import { Lightbox, imageItemKind } from "./lightbox.js";
|
|
9
|
+
const DEFAULT_IMAGE_GRID_LABELS = {
|
|
10
|
+
list: "Images",
|
|
11
|
+
item: (index, count) => `Image ${index} of ${count}`,
|
|
12
|
+
actions: (name) => `Actions for ${name}`
|
|
13
|
+
};
|
|
14
|
+
const ASPECT_CLASS = {
|
|
15
|
+
square: "aspect-square",
|
|
16
|
+
video: "aspect-video",
|
|
17
|
+
auto: "aspect-[4/3]"
|
|
18
|
+
};
|
|
19
|
+
function ImageGrid({
|
|
20
|
+
items,
|
|
21
|
+
fetcher,
|
|
22
|
+
renderActions,
|
|
23
|
+
onOpen,
|
|
24
|
+
columns,
|
|
25
|
+
minTileSize = 96,
|
|
26
|
+
aspect = "square",
|
|
27
|
+
lightbox,
|
|
28
|
+
labels: labelsProp,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
...rest
|
|
32
|
+
}) {
|
|
33
|
+
const labels = useKitLabels("imageGrid", DEFAULT_IMAGE_GRID_LABELS, labelsProp);
|
|
34
|
+
const [openIndex, setOpenIndex] = useState(null);
|
|
35
|
+
const count = items.length;
|
|
36
|
+
const gridStyle = {
|
|
37
|
+
gridTemplateColumns: columns !== void 0 ? `repeat(${columns}, minmax(0, 1fr))` : `repeat(auto-fill, minmax(min(${minTileSize}px, 100%), 1fr))`,
|
|
38
|
+
...style
|
|
39
|
+
};
|
|
40
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
41
|
+
/* @__PURE__ */ jsx(
|
|
42
|
+
"ul",
|
|
43
|
+
{
|
|
44
|
+
"aria-label": labels.list,
|
|
45
|
+
...rest,
|
|
46
|
+
className: cn("grid gap-2", className),
|
|
47
|
+
style: gridStyle,
|
|
48
|
+
children: items.map((item, i) => {
|
|
49
|
+
const name = item.alt || labels.item(i + 1, count);
|
|
50
|
+
const kind = imageItemKind(item);
|
|
51
|
+
const actions = renderActions?.(item, i);
|
|
52
|
+
return /* @__PURE__ */ jsxs("li", { className: "relative min-w-0", children: [
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"button",
|
|
55
|
+
{
|
|
56
|
+
type: "button",
|
|
57
|
+
"aria-label": name,
|
|
58
|
+
onClick: () => onOpen ? onOpen(i) : setOpenIndex(i),
|
|
59
|
+
className: cn(
|
|
60
|
+
"block w-full overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-hover)]",
|
|
61
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
62
|
+
ASPECT_CLASS[aspect]
|
|
63
|
+
),
|
|
64
|
+
children: kind === "file" ? /* @__PURE__ */ jsxs("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)]", children: [
|
|
65
|
+
/* @__PURE__ */ jsx(FileText, { "aria-hidden": true, className: "size-6 shrink-0 text-[var(--text-muted)]" }),
|
|
66
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "line-clamp-2 break-all", children: item.fileName ?? item.alt })
|
|
67
|
+
] }) : /* @__PURE__ */ jsx(
|
|
68
|
+
AuthedImage,
|
|
69
|
+
{
|
|
70
|
+
src: item.thumbnailSrc ?? item.src,
|
|
71
|
+
alt: "",
|
|
72
|
+
fetcher,
|
|
73
|
+
loading: "lazy",
|
|
74
|
+
wrapperClassName: "h-full w-full",
|
|
75
|
+
className: cn("h-full w-full", aspect === "auto" ? "object-contain" : "object-cover")
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
actions !== void 0 && actions !== null && actions !== false && /* @__PURE__ */ jsx(
|
|
81
|
+
"div",
|
|
82
|
+
{
|
|
83
|
+
role: "group",
|
|
84
|
+
"aria-label": labels.actions(name),
|
|
85
|
+
className: "absolute end-1 top-1 flex gap-1",
|
|
86
|
+
children: actions
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
item.caption !== void 0 && item.caption !== null && item.caption !== false && /* @__PURE__ */ jsx("div", { className: "mt-1 line-clamp-2 text-xs text-[var(--text-secondary)]", children: item.caption })
|
|
90
|
+
] }, item.key ?? item.src);
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
!onOpen && /* @__PURE__ */ jsx(
|
|
95
|
+
Lightbox,
|
|
96
|
+
{
|
|
97
|
+
open: openIndex !== null,
|
|
98
|
+
onClose: () => setOpenIndex(null),
|
|
99
|
+
items,
|
|
100
|
+
index: openIndex ?? 0,
|
|
101
|
+
onIndexChange: setOpenIndex,
|
|
102
|
+
fetcher,
|
|
103
|
+
...lightbox
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
] });
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
DEFAULT_IMAGE_GRID_LABELS,
|
|
110
|
+
ImageGrid
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=image-grid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/image-grid.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from \"react\";\nimport { FileText } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { AuthedImage } from \"./authed-image\";\nimport { Lightbox, imageItemKind } from \"./lightbox\";\nimport type { ImageItem, LightboxLabels } from \"./lightbox\";\n\nexport interface ImageGridLabels {\n /** The list's accessible name. */\n list: string;\n /** An open button's name when an item's `alt` is empty: \"Image 3 of 7\". */\n item: (index: number, count: number) => string;\n /** The name of the group of per-image actions, given the image's name. */\n actions: (name: string) => string;\n}\n\nexport const DEFAULT_IMAGE_GRID_LABELS: ImageGridLabels = {\n list: \"Images\",\n item: (index, count) => `Image ${index} of ${count}`,\n actions: (name) => `Actions for ${name}`,\n};\n\nexport interface ImageGridProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"children\"> {\n items: readonly ImageItem[];\n /** The app's authenticated GET — thumbnails and the lightbox fetch through it. Only\n * images are fetched for the grid; a file (PDF) tile shows its icon and name. */\n fetcher?: AuthedFetcher | null;\n /**\n * Per-image actions — a delete, an edit. Rendered BESIDE the open button (on top of\n * the thumbnail's corner), never inside it: a button inside a button is invalid HTML\n * and a screen reader flattens it into the outer one's name, where it cannot be\n * reached. Use `IconButton variant=\"overlay\"` for controls that sit on a photo.\n */\n renderActions?: (item: ImageItem, index: number) => ReactNode;\n /**\n * What a tile's click does. Left out, the grid opens its own {@link Lightbox} on that\n * image. Given, the grid opens nothing — for a caller that keeps the lightbox's\n * index in the URL, or that opens something else.\n */\n onOpen?: (index: number) => void;\n /**\n * A fixed column count. Left out, the grid is responsive: as many columns of at\n * least {@link minTileSize} px as fit, so the same grid is three across in a card on\n * a phone and eight across on a desktop page — no breakpoints to choose.\n */\n columns?: number;\n /** The smallest a tile may be in the responsive layout. Default 96 (px). */\n minTileSize?: number;\n /** `square` (default) crops to a square; `video` to 16:9; `auto` keeps each image's\n * own proportions (a floor plan), within a square-ish box. */\n aspect?: \"square\" | \"video\" | \"auto\";\n /** Props for the built-in lightbox (ignored with {@link onOpen}). */\n lightbox?: {\n loop?: boolean;\n zoom?: boolean;\n download?: boolean;\n onDownload?: (item: ImageItem, index: number) => void;\n labels?: Partial<LightboxLabels>;\n };\n labels?: Partial<ImageGridLabels>;\n}\n\nconst ASPECT_CLASS = {\n square: \"aspect-square\",\n video: \"aspect-video\",\n auto: \"aspect-[4/3]\",\n} as const;\n\n/**\n * A grid of thumbnails that open in a {@link Lightbox} — kastlan's room and defect\n * photos (room-inspector, unit-rooms-tab) and its floor plans (unit-floor-plan), each\n * of which drew its own `grid grid-cols-N` of `<img>` that nothing could open, with no\n * loading state and nowhere to put a delete.\n *\n * Each tile is a real `<button>` named by the image's alt, with the actions beside it\n * and the caption below it — outside the button, so the name stays the alt and the\n * caption is read once. Placeholders come from {@link AuthedImage}: a skeleton while\n * loading, a \"couldn't load\" tile on failure.\n */\nexport function ImageGrid({\n items,\n fetcher,\n renderActions,\n onOpen,\n columns,\n minTileSize = 96,\n aspect = \"square\",\n lightbox,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: ImageGridProps) {\n const labels = useKitLabels(\"imageGrid\", DEFAULT_IMAGE_GRID_LABELS, labelsProp);\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n const count = items.length;\n\n const gridStyle: CSSProperties = {\n gridTemplateColumns:\n columns !== undefined\n ? `repeat(${columns}, minmax(0, 1fr))`\n : `repeat(auto-fill, minmax(min(${minTileSize}px, 100%), 1fr))`,\n ...style,\n };\n\n return (\n <>\n <ul\n aria-label={labels.list}\n {...rest}\n className={cn(\"grid gap-2\", className)}\n style={gridStyle}\n >\n {items.map((item, i) => {\n const name = item.alt || labels.item(i + 1, count);\n const kind = imageItemKind(item);\n const actions = renderActions?.(item, i);\n return (\n <li key={item.key ?? item.src} className=\"relative min-w-0\">\n <button\n type=\"button\"\n aria-label={name}\n onClick={() => (onOpen ? onOpen(i) : setOpenIndex(i))}\n className={cn(\n \"block w-full overflow-hidden rounded-md border border-[var(--border)] bg-[var(--bg-hover)]\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]\",\n ASPECT_CLASS[aspect],\n )}\n >\n {kind === \"file\" ? (\n <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)]\">\n <FileText aria-hidden className=\"size-6 shrink-0 text-[var(--text-muted)]\" />\n <span aria-hidden className=\"line-clamp-2 break-all\">{item.fileName ?? item.alt}</span>\n </span>\n ) : (\n <AuthedImage\n src={item.thumbnailSrc ?? item.src}\n // The button is named already; the picture inside it is decoration\n // to a reader, or the name would be read twice.\n alt=\"\"\n fetcher={fetcher}\n loading=\"lazy\"\n wrapperClassName=\"h-full w-full\"\n className={cn(\"h-full w-full\", aspect === \"auto\" ? \"object-contain\" : \"object-cover\")}\n />\n )}\n </button>\n {actions !== undefined && actions !== null && actions !== false && (\n <div\n role=\"group\"\n aria-label={labels.actions(name)}\n className=\"absolute end-1 top-1 flex gap-1\"\n >\n {actions}\n </div>\n )}\n {item.caption !== undefined && item.caption !== null && item.caption !== false && (\n <div className=\"mt-1 line-clamp-2 text-xs text-[var(--text-secondary)]\">{item.caption}</div>\n )}\n </li>\n );\n })}\n </ul>\n {!onOpen && (\n <Lightbox\n open={openIndex !== null}\n onClose={() => setOpenIndex(null)}\n items={items}\n index={openIndex ?? 0}\n onIndexChange={setOpenIndex}\n fetcher={fetcher}\n {...lightbox}\n />\n )}\n </>\n );\n}\n"],"mappings":";AA8GI,mBAyBgB,KADF,YAxBd;AA9GJ,SAAS,gBAAgB;AAEzB,SAAS,gBAAgB;AAEzB,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAE7B,SAAS,mBAAmB;AAC5B,SAAS,UAAU,qBAAqB;AAYjC,MAAM,4BAA6C;AAAA,EACxD,MAAM;AAAA,EACN,MAAM,CAAC,OAAO,UAAU,SAAS,KAAK,OAAO,KAAK;AAAA,EAClD,SAAS,CAAC,SAAS,eAAe,IAAI;AACxC;AA0CA,MAAM,eAAe;AAAA,EACnB,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AACR;AAaO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,SAAS;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,SAAS,aAAa,aAAa,2BAA2B,UAAU;AAC9E,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,QAAQ,MAAM;AAEpB,QAAM,YAA2B;AAAA,IAC/B,qBACE,YAAY,SACR,UAAU,OAAO,sBACjB,gCAAgC,WAAW;AAAA,IACjD,GAAG;AAAA,EACL;AAEA,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,cAAY,OAAO;AAAA,QAClB,GAAG;AAAA,QACJ,WAAW,GAAG,cAAc,SAAS;AAAA,QACrC,OAAO;AAAA,QAEN,gBAAM,IAAI,CAAC,MAAM,MAAM;AACtB,gBAAM,OAAO,KAAK,OAAO,OAAO,KAAK,IAAI,GAAG,KAAK;AACjD,gBAAM,OAAO,cAAc,IAAI;AAC/B,gBAAM,UAAU,gBAAgB,MAAM,CAAC;AACvC,iBACE,qBAAC,QAA8B,WAAU,oBACvC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY;AAAA,gBACZ,SAAS,MAAO,SAAS,OAAO,CAAC,IAAI,aAAa,CAAC;AAAA,gBACnD,WAAW;AAAA,kBACT;AAAA,kBACA;AAAA,kBACA,aAAa,MAAM;AAAA,gBACrB;AAAA,gBAEC,mBAAS,SACR,qBAAC,UAAK,WAAU,sHACd;AAAA,sCAAC,YAAS,eAAW,MAAC,WAAU,4CAA2C;AAAA,kBAC3E,oBAAC,UAAK,eAAW,MAAC,WAAU,0BAA0B,eAAK,YAAY,KAAK,KAAI;AAAA,mBAClF,IAEA;AAAA,kBAAC;AAAA;AAAA,oBACC,KAAK,KAAK,gBAAgB,KAAK;AAAA,oBAG/B,KAAI;AAAA,oBACJ;AAAA,oBACA,SAAQ;AAAA,oBACR,kBAAiB;AAAA,oBACjB,WAAW,GAAG,iBAAiB,WAAW,SAAS,mBAAmB,cAAc;AAAA;AAAA,gBACtF;AAAA;AAAA,YAEJ;AAAA,YACC,YAAY,UAAa,YAAY,QAAQ,YAAY,SACxD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,cAAY,OAAO,QAAQ,IAAI;AAAA,gBAC/B,WAAU;AAAA,gBAET;AAAA;AAAA,YACH;AAAA,YAED,KAAK,YAAY,UAAa,KAAK,YAAY,QAAQ,KAAK,YAAY,SACvE,oBAAC,SAAI,WAAU,0DAA0D,eAAK,SAAQ;AAAA,eAvCjF,KAAK,OAAO,KAAK,GAyC1B;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH;AAAA,IACC,CAAC,UACA;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,cAAc;AAAA,QACpB,SAAS,MAAM,aAAa,IAAI;AAAA,QAChC;AAAA,QACA,OAAO,aAAa;AAAA,QACpB,eAAe;AAAA,QACf;AAAA,QACC,GAAG;AAAA;AAAA,IACN;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { AuthedFetcher } from '../hooks/use-authed-src.js';
|
|
4
|
+
|
|
5
|
+
/** `image`: shown as a picture. `file`: anything a browser cannot put in an `<img>` —
|
|
6
|
+
* a floor plan uploaded as a PDF — shown as a file card with open and download. */
|
|
7
|
+
type ImageItemKind = "image" | "file";
|
|
8
|
+
interface ImageItem {
|
|
9
|
+
/** React key. Defaults to `src`. */
|
|
10
|
+
key?: string | number;
|
|
11
|
+
/** The full-size file — what the lightbox shows and downloads. An API path when the
|
|
12
|
+
* component is given a `fetcher`. */
|
|
13
|
+
src: string;
|
|
14
|
+
/** A smaller rendition for the grid, when the server has one. Default: `src`. */
|
|
15
|
+
thumbnailSrc?: string;
|
|
16
|
+
/** Required: the picture's text alternative, and the open button's name. */
|
|
17
|
+
alt: string;
|
|
18
|
+
/** Shown under the thumbnail and at the foot of the lightbox. */
|
|
19
|
+
caption?: ReactNode;
|
|
20
|
+
/** The file's MIME type, when known (`"application/pdf"`). Decides {@link kind}. */
|
|
21
|
+
mimeType?: string;
|
|
22
|
+
/** Say it outright. Default: from {@link mimeType}, else from the file extension in
|
|
23
|
+
* `src`, else — for a fetched file — from the type the server sent back. */
|
|
24
|
+
kind?: ImageItemKind;
|
|
25
|
+
/** The name a download is saved under, and a file card's heading. */
|
|
26
|
+
fileName?: string;
|
|
27
|
+
}
|
|
28
|
+
/** What an item is, from what is known about it WITHOUT fetching it — see
|
|
29
|
+
* {@link ImageItem.kind}. `fetchedType` is the blob's type once it is in. */
|
|
30
|
+
declare function imageItemKind(item: ImageItem, fetchedType?: string): ImageItemKind;
|
|
31
|
+
interface LightboxLabels {
|
|
32
|
+
/** The dialog's accessible name. */
|
|
33
|
+
dialog: string;
|
|
34
|
+
close: string;
|
|
35
|
+
previous: string;
|
|
36
|
+
next: string;
|
|
37
|
+
/** The visible counter: "3 / 7". */
|
|
38
|
+
counter: (index: number, count: number) => string;
|
|
39
|
+
/** What a screen reader hears on each page turn: "Image 3 of 7". The alt follows. */
|
|
40
|
+
position: (index: number, count: number) => string;
|
|
41
|
+
download: string;
|
|
42
|
+
/** The zoom toggle (a pressed/not-pressed button — its name does not change). */
|
|
43
|
+
zoom: string;
|
|
44
|
+
/** A file card's note: the file has no picture to show. */
|
|
45
|
+
noPreview: string;
|
|
46
|
+
/** A file card's link to the browser's own viewer (a PDF opens in its viewer). */
|
|
47
|
+
openInNewTab: string;
|
|
48
|
+
}
|
|
49
|
+
declare const DEFAULT_LIGHTBOX_LABELS: LightboxLabels;
|
|
50
|
+
interface LightboxProps {
|
|
51
|
+
open: boolean;
|
|
52
|
+
/** The X, Escape and the Back gesture all call this. */
|
|
53
|
+
onClose: () => void;
|
|
54
|
+
items: readonly ImageItem[];
|
|
55
|
+
/** The item on screen (0-based). Controlled, so the grid — or a URL — can own it. */
|
|
56
|
+
index: number;
|
|
57
|
+
onIndexChange: (index: number) => void;
|
|
58
|
+
/** The app's authenticated GET (see {@link useAuthedSrc}); left out, `src` is
|
|
59
|
+
* loaded by the browser as usual. */
|
|
60
|
+
fetcher?: AuthedFetcher | null;
|
|
61
|
+
/** Previous from the first goes to the last, and next from the last to the first.
|
|
62
|
+
* Default `false`: the buttons at the ends are disabled, which says where the set
|
|
63
|
+
* ends. */
|
|
64
|
+
loop?: boolean;
|
|
65
|
+
/** Double-click (or double-tap) the picture, or the zoom button, to see it at full
|
|
66
|
+
* size and pan by scrolling. Default `true`. Pinch zoom is the browser's own. */
|
|
67
|
+
zoom?: boolean;
|
|
68
|
+
/** A download link in the header. Default `true`. */
|
|
69
|
+
download?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Download through the app instead — kastlan's floor plans have `openDocument(id)`,
|
|
72
|
+
* which knows the stored file name. Replaces the link with a button that calls this.
|
|
73
|
+
*/
|
|
74
|
+
onDownload?: (item: ImageItem, index: number) => void;
|
|
75
|
+
/** Push a history entry so Back closes the viewer. Default `true`; see
|
|
76
|
+
* `FullBleedDialog`'s `backCloses` for when to turn it off. */
|
|
77
|
+
backCloses?: boolean;
|
|
78
|
+
labels?: Partial<LightboxLabels>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A full-screen viewer for a set of images (and files that are not images).
|
|
82
|
+
*
|
|
83
|
+
* Built on {@link FullBleedDialog} rather than beside it, so the viewer has the kit's
|
|
84
|
+
* one implementation of everything an overlay owes the page: the focus trap, Escape,
|
|
85
|
+
* the Back gesture closing it instead of leaving the page (`useOverlayHistory`), the
|
|
86
|
+
* body-scroll lock and the enter/exit motion.
|
|
87
|
+
*
|
|
88
|
+
* Paging: the previous/next buttons, ←/→ (which swap in a right-to-left page — the
|
|
89
|
+
* direction is read at the point the viewer is rendered, since the dialog itself is
|
|
90
|
+
* portalled out of it), Home/End, and a horizontal swipe on a touch screen.
|
|
91
|
+
*
|
|
92
|
+
* Zoom is a TOGGLE, not a gesture: double-click (or the zoom button) shows the picture
|
|
93
|
+
* at its natural size and the body scrolls to pan. Pinch-to-zoom is not implemented —
|
|
94
|
+
* the browser's own page pinch still works — because a pinch handler would have to own
|
|
95
|
+
* every touch on the picture, and with it the swipe.
|
|
96
|
+
*
|
|
97
|
+
* A non-image (a PDF floor plan) is shown as a file card: its name, "no preview", a
|
|
98
|
+
* link that opens it in the browser's own viewer, and the download. An `<iframe>`
|
|
99
|
+
* preview was the alternative; phone browsers render a PDF in one inconsistently or
|
|
100
|
+
* not at all, and a card works everywhere.
|
|
101
|
+
*/
|
|
102
|
+
declare function Lightbox({ open, onClose, items, index: indexProp, onIndexChange, fetcher, loop, zoom: zoomEnabled, download, onDownload, backCloses, labels: labelsProp, }: LightboxProps): react.JSX.Element;
|
|
103
|
+
|
|
104
|
+
export { DEFAULT_LIGHTBOX_LABELS, type ImageItem, type ImageItemKind, Lightbox, type LightboxLabels, type LightboxProps, imageItemKind };
|