@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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The field for a verification code — the six digits an authenticator app shows, or the
|
|
6
|
+
* ones a sign-in mail or SMS carries.
|
|
7
|
+
*
|
|
8
|
+
* Asked for by kastlan (input audit, item 3) and keksdose (K8) for the sign-in
|
|
9
|
+
* verification step, and already written twice by hand inside {@link TwoFactorSetting}.
|
|
10
|
+
* The three copies agreed on `inputMode="numeric"` and `autoComplete="one-time-code"`
|
|
11
|
+
* and on nothing else: kastlan's login stripped non-digits and capped at six, keksdose's
|
|
12
|
+
* capped at eight and kept whatever was typed, and the settings card kept everything,
|
|
13
|
+
* spaces included — so "123 456" pasted from an authenticator that groups its digits
|
|
14
|
+
* reached the server with the space in it.
|
|
15
|
+
*
|
|
16
|
+
* ONE FIELD, NOT SIX BOXES. The six-box look is the fashionable one, and every part of
|
|
17
|
+
* it works against the two ways a code actually arrives. A paste lands in ONE input,
|
|
18
|
+
* so the boxes need a paste handler that spreads it; the platform's own code autofill
|
|
19
|
+
* (iOS and macOS offer the code from Messages and Mail above the keyboard, Android's
|
|
20
|
+
* keyboards do the same) fills the ONE field marked `one-time-code`, so the boxes need
|
|
21
|
+
* that field to exist anyway, hidden; and a screen reader hears six unlabelled inputs
|
|
22
|
+
* of one character each. One text field gets all three right for free.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT DOES TO THE TEXT:
|
|
25
|
+
* - **Digits only**, as a STRING. A code may start with 0, and a number field would
|
|
26
|
+
* drop it ("012345" → 12345) — kastlan's login says as much in a comment. Every
|
|
27
|
+
* character that is not a digit is dropped as it arrives, so the spaces and dashes
|
|
28
|
+
* of "123 456" and "123-456" go, whether typed or pasted. Full-width digits (what a
|
|
29
|
+
* Chinese input method types: "123") are folded to ASCII first (NFKC).
|
|
30
|
+
* - **No `maxLength` attribute.** The browser applies it BEFORE the change event, so
|
|
31
|
+
* pasting "123 456" into a `maxLength={6}` field kept "123 45" and the cleaning saw
|
|
32
|
+
* one digit short. The length is enforced here instead, after the cleaning: a field
|
|
33
|
+
* that is full refuses another digit, as `maxLength` would.
|
|
34
|
+
* - **A paste that is a whole code replaces the field**, wherever the caret is: a
|
|
35
|
+
* person pasting six digits means "this is the code", not "insert these into the
|
|
36
|
+
* stale one".
|
|
37
|
+
* - **The caret stays put** when a character is dropped, rather than jumping to the
|
|
38
|
+
* end as a controlled input does when its value is put back.
|
|
39
|
+
*
|
|
40
|
+
* `onComplete` fires when an edit — a keystroke, a paste, the platform's autofill —
|
|
41
|
+
* makes the code `length` digits long. It is a notification, nothing more: the field
|
|
42
|
+
* never submits a form on its own. Whether a full code verifies at once is the app's
|
|
43
|
+
* call (a sign-in step may; a "turn off two-factor" form that also wants a password
|
|
44
|
+
* should not). It does not fire for a value set from outside.
|
|
45
|
+
*
|
|
46
|
+
* `label`, `hint` and `error` read as on every kit field: a floating label, a text hint
|
|
47
|
+
* as a caption under the field (a {@link FieldHint} "?" rides the label line instead),
|
|
48
|
+
* the error under that, both attached through `aria-describedby` after the caller's own.
|
|
49
|
+
*/
|
|
50
|
+
interface OneTimeCodeInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type" | "inputMode" | "autoComplete" | "maxLength" | "pattern"> {
|
|
51
|
+
/** The digits so far — a string, so leading zeros survive. */
|
|
52
|
+
value: string;
|
|
53
|
+
/** The cleaned code after every edit: digits only, never longer than `length`. */
|
|
54
|
+
onChange: (code: string) => void;
|
|
55
|
+
/** How many digits the code has. Default 6. The field refuses digits past it. */
|
|
56
|
+
length?: number;
|
|
57
|
+
/** Called with the code when an edit makes it `length` digits long. Never submits
|
|
58
|
+
* anything itself. */
|
|
59
|
+
onComplete?: (code: string) => void;
|
|
60
|
+
/** The floating label, as on {@link Input}. Without one, name the field with
|
|
61
|
+
* `aria-label` or `aria-labelledby` (kastlan's login points it at its prompt). */
|
|
62
|
+
label?: ReactNode;
|
|
63
|
+
/** Plain text: a caption under the field, attached with `aria-describedby`. Anything
|
|
64
|
+
* else (a {@link FieldHint}): on the label line — or at the field's end edge when
|
|
65
|
+
* there is no label. The rule {@link Select}'s `hint` follows. */
|
|
66
|
+
hint?: ReactNode;
|
|
67
|
+
/** What is wrong ("That code has expired"). Rendered under the field, attached with
|
|
68
|
+
* `aria-describedby`, and implies `invalid`. */
|
|
69
|
+
error?: ReactNode;
|
|
70
|
+
/** Paint the field invalid and set `aria-invalid` without a message of its own. */
|
|
71
|
+
invalid?: boolean;
|
|
72
|
+
/** Classes for the `<input>`; `className` styles the field's wrapper once it has a
|
|
73
|
+
* label, the `<input>` itself when it has none — as on {@link Input}. */
|
|
74
|
+
inputClassName?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Keep the field `readOnly` until it first takes focus.
|
|
77
|
+
*
|
|
78
|
+
* A browser that has a login saved for the site fills it in on load — and on a page
|
|
79
|
+
* where the code field is the only text field near a password field, it can take the
|
|
80
|
+
* code field for the username and write the address into it. A read-only field is
|
|
81
|
+
* never autofilled, and focus is the moment a person (or the platform's code
|
|
82
|
+
* suggestion, which needs a focused field anyway) starts to use it. So the field
|
|
83
|
+
* stays read-only until then, and only until then.
|
|
84
|
+
*
|
|
85
|
+
* It keeps the LOOK of an editable field throughout ({@link FIELD_WRITABLE_LOOK}):
|
|
86
|
+
* this is the one place where read-only does not mean "you may not type here", and
|
|
87
|
+
* without the opt-out the field would sit there greyed until it is clicked.
|
|
88
|
+
*
|
|
89
|
+
* {@link TwoFactorSetting} has always done this — its code fields share a page with
|
|
90
|
+
* the current-password field of the disable form — and keeps doing it through this
|
|
91
|
+
* prop. Off by default, because it is a trade: a field that is read-only at the
|
|
92
|
+
* moment focus lands can be announced as read-only by a screen reader, and on some
|
|
93
|
+
* mobile browsers it does not raise the keyboard until a second tap. Turn it on where
|
|
94
|
+
* a saved login actually lands in the field.
|
|
95
|
+
*/
|
|
96
|
+
readOnlyUntilFocus?: boolean;
|
|
97
|
+
}
|
|
98
|
+
declare const OneTimeCodeInput: react.ForwardRefExoticComponent<OneTimeCodeInputProps & react.RefAttributes<HTMLInputElement>>;
|
|
99
|
+
|
|
100
|
+
export { OneTimeCodeInput, type OneTimeCodeInputProps };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId, useState } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
|
|
6
|
+
import { FIELD_BASE, FIELD_INVALID, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FloatingField } from "./ui.js";
|
|
7
|
+
const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
8
|
+
const FIELD_CAPTION_CLASS = "mt-1 text-[11px] leading-tight text-[var(--text-muted)]";
|
|
9
|
+
const CODE_TYPE = "tabular-nums tracking-widest";
|
|
10
|
+
function digitsOf(text) {
|
|
11
|
+
return text.normalize("NFKC").replace(/\D/g, "");
|
|
12
|
+
}
|
|
13
|
+
const OneTimeCodeInput = forwardRef(
|
|
14
|
+
function OneTimeCodeInput2(props, ref) {
|
|
15
|
+
const {
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
length = 6,
|
|
19
|
+
onComplete,
|
|
20
|
+
label,
|
|
21
|
+
hint,
|
|
22
|
+
error,
|
|
23
|
+
invalid: invalidProp,
|
|
24
|
+
className,
|
|
25
|
+
inputClassName,
|
|
26
|
+
readOnlyUntilFocus = false,
|
|
27
|
+
readOnly,
|
|
28
|
+
id,
|
|
29
|
+
placeholder,
|
|
30
|
+
onFocus,
|
|
31
|
+
onPaste,
|
|
32
|
+
"aria-describedby": ariaDescribedBy,
|
|
33
|
+
"aria-invalid": ariaInvalid,
|
|
34
|
+
...rest
|
|
35
|
+
} = props;
|
|
36
|
+
const size = Math.max(1, Math.floor(length) || 1);
|
|
37
|
+
const generatedId = useId();
|
|
38
|
+
const fieldId = id ?? generatedId;
|
|
39
|
+
const hintId = useId();
|
|
40
|
+
const errorId = useId();
|
|
41
|
+
const textHint = typeof hint === "string" && hint !== "" || typeof hint === "number";
|
|
42
|
+
const hasError = hasMessage(error);
|
|
43
|
+
const reserve = "error" in props || "hint" in props;
|
|
44
|
+
const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);
|
|
45
|
+
const invalid = Boolean(invalidProp) || hasError || ariaInvalid === true || ariaInvalid === "true";
|
|
46
|
+
const labelled = label !== void 0;
|
|
47
|
+
const [focusedOnce, setFocusedOnce] = useState(false);
|
|
48
|
+
const guarding = readOnlyUntilFocus && !focusedOnce;
|
|
49
|
+
const commit = (next) => {
|
|
50
|
+
if (next === value) return;
|
|
51
|
+
onChange(next);
|
|
52
|
+
if (next.length === size) onComplete?.(next);
|
|
53
|
+
};
|
|
54
|
+
const handleChange = (e) => {
|
|
55
|
+
const input2 = e.currentTarget;
|
|
56
|
+
const raw = input2.value;
|
|
57
|
+
const cleaned = digitsOf(raw);
|
|
58
|
+
const refused = cleaned.length > size && value.length >= size;
|
|
59
|
+
const next = refused ? value : cleaned.slice(0, size);
|
|
60
|
+
if (next !== raw) {
|
|
61
|
+
const caret = input2.selectionStart ?? raw.length;
|
|
62
|
+
const at = refused ? Math.max(0, caret - (raw.length - value.length)) : Math.min(digitsOf(raw.slice(0, caret)).length, next.length);
|
|
63
|
+
input2.value = next;
|
|
64
|
+
try {
|
|
65
|
+
input2.setSelectionRange(at, at);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
commit(next);
|
|
70
|
+
};
|
|
71
|
+
const handlePaste = (e) => {
|
|
72
|
+
onPaste?.(e);
|
|
73
|
+
if (e.defaultPrevented || readOnly || guarding || rest.disabled) return;
|
|
74
|
+
const pasted = digitsOf(e.clipboardData.getData("text"));
|
|
75
|
+
if (pasted.length !== size) return;
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
commit(pasted);
|
|
78
|
+
};
|
|
79
|
+
const handleFocus = (e) => {
|
|
80
|
+
if (guarding) setFocusedOnce(true);
|
|
81
|
+
onFocus?.(e);
|
|
82
|
+
};
|
|
83
|
+
const input = /* @__PURE__ */ jsx(
|
|
84
|
+
"input",
|
|
85
|
+
{
|
|
86
|
+
ref,
|
|
87
|
+
id: labelled ? fieldId : id,
|
|
88
|
+
...rest,
|
|
89
|
+
type: "text",
|
|
90
|
+
inputMode: "numeric",
|
|
91
|
+
autoComplete: "one-time-code",
|
|
92
|
+
pattern: "[0-9]*",
|
|
93
|
+
placeholder: labelled ? " " : placeholder,
|
|
94
|
+
value,
|
|
95
|
+
readOnly: readOnly || guarding,
|
|
96
|
+
onChange: handleChange,
|
|
97
|
+
onPaste: handlePaste,
|
|
98
|
+
onFocus: handleFocus,
|
|
99
|
+
"aria-invalid": invalid || void 0,
|
|
100
|
+
"aria-describedby": describedBy,
|
|
101
|
+
className: cn(
|
|
102
|
+
labelled ? FLOATING_INPUT_CLASS : FIELD_BASE,
|
|
103
|
+
CODE_TYPE,
|
|
104
|
+
// Read-only for the guard's sake only: keep the editable look. A caller's
|
|
105
|
+
// own `readOnly` keeps FIELD_BASE's settled grey, as everywhere else.
|
|
106
|
+
guarding && !readOnly && FIELD_WRITABLE_LOOK,
|
|
107
|
+
!labelled && className,
|
|
108
|
+
inputClassName,
|
|
109
|
+
invalid && FIELD_INVALID
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
const nonTextHint = hasMessage(hint) && !textHint;
|
|
114
|
+
let field;
|
|
115
|
+
if (labelled) {
|
|
116
|
+
field = /* @__PURE__ */ jsx(FloatingField, { className, htmlFor: fieldId, label, hint: nonTextHint ? hint : void 0, children: input });
|
|
117
|
+
} else if (nonTextHint) {
|
|
118
|
+
field = /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
119
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: input }),
|
|
120
|
+
/* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", children: hint })
|
|
121
|
+
] });
|
|
122
|
+
} else {
|
|
123
|
+
field = input;
|
|
124
|
+
}
|
|
125
|
+
if (!textHint && !hasError && !reserve) return field;
|
|
126
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
127
|
+
field,
|
|
128
|
+
textHint && /* @__PURE__ */ jsx("p", { id: hintId, className: FIELD_CAPTION_CLASS, children: hint }),
|
|
129
|
+
hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
|
|
130
|
+
] });
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
OneTimeCodeInput.displayName = "OneTimeCodeInput";
|
|
134
|
+
export {
|
|
135
|
+
OneTimeCodeInput
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=one-time-code-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/one-time-code-input.tsx"],"sourcesContent":["import { forwardRef, useId, useState } from \"react\";\nimport type { ChangeEvent, ClipboardEvent, FocusEvent, InputHTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { FIELD_BASE, FIELD_INVALID, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FloatingField } from \"./ui\";\n\n/**\n * The field for a verification code — the six digits an authenticator app shows, or the\n * ones a sign-in mail or SMS carries.\n *\n * Asked for by kastlan (input audit, item 3) and keksdose (K8) for the sign-in\n * verification step, and already written twice by hand inside {@link TwoFactorSetting}.\n * The three copies agreed on `inputMode=\"numeric\"` and `autoComplete=\"one-time-code\"`\n * and on nothing else: kastlan's login stripped non-digits and capped at six, keksdose's\n * capped at eight and kept whatever was typed, and the settings card kept everything,\n * spaces included — so \"123 456\" pasted from an authenticator that groups its digits\n * reached the server with the space in it.\n *\n * ONE FIELD, NOT SIX BOXES. The six-box look is the fashionable one, and every part of\n * it works against the two ways a code actually arrives. A paste lands in ONE input,\n * so the boxes need a paste handler that spreads it; the platform's own code autofill\n * (iOS and macOS offer the code from Messages and Mail above the keyboard, Android's\n * keyboards do the same) fills the ONE field marked `one-time-code`, so the boxes need\n * that field to exist anyway, hidden; and a screen reader hears six unlabelled inputs\n * of one character each. One text field gets all three right for free.\n *\n * WHAT IT DOES TO THE TEXT:\n * - **Digits only**, as a STRING. A code may start with 0, and a number field would\n * drop it (\"012345\" → 12345) — kastlan's login says as much in a comment. Every\n * character that is not a digit is dropped as it arrives, so the spaces and dashes\n * of \"123 456\" and \"123-456\" go, whether typed or pasted. Full-width digits (what a\n * Chinese input method types: \"123\") are folded to ASCII first (NFKC).\n * - **No `maxLength` attribute.** The browser applies it BEFORE the change event, so\n * pasting \"123 456\" into a `maxLength={6}` field kept \"123 45\" and the cleaning saw\n * one digit short. The length is enforced here instead, after the cleaning: a field\n * that is full refuses another digit, as `maxLength` would.\n * - **A paste that is a whole code replaces the field**, wherever the caret is: a\n * person pasting six digits means \"this is the code\", not \"insert these into the\n * stale one\".\n * - **The caret stays put** when a character is dropped, rather than jumping to the\n * end as a controlled input does when its value is put back.\n *\n * `onComplete` fires when an edit — a keystroke, a paste, the platform's autofill —\n * makes the code `length` digits long. It is a notification, nothing more: the field\n * never submits a form on its own. Whether a full code verifies at once is the app's\n * call (a sign-in step may; a \"turn off two-factor\" form that also wants a password\n * should not). It does not fire for a value set from outside.\n *\n * `label`, `hint` and `error` read as on every kit field: a floating label, a text hint\n * as a caption under the field (a {@link FieldHint} \"?\" rides the label line instead),\n * the error under that, both attached through `aria-describedby` after the caller's own.\n */\nexport interface OneTimeCodeInputProps\n extends Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"inputMode\" | \"autoComplete\" | \"maxLength\" | \"pattern\"\n > {\n /** The digits so far — a string, so leading zeros survive. */\n value: string;\n /** The cleaned code after every edit: digits only, never longer than `length`. */\n onChange: (code: string) => void;\n /** How many digits the code has. Default 6. The field refuses digits past it. */\n length?: number;\n /** Called with the code when an edit makes it `length` digits long. Never submits\n * anything itself. */\n onComplete?: (code: string) => void;\n /** The floating label, as on {@link Input}. Without one, name the field with\n * `aria-label` or `aria-labelledby` (kastlan's login points it at its prompt). */\n label?: ReactNode;\n /** Plain text: a caption under the field, attached with `aria-describedby`. Anything\n * else (a {@link FieldHint}): on the label line — or at the field's end edge when\n * there is no label. The rule {@link Select}'s `hint` follows. */\n hint?: ReactNode;\n /** What is wrong (\"That code has expired\"). Rendered under the field, attached with\n * `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Paint the field invalid and set `aria-invalid` without a message of its own. */\n invalid?: boolean;\n /** Classes for the `<input>`; `className` styles the field's wrapper once it has a\n * label, the `<input>` itself when it has none — as on {@link Input}. */\n inputClassName?: string;\n /**\n * Keep the field `readOnly` until it first takes focus.\n *\n * A browser that has a login saved for the site fills it in on load — and on a page\n * where the code field is the only text field near a password field, it can take the\n * code field for the username and write the address into it. A read-only field is\n * never autofilled, and focus is the moment a person (or the platform's code\n * suggestion, which needs a focused field anyway) starts to use it. So the field\n * stays read-only until then, and only until then.\n *\n * It keeps the LOOK of an editable field throughout ({@link FIELD_WRITABLE_LOOK}):\n * this is the one place where read-only does not mean \"you may not type here\", and\n * without the opt-out the field would sit there greyed until it is clicked.\n *\n * {@link TwoFactorSetting} has always done this — its code fields share a page with\n * the current-password field of the disable form — and keeps doing it through this\n * prop. Off by default, because it is a trade: a field that is read-only at the\n * moment focus lands can be announced as read-only by a screen reader, and on some\n * mobile browsers it does not raise the keyboard until a second tap. Turn it on where\n * a saved login actually lands in the field.\n */\n readOnlyUntilFocus?: boolean;\n}\n\n// The error line under a field — the type of `ui.tsx`'s (module-private) one, so this\n// field's message is indistinguishable from an Input's.\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\nconst FIELD_CAPTION_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\";\n\n// Equal-width digits, spaced a little apart, so a code reads — and compares against the\n// authenticator's screen — digit by digit.\nconst CODE_TYPE = \"tabular-nums tracking-widest\";\n\n/** The ASCII digits in `text`, in order. NFKC folds full-width digits (\"1\") to \"1\". */\nfunction digitsOf(text: string): string {\n return text.normalize(\"NFKC\").replace(/\\D/g, \"\");\n}\n\nexport const OneTimeCodeInput = forwardRef<HTMLInputElement, OneTimeCodeInputProps>(\n function OneTimeCodeInput(props, ref) {\n const {\n value,\n onChange,\n length = 6,\n onComplete,\n label,\n hint,\n error,\n invalid: invalidProp,\n className,\n inputClassName,\n readOnlyUntilFocus = false,\n readOnly,\n id,\n placeholder,\n onFocus,\n onPaste,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n ...rest\n } = props;\n // A length below one is no code at all; a fractional one is a typo.\n const size = Math.max(1, Math.floor(length) || 1);\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n const hintId = useId();\n const errorId = useId();\n // Text is a caption under the field; a FieldHint rides the label line. See `hint`.\n const textHint = (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n const hasError = hasMessage(error);\n // Passed at all — even as `undefined` — the field keeps its box, so the `<input>` is\n // never re-parented (and never loses focus) when a message comes or goes. See\n // `FieldGroup` in ui.tsx.\n const reserve = \"error\" in props || \"hint\" in props;\n const describedBy = mergeDescribedBy(ariaDescribedBy, textHint && hintId, hasError && errorId);\n const invalid = Boolean(invalidProp) || hasError || ariaInvalid === true || ariaInvalid === \"true\";\n const labelled = label !== undefined;\n\n // Focused once = in use; the guard is down for good from then on (see the prop).\n const [focusedOnce, setFocusedOnce] = useState(false);\n const guarding = readOnlyUntilFocus && !focusedOnce;\n\n const commit = (next: string) => {\n if (next === value) return;\n onChange(next);\n if (next.length === size) onComplete?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n const input = e.currentTarget;\n const raw = input.value;\n const cleaned = digitsOf(raw);\n // Past the length: a full field refuses the digit (what `maxLength` does), a\n // partial one keeps the first `size` — a paste longer than the room left.\n const refused = cleaned.length > size && value.length >= size;\n const next = refused ? value : cleaned.slice(0, size);\n if (next !== raw) {\n // Write the cleaned text back NOW, with the caret where the typist left it.\n // React would otherwise put its value back after this handler, and setting an\n // input's value moves the caret to the end — so a space typed mid-code threw\n // the caret to the last digit. React finds the DOM already agreeing and leaves\n // it alone.\n const caret = input.selectionStart ?? raw.length;\n const at = refused\n ? Math.max(0, caret - (raw.length - value.length))\n : Math.min(digitsOf(raw.slice(0, caret)).length, next.length);\n input.value = next;\n try {\n input.setSelectionRange(at, at);\n } catch {\n /* not every input type has a selection; a text field does */\n }\n }\n commit(next);\n };\n\n const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {\n onPaste?.(e);\n // A read-only field still receives the event; it must not take the code.\n if (e.defaultPrevented || readOnly || guarding || rest.disabled) return;\n const pasted = digitsOf(e.clipboardData.getData(\"text\"));\n // Anything else — a few digits into a partial code — is an ordinary insertion,\n // and the change handler cleans it.\n if (pasted.length !== size) return;\n e.preventDefault();\n commit(pasted);\n };\n\n const handleFocus = (e: FocusEvent<HTMLInputElement>) => {\n if (guarding) setFocusedOnce(true);\n onFocus?.(e);\n };\n\n const input = (\n <input\n ref={ref}\n id={labelled ? fieldId : id}\n {...rest}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n // The numeric keypad's older cue, for the browsers that predate `inputMode`.\n // The value is digits only, so it can never fail the pattern.\n pattern=\"[0-9]*\"\n // A labelled field's placeholder is a single space: it drives the floating\n // label's `placeholder-shown` trick. Unlabelled, the caller's (\"000000\") shows.\n placeholder={labelled ? \" \" : placeholder}\n value={value}\n readOnly={readOnly || guarding}\n onChange={handleChange}\n onPaste={handlePaste}\n onFocus={handleFocus}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n className={cn(\n labelled ? FLOATING_INPUT_CLASS : FIELD_BASE,\n CODE_TYPE,\n // Read-only for the guard's sake only: keep the editable look. A caller's\n // own `readOnly` keeps FIELD_BASE's settled grey, as everywhere else.\n guarding && !readOnly && FIELD_WRITABLE_LOOK,\n !labelled && className,\n inputClassName,\n invalid && FIELD_INVALID,\n )}\n />\n );\n\n const nonTextHint = hasMessage(hint) && !textHint;\n let field: ReactNode;\n if (labelled) {\n field = (\n <FloatingField className={className} htmlFor={fieldId} label={label} hint={nonTextHint ? hint : undefined}>\n {input}\n </FloatingField>\n );\n } else if (nonTextHint) {\n // No label line to ride: the \"?\" goes at the field's end edge, outside the box —\n // where an unlabelled Select puts it.\n field = (\n <div className=\"flex items-center gap-1.5\">\n <div className=\"min-w-0 flex-1\">{input}</div>\n <span className=\"flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n } else {\n field = input;\n }\n\n if (!textHint && !hasError && !reserve) return field;\n return (\n <div>\n {field}\n {textHint && (\n <p id={hintId} className={FIELD_CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n },\n);\nOneTimeCodeInput.displayName = \"OneTimeCodeInput\";\n"],"mappings":"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","names":["OneTimeCodeInput","input"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import 'react';
|
|
2
|
+
import '../lib/phone.js';
|
|
3
|
+
export { ab as DEFAULT_PHONE_INPUT_LABELS, bq as PhoneInput, br as PhoneInputLabels, bs as PhoneInputProps } from '../feedback-BxeQVzwq.js';
|
|
4
|
+
import '../feedback-attachment-fGAzZPf0.js';
|
|
5
|
+
import '../feedback/feedback-inbox.js';
|
|
6
|
+
import 'lucide-react';
|
|
7
|
+
import './tooltip.js';
|
|
8
|
+
import '../hooks/use-anchored-rect.js';
|
|
9
|
+
import '../lib/clipping.js';
|
|
10
|
+
import '../data-table-labels-B7OdnM0S.js';
|
|
11
|
+
import './data-table-sort.js';
|
|
12
|
+
import './swipeable-row.js';
|
|
13
|
+
import './mini-calendar.js';
|
|
14
|
+
import './calendar-heatmap.js';
|
|
15
|
+
import './popover.js';
|
|
16
|
+
import './chip.js';
|
|
17
|
+
import './field-sync.js';
|
|
18
|
+
import '../wizard/types.js';
|
|
19
|
+
import '../tour/tour.js';
|
|
20
|
+
import '../search/command-palette.js';
|
|
21
|
+
import '../search/global-search.js';
|
|
22
|
+
import '../search/search-index.js';
|
|
23
|
+
import './month-picker.js';
|
|
24
|
+
import './page-contents.js';
|
|
25
|
+
import './series-chart-labels.js';
|
|
26
|
+
import './pie-chart-labels.js';
|
|
27
|
+
import './sparkline.js';
|
|
28
|
+
import './stat-tile.js';
|
|
29
|
+
import './signature-pad.js';
|
|
30
|
+
import './password-strength.js';
|
|
31
|
+
import './swatch-picker.js';
|
|
32
|
+
import './tile-radio.js';
|
|
33
|
+
import './icon-picker.js';
|
|
34
|
+
import './dialog-frame.js';
|
|
35
|
+
import './modal.js';
|
|
36
|
+
import '../hooks/use-search-param-state.js';
|
|
37
|
+
import './measured-grid.js';
|
|
38
|
+
import './account-settings-labels.js';
|
|
39
|
+
import './floating-panel.js';
|
|
40
|
+
import './bulk-action-bar.js';
|
|
41
|
+
import './list.js';
|
|
42
|
+
import './signed-amount.js';
|
|
43
|
+
import './breadcrumbs.js';
|
|
44
|
+
import './toast.js';
|
|
45
|
+
import './description-list.js';
|
|
46
|
+
import './line-items.js';
|
|
47
|
+
import './progress-bar.js';
|
|
48
|
+
import './error-boundary.js';
|
|
49
|
+
import './authed-image.js';
|
|
50
|
+
import '../hooks/use-authed-src.js';
|
|
51
|
+
import './image-grid.js';
|
|
52
|
+
import './lightbox.js';
|
|
53
|
+
import './write-lock.js';
|
|
54
|
+
import './legal.js';
|
|
55
|
+
import './account-chips.js';
|
|
56
|
+
import '../lib/format.js';
|
|
57
|
+
import './reauth-dialog.js';
|
|
58
|
+
import './server-wake.js';
|
|
59
|
+
import '../lib/server-wake.js';
|
|
60
|
+
import './translation-review-labels.js';
|
|
61
|
+
import '../lib/translation-review.js';
|
|
62
|
+
import './country-select.js';
|
|
63
|
+
import './inline-edit-field.js';
|
|
64
|
+
import '../lib/iban.js';
|
|
65
|
+
import './sign-chip.js';
|
|
66
|
+
import './column-mapper.js';
|
|
67
|
+
import '../lib/column-mapping.js';
|
|
68
|
+
import '../lib/table-text.js';
|
|
69
|
+
import './field-parts.js';
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useId, useState } from "react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import {
|
|
7
|
+
PHONE_COUNTRIES,
|
|
8
|
+
PHONE_DIAL_CODES,
|
|
9
|
+
formatNationalPhone,
|
|
10
|
+
formatPhone,
|
|
11
|
+
isE164,
|
|
12
|
+
parsePhone
|
|
13
|
+
} from "../lib/phone.js";
|
|
14
|
+
import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
|
|
15
|
+
import { Input, Select } from "./ui.js";
|
|
16
|
+
const DEFAULT_PHONE_INPUT_LABELS = {
|
|
17
|
+
countryCode: "Country code",
|
|
18
|
+
other: "Other"
|
|
19
|
+
};
|
|
20
|
+
function usePhoneInputLabels(prop) {
|
|
21
|
+
const labels = useKitLabels("phoneInput", DEFAULT_PHONE_INPUT_LABELS, prop);
|
|
22
|
+
return labels;
|
|
23
|
+
}
|
|
24
|
+
const CAPTION_CLASS = "mt-1 text-[11px] leading-tight text-[var(--text-muted)]";
|
|
25
|
+
const ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
26
|
+
const isTextHint = (hint) => typeof hint === "string" && hint !== "" || typeof hint === "number";
|
|
27
|
+
function storedNumber(value) {
|
|
28
|
+
return isE164(value) ? parsePhone(value, "other") : null;
|
|
29
|
+
}
|
|
30
|
+
function countryOf(value) {
|
|
31
|
+
const parsed = storedNumber(value);
|
|
32
|
+
return parsed ? parsed.country ?? "other" : null;
|
|
33
|
+
}
|
|
34
|
+
function emitFor(raw, country) {
|
|
35
|
+
return parsePhone(raw, country)?.e164 ?? raw;
|
|
36
|
+
}
|
|
37
|
+
function displayOf(value, country) {
|
|
38
|
+
const parsed = storedNumber(value);
|
|
39
|
+
if (!parsed) return value;
|
|
40
|
+
if (parsed.country !== null && parsed.country === country) {
|
|
41
|
+
return formatNationalPhone(parsed.national, parsed.country);
|
|
42
|
+
}
|
|
43
|
+
return formatPhone(parsed.e164);
|
|
44
|
+
}
|
|
45
|
+
const PhoneInput = forwardRef(function PhoneInput2({
|
|
46
|
+
value,
|
|
47
|
+
onValueChange,
|
|
48
|
+
onChange,
|
|
49
|
+
onBlur,
|
|
50
|
+
defaultCountry = "CH",
|
|
51
|
+
countries = PHONE_COUNTRIES,
|
|
52
|
+
labels,
|
|
53
|
+
className,
|
|
54
|
+
inputClassName,
|
|
55
|
+
label,
|
|
56
|
+
hint,
|
|
57
|
+
error,
|
|
58
|
+
invalid,
|
|
59
|
+
disabled,
|
|
60
|
+
"aria-describedby": ariaDescribedBy,
|
|
61
|
+
...rest
|
|
62
|
+
}, ref) {
|
|
63
|
+
const text = usePhoneInputLabels(labels);
|
|
64
|
+
const [country, setCountry] = useState(() => countryOf(value) ?? defaultCountry);
|
|
65
|
+
const [draft, setDraft] = useState(null);
|
|
66
|
+
const [emitted, setEmitted] = useState(null);
|
|
67
|
+
const [seen, setSeen] = useState(value);
|
|
68
|
+
if (seen !== value) {
|
|
69
|
+
setSeen(value);
|
|
70
|
+
if (value !== emitted) {
|
|
71
|
+
const own = countryOf(value);
|
|
72
|
+
if (own) setCountry(own);
|
|
73
|
+
setDraft(null);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const emit = (next) => {
|
|
77
|
+
setEmitted(next);
|
|
78
|
+
onValueChange?.(next);
|
|
79
|
+
};
|
|
80
|
+
const handleChange = (e) => {
|
|
81
|
+
onChange?.(e);
|
|
82
|
+
const raw = e.target.value;
|
|
83
|
+
setDraft(raw);
|
|
84
|
+
const parsed = parsePhone(raw, country);
|
|
85
|
+
if (parsed && parsed.country !== country) setCountry(parsed.country ?? "other");
|
|
86
|
+
emit(parsed?.e164 ?? raw);
|
|
87
|
+
};
|
|
88
|
+
const handleCountry = (next) => {
|
|
89
|
+
setCountry(next);
|
|
90
|
+
const stored = storedNumber(value);
|
|
91
|
+
if (stored) {
|
|
92
|
+
if (next === "other" || stored.country === null || stored.country === next) {
|
|
93
|
+
setDraft(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
emit(parsePhone(stored.national, next)?.e164 ?? stored.national);
|
|
97
|
+
setDraft(null);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const source = draft ?? value;
|
|
101
|
+
const parsed = source.trim() === "" ? null : parsePhone(source, next);
|
|
102
|
+
if (parsed && parsed.e164 !== value) {
|
|
103
|
+
setDraft(source);
|
|
104
|
+
emit(parsed.e164);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);
|
|
108
|
+
const options = [...countries];
|
|
109
|
+
if (country !== "other" && !options.includes(country)) options.push(country);
|
|
110
|
+
const captionId = useId();
|
|
111
|
+
const errorId = useId();
|
|
112
|
+
const caption = isTextHint(hint);
|
|
113
|
+
const showError = hasMessage(error);
|
|
114
|
+
const named = typeof label === "string" && label !== "";
|
|
115
|
+
return /* @__PURE__ */ jsxs(
|
|
116
|
+
"div",
|
|
117
|
+
{
|
|
118
|
+
className,
|
|
119
|
+
role: named ? "group" : void 0,
|
|
120
|
+
"aria-label": named ? label : void 0,
|
|
121
|
+
children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-2", children: [
|
|
123
|
+
/* @__PURE__ */ jsxs(
|
|
124
|
+
Select,
|
|
125
|
+
{
|
|
126
|
+
"aria-label": text.countryCode,
|
|
127
|
+
value: country,
|
|
128
|
+
disabled,
|
|
129
|
+
onChange: (e) => handleCountry(e.target.value),
|
|
130
|
+
className: "w-28 shrink-0",
|
|
131
|
+
selectClassName: "h-full tabular-nums",
|
|
132
|
+
children: [
|
|
133
|
+
options.map((code) => /* @__PURE__ */ jsx("option", { value: code, children: `${code} +${PHONE_DIAL_CODES[code]}` }, code)),
|
|
134
|
+
/* @__PURE__ */ jsx("option", { value: "other", children: text.other })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
139
|
+
Input,
|
|
140
|
+
{
|
|
141
|
+
ref,
|
|
142
|
+
...rest,
|
|
143
|
+
type: "tel",
|
|
144
|
+
autoComplete: rest.autoComplete ?? "tel",
|
|
145
|
+
label,
|
|
146
|
+
hint: caption ? void 0 : hint,
|
|
147
|
+
invalid: invalid || showError,
|
|
148
|
+
"aria-describedby": mergeDescribedBy(ariaDescribedBy, caption && captionId, showError && errorId),
|
|
149
|
+
disabled,
|
|
150
|
+
value: display,
|
|
151
|
+
onChange: handleChange,
|
|
152
|
+
onBlur: (e) => {
|
|
153
|
+
setDraft(null);
|
|
154
|
+
onBlur?.(e);
|
|
155
|
+
},
|
|
156
|
+
inputClassName: cn("tabular-nums", inputClassName)
|
|
157
|
+
}
|
|
158
|
+
) })
|
|
159
|
+
] }),
|
|
160
|
+
caption && /* @__PURE__ */ jsx("p", { id: captionId, className: CAPTION_CLASS, children: hint }),
|
|
161
|
+
showError && /* @__PURE__ */ jsx("p", { id: errorId, className: ERROR_CLASS, children: error })
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
PhoneInput.displayName = "PhoneInput";
|
|
167
|
+
export {
|
|
168
|
+
DEFAULT_PHONE_INPUT_LABELS,
|
|
169
|
+
PhoneInput
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=phone-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { forwardRef, useId, useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { Input, Select, type InputProps } from \"./ui\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\nexport const DEFAULT_PHONE_INPUT_LABELS: PhoneInputLabels = {\n countryCode: \"Country code\",\n other: \"Other\",\n};\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** The caption and the error under the row — the type every kit field uses for them\n * (Select's caption, Input's `error`). */\nconst CAPTION_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\";\nconst ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nconst isTextHint = (hint: unknown): hint is string | number =>\n (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: Partial<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(function PhoneInput(\n {\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n },\n ref,\n) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n const captionId = useId();\n const errorId = useId();\n const caption = isTextHint(hint);\n const showError = hasMessage(error);\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n <div className=\"flex items-stretch gap-2\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n // A FieldHint \"?\" rides the label line; text goes under the whole row.\n hint={caption ? undefined : hint}\n invalid={invalid || showError}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, caption && captionId, showError && errorId)}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {caption && (\n <p id={captionId} className={CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {showError && (\n <p id={errorId} className={ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n});\nPhoneInput.displayName = \"PhoneInput\";\n"],"mappings":";AAqPQ,SAWI,KAXJ;AArPR,SAAS,YAAY,OAAO,gBAAgB;AAE5C,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,YAAY,wBAAwB;AAC7C,SAAS,OAAO,cAA+B;AAaxC,MAAM,6BAA+C;AAAA,EAC1D,aAAa;AAAA,EACb,OAAO;AACT;AAKA,SAAS,oBAAoB,MAA+D;AAC1F,QAAM,SAA2B,aAAa,cAAc,4BAA4B,IAAI;AAC5F,SAAO;AACT;AAMA,MAAM,gBAAgB;AACtB,MAAM,cAAc;AAEpB,MAAM,aAAa,CAAC,SACjB,OAAO,SAAS,YAAY,SAAS,MAAO,OAAO,SAAS;AAG/D,SAAS,aAAa,OAAmC;AACvD,SAAO,OAAO,KAAK,IAAI,WAAW,OAAO,OAAO,IAAI;AACtD;AAGA,SAAS,UAAU,OAAoC;AACrD,QAAM,SAAS,aAAa,KAAK;AACjC,SAAO,SAAU,OAAO,WAAW,UAAW;AAChD;AAIA,SAAS,QAAQ,KAAa,SAA+B;AAC3D,SAAO,WAAW,KAAK,OAAO,GAAG,QAAQ;AAC3C;AAOA,SAAS,UAAU,OAAe,SAA+B;AAC/D,QAAM,SAAS,aAAa,KAAK;AACjC,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,OAAO,YAAY,QAAQ,OAAO,YAAY,SAAS;AACzD,WAAO,oBAAoB,OAAO,UAAU,OAAO,OAAO;AAAA,EAC5D;AACA,SAAO,YAAY,OAAO,IAAI;AAChC;AAkEO,MAAM,aAAa,WAA8C,SAASA,YAC/E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB,GAAG;AACL,GACA,KACA;AACA,QAAM,OAAO,oBAAoB,MAAM;AACvC,QAAM,CAAC,SAAS,UAAU,IAAI,SAAuB,MAAM,UAAU,KAAK,KAAK,cAAc;AAI7F,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB,IAAI;AAEtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAwB,IAAI;AAC1D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,MAAI,SAAS,OAAO;AAClB,YAAQ,KAAK;AACb,QAAI,UAAU,SAAS;AAErB,YAAM,MAAM,UAAU,KAAK;AAC3B,UAAI,IAAK,YAAW,GAAG;AACvB,eAAS,IAAI;AAAA,IACf;AAAA,EACF;AAEA,QAAM,OAAO,CAAC,SAAiB;AAC7B,eAAW,IAAI;AACf,oBAAgB,IAAI;AAAA,EACtB;AAEA,QAAM,eAAe,CAAC,MAAqC;AACzD,eAAW,CAAC;AACZ,UAAM,MAAM,EAAE,OAAO;AACrB,aAAS,GAAG;AACZ,UAAM,SAAS,WAAW,KAAK,OAAO;AAEtC,QAAI,UAAU,OAAO,YAAY,QAAS,YAAW,OAAO,WAAW,OAAO;AAC9E,SAAK,QAAQ,QAAQ,GAAG;AAAA,EAC1B;AAEA,QAAM,gBAAgB,CAAC,SAAuB;AAC5C,eAAW,IAAI;AACf,UAAM,SAAS,aAAa,KAAK;AACjC,QAAI,QAAQ;AAGV,UAAI,SAAS,WAAW,OAAO,YAAY,QAAQ,OAAO,YAAY,MAAM;AAC1E,iBAAS,IAAI;AACb;AAAA,MACF;AAGA,WAAK,WAAW,OAAO,UAAU,IAAI,GAAG,QAAQ,OAAO,QAAQ;AAC/D,eAAS,IAAI;AACb;AAAA,IACF;AAEA,UAAM,SAAS,SAAS;AACxB,UAAM,SAAS,OAAO,KAAK,MAAM,KAAK,OAAO,WAAW,QAAQ,IAAI;AACpE,QAAI,UAAU,OAAO,SAAS,OAAO;AACnC,eAAS,MAAM;AACf,WAAK,OAAO,IAAI;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,QAAQ,QAAQ,OAAO,OAAO,MAAM,QAAQ,QAAQ,UAAU,OAAO,OAAO;AAItG,QAAM,UAA8B,CAAC,GAAG,SAAS;AACjD,MAAI,YAAY,WAAW,CAAC,QAAQ,SAAS,OAAO,EAAG,SAAQ,KAAK,OAAO;AAE3E,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,MAAM;AACtB,QAAM,UAAU,WAAW,IAAI;AAC/B,QAAM,YAAY,WAAW,KAAK;AAClC,QAAM,QAAQ,OAAO,UAAU,YAAY,UAAU;AAErD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAM,QAAQ,UAAU;AAAA,MACxB,cAAY,QAAQ,QAAQ;AAAA,MAE5B;AAAA,6BAAC,SAAI,WAAU,4BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,cAAY,KAAK;AAAA,cACjB,OAAO;AAAA,cACP;AAAA,cACA,UAAU,CAAC,MAAM,cAAc,EAAE,OAAO,KAAqB;AAAA,cAC7D,WAAU;AAAA,cAGV,iBAAgB;AAAA,cAEf;AAAA,wBAAQ,IAAI,CAAC,SACZ,oBAAC,YAAkB,OAAO,MACvB,aAAG,IAAI,KAAK,iBAAiB,IAAI,CAAC,MADxB,IAEb,CACD;AAAA,gBACD,oBAAC,YAAO,OAAM,SAAS,eAAK,OAAM;AAAA;AAAA;AAAA,UACpC;AAAA,UAEA,oBAAC,SAAI,WAAU,kBACb;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACC,GAAG;AAAA,cACJ,MAAK;AAAA,cACL,cAAc,KAAK,gBAAgB;AAAA,cACnC;AAAA,cAEA,MAAM,UAAU,SAAY;AAAA,cAC5B,SAAS,WAAW;AAAA,cACpB,oBAAkB,iBAAiB,iBAAiB,WAAW,WAAW,aAAa,OAAO;AAAA,cAC9F;AAAA,cACA,OAAO;AAAA,cACP,UAAU;AAAA,cACV,QAAQ,CAAC,MAAoC;AAC3C,yBAAS,IAAI;AACb,yBAAS,CAAC;AAAA,cACZ;AAAA,cACA,gBAAgB,GAAG,gBAAgB,cAAc;AAAA;AAAA,UACnD,GACF;AAAA,WACF;AAAA,QACC,WACC,oBAAC,OAAE,IAAI,WAAW,WAAW,eAC1B,gBACH;AAAA,QAED,aACC,oBAAC,OAAE,IAAI,SAAS,WAAW,aACxB,iBACH;AAAA;AAAA;AAAA,EAEJ;AAEJ,CAAC;AACD,WAAW,cAAc;","names":["PhoneInput"]}
|
|
@@ -70,8 +70,8 @@ interface ReauthDialogProps {
|
|
|
70
70
|
* `requirePassword` tile. Both are the same three facts — the field is the CURRENT
|
|
71
71
|
* password (the password manager fills it, never generates one), a wrong one stays in
|
|
72
72
|
* the dialog with its error, and the APP verifies it — so the promise handling is
|
|
73
|
-
* `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput`
|
|
74
|
-
*
|
|
73
|
+
* `DangerConfirm`'s own, shared, the field is its `CurrentPasswordInput` with an
|
|
74
|
+
* `error`, and this is the modal form of them.
|
|
75
75
|
*
|
|
76
76
|
* A component rather than a `useReauth()` promise: the step that follows the check
|
|
77
77
|
* (Kurvenschmiede's WebAuthn ceremony) needs what the server answered, and the error
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useContext, useEffect, useId, useRef, useState } from "react";
|
|
4
4
|
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
-
import { usePromisePending } from "./danger-confirm.js";
|
|
5
|
+
import { CurrentPasswordInput, usePromisePending } from "./danger-confirm.js";
|
|
6
6
|
import { DialogFrame } from "./dialog-frame.js";
|
|
7
7
|
import { ModalCloseContext } from "./modal.js";
|
|
8
|
-
import { Button,
|
|
8
|
+
import { Button, Spinner } from "./ui.js";
|
|
9
9
|
const DEFAULT_REAUTH_DIALOG_LABELS = {
|
|
10
10
|
title: "Confirm it\u2019s you",
|
|
11
11
|
description: "Enter your current password to continue.",
|
|
@@ -99,17 +99,15 @@ function ReauthDialog({
|
|
|
99
99
|
submit();
|
|
100
100
|
},
|
|
101
101
|
children: /* @__PURE__ */ jsx(
|
|
102
|
-
|
|
102
|
+
CurrentPasswordInput,
|
|
103
103
|
{
|
|
104
104
|
ref: inputRef,
|
|
105
|
-
type: "password",
|
|
106
|
-
autoComplete: "current-password",
|
|
107
105
|
label: labels.password,
|
|
108
106
|
value: password,
|
|
109
|
-
|
|
107
|
+
busy,
|
|
110
108
|
error: shownError,
|
|
111
|
-
|
|
112
|
-
setPassword(
|
|
109
|
+
onValueChange: (next) => {
|
|
110
|
+
setPassword(next);
|
|
113
111
|
setEdited(true);
|
|
114
112
|
}
|
|
115
113
|
}
|