@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,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { IBAN_LENGTHS, compactIban, formatIban, ibanProblem } from "../lib/iban.js";
|
|
7
|
+
import { assignRef, hasMessage } from "./choice-parts.js";
|
|
8
|
+
import { Input } from "./ui.js";
|
|
9
|
+
const DEFAULT_IBAN_INPUT_LABELS = {
|
|
10
|
+
format: "An IBAN starts with a two-letter country code and two check digits.",
|
|
11
|
+
country: (code) => `\u201C${code}\u201D is not the country code of an IBAN.`,
|
|
12
|
+
length: (actual, expected) => `An IBAN from this country has ${expected} characters \u2014 this one has ${actual}.`,
|
|
13
|
+
checksum: "The check digits don\u2019t match \u2014 a character is probably mistyped.",
|
|
14
|
+
qrRequired: "This is a regular IBAN. A QR-bill needs the account\u2019s QR-IBAN.",
|
|
15
|
+
qrNotAllowed: "This is a QR-IBAN, which only receives QR-bill payments. Enter the account\u2019s regular IBAN."
|
|
16
|
+
};
|
|
17
|
+
function useIbanInputLabels(prop) {
|
|
18
|
+
const labels = useKitLabels("ibanInput", DEFAULT_IBAN_INPUT_LABELS, prop);
|
|
19
|
+
return labels;
|
|
20
|
+
}
|
|
21
|
+
const alnumCount = (text) => text.replace(/[^0-9A-Za-z]/g, "").length;
|
|
22
|
+
function indexAfter(text, count) {
|
|
23
|
+
if (count <= 0) return 0;
|
|
24
|
+
let seen = 0;
|
|
25
|
+
for (let i = 0; i < text.length; i++) {
|
|
26
|
+
if (/[0-9A-Za-z]/.test(text[i])) seen += 1;
|
|
27
|
+
if (seen === count) return i + 1;
|
|
28
|
+
}
|
|
29
|
+
return text.length;
|
|
30
|
+
}
|
|
31
|
+
function visibleProblem(compact, kind, focused) {
|
|
32
|
+
if (compact === "") return null;
|
|
33
|
+
const problem = ibanProblem(compact, kind);
|
|
34
|
+
if (problem === null || !focused) return problem;
|
|
35
|
+
if (compact.length < 4) return null;
|
|
36
|
+
if (problem === "format" || problem === "country") return problem;
|
|
37
|
+
return compact.length >= IBAN_LENGTHS[compact.slice(0, 2)] ? problem : null;
|
|
38
|
+
}
|
|
39
|
+
function messageFor(problem, compact, labels) {
|
|
40
|
+
switch (problem) {
|
|
41
|
+
case "format":
|
|
42
|
+
return labels.format;
|
|
43
|
+
case "country":
|
|
44
|
+
return labels.country(compact.slice(0, 2));
|
|
45
|
+
case "length":
|
|
46
|
+
return labels.length(compact.length, IBAN_LENGTHS[compact.slice(0, 2)]);
|
|
47
|
+
case "checksum":
|
|
48
|
+
return labels.checksum;
|
|
49
|
+
case "qrRequired":
|
|
50
|
+
return labels.qrRequired;
|
|
51
|
+
case "qrNotAllowed":
|
|
52
|
+
return labels.qrNotAllowed;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const IbanInput = forwardRef(function IbanInput2({ value, onValueChange, onChange, onFocus, onBlur, kind = "any", labels, error, inputClassName, ...rest }, ref) {
|
|
56
|
+
const text = useIbanInputLabels(labels);
|
|
57
|
+
const compact = compactIban(value);
|
|
58
|
+
const shown = formatIban(compact);
|
|
59
|
+
const [focused, setFocused] = useState(false);
|
|
60
|
+
const innerRef = useRef(null);
|
|
61
|
+
const caretRef = useRef(null);
|
|
62
|
+
const setRefs = useCallback(
|
|
63
|
+
(el) => {
|
|
64
|
+
innerRef.current = el;
|
|
65
|
+
assignRef(ref, el);
|
|
66
|
+
},
|
|
67
|
+
[ref]
|
|
68
|
+
);
|
|
69
|
+
useLayoutEffect(() => {
|
|
70
|
+
const el = innerRef.current;
|
|
71
|
+
const count = caretRef.current;
|
|
72
|
+
if (el === null || count === null) return;
|
|
73
|
+
caretRef.current = null;
|
|
74
|
+
if (el.ownerDocument.activeElement !== el) return;
|
|
75
|
+
const at = indexAfter(el.value, count);
|
|
76
|
+
el.setSelectionRange(at, at);
|
|
77
|
+
});
|
|
78
|
+
const handleChange = (e) => {
|
|
79
|
+
onChange?.(e);
|
|
80
|
+
const el = e.target;
|
|
81
|
+
const raw = el.value;
|
|
82
|
+
const next = compactIban(raw);
|
|
83
|
+
const dropped = alnumCount(raw) - next.length;
|
|
84
|
+
const count = Math.max(0, alnumCount(raw.slice(0, el.selectionStart ?? raw.length)) - dropped);
|
|
85
|
+
if (next === compact) {
|
|
86
|
+
el.value = shown;
|
|
87
|
+
const at = indexAfter(shown, count);
|
|
88
|
+
el.setSelectionRange(at, at);
|
|
89
|
+
} else {
|
|
90
|
+
caretRef.current = count;
|
|
91
|
+
}
|
|
92
|
+
onValueChange?.(next);
|
|
93
|
+
};
|
|
94
|
+
const problem = visibleProblem(compact, kind, focused);
|
|
95
|
+
return /* @__PURE__ */ jsx(
|
|
96
|
+
Input,
|
|
97
|
+
{
|
|
98
|
+
ref: setRefs,
|
|
99
|
+
...rest,
|
|
100
|
+
type: "text",
|
|
101
|
+
value: shown,
|
|
102
|
+
onChange: handleChange,
|
|
103
|
+
onFocus: (e) => {
|
|
104
|
+
setFocused(true);
|
|
105
|
+
onFocus?.(e);
|
|
106
|
+
},
|
|
107
|
+
onBlur: (e) => {
|
|
108
|
+
setFocused(false);
|
|
109
|
+
onBlur?.(e);
|
|
110
|
+
},
|
|
111
|
+
autoComplete: rest.autoComplete ?? "off",
|
|
112
|
+
autoCapitalize: rest.autoCapitalize ?? "characters",
|
|
113
|
+
autoCorrect: rest.autoCorrect ?? "off",
|
|
114
|
+
spellCheck: rest.spellCheck ?? false,
|
|
115
|
+
error: hasMessage(error) ? error : problem ? messageFor(problem, compact, text) : void 0,
|
|
116
|
+
inputClassName: cn("tabular-nums", inputClassName)
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
IbanInput.displayName = "IbanInput";
|
|
121
|
+
export {
|
|
122
|
+
DEFAULT_IBAN_INPUT_LABELS,
|
|
123
|
+
IbanInput
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=iban-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/iban-input.tsx"],"sourcesContent":["import { forwardRef, useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ChangeEvent, FocusEvent, ReactNode } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { IBAN_LENGTHS, compactIban, formatIban, ibanProblem } from \"../lib/iban\";\nimport type { IbanKind, IbanProblem } from \"../lib/iban\";\nimport { assignRef, hasMessage } from \"./choice-parts\";\nimport { Input, type InputProps } from \"./ui\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The messages {@link IbanInput} shows under itself, one per {@link IbanProblem}. */\nexport interface IbanInputLabels {\n /** The value does not start with a two-letter country code and two check digits. */\n format: string;\n /** The first two letters are no country in the IBAN registry. `code` is those two\n * letters, as typed (upper case). */\n country: (code: string) => string;\n /** The wrong length for the country: `actual` characters typed, `expected` for that\n * country (spaces not counted in either). */\n length: (actual: number, expected: number) => string;\n /** The mod-97 check digits do not match — a mistyped or swapped character. */\n checksum: string;\n /** `kind=\"qr\"`: a valid IBAN, but not a QR-IBAN. */\n qrRequired: string;\n /** `kind=\"plain\"`: a QR-IBAN, which cannot receive an ordinary transfer. */\n qrNotAllowed: string;\n}\n\nexport const DEFAULT_IBAN_INPUT_LABELS: IbanInputLabels = {\n format: \"An IBAN starts with a two-letter country code and two check digits.\",\n country: (code) => `“${code}” is not the country code of an IBAN.`,\n length: (actual, expected) =>\n `An IBAN from this country has ${expected} characters — this one has ${actual}.`,\n checksum: \"The check digits don’t match — a character is probably mistyped.\",\n qrRequired: \"This is a regular IBAN. A QR-bill needs the account’s QR-IBAN.\",\n qrNotAllowed: \"This is a QR-IBAN, which only receives QR-bill payments. Enter the account’s regular IBAN.\",\n};\n\n/**\n * The `ibanInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction useIbanInputLabels(prop: Partial<IbanInputLabels> | undefined): IbanInputLabels {\n const labels: IbanInputLabels = useKitLabels(\"ibanInput\", DEFAULT_IBAN_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nconst alnumCount = (text: string) => text.replace(/[^0-9A-Za-z]/g, \"\").length;\n\n/** The index just after the `count`-th letter or digit of `text` — where the caret\n * goes once the grouping has moved the characters it was between. */\nfunction indexAfter(text: string, count: number): number {\n if (count <= 0) return 0;\n let seen = 0;\n for (let i = 0; i < text.length; i++) {\n if (/[0-9A-Za-z]/.test(text[i])) seen += 1;\n if (seen === count) return i + 1;\n }\n return text.length;\n}\n\n/**\n * The problem worth SHOWING right now. Settled (not focused): whatever is wrong. While\n * typing: only what more typing cannot fix — a broken start or an unknown country from\n * the fourth character, and the rest once the country's full length is there. A\n * half-typed IBAN is not wrong, and a field that says \"wrong length\" from the first\n * keystroke is a field people learn to ignore.\n */\nfunction visibleProblem(compact: string, kind: IbanKind, focused: boolean): IbanProblem | null {\n if (compact === \"\") return null;\n const problem = ibanProblem(compact, kind);\n if (problem === null || !focused) return problem;\n if (compact.length < 4) return null;\n if (problem === \"format\" || problem === \"country\") return problem;\n return compact.length >= IBAN_LENGTHS[compact.slice(0, 2)]! ? problem : null;\n}\n\nfunction messageFor(problem: IbanProblem, compact: string, labels: IbanInputLabels): string {\n switch (problem) {\n case \"format\":\n return labels.format;\n case \"country\":\n return labels.country(compact.slice(0, 2));\n case \"length\":\n return labels.length(compact.length, IBAN_LENGTHS[compact.slice(0, 2)]!);\n case \"checksum\":\n return labels.checksum;\n case \"qrRequired\":\n return labels.qrRequired;\n case \"qrNotAllowed\":\n return labels.qrNotAllowed;\n }\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface IbanInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The IBAN, compact or not — it is shown in groups of four either way. `\"\"` is\n * \"no IBAN\". */\n value: string;\n /** The compact, upper-case IBAN (the electronic format, what to store) on every\n * change — valid or not: a half-typed IBAN is still the field's value. Plain\n * `onChange` still fires alongside it with the raw event, whose text is the GROUPED\n * one, so bind this rather than `onChange` to a form. */\n onValueChange?: (value: string) => void;\n /** Which IBANs the field accepts. Default `\"any\"`. `\"qr\"` / `\"plain\"`: see\n * {@link IbanKind} — and why the difference matters, {@link isQrIban}. */\n kind?: IbanKind;\n /** The messages, per field. See {@link IbanInputLabels}. */\n labels?: Partial<IbanInputLabels>;\n /** The caller's own message, shown INSTEAD of the built-in one while it is set (a\n * server's \"this account is already on file\"). `null`, `false` and `\"\"` are no\n * message, so `touched && errors.iban` can be passed as it is. */\n error?: ReactNode;\n}\n\n/**\n * An IBAN field (kastlan, keksdose): typed in any shape, shown in groups of four,\n * reported compact.\n *\n * Both apps had a text field and a regex. What that left out is everything between\n * the keyboard and the stored value: a pasted \"ch09 0000-0000…\" stayed lower case\n * with its hyphens, the grouping that makes 21 characters checkable by eye was\n * missing, and a wrong IBAN was found out by the bank. Here:\n *\n * - **Grouped as you type.** The field shows the paper format ({@link formatIban});\n * spaces, hyphens and lower case in what is typed or pasted are folded away, and\n * the caret stays between the same two characters while the groups move around it.\n * - **Compact out.** `onValueChange` gets the electronic format — upper case, no\n * spaces — which is the one to store and to compare.\n * - **Checked, never blocked.** A wrong country, length or checksum shows a message\n * under the field, through the kit's `error` (so it is the field's\n * `aria-describedby` and paints it), but every keystroke still lands: a field that\n * refuses characters cannot be corrected one character at a time. While the field\n * has focus only what more typing cannot fix is shown; the rest waits for the full\n * length or for blur. Whether the field is REQUIRED stays the form's rule — an empty\n * field says nothing.\n * - **QR-IBAN or not** (kastlan): `kind=\"qr\"` refuses a regular IBAN and\n * `kind=\"plain\"` refuses a QR-IBAN — a QR-bill can only be paid to a QR-IBAN, and a\n * QR-IBAN can receive nothing else. See {@link isQrIban}.\n *\n * It is {@link Input} underneath, so `label`, `hint`, `error`, `invalid` and the rest\n * are Input's own and the field reads like every other. A form that must not submit\n * an invalid IBAN checks {@link isValidIban} / {@link ibanProblem} itself — the field\n * shows the problem but does not own the submit.\n */\nexport const IbanInput = forwardRef<HTMLInputElement, IbanInputProps>(function IbanInput(\n { value, onValueChange, onChange, onFocus, onBlur, kind = \"any\", labels, error, inputClassName, ...rest },\n ref,\n) {\n const text = useIbanInputLabels(labels);\n const compact = compactIban(value);\n const shown = formatIban(compact);\n const [focused, setFocused] = useState(false);\n const innerRef = useRef<HTMLInputElement | null>(null);\n // Letters and digits before the caret, kept across the re-render that regroups.\n const caretRef = useRef<number | null>(null);\n const setRefs = useCallback(\n (el: HTMLInputElement | null) => {\n innerRef.current = el;\n assignRef(ref, el);\n },\n [ref],\n );\n\n // Every render: when a change moved the groups, put the caret back after the same\n // character it followed. Only while focused — a value set from outside moves nothing.\n useLayoutEffect(() => {\n const el = innerRef.current;\n const count = caretRef.current;\n if (el === null || count === null) return;\n caretRef.current = null;\n if (el.ownerDocument.activeElement !== el) return;\n const at = indexAfter(el.value, count);\n el.setSelectionRange(at, at);\n });\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const el = e.target;\n const raw = el.value;\n const next = compactIban(raw);\n // Characters compactIban dropped from the front (a pasted \"IBAN \" label) are not\n // in the value, so they are not counted before the caret either.\n const dropped = alnumCount(raw) - next.length;\n const count = Math.max(0, alnumCount(raw.slice(0, el.selectionStart ?? raw.length)) - dropped);\n if (next === compact) {\n // Nothing the value holds changed — a space typed, a group's space deleted — so\n // no re-render will come to regroup it. Regroup now, and keep the caret: after\n // a Backspace on a group's space, that is just before it.\n el.value = shown;\n const at = indexAfter(shown, count);\n el.setSelectionRange(at, at);\n } else {\n caretRef.current = count;\n }\n onValueChange?.(next);\n };\n\n const problem = visibleProblem(compact, kind, focused);\n return (\n <Input\n ref={setRefs}\n {...rest}\n type=\"text\"\n value={shown}\n onChange={handleChange}\n onFocus={(e: FocusEvent<HTMLInputElement>) => {\n setFocused(true);\n onFocus?.(e);\n }}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setFocused(false);\n onBlur?.(e);\n }}\n autoComplete={rest.autoComplete ?? \"off\"}\n autoCapitalize={rest.autoCapitalize ?? \"characters\"}\n autoCorrect={rest.autoCorrect ?? \"off\"}\n spellCheck={rest.spellCheck ?? false}\n // ALWAYS passed, even while there is no message: a field that takes `error`\n // keeps its box, so the message coming and going under the caret never remounts\n // the input (see Input's FieldGroup) — this one comes and goes as you type.\n error={hasMessage(error) ? error : problem ? messageFor(problem, compact, text) : undefined}\n // Groups of four line up only when every digit is the same width.\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n );\n});\nIbanInput.displayName = \"IbanInput\";\n"],"mappings":"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","names":["IbanInput"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ComponentType, ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { ComponentType, ReactNode, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
import { TileSize } from './tile-radio.js';
|
|
4
4
|
|
|
5
5
|
interface IconPickerLabels {
|
|
@@ -31,7 +31,16 @@ interface IconOption<T extends string> {
|
|
|
31
31
|
/** A remark read after the name and shown under it in the bubble ("used by
|
|
32
32
|
* Leisure"). The tile wears a dot while it has one. */
|
|
33
33
|
note?: string;
|
|
34
|
+
/** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a
|
|
35
|
+
* `disabledReason` the tile stays reachable instead and says why. */
|
|
34
36
|
disabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Why THIS symbol cannot be chosen right now — {@link SwatchOption.disabledReason}'s
|
|
39
|
+
* twin: the tile is `aria-disabled` but reachable by Tab and the arrow keys, the reason
|
|
40
|
+
* shows in its bubble under its name and describes it, and choosing it does nothing.
|
|
41
|
+
* Wins over `disabled`. Kept through a search like any other tile.
|
|
42
|
+
*/
|
|
43
|
+
disabledReason?: ReactNode;
|
|
35
44
|
}
|
|
36
45
|
interface IconPickerProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "children" | "defaultValue"> {
|
|
37
46
|
options: IconOption<T>[];
|
|
@@ -65,6 +74,16 @@ interface IconPickerProps<T extends string> extends Omit<ComponentPropsWithoutRe
|
|
|
65
74
|
tileClassName?: string;
|
|
66
75
|
disabled?: boolean;
|
|
67
76
|
labels?: Partial<IconPickerLabels>;
|
|
77
|
+
/**
|
|
78
|
+
* An 11px static label in a strip over the picker, naming the grid — keksdose K2,
|
|
79
|
+
* {@link SwatchPickerProps.label}'s twin: a symbol picker standing in a form row
|
|
80
|
+
* beside labelled fields, with its label on their line. With `searchable` the strip
|
|
81
|
+
* is over the search box too, which keeps its own name. With a label, `className`
|
|
82
|
+
* styles the wrapper.
|
|
83
|
+
*/
|
|
84
|
+
label?: ReactNode;
|
|
85
|
+
/** A {@link FieldHint} beside the label. Only with `label`. */
|
|
86
|
+
hint?: ReactNode;
|
|
68
87
|
}
|
|
69
88
|
/**
|
|
70
89
|
* A grid of symbols that is one radio group, with an optional search.
|
|
@@ -76,6 +95,6 @@ interface IconPickerProps<T extends string> extends Omit<ComponentPropsWithoutRe
|
|
|
76
95
|
* Keyboard is {@link SwatchPicker}'s: one tab stop, arrows to move (flipped in a
|
|
77
96
|
* right-to-left page), Home and End.
|
|
78
97
|
*/
|
|
79
|
-
declare function IconPicker<T extends string>({ options, value, onChange, allowNone, mixed, activation, searchable, size, tileClassName, disabled, labels, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest }: IconPickerProps<T>): react.JSX.Element;
|
|
98
|
+
declare function IconPicker<T extends string>({ options, value, onChange, allowNone, mixed, activation, searchable, size, tileClassName, disabled, labels, label, hint, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest }: IconPickerProps<T>): react.JSX.Element;
|
|
80
99
|
|
|
81
100
|
export { DEFAULT_ICON_PICKER_LABELS, type IconOption, IconPicker, type IconPickerLabels, type IconPickerProps };
|
|
@@ -5,6 +5,7 @@ import { cn } from "../lib/cn.js";
|
|
|
5
5
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
6
6
|
import { SearchField } from "./search-field.js";
|
|
7
7
|
import { TILE_SIZE, TileRadioGroup } from "./tile-radio.js";
|
|
8
|
+
import { hasContent, LabelStrip } from "./field-parts.js";
|
|
8
9
|
const DEFAULT_ICON_PICKER_LABELS = {
|
|
9
10
|
none: "No icon",
|
|
10
11
|
mixed: "Mixed: the selected items have different icons",
|
|
@@ -27,6 +28,8 @@ function IconPicker({
|
|
|
27
28
|
tileClassName,
|
|
28
29
|
disabled = false,
|
|
29
30
|
labels,
|
|
31
|
+
label,
|
|
32
|
+
hint,
|
|
30
33
|
className,
|
|
31
34
|
"aria-label": ariaLabel,
|
|
32
35
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -37,6 +40,8 @@ function IconPicker({
|
|
|
37
40
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
38
41
|
const locale = useKitLocale();
|
|
39
42
|
const mixedId = useId();
|
|
43
|
+
const labelId = useId();
|
|
44
|
+
const labelled = hasContent(label);
|
|
40
45
|
const [query, setQuery] = useState("");
|
|
41
46
|
const needle = fold(query.trim(), locale);
|
|
42
47
|
const visible = useMemo(
|
|
@@ -50,9 +55,15 @@ function IconPicker({
|
|
|
50
55
|
// "None" stays through a search: it is the way back to no icon, and it matches
|
|
51
56
|
// no word anybody would type for it.
|
|
52
57
|
...allowNone ? [{ key: null, label: text.none }] : [],
|
|
53
|
-
...visible.map((o) => ({
|
|
58
|
+
...visible.map((o) => ({
|
|
59
|
+
key: o.value,
|
|
60
|
+
label: o.label,
|
|
61
|
+
note: o.note,
|
|
62
|
+
disabled: o.disabled,
|
|
63
|
+
disabledReason: o.disabledReason
|
|
64
|
+
}))
|
|
54
65
|
];
|
|
55
|
-
|
|
66
|
+
const picker = /* @__PURE__ */ jsxs("div", { ...rest, className: cn("space-y-2", !labelled && className), children: [
|
|
56
67
|
searchable && /* @__PURE__ */ jsx(
|
|
57
68
|
SearchField,
|
|
58
69
|
{
|
|
@@ -68,7 +79,7 @@ function IconPicker({
|
|
|
68
79
|
{
|
|
69
80
|
role: "radiogroup",
|
|
70
81
|
"aria-label": ariaLabel,
|
|
71
|
-
"aria-labelledby": ariaLabelledBy,
|
|
82
|
+
"aria-labelledby": labelled && ariaLabel === void 0 && ariaLabelledBy === void 0 ? labelId : ariaLabelledBy,
|
|
72
83
|
"aria-describedby": [ariaDescribedBy, mixed && mixedId].filter(Boolean).join(" ") || void 0,
|
|
73
84
|
"aria-disabled": disabled || void 0,
|
|
74
85
|
className: "flex flex-wrap items-center gap-1.5",
|
|
@@ -77,7 +88,9 @@ function IconPicker({
|
|
|
77
88
|
{
|
|
78
89
|
items,
|
|
79
90
|
checked: mixed ? void 0 : value,
|
|
80
|
-
onSelect:
|
|
91
|
+
onSelect: (next) => {
|
|
92
|
+
if (!hasContent(next === null ? void 0 : byValue.get(next)?.disabledReason)) onChange(next);
|
|
93
|
+
},
|
|
81
94
|
activation,
|
|
82
95
|
disabled,
|
|
83
96
|
size,
|
|
@@ -100,6 +113,8 @@ function IconPicker({
|
|
|
100
113
|
// the document height (see tokens.css).
|
|
101
114
|
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", "aria-atomic": true, className: "sr-only-fixed", children: needle ? text.resultCount(visible.length) : "" })
|
|
102
115
|
] });
|
|
116
|
+
if (!labelled) return picker;
|
|
117
|
+
return /* @__PURE__ */ jsx(LabelStrip, { labelId, label, hint, disabled, pad: "clear", className, children: picker });
|
|
103
118
|
}
|
|
104
119
|
export {
|
|
105
120
|
DEFAULT_ICON_PICKER_LABELS,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/icon-picker.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ComponentType } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { SearchField } from \"./search-field\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\n\nexport interface IconPickerLabels {\n /** The \"no icon\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n /** The search box's accessible name and placeholder, when `searchable` is set. */\n search: string;\n /** Shown in place of the tiles when the search matches nothing. */\n noResults: string;\n /** Announced as the search narrows the grid. Receives the number of matches. */\n resultCount: (count: number) => string;\n}\n\nexport const DEFAULT_ICON_PICKER_LABELS: IconPickerLabels = {\n none: \"No icon\",\n mixed: \"Mixed: the selected items have different icons\",\n search: \"Search icons\",\n noResults: \"No icons match\",\n resultCount: (count) => (count === 1 ? \"1 icon\" : `${count} icons`),\n};\n\nexport interface IconOption<T extends string> {\n value: T;\n /** A Lucide icon, or any component that takes a `className`. Rendered `aria-hidden`\n * — the tile's name is `label`. */\n icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** What the symbol IS, in the user's language (\"Game controller\") — not the icon\n * library's identifier (`Gamepad2`), which is nobody's word for anything. */\n label: string;\n /** More words the search matches (\"games\", \"play\"), besides `label`. */\n keywords?: string[];\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Leisure\"). The tile wears a dot while it has one. */\n note?: string;\n disabled?: boolean;\n}\n\nexport interface IconPickerProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\"> {\n options: IconOption<T>[];\n /** The chosen icon, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no icon\" tile whose value is `null`. */\n allowNone?: boolean;\n /** See {@link SwatchPickerProps.mixed}: nothing is checked, and the group is\n * described as mixed. */\n mixed?: boolean;\n /**\n * \"automatic\" (default): an arrow key moves AND chooses, the APG radio group.\n * \"manual\": arrows move focus only; Space or Enter chooses. For a picker whose every\n * change is expensive — a bulk edit that saves as it goes.\n */\n activation?: \"automatic\" | \"manual\";\n /** A search box above the grid, matching `label` and `keywords`. For a set large\n * enough that scanning it is slower than typing. */\n searchable?: boolean;\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: Partial<IconPickerLabels>;\n}\n\n/** Case- and accent-insensitive, so \"cafe\" finds \"Café\". */\nfunction fold(text: string, locale: string | undefined): string {\n return text.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLocaleLowerCase(locale);\n}\n\n/**\n * A grid of symbols that is one radio group, with an optional search.\n *\n * Name the group with `aria-label` or `aria-labelledby`. The name lands on the\n * radiogroup itself, not on the outer box: with `searchable` the outer box also holds\n * the search field, which has a name of its own.\n *\n * Keyboard is {@link SwatchPicker}'s: one tab stop, arrows to move (flipped in a\n * right-to-left page), Home and End.\n */\nexport function IconPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n searchable = false,\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: IconPickerProps<T>) {\n const text = useKitLabels(\"iconPicker\", DEFAULT_ICON_PICKER_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n const mixedId = useId();\n const [query, setQuery] = useState(\"\");\n const needle = fold(query.trim(), locale);\n\n const visible = useMemo(\n () =>\n needle\n ? options.filter((o) =>\n [o.label, ...(o.keywords ?? [])].some((w) => fold(w, locale).includes(needle)),\n )\n : options,\n [options, needle, locale],\n );\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n // \"None\" stays through a search: it is the way back to no icon, and it matches\n // no word anybody would type for it.\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...visible.map((o) => ({ key: o.value, label: o.label, note: o.note, disabled: o.disabled })),\n ];\n\n return (\n <div {...rest} className={cn(\"space-y-2\", className)}>\n {searchable && (\n <SearchField\n value={query}\n onChange={setQuery}\n aria-label={text.search}\n clearLabel={common.clear}\n disabled={disabled}\n />\n )}\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-describedby={[ariaDescribedBy, mixed && mixedId].filter(Boolean).join(\" \") || undefined}\n aria-disabled={disabled || undefined}\n className=\"flex flex-wrap items-center gap-1.5\"\n >\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n onSelect={onChange}\n activation={activation}\n disabled={disabled}\n size={size}\n renderTile={(item) => {\n const Icon = item.key === null ? undefined : byValue.get(item.key)?.icon;\n return Icon ? <Icon aria-hidden className={TILE_SIZE[size].glyph} /> : null;\n }}\n tileClassName={(_item, selected) =>\n cn(\n selected ? \"text-[var(--text-primary)]\" : \"text-[var(--text-secondary)]\",\n tileClassName,\n )\n }\n />\n </div>\n {needle && visible.length === 0 && (\n <p className=\"text-xs text-[var(--text-muted)]\">{text.noResults}</p>\n )}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n {searchable && (\n // Typing moves no focus and the grid changes silently, so the count is said.\n // `sr-only-fixed`: this sits inside a consumer's markup and must not extend\n // the document height (see tokens.css).\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {needle ? text.resultCount(visible.length) : \"\"}\n </span>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/icon-picker.tsx"],"sourcesContent":["import { useId, useMemo, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ComponentType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { SearchField } from \"./search-field\";\nimport { TILE_SIZE, TileRadioGroup } from \"./tile-radio\";\nimport type { TileItem, TileSize } from \"./tile-radio\";\nimport { hasContent, LabelStrip } from \"./field-parts\";\n\nexport interface IconPickerLabels {\n /** The \"no icon\" tile's name, when `allowNone` is set. */\n none: string;\n /** Describes the group while `mixed` is set — a bulk edit over rows that disagree. */\n mixed: string;\n /** The search box's accessible name and placeholder, when `searchable` is set. */\n search: string;\n /** Shown in place of the tiles when the search matches nothing. */\n noResults: string;\n /** Announced as the search narrows the grid. Receives the number of matches. */\n resultCount: (count: number) => string;\n}\n\nexport const DEFAULT_ICON_PICKER_LABELS: IconPickerLabels = {\n none: \"No icon\",\n mixed: \"Mixed: the selected items have different icons\",\n search: \"Search icons\",\n noResults: \"No icons match\",\n resultCount: (count) => (count === 1 ? \"1 icon\" : `${count} icons`),\n};\n\nexport interface IconOption<T extends string> {\n value: T;\n /** A Lucide icon, or any component that takes a `className`. Rendered `aria-hidden`\n * — the tile's name is `label`. */\n icon: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** What the symbol IS, in the user's language (\"Game controller\") — not the icon\n * library's identifier (`Gamepad2`), which is nobody's word for anything. */\n label: string;\n /** More words the search matches (\"games\", \"play\"), besides `label`. */\n keywords?: string[];\n /** A remark read after the name and shown under it in the bubble (\"used by\n * Leisure\"). The tile wears a dot while it has one. */\n note?: string;\n /** Out of the choice and out of reach — skipped by the arrow keys and Tab. With a\n * `disabledReason` the tile stays reachable instead and says why. */\n disabled?: boolean;\n /**\n * Why THIS symbol cannot be chosen right now — {@link SwatchOption.disabledReason}'s\n * twin: the tile is `aria-disabled` but reachable by Tab and the arrow keys, the reason\n * shows in its bubble under its name and describes it, and choosing it does nothing.\n * Wins over `disabled`. Kept through a search like any other tile.\n */\n disabledReason?: ReactNode;\n}\n\nexport interface IconPickerProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\" | \"defaultValue\"> {\n options: IconOption<T>[];\n /** The chosen icon, or `null` for none. */\n value: T | null;\n onChange: (value: T | null) => void;\n /** Lead with a \"no icon\" tile whose value is `null`. */\n allowNone?: boolean;\n /** See {@link SwatchPickerProps.mixed}: nothing is checked, and the group is\n * described as mixed. */\n mixed?: boolean;\n /**\n * \"automatic\" (default): an arrow key moves AND chooses, the APG radio group.\n * \"manual\": arrows move focus only; Space or Enter chooses. For a picker whose every\n * change is expensive — a bulk edit that saves as it goes.\n */\n activation?: \"automatic\" | \"manual\";\n /** A search box above the grid, matching `label` and `keywords`. For a set large\n * enough that scanning it is slower than typing. */\n searchable?: boolean;\n /** 28 / 32 / 44px tiles. Default \"md\"; \"lg\" is the touch-target size. */\n size?: TileSize;\n /**\n * Classes for every tile's frame, merged after the kit's own — so a caller's\n * `size-*` beats `size`'s. For a tile that has to follow a container query or a\n * breakpoint (keksdose grows its tiles to 40px in a narrow card:\n * `tileClassName=\"@max-md:size-10\"`) without reaching into the kit's markup with an\n * `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s\n * dimensions; the tile grows round it.\n */\n tileClassName?: string;\n disabled?: boolean;\n labels?: Partial<IconPickerLabels>;\n /**\n * An 11px static label in a strip over the picker, naming the grid — keksdose K2,\n * {@link SwatchPickerProps.label}'s twin: a symbol picker standing in a form row\n * beside labelled fields, with its label on their line. With `searchable` the strip\n * is over the search box too, which keeps its own name. With a label, `className`\n * styles the wrapper.\n */\n label?: ReactNode;\n /** A {@link FieldHint} beside the label. Only with `label`. */\n hint?: ReactNode;\n}\n\n/** Case- and accent-insensitive, so \"cafe\" finds \"Café\". */\nfunction fold(text: string, locale: string | undefined): string {\n return text.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLocaleLowerCase(locale);\n}\n\n/**\n * A grid of symbols that is one radio group, with an optional search.\n *\n * Name the group with `aria-label` or `aria-labelledby`. The name lands on the\n * radiogroup itself, not on the outer box: with `searchable` the outer box also holds\n * the search field, which has a name of its own.\n *\n * Keyboard is {@link SwatchPicker}'s: one tab stop, arrows to move (flipped in a\n * right-to-left page), Home and End.\n */\nexport function IconPicker<T extends string>({\n options,\n value,\n onChange,\n allowNone = false,\n mixed = false,\n activation = \"automatic\",\n searchable = false,\n size = \"md\",\n tileClassName,\n disabled = false,\n labels,\n label,\n hint,\n className,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n}: IconPickerProps<T>) {\n const text = useKitLabels(\"iconPicker\", DEFAULT_ICON_PICKER_LABELS, labels);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale();\n const mixedId = useId();\n const labelId = useId();\n const labelled = hasContent(label);\n const [query, setQuery] = useState(\"\");\n const needle = fold(query.trim(), locale);\n\n const visible = useMemo(\n () =>\n needle\n ? options.filter((o) =>\n [o.label, ...(o.keywords ?? [])].some((w) => fold(w, locale).includes(needle)),\n )\n : options,\n [options, needle, locale],\n );\n const byValue = useMemo(() => new Map(options.map((o) => [o.value, o])), [options]);\n const items: TileItem<T>[] = [\n // \"None\" stays through a search: it is the way back to no icon, and it matches\n // no word anybody would type for it.\n ...(allowNone ? [{ key: null, label: text.none }] : []),\n ...visible.map((o) => ({\n key: o.value,\n label: o.label,\n note: o.note,\n disabled: o.disabled,\n disabledReason: o.disabledReason,\n })),\n ];\n\n const picker = (\n <div {...rest} className={cn(\"space-y-2\", !labelled && className)}>\n {searchable && (\n <SearchField\n value={query}\n onChange={setQuery}\n aria-label={text.search}\n clearLabel={common.clear}\n disabled={disabled}\n />\n )}\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n // The strip's label names the GRID, unless the caller named it already.\n aria-labelledby={labelled && ariaLabel === undefined && ariaLabelledBy === undefined ? labelId : ariaLabelledBy}\n aria-describedby={[ariaDescribedBy, mixed && mixedId].filter(Boolean).join(\" \") || undefined}\n aria-disabled={disabled || undefined}\n className=\"flex flex-wrap items-center gap-1.5\"\n >\n <TileRadioGroup\n items={items}\n checked={mixed ? undefined : value}\n // A locked tile is refused by the tiles themselves; asked again here so a\n // reason can never become a change, whatever the tiles do.\n onSelect={(next) => {\n if (!hasContent(next === null ? undefined : byValue.get(next)?.disabledReason)) onChange(next);\n }}\n activation={activation}\n disabled={disabled}\n size={size}\n renderTile={(item) => {\n const Icon = item.key === null ? undefined : byValue.get(item.key)?.icon;\n return Icon ? <Icon aria-hidden className={TILE_SIZE[size].glyph} /> : null;\n }}\n tileClassName={(_item, selected) =>\n cn(\n selected ? \"text-[var(--text-primary)]\" : \"text-[var(--text-secondary)]\",\n tileClassName,\n )\n }\n />\n </div>\n {needle && visible.length === 0 && (\n <p className=\"text-xs text-[var(--text-muted)]\">{text.noResults}</p>\n )}\n {mixed && (\n <span id={mixedId} hidden>\n {text.mixed}\n </span>\n )}\n {searchable && (\n // Typing moves no focus and the grid changes silently, so the count is said.\n // `sr-only-fixed`: this sits inside a consumer's markup and must not extend\n // the document height (see tokens.css).\n <span role=\"status\" aria-live=\"polite\" aria-atomic className=\"sr-only-fixed\">\n {needle ? text.resultCount(visible.length) : \"\"}\n </span>\n )}\n </div>\n );\n if (!labelled) return picker;\n return (\n <LabelStrip labelId={labelId} label={label} hint={hint} disabled={disabled} pad=\"clear\" className={className}>\n {picker}\n </LabelStrip>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, InputHTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every string {@link InlineEditField} says on its own behalf — the `inlineEdit`
|
|
6
|
+
* namespace of `<UiKitProvider labels>`, overridable per instance through `labels`.
|
|
7
|
+
*/
|
|
8
|
+
interface InlineEditLabels {
|
|
9
|
+
/** The display button's tooltip and description — what pressing it does. Receives
|
|
10
|
+
* the field's `label` ("Budget name" → "Edit Budget name"). */
|
|
11
|
+
edit: (label: string) => string;
|
|
12
|
+
/** Shown (and spoken) when `onCommit` rejects and no `formatError` says better. */
|
|
13
|
+
failed: string;
|
|
14
|
+
/** What the display shows for an empty value, when no `placeholder` is given. */
|
|
15
|
+
empty: string;
|
|
16
|
+
}
|
|
17
|
+
declare const DEFAULT_INLINE_EDIT_LABELS: InlineEditLabels;
|
|
18
|
+
/** What an `editor` render prop is handed: the draft and the means to end the edit. */
|
|
19
|
+
interface InlineEditorProps<T> {
|
|
20
|
+
/** The draft — seeded from `value` each time the editor opens. */
|
|
21
|
+
value: T;
|
|
22
|
+
/** Replace the draft (every keystroke). */
|
|
23
|
+
onChange: (next: T) => void;
|
|
24
|
+
/**
|
|
25
|
+
* End the edit and save. Without an argument it saves the draft; WITH one it saves
|
|
26
|
+
* that — for an editor whose own commit hands over a resolved value (`NumberInput`'s
|
|
27
|
+
* `onCommit` gives "1050" for a typed "1100-50"). Called twice (the editor's own
|
|
28
|
+
* Enter, then the field's), the second call does nothing.
|
|
29
|
+
*/
|
|
30
|
+
commit: (next?: T) => void;
|
|
31
|
+
/** End the edit and drop the draft — what Escape does. */
|
|
32
|
+
cancel: () => void;
|
|
33
|
+
/** The save is in flight: make the control read-only (not disabled — it has focus). */
|
|
34
|
+
pending: boolean;
|
|
35
|
+
/** The field's `label`, for the control's accessible name. */
|
|
36
|
+
label: string;
|
|
37
|
+
/** The last save failed and the draft has not been touched since. */
|
|
38
|
+
invalid: boolean;
|
|
39
|
+
/** The id of the failure message while there is one, for `aria-describedby`. */
|
|
40
|
+
describedBy: string | undefined;
|
|
41
|
+
/** The four above as attributes — spread them on a kit field that takes the DOM
|
|
42
|
+
* spelling: `<AmountInput {...p.inputProps} … />`. */
|
|
43
|
+
inputProps: {
|
|
44
|
+
"aria-label": string;
|
|
45
|
+
"aria-invalid": true | undefined;
|
|
46
|
+
"aria-describedby": string | undefined;
|
|
47
|
+
readOnly: boolean;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
interface InlineEditFieldBaseProps<T> {
|
|
51
|
+
/** The saved value — what the display shows, and what the editor opens on. */
|
|
52
|
+
value: T;
|
|
53
|
+
/**
|
|
54
|
+
* Save `next`. Return a promise and the field waits: the editor stays open and
|
|
55
|
+
* read-only with a spinner until it settles, closes when it resolves, and STAYS OPEN
|
|
56
|
+
* with the failure under it when it rejects (or throws) — the typed value is kept for
|
|
57
|
+
* a retry. Not called when the draft equals `value` (see `isEqual`): opening the
|
|
58
|
+
* editor and leaving it unchanged is not a write.
|
|
59
|
+
*/
|
|
60
|
+
onCommit: (next: T) => void | Promise<unknown>;
|
|
61
|
+
/** What the value is ("Budget name", "Assigned") — the editor's accessible name and
|
|
62
|
+
* the subject of the display's "Edit …" description. Not drawn: inline, the row or
|
|
63
|
+
* the column header is the visible label. */
|
|
64
|
+
label: string;
|
|
65
|
+
/** The value as the display shows it — a formatted amount, a date. Default: the
|
|
66
|
+
* value as text. */
|
|
67
|
+
display?: (value: T) => ReactNode;
|
|
68
|
+
/** Whether the draft is unchanged, so leaving the editor is not a write. Default
|
|
69
|
+
* `Object.is`. keksdose compares assigned amounts as NUMBERS — "1100" is the
|
|
70
|
+
* server's "1100.0000". */
|
|
71
|
+
isEqual?: (a: T, b: T) => boolean;
|
|
72
|
+
/** What the display shows for an empty value (`""`, `null`, an empty `display`).
|
|
73
|
+
* Default `labels.empty`. */
|
|
74
|
+
placeholder?: ReactNode;
|
|
75
|
+
/** `"end"` for a figure in a right-aligned money column. Default `"start"`. */
|
|
76
|
+
align?: "start" | "end";
|
|
77
|
+
/** Look, do not touch — a guest's view: the value as plain text, no button. */
|
|
78
|
+
readOnly?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Why it cannot be edited now — the write lock's sentence, a closed period. The
|
|
81
|
+
* value stays a BUTTON (focusable, `aria-disabled`) with the reason in the kit
|
|
82
|
+
* Tooltip and its description; it opens no editor. An editor that is open when the
|
|
83
|
+
* reason arrives closes, its draft dropped: a field you can type into must be one
|
|
84
|
+
* whose value can be saved (keksdose dev#496).
|
|
85
|
+
*/
|
|
86
|
+
disabledReason?: ReactNode;
|
|
87
|
+
/** This field COMMITS — under a locked {@link WriteLockProvider} it takes the
|
|
88
|
+
* `disabledReason` path with the lock's reason (which wins over its own). No
|
|
89
|
+
* provider, or an unlocked one: no effect. */
|
|
90
|
+
commit?: boolean;
|
|
91
|
+
/** The value is the user's own data: `data-private` on the display and the editor,
|
|
92
|
+
* so the app's demo-mode blur covers both. */
|
|
93
|
+
redact?: boolean;
|
|
94
|
+
/** Turns a rejection into words. Default `labels.failed`. */
|
|
95
|
+
formatError?: (error: unknown) => ReactNode;
|
|
96
|
+
/** Controlled open state — a row's swipe action that opens the editor from outside
|
|
97
|
+
* (keksdose's assigned cell). Left out, the field keeps its own. */
|
|
98
|
+
editing?: boolean;
|
|
99
|
+
/** Called with the next open state: on open, on a save that resolved, on cancel. */
|
|
100
|
+
onEditingChange?: (editing: boolean) => void;
|
|
101
|
+
/** Attributes for the DEFAULT text editor's `<input>` — `maxLength`, `inputMode`,
|
|
102
|
+
* `autoComplete`. Ignored with `editor`. */
|
|
103
|
+
inputProps?: Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type">;
|
|
104
|
+
/** Classes for the root. */
|
|
105
|
+
className?: string;
|
|
106
|
+
labels?: Partial<InlineEditLabels>;
|
|
107
|
+
}
|
|
108
|
+
type EditorRender<T> = (props: InlineEditorProps<T>) => ReactNode;
|
|
109
|
+
/**
|
|
110
|
+
* The editor. Optional for a string value — the kit's `Input`, compact — and
|
|
111
|
+
* REQUIRED for anything else, since only the caller knows how to edit a number or an
|
|
112
|
+
* amount. Render a kit field from the props it is handed:
|
|
113
|
+
*
|
|
114
|
+
* ```tsx
|
|
115
|
+
* editor={(p) => (
|
|
116
|
+
* <NumberInput calculator={false} ariaLabel={p.label} value={p.value} onChange={p.onChange}
|
|
117
|
+
* onCommit={p.commit} aria-invalid={p.invalid || undefined} aria-describedby={p.describedBy} />
|
|
118
|
+
* )}
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* The field itself handles Enter (save), Escape (cancel) and focus leaving the editor
|
|
122
|
+
* (save) around whatever is rendered — a key the editor has already handled
|
|
123
|
+
* (`preventDefault`, as a listbox does for its Enter) is left to it. Focus counts as
|
|
124
|
+
* "inside" across a portal, so a picker or a phone keypad the editor opens does not
|
|
125
|
+
* end the edit.
|
|
126
|
+
*/
|
|
127
|
+
type InlineEditFieldProps<T = string> = InlineEditFieldBaseProps<T> & ([T] extends [string] ? {
|
|
128
|
+
editor?: EditorRender<T>;
|
|
129
|
+
} : {
|
|
130
|
+
editor: EditorRender<T>;
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* A value that edits in place: a quiet button showing the value; click it (or Enter
|
|
134
|
+
* on it) and it becomes a field; Enter or leaving the field saves, Escape cancels
|
|
135
|
+
* (keksdose K9).
|
|
136
|
+
*
|
|
137
|
+
* keksdose has three of them by hand — the budget's assigned cell (budget-cells.tsx),
|
|
138
|
+
* the invoice line's value cells (invoice-lines-table.tsx `EditableCell`) and the
|
|
139
|
+
* budget rename (budgets-page.tsx, an `Input` + Save + Cancel with no Escape) — and
|
|
140
|
+
* each had to learn the same rules:
|
|
141
|
+
*
|
|
142
|
+
* - **The display's accessible name is the VALUE.** A cell named "Edit" reads a table
|
|
143
|
+
* of "edit, edit, edit"; the action is the description instead ("Edit Assigned"),
|
|
144
|
+
* in the kit Tooltip on hover and focus.
|
|
145
|
+
* - **An unchanged draft is not a write.** Opening the editor and leaving it was a PUT
|
|
146
|
+
* in keksdose until `isEqual` compared numbers, and on a sealed budget an outbox row
|
|
147
|
+
* for a change nobody made.
|
|
148
|
+
* - **A failed save keeps the editor.** The typed value is the user's work; the
|
|
149
|
+
* failure is shown under it (attached with `aria-describedby`, and spoken), and the
|
|
150
|
+
* next keystroke clears it. Escape still drops the draft.
|
|
151
|
+
* - **Locked means a disabled BUTTON, not a disabled field** (keksdose dev#496): at
|
|
152
|
+
* rest a real row shows this button, so a locked page shows the same, with the
|
|
153
|
+
* reason — never a field that takes keys it cannot save.
|
|
154
|
+
*
|
|
155
|
+
* Inside a {@link DataTable} cell it needs nothing extra: the table already leaves a
|
|
156
|
+
* click or a key on a control of the cell to that control. Focus returns to the value
|
|
157
|
+
* after Enter or Escape; after a save that came from focus leaving, it stays where the
|
|
158
|
+
* user put it.
|
|
159
|
+
*
|
|
160
|
+
* ```tsx
|
|
161
|
+
* <InlineEditField label={t("budgets.name")} value={budget.name}
|
|
162
|
+
* onCommit={(name) => rename.mutateAsync({ id: budget.id, name })} inputProps={{ maxLength: 120 }} />
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
declare function InlineEditField<T = string>(props: InlineEditFieldProps<T>): react.JSX.Element;
|
|
166
|
+
|
|
167
|
+
export { DEFAULT_INLINE_EDIT_LABELS, InlineEditField, type InlineEditFieldProps, type InlineEditLabels, type InlineEditorProps };
|