@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,119 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Check, Minus, X } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels";
|
|
5
|
+
import { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from "./data-table-labels";
|
|
6
|
+
import type { DataTableColumn } from "./data-table";
|
|
7
|
+
|
|
8
|
+
export interface BooleanMarkProps {
|
|
9
|
+
/** `true` → a check, `false` → a cross (or a dash, see `falseAs`), `null` /
|
|
10
|
+
* `undefined` → a dash. */
|
|
11
|
+
value: boolean | null | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* How `false` is drawn. `"cross"` (default) when "no" is news worth seeing — a
|
|
14
|
+
* failed check, a missing document. `"dash"` for a column that is mostly `false` and
|
|
15
|
+
* reads as a list of the `true` ones (kastlan's BoolCell): a column of crosses is
|
|
16
|
+
* noise. The screen-reader text says "No" either way.
|
|
17
|
+
*/
|
|
18
|
+
falseAs?: "cross" | "dash";
|
|
19
|
+
/** The spoken words, over `dataTable.booleanTrue` / `booleanFalse` / `booleanUnset`
|
|
20
|
+
* from `<UiKitProvider labels>`. */
|
|
21
|
+
labels?: Partial<Pick<DataTableLabels, "booleanTrue" | "booleanFalse" | "booleanUnset">>;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A yes/no value in a table cell: a green check, a muted cross, or a faint dash for
|
|
27
|
+
* "not set" — with the word for a screen reader, since a glyph alone is announced as
|
|
28
|
+
* nothing at all (or as "image").
|
|
29
|
+
*
|
|
30
|
+
* kastlan's `BoolCell` (shared/components/data-table/bool-cell.tsx) drew the icons with
|
|
31
|
+
* no text, so a reader walking the table heard an empty cell for every row; the apps'
|
|
32
|
+
* Yes/No chips were the other answer, and too loud for a column of them.
|
|
33
|
+
*/
|
|
34
|
+
export function BooleanMark({ value, falseAs = "cross", labels: labelsProp, className }: BooleanMarkProps) {
|
|
35
|
+
const labels = useKitLabels("dataTable", DEFAULT_DATA_TABLE_LABELS, labelsProp);
|
|
36
|
+
const state = value === true ? "true" : value === false ? "false" : "unset";
|
|
37
|
+
const text =
|
|
38
|
+
state === "true" ? labels.booleanTrue : state === "false" ? labels.booleanFalse : labels.booleanUnset;
|
|
39
|
+
const Icon = state === "true" ? Check : state === "false" && falseAs === "cross" ? X : Minus;
|
|
40
|
+
return (
|
|
41
|
+
<span data-value={state} className={cn("inline-flex items-center align-middle", className)}>
|
|
42
|
+
<Icon
|
|
43
|
+
aria-hidden
|
|
44
|
+
className={cn(
|
|
45
|
+
"size-4",
|
|
46
|
+
state === "true"
|
|
47
|
+
? "text-[var(--success)]"
|
|
48
|
+
: state === "false" && falseAs === "cross"
|
|
49
|
+
? "text-[var(--text-muted)]"
|
|
50
|
+
: "text-[var(--text-placeholder)]",
|
|
51
|
+
)}
|
|
52
|
+
/>
|
|
53
|
+
<span className="sr-only">{text}</span>
|
|
54
|
+
</span>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface BooleanColumnOptions<T> {
|
|
59
|
+
key: string;
|
|
60
|
+
header: ReactNode;
|
|
61
|
+
/** The value of this row. */
|
|
62
|
+
value: (row: T) => boolean | null | undefined;
|
|
63
|
+
/** See {@link BooleanMarkProps.falseAs}. */
|
|
64
|
+
falseAs?: "cross" | "dash";
|
|
65
|
+
/** Sortable, `true` first on the first click (a column is sorted to find the yes
|
|
66
|
+
* rows). Default true. */
|
|
67
|
+
sortable?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* A select filter over the column, with these option labels — pass them translated.
|
|
70
|
+
* Omitted: no filter. The option values are `"true"` / `"false"` (and `"unset"` when
|
|
71
|
+
* `unset` is given), which is also what `filterHref` takes for this column.
|
|
72
|
+
*/
|
|
73
|
+
filterOptions?: { true: string; false: string; unset?: string };
|
|
74
|
+
/** Further column settings (`mobileHidden`, `headClassName`, …), merged over. */
|
|
75
|
+
column?: Partial<DataTableColumn<T>>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A {@link DataTableColumn} for a yes/no field: {@link BooleanMark} cells,
|
|
80
|
+
* sortable with `true` first, and optionally a Yes/No select filter.
|
|
81
|
+
*/
|
|
82
|
+
export function booleanColumn<T>({
|
|
83
|
+
key,
|
|
84
|
+
header,
|
|
85
|
+
value,
|
|
86
|
+
falseAs,
|
|
87
|
+
sortable = true,
|
|
88
|
+
filterOptions,
|
|
89
|
+
column,
|
|
90
|
+
}: BooleanColumnOptions<T>): DataTableColumn<T> {
|
|
91
|
+
const asKey = (row: T) => {
|
|
92
|
+
const v = value(row);
|
|
93
|
+
return v === true ? "true" : v === false ? "false" : "unset";
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
key,
|
|
97
|
+
header,
|
|
98
|
+
cell: (row) => <BooleanMark value={value(row)} falseAs={falseAs} />,
|
|
99
|
+
// 0 for yes, 1 for no, null for unset (which the table sorts last either way).
|
|
100
|
+
sortBy: sortable
|
|
101
|
+
? (row) => {
|
|
102
|
+
const v = value(row);
|
|
103
|
+
return v == null ? null : v ? 0 : 1;
|
|
104
|
+
}
|
|
105
|
+
: undefined,
|
|
106
|
+
filter: filterOptions
|
|
107
|
+
? {
|
|
108
|
+
type: "select",
|
|
109
|
+
getValue: asKey,
|
|
110
|
+
options: [
|
|
111
|
+
{ value: "true", label: filterOptions.true },
|
|
112
|
+
{ value: "false", label: filterOptions.false },
|
|
113
|
+
...(filterOptions.unset !== undefined ? [{ value: "unset", label: filterOptions.unset }] : []),
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
: undefined,
|
|
117
|
+
...column,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -174,3 +174,80 @@ export function decodeFilterValueOfType(type: FilterValue["type"], raw: string):
|
|
|
174
174
|
export function decodeFilterValue<T>(filter: ColumnFilter<T>, raw: string): FilterValue {
|
|
175
175
|
return decodeFilterValueOfType(filter.type, raw);
|
|
176
176
|
}
|
|
177
|
+
|
|
178
|
+
// ---------- Filter builders ----------
|
|
179
|
+
//
|
|
180
|
+
// Terse builders for `DataTableColumn.filter`, lifted from kastlan's column-filters.ts,
|
|
181
|
+
// which every converted list page imports. The one that earns its keep is `selectFilter`:
|
|
182
|
+
// in `serverPagination` mode a select filter MUST declare its options (the table would
|
|
183
|
+
// otherwise derive them from the rows of the page on screen), and the options an app has
|
|
184
|
+
// to hand are usually a translated `{ value: label }` map.
|
|
185
|
+
|
|
186
|
+
/** A free-text filter over `getValue`. */
|
|
187
|
+
export function textFilter<T>(getValue: (row: T) => string): ColumnFilter<T> {
|
|
188
|
+
return { type: "text", getValue };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A multi-select filter. `options` is an options array, or a `{ value: label }` record
|
|
193
|
+
* (a translated status map) — kept in its key order. Left out, the table derives the
|
|
194
|
+
* options from the rows it holds.
|
|
195
|
+
*/
|
|
196
|
+
export function selectFilter<T>(
|
|
197
|
+
getValue: (row: T) => string,
|
|
198
|
+
options?: { value: string; label?: string }[] | Record<string, string>,
|
|
199
|
+
): ColumnFilter<T> {
|
|
200
|
+
const opts = Array.isArray(options)
|
|
201
|
+
? options
|
|
202
|
+
: options
|
|
203
|
+
? Object.entries(options).map(([value, label]) => ({ value, label }))
|
|
204
|
+
: undefined;
|
|
205
|
+
return { type: "select", getValue, options: opts };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A from/to date filter over an ISO date (or date-time) string. */
|
|
209
|
+
export function dateFilter<T>(getValue: (row: T) => string | null | undefined): ColumnFilter<T> {
|
|
210
|
+
return { type: "date", getValue };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A min/max (and absolute-value) filter over a number. */
|
|
214
|
+
export function numberFilter<T>(getValue: (row: T) => number | null | undefined): ColumnFilter<T> {
|
|
215
|
+
return { type: "number", getValue };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A link to a list page with one column filter already applied — in the table's own URL
|
|
220
|
+
* scheme (`f.<column>=<encoded>`), which a table with `urlSync` (or
|
|
221
|
+
* {@link useTableUrlState}) reads on mount. kastlan's `buildListUrl`, which had to
|
|
222
|
+
* restate the `f.` prefix to do it.
|
|
223
|
+
*
|
|
224
|
+
* `value` is a string for a text filter, an array for a select filter, or any
|
|
225
|
+
* {@link FilterValue}. A filter that narrows nothing returns `path` unchanged. A `path`
|
|
226
|
+
* that already carries a query keeps it; a `#fragment` stays at the end.
|
|
227
|
+
*
|
|
228
|
+
* filterHref("/units", "status", ["vacant"]) // "/units?f.status=vacant"
|
|
229
|
+
*/
|
|
230
|
+
export function filterHref(
|
|
231
|
+
path: string,
|
|
232
|
+
column: string,
|
|
233
|
+
value: string | string[] | FilterValue,
|
|
234
|
+
/** The target table's `urlSync` prefix, if it has one (`{ prefix: "inv." }` →
|
|
235
|
+
* `inv.f.<column>`). */
|
|
236
|
+
options?: { prefix?: string },
|
|
237
|
+
): string {
|
|
238
|
+
const state: FilterValue =
|
|
239
|
+
typeof value === "string"
|
|
240
|
+
? { type: "text", q: value }
|
|
241
|
+
: Array.isArray(value)
|
|
242
|
+
? { type: "select", values: value }
|
|
243
|
+
: value;
|
|
244
|
+
const encoded = encodeFilterValue(state);
|
|
245
|
+
if (encoded == null) return path;
|
|
246
|
+
const hashAt = path.indexOf("#");
|
|
247
|
+
const base = hashAt >= 0 ? path.slice(0, hashAt) : path;
|
|
248
|
+
const hash = hashAt >= 0 ? path.slice(hashAt) : "";
|
|
249
|
+
const queryAt = base.indexOf("?");
|
|
250
|
+
const params = new URLSearchParams(queryAt >= 0 ? base.slice(queryAt + 1) : "");
|
|
251
|
+
params.set(`${options?.prefix ?? ""}f.${column}`, encoded);
|
|
252
|
+
return `${queryAt >= 0 ? base.slice(0, queryAt) : base}?${params.toString()}${hash}`;
|
|
253
|
+
}
|
|
@@ -69,6 +69,22 @@ export interface DataTableLabels {
|
|
|
69
69
|
rowCount: (total: number) => string;
|
|
70
70
|
/** The column-settings rail and its heading: "Columns (4/9)". */
|
|
71
71
|
columnsCount: (visible: number, total: number) => string;
|
|
72
|
+
|
|
73
|
+
// ---- Empty state, row actions, boolean cells (0.12.0) ----
|
|
74
|
+
|
|
75
|
+
/** The body of a table with no rows and no `empty` prop of its own. A sentence, not
|
|
76
|
+
* "—": a dash in the only row of a table reads as one row with a missing value. */
|
|
77
|
+
empty: string;
|
|
78
|
+
/** The accessible name of the `rowActions` column (its header is visually empty). */
|
|
79
|
+
actions: string;
|
|
80
|
+
/** The `kind: "edit"` row action's name — its tooltip and its swipe label. */
|
|
81
|
+
edit: string;
|
|
82
|
+
/** The `kind: "delete"` row action's name. */
|
|
83
|
+
delete: string;
|
|
84
|
+
/** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */
|
|
85
|
+
booleanTrue: string;
|
|
86
|
+
booleanFalse: string;
|
|
87
|
+
booleanUnset: string;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
@@ -120,6 +136,13 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
|
|
|
120
136
|
pageRange: (from, to, total) => `${from}–${to} / ${total}`,
|
|
121
137
|
rowCount: (total) => `${total}`,
|
|
122
138
|
columnsCount: (visible, total) => `Columns (${visible}/${total})`,
|
|
139
|
+
empty: "No entries",
|
|
140
|
+
actions: "Actions",
|
|
141
|
+
edit: "Edit",
|
|
142
|
+
delete: "Delete",
|
|
143
|
+
booleanTrue: "Yes",
|
|
144
|
+
booleanFalse: "No",
|
|
145
|
+
booleanUnset: "Not set",
|
|
123
146
|
};
|
|
124
147
|
|
|
125
148
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import {
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
ChevronDown,
|
|
9
9
|
ChevronRight,
|
|
10
10
|
Filter,
|
|
11
|
+
Pencil,
|
|
12
|
+
Trash2,
|
|
11
13
|
X,
|
|
12
14
|
} from "lucide-react";
|
|
13
|
-
import { Card, Spinner } from "./ui";
|
|
15
|
+
import { Card, IconButton, Spinner } from "./ui";
|
|
14
16
|
import { cn } from "../lib/cn";
|
|
15
17
|
import {
|
|
16
18
|
defaultFilterState,
|
|
@@ -239,8 +241,11 @@ export interface DataTableProps<T> {
|
|
|
239
241
|
* shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.
|
|
240
242
|
* URL state takes precedence over localStorage on initial load. Requires the
|
|
241
243
|
* consuming app to render inside a react-router Router.
|
|
244
|
+
*
|
|
245
|
+
* `{ prefix }` namespaces them, for two URL-synced tables on one page:
|
|
246
|
+
* `urlSync={{ prefix: "inv." }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.
|
|
242
247
|
*/
|
|
243
|
-
urlSync?:
|
|
248
|
+
urlSync?: TableUrlSync;
|
|
244
249
|
/**
|
|
245
250
|
* When true (desktop only), the table fills its parent's height and scrolls
|
|
246
251
|
* INTERNALLY, with the header pinned — so the page itself doesn't add a second,
|
|
@@ -431,6 +436,98 @@ export interface DataTableProps<T> {
|
|
|
431
436
|
/** Extra classes for the table's root element (the card, or with `frame={false}`
|
|
432
437
|
* the plain wrapper). Merged last, so a caller's margin or width wins. */
|
|
433
438
|
className?: string;
|
|
439
|
+
/**
|
|
440
|
+
* Per-row actions: an actions column of icon buttons at the row's end on desktop,
|
|
441
|
+
* and the same actions as swipe actions on a phone card (where the column is not
|
|
442
|
+
* shown). kastlan's `ResourceListPage` appended this column by hand to every CRUD
|
|
443
|
+
* list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a
|
|
444
|
+
* `stopPropagation` on each so a press did not also open the row.
|
|
445
|
+
*
|
|
446
|
+
* `kind: "edit"` / `"delete"` bring the icon, the label (`labels.edit` /
|
|
447
|
+
* `labels.delete`), the tone and the swipe side; any other action states its own
|
|
448
|
+
* `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's
|
|
449
|
+
* `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.
|
|
450
|
+
*
|
|
451
|
+
* const confirm = useConfirm();
|
|
452
|
+
* rowActions={[
|
|
453
|
+
* { kind: "edit", onAction: openEditor },
|
|
454
|
+
* { kind: "delete", onAction: (r) => remove(r.id),
|
|
455
|
+
* confirm: (r) => confirm({ title: t("delete", { name: r.name }), tone: "danger" }) },
|
|
456
|
+
* ]}
|
|
457
|
+
*
|
|
458
|
+
* A function receives the row, for actions that differ per row. An explicit
|
|
459
|
+
* `mobileSwipeActions` wins over the swipes derived from here.
|
|
460
|
+
*/
|
|
461
|
+
rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);
|
|
462
|
+
/**
|
|
463
|
+
* The bar above the table — a search field, filter chips, a create button. Inside
|
|
464
|
+
* the table's frame, above the header, on desktop and phone alike (resource-list
|
|
465
|
+
* pages set a search box in a `div` above the card, outside the frame it filters).
|
|
466
|
+
*/
|
|
467
|
+
toolbar?: ReactNode;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** One entry of {@link DataTableProps.rowActions}. */
|
|
471
|
+
export interface DataTableRowAction<T> {
|
|
472
|
+
/** A preset: `"edit"` (pencil, `labels.edit`, swipes toward the END) or `"delete"`
|
|
473
|
+
* (bin, `labels.delete`, danger tone, swipes toward the START). */
|
|
474
|
+
kind?: "edit" | "delete";
|
|
475
|
+
/** The action's name — its tooltip, accessible name and swipe label. Required
|
|
476
|
+
* without a `kind`. */
|
|
477
|
+
label?: string;
|
|
478
|
+
/** The glyph. Required without a `kind`. */
|
|
479
|
+
icon?: ReactNode;
|
|
480
|
+
/** Runs after `confirm` (when given) has said yes. */
|
|
481
|
+
onAction: (row: T) => void;
|
|
482
|
+
/** Asked before `onAction`; a `false` (or a promise of one) cancels. */
|
|
483
|
+
confirm?: (row: T) => boolean | Promise<boolean>;
|
|
484
|
+
/** Default: `"danger"` for `kind: "delete"`, else `"default"`. */
|
|
485
|
+
tone?: "default" | "danger";
|
|
486
|
+
/** Leave the action out for this row. */
|
|
487
|
+
hidden?: (row: T) => boolean;
|
|
488
|
+
/** Why the action is locked for this row — the button stays focusable and says so,
|
|
489
|
+
* and the row gets no swipe for it. See `IconButton`'s `disabledReason`. */
|
|
490
|
+
disabledReason?: (row: T) => ReactNode;
|
|
491
|
+
/** Which way the phone card is dragged to commit it, or `false` for no swipe.
|
|
492
|
+
* Default: `"start"` for a danger action, `"end"` otherwise. */
|
|
493
|
+
swipe?: "start" | "end" | false;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** The actions column's key. Not a column a caller declares, so it is kept out of the
|
|
497
|
+
* column-settings panel. */
|
|
498
|
+
const ROW_ACTIONS_KEY = "__rowActions";
|
|
499
|
+
|
|
500
|
+
interface ResolvedRowAction<T> {
|
|
501
|
+
key: string;
|
|
502
|
+
label: string;
|
|
503
|
+
icon: ReactNode;
|
|
504
|
+
tone: "default" | "danger";
|
|
505
|
+
swipe: "start" | "end" | false;
|
|
506
|
+
action: DataTableRowAction<T>;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function resolveRowAction<T>(
|
|
510
|
+
action: DataTableRowAction<T>,
|
|
511
|
+
index: number,
|
|
512
|
+
labels: DataTableLabels,
|
|
513
|
+
): ResolvedRowAction<T> {
|
|
514
|
+
const tone = action.tone ?? (action.kind === "delete" ? "danger" : "default");
|
|
515
|
+
return {
|
|
516
|
+
key: action.kind ?? `${index}:${action.label ?? ""}`,
|
|
517
|
+
label: action.label ?? (action.kind === "edit" ? labels.edit : action.kind === "delete" ? labels.delete : ""),
|
|
518
|
+
icon:
|
|
519
|
+
action.icon ??
|
|
520
|
+
(action.kind === "edit" ? <Pencil /> : action.kind === "delete" ? <Trash2 /> : null),
|
|
521
|
+
tone,
|
|
522
|
+
swipe: action.swipe ?? (tone === "danger" ? "start" : "end"),
|
|
523
|
+
action,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Ask, then act. */
|
|
528
|
+
async function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {
|
|
529
|
+
if (action.confirm && !(await action.confirm(row))) return;
|
|
530
|
+
action.onAction(row);
|
|
434
531
|
}
|
|
435
532
|
|
|
436
533
|
/** See {@link DataTableProps.chrome}. */
|
|
@@ -516,7 +613,8 @@ function cleanAttrs(
|
|
|
516
613
|
* header is not a string and whose key is not human-readable is a call site that
|
|
517
614
|
* should pass a string header, and that is visible in what gets announced.
|
|
518
615
|
*/
|
|
519
|
-
function columnLabel<T>(col: DataTableColumn<T
|
|
616
|
+
function columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {
|
|
617
|
+
if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
|
|
520
618
|
return typeof col.header === "string" ? col.header : col.key;
|
|
521
619
|
}
|
|
522
620
|
|
|
@@ -528,6 +626,20 @@ function columnLabel<T>(col: DataTableColumn<T>): string {
|
|
|
528
626
|
const OWN_CONTROL =
|
|
529
627
|
'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
530
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Whether a click that reached `row`'s handler did not start in the row's own DOM.
|
|
631
|
+
*
|
|
632
|
+
* React bubbles an event through the COMPONENT tree, portals included: a popover, a
|
|
633
|
+
* menu or a dialog a cell (or the expansion under it) opens renders at `<body>` but
|
|
634
|
+
* still reports its clicks to every React ancestor — the row among them. So a click
|
|
635
|
+
* in a cell's portalled date picker opened the row it came from, and a page wrapped
|
|
636
|
+
* such content in `<div onClick={(e) => e.stopPropagation()}>` to stop it (kastlan's
|
|
637
|
+
* feedback page did, round its comments). The row owns the clicks on the row.
|
|
638
|
+
*/
|
|
639
|
+
function fromOutside(target: EventTarget | null, row: HTMLElement): boolean {
|
|
640
|
+
return !(target instanceof Node) || !row.contains(target);
|
|
641
|
+
}
|
|
642
|
+
|
|
531
643
|
/** Whether an event on `row` started on a control nested inside it. */
|
|
532
644
|
function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
|
|
533
645
|
if (!(target instanceof Element)) return false;
|
|
@@ -617,9 +729,13 @@ function RowLink({
|
|
|
617
729
|
// here happens before any descendant can silence the event; `stopPropagation`
|
|
618
730
|
// does not undo a `preventDefault`.
|
|
619
731
|
onClickCapture={(e) => {
|
|
732
|
+
// A click in a portal a descendant opened is not a click on the link: leave
|
|
733
|
+
// it (and a link inside it) alone. See `fromOutside`.
|
|
734
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
620
735
|
if (isPlainLeftClick(e)) e.preventDefault();
|
|
621
736
|
}}
|
|
622
737
|
onClick={(e) => {
|
|
738
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
623
739
|
if (!isPlainLeftClick(e)) {
|
|
624
740
|
e.stopPropagation();
|
|
625
741
|
return;
|
|
@@ -639,12 +755,12 @@ function RowLink({
|
|
|
639
755
|
|
|
640
756
|
|
|
641
757
|
|
|
642
|
-
import { DEFAULT_PERSIST_PREFIX, useTableState } from "./use-table-state";
|
|
758
|
+
import { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from "./use-table-state";
|
|
643
759
|
import { useMobileReveal } from "./use-mobile-reveal";
|
|
644
760
|
|
|
645
761
|
export function DataTable<T>({
|
|
646
762
|
rows,
|
|
647
|
-
columns,
|
|
763
|
+
columns: columnsProp,
|
|
648
764
|
rowKey,
|
|
649
765
|
defaultPageSize = 25,
|
|
650
766
|
onRowClick,
|
|
@@ -682,6 +798,8 @@ export function DataTable<T>({
|
|
|
682
798
|
multiSort: multiSortProp,
|
|
683
799
|
frame = true,
|
|
684
800
|
className,
|
|
801
|
+
rowActions,
|
|
802
|
+
toolbar,
|
|
685
803
|
}: DataTableProps<T>) {
|
|
686
804
|
const compact = density === "compact";
|
|
687
805
|
const minimal = chrome === "minimal";
|
|
@@ -711,6 +829,74 @@ export function DataTable<T>({
|
|
|
711
829
|
[labelOverrides, labelsProp],
|
|
712
830
|
);
|
|
713
831
|
const locale = useKitLocale(localeProp);
|
|
832
|
+
|
|
833
|
+
// ---- Row actions ----
|
|
834
|
+
// The actions column is appended to the caller's columns rather than drawn as a
|
|
835
|
+
// second special case beside the selection column: it then gets the cell padding,
|
|
836
|
+
// the widths and the phone rules (`mobileHidden`) every other column gets.
|
|
837
|
+
const rowActionsFor = useCallback(
|
|
838
|
+
(row: T): ResolvedRowAction<T>[] =>
|
|
839
|
+
rowActions
|
|
840
|
+
? (typeof rowActions === "function" ? rowActions(row) : rowActions)
|
|
841
|
+
.filter((a) => !a.hidden?.(row))
|
|
842
|
+
.map((a, i) => resolveRowAction(a, i, labels))
|
|
843
|
+
: [],
|
|
844
|
+
[rowActions, labels],
|
|
845
|
+
);
|
|
846
|
+
const columns = useMemo<DataTableColumn<T>[]>(
|
|
847
|
+
() =>
|
|
848
|
+
rowActions
|
|
849
|
+
? [
|
|
850
|
+
...columnsProp,
|
|
851
|
+
{
|
|
852
|
+
key: ROW_ACTIONS_KEY,
|
|
853
|
+
header: <span className="sr-only">{labels.actions}</span>,
|
|
854
|
+
cell: (row) => (
|
|
855
|
+
<div className="flex items-center justify-end gap-0.5">
|
|
856
|
+
{rowActionsFor(row).map((a) => (
|
|
857
|
+
<IconButton
|
|
858
|
+
key={a.key}
|
|
859
|
+
size={density === "compact" ? "2xs" : "xs"}
|
|
860
|
+
label={a.label}
|
|
861
|
+
tone={a.tone === "danger" ? "danger" : undefined}
|
|
862
|
+
disabledReason={a.action.disabledReason?.(row)}
|
|
863
|
+
// The row's click handler already skips its own controls; this is for
|
|
864
|
+
// the caller who wraps the table in a clickable element of their own.
|
|
865
|
+
stopPropagation
|
|
866
|
+
onClick={() => void runRowAction(a.action, row)}
|
|
867
|
+
>
|
|
868
|
+
{a.icon}
|
|
869
|
+
</IconButton>
|
|
870
|
+
))}
|
|
871
|
+
</div>
|
|
872
|
+
),
|
|
873
|
+
className: "w-px whitespace-nowrap text-end",
|
|
874
|
+
headClassName: "w-px",
|
|
875
|
+
// On a phone the same actions are the card's swipes.
|
|
876
|
+
mobileHidden: true,
|
|
877
|
+
noRowLink: true,
|
|
878
|
+
},
|
|
879
|
+
]
|
|
880
|
+
: columnsProp,
|
|
881
|
+
[columnsProp, rowActions, rowActionsFor, labels.actions, density],
|
|
882
|
+
);
|
|
883
|
+
const swipesFromActions = (row: T): MobileSwipeActions | null => {
|
|
884
|
+
const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
|
|
885
|
+
if (!swipes.length) return null;
|
|
886
|
+
const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({
|
|
887
|
+
label: a.label,
|
|
888
|
+
icon: a.icon,
|
|
889
|
+
onCommit: () => void runRowAction(a.action, row),
|
|
890
|
+
className: a.tone === "danger" ? "bg-[var(--danger)]" : "bg-[var(--brand)]",
|
|
891
|
+
armedClassName: a.tone === "danger" ? "bg-[var(--danger-hover)]" : "bg-[var(--brand-hover)]",
|
|
892
|
+
});
|
|
893
|
+
return {
|
|
894
|
+
start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
|
|
895
|
+
end: swipes.filter((a) => a.swipe === "end").map(toSwipe),
|
|
896
|
+
};
|
|
897
|
+
};
|
|
898
|
+
const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);
|
|
899
|
+
|
|
714
900
|
// Sorting, filtering and paging all change WHICH rows are on screen without
|
|
715
901
|
// moving focus — the header button the user pressed is still the header button
|
|
716
902
|
// they are on — so there is no other channel to say it on. See use-announce.ts.
|
|
@@ -875,7 +1061,7 @@ export function DataTable<T>({
|
|
|
875
1061
|
// criterion BEHIND the existing one, and answering "Sorted by Date" to a click on
|
|
876
1062
|
// Name misreports what the click did. The third click drops the column entirely,
|
|
877
1063
|
// which is the state a user is most likely to have reached by accident.
|
|
878
|
-
const name = col ? columnLabel(col) : key;
|
|
1064
|
+
const name = col ? columnLabel(col, labels) : key;
|
|
879
1065
|
const entry = next.find((s) => s.key === key);
|
|
880
1066
|
announce(
|
|
881
1067
|
!entry
|
|
@@ -1122,7 +1308,10 @@ export function DataTable<T>({
|
|
|
1122
1308
|
const totalCount = columns.length;
|
|
1123
1309
|
// Spans the full row width including the optional leading selection column.
|
|
1124
1310
|
const totalColSpan = visibleCount + (selection ? 1 : 0);
|
|
1125
|
-
|
|
1311
|
+
// The actions column is the table's own, not one the user can hide: out of the count.
|
|
1312
|
+
const columnsCountLabel = rowActions
|
|
1313
|
+
? labels.columnsCount(visibleCount - 1, totalCount - 1)
|
|
1314
|
+
: labels.columnsCount(visibleCount, totalCount);
|
|
1126
1315
|
// Match Tailwind's `md` breakpoint: we render either the table or the card
|
|
1127
1316
|
// list — never both — so we don't double up DOM nodes that screen readers and
|
|
1128
1317
|
// integration tests would have to disambiguate.
|
|
@@ -1252,7 +1441,7 @@ export function DataTable<T>({
|
|
|
1252
1441
|
// uses for exactly this, and it points the way the row actually opens: right into
|
|
1253
1442
|
// a sheet, or down into an inline panel that flips it when expanded.
|
|
1254
1443
|
const opensDetail = interactive && !!expandedRow;
|
|
1255
|
-
const swipeActions =
|
|
1444
|
+
const swipeActions = swipeActionsFor?.(row);
|
|
1256
1445
|
const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;
|
|
1257
1446
|
// An expanded row never swipes: the editor below it owns the horizontal space,
|
|
1258
1447
|
// and dragging the header away from its own form reads as a glitch.
|
|
@@ -1362,7 +1551,14 @@ export function DataTable<T>({
|
|
|
1362
1551
|
<div
|
|
1363
1552
|
role={interactive ? "button" : undefined}
|
|
1364
1553
|
tabIndex={interactive ? 0 : undefined}
|
|
1365
|
-
onClick={
|
|
1554
|
+
onClick={
|
|
1555
|
+
interactive
|
|
1556
|
+
? (e) => {
|
|
1557
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
1558
|
+
onRowClick!(row);
|
|
1559
|
+
}
|
|
1560
|
+
: undefined
|
|
1561
|
+
}
|
|
1366
1562
|
onKeyDown={
|
|
1367
1563
|
interactive
|
|
1368
1564
|
? (e) => {
|
|
@@ -1445,6 +1641,19 @@ export function DataTable<T>({
|
|
|
1445
1641
|
card list for the table wholesale, which would remount a region living
|
|
1446
1642
|
inside either one. */}
|
|
1447
1643
|
<span {...regionProps} />
|
|
1644
|
+
{toolbar != null && toolbar !== false && (
|
|
1645
|
+
<div
|
|
1646
|
+
data-table-toolbar=""
|
|
1647
|
+
className={cn(
|
|
1648
|
+
"flex flex-wrap items-center gap-2",
|
|
1649
|
+
frame
|
|
1650
|
+
? cn("border-b border-[var(--border)]", compact ? "px-2 py-1.5" : "px-3 py-2")
|
|
1651
|
+
: "pb-3",
|
|
1652
|
+
)}
|
|
1653
|
+
>
|
|
1654
|
+
{toolbar}
|
|
1655
|
+
</div>
|
|
1656
|
+
)}
|
|
1448
1657
|
{!isMdUp && (
|
|
1449
1658
|
<div ref={mobileRootRef}>
|
|
1450
1659
|
{/* Mobile filter access (feedback #299): the per-column filter popovers
|
|
@@ -1489,7 +1698,7 @@ export function DataTable<T>({
|
|
|
1489
1698
|
compact ? "px-3 py-3 text-xs" : "px-4 py-6 text-sm",
|
|
1490
1699
|
)}
|
|
1491
1700
|
>
|
|
1492
|
-
{isLoading ? <LoadingText label={labels.loading} /> : (empty ??
|
|
1701
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
|
|
1493
1702
|
</li>
|
|
1494
1703
|
)}
|
|
1495
1704
|
{/* Endless-scroll sentinel: observed by IntersectionObserver to pull in
|
|
@@ -1708,7 +1917,7 @@ export function DataTable<T>({
|
|
|
1708
1917
|
</Popover>
|
|
1709
1918
|
)}
|
|
1710
1919
|
</div>
|
|
1711
|
-
{resizable && (
|
|
1920
|
+
{resizable && col.key !== ROW_ACTIONS_KEY && (
|
|
1712
1921
|
<span
|
|
1713
1922
|
role="separator"
|
|
1714
1923
|
aria-orientation="vertical"
|
|
@@ -1800,6 +2009,7 @@ export function DataTable<T>({
|
|
|
1800
2009
|
// not also open the row. Cells no longer have to
|
|
1801
2010
|
// `stopPropagation` to get that.
|
|
1802
2011
|
if (fromOwnControl(e.target, e.currentTarget)) return;
|
|
2012
|
+
if (fromOutside(e.target, e.currentTarget)) return;
|
|
1803
2013
|
// Modifier-clicks anywhere in the row drive
|
|
1804
2014
|
// selection instead of expanding it (feedback #289):
|
|
1805
2015
|
// Ctrl/Cmd toggles a single row, Shift selects the
|
|
@@ -1935,7 +2145,7 @@ export function DataTable<T>({
|
|
|
1935
2145
|
>
|
|
1936
2146
|
{/* Nothing is not the same as not-yet: "no results" while the first
|
|
1937
2147
|
page is still in flight is a claim the table cannot make. */}
|
|
1938
|
-
{isLoading ? <LoadingText label={labels.loading} /> : (empty ??
|
|
2148
|
+
{isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
|
|
1939
2149
|
</td>
|
|
1940
2150
|
</tr>
|
|
1941
2151
|
)}
|
|
@@ -2018,6 +2228,7 @@ export function DataTable<T>({
|
|
|
2018
2228
|
</button>
|
|
2019
2229
|
<div className="flex flex-col gap-0.5">
|
|
2020
2230
|
{columns.map((col) => {
|
|
2231
|
+
if (col.key === ROW_ACTIONS_KEY) return null;
|
|
2021
2232
|
const checked = !hiddenCols.has(col.key);
|
|
2022
2233
|
const headerLabel = columnLabel(col);
|
|
2023
2234
|
return (
|