@eifi1/ui-kit 0.11.0 → 0.13.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 +59 -34
- 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 +40 -4
- package/dist/components/amount-input.js +50 -12
- 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 +10 -2
- package/dist/components/bulk-action-bar.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/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +26 -18
- 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.d.ts +3 -1
- package/dist/components/combobox-core.js +18 -5
- 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-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +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 +125 -27
- 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 +40 -20
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +281 -0
- package/dist/components/error-boundary.js +302 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- 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/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- 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 -4
- package/dist/components/list.js +35 -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 +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 +42 -20
- 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 +20 -5
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- 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 +5 -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/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- 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 +122 -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 +6 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.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 +238 -0
- package/dist/components/text-link.js +144 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +35 -0
- package/dist/components/toggle-group.js +43 -14
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +53 -3
- package/dist/components/toggle-legend.js +54 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +364 -34
- 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-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +21 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +173 -0
- package/dist/feedback/feedback-thread.js +264 -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 +164 -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 +161 -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 +161 -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 +161 -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 +161 -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 +161 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +39 -19
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
- package/dist/lib/format.d.ts +162 -0
- package/dist/lib/format.js +151 -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 +283 -0
- package/dist/rhf/fields.js +631 -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/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +120 -57
- 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 +21 -4
- package/dist/shell/topbar-action-menu.js +18 -2
- 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/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -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 +30 -5
- package/dist/wizard/wizard-step.js +34 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +6 -6
- 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 +109 -10
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +14 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +33 -9
- package/src/components/combobox.tsx +33 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +230 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +25 -7
- package/src/components/error-boundary.tsx +640 -0
- package/src/components/field-sync.tsx +48 -40
- 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/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- 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 +82 -19
- 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 +65 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +27 -7
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +17 -1
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +10 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/popover.tsx +3 -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 +295 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +11 -3
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +356 -0
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +93 -20
- package/src/components/toggle-legend.tsx +108 -2
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +707 -49
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +49 -7
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +510 -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 +118 -8
- package/src/i18n/locales/de.ts +161 -0
- package/src/i18n/locales/es.ts +158 -0
- package/src/i18n/locales/fr.ts +161 -0
- package/src/i18n/locales/hu.ts +159 -0
- package/src/i18n/locales/it.ts +159 -0
- package/src/i18n/locales/zh.ts +158 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +348 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1135 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +153 -3
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +32 -4
- package/src/shell.ts +2 -0
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +75 -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,
|
|
@@ -231,6 +242,30 @@ const META_TONE: Record<ActionCardMetaTone, string> = {
|
|
|
231
242
|
success: "text-[var(--success)]",
|
|
232
243
|
};
|
|
233
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
|
+
|
|
234
269
|
export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "title"> {
|
|
235
270
|
/** What the card does — the button's accessible name. */
|
|
236
271
|
title: ReactNode;
|
|
@@ -247,6 +282,21 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
|
|
|
247
282
|
metaTone?: ActionCardMetaTone;
|
|
248
283
|
/** A Lucide icon (or any component taking a `className`), shown at the card's start. */
|
|
249
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;
|
|
250
300
|
}
|
|
251
301
|
|
|
252
302
|
/**
|
|
@@ -254,7 +304,8 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
|
|
|
254
304
|
* card's icon, title and description, plus a `meta` line, that runs `onClick` the
|
|
255
305
|
* moment it is pressed. keksdose's privacy enrolment (privacy-enroll-dialog:345,
|
|
256
306
|
* `CustodyOption`) offers two custody modes this way — picking one IS the next step,
|
|
257
|
-
* so there is no checked state to show and no "Continue" to press after it.
|
|
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).
|
|
258
309
|
*
|
|
259
310
|
* A separate component rather than `ChoiceCard as="button"`: a ChoiceCard is an
|
|
260
311
|
* `<input>` (its ref, its `checked`, its form value), and a button shares none of it.
|
|
@@ -262,9 +313,24 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
|
|
|
262
313
|
* "Keep the key yourself, button" and then the explanation, not one long name.
|
|
263
314
|
*/
|
|
264
315
|
export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(function ActionCard(
|
|
265
|
-
{
|
|
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
|
+
},
|
|
266
331
|
ref,
|
|
267
332
|
) {
|
|
333
|
+
const kitLink = useKitLink();
|
|
268
334
|
const generated = useId();
|
|
269
335
|
const baseId = id ?? generated;
|
|
270
336
|
const titleId = `${baseId}-title`;
|
|
@@ -272,28 +338,23 @@ export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(functio
|
|
|
272
338
|
const metaId = `${baseId}-meta`;
|
|
273
339
|
const showDescription = hasMessage(description);
|
|
274
340
|
const showMeta = hasMessage(meta);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]",
|
|
293
|
-
className,
|
|
294
|
-
)}
|
|
295
|
-
>
|
|
296
|
-
{Icon && <Icon aria-hidden className="mt-0.5 size-5 shrink-0 text-[var(--text-secondary)]" />}
|
|
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)} />}
|
|
297
358
|
<span className="min-w-0 flex-1 space-y-1">
|
|
298
359
|
<span id={titleId} className="block text-sm font-medium leading-5 text-[var(--text-primary)]">
|
|
299
360
|
{title}
|
|
@@ -309,11 +370,61 @@ export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(functio
|
|
|
309
370
|
</span>
|
|
310
371
|
)}
|
|
311
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}
|
|
312
415
|
</button>
|
|
313
416
|
);
|
|
314
417
|
});
|
|
315
418
|
ActionCard.displayName = "ActionCard";
|
|
316
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
|
+
|
|
317
428
|
/* ── ChoiceCardGroup ─────────────────────────────────────────────────────── */
|
|
318
429
|
|
|
319
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";
|
|
@@ -260,7 +261,9 @@ export interface ComboboxCore<V extends string | number> {
|
|
|
260
261
|
rect: AnchorRect | null;
|
|
261
262
|
/** Where the dropdown goes, clamped to the visible viewport (feedback #135). */
|
|
262
263
|
placement: AnchoredPanel;
|
|
263
|
-
|
|
264
|
+
/** Put an option in the label cache, so {@link ComboboxCore.resolve} still knows a
|
|
265
|
+
* value the caller just chose after the result list has moved on. */
|
|
266
|
+
rememberOption: (o: ComboOption<V>) => void;
|
|
264
267
|
resolve: (v: V) => ComboOption<V> | null;
|
|
265
268
|
/** Close and hand focus back to the trigger — the keyboard paths only. See
|
|
266
269
|
* {@link useDropdown} for why a pointer dismissal must not do this. */
|
|
@@ -318,10 +321,23 @@ export function useComboboxCore<V extends string | number>({
|
|
|
318
321
|
useOutsideClick([triggerRef, panelRef], close, open && !isPhone);
|
|
319
322
|
useEscapeKey(closeToTrigger, open);
|
|
320
323
|
|
|
321
|
-
// Reset the query
|
|
324
|
+
// Reset the query each time the panel opens, and the highlighted row whenever the
|
|
325
|
+
// visible set changes (a new query, or the panel opening or closing). Adjusted while
|
|
326
|
+
// rendering against the previous values rather than in an effect, so the render that
|
|
327
|
+
// opens the panel already searches for "" and highlights the first row.
|
|
328
|
+
const [prevOpen, setPrevOpen] = useState(open);
|
|
329
|
+
const [prevQuery, setPrevQuery] = useState(query);
|
|
330
|
+
if (open !== prevOpen || query !== prevQuery) {
|
|
331
|
+
const opened = open && !prevOpen;
|
|
332
|
+
setPrevOpen(open);
|
|
333
|
+
setPrevQuery(opened ? "" : query);
|
|
334
|
+
if (opened) setQuery("");
|
|
335
|
+
setActive(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Focus the search box each time the panel opens.
|
|
322
339
|
useEffect(() => {
|
|
323
340
|
if (!open) return;
|
|
324
|
-
setQuery("");
|
|
325
341
|
const id = requestAnimationFrame(() => inputRef.current?.focus());
|
|
326
342
|
return () => cancelAnimationFrame(id);
|
|
327
343
|
}, [open]);
|
|
@@ -345,11 +361,9 @@ export function useComboboxCore<V extends string | number>({
|
|
|
345
361
|
}, [options, results]);
|
|
346
362
|
const resolve = (v: V): ComboOption<V> | null =>
|
|
347
363
|
options?.find((o) => o.value === v) ?? cacheRef.current.get(v) ?? null;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
setActive(0);
|
|
352
|
-
}, [query, open]);
|
|
364
|
+
const rememberOption = (o: ComboOption<V>) => {
|
|
365
|
+
cacheRef.current.set(o.value, o);
|
|
366
|
+
};
|
|
353
367
|
|
|
354
368
|
return {
|
|
355
369
|
triggerRef,
|
|
@@ -368,7 +382,7 @@ export function useComboboxCore<V extends string | number>({
|
|
|
368
382
|
minChars,
|
|
369
383
|
rect,
|
|
370
384
|
placement,
|
|
371
|
-
|
|
385
|
+
rememberOption,
|
|
372
386
|
resolve,
|
|
373
387
|
closeToTrigger,
|
|
374
388
|
};
|
|
@@ -636,6 +650,10 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
636
650
|
// The rows are provisional while a lookup is in flight — whether or not the
|
|
637
651
|
// previous query's are still showing.
|
|
638
652
|
aria-busy={busy || undefined}
|
|
653
|
+
// It scrolls, and says so where jsdom can see it (see `CLIPS_ATTRIBUTE`): a
|
|
654
|
+
// Tooltip in a row then portals out in a test exactly as it does in the
|
|
655
|
+
// browser, instead of joining the option's accessible name (keksdose E8).
|
|
656
|
+
{...{ [CLIPS_ATTRIBUTE]: "" }}
|
|
639
657
|
className={cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none")}
|
|
640
658
|
>
|
|
641
659
|
{results.map((o, i) => {
|
|
@@ -772,6 +790,9 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
772
790
|
inputRef={inputRef}
|
|
773
791
|
closeLabel={closeLabel}
|
|
774
792
|
>
|
|
793
|
+
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- never focused
|
|
794
|
+
itself: it catches the listbox keys bubbling from the sheet's search <input>,
|
|
795
|
+
which holds focus while the list is up. */}
|
|
775
796
|
<div onKeyDown={onKeyDown} className="relative">
|
|
776
797
|
{list}
|
|
777
798
|
{announcement}
|
|
@@ -791,6 +812,9 @@ export function ComboboxPanel<V extends string | number>({
|
|
|
791
812
|
: { left: rect.left };
|
|
792
813
|
|
|
793
814
|
return createPortal(
|
|
815
|
+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- never focused
|
|
816
|
+
itself: it catches the listbox keys bubbling from the search <input> inside it,
|
|
817
|
+
which holds focus while the panel is open. */
|
|
794
818
|
<div
|
|
795
819
|
// Spread FIRST: `onKeyDown` below is the listbox keyboard (Up/Down/Home/End/
|
|
796
820
|
// Enter/Tab) and the placement is measured rather than chosen, so neither is a
|
|
@@ -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,8 +417,10 @@ 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}
|
|
423
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
412
424
|
autoFocus={autoFocus}
|
|
413
425
|
onBlur={onBlur}
|
|
414
426
|
onMouseDown={primaryOnly.onMouseDown}
|
|
@@ -729,12 +741,22 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
729
741
|
invalid,
|
|
730
742
|
error,
|
|
731
743
|
"aria-label": ariaLabel,
|
|
744
|
+
// Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
|
|
745
|
+
// render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
|
|
746
|
+
// on the wrapper div the hint and required state described nothing.
|
|
747
|
+
"aria-describedby": ariaDescribedBy,
|
|
748
|
+
"aria-invalid": ariaInvalid,
|
|
749
|
+
"aria-required": ariaRequired,
|
|
732
750
|
...rest
|
|
733
751
|
}: InlineEntityComboboxProps<V, C>) {
|
|
734
752
|
// The clear value reads as "nothing selected", whichever one the caller picked —
|
|
735
753
|
// so from here down `value` is the id or `null`, as it always was.
|
|
736
754
|
const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
|
|
737
|
-
const field = useComboboxFieldError(
|
|
755
|
+
const field = useComboboxFieldError(
|
|
756
|
+
error,
|
|
757
|
+
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
758
|
+
ariaDescribedBy,
|
|
759
|
+
);
|
|
738
760
|
const generated = useId();
|
|
739
761
|
const fieldId = id ?? generated;
|
|
740
762
|
// See the twin above on why these hang off the generated id.
|
|
@@ -853,9 +875,9 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
853
875
|
};
|
|
854
876
|
|
|
855
877
|
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
|
|
878
|
+
// `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
|
|
879
|
+
// description, invalid or required state: those belong on the <input> below,
|
|
880
|
+
// which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
|
|
859
881
|
// handlers carrying live #309 and dev#549 cannot be replaced from outside.
|
|
860
882
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
861
883
|
{/* A real <label for> — see {@link Combobox}. */}
|
|
@@ -889,8 +911,10 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
|
|
|
889
911
|
aria-autocomplete="list"
|
|
890
912
|
aria-invalid={field.isInvalid || undefined}
|
|
891
913
|
aria-describedby={field.describedBy}
|
|
914
|
+
aria-required={ariaRequired}
|
|
892
915
|
autoComplete="off"
|
|
893
916
|
disabled={disabled}
|
|
917
|
+
// eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
|
|
894
918
|
autoFocus={autoFocus}
|
|
895
919
|
// `inputMode="none"` rather than readOnly, for the same reason Combobox
|
|
896
920
|
// above gives: the sheet carries the keyboard, and a readOnly field would
|
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
1
2
|
import { X } from "lucide-react";
|
|
2
3
|
import { cn } from "../lib/cn";
|
|
3
4
|
import { MiniCalendar } from "./mini-calendar";
|
|
@@ -53,6 +54,15 @@ export function FilterPopover<T>({
|
|
|
53
54
|
// No `"en"` fallback any more: that pinned the calendar to English in a table that
|
|
54
55
|
// was never handed a locale, under a provider that had one.
|
|
55
56
|
const locale = useKitLocale(localeProp);
|
|
57
|
+
// What `autoFocus` did, done by hand: a callback ref runs in the same commit phase
|
|
58
|
+
// React's own autofocus does, once when the input mounts (it is stable across
|
|
59
|
+
// renders, so a re-render does not re-take focus).
|
|
60
|
+
const focusOnMount = useCallback(
|
|
61
|
+
(el: HTMLInputElement | null) => {
|
|
62
|
+
if (el && autoFocus) el.focus();
|
|
63
|
+
},
|
|
64
|
+
[autoFocus],
|
|
65
|
+
);
|
|
56
66
|
const filter = resolveFilter(column);
|
|
57
67
|
if (!filter) return null;
|
|
58
68
|
|
|
@@ -80,11 +90,9 @@ export function FilterPopover<T>({
|
|
|
80
90
|
<input
|
|
81
91
|
type="text"
|
|
82
92
|
// A prop now, defaulting to what the popover needs; see `autoFocus` on the
|
|
83
|
-
// props above.
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
// shrink the backlog without emptying it.
|
|
87
|
-
autoFocus={autoFocus}
|
|
93
|
+
// props above. Focused by `focusOnMount` rather than the `autoFocus`
|
|
94
|
+
// attribute, because here taking focus is the default, not an opt-in.
|
|
95
|
+
ref={focusOnMount}
|
|
88
96
|
value={v.q}
|
|
89
97
|
onChange={(e) => onChange({ type: "text", q: e.target.value })}
|
|
90
98
|
placeholder={labels.filterPlaceholder}
|
|
@@ -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
|
+
}
|