@eifi1/ui-kit 0.10.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 +66 -37
- 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/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -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/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +107 -3
- package/dist/components/button-group.js +60 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +17 -4
- package/dist/components/calendar-heatmap.d.ts +162 -0
- package/dist/components/calendar-heatmap.js +336 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- 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 +66 -2
- package/dist/components/choice-card.js +108 -1
- 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 +52 -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 +104 -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 +69 -0
- package/dist/components/field.js +75 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +51 -208
- package/dist/components/file-dropzone.d.ts +50 -2
- package/dist/components/floating-panel.d.ts +156 -4
- package/dist/components/floating-panel.js +193 -29
- 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 +37 -3
- package/dist/components/list.js +39 -14
- 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 +13 -2
- package/dist/components/menu-item.js +11 -2
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +82 -5
- package/dist/components/mini-calendar.js +212 -64
- 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 +67 -0
- package/dist/components/nav-pills.js +69 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +26 -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 +27 -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 +17 -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 +73 -3
- package/dist/components/progress-bar.js +110 -8
- 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/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +50 -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/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +77 -11
- package/dist/components/table.js +85 -17
- 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/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +50 -1
- package/dist/components/toggle-group.d.ts +28 -0
- package/dist/components/toggle-group.js +58 -9
- 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/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +52 -729
- package/dist/components/ui.js +296 -27
- 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/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.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 +50 -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 +17 -4
- package/dist/i18n/defaults.js +33 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +47 -310
- 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 +17 -4
- package/dist/i18n/locales/de-CH.d.ts +17 -4
- package/dist/i18n/locales/de-informal.d.ts +17 -4
- package/dist/i18n/locales/de.d.ts +17 -4
- package/dist/i18n/locales/de.js +165 -4
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +17 -4
- package/dist/i18n/locales/es.js +162 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -4
- package/dist/i18n/locales/fr.js +162 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -4
- package/dist/i18n/locales/hu.js +162 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -4
- package/dist/i18n/locales/it.js +162 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -4
- package/dist/i18n/locales/zh.js +162 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- 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 +50 -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/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +63 -1
- package/dist/rhf.js +3 -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 +84 -3
- package/dist/shell/topbar-action-menu.js +115 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +58 -2
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -1
- package/dist/wizard/wizard-context.d.ts +30 -1
- package/dist/wizard/wizard-context.js +23 -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 +51 -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/alert-banner.tsx +36 -7
- 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/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +129 -4
- package/src/components/calendar-heatmap.tsx +599 -0
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +208 -1
- 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 +231 -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 +188 -0
- package/src/components/floating-panel.tsx +380 -16
- 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 +93 -18
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +29 -3
- package/src/components/mini-calendar.tsx +317 -57
- 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 +152 -0
- 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 +241 -13
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/series-chart.tsx +21 -0
- 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/status-dot.tsx +10 -2
- package/src/components/table.tsx +186 -23
- package/src/components/text-link.tsx +248 -0
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +106 -13
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +572 -44
- package/src/components/use-table-state.ts +265 -33
- package/src/components/user-avatar.tsx +57 -3
- 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 +32 -0
- package/src/i18n/kit-labels.tsx +104 -8
- package/src/i18n/locales/de.ts +162 -0
- package/src/i18n/locales/es.ts +159 -0
- package/src/i18n/locales/fr.ts +162 -0
- package/src/i18n/locales/hu.ts +160 -0
- package/src/i18n/locales/it.ts +160 -0
- package/src/i18n/locales/zh.ts +159 -0
- package/src/index.ts +64 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -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/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +8 -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 +260 -49
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +50 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useRef } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ButtonHTMLAttributes,
|
|
4
|
+
ChangeEvent,
|
|
5
|
+
ComponentType,
|
|
6
|
+
InputHTMLAttributes,
|
|
7
|
+
MouseEvent,
|
|
8
|
+
ReactElement,
|
|
9
|
+
ReactNode,
|
|
10
|
+
Ref,
|
|
11
|
+
} from "react";
|
|
3
12
|
import { Check, Minus } from "lucide-react";
|
|
4
13
|
import { cn } from "../lib/cn";
|
|
5
14
|
import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
15
|
+
import { useKitLink } from "../i18n/kit-labels";
|
|
16
|
+
import { pickLinkRenderer } from "./text-link";
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
19
|
* A checkbox or a radio, presented as a bordered card: a title, a line of description,
|
|
@@ -218,6 +229,202 @@ export const ChoiceCard = forwardRef<HTMLInputElement, ChoiceCardProps>(function
|
|
|
218
229
|
});
|
|
219
230
|
ChoiceCard.displayName = "ChoiceCard";
|
|
220
231
|
|
|
232
|
+
/* ── ActionCard ──────────────────────────────────────────────────────────── */
|
|
233
|
+
|
|
234
|
+
/** The colour of {@link ActionCardProps.meta}. */
|
|
235
|
+
export type ActionCardMetaTone = "muted" | "warning" | "danger" | "info" | "success";
|
|
236
|
+
|
|
237
|
+
const META_TONE: Record<ActionCardMetaTone, string> = {
|
|
238
|
+
muted: "text-[var(--text-muted)]",
|
|
239
|
+
warning: "text-[var(--warning)]",
|
|
240
|
+
danger: "text-[var(--danger)]",
|
|
241
|
+
info: "text-[var(--info)]",
|
|
242
|
+
success: "text-[var(--success)]",
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** The colour of {@link ActionCardProps.icon}. `default` is the secondary text colour
|
|
246
|
+
* the icon has always had; `brand` tints it (keksdose's privacy enrolment). */
|
|
247
|
+
export type ActionCardIconTone = "default" | "muted" | "brand" | "warning" | "danger" | "info" | "success";
|
|
248
|
+
|
|
249
|
+
const ICON_TONE: Record<ActionCardIconTone, string> = {
|
|
250
|
+
default: "text-[var(--text-secondary)]",
|
|
251
|
+
...META_TONE,
|
|
252
|
+
brand: "text-[var(--brand)]",
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** What {@link ActionCardProps.renderLink} (and the provider's `linkComponent`) is
|
|
256
|
+
* handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */
|
|
257
|
+
export interface ActionCardLinkProps {
|
|
258
|
+
href: string;
|
|
259
|
+
/** The card's whole look — keep it. */
|
|
260
|
+
className: string;
|
|
261
|
+
children: ReactNode;
|
|
262
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
263
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
264
|
+
id?: string;
|
|
265
|
+
[key: `aria-${string}`]: string | boolean | number | undefined;
|
|
266
|
+
[key: `data-${string}`]: unknown;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "title"> {
|
|
270
|
+
/** What the card does — the button's accessible name. */
|
|
271
|
+
title: ReactNode;
|
|
272
|
+
/** A line or two on what it means. Attached with `aria-describedby`. */
|
|
273
|
+
description?: ReactNode;
|
|
274
|
+
/**
|
|
275
|
+
* What it costs, or what else to know — a third line, said as prose and not as fine
|
|
276
|
+
* print (keksdose's custody choice: both options are legitimate and neither is free,
|
|
277
|
+
* so a card listing only benefits would be selling rather than explaining). Attached
|
|
278
|
+
* with `aria-describedby` after the description.
|
|
279
|
+
*/
|
|
280
|
+
meta?: ReactNode;
|
|
281
|
+
/** The meta line's colour. Default `muted`; keksdose's cost line is `warning`. */
|
|
282
|
+
metaTone?: ActionCardMetaTone;
|
|
283
|
+
/** A Lucide icon (or any component taking a `className`), shown at the card's start. */
|
|
284
|
+
icon?: ChoiceCardProps["icon"];
|
|
285
|
+
/** The icon's colour. Default `default` (the secondary text colour). */
|
|
286
|
+
iconTone?: ActionCardIconTone;
|
|
287
|
+
/** Classes on the icon, after {@link iconTone} — a size, a tinted tile behind it. */
|
|
288
|
+
iconClassName?: string;
|
|
289
|
+
/**
|
|
290
|
+
* Makes the card a LINK to another page rather than a button that acts: kastlan's
|
|
291
|
+
* section tiles on the group overview (group-overview-page), each an icon, a name and
|
|
292
|
+
* a line on what is there. A real `<a href>`, so a middle click opens it in a new
|
|
293
|
+
* tab. A `disabled` card renders the inert button instead — a link cannot be
|
|
294
|
+
* disabled. The ref then reaches the `<a>`.
|
|
295
|
+
*/
|
|
296
|
+
href?: string;
|
|
297
|
+
/** Your router's link for an in-app `href`. Default: the `<UiKitProvider
|
|
298
|
+
* linkComponent>`, then a plain `<a>`. Ignored without `href`. */
|
|
299
|
+
renderLink?: (props: ActionCardLinkProps) => ReactElement;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A {@link ChoiceCard} that ACTS rather than holds a state: a `<button>` with the
|
|
304
|
+
* card's icon, title and description, plus a `meta` line, that runs `onClick` the
|
|
305
|
+
* moment it is pressed. keksdose's privacy enrolment (privacy-enroll-dialog:345,
|
|
306
|
+
* `CustodyOption`) offers two custody modes this way — picking one IS the next step,
|
|
307
|
+
* so there is no checked state to show and no "Continue" to press after it. With
|
|
308
|
+
* `href` it is the same card as a link (kastlan's overview section tiles).
|
|
309
|
+
*
|
|
310
|
+
* A separate component rather than `ChoiceCard as="button"`: a ChoiceCard is an
|
|
311
|
+
* `<input>` (its ref, its `checked`, its form value), and a button shares none of it.
|
|
312
|
+
* Named by the title alone and described by the rest, so a screen reader hears
|
|
313
|
+
* "Keep the key yourself, button" and then the explanation, not one long name.
|
|
314
|
+
*/
|
|
315
|
+
export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(function ActionCard(
|
|
316
|
+
{
|
|
317
|
+
title,
|
|
318
|
+
description,
|
|
319
|
+
meta,
|
|
320
|
+
metaTone = "muted",
|
|
321
|
+
icon: Icon,
|
|
322
|
+
iconTone = "default",
|
|
323
|
+
iconClassName,
|
|
324
|
+
href,
|
|
325
|
+
renderLink,
|
|
326
|
+
className,
|
|
327
|
+
id,
|
|
328
|
+
type = "button",
|
|
329
|
+
...rest
|
|
330
|
+
},
|
|
331
|
+
ref,
|
|
332
|
+
) {
|
|
333
|
+
const kitLink = useKitLink();
|
|
334
|
+
const generated = useId();
|
|
335
|
+
const baseId = id ?? generated;
|
|
336
|
+
const titleId = `${baseId}-title`;
|
|
337
|
+
const descriptionId = `${baseId}-description`;
|
|
338
|
+
const metaId = `${baseId}-meta`;
|
|
339
|
+
const showDescription = hasMessage(description);
|
|
340
|
+
const showMeta = hasMessage(meta);
|
|
341
|
+
const look = cn(
|
|
342
|
+
"flex w-full items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 text-start shadow-sm transition-colors",
|
|
343
|
+
"hover:border-[var(--brand)] hover:bg-[var(--bg-hover)]",
|
|
344
|
+
// The outline, as on ChoiceCard: the card is what the eye is on.
|
|
345
|
+
"focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
|
|
346
|
+
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]",
|
|
347
|
+
className,
|
|
348
|
+
);
|
|
349
|
+
const labelledBy = rest["aria-labelledby"] ?? titleId;
|
|
350
|
+
const describedBy = mergeDescribedBy(
|
|
351
|
+
rest["aria-describedby"],
|
|
352
|
+
showDescription && descriptionId,
|
|
353
|
+
showMeta && metaId,
|
|
354
|
+
);
|
|
355
|
+
const body = (
|
|
356
|
+
<>
|
|
357
|
+
{Icon && <Icon aria-hidden className={cn("mt-0.5 size-5 shrink-0", ICON_TONE[iconTone], iconClassName)} />}
|
|
358
|
+
<span className="min-w-0 flex-1 space-y-1">
|
|
359
|
+
<span id={titleId} className="block text-sm font-medium leading-5 text-[var(--text-primary)]">
|
|
360
|
+
{title}
|
|
361
|
+
</span>
|
|
362
|
+
{showDescription && (
|
|
363
|
+
<span id={descriptionId} className="block text-sm text-[var(--text-secondary)]">
|
|
364
|
+
{description}
|
|
365
|
+
</span>
|
|
366
|
+
)}
|
|
367
|
+
{showMeta && (
|
|
368
|
+
<span id={metaId} className={cn("block text-sm", META_TONE[metaTone])}>
|
|
369
|
+
{meta}
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
</span>
|
|
373
|
+
</>
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (href !== undefined && !rest.disabled) {
|
|
377
|
+
// The button-only attributes stay behind; `aria-*`, `data-*` and handlers ride along.
|
|
378
|
+
const {
|
|
379
|
+
disabled: _disabled,
|
|
380
|
+
form: _form,
|
|
381
|
+
formAction: _formAction,
|
|
382
|
+
name: _name,
|
|
383
|
+
value: _value,
|
|
384
|
+
onClick,
|
|
385
|
+
...anchorRest
|
|
386
|
+
} = rest;
|
|
387
|
+
const linkProps: ActionCardLinkProps = {
|
|
388
|
+
...(anchorRest as Partial<ActionCardLinkProps>),
|
|
389
|
+
ref: ref as unknown as Ref<HTMLAnchorElement>,
|
|
390
|
+
id,
|
|
391
|
+
href,
|
|
392
|
+
"aria-labelledby": labelledBy,
|
|
393
|
+
"aria-describedby": describedBy,
|
|
394
|
+
onClick: onClick as unknown as ActionCardLinkProps["onClick"],
|
|
395
|
+
className: look,
|
|
396
|
+
children: body,
|
|
397
|
+
};
|
|
398
|
+
const render = pickLinkRenderer(renderLink, kitLink, href);
|
|
399
|
+
if (render) return <RenderedActionLink render={render} {...linkProps} />;
|
|
400
|
+
const { children: content, ...anchor } = linkProps;
|
|
401
|
+
return <a {...anchor}>{content}</a>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<button
|
|
406
|
+
ref={ref}
|
|
407
|
+
id={id}
|
|
408
|
+
type={type}
|
|
409
|
+
{...rest}
|
|
410
|
+
aria-labelledby={labelledBy}
|
|
411
|
+
aria-describedby={describedBy}
|
|
412
|
+
className={look}
|
|
413
|
+
>
|
|
414
|
+
{body}
|
|
415
|
+
</button>
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
ActionCard.displayName = "ActionCard";
|
|
419
|
+
|
|
420
|
+
/** Calls the link renderer as a component, so a router link's hooks are its own. */
|
|
421
|
+
function RenderedActionLink({
|
|
422
|
+
render,
|
|
423
|
+
...props
|
|
424
|
+
}: ActionCardLinkProps & { render: (props: ActionCardLinkProps) => ReactElement }) {
|
|
425
|
+
return render(props);
|
|
426
|
+
}
|
|
427
|
+
|
|
221
428
|
/* ── ChoiceCardGroup ─────────────────────────────────────────────────────── */
|
|
222
429
|
|
|
223
430
|
export interface ChoiceCardOption<T extends string> {
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "react";
|
|
11
11
|
import { Check, Plus } from "lucide-react";
|
|
12
12
|
import { cn } from "../lib/cn";
|
|
13
|
+
import { CLIPS_ATTRIBUTE } from "../lib/clipping";
|
|
13
14
|
import { DropdownSearchHeader } from "./dropdown";
|
|
14
15
|
import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
|
|
15
16
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
@@ -636,6 +637,10 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
636
637
|
// The rows are provisional while a lookup is in flight — whether or not the
|
|
637
638
|
// previous query's are still showing.
|
|
638
639
|
aria-busy={busy || undefined}
|
|
640
|
+
// It scrolls, and says so where jsdom can see it (see `CLIPS_ATTRIBUTE`): a
|
|
641
|
+
// Tooltip in a row then portals out in a test exactly as it does in the
|
|
642
|
+
// browser, instead of joining the option's accessible name (keksdose E8).
|
|
643
|
+
{...{ [CLIPS_ATTRIBUTE]: "" }}
|
|
639
644
|
className={cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none")}
|
|
640
645
|
>
|
|
641
646
|
{results.map((o, i) => {
|
|
@@ -113,7 +113,7 @@ function SuggestionList({
|
|
|
113
113
|
dir={dir}
|
|
114
114
|
style={{ top, left: rect.left, width: rect.width }}
|
|
115
115
|
>
|
|
116
|
-
<ul id={id} role="listbox" className={LIST_CLASS} style={{ maxHeight }}>
|
|
116
|
+
<ul id={id} role="listbox" data-clips="" className={LIST_CLASS} style={{ maxHeight }}>
|
|
117
117
|
{children}
|
|
118
118
|
</ul>
|
|
119
119
|
</div>,
|
|
@@ -238,9 +238,19 @@ export function Combobox({
|
|
|
238
238
|
onBlur,
|
|
239
239
|
onSubmit,
|
|
240
240
|
"aria-label": ariaLabel,
|
|
241
|
+
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
242
|
+
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
243
|
+
// on the wrapper div the hint and required state described nothing.
|
|
244
|
+
"aria-describedby": ariaDescribedBy,
|
|
245
|
+
"aria-invalid": ariaInvalid,
|
|
246
|
+
"aria-required": ariaRequired,
|
|
241
247
|
...rest
|
|
242
248
|
}: ComboboxProps) {
|
|
243
|
-
const field = useComboboxFieldError(
|
|
249
|
+
const field = useComboboxFieldError(
|
|
250
|
+
error,
|
|
251
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
252
|
+
ariaDescribedBy,
|
|
253
|
+
);
|
|
244
254
|
const generated = useId();
|
|
245
255
|
const fieldId = id ?? generated;
|
|
246
256
|
// Derived from the GENERATED id, never from `id`: a caller's id is theirs to
|
|
@@ -366,9 +376,9 @@ export function Combobox({
|
|
|
366
376
|
: null;
|
|
367
377
|
|
|
368
378
|
return (
|
|
369
|
-
// `rest` dresses the outer box — a `data-tour` anchor, a test id
|
|
370
|
-
//
|
|
371
|
-
// the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
379
|
+
// `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
|
|
380
|
+
// description, invalid or required state: those belong on the <input> below,
|
|
381
|
+
// which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
372
382
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
373
383
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
374
384
|
{/* A real <label for> since lenkbank's tests met a field `getByLabelText` could
|
|
@@ -407,6 +417,7 @@ export function Combobox({
|
|
|
407
417
|
aria-autocomplete="list"
|
|
408
418
|
aria-invalid={field.isInvalid || undefined}
|
|
409
419
|
aria-describedby={field.describedBy}
|
|
420
|
+
aria-required={ariaRequired}
|
|
410
421
|
autoComplete="off"
|
|
411
422
|
disabled={disabled}
|
|
412
423
|
autoFocus={autoFocus}
|
|
@@ -729,12 +740,22 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
729
740
|
invalid,
|
|
730
741
|
error,
|
|
731
742
|
"aria-label": ariaLabel,
|
|
743
|
+
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
744
|
+
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
745
|
+
// on the wrapper div the hint and required state described nothing.
|
|
746
|
+
"aria-describedby": ariaDescribedBy,
|
|
747
|
+
"aria-invalid": ariaInvalid,
|
|
748
|
+
"aria-required": ariaRequired,
|
|
732
749
|
...rest
|
|
733
750
|
}: InlineEntityComboboxProps<V, C>) {
|
|
734
751
|
// The clear value reads as "nothing selected", whichever one the caller picked —
|
|
735
752
|
// so from here down `value` is the id or `null`, as it always was.
|
|
736
753
|
const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
|
|
737
|
-
const field = useComboboxFieldError(
|
|
754
|
+
const field = useComboboxFieldError(
|
|
755
|
+
error,
|
|
756
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
757
|
+
ariaDescribedBy,
|
|
758
|
+
);
|
|
738
759
|
const generated = useId();
|
|
739
760
|
const fieldId = id ?? generated;
|
|
740
761
|
// See the twin above on why these hang off the generated id.
|
|
@@ -853,9 +874,9 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
853
874
|
};
|
|
854
875
|
|
|
855
876
|
return (
|
|
856
|
-
// `rest` dresses the outer box — a `data-tour` anchor, a test id
|
|
857
|
-
//
|
|
858
|
-
// the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
877
|
+
// `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
|
|
878
|
+
// description, invalid or required state: those belong on the <input> below,
|
|
879
|
+
// which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
859
880
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
860
881
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
861
882
|
{/* A real <label for> — see {@link Combobox}. */}
|
|
@@ -889,6 +910,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
889
910
|
aria-autocomplete="list"
|
|
890
911
|
aria-invalid={field.isInvalid || undefined}
|
|
891
912
|
aria-describedby={field.describedBy}
|
|
913
|
+
aria-required={ariaRequired}
|
|
892
914
|
autoComplete="off"
|
|
893
915
|
disabled={disabled}
|
|
894
916
|
autoFocus={autoFocus}
|
|
@@ -56,7 +56,8 @@ export interface CopyButtonProps
|
|
|
56
56
|
stopPropagation?: boolean;
|
|
57
57
|
/** Icon variant only: where the result tooltip opens. */
|
|
58
58
|
tooltipSide?: TooltipSide;
|
|
59
|
-
/** Pass through to the tooltip
|
|
59
|
+
/** Pass through to the tooltip. Left unset, the Tooltip decides on its own — it
|
|
60
|
+
* portals inside a clipping container (a DataTable's `data-clips` scroller). */
|
|
60
61
|
tooltipPortal?: boolean;
|
|
61
62
|
/** ms until the button returns to idle. Default 2000. */
|
|
62
63
|
resetAfter?: number;
|
|
@@ -93,7 +94,7 @@ export function CopyButton({
|
|
|
93
94
|
tone,
|
|
94
95
|
stopPropagation,
|
|
95
96
|
tooltipSide = "top",
|
|
96
|
-
tooltipPortal
|
|
97
|
+
tooltipPortal,
|
|
97
98
|
resetAfter = 2000,
|
|
98
99
|
onCopied,
|
|
99
100
|
labels,
|
|
@@ -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
|
/**
|