@eifi1/ui-kit 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -34
- package/dist/chart.d.ts +3 -1
- package/dist/chart.js +2 -0
- package/dist/chart.js.map +1 -1
- package/dist/components/account-settings-labels.d.ts +105 -0
- package/dist/components/account-settings-labels.js +81 -0
- package/dist/components/account-settings-labels.js.map +1 -0
- package/dist/components/account-settings.d.ts +36 -52
- package/dist/components/account-settings.js +44 -8
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +40 -4
- package/dist/components/amount-input.js +50 -12
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +68 -0
- package/dist/components/authed-image.js +117 -0
- package/dist/components/authed-image.js.map +1 -0
- package/dist/components/breadcrumbs.d.ts +2 -1
- package/dist/components/breadcrumbs.js +5 -2
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +51 -2
- package/dist/components/button-group.js +5 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +15 -4
- package/dist/components/calendar-heatmap.d.ts +41 -3
- package/dist/components/calendar-heatmap.js +48 -7
- package/dist/components/calendar-heatmap.js.map +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +26 -18
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +35 -3
- package/dist/components/choice-card.js +83 -25
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +18 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +31 -9
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +51 -70
- package/dist/components/copy-button.js +1 -1
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +63 -0
- package/dist/components/data-table-cells.js +64 -0
- package/dist/components/data-table-cells.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-filters.js +31 -1
- package/dist/components/data-table-filters.js.map +1 -1
- package/dist/components/data-table-labels.d.ts +13 -0
- package/dist/components/data-table-labels.js +8 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +125 -27
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +3 -1
- package/dist/components/date-picker.js +15 -2
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +23 -3
- package/dist/components/description-list.js +23 -3
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +1 -0
- package/dist/components/entity-combobox.d.ts +1 -1
- package/dist/components/entity-combobox.js +40 -20
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +281 -0
- package/dist/components/error-boundary.js +302 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/field.d.ts +30 -4
- package/dist/components/field.js +40 -17
- package/dist/components/field.js.map +1 -1
- package/dist/components/file-button.d.ts +50 -208
- package/dist/components/file-dropzone.d.ts +49 -2
- package/dist/components/floating-panel.d.ts +13 -4
- package/dist/components/floating-panel.js +7 -3
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +53 -0
- package/dist/components/form-actions.js +98 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/image-grid.d.ts +67 -0
- package/dist/components/image-grid.js +112 -0
- package/dist/components/image-grid.js.map +1 -0
- package/dist/components/lightbox.d.ts +104 -0
- package/dist/components/lightbox.js +229 -0
- package/dist/components/lightbox.js.map +1 -0
- package/dist/components/line-items.d.ts +114 -0
- package/dist/components/line-items.js +238 -0
- package/dist/components/line-items.js.map +1 -0
- package/dist/components/list.d.ts +37 -4
- package/dist/components/list.js +35 -14
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +24 -0
- package/dist/components/loading-state.js +32 -0
- package/dist/components/loading-state.js.map +1 -0
- package/dist/components/menu-item.d.ts +2 -1
- package/dist/components/menu-item.js +5 -1
- package/dist/components/menu-item.js.map +1 -1
- package/dist/components/mini-calendar.d.ts +13 -1
- package/dist/components/mini-calendar.js +46 -2
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +20 -1
- package/dist/components/modal.js +26 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/month-picker.d.ts +21 -1
- package/dist/components/month-picker.js +139 -26
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +1 -1
- package/dist/components/multi-entity-combobox.js +42 -20
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/nav-pills.d.ts +2 -1
- package/dist/components/nav-pills.js +5 -1
- package/dist/components/nav-pills.js.map +1 -1
- package/dist/components/number-field.d.ts +24 -5
- package/dist/components/number-field.js +5 -7
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +25 -5
- package/dist/components/number-input.js +7 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +20 -5
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/passkeys-setting.d.ts +67 -0
- package/dist/components/passkeys-setting.js +214 -0
- package/dist/components/passkeys-setting.js.map +1 -0
- package/dist/components/picker-sheet.js +5 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/pie-chart-labels.d.ts +20 -0
- package/dist/components/pie-chart-labels.js +12 -0
- package/dist/components/pie-chart-labels.js.map +1 -0
- package/dist/components/pie-chart.d.ts +101 -0
- package/dist/components/pie-chart.js +283 -0
- package/dist/components/pie-chart.js.map +1 -0
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress-bar.d.ts +44 -3
- package/dist/components/progress-bar.js +58 -22
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/qr-code.d.ts +34 -0
- package/dist/components/qr-code.js +57 -0
- package/dist/components/qr-code.js.map +1 -0
- package/dist/components/search-field.d.ts +15 -3
- package/dist/components/search-field.js +5 -1
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/settings-fields.d.ts +49 -1
- package/dist/components/signed-amount.d.ts +122 -0
- package/dist/components/signed-amount.js +152 -0
- package/dist/components/signed-amount.js.map +1 -0
- package/dist/components/skeleton.d.ts +10 -3
- package/dist/components/skeleton.js +12 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/stat-tile.d.ts +2 -1
- package/dist/components/stat-tile.js +6 -3
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/table.d.ts +39 -5
- package/dist/components/table.js +45 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +238 -0
- package/dist/components/text-link.js +144 -0
- package/dist/components/text-link.js.map +1 -0
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +49 -1
- package/dist/components/toggle-group.d.ts +35 -0
- package/dist/components/toggle-group.js +43 -14
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +53 -3
- package/dist/components/toggle-legend.js +54 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +51 -779
- package/dist/components/ui.js +364 -34
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +3 -52
- package/dist/components/use-table-state.js +128 -30
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
- package/dist/data-table.d.ts +2 -1
- package/dist/data-table.js +5 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +26 -3
- package/dist/feedback/feedback-dialog.js +21 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +173 -0
- package/dist/feedback/feedback-thread.js +264 -0
- package/dist/feedback/feedback-thread.js.map +1 -0
- package/dist/feedback.d.ts +2 -1
- package/dist/feedback.js +1 -0
- package/dist/feedback.js.map +1 -1
- package/dist/hooks/use-authed-src.d.ts +71 -0
- package/dist/hooks/use-authed-src.js +63 -0
- package/dist/hooks/use-authed-src.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +49 -2
- package/dist/hooks/use-hotkey.d.ts +59 -0
- package/dist/hooks/use-hotkey.js +105 -0
- package/dist/hooks/use-hotkey.js.map +1 -0
- package/dist/hooks/use-search-param-state.d.ts +83 -0
- package/dist/hooks/use-search-param-state.js +94 -0
- package/dist/hooks/use-search-param-state.js.map +1 -0
- package/dist/i18n/defaults.d.ts +15 -4
- package/dist/i18n/defaults.js +31 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +46 -312
- package/dist/i18n/kit-labels.js +20 -5
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
- package/dist/i18n/locales/de-CH.d.ts +15 -4
- package/dist/i18n/locales/de-informal.d.ts +15 -4
- package/dist/i18n/locales/de.d.ts +15 -4
- package/dist/i18n/locales/de.js +164 -3
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +15 -4
- package/dist/i18n/locales/es.js +161 -3
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +15 -4
- package/dist/i18n/locales/fr.js +161 -3
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +15 -4
- package/dist/i18n/locales/hu.js +161 -3
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +15 -4
- package/dist/i18n/locales/it.js +161 -3
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +15 -4
- package/dist/i18n/locales/zh.js +161 -3
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +39 -19
- package/dist/index.js +40 -1
- package/dist/index.js.map +1 -1
- package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
- package/dist/lib/format.d.ts +162 -0
- package/dist/lib/format.js +151 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/qr-encode.d.ts +44 -0
- package/dist/lib/qr-encode.js +338 -0
- package/dist/lib/qr-encode.js.map +1 -0
- package/dist/rhf/fields.d.ts +283 -0
- package/dist/rhf/fields.js +631 -0
- package/dist/rhf/fields.js.map +1 -0
- package/dist/rhf/form.d.ts +49 -1
- package/dist/rhf/line-items.d.ts +31 -0
- package/dist/rhf/line-items.js +59 -0
- package/dist/rhf/line-items.js.map +1 -0
- package/dist/rhf.d.ts +61 -1
- package/dist/rhf.js +2 -0
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +99 -2
- package/dist/shell/app-shell.js +120 -57
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/auth-layout.d.ts +74 -0
- package/dist/shell/auth-layout.js +97 -0
- package/dist/shell/auth-layout.js.map +1 -0
- package/dist/shell/top-bar-brand.d.ts +81 -0
- package/dist/shell/top-bar-brand.js +47 -0
- package/dist/shell/top-bar-brand.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +21 -4
- package/dist/shell/topbar-action-menu.js +18 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +56 -1
- package/dist/shell.js +2 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +48 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-context.d.ts +24 -1
- package/dist/wizard/wizard-context.js +19 -2
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +30 -5
- package/dist/wizard/wizard-step.js +34 -3
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +50 -3
- package/package.json +6 -6
- package/src/chart.ts +4 -0
- package/src/components/account-settings-labels.ts +190 -0
- package/src/components/account-settings.tsx +110 -63
- package/src/components/amount-input.tsx +109 -10
- package/src/components/authed-image.tsx +190 -0
- package/src/components/breadcrumbs.tsx +8 -2
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/button-group.tsx +7 -2
- package/src/components/calendar-heatmap.tsx +105 -10
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +14 -4
- package/src/components/choice-card.tsx +136 -25
- package/src/components/combobox-core.tsx +33 -9
- package/src/components/combobox.tsx +33 -9
- package/src/components/copy-button.tsx +3 -2
- package/src/components/data-table-cells.tsx +119 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table-filters.ts +77 -0
- package/src/components/data-table-labels.ts +23 -0
- package/src/components/data-table.tsx +230 -13
- package/src/components/date-picker.tsx +19 -3
- package/src/components/description-list.tsx +52 -3
- package/src/components/entity-combobox.tsx +25 -7
- package/src/components/error-boundary.tsx +640 -0
- package/src/components/field-sync.tsx +48 -40
- package/src/components/field.tsx +56 -5
- package/src/components/floating-panel.tsx +20 -7
- package/src/components/form-actions.tsx +191 -0
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/image-grid.tsx +181 -0
- package/src/components/lightbox.tsx +374 -0
- package/src/components/line-items.tsx +385 -0
- package/src/components/list.tsx +82 -19
- package/src/components/loading-state.tsx +47 -0
- package/src/components/menu-item.tsx +7 -2
- package/src/components/mini-calendar.tsx +72 -3
- package/src/components/modal.tsx +65 -3
- package/src/components/month-picker.tsx +181 -26
- package/src/components/multi-entity-combobox.tsx +27 -7
- package/src/components/nav-pills.tsx +8 -1
- package/src/components/number-field.tsx +15 -10
- package/src/components/number-input.tsx +17 -1
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/passkeys-setting.tsx +331 -0
- package/src/components/picker-sheet.tsx +10 -1
- package/src/components/pie-chart-labels.ts +32 -0
- package/src/components/pie-chart.tsx +479 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/progress-bar.tsx +129 -19
- package/src/components/qr-code.tsx +83 -0
- package/src/components/search-field.tsx +20 -4
- package/src/components/signed-amount.tsx +295 -0
- package/src/components/skeleton.tsx +21 -3
- package/src/components/stat-tile.tsx +11 -3
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/table.tsx +102 -13
- package/src/components/text-link.tsx +356 -0
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +93 -20
- package/src/components/toggle-legend.tsx +108 -2
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +707 -49
- package/src/components/use-table-state.ts +265 -33
- package/src/data-table.ts +10 -0
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +49 -7
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +510 -0
- package/src/feedback.ts +1 -0
- package/src/hooks/use-authed-src.ts +161 -0
- package/src/hooks/use-hotkey.ts +172 -0
- package/src/hooks/use-search-param-state.ts +187 -0
- package/src/i18n/defaults.ts +30 -0
- package/src/i18n/kit-labels.tsx +118 -8
- package/src/i18n/locales/de.ts +161 -0
- package/src/i18n/locales/es.ts +158 -0
- package/src/i18n/locales/fr.ts +161 -0
- package/src/i18n/locales/hu.ts +159 -0
- package/src/i18n/locales/it.ts +159 -0
- package/src/i18n/locales/zh.ts +158 -0
- package/src/index.ts +59 -0
- package/src/lib/format.ts +348 -0
- package/src/lib/qr-encode.ts +420 -0
- package/src/rhf/fields.tsx +1135 -0
- package/src/rhf/line-items.tsx +130 -0
- package/src/rhf.ts +6 -0
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +153 -3
- package/src/shell/auth-layout.tsx +190 -0
- package/src/shell/top-bar-brand.tsx +76 -0
- package/src/shell/topbar-action-menu.tsx +32 -4
- package/src/shell.ts +2 -0
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-context.tsx +41 -1
- package/src/wizard/wizard-step.tsx +75 -6
- package/tokens.css +12 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useKitLabelOverrides } from "../i18n/kit-labels";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `accountSettings` namespace of `<UiKitProvider labels>`: one record per section —
|
|
6
|
+
* `profile`, `password`, `twoFactor`, `passkeys` — each merged key by key, so an app
|
|
7
|
+
* can translate `accountSettings.twoFactor.scanHint` alone.
|
|
8
|
+
*
|
|
9
|
+
* Before 0.12.0 every section took a REQUIRED `labels` prop and had no English of its
|
|
10
|
+
* own, so each app spelled out 30-odd keys at the call site and still hard-coded some
|
|
11
|
+
* (kastlan's profile page passed `email: "Email"` in English to a German page). The
|
|
12
|
+
* prop stays, now partial, and wins over the provider as everywhere else in the kit.
|
|
13
|
+
*
|
|
14
|
+
* Its own module, beside the components rather than in them, so `PasskeysSetting`
|
|
15
|
+
* (its own file) and the older three read one tree without importing each other.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface ProfileSettingLabels {
|
|
19
|
+
title: string;
|
|
20
|
+
email: string;
|
|
21
|
+
role: string;
|
|
22
|
+
memberSince: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
save: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PasswordSettingLabels {
|
|
28
|
+
title: string;
|
|
29
|
+
current: string;
|
|
30
|
+
next: string;
|
|
31
|
+
confirm: string;
|
|
32
|
+
submit: string;
|
|
33
|
+
tooShort: string;
|
|
34
|
+
mismatch: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TwoFactorSettingLabels {
|
|
38
|
+
status: string;
|
|
39
|
+
enabledText: string;
|
|
40
|
+
disabledText: string;
|
|
41
|
+
enable: string;
|
|
42
|
+
scanHint: string;
|
|
43
|
+
codeLabel: string;
|
|
44
|
+
verify: string;
|
|
45
|
+
disableSection: string;
|
|
46
|
+
password: string;
|
|
47
|
+
disable: string;
|
|
48
|
+
/**
|
|
49
|
+
* Alt text for the setup QR code, which shipped as a hardcoded `alt="QR"`.
|
|
50
|
+
*
|
|
51
|
+
* OPTIONAL, like the two keys below, and not because it matters less: this interface
|
|
52
|
+
* is annotated at consumer call sites (`const LABELS: TwoFactorSettingLabels = {…}`),
|
|
53
|
+
* so a new REQUIRED key is a compile error in every app on the next `npm update` —
|
|
54
|
+
* the additive-API rule in the README. Each falls back to the provider's
|
|
55
|
+
* `accountSettings.twoFactor`, then English.
|
|
56
|
+
*/
|
|
57
|
+
qrAlt?: string;
|
|
58
|
+
/** The line over the manual-entry key, for a phone that cannot scan its own screen. */
|
|
59
|
+
secretHint?: string;
|
|
60
|
+
/** The copy button beside that key. */
|
|
61
|
+
copySecret?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PasskeysSettingLabels {
|
|
65
|
+
title: string;
|
|
66
|
+
/** The line under the title saying what a passkey is for. */
|
|
67
|
+
description: string;
|
|
68
|
+
empty: string;
|
|
69
|
+
loading: string;
|
|
70
|
+
/** Accessible name of the list. */
|
|
71
|
+
list: string;
|
|
72
|
+
nameLabel: string;
|
|
73
|
+
namePlaceholder: string;
|
|
74
|
+
add: string;
|
|
75
|
+
/** The add button while the app's WebAuthn ceremony runs — the OS prompt is open. */
|
|
76
|
+
adding: string;
|
|
77
|
+
rename: string;
|
|
78
|
+
/** The row's rename button, named for its passkey — it starts with `rename`, the
|
|
79
|
+
* visible text, so voice control finds it by what it says. */
|
|
80
|
+
renameItem: (name: string) => string;
|
|
81
|
+
/** Accessible name of the inline rename field. */
|
|
82
|
+
renameField: (name: string) => string;
|
|
83
|
+
save: string;
|
|
84
|
+
cancel: string;
|
|
85
|
+
delete: string;
|
|
86
|
+
/** The row's delete button, named for its passkey (see `renameItem`). */
|
|
87
|
+
deleteItem: (name: string) => string;
|
|
88
|
+
/** The inline question a delete asks first. */
|
|
89
|
+
deleteConfirm: (name: string) => string;
|
|
90
|
+
/** The destructive button that answers it. */
|
|
91
|
+
confirmDelete: string;
|
|
92
|
+
/** `date` arrives formatted in the provider's locale. */
|
|
93
|
+
created: (date: string) => string;
|
|
94
|
+
lastUsed: (date: string) => string;
|
|
95
|
+
neverUsed: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AccountSettingsLabels {
|
|
99
|
+
profile: ProfileSettingLabels;
|
|
100
|
+
password: PasswordSettingLabels;
|
|
101
|
+
twoFactor: Required<TwoFactorSettingLabels>;
|
|
102
|
+
passkeys: PasskeysSettingLabels;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const DEFAULT_ACCOUNT_SETTINGS_LABELS: AccountSettingsLabels = {
|
|
106
|
+
profile: {
|
|
107
|
+
title: "Profile",
|
|
108
|
+
email: "Email",
|
|
109
|
+
role: "Role",
|
|
110
|
+
memberSince: "Member since",
|
|
111
|
+
displayName: "Display name",
|
|
112
|
+
save: "Save",
|
|
113
|
+
},
|
|
114
|
+
password: {
|
|
115
|
+
title: "Change password",
|
|
116
|
+
current: "Current password",
|
|
117
|
+
next: "New password",
|
|
118
|
+
confirm: "Confirm new password",
|
|
119
|
+
submit: "Change password",
|
|
120
|
+
tooShort: "The new password is too short.",
|
|
121
|
+
mismatch: "The passwords don’t match.",
|
|
122
|
+
},
|
|
123
|
+
twoFactor: {
|
|
124
|
+
status: "Two-factor authentication",
|
|
125
|
+
enabledText: "On",
|
|
126
|
+
disabledText: "Off",
|
|
127
|
+
enable: "Set up two-factor authentication",
|
|
128
|
+
scanHint: "Scan this code with your authenticator app, then enter the code it shows.",
|
|
129
|
+
codeLabel: "Verification code",
|
|
130
|
+
verify: "Verify and turn on",
|
|
131
|
+
disableSection: "Turn off two-factor authentication",
|
|
132
|
+
password: "Current password",
|
|
133
|
+
disable: "Turn off",
|
|
134
|
+
// Unchanged from the fallback it had before the namespace (i18n-labels.test pins it).
|
|
135
|
+
qrAlt: "QR code",
|
|
136
|
+
secretHint: "Can’t scan it? Enter this key in the app instead:",
|
|
137
|
+
copySecret: "Copy key",
|
|
138
|
+
},
|
|
139
|
+
passkeys: {
|
|
140
|
+
title: "Passkeys",
|
|
141
|
+
description: "Sign in with your fingerprint, face or device PIN instead of a password.",
|
|
142
|
+
empty: "No passkeys yet",
|
|
143
|
+
loading: "Loading passkeys…",
|
|
144
|
+
list: "Your passkeys",
|
|
145
|
+
nameLabel: "Name (optional)",
|
|
146
|
+
namePlaceholder: "e.g. Laptop, Phone",
|
|
147
|
+
add: "Add passkey",
|
|
148
|
+
adding: "Waiting for your device…",
|
|
149
|
+
rename: "Rename",
|
|
150
|
+
renameItem: (name) => `Rename ${name}`,
|
|
151
|
+
renameField: (name) => `New name for ${name}`,
|
|
152
|
+
save: "Save",
|
|
153
|
+
cancel: "Cancel",
|
|
154
|
+
delete: "Delete",
|
|
155
|
+
deleteItem: (name) => `Delete ${name}`,
|
|
156
|
+
deleteConfirm: (name) => `Delete “${name}”? It can no longer be used to sign in.`,
|
|
157
|
+
confirmDelete: "Delete passkey",
|
|
158
|
+
created: (date) => `Added ${date}`,
|
|
159
|
+
lastUsed: (date) => `Last used ${date}`,
|
|
160
|
+
neverUsed: "Never used",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* One section of the namespace, resolved: English, then the provider's
|
|
166
|
+
* `accountSettings.<section>`, then the component's own `labels` prop. An `undefined`
|
|
167
|
+
* in either source is skipped, so a prop object built from optional values cannot
|
|
168
|
+
* blank out a default.
|
|
169
|
+
*/
|
|
170
|
+
export function useAccountSettingsLabels<K extends keyof AccountSettingsLabels>(
|
|
171
|
+
section: K,
|
|
172
|
+
prop?: Partial<AccountSettingsLabels[K]>,
|
|
173
|
+
): AccountSettingsLabels[K] {
|
|
174
|
+
const fromProvider = useKitLabelOverrides("accountSettings")?.[section] as
|
|
175
|
+
| Partial<AccountSettingsLabels[K]>
|
|
176
|
+
| undefined;
|
|
177
|
+
return useMemo(
|
|
178
|
+
() => ({
|
|
179
|
+
...DEFAULT_ACCOUNT_SETTINGS_LABELS[section],
|
|
180
|
+
...definedOnly(fromProvider),
|
|
181
|
+
...definedOnly(prop),
|
|
182
|
+
}),
|
|
183
|
+
[section, fromProvider, prop],
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function definedOnly<T extends object>(value: T | undefined): Partial<T> {
|
|
188
|
+
if (!value) return {};
|
|
189
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as Partial<T>;
|
|
190
|
+
}
|
|
@@ -2,24 +2,37 @@ import { useId, useState } from "react";
|
|
|
2
2
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { Button, Card, FIELD_WRITABLE_LOOK, Input } from "./ui";
|
|
4
4
|
import { UserAvatar } from "./user-avatar";
|
|
5
|
+
import { CopyButton } from "./copy-button";
|
|
6
|
+
import { QrCode } from "./qr-code";
|
|
5
7
|
import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
|
|
8
|
+
import { useAccountSettingsLabels } from "./account-settings-labels";
|
|
9
|
+
import type {
|
|
10
|
+
PasswordSettingLabels,
|
|
11
|
+
ProfileSettingLabels,
|
|
12
|
+
TwoFactorSettingLabels,
|
|
13
|
+
} from "./account-settings-labels";
|
|
14
|
+
|
|
15
|
+
// The label types moved beside the namespace's defaults (0.12.0); re-exported here so
|
|
16
|
+
// every existing `import type { ProfileSettingLabels }` keeps resolving.
|
|
17
|
+
export type {
|
|
18
|
+
AccountSettingsLabels,
|
|
19
|
+
PasskeysSettingLabels,
|
|
20
|
+
PasswordSettingLabels,
|
|
21
|
+
ProfileSettingLabels,
|
|
22
|
+
TwoFactorSettingLabels,
|
|
23
|
+
} from "./account-settings-labels";
|
|
24
|
+
export { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "./account-settings-labels";
|
|
6
25
|
|
|
7
26
|
/**
|
|
8
27
|
* App-agnostic account-settings SECTIONS (feedback #333). The presentation lives
|
|
9
28
|
* in @hb/ui; each app injects its own auth-API handlers + translated labels and
|
|
10
29
|
* arranges these next to its own app-specific cards (appearance, budgets, …).
|
|
11
30
|
* Every section is a self-contained <Card> owning its transient form state.
|
|
31
|
+
*
|
|
32
|
+
* Every `labels` prop is optional since 0.12.0: the strings come from the provider's
|
|
33
|
+
* `accountSettings` namespace, then English (see account-settings-labels.ts).
|
|
12
34
|
*/
|
|
13
35
|
|
|
14
|
-
export interface ProfileSettingLabels {
|
|
15
|
-
title: string;
|
|
16
|
-
email: string;
|
|
17
|
-
role: string;
|
|
18
|
-
memberSince: string;
|
|
19
|
-
displayName: string;
|
|
20
|
-
save: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
36
|
export function ProfileSetting({
|
|
24
37
|
name,
|
|
25
38
|
email,
|
|
@@ -29,7 +42,7 @@ export function ProfileSetting({
|
|
|
29
42
|
onChange,
|
|
30
43
|
onSave,
|
|
31
44
|
saving,
|
|
32
|
-
labels,
|
|
45
|
+
labels: labelsProp,
|
|
33
46
|
}: {
|
|
34
47
|
name?: string | null;
|
|
35
48
|
email?: string | null;
|
|
@@ -39,8 +52,10 @@ export function ProfileSetting({
|
|
|
39
52
|
onChange: (value: string) => void;
|
|
40
53
|
onSave: () => void;
|
|
41
54
|
saving?: boolean;
|
|
42
|
-
labels:
|
|
55
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { profile } }}>` > English. */
|
|
56
|
+
labels?: Partial<ProfileSettingLabels>;
|
|
43
57
|
}) {
|
|
58
|
+
const labels = useAccountSettingsLabels("profile", labelsProp);
|
|
44
59
|
const dirty = value.trim().length > 0 && value.trim() !== (name ?? "");
|
|
45
60
|
// Generated, not the literal "display-name" it was: two of these on one page (an
|
|
46
61
|
// admin editing someone else's profile beside their own) shared an id, and the
|
|
@@ -52,7 +67,12 @@ export function ProfileSetting({
|
|
|
52
67
|
<UserAvatar name={name} email={email} size="lg" />
|
|
53
68
|
<div className="min-w-0">
|
|
54
69
|
<div className="text-sm font-medium">{labels.title}</div>
|
|
55
|
-
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
70
|
+
<div className="truncate font-mono text-xs text-[var(--text-muted)]">
|
|
71
|
+
{/* `email` was a required key that nothing rendered; it names the address
|
|
72
|
+
for a screen reader, which otherwise hears a bare string under a title. */}
|
|
73
|
+
<span className="sr-only">{labels.email}: </span>
|
|
74
|
+
{email ?? "—"}
|
|
75
|
+
</div>
|
|
56
76
|
</div>
|
|
57
77
|
</div>
|
|
58
78
|
<div className="grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-sm">
|
|
@@ -75,28 +95,20 @@ export function ProfileSetting({
|
|
|
75
95
|
);
|
|
76
96
|
}
|
|
77
97
|
|
|
78
|
-
export interface PasswordSettingLabels {
|
|
79
|
-
title: string;
|
|
80
|
-
current: string;
|
|
81
|
-
next: string;
|
|
82
|
-
confirm: string;
|
|
83
|
-
submit: string;
|
|
84
|
-
tooShort: string;
|
|
85
|
-
mismatch: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
98
|
export function PasswordSetting({
|
|
89
99
|
onSubmit,
|
|
90
100
|
pending,
|
|
91
101
|
minLength = 8,
|
|
92
|
-
labels,
|
|
102
|
+
labels: labelsProp,
|
|
93
103
|
}: {
|
|
94
104
|
/** Called with the validated (current, new) pair; return a promise to auto-clear on success. */
|
|
95
105
|
onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;
|
|
96
106
|
pending?: boolean;
|
|
97
107
|
minLength?: number;
|
|
98
|
-
labels:
|
|
108
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { password } }}>` > English. */
|
|
109
|
+
labels?: Partial<PasswordSettingLabels>;
|
|
99
110
|
}) {
|
|
111
|
+
const labels = useAccountSettingsLabels("password", labelsProp);
|
|
100
112
|
const [current, setCurrent] = useState("");
|
|
101
113
|
const [next, setNext] = useState("");
|
|
102
114
|
const [confirm, setConfirm] = useState("");
|
|
@@ -136,34 +148,35 @@ export function PasswordSetting({
|
|
|
136
148
|
);
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
/**
|
|
152
|
+
* What the app's setup call returned.
|
|
153
|
+
*
|
|
154
|
+
* `otpauthUri` (0.12.0) is what an authenticator backend actually hands out; the kit
|
|
155
|
+
* draws the QR code itself ({@link QrCode}) and reads the manual-entry key out of the
|
|
156
|
+
* URI's `secret` parameter when `secret` is not given. The `qrSvg` shape — a
|
|
157
|
+
* base64-encoded SVG image — is what the section took before, which made every app
|
|
158
|
+
* render a QR library to a static string and base64 it only for the kit to draw it back.
|
|
159
|
+
*/
|
|
160
|
+
export type TwoFactorSetupData =
|
|
161
|
+
| { qrSvg: string; secret: string }
|
|
162
|
+
| { otpauthUri: string; secret?: string };
|
|
163
|
+
|
|
164
|
+
/** The `secret` parameter of an `otpauth://` URI, or null. */
|
|
165
|
+
function otpauthSecret(uri: string): string | null {
|
|
166
|
+
const match = /[?&]secret=([^&#]*)/i.exec(uri);
|
|
167
|
+
if (!match) return null;
|
|
168
|
+
try {
|
|
169
|
+
return decodeURIComponent(match[1]) || null;
|
|
170
|
+
} catch {
|
|
171
|
+
return match[1] || null;
|
|
172
|
+
}
|
|
161
173
|
}
|
|
162
174
|
|
|
163
|
-
/**
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
/** "JBSW Y3DP EHPK 3PXP": the key in groups of four, which is how an authenticator's
|
|
176
|
+
* manual-entry screen shows it and how a person copies it by eye. */
|
|
177
|
+
function groupSecret(secret: string): string {
|
|
178
|
+
return secret.replace(/\s+/g, "").replace(/(.{4})(?=.)/g, "$1 ");
|
|
179
|
+
}
|
|
167
180
|
|
|
168
181
|
export function TwoFactorSetting({
|
|
169
182
|
enabled,
|
|
@@ -172,17 +185,23 @@ export function TwoFactorSetting({
|
|
|
172
185
|
onEnable,
|
|
173
186
|
onDisable,
|
|
174
187
|
busy,
|
|
175
|
-
|
|
188
|
+
renderQr,
|
|
189
|
+
labels: labelsProp,
|
|
176
190
|
}: {
|
|
177
191
|
enabled: boolean;
|
|
178
|
-
/**
|
|
179
|
-
setup:
|
|
192
|
+
/** What the app's setup call returned; null before setup starts. */
|
|
193
|
+
setup: TwoFactorSetupData | null;
|
|
180
194
|
onStartSetup: () => void;
|
|
181
195
|
onEnable: (code: string) => void;
|
|
182
196
|
onDisable: (password: string, code: string) => void;
|
|
183
197
|
busy?: boolean;
|
|
184
|
-
|
|
198
|
+
/** Draw the code yourself (a branded QR, a library you already ship). Given the
|
|
199
|
+
* `otpauthUri`; the kit's own {@link QrCode} otherwise. Unused with `qrSvg`. */
|
|
200
|
+
renderQr?: (otpauthUri: string) => ReactNode;
|
|
201
|
+
/** Prop > `<UiKitProvider labels={{ accountSettings: { twoFactor } }}>` > English. */
|
|
202
|
+
labels?: Partial<TwoFactorSettingLabels>;
|
|
185
203
|
}) {
|
|
204
|
+
const labels = useAccountSettingsLabels("twoFactor", labelsProp);
|
|
186
205
|
const [code, setCode] = useState("");
|
|
187
206
|
const [password, setPassword] = useState("");
|
|
188
207
|
// Browsers autofill one-time-code fields with the saved username; keep the
|
|
@@ -195,6 +214,9 @@ export function TwoFactorSetting({
|
|
|
195
214
|
// Only for the "Status: Enabled" composition — the punctuation between a field's
|
|
196
215
|
// name and its value is the language's (see `CommonLabels.fieldValue`).
|
|
197
216
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
217
|
+
const secret = setup
|
|
218
|
+
? (setup.secret ?? ("otpauthUri" in setup ? otpauthSecret(setup.otpauthUri) : null))
|
|
219
|
+
: null;
|
|
198
220
|
|
|
199
221
|
return (
|
|
200
222
|
<Card className="p-4 space-y-3">
|
|
@@ -216,15 +238,40 @@ export function TwoFactorSetting({
|
|
|
216
238
|
their quiet zone. On the dark theme a surface-coloured backing makes it
|
|
217
239
|
slow to acquire, and on a warm light preset it lowers the contrast ratio
|
|
218
240
|
the spec is written against. This is the one place in the kit where a
|
|
219
|
-
literal colour is the correct answer.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
literal colour is the correct answer. `data-private`: the code IS the
|
|
242
|
+
second factor, and demo mode blurs it like any other secret. */}
|
|
243
|
+
{"otpauthUri" in setup ? (
|
|
244
|
+
// No backing of its own: QrCode draws its quiet zone white inside the SVG.
|
|
245
|
+
<div data-private className="inline-block overflow-hidden rounded-md">
|
|
246
|
+
{renderQr ? (
|
|
247
|
+
renderQr(setup.otpauthUri)
|
|
248
|
+
) : (
|
|
249
|
+
<QrCode value={setup.otpauthUri} label={labels.qrAlt} className="size-48" />
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
) : (
|
|
253
|
+
<div data-private className="inline-block rounded-md bg-white p-2">
|
|
254
|
+
<img
|
|
255
|
+
alt={labels.qrAlt}
|
|
256
|
+
src={`data:image/svg+xml;base64,${setup.qrSvg}`}
|
|
257
|
+
className="h-48 w-48"
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
{secret && (
|
|
262
|
+
<div className="space-y-1">
|
|
263
|
+
<div className="text-xs text-[var(--text-secondary)]">{labels.secretHint}</div>
|
|
264
|
+
<div className="flex items-center gap-1">
|
|
265
|
+
{/* `dir="ltr"`: a base32 key is Latin letters and digits in a fixed
|
|
266
|
+
order, and in an RTL page the groups would otherwise be laid out
|
|
267
|
+
right to left. */}
|
|
268
|
+
<code data-private dir="ltr" className="break-all font-mono text-xs select-all">
|
|
269
|
+
{groupSecret(secret)}
|
|
270
|
+
</code>
|
|
271
|
+
<CopyButton text={secret} label={labels.copySecret} size="xs" tone="muted" />
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
228
275
|
<Input
|
|
229
276
|
inputMode="numeric"
|
|
230
277
|
autoComplete="one-time-code"
|
|
@@ -38,6 +38,15 @@ interface AmountInputProps {
|
|
|
38
38
|
className?: string;
|
|
39
39
|
id?: string;
|
|
40
40
|
ariaLabel?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —
|
|
43
|
+
* `{(ids) => <AmountInput {...ids} … />}` — is typed and reaches the input: the
|
|
44
|
+
* hint and error are described, a required amount is announced as required, and
|
|
45
|
+
* `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.
|
|
46
|
+
*/
|
|
47
|
+
"aria-describedby"?: string;
|
|
48
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
49
|
+
"aria-required"?: boolean | "true" | "false";
|
|
41
50
|
/** Every user-facing string this component and the two it composes own, so a
|
|
42
51
|
* translating host can supply its own. Each key optional, and each a per-field
|
|
43
52
|
* override of the kit-wide translation in `<UiKitProvider labels>`: `currency`
|
|
@@ -141,6 +150,72 @@ interface AmountInputProps {
|
|
|
141
150
|
* it left a 36px figure 16px to live in.
|
|
142
151
|
*/
|
|
143
152
|
align?: "start" | "center";
|
|
153
|
+
/**
|
|
154
|
+
* Decimals the committed amount is rounded to (kastlan 0.12: "12.345" and a
|
|
155
|
+
* calculator's "100/3" reached a `Numeric(10,2)` column unrounded and failed with a
|
|
156
|
+
* 422). Default: the currency's minor unit, from `Intl` (CHF and EUR 2, JPY 0). With
|
|
157
|
+
* no `currency`, nothing is rounded unless this is set. Rounding is half away from
|
|
158
|
+
* zero (12.345 → 12.35, -12.345 → -12.35) and happens when the figure settles: on
|
|
159
|
+
* blur, on Enter, and on every result the calculator writes back. Keystrokes are left
|
|
160
|
+
* alone, so "12.3" can still become "12.34".
|
|
161
|
+
*/
|
|
162
|
+
digits?: number;
|
|
163
|
+
/** The smallest amount the field settles on. A lower figure is raised to it when it
|
|
164
|
+
* settles, like `digits`. Compared with the signed figure. */
|
|
165
|
+
min?: number;
|
|
166
|
+
/** The largest amount the field settles on (a deposit release capped at the
|
|
167
|
+
* balance). A higher figure is lowered to it when it settles. */
|
|
168
|
+
max?: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The currency's minor unit (CHF 2, JPY 0), or `undefined` for no currency or a code
|
|
172
|
+
* `Intl` does not accept. */
|
|
173
|
+
function currencyDigits(code: string | undefined): number | undefined {
|
|
174
|
+
if (!code) return undefined;
|
|
175
|
+
try {
|
|
176
|
+
return new Intl.NumberFormat("en", { style: "currency", currency: code }).resolvedOptions()
|
|
177
|
+
.maximumFractionDigits;
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A settled figure, rounded to `digits` (half away from zero) and clamped to
|
|
185
|
+
* `min`/`max`. Only a plain number is touched. A half-typed draft or an expression
|
|
186
|
+
* comes back as it was. Rounding works in decimal exponent notation, because
|
|
187
|
+
* `Math.round(1.005 * 100)` is 100 (1.005 is 1.00499… in binary).
|
|
188
|
+
*/
|
|
189
|
+
function settleAmount(text: string, digits: number | undefined, min?: number, max?: number): string {
|
|
190
|
+
const t = text.trim();
|
|
191
|
+
if (!/^-?\d*\.?\d*$/.test(t) || !/\d/.test(t)) return text;
|
|
192
|
+
let n = Number(t);
|
|
193
|
+
if (!Number.isFinite(n)) return text;
|
|
194
|
+
if (digits !== undefined) {
|
|
195
|
+
const magnitude = Number(`${Math.round(Number(`${Math.abs(n)}e${digits}`))}e-${digits}`);
|
|
196
|
+
n = n < 0 ? -magnitude : magnitude;
|
|
197
|
+
}
|
|
198
|
+
if (min !== undefined && n < min) n = min;
|
|
199
|
+
if (max !== undefined && n > max) n = max;
|
|
200
|
+
const out = formatResult(n);
|
|
201
|
+
// Unchanged figures keep their own spelling ("12.50" stays "12.50").
|
|
202
|
+
return Number(out) === Number(t) ? text : out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The locale's decimal mark, as far as this field can type it: "," or "." (kastlan 40).
|
|
207
|
+
* The same answer `NumberField` gives, for the same reason: a mark outside those two
|
|
208
|
+
* (Arabic's "٫") is not one {@link sanitizeLive} lets through, so those locales keep
|
|
209
|
+
* the dot rather than get a field that eats the key it shows.
|
|
210
|
+
*/
|
|
211
|
+
function decimalMark(locale: string | undefined): "," | "." {
|
|
212
|
+
try {
|
|
213
|
+
const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
|
|
214
|
+
return part?.value === "," ? "," : ".";
|
|
215
|
+
} catch {
|
|
216
|
+
// An invalid tag throws a RangeError; a typo in a locale must not take the form down.
|
|
217
|
+
return ".";
|
|
218
|
+
}
|
|
144
219
|
}
|
|
145
220
|
|
|
146
221
|
// The consuming app's ONE money palette (`--money-expense` / `--money-income`),
|
|
@@ -177,15 +252,17 @@ const DISPLAY_INPUT_CLASS = cn(
|
|
|
177
252
|
* sum that comes out positive still takes the chip with it; "-20+50" → "7" is not,
|
|
178
253
|
* and "-20+" evaluates to nothing at all.
|
|
179
254
|
*/
|
|
180
|
-
function isResultOf(previous: string, text: string): boolean {
|
|
255
|
+
function isResultOf(previous: string, text: string, settle: (text: string) => string): boolean {
|
|
181
256
|
const n = evaluateExpression(previous);
|
|
182
|
-
|
|
257
|
+
// Compared settled, since the field rounds what the calculator hands it.
|
|
258
|
+
return n !== null && settle(formatResult(n)) === text.trim();
|
|
183
259
|
}
|
|
184
260
|
|
|
185
261
|
export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
186
|
-
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid, className, id, ariaLabel, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames }, ref) => {
|
|
262
|
+
({ value, onChange, currency, onCurrencyChange, placeholder, label, disabled, invalid: invalidProp, className, id, ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, autoFocus, tone = "neutral", negative = false, onNegativeChange, variant = "field", align = "start", labels, currencyNames, digits: digitsProp, min, max }, ref) => {
|
|
187
263
|
const generatedId = useId();
|
|
188
264
|
const fieldId = id ?? generatedId;
|
|
265
|
+
const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
|
|
189
266
|
const editable = !!onCurrencyChange;
|
|
190
267
|
// On phones we suppress the OS keyboard (inputMode="none" below) and show our
|
|
191
268
|
// own calculator numpad, so the desktop popover trigger is hidden. The
|
|
@@ -220,6 +297,20 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
220
297
|
// minus and has to survive keystroke-for-keystroke.
|
|
221
298
|
const signOwned = onNegativeChange !== undefined;
|
|
222
299
|
const shown = signOwned && negative && isBareAmount(value) ? `-${value}` : value;
|
|
300
|
+
// What the user SEES is `shown` with the locale's decimal mark (kastlan 40: a
|
|
301
|
+
// fr-CH user typed "1,5" and watched it turn into "1.5" under their fingers). Display only — `value`, `onChange`, the sign split and the settle all
|
|
302
|
+
// stay dot-decimal, because that string is the contract: RhfMoneyField's
|
|
303
|
+
// `parseAmount`, every app's `Number(value)` and the calculator's evaluator read
|
|
304
|
+
// it. The way back is free: {@link sanitizeLive} already folds a typed "," into
|
|
305
|
+
// ".", so a comma typed, pasted or tapped on the numpad lands as a dot.
|
|
306
|
+
//
|
|
307
|
+
// One character for one character, and no digit grouping: the caret stays where
|
|
308
|
+
// the typist put it, and "1.000" cannot be read back as a thousand in one locale
|
|
309
|
+
// and as one in the other. de-CH and it-CH are "." — CLDR says so, and this
|
|
310
|
+
// follows `Intl` rather than a guess about a country.
|
|
311
|
+
const locale = useKitLocale();
|
|
312
|
+
const mark = decimalMark(locale);
|
|
313
|
+
const display = mark === "," ? shown.replace(/\./g, ",") : shown;
|
|
223
314
|
// Every route into the field — typing, the numpad sheet, the desktop
|
|
224
315
|
// calculator, the blur/Enter commit — funnels through here, so the split is
|
|
225
316
|
// written once and the four entry paths cannot drift.
|
|
@@ -229,6 +320,8 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
229
320
|
// edit the field's own text that is simply what the field was showing; the
|
|
230
321
|
// desktop calculator keeps its expression in a popover the field never sees,
|
|
231
322
|
// so it passes it in (see `CalculatorButton`'s `onChange`).
|
|
323
|
+
const digits = digitsProp ?? currencyDigits(currency);
|
|
324
|
+
const settle = (text: string) => settleAmount(text, digits, min, max);
|
|
232
325
|
const handleText = (raw: string, previous: string = shown) => {
|
|
233
326
|
const text = sanitizeLive(raw);
|
|
234
327
|
if (!signOwned) {
|
|
@@ -239,7 +332,7 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
239
332
|
if (sign) {
|
|
240
333
|
// SET, not flip: re-reporting the same sign on every keystroke is a no-op.
|
|
241
334
|
onNegativeChange(sign === "-");
|
|
242
|
-
} else if (looksLikeExpression(previous) && isBareAmount(text) && isResultOf(previous, text)) {
|
|
335
|
+
} else if (looksLikeExpression(previous) && isBareAmount(text) && isResultOf(previous, text, settle)) {
|
|
243
336
|
// A calculation that just RESOLVED states its sign in both directions. The
|
|
244
337
|
// figure it ran on is the SIGNED one the user could see — "-12+30" is +18 —
|
|
245
338
|
// so a positive result has to move the chip too, or the field would answer
|
|
@@ -252,7 +345,7 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
252
345
|
}
|
|
253
346
|
onChange(sign ? rest : text);
|
|
254
347
|
};
|
|
255
|
-
const commit = () => handleText(commitExpression(shown));
|
|
348
|
+
const commit = () => handleText(settle(commitExpression(shown)));
|
|
256
349
|
const { open, setOpen, wrapperRef, panelRef, query, setQuery, inputRef } = useDropdownSearch();
|
|
257
350
|
// The chip the currency list hangs off (Keksdose dev#548). It is portalled now, so
|
|
258
351
|
// the panel needs a real trigger rect rather than a relative parent — see
|
|
@@ -271,7 +364,6 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
271
364
|
}, [labels?.currency, labels?.currencySearch]);
|
|
272
365
|
const currencyText = useKitLabels("currency", DEFAULT_CURRENCY_LABELS, currencyOverrides);
|
|
273
366
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
274
|
-
const locale = useKitLocale();
|
|
275
367
|
|
|
276
368
|
const selected = getCurrency(currency);
|
|
277
369
|
const filtered = useMemo(() => {
|
|
@@ -311,7 +403,7 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
311
403
|
// needs: a zero to aim at, at full size. The caller supplies it because
|
|
312
404
|
// only the app knows the locale's decimal separator.
|
|
313
405
|
placeholder={asDisplay ? (placeholder ?? "0") : label !== undefined ? " " : placeholder}
|
|
314
|
-
value={
|
|
406
|
+
value={display}
|
|
315
407
|
onChange={(e) => handleText(e.target.value)}
|
|
316
408
|
onFocus={() => setFocused(true)}
|
|
317
409
|
onBlur={() => {
|
|
@@ -350,6 +442,8 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
350
442
|
invalid && FIELD_INVALID,
|
|
351
443
|
)}
|
|
352
444
|
aria-invalid={invalid || undefined}
|
|
445
|
+
aria-describedby={ariaDescribedBy}
|
|
446
|
+
aria-required={ariaRequired}
|
|
353
447
|
/>
|
|
354
448
|
{label !== undefined && (
|
|
355
449
|
// The display shape drops the label VISUALLY, not from the accessibility
|
|
@@ -375,8 +469,10 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
375
469
|
>
|
|
376
470
|
{showCalc && (
|
|
377
471
|
<CalculatorButton
|
|
378
|
-
|
|
379
|
-
|
|
472
|
+
// The mark the field shows, as NumberInput hands its calculator; the
|
|
473
|
+
// evaluator reads either, and its result comes back dot-decimal.
|
|
474
|
+
value={display}
|
|
475
|
+
onChange={(result, expression) => handleText(settle(result), expression)}
|
|
380
476
|
className="px-1.5"
|
|
381
477
|
ariaLabel={labels?.calculatorTrigger}
|
|
382
478
|
labels={labels?.calculator}
|
|
@@ -458,11 +554,14 @@ export const AmountInput = forwardRef<HTMLInputElement, AmountInputProps>(
|
|
|
458
554
|
</div>
|
|
459
555
|
{showNumpad && (
|
|
460
556
|
<NumberPadSheet
|
|
461
|
-
|
|
557
|
+
// Localised like the field it mirrors; its keys come back through
|
|
558
|
+
// `sanitizeLive`, so the "." key and a "," both land as a dot.
|
|
559
|
+
value={display}
|
|
462
560
|
onChange={handleText}
|
|
463
561
|
onDone={() => innerRef.current?.blur()}
|
|
464
562
|
label={label}
|
|
465
563
|
labels={labels?.pad}
|
|
564
|
+
decimalMark={mark}
|
|
466
565
|
/>
|
|
467
566
|
)}
|
|
468
567
|
</div>
|