@eifi1/ui-kit 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -32
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +24 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +102 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +23 -2
- package/dist/components/list.js +32 -13
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +15 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group.js +35 -6
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +215 -23
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +156 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +153 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +153 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +153 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +153 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +153 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +37 -17
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +12 -1
- package/dist/shell/topbar-action-menu.js +16 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +224 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +65 -16
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +248 -0
- package/src/components/toggle-group.tsx +58 -10
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/ui.tsx +426 -36
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +102 -8
- package/src/i18n/locales/de.ts +153 -0
- package/src/i18n/locales/es.ts +150 -0
- package/src/i18n/locales/fr.ts +153 -0
- package/src/i18n/locales/hu.ts +151 -0
- package/src/i18n/locales/it.ts +151 -0
- package/src/i18n/locales/zh.ts +150 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +21 -0
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A key combination: modifiers and one key, joined by "+", case-insensitive —
|
|
5
|
+
* `"Ctrl+Shift+F"`, `"Mod+K"`, `"Alt+ArrowUp"`, `"Escape"`, `"?"`.
|
|
6
|
+
*
|
|
7
|
+
* Modifiers: `Ctrl` (`Control`), `Meta` (`Cmd`, `Command`), `Shift`, `Alt` (`Option`),
|
|
8
|
+
* and `Mod` — Cmd on Apple platforms, Ctrl everywhere else, the one to use for an
|
|
9
|
+
* app shortcut. The key is `KeyboardEvent.key` ("f", "Enter", "ArrowUp", "/"), or a
|
|
10
|
+
* `KeyboardEvent.code` ("KeyF", "Digit1") when the physical key is what matters.
|
|
11
|
+
* Several combos: pass an array.
|
|
12
|
+
*/
|
|
13
|
+
export type Hotkey = string;
|
|
14
|
+
|
|
15
|
+
export interface UseHotkeyOptions {
|
|
16
|
+
/** Off while `false` (default `true`). */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
/** Call `preventDefault()` on a match (default `true`): Ctrl+Shift+F must not also
|
|
19
|
+
* open the browser's own dialog. */
|
|
20
|
+
preventDefault?: boolean;
|
|
21
|
+
/** Where to listen. Default `document`. A ref, an element, or `window`. */
|
|
22
|
+
target?: EventTarget | null | { current: EventTarget | null };
|
|
23
|
+
/**
|
|
24
|
+
* Fire while the focus is in a text field, select or contenteditable too. Default
|
|
25
|
+
* `false`: a shortcut must not steal the letters someone is typing. A combo with Ctrl,
|
|
26
|
+
* Meta or Alt is not typing — those still fire unless this is `"never"`.
|
|
27
|
+
*/
|
|
28
|
+
allowInInputs?: boolean | "never";
|
|
29
|
+
/** `keydown` (default) or `keyup`. */
|
|
30
|
+
event?: "keydown" | "keyup";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ParsedHotkey {
|
|
34
|
+
ctrl: boolean;
|
|
35
|
+
meta: boolean;
|
|
36
|
+
shift: boolean;
|
|
37
|
+
alt: boolean;
|
|
38
|
+
key: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether this is an Apple platform, where `Mod` means Cmd. */
|
|
42
|
+
export function isApplePlatform(): boolean {
|
|
43
|
+
if (typeof navigator === "undefined") return false;
|
|
44
|
+
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
|
45
|
+
return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Parse one combo. `apple` decides what `Mod` is. Throws on an empty key. */
|
|
49
|
+
export function parseHotkey(combo: Hotkey, apple = isApplePlatform()): ParsedHotkey {
|
|
50
|
+
const parts = combo
|
|
51
|
+
.split("+")
|
|
52
|
+
.map((p) => p.trim())
|
|
53
|
+
// "Ctrl++" is Ctrl and "+": an empty part after a "+" is the plus key itself.
|
|
54
|
+
.map((p, i, all) => (p === "" && i === all.length - 1 ? "+" : p))
|
|
55
|
+
.filter((p) => p !== "");
|
|
56
|
+
const out: ParsedHotkey = { ctrl: false, meta: false, shift: false, alt: false, key: "" };
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
switch (part.toLowerCase()) {
|
|
59
|
+
case "ctrl":
|
|
60
|
+
case "control":
|
|
61
|
+
out.ctrl = true;
|
|
62
|
+
break;
|
|
63
|
+
case "meta":
|
|
64
|
+
case "cmd":
|
|
65
|
+
case "command":
|
|
66
|
+
out.meta = true;
|
|
67
|
+
break;
|
|
68
|
+
case "mod":
|
|
69
|
+
if (apple) out.meta = true;
|
|
70
|
+
else out.ctrl = true;
|
|
71
|
+
break;
|
|
72
|
+
case "shift":
|
|
73
|
+
out.shift = true;
|
|
74
|
+
break;
|
|
75
|
+
case "alt":
|
|
76
|
+
case "option":
|
|
77
|
+
out.alt = true;
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
out.key = part;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!out.key) throw new Error(`useHotkey: "${combo}" names no key`);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const KEY_ALIASES: Record<string, string> = {
|
|
88
|
+
esc: "escape",
|
|
89
|
+
space: " ",
|
|
90
|
+
del: "delete",
|
|
91
|
+
up: "arrowup",
|
|
92
|
+
down: "arrowdown",
|
|
93
|
+
left: "arrowleft",
|
|
94
|
+
right: "arrowright",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Whether `e` is `combo` — every modifier as named (no more, no fewer), and the key. */
|
|
98
|
+
export function matchesHotkey(e: KeyboardEvent, combo: ParsedHotkey): boolean {
|
|
99
|
+
if (e.ctrlKey !== combo.ctrl || e.metaKey !== combo.meta || e.altKey !== combo.alt) return false;
|
|
100
|
+
const want = KEY_ALIASES[combo.key.toLowerCase()] ?? combo.key.toLowerCase();
|
|
101
|
+
const key = (e.key ?? "").toLowerCase();
|
|
102
|
+
const code = (e.code ?? "").toLowerCase();
|
|
103
|
+
const keyMatches = key === want || code === want;
|
|
104
|
+
if (!keyMatches) return false;
|
|
105
|
+
// Shift must match exactly for a letter or a named key; for a symbol it is how the
|
|
106
|
+
// symbol is typed ("?" is Shift+/ on a US layout, a plain key on others), so an
|
|
107
|
+
// unnamed Shift is not held against it.
|
|
108
|
+
const symbol = want.length === 1 && !/[a-z0-9]/.test(want);
|
|
109
|
+
return symbol && !combo.shift ? true : e.shiftKey === combo.shift;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Is the focus somewhere that takes typed characters? */
|
|
113
|
+
function isTypingTarget(target: EventTarget | null): boolean {
|
|
114
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
115
|
+
if (target.isContentEditable) return true;
|
|
116
|
+
if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) return true;
|
|
117
|
+
if (target instanceof HTMLInputElement) {
|
|
118
|
+
return !["button", "checkbox", "radio", "submit", "reset", "range", "color", "file", "image"].includes(target.type);
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Call `handler` when `combo` is pressed — kastlan's feedback button (Ctrl+Shift+F,
|
|
125
|
+
* feedback-button:60) wired it by hand, and so will every next shortcut.
|
|
126
|
+
*
|
|
127
|
+
* - `Mod` is Cmd on a Mac and Ctrl elsewhere, read once per mount.
|
|
128
|
+
* - Modifiers match EXACTLY: `"Ctrl+K"` does not fire on Ctrl+Shift+K.
|
|
129
|
+
* - Typing in a field is left alone unless `allowInInputs`, except for a combo with
|
|
130
|
+
* Ctrl / Meta / Alt, which no one types as text.
|
|
131
|
+
* - A key held down fires once, not on every repeat.
|
|
132
|
+
* - The newest `handler` is always called; changing it does not re-attach the listener.
|
|
133
|
+
*
|
|
134
|
+
* `useCommandKey` (the ⌘K palette hook) is unchanged; this is its general form.
|
|
135
|
+
*/
|
|
136
|
+
export function useHotkey(
|
|
137
|
+
combo: Hotkey | ReadonlyArray<Hotkey>,
|
|
138
|
+
handler: (event: KeyboardEvent) => void,
|
|
139
|
+
options: UseHotkeyOptions = {},
|
|
140
|
+
): void {
|
|
141
|
+
const { enabled = true, preventDefault = true, target, allowInInputs = false, event = "keydown" } = options;
|
|
142
|
+
const handlerRef = useRef(handler);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
handlerRef.current = handler;
|
|
145
|
+
});
|
|
146
|
+
const key = typeof combo === "string" ? combo : combo.join("\u0000");
|
|
147
|
+
const targetEl = target && "current" in target ? undefined : target;
|
|
148
|
+
const targetRef = target && "current" in target ? target : undefined;
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!enabled) return;
|
|
152
|
+
const node: EventTarget | null =
|
|
153
|
+
targetRef?.current ?? targetEl ?? (typeof document === "undefined" ? null : document);
|
|
154
|
+
if (!node) return;
|
|
155
|
+
const apple = isApplePlatform();
|
|
156
|
+
const parsed = key.split("\u0000").map((c) => parseHotkey(c, apple));
|
|
157
|
+
const listener = (raw: Event) => {
|
|
158
|
+
const e = raw as KeyboardEvent;
|
|
159
|
+
if (e.repeat) return;
|
|
160
|
+
const hit = parsed.find((p) => matchesHotkey(e, p));
|
|
161
|
+
if (!hit) return;
|
|
162
|
+
if (allowInInputs !== true && isTypingTarget(e.target)) {
|
|
163
|
+
const chorded = hit.ctrl || hit.meta || hit.alt;
|
|
164
|
+
if (allowInInputs === "never" || !chorded) return;
|
|
165
|
+
}
|
|
166
|
+
if (preventDefault) e.preventDefault();
|
|
167
|
+
handlerRef.current(e);
|
|
168
|
+
};
|
|
169
|
+
node.addEventListener(event, listener);
|
|
170
|
+
return () => node.removeEventListener(event, listener);
|
|
171
|
+
}, [key, enabled, preventDefault, allowInInputs, event, targetEl, targetRef]);
|
|
172
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useLocation, useNavigate, useSearchParams } from "react-router";
|
|
2
|
+
|
|
3
|
+
type Updater<T> = T | ((prev: T) => T);
|
|
4
|
+
|
|
5
|
+
export interface SearchParamStateOptions<T> {
|
|
6
|
+
/**
|
|
7
|
+
* The value of a param that IS present. Return `undefined` (or throw) for a value
|
|
8
|
+
* that does not parse — a hand-edited or stale link — and the default is used.
|
|
9
|
+
* Default: the raw string.
|
|
10
|
+
*/
|
|
11
|
+
parse?: (raw: string) => T | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The param for a value, or `null` to remove it. Default: `String(value)`, and
|
|
14
|
+
* `null` for `null` / `undefined`. A value that serialises the same as the default
|
|
15
|
+
* removes the param too, so the default is always the clean URL.
|
|
16
|
+
*/
|
|
17
|
+
serialize?: (value: T) => string | null;
|
|
18
|
+
/** Shorthand for `history: "replace"`. */
|
|
19
|
+
replace?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* What a change does to the browser history:
|
|
22
|
+
* - `"push"` (default): a new entry, so Back undoes the change.
|
|
23
|
+
* - `"replace"`: the current entry is rewritten — a tab, a sort, a view toggle,
|
|
24
|
+
* which are not steps anyone wants Back to walk through.
|
|
25
|
+
* - `"replace-on-clear"`: push when a value is set, replace when it is cleared (the
|
|
26
|
+
* "open a form, close it" pattern).
|
|
27
|
+
*/
|
|
28
|
+
history?: "push" | "replace" | "replace-on-clear";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const defaultSerialize = (value: unknown): string | null =>
|
|
32
|
+
value === null || value === undefined ? null : String(value);
|
|
33
|
+
|
|
34
|
+
/** The route state of the current entry, carried over a `replace` so a query rewrite
|
|
35
|
+
* does not drop what a navigation put there (a dialog's opener mark, below). */
|
|
36
|
+
function carriedState(state: unknown): Record<string, unknown> | undefined {
|
|
37
|
+
return state && typeof state === "object" ? (state as Record<string, unknown>) : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One URL search param as React state: `const [view, setView] = useSearchParamState("view", "list")`.
|
|
42
|
+
*
|
|
43
|
+
* Replaces the hand-rolled `setParams((prev) => { const p = new URLSearchParams(prev);
|
|
44
|
+
* …set/delete…; return p })` blocks that keksdose had in a dozen pages and kastlan ported
|
|
45
|
+
* as its own copy of this hook (shared/hooks/use-search-param-state.ts, both apps). The
|
|
46
|
+
* kit's version takes the DEFAULT as an argument rather than inside `parse`, so the
|
|
47
|
+
* two rules that follow from it cannot be forgotten at a call site:
|
|
48
|
+
*
|
|
49
|
+
* * an absent or unparseable param reads as the default, and
|
|
50
|
+
* * setting the default removes the param — the default view is the clean URL.
|
|
51
|
+
*
|
|
52
|
+
* Every other param in the query string is left as it is. Two updates in one tick do
|
|
53
|
+
* not compose — react-router resolves each against the params of the last render.
|
|
54
|
+
* Requires a react-router Router.
|
|
55
|
+
*/
|
|
56
|
+
export function useSearchParamState<T = string>(
|
|
57
|
+
key: string,
|
|
58
|
+
defaultValue: T,
|
|
59
|
+
options: SearchParamStateOptions<T> = {},
|
|
60
|
+
): [T, (next: Updater<T>) => void] {
|
|
61
|
+
const { parse, serialize = defaultSerialize as (value: T) => string | null } = options;
|
|
62
|
+
const history = options.history ?? (options.replace ? "replace" : "push");
|
|
63
|
+
const [params, setParams] = useSearchParams();
|
|
64
|
+
const location = useLocation();
|
|
65
|
+
|
|
66
|
+
const read = (raw: string | null): T => {
|
|
67
|
+
if (raw === null) return defaultValue;
|
|
68
|
+
if (!parse) return raw as unknown as T;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = parse(raw);
|
|
71
|
+
return parsed === undefined ? defaultValue : parsed;
|
|
72
|
+
} catch {
|
|
73
|
+
return defaultValue;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const value = read(params.get(key));
|
|
77
|
+
const defaultSerialized = serialize(defaultValue);
|
|
78
|
+
const toParam = (v: T): string | null => {
|
|
79
|
+
const s = serialize(v);
|
|
80
|
+
return s === defaultSerialized ? null : s;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const setValue = (next: Updater<T>) => {
|
|
84
|
+
// Resolved against the rendered value only to choose the history mode; the write
|
|
85
|
+
// resolves again inside react-router's updater, against the params it hands over.
|
|
86
|
+
const preview = typeof next === "function" ? (next as (prev: T) => T)(value) : next;
|
|
87
|
+
const replace = history === "replace" || (history === "replace-on-clear" && toParam(preview) === null);
|
|
88
|
+
setParams(
|
|
89
|
+
(prev) => {
|
|
90
|
+
const p = new URLSearchParams(prev);
|
|
91
|
+
const resolved = typeof next === "function" ? (next as (prev: T) => T)(read(p.get(key))) : next;
|
|
92
|
+
const serialized = toParam(resolved);
|
|
93
|
+
if (serialized === null) p.delete(key);
|
|
94
|
+
else p.set(key, serialized);
|
|
95
|
+
return p;
|
|
96
|
+
},
|
|
97
|
+
replace ? { replace, state: carriedState(location.state) } : undefined,
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return [value, setValue];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The active tab in `?tab=` (or `key`), with the default tab as the clean URL, for the
|
|
106
|
+
* kit's `Tabs`:
|
|
107
|
+
*
|
|
108
|
+
* const [tab, setTab] = useTabParam("overview", { tabs: ["overview", "history"] });
|
|
109
|
+
* <Tabs tabs={…} active={tab} onChange={setTab} />
|
|
110
|
+
*
|
|
111
|
+
* Switching tabs REPLACES the history entry — a tab is a view of this page, not a
|
|
112
|
+
* place Back should step through. `tabs`, when given, turns a param naming no tab (a
|
|
113
|
+
* renamed tab, an old link) into the default rather than into a blank panel.
|
|
114
|
+
* kastlan's use-tab-param.ts, with the validation it did not have.
|
|
115
|
+
*/
|
|
116
|
+
export function useTabParam<T extends string>(
|
|
117
|
+
defaultTab: T,
|
|
118
|
+
options: { key?: string; tabs?: readonly T[] } = {},
|
|
119
|
+
): [T, (tab: T) => void] {
|
|
120
|
+
const { key = "tab", tabs } = options;
|
|
121
|
+
return useSearchParamState<T>(key, defaultTab, {
|
|
122
|
+
parse: (raw) => (!tabs || (tabs as readonly string[]).includes(raw) ? (raw as T) : undefined),
|
|
123
|
+
history: "replace",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Where a dialog's open state lives: a param's presence (`"edit"` → `?edit=1`), or one
|
|
128
|
+
* value of a shared param (`{ key: "dialog", value: "create-lease" }`). */
|
|
129
|
+
export type DialogParam = string | { key: string; value: string };
|
|
130
|
+
|
|
131
|
+
/** The route-state mark an opener leaves on the entry it pushes, so the close knows it
|
|
132
|
+
* may go BACK over it rather than add a second entry. */
|
|
133
|
+
const OPENED_BY_KIT = "__uiKitDialogParam";
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A dialog's open state in the URL, for `Modal`'s `urlParam` and anything else that
|
|
137
|
+
* opens one: `const [open, setOpen] = useDialogParam({ key: "dialog", value: "create" })`.
|
|
138
|
+
*
|
|
139
|
+
* * **Opening pushes** an entry, so the platform Back gesture closes the dialog — by
|
|
140
|
+
* leaving the entry — and Forward opens it again, like any other step.
|
|
141
|
+
* * **Closing goes back** over that entry when this hook pushed it, so a closed dialog
|
|
142
|
+
* leaves no dead entry behind. A dialog that arrived OPEN (a deep link, a reload, a
|
|
143
|
+
* plain `<Link>` to it) has nothing of ours to go back over, and its close removes the
|
|
144
|
+
* param in place instead — Back from there leaves the page, as it should.
|
|
145
|
+
*
|
|
146
|
+
* That is why it is not `useSearchParamState`: going back is the only close that
|
|
147
|
+
* leaves the history as it was before the dialog, and only the opener knows whether it
|
|
148
|
+
* may.
|
|
149
|
+
*/
|
|
150
|
+
export function useDialogParam(param: DialogParam): [boolean, (open: boolean) => void] {
|
|
151
|
+
const { key, value } = typeof param === "string" ? { key: param, value: "1" } : param;
|
|
152
|
+
const [params, setParams] = useSearchParams();
|
|
153
|
+
const location = useLocation();
|
|
154
|
+
const navigate = useNavigate();
|
|
155
|
+
const open = typeof param === "string" ? params.has(key) : params.get(key) === value;
|
|
156
|
+
const mark = `${key}=${value}`;
|
|
157
|
+
|
|
158
|
+
const setOpen = (next: boolean) => {
|
|
159
|
+
if (next === open) return;
|
|
160
|
+
const state = carriedState(location.state);
|
|
161
|
+
if (next) {
|
|
162
|
+
setParams(
|
|
163
|
+
(prev) => {
|
|
164
|
+
const p = new URLSearchParams(prev);
|
|
165
|
+
p.set(key, value);
|
|
166
|
+
return p;
|
|
167
|
+
},
|
|
168
|
+
{ state: { ...state, [OPENED_BY_KIT]: mark } },
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (state?.[OPENED_BY_KIT] === mark) {
|
|
173
|
+
void navigate(-1);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
setParams(
|
|
177
|
+
(prev) => {
|
|
178
|
+
const p = new URLSearchParams(prev);
|
|
179
|
+
p.delete(key);
|
|
180
|
+
return p;
|
|
181
|
+
},
|
|
182
|
+
{ replace: true, state },
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return [open, setOpen];
|
|
187
|
+
}
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { DEFAULT_TOUR_LABELS } from "../tour/tour";
|
|
|
12
12
|
import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette";
|
|
13
13
|
import { DEFAULT_GLOBAL_SEARCH_LABELS } from "../search/global-search";
|
|
14
14
|
import { DEFAULT_SERIES_CHART_LABELS } from "../components/series-chart-labels";
|
|
15
|
+
import { DEFAULT_PIE_CHART_LABELS } from "../components/pie-chart-labels";
|
|
15
16
|
import { DEFAULT_SPARKLINE_LABELS } from "../components/sparkline";
|
|
16
17
|
import { DEFAULT_STAT_TILE_LABELS } from "../components/stat-tile";
|
|
17
18
|
import { DEFAULT_SIGNATURE_PAD_LABELS } from "../components/signature-pad";
|
|
@@ -25,11 +26,26 @@ import { DEFAULT_MEASURED_GRID_LABELS } from "../components/measured-grid";
|
|
|
25
26
|
import { DEFAULT_CONFIRM_DIALOG_LABELS } from "../components/confirm-dialog";
|
|
26
27
|
import { DEFAULT_FLOATING_PANEL_LABELS } from "../components/floating-panel";
|
|
27
28
|
import { DEFAULT_FEEDBACK_ATTACHMENT_LABELS } from "../feedback/feedback-attachment";
|
|
29
|
+
import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "../feedback/feedback-dialog";
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_FEEDBACK_COMPOSER_LABELS,
|
|
32
|
+
DEFAULT_FEEDBACK_THREAD_LABELS,
|
|
33
|
+
} from "../feedback/feedback-thread";
|
|
34
|
+
import { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "../components/account-settings-labels";
|
|
28
35
|
import { DEFAULT_COPY_BUTTON_LABELS } from "../components/copy-button";
|
|
29
36
|
import { DEFAULT_BULK_ACTION_BAR_LABELS } from "../components/bulk-action-bar";
|
|
30
37
|
import { DEFAULT_LIST_LABELS } from "../components/list";
|
|
31
38
|
import { DEFAULT_BREADCRUMBS_LABELS } from "../components/breadcrumbs";
|
|
32
39
|
import { DEFAULT_TOAST_LABELS } from "../components/toast";
|
|
40
|
+
import { DEFAULT_FORM_ACTIONS_LABELS } from "../components/form-actions";
|
|
41
|
+
import { DEFAULT_DESCRIPTION_LIST_LABELS } from "../components/description-list";
|
|
42
|
+
import { DEFAULT_LINE_ITEMS_LABELS } from "../components/line-items";
|
|
43
|
+
import { DEFAULT_PROGRESS_BAR_LABELS } from "../components/progress-bar";
|
|
44
|
+
import { DEFAULT_SIGNED_AMOUNT_LABELS } from "../components/signed-amount";
|
|
45
|
+
import { DEFAULT_ERROR_BOUNDARY_LABELS } from "../components/error-boundary";
|
|
46
|
+
import { DEFAULT_AUTHED_IMAGE_LABELS } from "../components/authed-image";
|
|
47
|
+
import { DEFAULT_IMAGE_GRID_LABELS } from "../components/image-grid";
|
|
48
|
+
import { DEFAULT_LIGHTBOX_LABELS } from "../components/lightbox";
|
|
33
49
|
import {
|
|
34
50
|
DEFAULT_APP_SHELL_LABELS,
|
|
35
51
|
DEFAULT_CALCULATOR_LABELS,
|
|
@@ -80,6 +96,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
80
96
|
commandPalette: DEFAULT_COMMAND_PALETTE_LABELS,
|
|
81
97
|
globalSearch: DEFAULT_GLOBAL_SEARCH_LABELS,
|
|
82
98
|
seriesChart: DEFAULT_SERIES_CHART_LABELS,
|
|
99
|
+
pieChart: DEFAULT_PIE_CHART_LABELS,
|
|
83
100
|
sparkline: DEFAULT_SPARKLINE_LABELS,
|
|
84
101
|
statTile: DEFAULT_STAT_TILE_LABELS,
|
|
85
102
|
signaturePad: DEFAULT_SIGNATURE_PAD_LABELS,
|
|
@@ -91,6 +108,10 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
91
108
|
filePicker: DEFAULT_FILE_PICKER_LABELS,
|
|
92
109
|
measuredGrid: DEFAULT_MEASURED_GRID_LABELS,
|
|
93
110
|
feedbackAttachment: DEFAULT_FEEDBACK_ATTACHMENT_LABELS,
|
|
111
|
+
feedbackDialog: DEFAULT_FEEDBACK_DIALOG_LABELS,
|
|
112
|
+
feedbackThread: DEFAULT_FEEDBACK_THREAD_LABELS,
|
|
113
|
+
feedbackComposer: DEFAULT_FEEDBACK_COMPOSER_LABELS,
|
|
114
|
+
accountSettings: DEFAULT_ACCOUNT_SETTINGS_LABELS,
|
|
94
115
|
confirmDialog: DEFAULT_CONFIRM_DIALOG_LABELS,
|
|
95
116
|
floatingPanel: DEFAULT_FLOATING_PANEL_LABELS,
|
|
96
117
|
copyButton: DEFAULT_COPY_BUTTON_LABELS,
|
|
@@ -98,4 +119,13 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
98
119
|
list: DEFAULT_LIST_LABELS,
|
|
99
120
|
breadcrumbs: DEFAULT_BREADCRUMBS_LABELS,
|
|
100
121
|
toast: DEFAULT_TOAST_LABELS,
|
|
122
|
+
form: DEFAULT_FORM_ACTIONS_LABELS,
|
|
123
|
+
descriptionList: DEFAULT_DESCRIPTION_LIST_LABELS,
|
|
124
|
+
lineItems: DEFAULT_LINE_ITEMS_LABELS,
|
|
125
|
+
progressBar: DEFAULT_PROGRESS_BAR_LABELS,
|
|
126
|
+
signedAmount: DEFAULT_SIGNED_AMOUNT_LABELS,
|
|
127
|
+
errorBoundary: DEFAULT_ERROR_BOUNDARY_LABELS,
|
|
128
|
+
authedImage: DEFAULT_AUTHED_IMAGE_LABELS,
|
|
129
|
+
imageGrid: DEFAULT_IMAGE_GRID_LABELS,
|
|
130
|
+
lightbox: DEFAULT_LIGHTBOX_LABELS,
|
|
101
131
|
};
|
package/src/i18n/kit-labels.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import type { AnchorHTMLAttributes, ReactElement, Ref } from "react";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import type { DataTableLabels } from "../components/data-table-labels";
|
|
4
5
|
import type { MiniCalendarLabels, WeekDay } from "../components/mini-calendar";
|
|
@@ -14,6 +15,7 @@ import type { GlobalSearchLabels } from "../search/global-search";
|
|
|
14
15
|
import type { MonthPickerLabels } from "../components/month-picker";
|
|
15
16
|
import type { PageContentsLabels } from "../components/page-contents";
|
|
16
17
|
import type { SeriesChartLabels } from "../components/series-chart-labels";
|
|
18
|
+
import type { PieChartLabels } from "../components/pie-chart-labels";
|
|
17
19
|
import type { SparklineLabels } from "../components/sparkline";
|
|
18
20
|
import type { StatTileLabels } from "../components/stat-tile";
|
|
19
21
|
import type { SignaturePadLabels } from "../components/signature-pad";
|
|
@@ -25,6 +27,9 @@ import type { DialogFrameLabels } from "../components/dialog-frame";
|
|
|
25
27
|
import type { FilePickerLabels } from "../components/file-button";
|
|
26
28
|
import type { MeasuredGridLabels } from "../components/measured-grid";
|
|
27
29
|
import type { FeedbackAttachmentFieldLabels } from "../feedback/feedback-attachment";
|
|
30
|
+
import type { FeedbackDialogTextLabels } from "../feedback/feedback-dialog";
|
|
31
|
+
import type { FeedbackComposerLabels, FeedbackThreadLabels } from "../feedback/feedback-thread";
|
|
32
|
+
import type { AccountSettingsLabels } from "../components/account-settings-labels";
|
|
28
33
|
import type { ConfirmDialogLabels } from "../components/confirm-dialog";
|
|
29
34
|
import type { FloatingPanelLabels } from "../components/floating-panel";
|
|
30
35
|
import type { CopyButtonLabels } from "../components/copy-button";
|
|
@@ -32,6 +37,15 @@ import type { BulkActionBarLabels } from "../components/bulk-action-bar";
|
|
|
32
37
|
import type { ListLabels } from "../components/list";
|
|
33
38
|
import type { BreadcrumbsLabels } from "../components/breadcrumbs";
|
|
34
39
|
import type { ToastLabels } from "../components/toast";
|
|
40
|
+
import type { FormActionsLabels } from "../components/form-actions";
|
|
41
|
+
import type { DescriptionListLabels } from "../components/description-list";
|
|
42
|
+
import type { LineItemsLabels } from "../components/line-items";
|
|
43
|
+
import type { ProgressBarLabels } from "../components/progress-bar";
|
|
44
|
+
import type { SignedAmountLabels } from "../components/signed-amount";
|
|
45
|
+
import type { ErrorBoundaryLabels } from "../components/error-boundary";
|
|
46
|
+
import type { AuthedImageLabels } from "../components/authed-image";
|
|
47
|
+
import type { ImageGridLabels } from "../components/image-grid";
|
|
48
|
+
import type { LightboxLabels } from "../components/lightbox";
|
|
35
49
|
|
|
36
50
|
/**
|
|
37
51
|
* EVERY string the kit renders, as one typed tree — and an optional provider that
|
|
@@ -84,6 +98,9 @@ export interface CommonLabels {
|
|
|
84
98
|
/** The × that puts away a banner or a notice (`AlertBanner onDismiss`). Not `close`:
|
|
85
99
|
* nothing opened, and "Close" on a banner reads as closing the page it sits on. */
|
|
86
100
|
dismiss: string;
|
|
101
|
+
/** Read after a link that opens a new tab (`<Button href external>`, TextLink):
|
|
102
|
+
* nothing else tells a screen reader the page is about to change tabs. */
|
|
103
|
+
opensInNewTab: string;
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
|
|
@@ -221,6 +238,7 @@ export interface UiKitLabels {
|
|
|
221
238
|
commandPalette: CommandPaletteLabels;
|
|
222
239
|
globalSearch: GlobalSearchLabels;
|
|
223
240
|
seriesChart: SeriesChartLabels;
|
|
241
|
+
pieChart: PieChartLabels;
|
|
224
242
|
sparkline: SparklineLabels;
|
|
225
243
|
statTile: StatTileLabels;
|
|
226
244
|
signaturePad: SignaturePadLabels;
|
|
@@ -232,6 +250,14 @@ export interface UiKitLabels {
|
|
|
232
250
|
filePicker: FilePickerLabels;
|
|
233
251
|
measuredGrid: MeasuredGridLabels;
|
|
234
252
|
feedbackAttachment: FeedbackAttachmentFieldLabels;
|
|
253
|
+
/** 0.12.0: `FeedbackDialog`'s own strings; `FeedbackThread` / `FeedbackComposer`. */
|
|
254
|
+
feedbackDialog: FeedbackDialogTextLabels;
|
|
255
|
+
feedbackThread: FeedbackThreadLabels;
|
|
256
|
+
feedbackComposer: FeedbackComposerLabels;
|
|
257
|
+
/** 0.12.0: `ProfileSetting`, `PasswordSetting`, `TwoFactorSetting`, `PasskeysSetting`
|
|
258
|
+
* — one record per section. The provider takes a whole section record; the
|
|
259
|
+
* components merge it key by key, as a prop. */
|
|
260
|
+
accountSettings: AccountSettingsLabels;
|
|
235
261
|
confirmDialog: ConfirmDialogLabels;
|
|
236
262
|
floatingPanel: FloatingPanelLabels;
|
|
237
263
|
copyButton: CopyButtonLabels;
|
|
@@ -239,11 +265,36 @@ export interface UiKitLabels {
|
|
|
239
265
|
list: ListLabels;
|
|
240
266
|
breadcrumbs: BreadcrumbsLabels;
|
|
241
267
|
toast: ToastLabels;
|
|
268
|
+
form: FormActionsLabels;
|
|
269
|
+
descriptionList: DescriptionListLabels;
|
|
270
|
+
lineItems: LineItemsLabels;
|
|
271
|
+
progressBar: ProgressBarLabels;
|
|
272
|
+
signedAmount: SignedAmountLabels;
|
|
273
|
+
errorBoundary: ErrorBoundaryLabels;
|
|
274
|
+
authedImage: AuthedImageLabels;
|
|
275
|
+
imageGrid: ImageGridLabels;
|
|
276
|
+
lightbox: LightboxLabels;
|
|
242
277
|
}
|
|
243
278
|
|
|
244
|
-
/**
|
|
245
|
-
*
|
|
246
|
-
|
|
279
|
+
/**
|
|
280
|
+
* A label override, as deep as the labels go: an object of labels (a namespace, or a
|
|
281
|
+
* record inside one such as `accountSettings.passkeys` or `dataTable.presets`) may be
|
|
282
|
+
* partial at every level, because the provider merges it key by key at every level.
|
|
283
|
+
* A leaf stays as it is: a string, a function-valued label (`(n) => …`), and anything
|
|
284
|
+
* that is not purely an object (a `ReactNode` union) is given whole.
|
|
285
|
+
*/
|
|
286
|
+
export type LabelOverride<T> = [T] extends [(...args: never[]) => unknown]
|
|
287
|
+
? T
|
|
288
|
+
: [T] extends [readonly unknown[]]
|
|
289
|
+
? T
|
|
290
|
+
: [T] extends [object]
|
|
291
|
+
? { [K in keyof T]?: LabelOverride<T[K]> }
|
|
292
|
+
: T;
|
|
293
|
+
|
|
294
|
+
/** Any subset of the tree — each namespace may be partial, and so may any record
|
|
295
|
+
* inside one (`accountSettings.passkeys`, `dataTable.presets`): they are merged key
|
|
296
|
+
* by key, at every depth. */
|
|
297
|
+
export type UiKitLabelOverrides = { [K in keyof UiKitLabels]?: LabelOverride<UiKitLabels[K]> };
|
|
247
298
|
|
|
248
299
|
/* ── English defaults for the namespaces defined here ────────────────────── */
|
|
249
300
|
|
|
@@ -261,6 +312,7 @@ export const DEFAULT_COMMON_LABELS: CommonLabels = {
|
|
|
261
312
|
noResults: "No results",
|
|
262
313
|
fieldValue: (field, value) => `${field}: ${value}`,
|
|
263
314
|
dismiss: "Dismiss",
|
|
315
|
+
opensInNewTab: "opens in a new tab",
|
|
264
316
|
};
|
|
265
317
|
|
|
266
318
|
export const DEFAULT_DATE_PICKER_LABELS: DatePickerLabels = {
|
|
@@ -364,8 +416,20 @@ interface KitI18n {
|
|
|
364
416
|
labels?: UiKitLabelOverrides;
|
|
365
417
|
locale?: string;
|
|
366
418
|
weekStartsOn?: WeekDay;
|
|
419
|
+
linkComponent?: KitLinkComponent;
|
|
367
420
|
}
|
|
368
421
|
|
|
422
|
+
/**
|
|
423
|
+
* What every kit link receives: the anchor attributes the kit decided (href, class,
|
|
424
|
+
* aria-current, handlers, children, ref). An app's router link maps them —
|
|
425
|
+
* `({ href, ...p }) => <Link to={href} {...p} />` — ONCE, on the provider.
|
|
426
|
+
*/
|
|
427
|
+
export type KitLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
428
|
+
href: string;
|
|
429
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
430
|
+
};
|
|
431
|
+
export type KitLinkComponent = (props: KitLinkProps) => ReactElement;
|
|
432
|
+
|
|
369
433
|
const KitI18nContext = createContext<KitI18n>({});
|
|
370
434
|
|
|
371
435
|
export interface UiKitProviderProps {
|
|
@@ -387,6 +451,14 @@ export interface UiKitProviderProps {
|
|
|
387
451
|
* and switching it to `en-GB` to get Monday changes every date and number format.
|
|
388
452
|
*/
|
|
389
453
|
weekStartsOn?: WeekDay;
|
|
454
|
+
/**
|
|
455
|
+
* The app's router link, used by every kit component that renders an in-app link
|
|
456
|
+
* and was not handed its own `renderLink` — so `renderLink` stops being repeated on
|
|
457
|
+
* each ListItem, Breadcrumbs, StatTile, Chip… (kastlan). A component's own
|
|
458
|
+
* `renderLink` wins; external links and in-page `#anchor`s stay plain `<a>`, and a
|
|
459
|
+
* hash-router `#/path` is handed to it like `/path` (see `pickLinkRenderer`).
|
|
460
|
+
*/
|
|
461
|
+
linkComponent?: KitLinkComponent;
|
|
390
462
|
children: ReactNode;
|
|
391
463
|
}
|
|
392
464
|
|
|
@@ -398,15 +470,22 @@ export interface UiKitProviderProps {
|
|
|
398
470
|
* it names, so a page can re-label one table's `dataTable.table` without restating
|
|
399
471
|
* the language.
|
|
400
472
|
*/
|
|
401
|
-
export function UiKitProvider({
|
|
473
|
+
export function UiKitProvider({
|
|
474
|
+
labels,
|
|
475
|
+
locale,
|
|
476
|
+
weekStartsOn,
|
|
477
|
+
linkComponent,
|
|
478
|
+
children,
|
|
479
|
+
}: UiKitProviderProps) {
|
|
402
480
|
const outer = useContext(KitI18nContext);
|
|
403
481
|
const value = useMemo<KitI18n>(
|
|
404
482
|
() => ({
|
|
405
483
|
locale: locale ?? outer.locale,
|
|
406
484
|
weekStartsOn: weekStartsOn ?? outer.weekStartsOn,
|
|
407
485
|
labels: mergeOverrides(outer.labels, labels),
|
|
486
|
+
linkComponent: linkComponent ?? outer.linkComponent,
|
|
408
487
|
}),
|
|
409
|
-
[outer, labels, locale, weekStartsOn],
|
|
488
|
+
[outer, labels, locale, weekStartsOn, linkComponent],
|
|
410
489
|
);
|
|
411
490
|
return <KitI18nContext.Provider value={value}>{children}</KitI18nContext.Provider>;
|
|
412
491
|
}
|
|
@@ -415,14 +494,23 @@ function isRecord(v: unknown): v is Record<string, unknown> {
|
|
|
415
494
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
416
495
|
}
|
|
417
496
|
|
|
418
|
-
/**
|
|
419
|
-
|
|
497
|
+
/** A record of labels to merge into — not a React element, which is an object too but
|
|
498
|
+
* is a label given whole. */
|
|
499
|
+
function isLabelRecord(v: unknown): v is Record<string, unknown> {
|
|
500
|
+
return isRecord(v) && !("$$typeof" in v);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Merged key by key at EVERY depth — a namespace, a record inside it
|
|
504
|
+
* (`accountSettings.passkeys`, `dataTable.presets`), and so on down — to match
|
|
505
|
+
* {@link LabelOverride}. An `undefined` is skipped at every depth, so it never blanks
|
|
506
|
+
* out what is under it. */
|
|
507
|
+
function mergeNamespace<T extends object>(base: T, over: LabelOverride<T> | Partial<T> | undefined): T {
|
|
420
508
|
if (!over) return base;
|
|
421
509
|
const out = { ...base } as Record<string, unknown>;
|
|
422
510
|
for (const [k, v] of Object.entries(over)) {
|
|
423
511
|
if (v === undefined) continue;
|
|
424
512
|
const prev = out[k];
|
|
425
|
-
out[k] =
|
|
513
|
+
out[k] = isLabelRecord(prev) && isLabelRecord(v) ? mergeNamespace(prev, v) : v;
|
|
426
514
|
}
|
|
427
515
|
return out as T;
|
|
428
516
|
}
|
|
@@ -475,6 +563,12 @@ export function useKitLocale(prop?: string): string | undefined {
|
|
|
475
563
|
/** The week start the nearest `<UiKitProvider weekStartsOn>` pins, else `undefined`
|
|
476
564
|
* (the caller then asks the locale). A component's own prop goes first:
|
|
477
565
|
* `prop ?? useKitWeekStart()`. */
|
|
566
|
+
/** The provider's router link, or `undefined` — a component's own `renderLink` should
|
|
567
|
+
* win over it: `const Link = renderLink ?? useKitLink()`. */
|
|
568
|
+
export function useKitLink(): KitLinkComponent | undefined {
|
|
569
|
+
return useContext(KitI18nContext).linkComponent;
|
|
570
|
+
}
|
|
571
|
+
|
|
478
572
|
export function useKitWeekStart(): WeekDay | undefined {
|
|
479
573
|
return useContext(KitI18nContext).weekStartsOn;
|
|
480
574
|
}
|