@eifi1/ui-kit 0.21.0 → 0.23.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 +42 -25
- package/dist/chart.d.ts +19 -14
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +69 -166
- package/dist/components/danger-confirm.js +144 -50
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +21 -2
- package/dist/components/icon-picker.js +19 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +130 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +43 -2
- package/dist/components/swatch-picker.js +80 -32
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +348 -41
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +68 -56
- package/src/components/autocomplete.tsx +198 -114
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
|
@@ -2,12 +2,13 @@ import { useId, useState } from "react";
|
|
|
2
2
|
import type { ComponentProps, ReactNode } from "react";
|
|
3
3
|
import { NumberInput } from "./number-input";
|
|
4
4
|
import { evaluateExpression, formatResult } from "../lib/calc";
|
|
5
|
+
import { decimalMark } from "../lib/decimal-marks";
|
|
5
6
|
import { useKitLocale } from "../i18n/kit-labels";
|
|
6
7
|
|
|
7
8
|
type NumberInputProps = ComponentProps<typeof NumberInput>;
|
|
8
9
|
|
|
9
10
|
export interface NumberFieldProps
|
|
10
|
-
extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid"> {
|
|
11
|
+
extends Omit<NumberInputProps, "value" | "onChange" | "onCommit" | "suffix" | "invalid" | "error" | "locale"> {
|
|
11
12
|
/** The committed number. `null` is "no value" — shown as an empty field. */
|
|
12
13
|
value: number | null;
|
|
13
14
|
/**
|
|
@@ -72,20 +73,6 @@ export interface NumberFieldProps
|
|
|
72
73
|
"aria-required"?: boolean | "true" | "false";
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
/** The locale's decimal mark, as far as this field can type it: "," or ".". A mark
|
|
76
|
-
* outside those two (Arabic's "٫") is not something {@link sanitizeLive} lets
|
|
77
|
-
* through, so those locales keep the dot rather than get a field that eats keys. */
|
|
78
|
-
function decimalMark(locale: string | undefined): "," | "." {
|
|
79
|
-
try {
|
|
80
|
-
const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
|
|
81
|
-
return part?.value === "," ? "," : ".";
|
|
82
|
-
} catch {
|
|
83
|
-
// An invalid tag throws a RangeError; a typo in a locale prop must not take the
|
|
84
|
-
// form down with it.
|
|
85
|
-
return ".";
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
76
|
/** What the draft reads as while it is being typed — see `onValueChange`. `undefined`
|
|
90
77
|
* is "not a number yet", distinct from `null` (empty). */
|
|
91
78
|
function parseLive(text: string): number | null | undefined {
|
|
@@ -109,10 +96,6 @@ function roundTo(n: number, digits: number | undefined): number {
|
|
|
109
96
|
return Number.isFinite(rounded) ? rounded : n;
|
|
110
97
|
}
|
|
111
98
|
|
|
112
|
-
// The error line under a field — the same type as `ui.tsx`'s (module-private) one,
|
|
113
|
-
// so a NumberField's message is indistinguishable from an Input's.
|
|
114
|
-
const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
115
|
-
|
|
116
99
|
/**
|
|
117
100
|
* A number field whose value IS a number: {@link NumberInput}'s text, calculator and
|
|
118
101
|
* numpad, with the parse/commit/revert loop every consumer was writing on top of it.
|
|
@@ -149,10 +132,20 @@ const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
|
149
132
|
*
|
|
150
133
|
* LOCALE. The decimal mark is the locale's: a German user sees and types "1,5".
|
|
151
134
|
* NumberInput normalises every keystroke to a dot (so its evaluator reads one
|
|
152
|
-
* alphabet); this field maps the dot back before the draft is shown
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
135
|
+
* alphabet); this field maps the dot back before the draft is shown. The field never
|
|
136
|
+
* WRITES digit grouping — "1.000" would read as a thousand in German and as one in
|
|
137
|
+
* English, and a field that parses its own display back must not have that ambiguity —
|
|
138
|
+
* but since 0.22 it READS it, through NumberInput (keksdose K5): "1.234,56" typed by a
|
|
139
|
+
* German user commits 1234.56 rather than 1.23456, and "1,234.56" by an English one
|
|
140
|
+
* likewise. While a figure is typed the typist's own text stays on screen ("1.234,5",
|
|
141
|
+
* or a dot typed as the decimal in a comma locale); it is re-spelt in the locale's mark
|
|
142
|
+
* when it commits.
|
|
143
|
+
*
|
|
144
|
+
* NOT FOR MONEY. `digits={2}` rounds, but a currency is more than two decimals: JPY has
|
|
145
|
+
* none, a unit price has four, and the chip, the minor-unit default and `min`/`max` on
|
|
146
|
+
* the SIGNED figure are {@link MoneyField}'s (number value) and {@link AmountInput}'s
|
|
147
|
+
* (string value). A NumberField whose `unit` is a currency says so on the console in
|
|
148
|
+
* development — see NumberInput's money guard.
|
|
156
149
|
*/
|
|
157
150
|
export function NumberField(props: NumberFieldProps) {
|
|
158
151
|
const {
|
|
@@ -255,10 +248,6 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
255
248
|
|
|
256
249
|
const generatedId = useId();
|
|
257
250
|
const fieldId = id ?? generatedId;
|
|
258
|
-
const errorId = useId();
|
|
259
|
-
// `null`/`false`/`""` are what `touched && errors.x` evaluates to on the happy path.
|
|
260
|
-
const hasError = error !== undefined && error !== null && error !== false && error !== "";
|
|
261
|
-
const describedBy = [ariaDescribedBy, hasError ? errorId : undefined].filter(Boolean).join(" ");
|
|
262
251
|
|
|
263
252
|
// Straight onto NumberInput's <input>, which declares both now. They used to be set
|
|
264
253
|
// on the DOM node from an effect, because NumberInput had no props for them.
|
|
@@ -277,11 +266,11 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
277
266
|
)
|
|
278
267
|
: label;
|
|
279
268
|
|
|
280
|
-
|
|
269
|
+
return (
|
|
281
270
|
<NumberInput
|
|
282
271
|
{...rest}
|
|
283
272
|
id={fieldId}
|
|
284
|
-
aria-describedby={
|
|
273
|
+
aria-describedby={ariaDescribedBy}
|
|
285
274
|
aria-required={required || undefined}
|
|
286
275
|
label={labelWithUnit}
|
|
287
276
|
value={draft}
|
|
@@ -289,26 +278,19 @@ export function NumberField(props: NumberFieldProps) {
|
|
|
289
278
|
onCommit={commit}
|
|
290
279
|
min={min}
|
|
291
280
|
max={max}
|
|
281
|
+
// The mark this field spells its draft in, so the two can never disagree when
|
|
282
|
+
// this field's `locale` is not the provider's.
|
|
283
|
+
locale={locale}
|
|
292
284
|
suffix={unitPlacement === "suffix" ? unit : undefined}
|
|
293
|
-
invalid={invalid ||
|
|
285
|
+
invalid={invalid || ariaInvalid === true || ariaInvalid === "true"}
|
|
286
|
+
// NumberInput's `error` — the message, the description, the paint, and Input's
|
|
287
|
+
// steady box: no wrapper for a field that never takes `error`, and the SAME one
|
|
288
|
+
// for a field that does, message or not, so the message coming or going never
|
|
289
|
+
// remounts the <input> mid-keystroke. Only forwarded when it was passed, since
|
|
290
|
+
// passing the key is what asks for the box.
|
|
291
|
+
{...("error" in props ? { error } : {})}
|
|
294
292
|
/>
|
|
295
293
|
);
|
|
296
|
-
// Input's FieldGroup, restated: no wrapper for a field that never takes `error`, so
|
|
297
|
-
// the prop changed nothing about the DOM a flex row lays out — and a STEADY one for a
|
|
298
|
-
// field that does, message or not. Wrapping only while a message showed swapped the
|
|
299
|
-
// element above the <input>, which remounted it: focus, caret and the half-typed
|
|
300
|
-
// draft's keystrokes were lost the moment the message came or went.
|
|
301
|
-
if (!hasError && !("error" in props)) return field;
|
|
302
|
-
return (
|
|
303
|
-
<div>
|
|
304
|
-
{field}
|
|
305
|
-
{hasError && (
|
|
306
|
-
<p id={errorId} className={FIELD_ERROR_CLASS}>
|
|
307
|
-
{error}
|
|
308
|
-
</p>
|
|
309
|
-
)}
|
|
310
|
-
</div>
|
|
311
|
-
);
|
|
312
294
|
}
|
|
313
295
|
|
|
314
296
|
/**
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { CalculatorButton, type CalculatorButtonLabels } from "./calculator";
|
|
4
4
|
import { NumberPadSheet, type NumberPadSheetLabels } from "./numpad-sheet";
|
|
5
5
|
import { FIELD_BASE, FIELD_DISPLAY, FIELD_INVALID, FLOATING_INPUT_CLASS, FloatingField, PHONE_QUERY } from "./ui";
|
|
6
|
+
import { CURRENCIES } from "./currency-select";
|
|
7
|
+
import { hasMessage, mergeDescribedBy } from "./choice-parts";
|
|
6
8
|
import { cn } from "../lib/cn";
|
|
7
|
-
import { commitExpression, formatResult
|
|
9
|
+
import { commitExpression, formatResult } from "../lib/calc";
|
|
10
|
+
import { decimalMark, localizeMark, readTyped, showsTyped } from "../lib/decimal-marks";
|
|
8
11
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
12
|
+
import { useKitLocale } from "../i18n/kit-labels";
|
|
9
13
|
|
|
10
14
|
interface NumberInputProps {
|
|
15
|
+
/** The text, DOT-decimal ("1234.5", or a calculation being typed: "12+5"), as
|
|
16
|
+
* `onChange` reports it — so a caller's `Number(value)` reads it. The field SHOWS it
|
|
17
|
+
* in the locale's mark (see `locale`). A comma here is read as the decimal mark too,
|
|
18
|
+
* never as grouping. */
|
|
11
19
|
value: string;
|
|
12
|
-
/** Fired on every keystroke with the
|
|
13
|
-
* a
|
|
20
|
+
/** Fired on every keystroke with the text as a number reads it: grouping marks read
|
|
21
|
+
* off ("1.234,56" → "1234.56" for a German typist, keksdose K5), the decimal mark
|
|
22
|
+
* folded to a dot, operators kept so a typed expression survives. */
|
|
14
23
|
onChange: (value: string) => void;
|
|
15
24
|
/** Fired on blur/Enter with the *evaluated* value — use this for save-on-blur
|
|
16
25
|
* fields so the save always sees the resolved number, never "10+5". */
|
|
@@ -60,6 +69,29 @@ interface NumberInputProps {
|
|
|
60
69
|
hint?: ReactNode;
|
|
61
70
|
/** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
|
|
62
71
|
invalid?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* What is wrong with the value, in the caller's own words — {@link Input}'s `error`,
|
|
74
|
+
* the same anatomy (keksdose K5/K4: of the kit's fields only `Input` and
|
|
75
|
+
* `NumberField` had one, so a goal target or a rate field wrote its message as a
|
|
76
|
+
* loose `<p>` that nothing pointed at). Rendered under the field, after a text
|
|
77
|
+
* `hint`; merged into `aria-describedby` after the caller's ids and the hint's; and
|
|
78
|
+
* it implies `invalid`, so the field paints as well as announces. `null`, `false`
|
|
79
|
+
* and `""` — a `touched && errors.x` on the happy path — are no message.
|
|
80
|
+
*
|
|
81
|
+
* Passing the key at all (even `undefined`) keeps the field in a steady wrapper,
|
|
82
|
+
* message or not, so the message coming and going never remounts the `<input>` and
|
|
83
|
+
* takes the caret with it — see `Input`'s `FieldGroup`. A field that never passes
|
|
84
|
+
* `error` renders exactly the DOM it did before the prop existed.
|
|
85
|
+
*/
|
|
86
|
+
error?: ReactNode;
|
|
87
|
+
/**
|
|
88
|
+
* BCP 47 tag that decides the decimal mark the field SHOWS; default the
|
|
89
|
+
* `<UiKitProvider locale>`, else the runtime's (keksdose K5). A French or Italian
|
|
90
|
+
* reader sees "3,5", a German one too, de-CH and en-US see "3.5" — `Intl`
|
|
91
|
+
* decides, not a guess about a country. Display only: `value` and `onChange` stay
|
|
92
|
+
* dot-decimal. {@link NumberField} passes its own `locale` through here.
|
|
93
|
+
*/
|
|
94
|
+
locale?: string;
|
|
63
95
|
/**
|
|
64
96
|
* Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —
|
|
65
97
|
* `{(ids) => <NumberInput {...ids} … />}` — is typed and reaches the input: the hint
|
|
@@ -145,45 +177,128 @@ export function stepNumber(
|
|
|
145
177
|
return clamp(Number((origin + k * step).toFixed(places)));
|
|
146
178
|
}
|
|
147
179
|
|
|
180
|
+
// The error line under a field — the type of `ui.tsx`'s (module-private) one, so a
|
|
181
|
+
// NumberInput's message is indistinguishable from an Input's.
|
|
182
|
+
const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
183
|
+
|
|
184
|
+
// ── The money guard (kastlan 5, Kurvenschmiede) ─────────────────────────────────
|
|
185
|
+
|
|
186
|
+
// The codes the kit's own currency pickers offer, as whole words: "Amount (CHF)",
|
|
187
|
+
// "EUR", "Kosten CHF". Not every ISO 4217 code — "ALL", "TOP" and "PEN" are codes too,
|
|
188
|
+
// and an upper-case label would trip on them.
|
|
189
|
+
const CURRENCY_CODE = new RegExp(`(?:^|[^A-Za-z])(?:${CURRENCIES.map((c) => c.code).join("|")})(?![A-Za-z])`);
|
|
190
|
+
// Symbols no unit of measure is written with. Not "kr", "R" or "Fr": those are words
|
|
191
|
+
// and units as often as they are money.
|
|
192
|
+
const CURRENCY_SYMBOL = /[€$£¥₹₺₽₪₩₫฿₴₦₱]/;
|
|
193
|
+
|
|
194
|
+
/** Text that names a currency and no unit to go with it. A slash makes it a rate or
|
|
195
|
+
* a unit price ("CHF/m²", "€/kWh") — a figure whose decimals are its column's
|
|
196
|
+
* business, not the currency's — and that is not what the guard is for. */
|
|
197
|
+
function namesMoney(node: ReactNode): boolean {
|
|
198
|
+
if (typeof node !== "string" && typeof node !== "number") return false;
|
|
199
|
+
const text = String(node);
|
|
200
|
+
return (CURRENCY_CODE.test(text) || CURRENCY_SYMBOL.test(text)) && !text.includes("/") && !text.includes("%");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Is this field being used for MONEY? A currency in the unit (`suffix="CHF"`), or in
|
|
205
|
+
* the label, `ariaLabel` or placeholder of a field with no unit at all ("Estimated
|
|
206
|
+
* cost (CHF)"). Any other suffix is a unit — "%", "mm", "N·m" — and says what the
|
|
207
|
+
* digits are, whatever the label says. `digits` plays no part: NumberInput has none,
|
|
208
|
+
* and Kurvenschmiede's engineering fields at 2 and 3 decimals are exactly what this
|
|
209
|
+
* must stay quiet about.
|
|
210
|
+
*/
|
|
211
|
+
function looksLikeMoney(suffix: ReactNode, texts: ReactNode[]): boolean {
|
|
212
|
+
if (suffix !== undefined && suffix !== null && suffix !== "") return namesMoney(suffix);
|
|
213
|
+
return texts.some(namesMoney);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
|
|
218
|
+
* production one — the consumer's bundler replaces `import.meta.env.DEV` with `false`
|
|
219
|
+
* there, and this branch is dead code — and never under vitest, where every consumer
|
|
220
|
+
* suite would print it once per render of every field it has not migrated yet. Read
|
|
221
|
+
* per call rather than once per module, so a test can switch it on with `vi.stubEnv`.
|
|
222
|
+
* Optional-chained: `import.meta.env` is a Vite injection, absent under plain Node.
|
|
223
|
+
*/
|
|
224
|
+
function devWarnings(): boolean {
|
|
225
|
+
return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
|
|
226
|
+
}
|
|
227
|
+
|
|
148
228
|
/**
|
|
149
229
|
* A numeric text field with a built-in calculator: type a calculation straight
|
|
150
230
|
* in (e.g. "12+5", evaluated on blur/Enter) or click the trailing calculator
|
|
151
231
|
* icon for a keypad. String `value`/`onChange` contract, mirroring
|
|
152
|
-
* {@link AmountInput} — the currency-free counterpart for
|
|
153
|
-
*
|
|
232
|
+
* {@link AmountInput} — the currency-free counterpart for quantities, rates,
|
|
233
|
+
* percentages and measurements.
|
|
234
|
+
*
|
|
235
|
+
* NOT FOR MONEY. This field never rounds: "12.345", or a calculator's "100/3", is
|
|
236
|
+
* reported exactly as typed. kastlan's journal lines, a payment allocation and a
|
|
237
|
+
* defect's estimated cost were NumberInputs, and a 3-decimal amount reached a
|
|
238
|
+
* 2-decimal Money column and failed the save with a 422 — in one wizard after the
|
|
239
|
+
* record before it had already been created (kastlan 5). Money goes in
|
|
240
|
+
* {@link AmountInput} (string value, like this one) or {@link MoneyField} (number
|
|
241
|
+
* value): both settle to the currency's minor unit on blur, Enter and every calculator
|
|
242
|
+
* result, and take `digits` for a unit price at a finer scale. In development a
|
|
243
|
+
* NumberInput that looks like money — a currency as its unit, or a currency in the
|
|
244
|
+
* label of a field with no unit — says so on the console, once per field. A unit of
|
|
245
|
+
* measure ("mm", "N·m", "%") keeps it quiet, as does any number of decimals.
|
|
246
|
+
*
|
|
247
|
+
* THE LOCALE'S MARK (keksdose K5). The field shows the `locale`'s decimal mark — "3,5"
|
|
248
|
+
* to a French, Italian or German reader — and reads what is typed the way
|
|
249
|
+
* {@link AmountInput} does (keksdose G1): "1.234,56" from a German typist is 1234.56,
|
|
250
|
+
* "1,234.56" from an English one too, and a lone mark that cannot be grouping is the
|
|
251
|
+
* decimal, whichever key typed it. Before, it always showed "3.5" and folded
|
|
252
|
+
* "1.234,56" into 1.23456. `value` and `onChange` stay dot-decimal, which is the
|
|
253
|
+
* contract every caller's `Number(value)` reads.
|
|
254
|
+
*
|
|
255
|
+
* While a figure is being typed the field shows the typist's OWN text — "1.234,5"
|
|
256
|
+
* stays exactly that — rather than the value re-spelt: re-spelling turned a grouping
|
|
257
|
+
* "." into a "," under their fingers, after which no rule could tell the two apart, and
|
|
258
|
+
* a text that changes length under the caret moves it to the end. It is re-spelt in the
|
|
259
|
+
* locale's mark, without grouping, once the figure settles (blur, Enter, a step or a
|
|
260
|
+
* calculator result) or when the value changes from outside.
|
|
154
261
|
*/
|
|
155
|
-
export function NumberInput({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
262
|
+
export function NumberInput(props: NumberInputProps) {
|
|
263
|
+
const {
|
|
264
|
+
value,
|
|
265
|
+
onChange,
|
|
266
|
+
onCommit,
|
|
267
|
+
label,
|
|
268
|
+
ariaLabel,
|
|
269
|
+
labels,
|
|
270
|
+
placeholder,
|
|
271
|
+
disabled,
|
|
272
|
+
autoFocus,
|
|
273
|
+
className,
|
|
274
|
+
inputClassName,
|
|
275
|
+
id,
|
|
276
|
+
calculator = true,
|
|
277
|
+
variant = "field",
|
|
278
|
+
hint,
|
|
279
|
+
invalid: invalidProp,
|
|
280
|
+
error,
|
|
281
|
+
locale: localeProp,
|
|
282
|
+
"aria-describedby": ariaDescribedBy,
|
|
283
|
+
"aria-invalid": ariaInvalid,
|
|
284
|
+
"aria-required": ariaRequired,
|
|
285
|
+
suffix,
|
|
286
|
+
step,
|
|
287
|
+
min,
|
|
288
|
+
max,
|
|
289
|
+
} = props;
|
|
180
290
|
const generatedId = useId();
|
|
181
291
|
const fieldId = id ?? generatedId;
|
|
182
292
|
const hintId = useId();
|
|
293
|
+
const errorId = useId();
|
|
183
294
|
// Text is a caption under the field; a FieldHint rides the label line. See `hint`.
|
|
184
295
|
const textHint = (typeof hint === "string" && hint !== "") || typeof hint === "number";
|
|
185
|
-
const
|
|
186
|
-
|
|
296
|
+
const hasError = hasMessage(error);
|
|
297
|
+
// Passed at all — even as `undefined` — the field keeps its box; see `error`.
|
|
298
|
+
const reserve = "error" in props || "hint" in props;
|
|
299
|
+
// The standing advice first, the news second: the caller's ids, the caption, the error.
|
|
300
|
+
const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);
|
|
301
|
+
const invalid = Boolean(invalidProp) || hasError || ariaInvalid === true || ariaInvalid === "true";
|
|
187
302
|
const labelled = label !== undefined;
|
|
188
303
|
// On phones we suppress the OS keyboard (inputMode="none" below) for our own
|
|
189
304
|
// calculator numpad, so the desktop popover trigger is hidden (feedback #334).
|
|
@@ -219,13 +334,52 @@ export function NumberInput({
|
|
|
219
334
|
const showNumpad = isMobile && !disabled && focused;
|
|
220
335
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
221
336
|
|
|
337
|
+
// The mark is the locale's; see the component's note. `derived` is the value in that
|
|
338
|
+
// mark — one character for one, no grouping — and `draft` the typist's own text,
|
|
339
|
+
// shown only while it still says what the value says (see `showsTyped`).
|
|
340
|
+
const mark = decimalMark(useKitLocale(localeProp));
|
|
341
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
342
|
+
const derived = localizeMark(value, mark);
|
|
343
|
+
const display = draft !== null && showsTyped(draft, mark, value) ? draft : derived;
|
|
344
|
+
|
|
345
|
+
// A keystroke, a paste or a numpad key: the typist's text is kept, the value is what
|
|
346
|
+
// a number reads it as.
|
|
347
|
+
const typed = (raw: string) => {
|
|
348
|
+
setDraft(raw);
|
|
349
|
+
onChange(readTyped(raw, mark));
|
|
350
|
+
};
|
|
351
|
+
// A RESULT — the calculator's, the numpad's "=" — is dot-decimal already. It does not
|
|
352
|
+
// go through `typed`: "1.234" out of `100/81.03…` is never a thousand.
|
|
353
|
+
const result = (next: string) => {
|
|
354
|
+
setDraft(null);
|
|
355
|
+
onChange(next);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// The money guard: once per field, development only. See `looksLikeMoney`.
|
|
359
|
+
const warnedMoney = useRef(false);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
if (warnedMoney.current || !devWarnings()) return;
|
|
362
|
+
if (!looksLikeMoney(suffix, [label, ariaLabel, placeholder])) return;
|
|
363
|
+
warnedMoney.current = true;
|
|
364
|
+
const name = [label, ariaLabel, suffix].find((t) => typeof t === "string" || typeof t === "number");
|
|
365
|
+
console.warn(
|
|
366
|
+
`[ui-kit] NumberInput${name === undefined ? "" : ` "${String(name)}"`} looks like a money field: a ` +
|
|
367
|
+
`currency, and no unit. NumberInput never rounds, so "12.345" or a calculator's "100/3" is sent with ` +
|
|
368
|
+
`three or more decimals (kastlan 5: a 422 from a 2-decimal Money column). Use AmountInput (string ` +
|
|
369
|
+
`value) or MoneyField (number value) — both settle to the currency's minor unit. ` +
|
|
370
|
+
`(Development builds only; once per field.)`,
|
|
371
|
+
);
|
|
372
|
+
}, [suffix, label, ariaLabel, placeholder]);
|
|
373
|
+
|
|
222
374
|
const commit = () => {
|
|
375
|
+
setDraft(null);
|
|
223
376
|
const next = commitExpression(value);
|
|
224
377
|
if (next !== value) onChange(next);
|
|
225
378
|
onCommit?.(next);
|
|
226
379
|
};
|
|
227
380
|
|
|
228
381
|
const stepBy = (count: number) => {
|
|
382
|
+
setDraft(null);
|
|
229
383
|
// The step starts from the text as a commit would read it, so "12+5" steps from 17.
|
|
230
384
|
const resolved = commitExpression(value).trim();
|
|
231
385
|
const n = resolved === "" ? NaN : Number(resolved);
|
|
@@ -269,8 +423,8 @@ export function NumberInput({
|
|
|
269
423
|
// and an empty borderless figure shows nothing — so it keeps whatever
|
|
270
424
|
// placeholder the caller gave, falling back to a zero to aim at.
|
|
271
425
|
placeholder={asDisplay ? (placeholder ?? "0") : labelled ? " " : placeholder}
|
|
272
|
-
value={
|
|
273
|
-
onChange={(e) =>
|
|
426
|
+
value={display}
|
|
427
|
+
onChange={(e) => typed(e.target.value)}
|
|
274
428
|
onFocus={() => setFocused(true)}
|
|
275
429
|
onBlur={() => {
|
|
276
430
|
commit();
|
|
@@ -314,8 +468,10 @@ export function NumberInput({
|
|
|
314
468
|
<div ref={endRef} className="absolute inset-y-0 end-0 flex items-center">
|
|
315
469
|
{showCalc && (
|
|
316
470
|
<CalculatorButton
|
|
317
|
-
value
|
|
318
|
-
|
|
471
|
+
// Seeded with the value in the field's mark but WITHOUT the typist's
|
|
472
|
+
// grouping: the evaluator reads either mark, but "1.234,5" is two of them.
|
|
473
|
+
value={derived}
|
|
474
|
+
onChange={result}
|
|
319
475
|
className="self-stretch px-2.5"
|
|
320
476
|
ariaLabel={labels?.calculatorTrigger}
|
|
321
477
|
labels={labels?.calculator}
|
|
@@ -335,25 +491,36 @@ export function NumberInput({
|
|
|
335
491
|
)}
|
|
336
492
|
{showNumpad && (
|
|
337
493
|
<NumberPadSheet
|
|
338
|
-
|
|
339
|
-
|
|
494
|
+
// Localised like the field it mirrors, as AmountInput's is: its keys are
|
|
495
|
+
// typing, read by the locale's marks like the keyboard's; "=" is a result.
|
|
496
|
+
value={display}
|
|
497
|
+
onChange={typed}
|
|
498
|
+
onResult={result}
|
|
340
499
|
onDone={() => inputRef.current?.blur()}
|
|
341
500
|
label={label}
|
|
342
501
|
labels={labels?.pad}
|
|
502
|
+
decimalMark={mark}
|
|
343
503
|
/>
|
|
344
504
|
)}
|
|
345
505
|
</FloatingField>
|
|
346
506
|
);
|
|
347
|
-
if (!textHint) return field;
|
|
507
|
+
if (!textHint && !hasError && !reserve) return field;
|
|
348
508
|
// Outside the field's own `relative` box, as Select's caption is: the calculator
|
|
349
509
|
// and the unit are `inset-y-0` in it and would stretch down over a second line.
|
|
350
510
|
// `className` stays on the field, so adding a caption cannot change what it styles.
|
|
351
511
|
return (
|
|
352
512
|
<div>
|
|
353
513
|
{field}
|
|
354
|
-
|
|
355
|
-
{
|
|
356
|
-
|
|
514
|
+
{textHint && (
|
|
515
|
+
<p id={hintId} className="mt-1 text-[11px] leading-tight text-[var(--text-muted)]">
|
|
516
|
+
{hint}
|
|
517
|
+
</p>
|
|
518
|
+
)}
|
|
519
|
+
{hasError && (
|
|
520
|
+
<p id={errorId} className={FIELD_ERROR_CLASS}>
|
|
521
|
+
{error}
|
|
522
|
+
</p>
|
|
523
|
+
)}
|
|
357
524
|
</div>
|
|
358
525
|
);
|
|
359
526
|
}
|