@eifi1/ui-kit 0.48.1 → 0.49.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 +28 -13
- package/dist/admin/admin-action-confirm.js +1 -1
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.js +5 -1
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/user-roster.d.ts +11 -5
- package/dist/admin/user-roster.js +15 -4
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/accept-invitation.js +2 -2
- package/dist/auth/accept-invitation.js.map +1 -1
- package/dist/auth/not-found-page.js +1 -1
- package/dist/auth/not-found-page.js.map +1 -1
- package/dist/auth/reset-password-form.js +2 -2
- package/dist/auth/reset-password-form.js.map +1 -1
- package/dist/auth/verify-email.js +2 -2
- package/dist/auth/verify-email.js.map +1 -1
- package/dist/billing/plan-card.js +2 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/components/breadcrumbs.d.ts +19 -1
- package/dist/components/breadcrumbs.js +11 -4
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/button-look.d.ts +37 -7
- package/dist/components/button-look.js +33 -6
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/button.d.ts +4 -1
- package/dist/components/button.js +2 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/calendar-heatmap.js +2 -1
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart-edge.d.ts +10 -0
- package/dist/components/chart-edge.js +57 -0
- package/dist/components/chart-edge.js.map +1 -0
- package/dist/components/chart.js +2 -1
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +8 -1
- package/dist/components/chip.js +4 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-context.d.ts +5 -0
- package/dist/components/confirm-context.js +7 -0
- package/dist/components/confirm-context.js.map +1 -0
- package/dist/components/confirm-dialog.js +3 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +2 -2
- package/dist/components/danger-confirm.js +1 -1
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-fit.d.ts +8 -1
- package/dist/components/data-table-fit.js +28 -4
- package/dist/components/data-table-fit.js.map +1 -1
- package/dist/components/data-table-groups.d.ts +3 -1
- package/dist/components/data-table-groups.js +11 -5
- package/dist/components/data-table-groups.js.map +1 -1
- package/dist/components/data-table-head.d.ts +8 -1
- package/dist/components/data-table-head.js +11 -3
- package/dist/components/data-table-head.js.map +1 -1
- package/dist/components/data-table-leading-row.d.ts +3 -1
- package/dist/components/data-table-leading-row.js +2 -1
- package/dist/components/data-table-leading-row.js.map +1 -1
- package/dist/components/data-table-mobile.d.ts +48 -2
- package/dist/components/data-table-mobile.js +87 -10
- package/dist/components/data-table-mobile.js.map +1 -1
- package/dist/components/data-table-row-actions.d.ts +2 -1
- package/dist/components/data-table-row-actions.js +9 -2
- package/dist/components/data-table-row-actions.js.map +1 -1
- package/dist/components/data-table-row.d.ts +84 -0
- package/dist/components/data-table-row.js +261 -0
- package/dist/components/data-table-row.js.map +1 -0
- package/dist/components/data-table-selection.d.ts +3 -0
- package/dist/components/data-table-selection.js +12 -1
- package/dist/components/data-table-selection.js.map +1 -1
- package/dist/components/data-table-virtual.d.ts +98 -0
- package/dist/components/data-table-virtual.js +369 -0
- package/dist/components/data-table-virtual.js.map +1 -0
- package/dist/components/data-table.d.ts +74 -2
- package/dist/components/data-table.js +241 -221
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/error-boundary.js +2 -2
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/floating-panel.d.ts +36 -13
- package/dist/components/floating-panel.js +27 -7
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/form-actions.js +1 -1
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/icon-button.d.ts +11 -0
- package/dist/components/icon-button.js +7 -5
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/nav-pills.d.ts +8 -1
- package/dist/components/nav-pills.js +61 -38
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/page-header-context.d.ts +8 -0
- package/dist/components/page-header-context.js +7 -0
- package/dist/components/page-header-context.js.map +1 -0
- package/dist/components/page-header.d.ts +14 -2
- package/dist/components/page-header.js +10 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/pie-chart.js +2 -1
- package/dist/components/pie-chart.js.map +1 -1
- package/dist/components/progress-bar.js +5 -4
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/reauth-dialog.js +1 -1
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/row-actions-labels.js +3 -1
- package/dist/components/row-actions-labels.js.map +1 -1
- package/dist/components/row-actions.d.ts +43 -9
- package/dist/components/row-actions.js +78 -15
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/section-nav.d.ts +37 -0
- package/dist/components/section-nav.js +33 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/series-chart.js +8 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/share-card.js +1 -1
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/signature-pad.js +1 -1
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/sparkline.js +2 -1
- package/dist/components/sparkline.js.map +1 -1
- package/dist/components/stat-tile.js +14 -7
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +5 -1
- package/dist/components/status-dot.js +4 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/translation-review-editor.js +2 -2
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review.js +2 -2
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/treemap.d.ts +5 -2
- package/dist/components/treemap.js +26 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/use-table-state.js +37 -6
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/data-table.d.ts +1 -0
- package/dist/data-table.js.map +1 -1
- package/dist/demo/demo-banner.js +1 -1
- package/dist/demo/demo-banner.js.map +1 -1
- package/dist/demo/demo-ended.js +1 -1
- package/dist/demo/demo-ended.js.map +1 -1
- package/dist/demo/demo-start.js +1 -1
- package/dist/demo/demo-start.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-inbox.js +1 -1
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/hooks/use-anchored-panel.js +2 -1
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-dismiss.d.ts +9 -1
- package/dist/hooks/use-dismiss.js +25 -5
- package/dist/hooks/use-dismiss.js.map +1 -1
- package/dist/hooks/use-document-title.d.ts +14 -0
- package/dist/hooks/use-document-title.js +22 -0
- package/dist/hooks/use-document-title.js.map +1 -0
- package/dist/i18n/german.js +6 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +31 -1
- package/dist/i18n/kit-labels.js +43 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +6 -2
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +6 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +6 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +6 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +6 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +6 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +2 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +1 -0
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.js +2 -2
- package/dist/landing/landing-actions.js.map +1 -1
- package/dist/landing/public-header.js +2 -2
- package/dist/landing/public-header.js.map +1 -1
- package/dist/lib/clipboard-minus.d.ts +27 -0
- package/dist/lib/clipboard-minus.js +63 -0
- package/dist/lib/clipboard-minus.js.map +1 -0
- package/dist/lib/document-title.d.ts +17 -0
- package/dist/lib/document-title.js +29 -0
- package/dist/lib/document-title.js.map +1 -0
- package/dist/lib/float-clearance.js +8 -2
- package/dist/lib/float-clearance.js.map +1 -1
- package/dist/lib/format.d.ts +35 -10
- package/dist/lib/format.js +55 -11
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/minus.d.ts +22 -0
- package/dist/lib/minus.js +14 -0
- package/dist/lib/minus.js.map +1 -0
- package/dist/lib/status-tone.d.ts +30 -0
- package/dist/lib/status-tone.js +17 -0
- package/dist/lib/status-tone.js.map +1 -0
- package/dist/settings/settings-layout.js +3 -2
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +1 -1
- package/dist/shell/account-menu-language.js +9 -1
- package/dist/shell/account-menu-language.js.map +1 -1
- package/dist/shell/app-shell-labels.js +2 -1
- package/dist/shell/app-shell-labels.js.map +1 -1
- package/dist/shell/app-shell.d.ts +58 -16
- package/dist/shell/app-shell.js +158 -44
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/language-code.d.ts +3 -0
- package/dist/shell/language-code.js +8 -0
- package/dist/shell/language-code.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +14 -6
- package/dist/shell/topbar-controls.js +14 -17
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/testing/css-variables.generated.d.ts +1 -1
- package/dist/testing/css-variables.generated.js +1 -1
- package/dist/testing/css-variables.generated.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +82 -15
- package/dist/theme/chart-palette.js +51 -24
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +9 -0
- package/dist/theme/palette-presets.js +77 -62
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/tour/tour.js +1 -1
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +3 -3
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-action-confirm.tsx +1 -1
- package/src/admin/invitations-panel.tsx +7 -3
- package/src/admin/user-roster.tsx +23 -6
- package/src/auth/accept-invitation.tsx +3 -3
- package/src/auth/not-found-page.tsx +1 -1
- package/src/auth/reset-password-form.tsx +3 -3
- package/src/auth/verify-email.tsx +3 -3
- package/src/billing/plan-card.tsx +2 -2
- package/src/components/breadcrumbs.tsx +32 -4
- package/src/components/button-look.ts +86 -15
- package/src/components/button.tsx +6 -3
- package/src/components/calendar-heatmap.tsx +2 -1
- package/src/components/chart-edge.ts +73 -0
- package/src/components/chart.tsx +3 -1
- package/src/components/chip.tsx +10 -1
- package/src/components/confirm-context.ts +7 -0
- package/src/components/confirm-dialog.tsx +3 -3
- package/src/components/danger-confirm.tsx +3 -3
- package/src/components/data-table-fit.ts +58 -8
- package/src/components/data-table-groups.tsx +15 -4
- package/src/components/data-table-head.tsx +25 -2
- package/src/components/data-table-leading-row.tsx +4 -1
- package/src/components/data-table-mobile.tsx +162 -11
- package/src/components/data-table-row-actions.tsx +12 -3
- package/src/components/data-table-row.tsx +485 -0
- package/src/components/data-table-selection.ts +17 -1
- package/src/components/data-table-virtual.ts +547 -0
- package/src/components/data-table.tsx +367 -326
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/floating-panel.tsx +67 -16
- package/src/components/form-actions.tsx +2 -2
- package/src/components/icon-button.tsx +21 -8
- package/src/components/nav-pills.tsx +29 -2
- package/src/components/page-header-context.ts +8 -0
- package/src/components/page-header.tsx +34 -2
- package/src/components/pie-chart.tsx +2 -1
- package/src/components/progress-bar.tsx +6 -3
- package/src/components/reauth-dialog.tsx +1 -1
- package/src/components/row-actions-labels.ts +2 -0
- package/src/components/row-actions.tsx +154 -33
- package/src/components/section-nav.tsx +65 -0
- package/src/components/series-chart.tsx +12 -3
- package/src/components/share-card.tsx +1 -1
- package/src/components/signature-pad.tsx +1 -1
- package/src/components/sparkline.tsx +2 -1
- package/src/components/stat-tile.tsx +19 -7
- package/src/components/status-dot.tsx +7 -1
- package/src/components/tooltip.tsx +3 -1
- package/src/components/translation-review-editor.tsx +2 -2
- package/src/components/translation-review.tsx +2 -2
- package/src/components/treemap.tsx +34 -8
- package/src/components/use-table-state.ts +63 -8
- package/src/data-table.ts +5 -3
- package/src/demo/demo-banner.tsx +1 -1
- package/src/demo/demo-ended.tsx +1 -1
- package/src/demo/demo-start.tsx +1 -1
- package/src/feedback/feedback-inbox.tsx +1 -1
- package/src/hooks/use-anchored-panel.ts +9 -1
- package/src/hooks/use-dismiss.ts +43 -4
- package/src/hooks/use-document-title.ts +31 -0
- package/src/i18n/german.ts +4 -0
- package/src/i18n/kit-labels.tsx +76 -1
- package/src/i18n/locales/es.ts +4 -0
- package/src/i18n/locales/fr.ts +4 -0
- package/src/i18n/locales/hu.ts +4 -0
- package/src/i18n/locales/it.ts +4 -0
- package/src/i18n/locales/zh.ts +4 -0
- package/src/i18n/review.ts +2 -0
- package/src/i18n/shared-labels.ts +1 -0
- package/src/index.ts +13 -0
- package/src/landing/landing-actions.tsx +2 -2
- package/src/landing/public-header.tsx +2 -2
- package/src/lib/clipboard-minus.ts +94 -0
- package/src/lib/document-title.ts +41 -0
- package/src/lib/float-clearance.ts +19 -3
- package/src/lib/format.ts +115 -30
- package/src/lib/minus.ts +40 -0
- package/src/lib/status-tone.ts +43 -0
- package/src/settings/settings-layout.tsx +3 -2
- package/src/shell/account-menu-language.tsx +14 -2
- package/src/shell/app-shell-labels.ts +1 -0
- package/src/shell/app-shell.tsx +298 -69
- package/src/shell/language-code.ts +5 -0
- package/src/shell/topbar-controls.tsx +35 -15
- package/src/testing/css-variables.generated.ts +6 -6
- package/src/theme/chart-palette.ts +102 -20
- package/src/theme/palette-presets.ts +121 -84
- package/src/tour/tour.tsx +1 -1
- package/src/wizard/stepper-nav.tsx +4 -4
- package/tokens.css +28 -18
package/src/i18n/locales/it.ts
CHANGED
|
@@ -546,6 +546,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
546
546
|
theme: "Cambia tema",
|
|
547
547
|
palette: "Preset di aspetto",
|
|
548
548
|
language: "Lingua",
|
|
549
|
+
languageCurrent: (name) => `Scegli la lingua, attuale: ${name}`,
|
|
549
550
|
switchRole: "Cambia ruolo",
|
|
550
551
|
role: (value) => `Ruolo: ${value}`,
|
|
551
552
|
},
|
|
@@ -1888,10 +1889,13 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
1888
1889
|
rowActions: {
|
|
1889
1890
|
actions: "Azioni",
|
|
1890
1891
|
actionsFor: (name) => `Azioni per ${name}`,
|
|
1892
|
+
confirmTitle: (action, name) => (name ? `${action} «${name}»?` : `${action}?`),
|
|
1893
|
+
confirmBody: "Questa azione non può essere annullata.",
|
|
1891
1894
|
},
|
|
1892
1895
|
appShellMore: {
|
|
1893
1896
|
more: "Altro",
|
|
1894
1897
|
moreTitle: "Altre pagine",
|
|
1898
|
+
overview: "Panoramica",
|
|
1895
1899
|
},
|
|
1896
1900
|
// 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words. "Lei", as
|
|
1897
1901
|
// the rest of the catalogue.
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -529,6 +529,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
529
529
|
theme: "切换主题",
|
|
530
530
|
palette: "外观预设",
|
|
531
531
|
language: "语言",
|
|
532
|
+
languageCurrent: (name) => `选择语言,当前:${name}`,
|
|
532
533
|
switchRole: "切换角色",
|
|
533
534
|
role: (value) => `角色:${value}`,
|
|
534
535
|
},
|
|
@@ -1795,10 +1796,13 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
1795
1796
|
rowActions: {
|
|
1796
1797
|
actions: "操作",
|
|
1797
1798
|
actionsFor: (name) => `${name} 的操作`,
|
|
1799
|
+
confirmTitle: (action, name) => (name ? `${action}“${name}”?` : `${action}?`),
|
|
1800
|
+
confirmBody: "此操作无法撤销。",
|
|
1798
1801
|
},
|
|
1799
1802
|
appShellMore: {
|
|
1800
1803
|
more: "更多",
|
|
1801
1804
|
moreTitle: "更多页面",
|
|
1805
|
+
overview: "概览",
|
|
1802
1806
|
},
|
|
1803
1807
|
// 0.42.0 — docs/admin-harmonization.md §3.1: the admin area's shared words.
|
|
1804
1808
|
mailTestCard: {
|
package/src/i18n/review.ts
CHANGED
|
@@ -153,6 +153,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
|
|
|
153
153
|
"tabs.remove": [["{{tab}}"]],
|
|
154
154
|
"appShell.toggleGroup": [["{{group}}"]],
|
|
155
155
|
"topBar.role": [["{{role}}"]],
|
|
156
|
+
"topBar.languageCurrent": [["{{name}}"]],
|
|
156
157
|
"file.size": [[2_500_000]],
|
|
157
158
|
"wizard.step": [[2, 4]],
|
|
158
159
|
"tour.step": [[2, 4]],
|
|
@@ -279,6 +280,7 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
|
|
|
279
280
|
"signChip.direction": [["{{current}}", "{{next}}"]],
|
|
280
281
|
"signChip.switchTo": [["{{next}}"]],
|
|
281
282
|
"rowActions.actionsFor": [["{{name}}"]],
|
|
283
|
+
"rowActions.confirmTitle": [["{{action}}", "{{name}}"], ["{{action}}"]],
|
|
282
284
|
"columnMapper.readError": [["{{name}}"]],
|
|
283
285
|
"columnMapper.summary": [
|
|
284
286
|
[1, 1],
|
|
@@ -106,6 +106,7 @@ export const DEFAULT_TOP_BAR_LABELS: TopBarLabels = {
|
|
|
106
106
|
theme: "Toggle theme",
|
|
107
107
|
palette: "Appearance preset",
|
|
108
108
|
language: "Language",
|
|
109
|
+
languageCurrent: (name) => `Choose language, current: ${name}`,
|
|
109
110
|
switchRole: "Switch role",
|
|
110
111
|
role: (value) => `Role: ${value}`,
|
|
111
112
|
};
|
package/src/index.ts
CHANGED
|
@@ -154,6 +154,12 @@ export { useCloseTransition, OVERLAY_EXIT_MS } from "./hooks/use-close-transitio
|
|
|
154
154
|
export { useRowSwipe } from "./hooks/use-row-swipe";
|
|
155
155
|
// 0.28: keksdose's noindex marker, for the legal and auth pages of every app.
|
|
156
156
|
export { useNoIndex } from "./hooks/use-noindex";
|
|
157
|
+
// 0.49 KIT-9: the window's title from page, section and app, in the provider's format.
|
|
158
|
+
export { useDocumentTitle } from "./hooks/use-document-title";
|
|
159
|
+
// 0.49 KIT-1b: U+2212 before a number as "-", the copy handler's rule, for an app's text.
|
|
160
|
+
export { asciiMinusInNumbers } from "./lib/clipboard-minus";
|
|
161
|
+
export { DEFAULT_DOCUMENT_TITLE_FORMAT, formatDocumentTitle } from "./lib/document-title";
|
|
162
|
+
export type { DocumentTitleParts } from "./lib/document-title";
|
|
157
163
|
export type { SwipeStage, RowSwipeOptions, RowSwipeReturn } from "./hooks/use-row-swipe";
|
|
158
164
|
// Any element as a file drop target, screened like FileButton (kastlan).
|
|
159
165
|
export { useFileDrop, dragHasFiles } from "./hooks/use-file-drop";
|
|
@@ -381,10 +387,15 @@ export * from "./components/alert-banner";
|
|
|
381
387
|
export * from "./components/toggle-group";
|
|
382
388
|
// A wrapping set of links/buttons marked `aria-current` — Tabs-like looks, not tabs.
|
|
383
389
|
export * from "./components/nav-pills";
|
|
390
|
+
// 0.49.0 (KIT-6): a section's pages on the phone, one scrolling row of pills.
|
|
391
|
+
export * from "./components/section-nav";
|
|
384
392
|
// A pill carrying one VALUE — inert, a link, or a toggle, depending on which prop it is
|
|
385
393
|
// given — and the list field built from it. Distinct from Button on purpose: a row of
|
|
386
394
|
// buttons reads as "choose an action", a row of chips as "here are the things".
|
|
387
395
|
export * from "./components/chip";
|
|
396
|
+
// 0.49.0 (KIT-8): five domain-free status kinds and their tones — Chip/StatusDot `status`.
|
|
397
|
+
export { statusTone, STATUS_KINDS } from "./lib/status-tone";
|
|
398
|
+
export type { StatusKind, StatusTone } from "./lib/status-tone";
|
|
388
399
|
// ChipInput's own module since MOD-9: it renders Chip, so chip.tsx re-exports only its types.
|
|
389
400
|
export * from "./components/chip-input";
|
|
390
401
|
export * from "./components/wizard-stepper";
|
|
@@ -589,6 +600,8 @@ export type {
|
|
|
589
600
|
RowActionsColumnOptions,
|
|
590
601
|
RowActionsHeadProps,
|
|
591
602
|
RowActionTone,
|
|
603
|
+
// 0.49 KIT-12: a danger action's confirmation words.
|
|
604
|
+
RowActionConfirm,
|
|
592
605
|
} from "./components/row-actions";
|
|
593
606
|
// 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits).
|
|
594
607
|
export * from "./components/checkbox-group";
|
|
@@ -77,12 +77,12 @@ export function LandingActions({
|
|
|
77
77
|
className={cn("flex flex-col gap-3 sm:flex-row", align === "center" && "sm:justify-center", className)}
|
|
78
78
|
>
|
|
79
79
|
{session === "user" ? (
|
|
80
|
-
<Button href={openAppHref} variant="
|
|
80
|
+
<Button href={openAppHref} variant="accent" size={buttonSize} className={extra}>
|
|
81
81
|
{labels.openApp}
|
|
82
82
|
</Button>
|
|
83
83
|
) : (
|
|
84
84
|
<>
|
|
85
|
-
<Button href={accessLink.href} variant="
|
|
85
|
+
<Button href={accessLink.href} variant="accent" size={buttonSize} className={extra}>
|
|
86
86
|
{accessLink.label}
|
|
87
87
|
</Button>
|
|
88
88
|
{demoHref !== null && (
|
|
@@ -82,7 +82,7 @@ export function PublicHeader({
|
|
|
82
82
|
|
|
83
83
|
const actions =
|
|
84
84
|
session === "user" ? (
|
|
85
|
-
<Button href={openAppHref} variant="
|
|
85
|
+
<Button href={openAppHref} variant="accent" size={size}>
|
|
86
86
|
{labels.openApp}
|
|
87
87
|
</Button>
|
|
88
88
|
) : (
|
|
@@ -96,7 +96,7 @@ export function PublicHeader({
|
|
|
96
96
|
{labels.signIn}
|
|
97
97
|
</Button>
|
|
98
98
|
)}
|
|
99
|
-
<Button href={accessLink.href} variant="
|
|
99
|
+
<Button href={accessLink.href} variant="accent" size={size}>
|
|
100
100
|
{accessLink.label}
|
|
101
101
|
</Button>
|
|
102
102
|
</>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The copy handler for the one minus sign (0.49 KIT-1b, Marcel's follow-up to KIT-1).
|
|
3
|
+
*
|
|
4
|
+
* The kit prints a negative with U+2212 (lib/minus.ts). A spreadsheet does not read that
|
|
5
|
+
* as a number: a figure copied out of a kit table and pasted into Excel landed as text.
|
|
6
|
+
* So while a selection inside the page is copied, every U+2212 standing directly before a
|
|
7
|
+
* number — optionally after a currency code or symbol and spaces: "−CHF 1,250.50",
|
|
8
|
+
* "−€2,000", "−12%" — becomes the ASCII "-" in the clipboard, in `text/plain` AND in
|
|
9
|
+
* `text/html` (Excel pastes the HTML flavour, as cells). The page keeps "−".
|
|
10
|
+
*
|
|
11
|
+
* A copy with no such minus is left to the browser untouched, and so is any copy out of an
|
|
12
|
+
* input, a textarea or an editable element: what the person typed is theirs.
|
|
13
|
+
*
|
|
14
|
+
* Installed once per document by `UiKitProvider` (`copyAsciiMinus`, default `true`) —
|
|
15
|
+
* the one part every app mounts, the signed-out pages included.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** U+2212, then (optionally) a currency code or symbol, then spaces, then a digit. */
|
|
19
|
+
const MINUS_BEFORE_NUMBER = /\u2212(?=(?:[A-Z]{3}|\p{Sc})?[\s\u00a0\u202f]*\d)/gu;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `text` with each U+2212 that stands before a number (in the sense above) as the ASCII
|
|
23
|
+
* hyphen-minus, every other character as it was — for text an app hands a machine that
|
|
24
|
+
* did not come from a formatter's `minus: "ascii"`.
|
|
25
|
+
*/
|
|
26
|
+
export function asciiMinusInNumbers(text: string): string {
|
|
27
|
+
return text.replace(MINUS_BEFORE_NUMBER, "-");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasMinusBeforeNumber(text: string): boolean {
|
|
31
|
+
MINUS_BEFORE_NUMBER.lastIndex = 0;
|
|
32
|
+
const found = MINUS_BEFORE_NUMBER.test(text);
|
|
33
|
+
MINUS_BEFORE_NUMBER.lastIndex = 0;
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** An input, a textarea or an editable element: a copy there is the person's own text. */
|
|
38
|
+
function isEditable(node: Node | null | undefined): boolean {
|
|
39
|
+
const el = node instanceof Element ? node : node?.parentElement;
|
|
40
|
+
if (!el) return false;
|
|
41
|
+
if (el.closest("input, textarea, select")) return true;
|
|
42
|
+
return (el as HTMLElement).isContentEditable === true || el.closest('[contenteditable=""], [contenteditable="true"]') !== null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Table parts copied alone are wrapped back into a table, which Excel reads as cells. */
|
|
46
|
+
function wrapTableParts(container: HTMLElement): string {
|
|
47
|
+
const first = container.firstElementChild?.tagName;
|
|
48
|
+
const html = container.innerHTML;
|
|
49
|
+
if (first === "TD" || first === "TH") return `<table><tbody><tr>${html}</tr></tbody></table>`;
|
|
50
|
+
if (first === "TR") return `<table><tbody>${html}</tbody></table>`;
|
|
51
|
+
if (first === "TBODY" || first === "THEAD" || first === "TFOOT") return `<table>${html}</table>`;
|
|
52
|
+
return html;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onCopy(event: ClipboardEvent): void {
|
|
56
|
+
if (event.defaultPrevented || !event.clipboardData) return;
|
|
57
|
+
const doc = (event.target as Node | null)?.ownerDocument ?? document;
|
|
58
|
+
if (isEditable(event.target as Node | null) || isEditable(doc.activeElement)) return;
|
|
59
|
+
const selection = doc.getSelection();
|
|
60
|
+
if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;
|
|
61
|
+
if (isEditable(selection.anchorNode) || isEditable(selection.focusNode)) return;
|
|
62
|
+
const text = selection.toString();
|
|
63
|
+
if (!hasMinusBeforeNumber(text)) return;
|
|
64
|
+
|
|
65
|
+
const container = doc.createElement("div");
|
|
66
|
+
for (let i = 0; i < selection.rangeCount; i++) container.appendChild(selection.getRangeAt(i).cloneContents());
|
|
67
|
+
const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
68
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
69
|
+
node.nodeValue = asciiMinusInNumbers(node.nodeValue ?? "");
|
|
70
|
+
}
|
|
71
|
+
event.clipboardData.setData("text/plain", asciiMinusInNumbers(text));
|
|
72
|
+
event.clipboardData.setData("text/html", wrapTableParts(container));
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const installs = new WeakMap<Document, number>();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Listen for copies on `doc` until the returned function is called. Counted: nested
|
|
80
|
+
* providers install it once, and the last one to go removes it.
|
|
81
|
+
*/
|
|
82
|
+
export function installClipboardMinus(doc: Document = document): () => void {
|
|
83
|
+
const count = installs.get(doc) ?? 0;
|
|
84
|
+
if (count === 0) doc.addEventListener("copy", onCopy);
|
|
85
|
+
installs.set(doc, count + 1);
|
|
86
|
+
let done = false;
|
|
87
|
+
return () => {
|
|
88
|
+
if (done) return;
|
|
89
|
+
done = true;
|
|
90
|
+
const left = (installs.get(doc) ?? 1) - 1;
|
|
91
|
+
installs.set(doc, left);
|
|
92
|
+
if (left === 0) doc.removeEventListener("copy", onCopy);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window's title from its parts (0.49 KIT-9): `{page}`, `{section}` and `{app}` in a
|
|
3
|
+
* format, by default "{page} · {section} · {app}" — "Invoices · Finance · Kastlan".
|
|
4
|
+
*
|
|
5
|
+
* A part that is missing (undefined, null, empty or blank) is dropped WITH the separator
|
|
6
|
+
* before it, so "Invoices · Kastlan" never reads "Invoices · · Kastlan". The text between
|
|
7
|
+
* two parts is the separator; text before the first part and after the last is kept as
|
|
8
|
+
* written ("[{page} – {app}]" → "[KS]"). No part at all gives "".
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_DOCUMENT_TITLE_FORMAT = "{page} · {section} · {app}";
|
|
11
|
+
|
|
12
|
+
/** The parts {@link formatDocumentTitle} fills in. */
|
|
13
|
+
export interface DocumentTitleParts {
|
|
14
|
+
page?: string | null;
|
|
15
|
+
section?: string | null;
|
|
16
|
+
app?: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PLACEHOLDER = /\{(page|section|app)\}/g;
|
|
20
|
+
|
|
21
|
+
export function formatDocumentTitle(format: string, parts: DocumentTitleParts): string {
|
|
22
|
+
let prefix = "";
|
|
23
|
+
let out = "";
|
|
24
|
+
let pending = "";
|
|
25
|
+
let last = 0;
|
|
26
|
+
let seen = false;
|
|
27
|
+
for (const m of format.matchAll(PLACEHOLDER)) {
|
|
28
|
+
const literal = format.slice(last, m.index!);
|
|
29
|
+
last = m.index! + m[0].length;
|
|
30
|
+
if (!seen) prefix = literal;
|
|
31
|
+
else pending = out === "" ? "" : literal || pending;
|
|
32
|
+
seen = true;
|
|
33
|
+
const value = parts[m[1] as keyof DocumentTitleParts]?.trim();
|
|
34
|
+
if (!value) continue;
|
|
35
|
+
out += (out === "" ? "" : pending) + value;
|
|
36
|
+
pending = "";
|
|
37
|
+
}
|
|
38
|
+
if (!seen) return format;
|
|
39
|
+
if (out === "") return "";
|
|
40
|
+
return prefix + out + format.slice(last);
|
|
41
|
+
}
|
|
@@ -2,7 +2,8 @@ import { useEffect } from "react";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The room the floating launchers take above the bottom nav, published as
|
|
5
|
-
* `--app-float-clearance` on `<html>` so `AppShell` can pad `<main>` past them
|
|
5
|
+
* `--app-float-clearance` on `<html>` so `AppShell` can pad `<main>` past them, and with
|
|
6
|
+
* the nav included as `--kit-floating-inset` (0.49 KIT-11, below).
|
|
6
7
|
*
|
|
7
8
|
* keksdose dev#612 (UX-1.3, "the help button covers content"): the shell padded `<main>`
|
|
8
9
|
* for the nav plus 1.5rem, so on a phone the last row of every page ended under the help
|
|
@@ -23,12 +24,27 @@ import { useEffect } from "react";
|
|
|
23
24
|
|
|
24
25
|
const claims = new Map<symbol, string>();
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 0.49 KIT-11: `--kit-floating-inset` is the same room measured from the screen's bottom
|
|
29
|
+
* edge — the phone's bottom nav (`--app-nav-h`, 0px from `md` up) or the home indicator,
|
|
30
|
+
* whichever is taller, plus `--app-float-clearance` — so a page's own fixed or sticky
|
|
31
|
+
* footer, or its last row, can clear the launchers with one length at every width.
|
|
32
|
+
* Unset while no launcher is mounted, like `--app-float-clearance`: write
|
|
33
|
+
* `var(--kit-floating-inset, var(--app-nav-h, 0px))`.
|
|
34
|
+
*/
|
|
35
|
+
const NAV_OR_HOME_INDICATOR = "max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px))";
|
|
36
|
+
|
|
26
37
|
function publish() {
|
|
27
38
|
if (typeof document === "undefined") return;
|
|
28
39
|
const root = document.documentElement;
|
|
29
40
|
const values = [...claims.values()];
|
|
30
|
-
if (values.length === 0)
|
|
31
|
-
|
|
41
|
+
if (values.length === 0) {
|
|
42
|
+
root.style.removeProperty("--app-float-clearance");
|
|
43
|
+
root.style.removeProperty("--kit-floating-inset");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
root.style.setProperty("--app-float-clearance", values.length === 1 ? values[0] : `max(${values.join(", ")})`);
|
|
47
|
+
root.style.setProperty("--kit-floating-inset", `calc(${NAV_OR_HOME_INDICATOR} + var(--app-float-clearance))`);
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
/** Claim `value` (a CSS length: the launcher's top edge above the nav, plus a gap) until
|
package/src/lib/format.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useKitLocale, useKitTimeZone } from "../i18n/kit-labels";
|
|
3
3
|
import { parseIsoDate } from "./dates";
|
|
4
|
+
import { formatWithMinus, type MinusStyle } from "./minus";
|
|
4
5
|
import { roundHalfAway } from "./round";
|
|
5
6
|
|
|
7
|
+
export type { MinusStyle } from "./minus";
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Number, money, percentage, date and relative-time formatting — `Intl`, in one place,
|
|
8
11
|
* for the figures an app prints OUTSIDE a kit component.
|
|
@@ -19,9 +22,18 @@ import { roundHalfAway } from "./round";
|
|
|
19
22
|
* functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the
|
|
20
23
|
* kit components on it.
|
|
21
24
|
*
|
|
22
|
-
* A missing value — `null`, `undefined`,
|
|
23
|
-
*
|
|
24
|
-
* "
|
|
25
|
+
* A missing value — `null`, `undefined`, an unparseable date — formats as `empty`
|
|
26
|
+
* (default "—", the dash `StatTile` shows for no value), never as "NaN" or "Invalid
|
|
27
|
+
* Date". A number may also arrive as a numeric STRING (0.49, KIT-2) — "1850.00", "-12.5",
|
|
28
|
+
* "0": a Decimal field off a Python API — and is formatted like the number. Anything
|
|
29
|
+
* else (a non-numeric string, an object, `NaN`, ±`Infinity`) prints `empty` too, but is
|
|
30
|
+
* a bug upstream rather than a gap in the data, so a development build says so on the
|
|
31
|
+
* console, once per call site. Zero is a figure: `0` and `"0"` print "CHF 0.00", never
|
|
32
|
+
* the dash.
|
|
33
|
+
*
|
|
34
|
+
* A negative figure carries the typographic minus, U+2212 (0.49, KIT-1), in every
|
|
35
|
+
* locale — `Intl` writes a hyphen in most. `minus: "ascii"` for text a machine reads
|
|
36
|
+
* back (a CSV, the clipboard).
|
|
25
37
|
*/
|
|
26
38
|
|
|
27
39
|
/** What every formatter here prints for a missing value, unless told otherwise. */
|
|
@@ -47,6 +59,14 @@ export interface FormatNumberBaseOptions extends BaseOptions {
|
|
|
47
59
|
signDisplay?: Intl.NumberFormatOptions["signDisplay"];
|
|
48
60
|
/** Any further `Intl.NumberFormat` options, merged over the ones above. */
|
|
49
61
|
options?: Intl.NumberFormatOptions;
|
|
62
|
+
/**
|
|
63
|
+
* The minus sign of a negative figure (0.49, KIT-1). `"typographic"` (default):
|
|
64
|
+
* U+2212, as wide as "+" and the digits, as `SignedAmount` prints it — in every
|
|
65
|
+
* locale, also where `Intl` writes a hyphen. `"ascii"`: U+002D, for an export a
|
|
66
|
+
* machine parses back (a CSV download, the clipboard) — there, also pass
|
|
67
|
+
* `options: { useGrouping: false }` or write the raw number.
|
|
68
|
+
*/
|
|
69
|
+
minus?: MinusStyle;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
/** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */
|
|
@@ -115,44 +135,113 @@ function numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {
|
|
|
115
135
|
};
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
138
|
+
/**
|
|
139
|
+
* What the number formatters take (0.49, KIT-2): a number, a numeric string ("1850.00",
|
|
140
|
+
* "-12.5", "1e3" — what a Decimal arrives as in JSON), or `null` / `undefined` for no
|
|
141
|
+
* value. A string is read with `Number()`, so a figure with more significant digits than
|
|
142
|
+
* a double holds (about 15–17) is printed as its nearest double.
|
|
143
|
+
*/
|
|
144
|
+
export type FormattableNumber = number | string | null | undefined;
|
|
145
|
+
|
|
146
|
+
/** A plain decimal: optional sign, digits with an optional point, an optional exponent.
|
|
147
|
+
* No grouping marks, no decimal comma, no currency — those are formatted text, and
|
|
148
|
+
* reading them back is a locale's guess. */
|
|
149
|
+
const NUMERIC_STRING = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
150
|
+
|
|
151
|
+
/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
|
|
152
|
+
* production one (the consumer's bundler makes `import.meta.env.DEV` false) and never
|
|
153
|
+
* under vitest. Read per call so a test can switch it on with `vi.stubEnv`. */
|
|
154
|
+
function devWarnings(): boolean {
|
|
155
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const warnedSites = new Set<string>();
|
|
159
|
+
/** This module's own frames, skipped to find the caller ("Object.formatMoney" is a
|
|
160
|
+
* `useKitFormat` wrapper). */
|
|
161
|
+
const OWN_FRAME = /\b(?:formatNumber|formatMoney|formatPercent|toFormattable|warnInvalid|callSite)\b/;
|
|
162
|
+
|
|
163
|
+
/** The first stack frame outside the formatters: where the bad value was passed. */
|
|
164
|
+
function callSite(): string {
|
|
165
|
+
const frames = (new Error().stack ?? "").split("\n").slice(1);
|
|
166
|
+
return frames.find((frame) => !OWN_FRAME.test(frame))?.trim() ?? "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function describeValue(value: unknown): string {
|
|
170
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
|
|
171
|
+
try {
|
|
172
|
+
return JSON.stringify(value) ?? String(value);
|
|
173
|
+
} catch {
|
|
174
|
+
return Object.prototype.toString.call(value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A development build's console note about an invalid value — once per call site. */
|
|
179
|
+
function warnInvalid(fn: string, value: unknown): void {
|
|
180
|
+
if (!devWarnings()) return;
|
|
181
|
+
const site = callSite();
|
|
182
|
+
const key = `${fn}|${site || typeof value}`;
|
|
183
|
+
if (warnedSites.has(key)) return;
|
|
184
|
+
warnedSites.add(key);
|
|
185
|
+
const type = Array.isArray(value) ? "array" : typeof value;
|
|
186
|
+
console.warn(
|
|
187
|
+
`[ui-kit] ${fn} got ${describeValue(value)} (${type}), which is not a number: printed the empty value. ` +
|
|
188
|
+
"Pass a number, a numeric string, or null/undefined for a missing value." +
|
|
189
|
+
(site ? `\n at ${site.replace(/^at /, "")}` : ""),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* `value` as a finite number to format, or `undefined` to print `empty`. `null` and
|
|
195
|
+
* `undefined` are a missing value and pass quietly; anything that is neither a finite
|
|
196
|
+
* number nor a numeric string is an invalid one — ±Infinity included (lenkbank, adopting
|
|
197
|
+
* 0.13: a division by zero upstream; "∞" in a money column reads like data) — and a
|
|
198
|
+
* development build warns. `-0` is folded into `0`: zero carries no sign.
|
|
199
|
+
*/
|
|
200
|
+
function toFormattable(fn: string, value: unknown): number | undefined {
|
|
201
|
+
if (value === null || value === undefined) return undefined;
|
|
202
|
+
let n: number | undefined;
|
|
203
|
+
if (typeof value === "number") n = value;
|
|
204
|
+
else if (typeof value === "string" && NUMERIC_STRING.test(value.trim())) n = Number(value.trim());
|
|
205
|
+
if (n === undefined || !Number.isFinite(n)) {
|
|
206
|
+
warnInvalid(fn, value);
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
return n + 0;
|
|
122
210
|
}
|
|
123
211
|
|
|
124
212
|
/** A number in `locale`: "1,234.5", "1.234,5", "1’234.5" — with `unit`, "3.400 N". */
|
|
125
|
-
export function formatNumber(value:
|
|
126
|
-
|
|
127
|
-
|
|
213
|
+
export function formatNumber(value: FormattableNumber, options: FormatNumberOptions = {}): string {
|
|
214
|
+
const n = toFormattable("formatNumber", value);
|
|
215
|
+
if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
216
|
+
const figure = formatWithMinus(numberFormatter(options.locale, numberOptions(options)), n, options.minus);
|
|
128
217
|
return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;
|
|
129
218
|
}
|
|
130
219
|
|
|
131
220
|
/** An amount of `currency` (ISO 4217) in `locale`: "CHF 1’234.50", "1.234,50 €". */
|
|
132
|
-
export function formatMoney(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
): string {
|
|
137
|
-
if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
138
|
-
return numberFormatter(options.locale, {
|
|
221
|
+
export function formatMoney(value: FormattableNumber, currency: string, options: FormatMoneyOptions = {}): string {
|
|
222
|
+
const n = toFormattable("formatMoney", value);
|
|
223
|
+
if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
224
|
+
const formatter = numberFormatter(options.locale, {
|
|
139
225
|
style: "currency",
|
|
140
226
|
currency,
|
|
141
227
|
...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),
|
|
142
228
|
...numberOptions(options),
|
|
143
|
-
})
|
|
229
|
+
});
|
|
230
|
+
return formatWithMinus(formatter, n, options.minus);
|
|
144
231
|
}
|
|
145
232
|
|
|
146
233
|
/** A percentage in `locale` ("12%", "12 %", "+1.25%"). See {@link FormatPercentOptions.ratio}
|
|
147
234
|
* for what `value` means. Default digits: at most one. */
|
|
148
|
-
export function formatPercent(value:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
235
|
+
export function formatPercent(value: FormattableNumber, options: FormatPercentOptions = {}): string {
|
|
236
|
+
const n = toFormattable("formatPercent", value);
|
|
237
|
+
if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
|
|
238
|
+
const ratio = options.ratio === false ? n / 100 : n;
|
|
239
|
+
const formatter = numberFormatter(options.locale, {
|
|
152
240
|
style: "percent",
|
|
153
241
|
...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),
|
|
154
242
|
...numberOptions(options),
|
|
155
|
-
})
|
|
243
|
+
});
|
|
244
|
+
return formatWithMinus(formatter, ratio, options.minus);
|
|
156
245
|
}
|
|
157
246
|
|
|
158
247
|
/* ── Dates ─────────────────────────────────────────────────────────────────── */
|
|
@@ -327,13 +416,9 @@ type WithoutLocale<T> = Omit<T, "locale"> & { locale?: string };
|
|
|
327
416
|
export interface KitFormat {
|
|
328
417
|
/** The locale they format in — the provider's, or `undefined` (runtime default). */
|
|
329
418
|
locale: string | undefined;
|
|
330
|
-
formatNumber: (value:
|
|
331
|
-
formatMoney: (
|
|
332
|
-
|
|
333
|
-
currency: string,
|
|
334
|
-
options?: WithoutLocale<FormatMoneyOptions>,
|
|
335
|
-
) => string;
|
|
336
|
-
formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;
|
|
419
|
+
formatNumber: (value: FormattableNumber, options?: WithoutLocale<FormatNumberOptions>) => string;
|
|
420
|
+
formatMoney: (value: FormattableNumber, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;
|
|
421
|
+
formatPercent: (value: FormattableNumber, options?: WithoutLocale<FormatPercentOptions>) => string;
|
|
337
422
|
formatDate: (
|
|
338
423
|
value: DateInput,
|
|
339
424
|
style?: FormatDateStyle | Intl.DateTimeFormatOptions,
|
package/src/lib/minus.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one minus sign (0.49, KIT-1). `Intl.NumberFormat` writes a negative with the
|
|
3
|
+
* HYPHEN-MINUS (U+002D) in nearly every locale — en, de-CH, fr, it, es, hu, zh — and
|
|
4
|
+
* with the typographic MINUS SIGN (U+2212) in a few (sv, fi). The kit prints U+2212
|
|
5
|
+
* everywhere, as `SignedAmount` always has: it is as wide as "+" and the tabular
|
|
6
|
+
* digits, so a column of signed figures lines up, and it does not break a line the way
|
|
7
|
+
* a hyphen may.
|
|
8
|
+
*
|
|
9
|
+
* `"ascii"` is the other way round, for text a machine reads back — a CSV, the
|
|
10
|
+
* clipboard: every minus becomes U+002D, whatever the locale wrote, so `Number()` and a
|
|
11
|
+
* spreadsheet parse it.
|
|
12
|
+
*/
|
|
13
|
+
export type MinusStyle = "typographic" | "ascii";
|
|
14
|
+
|
|
15
|
+
/** U+2212 MINUS SIGN. */
|
|
16
|
+
export const TYPOGRAPHIC_MINUS = "\u2212";
|
|
17
|
+
|
|
18
|
+
/** The parts `formatToParts` writes a minus in (the exponent's, under `notation:
|
|
19
|
+
* "scientific"`, too). */
|
|
20
|
+
const MINUS_PARTS: ReadonlySet<string> = new Set(["minusSign", "exponentMinusSign"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `formatter.format(value)` with every minus sign in `minus` style. Only the parts
|
|
24
|
+
* `Intl` marks as a minus are touched — a hyphen inside a currency's name
|
|
25
|
+
* ("Bosnia-Herzegovina convertible marks") stays — and only when the text holds the
|
|
26
|
+
* other sign at all, so a positive figure costs no `formatToParts`.
|
|
27
|
+
*/
|
|
28
|
+
export function formatWithMinus(
|
|
29
|
+
formatter: Intl.NumberFormat,
|
|
30
|
+
value: number,
|
|
31
|
+
minus: MinusStyle = "typographic",
|
|
32
|
+
): string {
|
|
33
|
+
const text = formatter.format(value);
|
|
34
|
+
const [wanted, other] = minus === "ascii" ? ["-", TYPOGRAPHIC_MINUS] : [TYPOGRAPHIC_MINUS, "-"];
|
|
35
|
+
if (!text.includes(other)) return text;
|
|
36
|
+
return formatter
|
|
37
|
+
.formatToParts(value)
|
|
38
|
+
.map((part) => (MINUS_PARTS.has(part.type) ? wanted : part.value))
|
|
39
|
+
.join("");
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a status means, in five domain-free words, and the tone that says it (0.49 KIT-8).
|
|
3
|
+
*
|
|
4
|
+
* Every app paints its statuses by hand: kastlan's ~15 enums each carry a Chip tone
|
|
5
|
+
* (`shared/enums/enum-meta.ts`), keksdose's job and transaction states theirs, and the
|
|
6
|
+
* same meaning drifted into different colours — "unpaid" amber in one table, brand in
|
|
7
|
+
* the next. The kind is the decision ("does the reader have to act?"), the tone only
|
|
8
|
+
* its colour, so an app maps each of its states to a kind once and the colours agree
|
|
9
|
+
* everywhere:
|
|
10
|
+
*
|
|
11
|
+
* | kind | tone | for |
|
|
12
|
+
* |---|---|---|
|
|
13
|
+
* | `good` | `success` | done, settled, healthy: nothing to do (Paid, Verified) |
|
|
14
|
+
* | `attention` | `warning` | needs a look or an action soon, nothing broken yet (Unpaid, Due soon) |
|
|
15
|
+
* | `problem` | `danger` | something is wrong or late and needs action now (Overdue, Failed) |
|
|
16
|
+
* | `active` | `info` | in progress or current, neither good nor bad (Running, Sent) |
|
|
17
|
+
* | `inactive` | `neutral` | over, paused or not started: out of the way (Draft, Archived) |
|
|
18
|
+
*
|
|
19
|
+
* `brand` is not a status tone: it is the app's own colour, and a status in it reads as
|
|
20
|
+
* "this is the app" rather than as a state. Money (`income` / `expense`) is not one
|
|
21
|
+
* either: it says which way an amount flows, not how a record stands.
|
|
22
|
+
*/
|
|
23
|
+
export type StatusKind = "good" | "attention" | "problem" | "active" | "inactive";
|
|
24
|
+
|
|
25
|
+
/** The tones a {@link StatusKind} maps to — a subset of `ChipTone` and `StatusDotTone`. */
|
|
26
|
+
export type StatusTone = "success" | "warning" | "danger" | "info" | "neutral";
|
|
27
|
+
|
|
28
|
+
/** The five kinds, in the order the table above lists them. */
|
|
29
|
+
export const STATUS_KINDS: readonly StatusKind[] = ["good", "attention", "problem", "active", "inactive"];
|
|
30
|
+
|
|
31
|
+
const TONES: Readonly<Record<StatusKind, StatusTone>> = {
|
|
32
|
+
good: "success",
|
|
33
|
+
attention: "warning",
|
|
34
|
+
problem: "danger",
|
|
35
|
+
active: "info",
|
|
36
|
+
inactive: "neutral",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The tone for a status kind: good → success, attention → warning, problem → danger,
|
|
40
|
+
* active → info, inactive → neutral. See {@link StatusKind}. */
|
|
41
|
+
export function statusTone(kind: StatusKind): StatusTone {
|
|
42
|
+
return TONES[kind];
|
|
43
|
+
}
|
|
@@ -323,6 +323,7 @@ export function SettingsLayout<G extends string>({
|
|
|
323
323
|
<div className={pageClass} data-settings-view="group">
|
|
324
324
|
<PageHeader
|
|
325
325
|
size="compact"
|
|
326
|
+
documentTitle={false}
|
|
326
327
|
title={open.title}
|
|
327
328
|
description={open.help}
|
|
328
329
|
breadcrumbs={
|
|
@@ -347,7 +348,7 @@ export function SettingsLayout<G extends string>({
|
|
|
347
348
|
</div>
|
|
348
349
|
) : (
|
|
349
350
|
<div className={pageClass} data-settings-view="list">
|
|
350
|
-
<PageHeader size="compact" title={pageTitle} />
|
|
351
|
+
<PageHeader size="compact" documentTitle={false} title={pageTitle} />
|
|
351
352
|
{searchField}
|
|
352
353
|
{searching ? (
|
|
353
354
|
results
|
|
@@ -409,7 +410,7 @@ export function SettingsLayout<G extends string>({
|
|
|
409
410
|
);
|
|
410
411
|
body = (
|
|
411
412
|
<div className={pageClass} data-settings-view="desktop">
|
|
412
|
-
<PageHeader size="compact" title={pageTitle} />
|
|
413
|
+
<PageHeader size="compact" documentTitle={false} title={pageTitle} />
|
|
413
414
|
{searchField}
|
|
414
415
|
<div className="md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6">
|
|
415
416
|
{withSidebarFooter(
|