@eifi1/ui-kit 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -37
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/alert-banner.d.ts +26 -1
- package/dist/components/alert-banner.js +10 -7
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +26 -4
- package/dist/components/amount-input.js +5 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +51 -5
- package/dist/components/bulk-action-bar.js +88 -41
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +107 -3
- package/dist/components/button-group.js +60 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +17 -4
- package/dist/components/calendar-heatmap.d.ts +162 -0
- package/dist/components/calendar-heatmap.js +336 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +6 -3
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +66 -2
- package/dist/components/choice-card.js +108 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.js +2 -0
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +52 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +104 -12
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +18 -3
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +92 -0
- package/dist/components/error-boundary.js +111 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field.d.ts +69 -0
- package/dist/components/field.js +75 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +51 -208
- package/dist/components/file-dropzone.d.ts +50 -2
- package/dist/components/floating-panel.d.ts +156 -4
- package/dist/components/floating-panel.js +193 -29
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +37 -3
- package/dist/components/list.js +39 -14
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +13 -2
- package/dist/components/menu-item.js +11 -2
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +82 -5
- package/dist/components/mini-calendar.js +212 -64
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +19 -3
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +67 -0
- package/dist/components/nav-pills.js +69 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +26 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +27 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +17 -4
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +2 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/progress-bar.d.ts +73 -3
- package/dist/components/progress-bar.js +110 -8
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +4 -1
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +50 -1
- package/dist/components/signed-amount.d.ts +121 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +5 -2
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +6 -2
- package/dist/components/status-dot.js +6 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/table.d.ts +77 -11
- package/dist/components/table.js +85 -17
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +176 -0
- package/dist/components/text-link.js +109 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +8 -1
- package/dist/components/text.js +1 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +50 -1
- package/dist/components/toggle-group.d.ts +28 -0
- package/dist/components/toggle-group.js +58 -9
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +36 -3
- package/dist/components/toggle-legend.js +49 -0
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +11 -3
- package/dist/components/tooltip.js +2 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +52 -729
- package/dist/components/ui.js +296 -27
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +16 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +133 -0
- package/dist/feedback/feedback-thread.js +237 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +50 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +17 -4
- package/dist/i18n/defaults.js +33 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +47 -310
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
- package/dist/i18n/locales/de-CH.d.ts +17 -4
- package/dist/i18n/locales/de-informal.d.ts +17 -4
- package/dist/i18n/locales/de.d.ts +17 -4
- package/dist/i18n/locales/de.js +165 -4
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +17 -4
- package/dist/i18n/locales/es.js +162 -4
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +17 -4
- package/dist/i18n/locales/fr.js +162 -4
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +17 -4
- package/dist/i18n/locales/hu.js +162 -4
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +17 -4
- package/dist/i18n/locales/it.js +162 -4
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +17 -4
- package/dist/i18n/locales/zh.js +162 -4
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.js +43 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
- package/dist/lib/clipping.d.ts +21 -5
- package/dist/lib/clipping.js +3 -0
- package/dist/lib/clipping.js.map +1 -1
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/lib/format.d.ts +144 -0
- package/dist/lib/format.js +149 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +241 -0
- package/dist/rhf/fields.js +601 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +50 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +63 -1
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +58 -3
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +84 -3
- package/dist/shell/topbar-action-menu.js +115 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +58 -2
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -1
- package/dist/wizard/wizard-context.d.ts +30 -1
- package/dist/wizard/wizard-context.js +23 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +20 -5
- package/dist/wizard/wizard-step.js +16 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +51 -3
- package/package.json +1 -1
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/alert-banner.tsx +36 -7
- package/src/components/amount-input.tsx +13 -1
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +163 -57
- package/src/components/button-group.tsx +129 -4
- package/src/components/calendar-heatmap.tsx +599 -0
- package/src/components/chip.tsx +8 -4
- package/src/components/choice-card.tsx +208 -1
- package/src/components/combobox-core.tsx +5 -0
- package/src/components/combobox.tsx +31 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +231 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +18 -3
- package/src/components/error-boundary.tsx +216 -0
- package/src/components/field.tsx +188 -0
- package/src/components/floating-panel.tsx +380 -16
- package/src/components/form-actions.tsx +191 -0
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +93 -18
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +29 -3
- package/src/components/mini-calendar.tsx +317 -57
- package/src/components/modal.tsx +61 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +19 -3
- package/src/components/nav-pills.tsx +152 -0
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +16 -1
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +7 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/progress-bar.tsx +241 -13
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/series-chart.tsx +21 -0
- package/src/components/signed-amount.tsx +286 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +8 -2
- package/src/components/status-dot.tsx +10 -2
- package/src/components/table.tsx +186 -23
- package/src/components/text-link.tsx +248 -0
- package/src/components/text.tsx +9 -1
- package/src/components/toggle-group.tsx +106 -13
- package/src/components/toggle-legend.tsx +87 -2
- package/src/components/tooltip.tsx +17 -3
- package/src/components/ui.tsx +572 -44
- package/src/components/use-table-state.ts +265 -33
- package/src/components/user-avatar.tsx +57 -3
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-dialog.tsx +42 -3
- package/src/feedback/feedback-thread.tsx +434 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +32 -0
- package/src/i18n/kit-labels.tsx +104 -8
- package/src/i18n/locales/de.ts +162 -0
- package/src/i18n/locales/es.ts +159 -0
- package/src/i18n/locales/fr.ts +162 -0
- package/src/i18n/locales/hu.ts +160 -0
- package/src/i18n/locales/it.ts +160 -0
- package/src/i18n/locales/zh.ts +159 -0
- package/src/index.ts +64 -0
- package/src/lib/clipping.ts +22 -4
- package/src/lib/dates.ts +37 -0
- package/src/lib/format.ts +325 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1054 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +8 -0
- package/src/shell/app-shell.tsx +147 -2
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +260 -49
- package/src/shell.ts +2 -0
- package/src/wizard/wizard-context.tsx +50 -1
- package/src/wizard/wizard-step.tsx +43 -6
- package/tokens.css +12 -0
package/src/index.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
export * from "./lib/calc";
|
|
8
8
|
export { cn } from "./lib/cn";
|
|
9
9
|
export { logger, setStoreLog } from "./lib/logger";
|
|
10
|
+
// Intl number / money / percent / date / relative-time formatting, and `useKitFormat()`
|
|
11
|
+
// for the same bound to the provider's locale (kastlan's formatters.ts, English "5m ago").
|
|
12
|
+
export * from "./lib/format";
|
|
13
|
+
// localStorage that never throws (Safari private mode, blocked site data).
|
|
14
|
+
export { readStored, writeStored } from "./lib/safe-storage";
|
|
10
15
|
// Date helpers are also available via the "@eifi1/ui-kit/dates" subpath.
|
|
11
16
|
|
|
12
17
|
// ── hooks ────────────────────────────────────────────────────────────────────
|
|
@@ -38,9 +43,19 @@ export type { UseFileDropOptions, UseFileDropReturn, FileDropProps } from "./hoo
|
|
|
38
43
|
// kastlan's search boxes each carried a hand-rolled copy, two without the cleanup.
|
|
39
44
|
export { useDebounce, useDebouncedCallback } from "./hooks/use-debounce";
|
|
40
45
|
export type { DebouncedCallbackOptions, DebouncedFunction } from "./hooks/use-debounce";
|
|
46
|
+
// One search param as state, the active tab in `?tab=`, and a dialog's open state in the
|
|
47
|
+
// URL (Modal's `urlParam`) — kastlan's and keksdose's use-search-param-state, in the kit.
|
|
48
|
+
export { useSearchParamState, useTabParam, useDialogParam } from "./hooks/use-search-param-state";
|
|
49
|
+
export type { SearchParamStateOptions, DialogParam } from "./hooks/use-search-param-state";
|
|
41
50
|
// Copy that reports whether it worked — keksdose's copy buttons said "Copied" when it had not.
|
|
42
51
|
export { useCopyToClipboard, copyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
43
52
|
export type { CopyState, UseCopyToClipboardOptions, UseCopyToClipboardReturn } from "./hooks/use-copy-to-clipboard";
|
|
53
|
+
// Keyboard shortcuts ("Mod+K", "Ctrl+Shift+F") that leave typing in fields alone.
|
|
54
|
+
export { useHotkey, parseHotkey, matchesHotkey } from "./hooks/use-hotkey";
|
|
55
|
+
export type { Hotkey, ParsedHotkey, UseHotkeyOptions } from "./hooks/use-hotkey";
|
|
56
|
+
// A protected file as an object URL, fetched with the app's own auth (kastlan's AuthedImage).
|
|
57
|
+
export { useAuthedSrc } from "./hooks/use-authed-src";
|
|
58
|
+
export type { AuthedFetcher, AuthedSrcStatus, UseAuthedSrcOptions, UseAuthedSrcResult } from "./hooks/use-authed-src";
|
|
44
59
|
|
|
45
60
|
// ── theme / palettes ─────────────────────────────────────────────────────────
|
|
46
61
|
export * from "./theme/chart-palette";
|
|
@@ -56,6 +71,8 @@ export * from "./theme/palette-store";
|
|
|
56
71
|
|
|
57
72
|
// ── components ───────────────────────────────────────────────────────────────
|
|
58
73
|
export * from "./components/ui";
|
|
74
|
+
// The label-ABOVE field group (label, hint, error, required), inside wizards or out.
|
|
75
|
+
export * from "./components/field";
|
|
59
76
|
export * from "./components/search-field";
|
|
60
77
|
export * from "./components/dropdown";
|
|
61
78
|
export * from "./components/popover";
|
|
@@ -123,9 +140,19 @@ export * from "./components/chart-zoom";
|
|
|
123
140
|
export * from "./components/toggle-legend";
|
|
124
141
|
export * from "./components/facing-pair";
|
|
125
142
|
export * from "./components/series-chart-labels";
|
|
143
|
+
export * from "./components/pie-chart";
|
|
144
|
+
export * from "./components/pie-chart-labels";
|
|
126
145
|
export * from "./components/account-settings";
|
|
146
|
+
// 0.12.0: the fourth account section, and the QR code TwoFactorSetting draws its
|
|
147
|
+
// otpauth URI with (encoder in lib/qr-encode.ts — no dependency, see there).
|
|
148
|
+
export * from "./components/passkeys-setting";
|
|
149
|
+
export * from "./components/qr-code";
|
|
150
|
+
export { encodeQr } from "./lib/qr-encode";
|
|
151
|
+
export type { QrEncodeOptions, QrErrorCorrection, QrMatrix } from "./lib/qr-encode";
|
|
127
152
|
export * from "./components/alert-banner";
|
|
128
153
|
export * from "./components/toggle-group";
|
|
154
|
+
// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.
|
|
155
|
+
export * from "./components/nav-pills";
|
|
129
156
|
// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is
|
|
130
157
|
// given — and the list field built from it. Distinct from Button on purpose: a row of
|
|
131
158
|
// buttons reads as "choose an action", a row of chips as "here are the things".
|
|
@@ -137,16 +164,29 @@ export * from "./components/full-bleed-dialog";
|
|
|
137
164
|
export * from "./components/grouped-picker";
|
|
138
165
|
export * from "./components/file-dropzone";
|
|
139
166
|
export * from "./components/mini-calendar";
|
|
167
|
+
export * from "./components/calendar-heatmap";
|
|
140
168
|
export * from "./components/date-picker";
|
|
141
169
|
export * from "./components/tree-view";
|
|
142
170
|
export * from "./components/chart";
|
|
143
171
|
// 0.8.0 layout and feedback primitives the apps hand-rolled or took from shadcn/Radix.
|
|
144
172
|
export * from "./components/description-list";
|
|
173
|
+
// The Save / Cancel row forms and dialogs end in, and the editable line-item repeater
|
|
174
|
+
// (journal lines, lease components, invoice positions). 0.12.0.
|
|
175
|
+
export * from "./components/form-actions";
|
|
176
|
+
export * from "./components/line-items";
|
|
145
177
|
export * from "./components/progress-bar";
|
|
146
178
|
// Named: skeleton.tsx also holds SKELETON_CLASS, the look StatTile shares — internal.
|
|
147
179
|
export { Skeleton } from "./components/skeleton";
|
|
148
180
|
export type { SkeletonProps, SkeletonShape } from "./components/skeleton";
|
|
181
|
+
// 0.12.0: a centred spinner with words, signed figures and deltas, a render-error fallback.
|
|
182
|
+
export * from "./components/loading-state";
|
|
183
|
+
export * from "./components/signed-amount";
|
|
184
|
+
export * from "./components/error-boundary";
|
|
149
185
|
export * from "./components/copy-button";
|
|
186
|
+
export { AuthedImage, DEFAULT_AUTHED_IMAGE_LABELS } from "./components/authed-image";
|
|
187
|
+
export type { AuthedImageProps, AuthedImageLabels } from "./components/authed-image";
|
|
188
|
+
export * from "./components/image-grid";
|
|
189
|
+
export * from "./components/lightbox";
|
|
150
190
|
export * from "./components/button-group";
|
|
151
191
|
export * from "./components/table";
|
|
152
192
|
export * from "./components/separator";
|
|
@@ -160,6 +200,16 @@ export * from "./components/text";
|
|
|
160
200
|
export * from "./components/status-dot";
|
|
161
201
|
export * from "./components/page-header";
|
|
162
202
|
export * from "./components/breadcrumbs";
|
|
203
|
+
// The one inline link (kastlan's EntityLink / CellLink / legal and source links), through
|
|
204
|
+
// `<UiKitProvider linkComponent>`. Named: the file also holds the kit's internal link picker.
|
|
205
|
+
export { TextLink } from "./components/text-link";
|
|
206
|
+
export type {
|
|
207
|
+
TextLinkProps,
|
|
208
|
+
TextLinkRenderProps,
|
|
209
|
+
TextLinkTone,
|
|
210
|
+
TextLinkUnderline,
|
|
211
|
+
TextLinkCurrent,
|
|
212
|
+
} from "./components/text-link";
|
|
163
213
|
// The toast layer over sonner (an OPTIONAL peer, loaded lazily): `toast` mirrors sonner's
|
|
164
214
|
// API so apps migrate by swapping the import; `<Toaster>` carries the placement, theme,
|
|
165
215
|
// tones and z-index both apps had wired by hand.
|
|
@@ -197,7 +247,18 @@ export type {
|
|
|
197
247
|
MobileSwipeActions,
|
|
198
248
|
DataTableDensity,
|
|
199
249
|
DataTableChrome,
|
|
250
|
+
DataTableRowAction,
|
|
200
251
|
} from "./components/data-table";
|
|
252
|
+
// 0.12.0: the yes/no cell, and a server table's state held by its owner, URL-synced.
|
|
253
|
+
export * from "./components/data-table-cells";
|
|
254
|
+
export { useTableUrlState, readTableUrlState } from "./components/use-table-state";
|
|
255
|
+
export type {
|
|
256
|
+
TableUrlState,
|
|
257
|
+
TableUrlSync,
|
|
258
|
+
TableUrlStateProps,
|
|
259
|
+
UseTableUrlStateOptions,
|
|
260
|
+
UseTableUrlStateReturn,
|
|
261
|
+
} from "./components/use-table-state";
|
|
201
262
|
|
|
202
263
|
// ── shell (composable app chrome) ────────────────────────────────────────────
|
|
203
264
|
export * from "./shell/topbar-controls";
|
|
@@ -206,6 +267,8 @@ export * from "./shell/app-shell";
|
|
|
206
267
|
export * from "./shell/option-switcher-menu";
|
|
207
268
|
export * from "./shell/role-switcher";
|
|
208
269
|
export * from "./shell/topbar-action-menu";
|
|
270
|
+
export * from "./shell/top-bar-brand";
|
|
271
|
+
export * from "./shell/auth-layout";
|
|
209
272
|
|
|
210
273
|
// ── feedback ─────────────────────────────────────────────────────────────────
|
|
211
274
|
// The form somebody files a report with, and the parts an inbox is built from:
|
|
@@ -217,6 +280,7 @@ export * from "./shell/topbar-action-menu";
|
|
|
217
280
|
export * from "./feedback/feedback-attachment";
|
|
218
281
|
export * from "./feedback/feedback-dialog";
|
|
219
282
|
export * from "./feedback/feedback-inbox";
|
|
283
|
+
export * from "./feedback/feedback-thread";
|
|
220
284
|
|
|
221
285
|
// ── multi-step wizard ────────────────────────────────────────────────────────
|
|
222
286
|
// The engine (step machine, validators, collected data), the chrome around it
|
package/src/lib/clipping.ts
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
/** Internal (not exported from the package): the Tooltip's auto-portal check.
|
|
1
|
+
/** Internal (not exported from the package): the Tooltip's auto-portal check. The
|
|
2
|
+
* attribute name itself IS exported, as `CLIPS_ATTRIBUTE` from the tooltip module. */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The explicit "this element clips or scrolls" marker. A container carrying it counts
|
|
6
|
+
* as clipping whatever its computed `overflow` says.
|
|
7
|
+
*
|
|
8
|
+
* Why a marker as well as the computed style: the kit's containers get their
|
|
9
|
+
* `overflow` from Tailwind classes, and jsdom computes no stylesheet — so under test
|
|
10
|
+
* every scroller reads `overflow: visible`, the Tooltip stays in place, and its always-
|
|
11
|
+
* mounted bubble shows up in the cell's text (keksdose 0.10: "CheckingChecking"). The
|
|
12
|
+
* browser and the test then disagreed about where the bubble lives, and apps pinned
|
|
13
|
+
* `portal` on every tooltip in a table to make them agree. The kit's own scrollers
|
|
14
|
+
* (DataTable's body, Table's wrapper) carry the marker, so the answer is the same in
|
|
15
|
+
* both; an app marks its own with `{ [CLIPS_ATTRIBUTE]: "" }` or a literal `data-clips`.
|
|
16
|
+
*/
|
|
17
|
+
export const CLIPS_ATTRIBUTE = "data-clips";
|
|
2
18
|
|
|
3
19
|
/** Whether anything between `el` and `<body>` clips or scrolls its content — the
|
|
4
20
|
* containers an absolutely positioned bubble would be cut off by, or would widen.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
21
|
+
* Either the computed `overflow` says so or the element carries
|
|
22
|
+
* {@link CLIPS_ATTRIBUTE}. `<body>` and `<html>` are not counted: their `overflow`
|
|
23
|
+
* belongs to the viewport (a dialog's scroll lock sets it), and the viewport clips a
|
|
24
|
+
* portalled bubble just the same. */
|
|
8
25
|
export function hasClippingAncestor(el: Element | null): boolean {
|
|
9
26
|
if (typeof window === "undefined" || !el) return false;
|
|
10
27
|
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
11
28
|
if (node === document.body || node === document.documentElement) return false;
|
|
29
|
+
if (node.hasAttribute(CLIPS_ATTRIBUTE)) return true;
|
|
12
30
|
const style = window.getComputedStyle(node);
|
|
13
31
|
if (style.overflowX !== "visible" || style.overflowY !== "visible") return true;
|
|
14
32
|
}
|
package/src/lib/dates.ts
CHANGED
|
@@ -149,6 +149,43 @@ export function addDaysIso(iso: string, days: number): string {
|
|
|
149
149
|
return toLocalIso(d);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* `d` moved by whole months, keeping the day of the month where the target has one.
|
|
154
|
+
* `setMonth` alone ROLLS OVER — 31 January + 1 month is 3 March — which would skip
|
|
155
|
+
* February entirely for anyone paging through the year with PageDown. Returns a new
|
|
156
|
+
* Date; `d` is not touched.
|
|
157
|
+
*/
|
|
158
|
+
export function addMonthsClamped(d: Date, months: number): Date {
|
|
159
|
+
const day = d.getDate();
|
|
160
|
+
const out = new Date(d.getFullYear(), d.getMonth() + months, 1);
|
|
161
|
+
const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();
|
|
162
|
+
out.setDate(Math.min(day, lastOfTarget));
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The first day of the week in `locale`, as a `Date#getDay` index (0 = Sunday).
|
|
168
|
+
*
|
|
169
|
+
* `Intl.Locale#getWeekInfo()` is the standard spelling; V8 shipped it first as the
|
|
170
|
+
* `weekInfo` accessor, and Firefox has neither yet — hence both reads and a Monday
|
|
171
|
+
* fallback, which is ISO 8601. Its `firstDay` counts 1 = Monday … 7 = Sunday, so `% 7`
|
|
172
|
+
* maps it onto `getDay`. `undefined` asks for the runtime's default locale.
|
|
173
|
+
*/
|
|
174
|
+
export function localeWeekStart(locale: string | undefined): 0 | 1 | 2 | 3 | 4 | 5 | 6 {
|
|
175
|
+
try {
|
|
176
|
+
const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
177
|
+
const loc = new Intl.Locale(tag) as Intl.Locale & {
|
|
178
|
+
getWeekInfo?: () => { firstDay: number };
|
|
179
|
+
weekInfo?: { firstDay: number };
|
|
180
|
+
};
|
|
181
|
+
const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;
|
|
182
|
+
if (typeof firstDay === "number") return (firstDay % 7) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
183
|
+
} catch {
|
|
184
|
+
// A malformed tag throws from `Intl.Locale`; a calendar still has to render.
|
|
185
|
+
}
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
152
189
|
/** Whether two Dates fall on the same local calendar day. */
|
|
153
190
|
export function sameYmd(a: Date, b: Date): boolean {
|
|
154
191
|
return (
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useKitLocale } from "../i18n/kit-labels";
|
|
3
|
+
import { parseIsoDate } from "./dates";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Number, money, percentage, date and relative-time formatting — `Intl`, in one place,
|
|
7
|
+
* for the figures an app prints OUTSIDE a kit component.
|
|
8
|
+
*
|
|
9
|
+
* Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned
|
|
10
|
+
* `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`
|
|
11
|
+
* whose "5m ago" / "3h ago" / "2d ago" stayed English in every language), its tenancy
|
|
12
|
+
* `format.ts` (`formatChf`, a `formatPct` gluing a "+" onto `toFixed(2)`), keksdose's
|
|
13
|
+
* own set. Every one of them is `Intl` with a locale bolted on, so the only thing
|
|
14
|
+
* worth sharing is the locale — and the kit already has one, the provider's.
|
|
15
|
+
*
|
|
16
|
+
* The plain functions take a `locale` (none: the runtime default, as every `Intl` API
|
|
17
|
+
* reads `undefined`). Inside a component, {@link useKitFormat} hands back the same
|
|
18
|
+
* functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the
|
|
19
|
+
* kit components on it.
|
|
20
|
+
*
|
|
21
|
+
* A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as
|
|
22
|
+
* `empty` (default "—", the dash `StatTile` shows for no value), never as "NaN" or
|
|
23
|
+
* "Invalid Date".
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** What every formatter here prints for a missing value, unless told otherwise. */
|
|
27
|
+
export const EMPTY_FORMATTED_VALUE = "—";
|
|
28
|
+
|
|
29
|
+
/** Fraction digits: a number pins both the minimum and the maximum (`2` → "3.10");
|
|
30
|
+
* an object sets them apart (a meter reading, 1–3). */
|
|
31
|
+
export type FormatDigits = number | { min?: number; max?: number };
|
|
32
|
+
|
|
33
|
+
interface BaseOptions {
|
|
34
|
+
/** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */
|
|
35
|
+
locale?: string;
|
|
36
|
+
/** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */
|
|
37
|
+
empty?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FormatNumberOptions extends BaseOptions {
|
|
41
|
+
digits?: FormatDigits;
|
|
42
|
+
/** Compact notation: 12 400 → "12K". */
|
|
43
|
+
compact?: boolean;
|
|
44
|
+
/** `"exceptZero"` for a change ("+3", "−3", "0"). Default: the minus only. */
|
|
45
|
+
signDisplay?: Intl.NumberFormatOptions["signDisplay"];
|
|
46
|
+
/** Any further `Intl.NumberFormat` options, merged over the ones above. */
|
|
47
|
+
options?: Intl.NumberFormatOptions;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FormatMoneyOptions extends FormatNumberOptions {
|
|
51
|
+
/** `"symbol"` (default), `"narrowSymbol"`, `"code"` ("CHF 12.00") or `"name"`. */
|
|
52
|
+
currencyDisplay?: Intl.NumberFormatOptions["currencyDisplay"];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface FormatPercentOptions extends FormatNumberOptions {
|
|
56
|
+
/**
|
|
57
|
+
* `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read
|
|
58
|
+
* it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %
|
|
59
|
+
* (kastlan's rent `change_pct`).
|
|
60
|
+
*/
|
|
61
|
+
ratio?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// One formatter per option set: a table formats the same way thousands of times, and
|
|
65
|
+
// constructing an Intl formatter is the expensive part.
|
|
66
|
+
const numberFormatters = new Map<string, Intl.NumberFormat>();
|
|
67
|
+
function numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {
|
|
68
|
+
const key = `${locale ?? ""}|${JSON.stringify(options)}`;
|
|
69
|
+
let f = numberFormatters.get(key);
|
|
70
|
+
if (!f) {
|
|
71
|
+
f = new Intl.NumberFormat(locale, options);
|
|
72
|
+
numberFormatters.set(key, f);
|
|
73
|
+
}
|
|
74
|
+
return f;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {
|
|
78
|
+
if (digits === undefined) return {};
|
|
79
|
+
if (typeof digits === "number") return { minimumFractionDigits: digits, maximumFractionDigits: digits };
|
|
80
|
+
return {
|
|
81
|
+
...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),
|
|
82
|
+
...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function numberOptions(o: FormatNumberOptions): Intl.NumberFormatOptions {
|
|
87
|
+
return {
|
|
88
|
+
...digitOptions(o.digits),
|
|
89
|
+
...(o.compact ? { notation: "compact" } : null),
|
|
90
|
+
...(o.signDisplay ? { signDisplay: o.signDisplay } : null),
|
|
91
|
+
...o.options,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isMissing(value: number | null | undefined): value is null | undefined {
|
|
96
|
+
return value === null || value === undefined || Number.isNaN(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A number in `locale`: "1,234.5", "1.234,5", "1’234.5". */
|
|
100
|
+
export function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {
|
|
101
|
+
if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
102
|
+
return numberFormatter(options.locale, numberOptions(options)).format(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** An amount of `currency` (ISO 4217) in `locale`: "CHF 1’234.50", "1.234,50 €". */
|
|
106
|
+
export function formatMoney(
|
|
107
|
+
value: number | null | undefined,
|
|
108
|
+
currency: string,
|
|
109
|
+
options: FormatMoneyOptions = {},
|
|
110
|
+
): string {
|
|
111
|
+
if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
112
|
+
return numberFormatter(options.locale, {
|
|
113
|
+
style: "currency",
|
|
114
|
+
currency,
|
|
115
|
+
...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),
|
|
116
|
+
...numberOptions(options),
|
|
117
|
+
}).format(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A percentage in `locale` ("12%", "12 %", "+1.25%"). See {@link FormatPercentOptions.ratio}
|
|
121
|
+
* for what `value` means. Default digits: at most one. */
|
|
122
|
+
export function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {
|
|
123
|
+
if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
124
|
+
const ratio = options.ratio === false ? value / 100 : value;
|
|
125
|
+
return numberFormatter(options.locale, {
|
|
126
|
+
style: "percent",
|
|
127
|
+
...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),
|
|
128
|
+
...numberOptions(options),
|
|
129
|
+
}).format(ratio);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Dates ─────────────────────────────────────────────────────────────────── */
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:
|
|
136
|
+
* - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`
|
|
137
|
+
* ("08/07/2026", "8 Jul 2026", "8 July 2026", "Wednesday, 8 July 2026" in en-GB).
|
|
138
|
+
* - `dateTime` — medium date and short time: a timestamp ("8 Jul 2026, 14:05").
|
|
139
|
+
* - `time` — the short time alone ("14:05").
|
|
140
|
+
* - `monthYear` — "July 2026".
|
|
141
|
+
*
|
|
142
|
+
* Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN
|
|
143
|
+
* ("dd.MM.yyyy") is deliberately not accepted: a pattern is one locale's order baked
|
|
144
|
+
* into a string, which is how an English page ends up with German dates.
|
|
145
|
+
*/
|
|
146
|
+
export type FormatDateStyle = "short" | "medium" | "long" | "full" | "dateTime" | "time" | "monthYear";
|
|
147
|
+
|
|
148
|
+
const DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {
|
|
149
|
+
short: { dateStyle: "short" },
|
|
150
|
+
medium: { dateStyle: "medium" },
|
|
151
|
+
long: { dateStyle: "long" },
|
|
152
|
+
full: { dateStyle: "full" },
|
|
153
|
+
dateTime: { dateStyle: "medium", timeStyle: "short" },
|
|
154
|
+
time: { timeStyle: "short" },
|
|
155
|
+
monthYear: { month: "long", year: "numeric" },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export interface FormatDateOptions extends BaseOptions {
|
|
159
|
+
/** An IANA zone ("Europe/Zurich") for a timestamp. Left out: the device's. */
|
|
160
|
+
timeZone?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What {@link formatDate} and {@link formatRelativeTime} accept. */
|
|
164
|
+
export type DateInput = string | number | Date | null | undefined;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* `value` as a Date, or null. A bare "YYYY-MM-DD" is a CALENDAR day and is read on the
|
|
168
|
+
* local calendar (`parseIsoDate`) — `new Date("2026-07-08")` is UTC midnight, which is
|
|
169
|
+
* the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch
|
|
170
|
+
* milliseconds, a Date) is an instant and goes through `Date` as is.
|
|
171
|
+
*/
|
|
172
|
+
export function toDate(value: DateInput): Date | null {
|
|
173
|
+
if (value === null || value === undefined || value === "") return null;
|
|
174
|
+
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return parseIsoDate(value);
|
|
175
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
176
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dateFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
180
|
+
function dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
|
|
181
|
+
const key = `${locale ?? ""}|${JSON.stringify(options)}`;
|
|
182
|
+
let f = dateFormatters.get(key);
|
|
183
|
+
if (!f) {
|
|
184
|
+
f = new Intl.DateTimeFormat(locale, options);
|
|
185
|
+
dateFormatters.set(key, f);
|
|
186
|
+
}
|
|
187
|
+
return f;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A date or timestamp in `locale`. Takes an ISO date ("2026-07-08", read on the local
|
|
192
|
+
* calendar), an ISO timestamp ("2026-07-08T14:05:00Z"), epoch milliseconds or a Date.
|
|
193
|
+
* `style` defaults to `medium`; see {@link FormatDateStyle}.
|
|
194
|
+
*
|
|
195
|
+
* `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain "YYYY-MM-DD" with an
|
|
196
|
+
* explicit locale; this is its superset for timestamps and the named styles.
|
|
197
|
+
*/
|
|
198
|
+
export function formatDate(
|
|
199
|
+
value: DateInput,
|
|
200
|
+
style: FormatDateStyle | Intl.DateTimeFormatOptions = "medium",
|
|
201
|
+
options: FormatDateOptions = {},
|
|
202
|
+
): string {
|
|
203
|
+
const d = toDate(value);
|
|
204
|
+
if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
205
|
+
const base = typeof style === "string" ? DATE_STYLES[style] : style;
|
|
206
|
+
return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface FormatRelativeTimeOptions extends BaseOptions {
|
|
210
|
+
/** The moment it is measured from. Default: now. */
|
|
211
|
+
now?: DateInput;
|
|
212
|
+
/** `"auto"` (default): "yesterday", "now", "next week". `"always"`: "1 day ago". */
|
|
213
|
+
numeric?: Intl.RelativeTimeFormatNumeric;
|
|
214
|
+
/** `"long"` (default) "3 hours ago", `"short"` "3 hr. ago", `"narrow"` "3h ago". */
|
|
215
|
+
style?: Intl.RelativeTimeFormatStyle;
|
|
216
|
+
/**
|
|
217
|
+
* Past this many days either way, print the date instead ({@link formatDate} in
|
|
218
|
+
* `absoluteStyle`) — kastlan's feed switches to the date after a week, because "5
|
|
219
|
+
* weeks ago" makes a reader do sums. Default: never.
|
|
220
|
+
*/
|
|
221
|
+
absoluteAfterDays?: number;
|
|
222
|
+
/** The style of that date. Default `medium`. */
|
|
223
|
+
absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const SECOND = 1000;
|
|
227
|
+
const MINUTE = 60 * SECOND;
|
|
228
|
+
const HOUR = 60 * MINUTE;
|
|
229
|
+
const DAY = 24 * HOUR;
|
|
230
|
+
|
|
231
|
+
/** [the largest |difference| a unit is used for, the unit, its length]. */
|
|
232
|
+
const RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [
|
|
233
|
+
[45 * SECOND, "second", SECOND],
|
|
234
|
+
[45 * MINUTE, "minute", MINUTE],
|
|
235
|
+
[22 * HOUR, "hour", HOUR],
|
|
236
|
+
[6.5 * DAY, "day", DAY],
|
|
237
|
+
[26 * DAY, "week", 7 * DAY],
|
|
238
|
+
[320 * DAY, "month", 30.44 * DAY],
|
|
239
|
+
[Infinity, "year", 365.25 * DAY],
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();
|
|
243
|
+
function relativeFormatter(
|
|
244
|
+
locale: string | undefined,
|
|
245
|
+
numeric: Intl.RelativeTimeFormatNumeric,
|
|
246
|
+
style: Intl.RelativeTimeFormatStyle,
|
|
247
|
+
): Intl.RelativeTimeFormat {
|
|
248
|
+
const key = `${locale ?? ""}|${numeric}|${style}`;
|
|
249
|
+
let f = relativeFormatters.get(key);
|
|
250
|
+
if (!f) {
|
|
251
|
+
f = new Intl.RelativeTimeFormat(locale, { numeric, style });
|
|
252
|
+
relativeFormatters.set(key, f);
|
|
253
|
+
}
|
|
254
|
+
return f;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* How long ago (or until) `value` is, in `locale`'s words, through
|
|
259
|
+
* `Intl.RelativeTimeFormat`: "3 hours ago", "vor 3 Stunden", "il y a 3 heures",
|
|
260
|
+
* "in 2 days", and under 45 seconds "now". The unit is picked by size — seconds,
|
|
261
|
+
* minutes, hours, days, weeks, months, years — and the count is rounded.
|
|
262
|
+
*/
|
|
263
|
+
export function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {
|
|
264
|
+
const d = toDate(value);
|
|
265
|
+
if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
266
|
+
const now = toDate(options.now ?? new Date()) ?? new Date();
|
|
267
|
+
const diff = d.getTime() - now.getTime();
|
|
268
|
+
const abs = Math.abs(diff);
|
|
269
|
+
if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {
|
|
270
|
+
return formatDate(d, options.absoluteStyle ?? "medium", options);
|
|
271
|
+
}
|
|
272
|
+
const rtf = relativeFormatter(options.locale, options.numeric ?? "auto", options.style ?? "long");
|
|
273
|
+
const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;
|
|
274
|
+
// Under 45 seconds is "now" rather than "12 seconds ago": a feed that ticks through
|
|
275
|
+
// the seconds is noise, and nothing reads a timestamp to the second.
|
|
276
|
+
const count = unit === "second" ? 0 : Math.round(diff / size);
|
|
277
|
+
// `+ 0` folds -0 into 0: `format(-0, …)` says "0 seconds ago" instead of "now".
|
|
278
|
+
return rtf.format(count + 0, unit);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ── Bound to the provider ─────────────────────────────────────────────────── */
|
|
282
|
+
|
|
283
|
+
type WithoutLocale<T> = Omit<T, "locale"> & { locale?: string };
|
|
284
|
+
|
|
285
|
+
/** What {@link useKitFormat} returns: the formatters above with the provider's locale
|
|
286
|
+
* as their default (an explicit `locale` in the options still wins). */
|
|
287
|
+
export interface KitFormat {
|
|
288
|
+
/** The locale they format in — the provider's, or `undefined` (runtime default). */
|
|
289
|
+
locale: string | undefined;
|
|
290
|
+
formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;
|
|
291
|
+
formatMoney: (
|
|
292
|
+
value: number | null | undefined,
|
|
293
|
+
currency: string,
|
|
294
|
+
options?: WithoutLocale<FormatMoneyOptions>,
|
|
295
|
+
) => string;
|
|
296
|
+
formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;
|
|
297
|
+
formatDate: (
|
|
298
|
+
value: DateInput,
|
|
299
|
+
style?: FormatDateStyle | Intl.DateTimeFormatOptions,
|
|
300
|
+
options?: WithoutLocale<FormatDateOptions>,
|
|
301
|
+
) => string;
|
|
302
|
+
formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins
|
|
307
|
+
* over it — the same precedence as a component's `locale` prop):
|
|
308
|
+
*
|
|
309
|
+
* const { formatMoney, formatRelativeTime } = useKitFormat();
|
|
310
|
+
* formatMoney(row.balance, "CHF");
|
|
311
|
+
*/
|
|
312
|
+
export function useKitFormat(locale?: string): KitFormat {
|
|
313
|
+
const kitLocale = useKitLocale(locale);
|
|
314
|
+
return useMemo<KitFormat>(
|
|
315
|
+
() => ({
|
|
316
|
+
locale: kitLocale,
|
|
317
|
+
formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),
|
|
318
|
+
formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),
|
|
319
|
+
formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),
|
|
320
|
+
formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),
|
|
321
|
+
formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),
|
|
322
|
+
}),
|
|
323
|
+
[kitLocale],
|
|
324
|
+
);
|
|
325
|
+
}
|