@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
package/src/index.ts
CHANGED
|
@@ -111,8 +111,27 @@ export * from "./components/disclosure";
|
|
|
111
111
|
export * from "./components/dialog-frame";
|
|
112
112
|
// Named, not `*`: danger-confirm also holds the password field and pending hook that
|
|
113
113
|
// ReauthDialog shares with it, which are kit-internal.
|
|
114
|
-
export {
|
|
115
|
-
|
|
114
|
+
export {
|
|
115
|
+
DangerConfirm,
|
|
116
|
+
DEFAULT_DANGER_CONFIRM_LABELS,
|
|
117
|
+
typedMatches,
|
|
118
|
+
// 0.22.0: the typed-confirmation field on its own (keksdose K7) and the current-password
|
|
119
|
+
// field kastlan wants for sign-in and re-authentication (kastlan 1).
|
|
120
|
+
TypedConfirmField,
|
|
121
|
+
CurrentPasswordInput,
|
|
122
|
+
} from "./components/danger-confirm";
|
|
123
|
+
export type {
|
|
124
|
+
DangerConfirmLabels,
|
|
125
|
+
DangerConfirmProps,
|
|
126
|
+
TypedMatch,
|
|
127
|
+
DangerConsequence,
|
|
128
|
+
TypedConfirmFieldProps,
|
|
129
|
+
TypedConfirmFieldLabels,
|
|
130
|
+
CurrentPasswordInputProps,
|
|
131
|
+
CurrentPasswordInputLabels,
|
|
132
|
+
// 0.23.0: onConfirm's second argument — the guards' values (keksdose G4b).
|
|
133
|
+
DangerConfirmValues,
|
|
134
|
+
} from "./components/danger-confirm";
|
|
116
135
|
// 0.18: re-authentication before a sensitive action (Kurvenschmiede 4).
|
|
117
136
|
export { ReauthDialog, DEFAULT_REAUTH_DIALOG_LABELS } from "./components/reauth-dialog";
|
|
118
137
|
export type { ReauthDialogProps, ReauthDialogLabels } from "./components/reauth-dialog";
|
|
@@ -259,8 +278,9 @@ export * from "./components/data-table-pagination";
|
|
|
259
278
|
export { FilterPopover } from "./components/data-table-filter-popover";
|
|
260
279
|
// data-table re-exports SortState internally; export the rest explicitly to
|
|
261
280
|
// avoid a duplicate SortState star-export (it comes from data-table-sort).
|
|
262
|
-
export { DataTable } from "./components/data-table";
|
|
281
|
+
export { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from "./components/data-table";
|
|
263
282
|
export type {
|
|
283
|
+
DataTableSortLabels,
|
|
264
284
|
DataTableColumn,
|
|
265
285
|
DataTableCellProps,
|
|
266
286
|
DataTableHeadProps,
|
|
@@ -406,3 +426,51 @@ export type {
|
|
|
406
426
|
// The legal pages' shell (0.19, H10): Imprint, Privacy Policy, Terms in every app.
|
|
407
427
|
export { LegalLayout, LegalSection, LegalLinks, DEFAULT_LEGAL_LABELS } from "./components/legal";
|
|
408
428
|
export type { LegalLabels, LegalLink, LegalLinksProps, LegalSectionProps, LegalLayoutProps } from "./components/legal";
|
|
429
|
+
|
|
430
|
+
// ── 0.22.0: the inputs round (kastlan's, keksdose's and Kurvenschmiede's audits) ──
|
|
431
|
+
export * from "./components/checkbox-group";
|
|
432
|
+
export * from "./components/one-time-code-input";
|
|
433
|
+
export * from "./components/language-select";
|
|
434
|
+
export * from "./components/tile-radio";
|
|
435
|
+
export * from "./components/inline-edit-field";
|
|
436
|
+
export * from "./components/sign-chip";
|
|
437
|
+
export * from "./components/country-select";
|
|
438
|
+
export { COUNTRY_CODES, countryName } from "./lib/countries";
|
|
439
|
+
export * from "./components/iban-input";
|
|
440
|
+
export {
|
|
441
|
+
IBAN_LENGTHS,
|
|
442
|
+
compactIban,
|
|
443
|
+
formatIban,
|
|
444
|
+
ibanCheckDigits,
|
|
445
|
+
ibanProblem,
|
|
446
|
+
isValidIban,
|
|
447
|
+
isQrIban,
|
|
448
|
+
} from "./lib/iban";
|
|
449
|
+
export type { IbanKind, IbanProblem } from "./lib/iban";
|
|
450
|
+
export { formatIsin, isinCheckDigit, isValidIsin } from "./lib/isin";
|
|
451
|
+
export * from "./components/phone-input";
|
|
452
|
+
export {
|
|
453
|
+
PHONE_DIAL_CODES,
|
|
454
|
+
PHONE_COUNTRIES,
|
|
455
|
+
parsePhone,
|
|
456
|
+
formatNationalPhone,
|
|
457
|
+
formatPhone,
|
|
458
|
+
isE164,
|
|
459
|
+
} from "./lib/phone";
|
|
460
|
+
export type { PhoneCountryCode, PhoneCountry, ParsedPhone } from "./lib/phone";
|
|
461
|
+
|
|
462
|
+
// ── 0.23.0: the apps' 0.22 adoption round (kastlan, keksdose, Kurvenschmiede) ──
|
|
463
|
+
// The label strip over custom content (keksdose G8).
|
|
464
|
+
export * from "./components/field-strip";
|
|
465
|
+
// Paste or drop a table, give each column a role (Kurvenschmiede's columns input,
|
|
466
|
+
// keksdose's import map step). The lexer itself stays in `@eifi1/ui-kit/table-text`.
|
|
467
|
+
export * from "./components/column-mapper";
|
|
468
|
+
export {
|
|
469
|
+
assignColumnRole,
|
|
470
|
+
guessMapping,
|
|
471
|
+
missingRoles,
|
|
472
|
+
readMappedTable,
|
|
473
|
+
readTextFile,
|
|
474
|
+
roleOfColumn,
|
|
475
|
+
} from "./lib/column-mapping";
|
|
476
|
+
export type { ColumnMapperResult, ColumnMapping, ColumnRole } from "./lib/column-mapping";
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which column of a table plays which part — the headless half of {@link ColumnMapper}.
|
|
3
|
+
*
|
|
4
|
+
* Two apps built this by hand, the same way. Kurvenschmiede's columns input
|
|
5
|
+
* (`features/control/columns-input.tsx`) reads a recorder's export and asks which
|
|
6
|
+
* column is the time, the setpoint and the actual; keksdose's bank-file import
|
|
7
|
+
* (`import-map-step.tsx`, `use-file-import.ts`) shows a bank's CSV and asks which
|
|
8
|
+
* column is the booking date, the amount, the payee. Both then have the same four
|
|
9
|
+
* rules, and keksdose wrote them down after getting each one wrong once:
|
|
10
|
+
*
|
|
11
|
+
* * **A role sits on one column at most, and a column carries one role.** Picking a
|
|
12
|
+
* role for a column MOVES it there from wherever it was ({@link assignColumnRole});
|
|
13
|
+
* the column it left falls back to "ignore" in the same step. There is no error
|
|
14
|
+
* state, because there is nothing wrong to report.
|
|
15
|
+
* * **What is still missing is named**, not left to a disabled button
|
|
16
|
+
* ({@link missingRoles}). A required role can be one of a group: a bank export that
|
|
17
|
+
* splits debit and credit has no amount column and needs none, so `required` may
|
|
18
|
+
* name a group, satisfied by any one of its members (keksdose's "money").
|
|
19
|
+
* * **A mapping is indexes into ONE table.** When the column count changes, the
|
|
20
|
+
* indexes mean nothing any more and are guessed afresh; when it does not (the
|
|
21
|
+
* header line toggled), they still point where they did — keksdose's `keepRoles`.
|
|
22
|
+
* * **The rows a role cannot read are reported by line**, not dropped in silence
|
|
23
|
+
* ({@link readMappedTable}): a footer under a recording, a total under a statement.
|
|
24
|
+
*
|
|
25
|
+
* Pure functions over plain data, so a two-step wizard can hold the mapping in its own
|
|
26
|
+
* state between steps, and a table the server sniffed (keksdose's) can be mapped with
|
|
27
|
+
* the same rules as one parsed here.
|
|
28
|
+
*/
|
|
29
|
+
import { tableNumber } from "./table-text";
|
|
30
|
+
import type { TableLine, TextTable } from "./table-text";
|
|
31
|
+
|
|
32
|
+
/** One part a column can play, defined by the caller. */
|
|
33
|
+
export interface ColumnRole<R extends string = string> {
|
|
34
|
+
/** The key the mapping is stored under. */
|
|
35
|
+
value: R;
|
|
36
|
+
/** What the role select shows — translated by the caller. */
|
|
37
|
+
label: string;
|
|
38
|
+
/**
|
|
39
|
+
* `true`: the table cannot be used until a column plays this role. A string names a
|
|
40
|
+
* GROUP: the table needs any one of the roles that share it — keksdose's amount,
|
|
41
|
+
* debit and credit, any of which carries the money.
|
|
42
|
+
*/
|
|
43
|
+
required?: boolean | string;
|
|
44
|
+
/**
|
|
45
|
+
* Every cell this role reads must be a number in the table's decimal convention
|
|
46
|
+
* ({@link tableNumber}). A row where one is not — a footer, a units line, a total
|
|
47
|
+
* with a grouping mark — is moved to `unread` with its line number instead of being
|
|
48
|
+
* handed over. Kurvenschmiede's time, setpoint and actual. The cells stay strings:
|
|
49
|
+
* converting is the caller's, with `tableNumber(cell, result.decimalComma)`.
|
|
50
|
+
*/
|
|
51
|
+
numeric?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Role → column index. A role that is absent or `null` sits on no column — so a
|
|
55
|
+
* mapping with nullable fields (keksdose's `CsvMapping`) is one as it stands. */
|
|
56
|
+
export type ColumnMapping<R extends string = string> = Partial<Record<R, number | null>>;
|
|
57
|
+
|
|
58
|
+
/** What the table is, read through a mapping. */
|
|
59
|
+
export interface ColumnMapperResult<R extends string = string> extends TextTable {
|
|
60
|
+
mapping: ColumnMapping<R>;
|
|
61
|
+
/** What the table still needs before it can be used: one entry per gap, each the
|
|
62
|
+
* roles any of which would fill it (one role, or a required group). */
|
|
63
|
+
missing: R[][];
|
|
64
|
+
/** Every required role placed, and at least one row read. */
|
|
65
|
+
complete: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const placedAt = <R extends string>(mapping: ColumnMapping<R>, role: R, width?: number): number | null => {
|
|
69
|
+
const column = mapping[role];
|
|
70
|
+
if (typeof column !== "number") return null;
|
|
71
|
+
return width === undefined || column < width ? column : null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** The role on this column, or `null` for an ignored one. */
|
|
75
|
+
export function roleOfColumn<R extends string>(
|
|
76
|
+
roles: readonly ColumnRole<R>[],
|
|
77
|
+
mapping: ColumnMapping<R>,
|
|
78
|
+
column: number,
|
|
79
|
+
): R | null {
|
|
80
|
+
return roles.find((role) => mapping[role.value] === column)?.value ?? null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Put `role` on `column` — or, with `null`, take the column's role off it.
|
|
85
|
+
*
|
|
86
|
+
* Whatever this column played before stops, and the role moves here from any other
|
|
87
|
+
* column, in one new mapping (keksdose's `assignColumn`: "both halves are patches to
|
|
88
|
+
* the same object"). Other fields of the mapping are kept as they are.
|
|
89
|
+
*/
|
|
90
|
+
export function assignColumnRole<R extends string, M extends ColumnMapping<R>>(
|
|
91
|
+
roles: readonly ColumnRole<R>[],
|
|
92
|
+
mapping: M,
|
|
93
|
+
column: number,
|
|
94
|
+
role: R | null,
|
|
95
|
+
): M {
|
|
96
|
+
const next = { ...mapping } as Record<string, unknown>;
|
|
97
|
+
for (const { value } of roles) {
|
|
98
|
+
if (mapping[value] === column) next[value] = null;
|
|
99
|
+
}
|
|
100
|
+
if (role !== null) next[role] = column;
|
|
101
|
+
return next as M;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The gaps: each required role with no column, and each required group none of whose
|
|
106
|
+
* roles has one. In the order of `roles`. A column index past `width` is no column.
|
|
107
|
+
*/
|
|
108
|
+
export function missingRoles<R extends string>(
|
|
109
|
+
roles: readonly ColumnRole<R>[],
|
|
110
|
+
mapping: ColumnMapping<R>,
|
|
111
|
+
width?: number,
|
|
112
|
+
): R[][] {
|
|
113
|
+
const gaps: R[][] = [];
|
|
114
|
+
const groups = new Set<string>();
|
|
115
|
+
for (const role of roles) {
|
|
116
|
+
if (role.required === true) {
|
|
117
|
+
if (placedAt(mapping, role.value, width) === null) gaps.push([role.value]);
|
|
118
|
+
} else if (typeof role.required === "string" && !groups.has(role.required)) {
|
|
119
|
+
groups.add(role.required);
|
|
120
|
+
const members = roles.filter((other) => other.required === role.required);
|
|
121
|
+
if (members.every((member) => placedAt(mapping, member.value, width) === null)) {
|
|
122
|
+
gaps.push(members.map((member) => member.value));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return gaps;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const normalised = (text: string) => text.trim().toLowerCase();
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A first mapping for a table nobody has mapped yet.
|
|
133
|
+
*
|
|
134
|
+
* A column whose header IS a role's label or key gets that role. The required roles
|
|
135
|
+
* left over then take the free columns in order — Kurvenschmiede's "time, setpoint,
|
|
136
|
+
* actual are the first three columns", which is how a recorder writes them. Optional
|
|
137
|
+
* roles are not guessed by position: in a bank
|
|
138
|
+
* export of twelve columns, "the fourth role is the fourth column" is a wrong answer
|
|
139
|
+
* presented as a suggestion, and an "ignore" is the honest one.
|
|
140
|
+
*/
|
|
141
|
+
export function guessMapping<R extends string>(
|
|
142
|
+
roles: readonly ColumnRole<R>[],
|
|
143
|
+
header: readonly string[] | null,
|
|
144
|
+
width: number,
|
|
145
|
+
): ColumnMapping<R> {
|
|
146
|
+
const mapping: ColumnMapping<R> = {};
|
|
147
|
+
const taken = new Set<number>();
|
|
148
|
+
if (header) {
|
|
149
|
+
for (const role of roles) {
|
|
150
|
+
const names = [normalised(role.label), normalised(role.value)];
|
|
151
|
+
const column = header.findIndex((name, index) => !taken.has(index) && names.includes(normalised(name)));
|
|
152
|
+
if (column >= 0) {
|
|
153
|
+
mapping[role.value] = column;
|
|
154
|
+
taken.add(column);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const free = Array.from({ length: width }, (_, index) => index).filter((index) => !taken.has(index));
|
|
159
|
+
for (const role of roles) {
|
|
160
|
+
if (role.required !== true || typeof mapping[role.value] === "number") continue;
|
|
161
|
+
const column = free.shift();
|
|
162
|
+
if (column === undefined) break;
|
|
163
|
+
mapping[role.value] = column;
|
|
164
|
+
}
|
|
165
|
+
return mapping;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The table read through the mapping: the rows every `numeric` role can read, the
|
|
170
|
+
* others moved to `unread` (in line order, with the parse's own), what is missing,
|
|
171
|
+
* and whether the table can be used. Column indexes past the table's width count as
|
|
172
|
+
* unplaced.
|
|
173
|
+
*/
|
|
174
|
+
export function readMappedTable<R extends string>(
|
|
175
|
+
table: TextTable,
|
|
176
|
+
roles: readonly ColumnRole<R>[],
|
|
177
|
+
mapping: ColumnMapping<R>,
|
|
178
|
+
): ColumnMapperResult<R> {
|
|
179
|
+
const numeric = roles
|
|
180
|
+
.filter((role) => role.numeric)
|
|
181
|
+
.map((role) => placedAt(mapping, role.value, table.width))
|
|
182
|
+
.filter((column): column is number => column !== null);
|
|
183
|
+
const rows: string[][] = [];
|
|
184
|
+
const lines: TableLine[] = [];
|
|
185
|
+
const refused: TableLine[] = [];
|
|
186
|
+
table.rows.forEach((row, index) => {
|
|
187
|
+
if (numeric.every((column) => Number.isFinite(tableNumber(row[column] ?? "", table.decimalComma)))) {
|
|
188
|
+
rows.push(row);
|
|
189
|
+
lines.push(table.lines[index]);
|
|
190
|
+
} else {
|
|
191
|
+
refused.push(table.lines[index]);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const unread = refused.length ? [...table.unread, ...refused].sort((a, b) => a.line - b.line) : table.unread;
|
|
195
|
+
const missing = missingRoles(roles, mapping, table.width);
|
|
196
|
+
return {
|
|
197
|
+
...table,
|
|
198
|
+
rows,
|
|
199
|
+
lines,
|
|
200
|
+
unread,
|
|
201
|
+
mapping,
|
|
202
|
+
missing,
|
|
203
|
+
complete: missing.length === 0 && rows.length > 0,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A dropped or chosen file's text — UTF-8 when it is UTF-8, else Windows-1252.
|
|
209
|
+
*
|
|
210
|
+
* A German bank or bench export is very often written in Windows-1252, and reading it
|
|
211
|
+
* as UTF-8 turns every "ä" in a column name into "�" — no error, just a header nobody
|
|
212
|
+
* recognises. So the bytes are tried as strict UTF-8 first and read as Windows-1252
|
|
213
|
+
* when that fails (keksdose's server sniffs the encoding for the same reason; a file
|
|
214
|
+
* read in the browser had nothing doing it). Falls back to `FileReader` where a `File`
|
|
215
|
+
* has no `arrayBuffer` (Kurvenschmiede's `readText`).
|
|
216
|
+
*/
|
|
217
|
+
export async function readTextFile(file: Blob): Promise<string> {
|
|
218
|
+
const bytes = await bytesOf(file);
|
|
219
|
+
try {
|
|
220
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
221
|
+
} catch {
|
|
222
|
+
return new TextDecoder("windows-1252").decode(bytes);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function bytesOf(file: Blob): Promise<ArrayBuffer> {
|
|
227
|
+
if (typeof file.arrayBuffer === "function") return file.arrayBuffer();
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const reader = new FileReader();
|
|
230
|
+
reader.onload = () => resolve(reader.result as ArrayBuffer);
|
|
231
|
+
reader.onerror = () => reject(reader.error ?? new Error("read failed"));
|
|
232
|
+
reader.readAsArrayBuffer(file);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Countries as ISO 3166-1 alpha-2 codes, named by the runtime — for {@link CountrySelect}
|
|
3
|
+
* and for an app that shows the code it stored ("CH") as a name somewhere else (a
|
|
4
|
+
* table cell, an address line).
|
|
5
|
+
*
|
|
6
|
+
* The kit ships CODES, not names. Every runtime the kit supports has
|
|
7
|
+
* `Intl.DisplayNames({ type: "region" })`, which names all of these in every locale
|
|
8
|
+
* the browser knows — so a table of 249 names per language would be a large file
|
|
9
|
+
* that is out of date the day a country is renamed (Türkiye, Czechia, Eswatini) and
|
|
10
|
+
* still covers fewer languages than the browser does. What is left to ship is the
|
|
11
|
+
* list of which codes ARE countries, and that is one string.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Every officially assigned ISO 3166-1 alpha-2 code — the 249 countries, territories
|
|
16
|
+
* and areas of the standard, upper-case, in code order.
|
|
17
|
+
*
|
|
18
|
+
* User-assigned codes are deliberately not in it: `XK` (Kosovo) is a widespread
|
|
19
|
+
* convention, not ISO, and whether to offer it is the app's decision. An app that
|
|
20
|
+
* wants it passes its own list to `CountrySelect`'s `countries` — the runtime names
|
|
21
|
+
* `XK` as well.
|
|
22
|
+
*/
|
|
23
|
+
export const COUNTRY_CODES: readonly string[] = Object.freeze(
|
|
24
|
+
(
|
|
25
|
+
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ " +
|
|
26
|
+
"BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM " +
|
|
27
|
+
"DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS " +
|
|
28
|
+
"GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN " +
|
|
29
|
+
"KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ " +
|
|
30
|
+
"MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM " +
|
|
31
|
+
"PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV " +
|
|
32
|
+
"SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI " +
|
|
33
|
+
"VN VU WF WS YE YT ZA ZM ZW"
|
|
34
|
+
).split(" "),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** Two ASCII letters — the shape of an alpha-2 code, whatever the case. */
|
|
38
|
+
const ALPHA_2 = /^[A-Za-z]{2}$/;
|
|
39
|
+
|
|
40
|
+
/** `code` upper-cased when it has the shape of an alpha-2 code, else `undefined`. Does
|
|
41
|
+
* not check that ISO assigned it — see {@link COUNTRY_CODES} for why `XK` must pass. */
|
|
42
|
+
export function normalizeCountryCode(code: string | null | undefined): string | undefined {
|
|
43
|
+
return code && ALPHA_2.test(code) ? code.toUpperCase() : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// One `DisplayNames` per locale, built on first use. `null` records a locale the
|
|
47
|
+
// runtime refused (a malformed tag) or a runtime without the API, so neither is
|
|
48
|
+
// retried for every row of a 249-row list.
|
|
49
|
+
const namers = new Map<string, Intl.DisplayNames | null>();
|
|
50
|
+
|
|
51
|
+
function namer(locale: string): Intl.DisplayNames | null {
|
|
52
|
+
let n = namers.get(locale);
|
|
53
|
+
if (n === undefined) {
|
|
54
|
+
try {
|
|
55
|
+
n = new Intl.DisplayNames([locale], { type: "region", fallback: "code" });
|
|
56
|
+
} catch {
|
|
57
|
+
n = null;
|
|
58
|
+
}
|
|
59
|
+
namers.set(locale, n);
|
|
60
|
+
}
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The name of the country `code` stands for, in `locale` — `countryName("CH", "de-CH")`
|
|
66
|
+
* is "Schweiz". Falls back to the upper-cased code itself when the runtime has no name
|
|
67
|
+
* for it, rejects the locale, or has no `Intl.DisplayNames`: a code is still the right
|
|
68
|
+
* answer, where an empty cell would not be.
|
|
69
|
+
*
|
|
70
|
+
* `locale` defaults to English rather than to the runtime's language, the rule the
|
|
71
|
+
* kit's currency names follow (`currencyName`): an app that never set a locale reads
|
|
72
|
+
* the kit's English everywhere else, and a country list in the browser's language
|
|
73
|
+
* beside English labels — or in the test runner's — is two languages on one form.
|
|
74
|
+
*
|
|
75
|
+
* The code is upper-cased first. `DisplayNames` is case-sensitive about it in some
|
|
76
|
+
* runtimes (Node answers "ch" with "ch"), and the codes reach this from forms,
|
|
77
|
+
* URLs and databases in either case.
|
|
78
|
+
*/
|
|
79
|
+
export function countryName(code: string, locale: string = "en"): string {
|
|
80
|
+
const upper = code.toUpperCase();
|
|
81
|
+
const n = namer(locale);
|
|
82
|
+
if (n) {
|
|
83
|
+
try {
|
|
84
|
+
const name = n.of(upper);
|
|
85
|
+
if (name) return name;
|
|
86
|
+
} catch {
|
|
87
|
+
// Not a well-formed region subtag ("C1", "") — the code is all there is.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return upper;
|
|
91
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale's decimal mark and the reading of typed grouping marks, shared by the
|
|
3
|
+
* kit's numeric text fields: {@link AmountInput}, {@link NumberInput} and, through
|
|
4
|
+
* NumberInput, {@link NumberField} and {@link MoneyField}.
|
|
5
|
+
*
|
|
6
|
+
* Package-private — not re-exported from the barrel. It lives here rather than in
|
|
7
|
+
* `calc.ts` because that module is re-exported wholesale (`export * from
|
|
8
|
+
* "./lib/calc"`), and these are the fields' plumbing, not API.
|
|
9
|
+
*
|
|
10
|
+
* WHY ONE MODULE. AmountInput learned the locale's mark (kastlan 40) and the grouping
|
|
11
|
+
* parse (keksdose G1); NumberInput learned neither, so the same "1.234,56" a German
|
|
12
|
+
* user typed into a money field read as 1234.56 and, typed into the budget-goal field
|
|
13
|
+
* next to it, as 1.23456 (keksdose K5). Two private copies of the same rule are how
|
|
14
|
+
* that happened, so the rule now has one home.
|
|
15
|
+
*/
|
|
16
|
+
import { sanitizeLive } from "./calc";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The locale's decimal mark, as far as a field can type it: "," or "." (kastlan 40).
|
|
20
|
+
* A mark outside those two (Arabic's "٫") is not one {@link sanitizeLive} lets
|
|
21
|
+
* through, so those locales keep the dot rather than get a field that eats the key it
|
|
22
|
+
* shows. `undefined` is the runtime's own locale, which is what `Intl` reads it as.
|
|
23
|
+
*/
|
|
24
|
+
export function decimalMark(locale: string | undefined): "," | "." {
|
|
25
|
+
try {
|
|
26
|
+
const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === "decimal");
|
|
27
|
+
return part?.value === "," ? "," : ".";
|
|
28
|
+
} catch {
|
|
29
|
+
// An invalid tag throws a RangeError; a typo in a locale must not take the form down.
|
|
30
|
+
return ".";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Which typed mark is the decimal one (keksdose G1). {@link sanitizeLive} folds every
|
|
36
|
+
* "," into ".", so on its own it read the German "1.234,56" as 1.23456 — and a money
|
|
37
|
+
* field's settle then made that 1.23, final. This decides per operand, from the
|
|
38
|
+
* typist's own text, before that fold:
|
|
39
|
+
*
|
|
40
|
+
* - both marks present: the one that is not the locale's is grouping, and goes;
|
|
41
|
+
* - in a "," locale, only "." present: grouping when it cannot be a decimal as a
|
|
42
|
+
* German reader writes it — twice ("1.234.567"), or in thousands shape ("1.234",
|
|
43
|
+
* "12.500") — otherwise a decimal ("1.5", typed by a dot-decimal habit);
|
|
44
|
+
* - in a "." locale, only "," present: grouping when it appears twice ("1,234,567");
|
|
45
|
+
* once it stays a decimal, because Swiss and German typists write "1,5" in a
|
|
46
|
+
* de-CH form (kastlan 40) and a unit price "1,789" is a decimal.
|
|
47
|
+
*
|
|
48
|
+
* Only a keystroke's text comes through here. A calculator result is dot-decimal
|
|
49
|
+
* already, and "1.234" from `100/81.03…` is never a thousand.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeTypedMarks(raw: string, mark: "," | "."): string {
|
|
52
|
+
const other = mark === "," ? "." : ",";
|
|
53
|
+
return raw.replace(/[0-9.,]+/g, (operand) => {
|
|
54
|
+
const hasMark = operand.includes(mark);
|
|
55
|
+
const count = operand.split(other).length - 1;
|
|
56
|
+
if (count === 0) return operand;
|
|
57
|
+
const grouping = hasMark
|
|
58
|
+
? true
|
|
59
|
+
: count > 1 || (mark === "," && /^\d{1,3}(\.\d{3})+$/.test(operand));
|
|
60
|
+
return grouping ? operand.split(other).join("") : operand;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a keystroke's raw text reports: grouping read off, then the field's live
|
|
65
|
+
* sanitising (comma → dot, stray characters out, one decimal point per operand). */
|
|
66
|
+
export function readTyped(raw: string, mark: "," | "."): string {
|
|
67
|
+
return sanitizeLive(normalizeTypedMarks(raw, mark));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Spacing a typist uses to group digits — a space, the no-break and narrow no-break
|
|
71
|
+
// spaces `Intl` itself writes for fr-CH ("1 234,5" with U+202F), and the Swiss
|
|
72
|
+
// apostrophes ("1'234.50"). The live sanitising drops them, so they never reach the
|
|
73
|
+
// value; the draft may still show them.
|
|
74
|
+
const GROUP_SPACING = /[\s'’\u00a0\u202f]/g;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether the field may keep SHOWING `raw` — the typist's own text — while it holds
|
|
78
|
+
* `canonical` (dot-decimal, as `onChange` reported it).
|
|
79
|
+
*
|
|
80
|
+
* Showing the typed text rather than re-spelling the value is what keeps the caret
|
|
81
|
+
* where the typist put it, and what keeps a grouping "." from being turned into a ","
|
|
82
|
+
* under their fingers — after which no rule could tell the two apart (keksdose G1).
|
|
83
|
+
* But only text that says the same as the value, and says it with nothing the field
|
|
84
|
+
* would have DROPPED: a letter, or a second decimal point in one operand ("1.2.3"),
|
|
85
|
+
* must not stay on screen while the value says "1.23" (Keksdose live #201 is the bug
|
|
86
|
+
* that showing it would bring back). Grouping marks and grouping spaces may stay.
|
|
87
|
+
*/
|
|
88
|
+
export function showsTyped(raw: string, mark: "," | ".", canonical: string): boolean {
|
|
89
|
+
const read = normalizeTypedMarks(raw, mark).replace(GROUP_SPACING, "");
|
|
90
|
+
const sanitized = sanitizeLive(read);
|
|
91
|
+
// Nothing dropped: the read text is the sanitised one, but for which mark it uses.
|
|
92
|
+
return sanitized === read.replace(/,/g, ".") && sanitized === sanitizeLive(canonical);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A dot-decimal text spelt in `mark` — display only, one character for one, so a
|
|
96
|
+
* re-spelling never moves a caret. */
|
|
97
|
+
export function localizeMark(text: string, mark: "," | "."): string {
|
|
98
|
+
return mark === "," ? text.replace(/\./g, ",") : text;
|
|
99
|
+
}
|