@eifi1/ui-kit 0.20.1 → 0.22.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/dist/chart.d.ts +10 -8
- 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 +27 -8
- package/dist/components/amount-input.js +15 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +8 -0
- package/dist/components/autocomplete.js +9 -4
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -8
- package/dist/components/calculator.d.ts +10 -8
- 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/combobox.d.ts +13 -2
- package/dist/components/combobox.js +17 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +66 -161
- package/dist/components/copy-button.d.ts +10 -8
- package/dist/components/country-select.d.ts +124 -0
- package/dist/components/country-select.js +237 -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 +66 -166
- package/dist/components/danger-confirm.js +133 -49
- 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 +25 -5
- package/dist/components/date-picker.js +110 -59
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +8 -1
- package/dist/components/entity-combobox.js +9 -4
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -8
- package/dist/components/field-anatomy.d.ts +95 -0
- package/dist/components/field-anatomy.js +84 -0
- package/dist/components/field-anatomy.js.map +1 -0
- package/dist/components/field-parts.d.ts +114 -0
- package/dist/components/field-parts.js +72 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/file-button.d.ts +10 -8
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +32 -9
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +10 -8
- package/dist/components/form-actions.js +61 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/hover-menu.d.ts +3 -0
- package/dist/components/hover-menu.js +2 -2
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/iban-input.d.ts +66 -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 +12 -2
- package/dist/components/icon-picker.js +9 -2
- 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 +127 -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 +20 -8
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +290 -86
- package/dist/components/month-picker.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 +25 -13
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +70 -13
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +10 -8
- 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 +66 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/series-chart.d.ts +10 -8
- package/dist/components/settings-fields.d.ts +10 -8
- package/dist/components/share-card.d.ts +8 -6
- 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 +27 -2
- package/dist/components/swatch-picker.js +73 -31
- 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 +10 -8
- package/dist/components/tile-radio.d.ts +68 -13
- package/dist/components/tile-radio.js +3 -3
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +15 -9
- 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/ui.d.ts +10 -8
- package/dist/components/ui.js +280 -78
- 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 +210 -3
- 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 +1 -1
- package/dist/feedback/feedback-thread.d.ts +37 -3
- package/dist/feedback/feedback-thread.js +38 -9
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
- package/dist/feedback.d.ts +2 -2
- package/dist/hooks/use-file-drop.d.ts +10 -8
- package/dist/i18n/defaults.d.ts +10 -8
- package/dist/i18n/defaults.js +17 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -8
- package/dist/i18n/german.js +50 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +10 -8
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -8
- package/dist/i18n/locales/de-CH.d.ts +10 -8
- package/dist/i18n/locales/en.d.ts +10 -8
- package/dist/i18n/locales/en.js +9 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +10 -8
- package/dist/i18n/locales/es.js +48 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -8
- package/dist/i18n/locales/fr.js +48 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -8
- package/dist/i18n/locales/hu.js +49 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -8
- package/dist/i18n/locales/it.js +49 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -8
- package/dist/i18n/locales/zh.js +48 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -8
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +17 -10
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
- 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/rhf/fields.d.ts +148 -11
- package/dist/rhf/fields.js +332 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +10 -8
- package/dist/rhf.d.ts +13 -9
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +10 -8
- package/dist/shell/top-bar-brand.d.ts +10 -8
- package/dist/shell/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +31 -28
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +10 -8
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -8
- package/dist/wizard.d.ts +10 -8
- 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 +49 -56
- package/src/components/autocomplete.tsx +133 -110
- 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/combobox.tsx +292 -253
- package/src/components/country-select.tsx +443 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +345 -77
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +173 -62
- package/src/components/entity-combobox.tsx +107 -87
- package/src/components/field-anatomy.tsx +190 -0
- package/src/components/field-parts.tsx +184 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +115 -2
- package/src/components/hover-menu.tsx +7 -4
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +26 -4
- 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 +10 -0
- package/src/components/month-picker.tsx +399 -63
- 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/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +118 -30
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +71 -16
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/ui.tsx +472 -105
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +313 -9
- package/src/feedback/feedback-dialog.tsx +2 -0
- package/src/feedback/feedback-thread.tsx +66 -5
- package/src/i18n/defaults.ts +16 -1
- package/src/i18n/german.ts +47 -0
- package/src/i18n/kit-labels.tsx +78 -2
- package/src/i18n/locales/en.ts +10 -0
- package/src/i18n/locales/es.ts +46 -0
- package/src/i18n/locales/fr.ts +45 -0
- package/src/i18n/locales/hu.ts +46 -0
- package/src/i18n/locales/it.ts +47 -0
- package/src/i18n/locales/zh.ts +44 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +53 -3
- 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/rhf/fields.tsx +606 -9
- package/src/rhf.ts +2 -1
- package/src/shell/topbar-action-menu.tsx +53 -29
- package/src/tour/tour.tsx +66 -28
|
@@ -50,6 +50,13 @@ interface EntityComboboxProps<V extends string | number, C extends ComboClearVal
|
|
|
50
50
|
/** What is wrong with the value, as {@link Input}'s `error`: rendered under the
|
|
51
51
|
* field, on the trigger's `aria-describedby`, and implies `invalid`. */
|
|
52
52
|
error?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Standing advice — {@link Combobox}'s `hint`, so the pickers read like every other
|
|
55
|
+
* field (keksdose K4): plain TEXT is a caption under the field on the trigger's
|
|
56
|
+
* `aria-describedby`, before any error; a {@link FieldHint} "?" rides the label line,
|
|
57
|
+
* or with no label sits at the trigger's end edge, outside the box.
|
|
58
|
+
*/
|
|
59
|
+
hint?: ReactNode;
|
|
53
60
|
/** Narrow `options` client-side by the query. Default `true`; `false` shows them
|
|
54
61
|
* as given (a server-ranked list). */
|
|
55
62
|
filter?: boolean;
|
|
@@ -68,6 +75,6 @@ interface EntityComboboxProps<V extends string | number, C extends ComboClearVal
|
|
|
68
75
|
* the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking
|
|
69
76
|
* several entities use {@link MultiEntityCombobox}.
|
|
70
77
|
*/
|
|
71
|
-
declare function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({ value, onChange, clearValue, options, loadOptions, loading, label, placeholder, searchPlaceholder, emptyLabel, clearable, clearLabel, closeLabel, disabled, onCreate, createLabel, className, invalid, error, filter, minChars, debounceMs, loadErrorLabel, "aria-label": ariaLabel, id, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rest }: EntityComboboxProps<V, C>): react.JSX.Element;
|
|
78
|
+
declare function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({ value, onChange, clearValue, options, loadOptions, loading, label, placeholder, searchPlaceholder, emptyLabel, clearable, clearLabel, closeLabel, disabled, onCreate, createLabel, className, invalid, error, hint, filter, minChars, debounceMs, loadErrorLabel, "aria-label": ariaLabel, id, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, ...rest }: EntityComboboxProps<V, C>): react.JSX.Element;
|
|
72
79
|
|
|
73
80
|
export { ComboClearValue, ComboOption, EntityCombobox, type EntityComboboxProps };
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useComboboxFieldError
|
|
11
11
|
} from "./combobox-core.js";
|
|
12
12
|
import { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
|
|
13
|
+
import { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from "./field-parts.js";
|
|
13
14
|
function EntityCombobox({
|
|
14
15
|
value,
|
|
15
16
|
onChange,
|
|
@@ -30,6 +31,7 @@ function EntityCombobox({
|
|
|
30
31
|
className,
|
|
31
32
|
invalid,
|
|
32
33
|
error,
|
|
34
|
+
hint,
|
|
33
35
|
filter,
|
|
34
36
|
minChars,
|
|
35
37
|
debounceMs,
|
|
@@ -53,10 +55,11 @@ function EntityCombobox({
|
|
|
53
55
|
minChars,
|
|
54
56
|
debounceMs
|
|
55
57
|
});
|
|
58
|
+
const hintParts = useFieldHint(hint, ariaDescribedBy);
|
|
56
59
|
const field = useComboboxFieldError(
|
|
57
60
|
error,
|
|
58
61
|
invalid || ariaInvalid === true || ariaInvalid === "true",
|
|
59
|
-
|
|
62
|
+
hintParts.describedBy
|
|
60
63
|
);
|
|
61
64
|
const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
|
|
62
65
|
search: searchPlaceholder,
|
|
@@ -84,8 +87,8 @@ function EntityCombobox({
|
|
|
84
87
|
// has one. Spread FIRST so the trigger's ARIA and the
|
|
85
88
|
// handlers that open the panel cannot be clobbered from outside.
|
|
86
89
|
/* @__PURE__ */ jsxs("div", { ...rest, className: cn("relative", className), children: [
|
|
87
|
-
label !== void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
|
|
88
|
-
/* @__PURE__ */ jsxs(
|
|
90
|
+
label !== void 0 && hintParts.labelHint === void 0 && /* @__PURE__ */ jsx(FieldLabel, { children: label }),
|
|
91
|
+
/* @__PURE__ */ jsx(EndHintRow, { hint: label === void 0 ? hintParts.labelHint : void 0, children: /* @__PURE__ */ jsxs(
|
|
89
92
|
"button",
|
|
90
93
|
{
|
|
91
94
|
ref: triggerRef,
|
|
@@ -157,7 +160,8 @@ function EntityCombobox({
|
|
|
157
160
|
) : /* @__PURE__ */ jsx(FieldChevron, {})
|
|
158
161
|
]
|
|
159
162
|
}
|
|
160
|
-
),
|
|
163
|
+
) }),
|
|
164
|
+
label !== void 0 && hintParts.labelHint !== void 0 && /* @__PURE__ */ jsx(StaticLabelRow, { hint: hintParts.labelHint, children: /* @__PURE__ */ jsx(FieldLabel, { className: LABEL_IN_ROW, children: label }) }),
|
|
161
165
|
/* @__PURE__ */ jsx(
|
|
162
166
|
ComboboxPanel,
|
|
163
167
|
{
|
|
@@ -178,6 +182,7 @@ function EntityCombobox({
|
|
|
178
182
|
createContent: labels.create(q)
|
|
179
183
|
}
|
|
180
184
|
),
|
|
185
|
+
/* @__PURE__ */ jsx(FieldCaption, { parts: hintParts }),
|
|
181
186
|
field.errorEl
|
|
182
187
|
] })
|
|
183
188
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/entity-combobox.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport {\n ComboboxPanel,\n useComboboxCore,\n useComboboxFieldError,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\n\nexport type { ComboClearValue, ComboOption } from \"./combobox-core\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */\n value: V | C | null | undefined;\n /** Selecting an option emits its value; the clear button emits `clearValue`. */\n onChange: (value: V | C) => void;\n /**\n * What the clear button emits. Default `null`. Pass `\"\"` for a form whose schema\n * wants an empty string for \"no choice\" (a zod `z.string()` field), rather than\n * mapping `null` in every `onChange` — the type of `onChange` follows it, so the\n * mapping cannot be forgotten on one field and not another. The picker also reads\n * the value back as empty, so `value=\"\"` shows the placeholder and no \"×\".\n * See {@link ComboClearValue} for why the choice is closed.\n */\n clearValue?: C;\n /** Already-loaded options (client-side filtered). Also used to resolve the\n * trigger label for the current `value`. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and called on open + as the query changes\n * (at `minChars` and up). Stale responses are ignored; a rejection empties the\n * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label\n * resolution, not as the result list. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag (OR-ed with the internal async state). */\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n disabled?: boolean;\n /** When set, a \"create\" row appears for a non-empty query with no exact match. */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n}\n\n/**\n * An id-keyed, searchable entity picker: a field-styled trigger showing the\n * selected item's label, and a portalled dropdown of `{label, sublabel, icon}`\n * options — loaded up front via `options` or lazily via `loadOptions`. Built on\n * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking\n * several entities use {@link MultiEntityCombobox}.\n */\nexport function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({\n value,\n onChange,\n clearValue = null as C,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n clearable,\n clearLabel,\n closeLabel,\n disabled,\n onCreate,\n createLabel,\n className,\n invalid,\n error,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,\n // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper\n // div the label's `htmlFor` named nothing and the hint described nothing.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: EntityComboboxProps<V, C>) {\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const field = useComboboxFieldError(\n error,\n invalid || ariaInvalid === true || ariaInvalid === \"true\",\n ariaDescribedBy,\n );\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen, rememberOption, query, triggerRef } = core;\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n // The clear value is \"nothing selected\" too, whichever one the caller picked.\n const chosen: V | null = value == null || value === clearValue ? null : (value as V);\n const selectedOption = chosen == null ? null : resolve(chosen);\n const q = query.trim();\n const showCreate =\n Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());\n const showClear = Boolean(clearable && chosen != null && !disabled);\n /** What the closed control is showing — the second half of its accessible name. */\n const triggerText = selectedOption?.label ?? placeholder ?? \"\";\n\n const choose = (o: ComboOption<V>) => {\n rememberOption(o);\n onChange(o.value);\n // Back to the trigger, not to <body>: the panel that held focus is about to\n // unmount, and a keyboard user who just answered this field should be standing\n // on it, ready to Tab to the next one.\n core.closeToTrigger();\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && <FieldLabel>{label}</FieldLabel>}\n <button\n ref={triggerRef}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={\n ariaLabel ??\n (typeof label === \"string\" ? common.fieldValue(label, triggerText) : undefined)\n }\n disabled={disabled}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n aria-required={ariaRequired}\n onClick={() => !disabled && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {selectedOption?.icon && <span className=\"shrink-0\">{selectedOption.icon}</span>}\n <span\n className={cn(\n \"truncate\",\n // A chosen value is the field's VALUE, so it is set in the same ink an\n // <input>'s value is — FIELD_BASE's own text colour, inherited rather\n // than restated (Keksdose dev#477). It used to be one notch lighter\n // than the typeahead fields beside it, which is visible when a picker\n // and a text field share a form row. Nothing selected keeps the\n // placeholder tone (`--text-placeholder`), which every field here\n // agrees on.\n !selectedOption && \"text-[var(--text-placeholder)]\",\n )}\n >\n {selectedOption?.label ?? placeholder ?? \"\"}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could\n never fire; the keys go to the trigger. A pointer shortcut only. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange(clearValue);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => v === value}\n onChoose={choose}\n showCreate={showCreate}\n onCreate={() => {\n onCreate?.(q);\n core.closeToTrigger();\n }}\n createContent={labels.create(q)}\n />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/entity-combobox.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from \"./ui\";\nimport {\n ComboboxPanel,\n useComboboxCore,\n useComboboxFieldError,\n type ComboClearValue,\n type ComboOption,\n} from \"./combobox-core\";\nimport { DEFAULT_COMBOBOX_LABELS, DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { EndHintRow, FieldCaption, LABEL_IN_ROW, StaticLabelRow, useFieldHint } from \"./field-parts\";\n\nexport type { ComboClearValue, ComboOption } from \"./combobox-core\";\n\n/** `onChange` is the kit's — \"a selection was made\", carrying values — rather\n * than the div's form event, so the DOM's spelling of it is omitted. */\nexport interface EntityComboboxProps<V extends string | number, C extends ComboClearValue = null>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\"> {\n /** Selected id; `null`, `undefined` or `clearValue` when nothing is selected. */\n value: V | C | null | undefined;\n /** Selecting an option emits its value; the clear button emits `clearValue`. */\n onChange: (value: V | C) => void;\n /**\n * What the clear button emits. Default `null`. Pass `\"\"` for a form whose schema\n * wants an empty string for \"no choice\" (a zod `z.string()` field), rather than\n * mapping `null` in every `onChange` — the type of `onChange` follows it, so the\n * mapping cannot be forgotten on one field and not another. The picker also reads\n * the value back as empty, so `value=\"\"` shows the placeholder and no \"×\".\n * See {@link ComboClearValue} for why the choice is closed.\n */\n clearValue?: C;\n /** Already-loaded options (client-side filtered). Also used to resolve the\n * trigger label for the current `value`. */\n options?: ComboOption<V>[];\n /** Async option source, debounced and called on open + as the query changes\n * (at `minChars` and up). Stale responses are ignored; a rejection empties the\n * list and says `loadErrorLabel` instead of leaving the last query's rows up. When set, `options` is used only for label\n * resolution, not as the result list. */\n loadOptions?: (query: string) => Promise<ComboOption<V>[]>;\n /** External loading flag (OR-ed with the internal async state). */\n loading?: boolean;\n label?: ReactNode;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyLabel?: string;\n clearable?: boolean;\n clearLabel?: string;\n /** The phone sheet's close button. Its own prop rather than a reuse of\n * `clearLabel`: \"clear the selection\" and \"close this screen\" are different\n * actions, and on a full-screen sheet the close button is the only way out —\n * so it is the one control here that MUST be in the reader's language. */\n closeLabel?: string;\n disabled?: boolean;\n /** When set, a \"create\" row appears for a non-empty query with no exact match. */\n onCreate?: (query: string) => void;\n createLabel?: (query: string) => string;\n /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** What is wrong with the value, as {@link Input}'s `error`: rendered under the\n * field, on the trigger's `aria-describedby`, and implies `invalid`. */\n error?: ReactNode;\n /**\n * Standing advice — {@link Combobox}'s `hint`, so the pickers read like every other\n * field (keksdose K4): plain TEXT is a caption under the field on the trigger's\n * `aria-describedby`, before any error; a {@link FieldHint} \"?\" rides the label line,\n * or with no label sits at the trigger's end edge, outside the box.\n */\n hint?: ReactNode;\n /** Narrow `options` client-side by the query. Default `true`; `false` shows them\n * as given (a server-ranked list). */\n filter?: boolean;\n /** Offer nothing, and call no `loadOptions`, below this many characters. Default\n * `0`, i.e. the list loads as the panel opens. */\n minChars?: number;\n /** `loadOptions` debounce. Default 150 ms. */\n debounceMs?: number;\n /** Shown when `loadOptions` rejects. Default: `combobox.loadError`. */\n loadErrorLabel?: string;\n}\n\n/**\n * An id-keyed, searchable entity picker: a field-styled trigger showing the\n * selected item's label, and a portalled dropdown of `{label, sublabel, icon}`\n * options — loaded up front via `options` or lazily via `loadOptions`. Built on\n * the shared field/anchor/dismiss/search primitives (no cmdk/Radix). For picking\n * several entities use {@link MultiEntityCombobox}.\n */\nexport function EntityCombobox<V extends string | number, C extends ComboClearValue = null>({\n value,\n onChange,\n clearValue = null as C,\n options,\n loadOptions,\n loading,\n label,\n placeholder,\n searchPlaceholder,\n emptyLabel,\n clearable,\n clearLabel,\n closeLabel,\n disabled,\n onCreate,\n createLabel,\n className,\n invalid,\n error,\n hint,\n filter,\n minChars,\n debounceMs,\n loadErrorLabel,\n \"aria-label\": ariaLabel,\n // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than\n // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,\n // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper\n // div the label's `htmlFor` named nothing and the hint described nothing.\n id,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n}: EntityComboboxProps<V, C>) {\n const core = useComboboxCore<V>({\n options,\n loadOptions,\n loading,\n filter,\n minChars,\n debounceMs,\n });\n const hintParts = useFieldHint(hint, ariaDescribedBy);\n const field = useComboboxFieldError(\n error,\n invalid || ariaInvalid === true || ariaInvalid === \"true\",\n hintParts.describedBy,\n );\n // The props are the per-instance overrides, the provider the app-wide ones; a\n // prop left `undefined` falls through to the provider rather than masking it.\n const labels = useKitLabels(\"combobox\", DEFAULT_COMBOBOX_LABELS, {\n search: searchPlaceholder,\n noResults: emptyLabel,\n clear: clearLabel,\n create: createLabel,\n });\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const { open, results, resolve, setOpen, rememberOption, query, triggerRef } = core;\n // One id per instance, generated here rather than in the core: `aria-controls` on\n // the trigger has to name the list while the list is still closed, so the id\n // belongs to whoever renders both ends of it.\n const listboxId = useId();\n\n // The clear value is \"nothing selected\" too, whichever one the caller picked.\n const chosen: V | null = value == null || value === clearValue ? null : (value as V);\n const selectedOption = chosen == null ? null : resolve(chosen);\n const q = query.trim();\n const showCreate =\n Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());\n const showClear = Boolean(clearable && chosen != null && !disabled);\n /** What the closed control is showing — the second half of its accessible name. */\n const triggerText = selectedOption?.label ?? placeholder ?? \"\";\n\n const choose = (o: ComboOption<V>) => {\n rememberOption(o);\n onChange(o.value);\n // Back to the trigger, not to <body>: the panel that held focus is about to\n // unmount, and a keyboard user who just answered this field should be standing\n // on it, ready to Tab to the next one.\n core.closeToTrigger();\n };\n\n return (\n // `rest` dresses the wrapper, which has no role; the accessible NAME — and the\n // id, description, invalid and required state above — go on the trigger, which\n // has one. Spread FIRST so the trigger's ARIA and the\n // handlers that open the panel cannot be clobbered from outside.\n <div {...rest} className={cn(\"relative\", className)}>\n {label !== undefined && hintParts.labelHint === undefined && <FieldLabel>{label}</FieldLabel>}\n <EndHintRow hint={label === undefined ? hintParts.labelHint : undefined}>\n <button\n ref={triggerRef}\n id={id}\n type=\"button\"\n // A combobox, not a button. The distinction is not pedantry: this control\n // carried `aria-invalid`, which `button` does not support, so a required\n // field left empty painted a rose border and told a reader nothing at all —\n // and ESLint flagged it as exactly that (`role-supports-aria-props`). The\n // fix the audit asked for is the role that describes what this IS: a closed\n // choice that expands into the list named below. `combobox` supports\n // `aria-invalid`, so the border and the announcement finally agree.\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listboxId}\n aria-haspopup=\"listbox\"\n // The label is a floating <span>, not a <label for>, so without this the\n // trigger's accessible name is whatever value happens to be selected —\n // \"Checking\" with nothing saying it is the account. The label AND the\n // value, because `aria-label` replaces the content rather than adding to\n // it, and a control that announces only its name has lost the answer.\n // A caller's own name wins over the composition: two fields labelled\n // \"Account\" on a transfer form are the from and the to, and only the\n // caller knows which is which.\n aria-label={\n ariaLabel ??\n (typeof label === \"string\" ? common.fieldValue(label, triggerText) : undefined)\n }\n disabled={disabled}\n aria-invalid={field.isInvalid || undefined}\n aria-describedby={field.describedBy}\n aria-required={ariaRequired}\n onClick={() => !disabled && setOpen((o) => !o)}\n // Down/Up opens the list from the closed trigger, per the APG. Enter and\n // Space already do it through the button's own click.\n onKeyDown={(e) => {\n if (disabled) return;\n if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n setOpen(true);\n }\n }}\n className={cn(\n FIELD_TRIGGER,\n \"pe-9\",\n label !== undefined && FIELD_FLOATING_PAD,\n disabled && \"cursor-not-allowed opacity-50\",\n field.isInvalid && FIELD_INVALID,\n )}\n >\n <span className=\"flex min-w-0 items-center gap-2\">\n {selectedOption?.icon && <span className=\"shrink-0\">{selectedOption.icon}</span>}\n <span\n className={cn(\n \"truncate\",\n // A chosen value is the field's VALUE, so it is set in the same ink an\n // <input>'s value is — FIELD_BASE's own text colour, inherited rather\n // than restated (Keksdose dev#477). It used to be one notch lighter\n // than the typeahead fields beside it, which is visible when a picker\n // and a text field share a form row. Nothing selected keeps the\n // placeholder tone (`--text-placeholder`), which every field here\n // agrees on.\n !selectedOption && \"text-[var(--text-placeholder)]\",\n )}\n >\n {selectedOption?.label ?? placeholder ?? \"\"}\n </span>\n </span>\n {showClear ? (\n /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside\n the trigger <button>, so it never holds focus and a key handler here could\n never fire; the keys go to the trigger. A pointer shortcut only. */\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label={labels.clear}\n onClick={(e) => {\n e.stopPropagation();\n onChange(clearValue);\n }}\n className=\"absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </span>\n ) : (\n <FieldChevron />\n )}\n </button>\n </EndHintRow>\n {/* With a \"?\" the label shares the top strip with it — after the trigger, so the\n \"?\" follows the control in the tab order, as on Input and Select. */}\n {label !== undefined && hintParts.labelHint !== undefined && (\n <StaticLabelRow hint={hintParts.labelHint}>\n <FieldLabel className={LABEL_IN_ROW}>{label}</FieldLabel>\n </StaticLabelRow>\n )}\n <ComboboxPanel\n core={core}\n listboxId={listboxId}\n // On a phone the panel becomes a full-screen sheet, which needs the field's\n // own label to say what it is asking for (live #200).\n sheetTitle={label ?? placeholder}\n searchPlaceholder={labels.search}\n emptyLabel={labels.noResults}\n closeLabel={closeLabel}\n loadErrorLabel={loadErrorLabel}\n isSelected={(v) => v === value}\n onChoose={choose}\n showCreate={showCreate}\n onCreate={() => {\n onCreate?.(q);\n core.closeToTrigger();\n }}\n createContent={labels.create(q)}\n />\n <FieldCaption parts={hintParts} />\n {field.errorEl}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,8 +6,10 @@ import './series-chart-labels.js';
|
|
|
6
6
|
import './toggle-legend.js';
|
|
7
7
|
import './series-chart-ticks.js';
|
|
8
8
|
import './series-chart-budget.js';
|
|
9
|
-
import '../kit-labels-
|
|
10
|
-
import '
|
|
9
|
+
import '../kit-labels-R8mIAc9N.js';
|
|
10
|
+
import '../data-table-labels-B7OdnM0S.js';
|
|
11
|
+
import './data-table-sort.js';
|
|
12
|
+
import './swipeable-row.js';
|
|
11
13
|
import './mini-calendar.js';
|
|
12
14
|
import './calendar-heatmap.js';
|
|
13
15
|
import './popover.js';
|
|
@@ -29,7 +31,6 @@ import './sparkline.js';
|
|
|
29
31
|
import './stat-tile.js';
|
|
30
32
|
import './signature-pad.js';
|
|
31
33
|
import './password-strength.js';
|
|
32
|
-
import './danger-confirm.js';
|
|
33
34
|
import './swatch-picker.js';
|
|
34
35
|
import './tile-radio.js';
|
|
35
36
|
import './icon-picker.js';
|
|
@@ -37,11 +38,10 @@ import './dialog-frame.js';
|
|
|
37
38
|
import './modal.js';
|
|
38
39
|
import '../hooks/use-search-param-state.js';
|
|
39
40
|
import './measured-grid.js';
|
|
40
|
-
import '../feedback-attachment-
|
|
41
|
+
import '../feedback-attachment-NhUm7Zga.js';
|
|
41
42
|
import '../feedback/feedback-thread.js';
|
|
42
43
|
import '../feedback/feedback-inbox.js';
|
|
43
44
|
import './account-settings-labels.js';
|
|
44
|
-
import './confirm-dialog.js';
|
|
45
45
|
import './floating-panel.js';
|
|
46
46
|
import './bulk-action-bar.js';
|
|
47
47
|
import './list.js';
|
|
@@ -60,14 +60,16 @@ import './write-lock.js';
|
|
|
60
60
|
import './legal.js';
|
|
61
61
|
import './account-chips.js';
|
|
62
62
|
import '../lib/format.js';
|
|
63
|
-
import '../data-table-3mi2R3so.js';
|
|
64
|
-
import './data-table-sort.js';
|
|
65
|
-
import './swipeable-row.js';
|
|
66
63
|
import './reauth-dialog.js';
|
|
67
64
|
import './server-wake.js';
|
|
68
65
|
import '../lib/server-wake.js';
|
|
69
66
|
import './translation-review-labels.js';
|
|
70
67
|
import '../lib/translation-review.js';
|
|
68
|
+
import './country-select.js';
|
|
69
|
+
import './inline-edit-field.js';
|
|
70
|
+
import '../lib/iban.js';
|
|
71
|
+
import '../lib/phone.js';
|
|
72
|
+
import './sign-chip.js';
|
|
71
73
|
|
|
72
74
|
/** Which chart of a facing pair. Physical, like the axes: charts do not mirror in RTL. */
|
|
73
75
|
type FacingSide = "left" | "right";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** `undefined`, `null`, `false` and `""` are what a caller's `cond && "…"` evaluates to
|
|
5
|
+
* when there is nothing to say; none of them is content. A numeric `0` is. */
|
|
6
|
+
declare function hasContent(node: ReactNode): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* A `hint` that is plain TEXT is a caption under the field; anything else (a
|
|
9
|
+
* `FieldHint` "?") rides the label line — the rule `Select` set in 0.15: the label
|
|
10
|
+
* line is 11px of strip shared with the label, and a sentence placed there was set on
|
|
11
|
+
* top of both the label and the value.
|
|
12
|
+
*/
|
|
13
|
+
declare function isTextHint(hint: ReactNode): boolean;
|
|
14
|
+
interface FieldMessages {
|
|
15
|
+
/** `invalid`, a message, or an `aria-invalid` from outside (a form library). */
|
|
16
|
+
isInvalid: boolean;
|
|
17
|
+
/** The caller's description, then the caption, then the error — the standing
|
|
18
|
+
* advice before the news, as `useFieldError` in ui.tsx orders them. */
|
|
19
|
+
describedBy: string | undefined;
|
|
20
|
+
/** The caption and the error, for under the field; `null` when there is neither. */
|
|
21
|
+
below: ReactNode;
|
|
22
|
+
/** The hint for the LABEL line (a `FieldHint`), or `undefined` when the hint is a
|
|
23
|
+
* caption or there is none. */
|
|
24
|
+
labelHint: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
/** `hint` / `error` / `invalid` resolved the way `Select` resolves them. */
|
|
27
|
+
declare function useFieldMessages({ hint, error, invalid, describedBy, ariaInvalid, }: {
|
|
28
|
+
hint?: ReactNode;
|
|
29
|
+
error?: ReactNode;
|
|
30
|
+
invalid?: boolean;
|
|
31
|
+
describedBy?: string;
|
|
32
|
+
ariaInvalid?: unknown;
|
|
33
|
+
}): FieldMessages;
|
|
34
|
+
/**
|
|
35
|
+
* The field and what is under it, as one box — `FieldGroup` in ui.tsx, for the same
|
|
36
|
+
* reasons written there. Nothing of its own while there is nothing under the field and
|
|
37
|
+
* the caller passed no `error` key (the DOM is exactly what it was before these props);
|
|
38
|
+
* once `error` is passed the box is kept even with no message, so the trigger is never
|
|
39
|
+
* reparented — and remounted, losing focus — as the message comes and goes.
|
|
40
|
+
*/
|
|
41
|
+
declare function FieldBox({ below, reserve, children, }: {
|
|
42
|
+
below: ReactNode;
|
|
43
|
+
reserve: boolean;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}): react.JSX.Element;
|
|
46
|
+
/**
|
|
47
|
+
* The static label in a trigger field's top strip — a {@link FieldLabel}, or, with a
|
|
48
|
+
* `FieldHint`, the label and its "?" laid out on ONE flex row (dev#468: two absolute
|
|
49
|
+
* offsets guessing at the same baseline put the "?" three pixels above its word).
|
|
50
|
+
* `FloatingField`'s static-label row, except that it stops at `end-9`: the date and
|
|
51
|
+
* month triggers keep that column (`pe-9`) for the calendar glyph or the clear ×, and a
|
|
52
|
+
* long label truncates before it rather than putting the "?" on top of it.
|
|
53
|
+
*/
|
|
54
|
+
declare function FieldLabelLine({ label, hint }: {
|
|
55
|
+
label: ReactNode;
|
|
56
|
+
hint?: ReactNode;
|
|
57
|
+
}): react.JSX.Element;
|
|
58
|
+
/**
|
|
59
|
+
* How tall the strip over a bare group is.
|
|
60
|
+
*
|
|
61
|
+
* - `"toggle"` — 16px. keksdose's `ClearedStatusPicker` (live #288 rework, #431,
|
|
62
|
+
* measured): *"16px of label strip plus a 26px group is exactly a labelled Input"*,
|
|
63
|
+
* so a status group in a row of 42px fields ends where they end. The label's line
|
|
64
|
+
* box runs into the group's top hairline, but the ACTIVE fill — the thing feedback
|
|
65
|
+
* #117 was about — starts at 19px, below the label's 17.75px.
|
|
66
|
+
* - `"tiles"` — 20px, keksdose's `FlagPicker` and location field (`pt-5`). A tile has
|
|
67
|
+
* no inset fill: its selection IS its frame, a 2px dark border-and-ring from the
|
|
68
|
+
* tile's top edge, so the tile has to start below the whole label line.
|
|
69
|
+
*/
|
|
70
|
+
declare const LABEL_STRIP_PAD: {
|
|
71
|
+
readonly toggle: "pt-4";
|
|
72
|
+
readonly tiles: "pt-5";
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The 11px label (and its `FieldHint`) over a group that is NOT a field — keksdose K2:
|
|
76
|
+
* a segmented control or a row of swatches standing in a form row beside labelled
|
|
77
|
+
* inputs, whose label must sit on the same line as theirs (`top-1`, `inset-x-3`, the
|
|
78
|
+
* type of a field's static label) without the group growing a field's border round
|
|
79
|
+
* it. `id` goes on the label so the group can be `aria-labelledby` it.
|
|
80
|
+
*
|
|
81
|
+
* `relative` is load-bearing twice over: the row is positioned against it, and a
|
|
82
|
+
* caller's `sr-only` label cannot escape it (sr-only-containment).
|
|
83
|
+
*/
|
|
84
|
+
declare function LabelStrip({ labelId, label, hint, disabled, pad, className, children, }: {
|
|
85
|
+
labelId: string;
|
|
86
|
+
label: ReactNode;
|
|
87
|
+
hint?: ReactNode;
|
|
88
|
+
/** Dim the label with its group, as `peer-disabled:` dims a field's own. */
|
|
89
|
+
disabled?: boolean;
|
|
90
|
+
pad: keyof typeof LABEL_STRIP_PAD;
|
|
91
|
+
className?: string;
|
|
92
|
+
children: ReactNode;
|
|
93
|
+
}): react.JSX.Element;
|
|
94
|
+
|
|
95
|
+
export { FieldBox, FieldLabelLine, type FieldMessages, LABEL_STRIP_PAD, LabelStrip, hasContent, isTextHint, useFieldMessages };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { FieldLabel } from "./ui.js";
|
|
6
|
+
function hasContent(node) {
|
|
7
|
+
return node !== void 0 && node !== null && node !== false && node !== "";
|
|
8
|
+
}
|
|
9
|
+
function isTextHint(hint) {
|
|
10
|
+
return typeof hint === "string" && hint !== "" || typeof hint === "number";
|
|
11
|
+
}
|
|
12
|
+
const FIELD_CAPTION_CLASS = "mt-1 text-[11px] leading-tight text-[var(--text-muted)]";
|
|
13
|
+
const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
14
|
+
function useFieldMessages({
|
|
15
|
+
hint,
|
|
16
|
+
error,
|
|
17
|
+
invalid,
|
|
18
|
+
describedBy,
|
|
19
|
+
ariaInvalid
|
|
20
|
+
}) {
|
|
21
|
+
const hintId = useId();
|
|
22
|
+
const errorId = useId();
|
|
23
|
+
const textHint = isTextHint(hint);
|
|
24
|
+
const hasError = hasContent(error);
|
|
25
|
+
return {
|
|
26
|
+
isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === "true",
|
|
27
|
+
describedBy: [describedBy, textHint && hintId, hasError && errorId].filter(Boolean).join(" ") || void 0,
|
|
28
|
+
below: textHint || hasError ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
|
+
textHint && /* @__PURE__ */ jsx("p", { id: hintId, className: FIELD_CAPTION_CLASS, children: hint }),
|
|
30
|
+
hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
|
|
31
|
+
] }) : null,
|
|
32
|
+
labelHint: !textHint && hasContent(hint) ? hint : void 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function FieldBox({
|
|
36
|
+
below,
|
|
37
|
+
reserve,
|
|
38
|
+
children
|
|
39
|
+
}) {
|
|
40
|
+
if (below === null && !reserve) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
41
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
42
|
+
children,
|
|
43
|
+
below
|
|
44
|
+
] });
|
|
45
|
+
}
|
|
46
|
+
function FieldLabelLine({ label, hint }) {
|
|
47
|
+
if (!hasContent(hint)) return /* @__PURE__ */ jsx(FieldLabel, { children: label });
|
|
48
|
+
return (
|
|
49
|
+
// `z-10` as FieldLabel has it: the trigger after it is `relative` and would paint
|
|
50
|
+
// over the row (and take the "?"'s hover) otherwise.
|
|
51
|
+
/* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1", children: [
|
|
52
|
+
/* @__PURE__ */ jsx(FieldLabel, { className: "static min-w-0 max-w-full", children: label }),
|
|
53
|
+
/* @__PURE__ */ jsx("span", { className: "pointer-events-auto flex shrink-0 items-center", children: hint })
|
|
54
|
+
] })
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const LABEL_STRIP_PAD = { toggle: "pt-4", tiles: "pt-5" };
|
|
58
|
+
function LabelStrip({
|
|
59
|
+
labelId,
|
|
60
|
+
label,
|
|
61
|
+
hint,
|
|
62
|
+
disabled,
|
|
63
|
+
pad,
|
|
64
|
+
className,
|
|
65
|
+
children
|
|
66
|
+
}) {
|
|
67
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("relative min-w-0", LABEL_STRIP_PAD[pad], className), children: [
|
|
68
|
+
/* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1", children: [
|
|
69
|
+
/* @__PURE__ */ jsx(FieldLabel, { id: labelId, className: cn("static min-w-0 max-w-full", disabled && "opacity-50"), children: label }),
|
|
70
|
+
hasContent(hint) && /* @__PURE__ */ jsx("span", { className: "pointer-events-auto flex shrink-0 items-center", children: hint })
|
|
71
|
+
] }),
|
|
72
|
+
children
|
|
73
|
+
] });
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
FieldBox,
|
|
77
|
+
FieldLabelLine,
|
|
78
|
+
LABEL_STRIP_PAD,
|
|
79
|
+
LabelStrip,
|
|
80
|
+
hasContent,
|
|
81
|
+
isTextHint,
|
|
82
|
+
useFieldMessages
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=field-anatomy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/field-anatomy.tsx"],"sourcesContent":["// Internal — not re-exported from the barrel. The pieces of a field's anatomy (its\n// label line, the caption and the error under it, the label STRIP over a bare group)\n// for the controls that are not built on `FloatingField`: the date and month pickers,\n// whose trigger is a button in a wrapper of their own, and the groups (ToggleGroup,\n// SwatchPicker, IconPicker), which have no field box at all.\n//\n// `Input` and `Select` keep their own copies in ui.tsx; these follow them class for\n// class, so a DatePicker's caption and a Select's caption are the same 11px line in\n// the same place (keksdose K4: \"uniform field anatomy … today it is a patchwork\").\n\nimport { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldLabel } from \"./ui\";\n\n/** `undefined`, `null`, `false` and `\"\"` are what a caller's `cond && \"…\"` evaluates to\n * when there is nothing to say; none of them is content. A numeric `0` is. */\nexport function hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/**\n * A `hint` that is plain TEXT is a caption under the field; anything else (a\n * `FieldHint` \"?\") rides the label line — the rule `Select` set in 0.15: the label\n * line is 11px of strip shared with the label, and a sentence placed there was set on\n * top of both the label and the value.\n */\nexport function isTextHint(hint: ReactNode): boolean {\n return (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n}\n\nconst FIELD_CAPTION_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\";\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nexport interface FieldMessages {\n /** `invalid`, a message, or an `aria-invalid` from outside (a form library). */\n isInvalid: boolean;\n /** The caller's description, then the caption, then the error — the standing\n * advice before the news, as `useFieldError` in ui.tsx orders them. */\n describedBy: string | undefined;\n /** The caption and the error, for under the field; `null` when there is neither. */\n below: ReactNode;\n /** The hint for the LABEL line (a `FieldHint`), or `undefined` when the hint is a\n * caption or there is none. */\n labelHint: ReactNode;\n}\n\n/** `hint` / `error` / `invalid` resolved the way `Select` resolves them. */\nexport function useFieldMessages({\n hint,\n error,\n invalid,\n describedBy,\n ariaInvalid,\n}: {\n hint?: ReactNode;\n error?: ReactNode;\n invalid?: boolean;\n describedBy?: string;\n ariaInvalid?: unknown;\n}): FieldMessages {\n const hintId = useId();\n const errorId = useId();\n const textHint = isTextHint(hint);\n const hasError = hasContent(error);\n return {\n isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === \"true\",\n describedBy:\n [describedBy, textHint && hintId, hasError && errorId].filter(Boolean).join(\" \") || undefined,\n below:\n textHint || hasError ? (\n <>\n {textHint && (\n <p id={hintId} className={FIELD_CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </>\n ) : null,\n labelHint: !textHint && hasContent(hint) ? hint : undefined,\n };\n}\n\n/**\n * The field and what is under it, as one box — `FieldGroup` in ui.tsx, for the same\n * reasons written there. Nothing of its own while there is nothing under the field and\n * the caller passed no `error` key (the DOM is exactly what it was before these props);\n * once `error` is passed the box is kept even with no message, so the trigger is never\n * reparented — and remounted, losing focus — as the message comes and goes.\n */\nexport function FieldBox({\n below,\n reserve,\n children,\n}: {\n below: ReactNode;\n reserve: boolean;\n children: ReactNode;\n}) {\n if (below === null && !reserve) return <>{children}</>;\n return (\n <div>\n {children}\n {below}\n </div>\n );\n}\n\n/**\n * The static label in a trigger field's top strip — a {@link FieldLabel}, or, with a\n * `FieldHint`, the label and its \"?\" laid out on ONE flex row (dev#468: two absolute\n * offsets guessing at the same baseline put the \"?\" three pixels above its word).\n * `FloatingField`'s static-label row, except that it stops at `end-9`: the date and\n * month triggers keep that column (`pe-9`) for the calendar glyph or the clear ×, and a\n * long label truncates before it rather than putting the \"?\" on top of it.\n */\nexport function FieldLabelLine({ label, hint }: { label: ReactNode; hint?: ReactNode }) {\n if (!hasContent(hint)) return <FieldLabel>{label}</FieldLabel>;\n return (\n // `z-10` as FieldLabel has it: the trigger after it is `relative` and would paint\n // over the row (and take the \"?\"'s hover) otherwise.\n <div className=\"pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1\">\n <FieldLabel className=\"static min-w-0 max-w-full\">{label}</FieldLabel>\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n}\n\n/**\n * How tall the strip over a bare group is.\n *\n * - `\"toggle\"` — 16px. keksdose's `ClearedStatusPicker` (live #288 rework, #431,\n * measured): *\"16px of label strip plus a 26px group is exactly a labelled Input\"*,\n * so a status group in a row of 42px fields ends where they end. The label's line\n * box runs into the group's top hairline, but the ACTIVE fill — the thing feedback\n * #117 was about — starts at 19px, below the label's 17.75px.\n * - `\"tiles\"` — 20px, keksdose's `FlagPicker` and location field (`pt-5`). A tile has\n * no inset fill: its selection IS its frame, a 2px dark border-and-ring from the\n * tile's top edge, so the tile has to start below the whole label line.\n */\nexport const LABEL_STRIP_PAD = { toggle: \"pt-4\", tiles: \"pt-5\" } as const;\n\n/**\n * The 11px label (and its `FieldHint`) over a group that is NOT a field — keksdose K2:\n * a segmented control or a row of swatches standing in a form row beside labelled\n * inputs, whose label must sit on the same line as theirs (`top-1`, `inset-x-3`, the\n * type of a field's static label) without the group growing a field's border round\n * it. `id` goes on the label so the group can be `aria-labelledby` it.\n *\n * `relative` is load-bearing twice over: the row is positioned against it, and a\n * caller's `sr-only` label cannot escape it (sr-only-containment).\n */\nexport function LabelStrip({\n labelId,\n label,\n hint,\n disabled,\n pad,\n className,\n children,\n}: {\n labelId: string;\n label: ReactNode;\n hint?: ReactNode;\n /** Dim the label with its group, as `peer-disabled:` dims a field's own. */\n disabled?: boolean;\n pad: keyof typeof LABEL_STRIP_PAD;\n className?: string;\n children: ReactNode;\n}) {\n return (\n <div className={cn(\"relative min-w-0\", LABEL_STRIP_PAD[pad], className)}>\n <div className=\"pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1\">\n {/* `static`: FieldLabel positions itself absolutely; here the ROW is\n positioned, so the label and its \"?\" are centred on one line by the flex\n layout (dev#468). */}\n <FieldLabel id={labelId} className={cn(\"static min-w-0 max-w-full\", disabled && \"opacity-50\")}>\n {label}\n </FieldLabel>\n {hasContent(hint) && <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>}\n </div>\n {children}\n </div>\n );\n}\n"],"mappings":";AAuEQ,mBAEI,KAFJ;AA7DR,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAIpB,SAAS,WAAW,MAA0B;AACnD,SAAO,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAC3E;AAQO,SAAS,WAAW,MAA0B;AACnD,SAAQ,OAAO,SAAS,YAAY,SAAS,MAAO,OAAO,SAAS;AACtE;AAEA,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAgBnB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMkB;AAChB,QAAM,SAAS,MAAM;AACrB,QAAM,UAAU,MAAM;AACtB,QAAM,WAAW,WAAW,IAAI;AAChC,QAAM,WAAW,WAAW,KAAK;AACjC,SAAO;AAAA,IACL,WAAW,QAAQ,OAAO,KAAK,YAAY,gBAAgB,QAAQ,gBAAgB;AAAA,IACnF,aACE,CAAC,aAAa,YAAY,QAAQ,YAAY,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAAA,IACtF,OACE,YAAY,WACV,iCACG;AAAA,kBACC,oBAAC,OAAE,IAAI,QAAQ,WAAW,qBACvB,gBACH;AAAA,MAED,YACC,oBAAC,OAAE,IAAI,SAAS,WAAW,mBACxB,iBACH;AAAA,OAEJ,IACE;AAAA,IACN,WAAW,CAAC,YAAY,WAAW,IAAI,IAAI,OAAO;AAAA,EACpD;AACF;AASO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,UAAU,QAAQ,CAAC,QAAS,QAAO,gCAAG,UAAS;AACnD,SACE,qBAAC,SACE;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;AAUO,SAAS,eAAe,EAAE,OAAO,KAAK,GAA2C;AACtF,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,oBAAC,cAAY,iBAAM;AACjD;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,WAAU,iFACb;AAAA,0BAAC,cAAW,WAAU,6BAA6B,iBAAM;AAAA,MACzD,oBAAC,UAAK,WAAU,kDAAkD,gBAAK;AAAA,OACzE;AAAA;AAEJ;AAcO,MAAM,kBAAkB,EAAE,QAAQ,QAAQ,OAAO,OAAO;AAYxD,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD,SACE,qBAAC,SAAI,WAAW,GAAG,oBAAoB,gBAAgB,GAAG,GAAG,SAAS,GACpE;AAAA,yBAAC,SAAI,WAAU,wEAIb;AAAA,0BAAC,cAAW,IAAI,SAAS,WAAW,GAAG,6BAA6B,YAAY,YAAY,GACzF,iBACH;AAAA,MACC,WAAW,IAAI,KAAK,oBAAC,UAAK,WAAU,kDAAkD,gBAAK;AAAA,OAC9F;AAAA,IACC;AAAA,KACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Small pieces the fields share for the 0.22.0 anatomy round (keksdose K3/K4, kastlan
|
|
6
|
+
* 8). Private to the package — not re-exported from the barrel — like `choice-parts`.
|
|
7
|
+
*
|
|
8
|
+
* Nothing here imports `ui.tsx`: `ui.tsx` imports this, and so do the combobox family
|
|
9
|
+
* and the native choice controls, which is the point of keeping the rules in one place.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Whether a `hint` is a CAPTION (plain text: a non-empty string or a number) or
|
|
13
|
+
* something for the label line (a {@link FieldHint} "?", or any other node).
|
|
14
|
+
*
|
|
15
|
+
* The rule {@link Select} and `NumberInput` already keep: the label line is 11px of
|
|
16
|
+
* strip shared with the label, and a sentence placed there ran over the label and into
|
|
17
|
+
* the value; a "?" under the field would be a button floating free of the label it
|
|
18
|
+
* explains.
|
|
19
|
+
*/
|
|
20
|
+
declare function isTextHint(hint: ReactNode): hint is string | number;
|
|
21
|
+
/** The caption under a field — Select's and NumberInput's type, so every field's
|
|
22
|
+
* standing advice reads alike. */
|
|
23
|
+
declare const FIELD_CAPTION_CLASS = "mt-1 text-[11px] leading-tight text-[var(--text-muted)]";
|
|
24
|
+
interface FieldHintParts {
|
|
25
|
+
/** The caller's `aria-describedby` with the caption's id appended — standing
|
|
26
|
+
* advice first; the error (the news) is appended after this by the field. */
|
|
27
|
+
describedBy: string | undefined;
|
|
28
|
+
/** The caption's id while there is one. */
|
|
29
|
+
captionId: string | undefined;
|
|
30
|
+
/** The caption itself, or `undefined` for a hint that is not text. */
|
|
31
|
+
captionText: string | number | undefined;
|
|
32
|
+
/** A non-text hint (a FieldHint) for the label line — or, on a field with no label
|
|
33
|
+
* line, the end edge outside the box. `undefined` when there is none. */
|
|
34
|
+
labelHint: ReactNode | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Split a field's `hint` into the caption under it (attached with `aria-describedby`)
|
|
38
|
+
* or the "?" on its label line. See {@link isTextHint}.
|
|
39
|
+
*/
|
|
40
|
+
declare function useFieldHint(hint: ReactNode, describedBy: string | undefined): FieldHintParts;
|
|
41
|
+
/** The caption under a field, as {@link Select} draws its own. */
|
|
42
|
+
declare function FieldCaption({ parts, className }: {
|
|
43
|
+
parts: FieldHintParts;
|
|
44
|
+
className?: string;
|
|
45
|
+
}): react.JSX.Element | null;
|
|
46
|
+
/**
|
|
47
|
+
* Classes that take a static floating label (`FLOATING_LABEL_STATIC`: `FieldLabel`,
|
|
48
|
+
* `ComboboxFieldLabel`) out of its own absolute slot and into a {@link StaticLabelRow}.
|
|
49
|
+
* `static` wins over `absolute` through `cn`'s merge; `min-w-0` lets it truncate in
|
|
50
|
+
* the row.
|
|
51
|
+
*/
|
|
52
|
+
declare const LABEL_IN_ROW = "static min-w-0";
|
|
53
|
+
/**
|
|
54
|
+
* A static label and its "?" on one line, in the top strip of a field that lays out its
|
|
55
|
+
* own label — the combobox family, which cannot use {@link FloatingField} because the
|
|
56
|
+
* trigger, the anchored panel and the label hang off a box of their own.
|
|
57
|
+
*
|
|
58
|
+
* FloatingField's static row, with one difference: it stops at `end-9`, the column the
|
|
59
|
+
* chevron (or the clear "×") takes, so a long label truncates before it rather than
|
|
60
|
+
* putting the "?" on top of the chevron. The hint keeps its width; the label gives way.
|
|
61
|
+
*/
|
|
62
|
+
declare function StaticLabelRow({ hint, children }: {
|
|
63
|
+
hint: ReactNode;
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
}): react.JSX.Element;
|
|
66
|
+
/**
|
|
67
|
+
* A field with no label line and a FieldHint "?": the hint at the end edge, OUTSIDE the
|
|
68
|
+
* box, as an unlabelled {@link Select} has placed it since 0.15.5. Without a hint the
|
|
69
|
+
* field is returned exactly as it was, so the wrapper costs nothing where it does not
|
|
70
|
+
* apply.
|
|
71
|
+
*/
|
|
72
|
+
declare function EndHintRow({ hint, className, children, }: {
|
|
73
|
+
hint: ReactNode | undefined;
|
|
74
|
+
className?: string;
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
}): react.JSX.Element;
|
|
77
|
+
interface LockReason {
|
|
78
|
+
/** The reason in force — the lock's (with `commit` under a locked
|
|
79
|
+
* `WriteLockProvider`), else the control's own `disabledReason`. */
|
|
80
|
+
reason: ReactNode;
|
|
81
|
+
/** There is a reason: the control is locked the focusable way. */
|
|
82
|
+
locked: boolean;
|
|
83
|
+
/** The id of the reason's hidden copy, for the control's `aria-describedby`. */
|
|
84
|
+
reasonId: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* {@link Button}'s `commit` + `disabledReason`, for a control that IS its own commit — a
|
|
88
|
+
* switch, a checkbox or a select that saves on change, a list row whose click writes.
|
|
89
|
+
* The lock's reason wins over the control's own (`useCommitReason`).
|
|
90
|
+
*/
|
|
91
|
+
declare function useLockReason(commit: boolean | undefined, disabledReason: ReactNode): LockReason;
|
|
92
|
+
/**
|
|
93
|
+
* The kit {@link Tooltip} around a locked control, with a `hidden` copy of the reason for
|
|
94
|
+
* the control's `aria-describedby` — Button's anatomy. The control goes in a FRAGMENT so
|
|
95
|
+
* the Tooltip does not also clone its bubble's id into the description: the hidden copy
|
|
96
|
+
* already describes it, and the same sentence twice would be read on every focus.
|
|
97
|
+
*
|
|
98
|
+
* `className` is the wrapper's, which becomes the outermost element while locked — a
|
|
99
|
+
* control moves its own layout classes here so a lock does not reflow the form.
|
|
100
|
+
*/
|
|
101
|
+
declare function LockedReason({ lock, className, children, }: {
|
|
102
|
+
lock: LockReason;
|
|
103
|
+
className?: string;
|
|
104
|
+
children: ReactNode;
|
|
105
|
+
}): react.JSX.Element;
|
|
106
|
+
/**
|
|
107
|
+
* Set by {@link CheckboxGroup}: its legend carries the one required star, so a box
|
|
108
|
+
* inside it keeps the native `required` (a form still refuses an empty set) but draws no
|
|
109
|
+
* star of its own — four stars on four languages would read as four separate demands.
|
|
110
|
+
* ChoiceCardGroup's `InGroupContext`, for the plain checkbox.
|
|
111
|
+
*/
|
|
112
|
+
declare const RequiredStarOnLegend: react.Context<boolean>;
|
|
113
|
+
|
|
114
|
+
export { EndHintRow, FIELD_CAPTION_CLASS, FieldCaption, type FieldHintParts, LABEL_IN_ROW, type LockReason, LockedReason, RequiredStarOnLegend, StaticLabelRow, isTextHint, useFieldHint, useLockReason };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useId } from "react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Tooltip } from "./tooltip.js";
|
|
6
|
+
import { useCommitReason } from "./write-lock.js";
|
|
7
|
+
import { hasMessage, mergeDescribedBy } from "./choice-parts.js";
|
|
8
|
+
function isTextHint(hint) {
|
|
9
|
+
return typeof hint === "string" && hint !== "" || typeof hint === "number";
|
|
10
|
+
}
|
|
11
|
+
const FIELD_CAPTION_CLASS = "mt-1 text-[11px] leading-tight text-[var(--text-muted)]";
|
|
12
|
+
function useFieldHint(hint, describedBy) {
|
|
13
|
+
const captionId = useId();
|
|
14
|
+
const text = isTextHint(hint);
|
|
15
|
+
return {
|
|
16
|
+
describedBy: text ? mergeDescribedBy(describedBy, captionId) : describedBy,
|
|
17
|
+
captionId: text ? captionId : void 0,
|
|
18
|
+
captionText: text ? hint : void 0,
|
|
19
|
+
labelHint: !text && hasMessage(hint) ? hint : void 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function FieldCaption({ parts, className }) {
|
|
23
|
+
if (parts.captionText === void 0) return null;
|
|
24
|
+
return /* @__PURE__ */ jsx("p", { id: parts.captionId, className: cn(FIELD_CAPTION_CLASS, className), children: parts.captionText });
|
|
25
|
+
}
|
|
26
|
+
const LABEL_IN_ROW = "static min-w-0";
|
|
27
|
+
function StaticLabelRow({ hint, children }) {
|
|
28
|
+
return /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1", children: [
|
|
29
|
+
children,
|
|
30
|
+
/* @__PURE__ */ jsx("span", { className: "pointer-events-auto flex shrink-0 items-center", children: hint })
|
|
31
|
+
] });
|
|
32
|
+
}
|
|
33
|
+
function EndHintRow({
|
|
34
|
+
hint,
|
|
35
|
+
className,
|
|
36
|
+
children
|
|
37
|
+
}) {
|
|
38
|
+
if (hint === void 0) return /* @__PURE__ */ jsx(Fragment, { children });
|
|
39
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-1.5", className), children: [
|
|
40
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children }),
|
|
41
|
+
/* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", children: hint })
|
|
42
|
+
] });
|
|
43
|
+
}
|
|
44
|
+
function useLockReason(commit, disabledReason) {
|
|
45
|
+
const reason = useCommitReason(commit, disabledReason);
|
|
46
|
+
const reasonId = useId();
|
|
47
|
+
return { reason, locked: hasMessage(reason), reasonId };
|
|
48
|
+
}
|
|
49
|
+
function LockedReason({
|
|
50
|
+
lock,
|
|
51
|
+
className,
|
|
52
|
+
children
|
|
53
|
+
}) {
|
|
54
|
+
return /* @__PURE__ */ jsx(Tooltip, { label: lock.reason, className, children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
55
|
+
children,
|
|
56
|
+
/* @__PURE__ */ jsx("span", { id: lock.reasonId, hidden: true, children: lock.reason })
|
|
57
|
+
] }) });
|
|
58
|
+
}
|
|
59
|
+
const RequiredStarOnLegend = createContext(false);
|
|
60
|
+
export {
|
|
61
|
+
EndHintRow,
|
|
62
|
+
FIELD_CAPTION_CLASS,
|
|
63
|
+
FieldCaption,
|
|
64
|
+
LABEL_IN_ROW,
|
|
65
|
+
LockedReason,
|
|
66
|
+
RequiredStarOnLegend,
|
|
67
|
+
StaticLabelRow,
|
|
68
|
+
isTextHint,
|
|
69
|
+
useFieldHint,
|
|
70
|
+
useLockReason
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=field-parts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/field-parts.tsx"],"sourcesContent":["import { createContext, useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Tooltip } from \"./tooltip\";\nimport { useCommitReason } from \"./write-lock\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\n\n/**\n * Small pieces the fields share for the 0.22.0 anatomy round (keksdose K3/K4, kastlan\n * 8). Private to the package — not re-exported from the barrel — like `choice-parts`.\n *\n * Nothing here imports `ui.tsx`: `ui.tsx` imports this, and so do the combobox family\n * and the native choice controls, which is the point of keeping the rules in one place.\n */\n\n/* ── hint ─────────────────────────────────────────────────────────────────── */\n\n/**\n * Whether a `hint` is a CAPTION (plain text: a non-empty string or a number) or\n * something for the label line (a {@link FieldHint} \"?\", or any other node).\n *\n * The rule {@link Select} and `NumberInput` already keep: the label line is 11px of\n * strip shared with the label, and a sentence placed there ran over the label and into\n * the value; a \"?\" under the field would be a button floating free of the label it\n * explains.\n */\nexport function isTextHint(hint: ReactNode): hint is string | number {\n return (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n}\n\n/** The caption under a field — Select's and NumberInput's type, so every field's\n * standing advice reads alike. */\nexport const FIELD_CAPTION_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\";\n\nexport interface FieldHintParts {\n /** The caller's `aria-describedby` with the caption's id appended — standing\n * advice first; the error (the news) is appended after this by the field. */\n describedBy: string | undefined;\n /** The caption's id while there is one. */\n captionId: string | undefined;\n /** The caption itself, or `undefined` for a hint that is not text. */\n captionText: string | number | undefined;\n /** A non-text hint (a FieldHint) for the label line — or, on a field with no label\n * line, the end edge outside the box. `undefined` when there is none. */\n labelHint: ReactNode | undefined;\n}\n\n/**\n * Split a field's `hint` into the caption under it (attached with `aria-describedby`)\n * or the \"?\" on its label line. See {@link isTextHint}.\n */\nexport function useFieldHint(hint: ReactNode, describedBy: string | undefined): FieldHintParts {\n const captionId = useId();\n const text = isTextHint(hint);\n return {\n describedBy: text ? mergeDescribedBy(describedBy, captionId) : describedBy,\n captionId: text ? captionId : undefined,\n captionText: text ? hint : undefined,\n labelHint: !text && hasMessage(hint) ? hint : undefined,\n };\n}\n\n/** The caption under a field, as {@link Select} draws its own. */\nexport function FieldCaption({ parts, className }: { parts: FieldHintParts; className?: string }) {\n if (parts.captionText === undefined) return null;\n return (\n <p id={parts.captionId} className={cn(FIELD_CAPTION_CLASS, className)}>\n {parts.captionText}\n </p>\n );\n}\n\n/**\n * Classes that take a static floating label (`FLOATING_LABEL_STATIC`: `FieldLabel`,\n * `ComboboxFieldLabel`) out of its own absolute slot and into a {@link StaticLabelRow}.\n * `static` wins over `absolute` through `cn`'s merge; `min-w-0` lets it truncate in\n * the row.\n */\nexport const LABEL_IN_ROW = \"static min-w-0\";\n\n/**\n * A static label and its \"?\" on one line, in the top strip of a field that lays out its\n * own label — the combobox family, which cannot use {@link FloatingField} because the\n * trigger, the anchored panel and the label hang off a box of their own.\n *\n * FloatingField's static row, with one difference: it stops at `end-9`, the column the\n * chevron (or the clear \"×\") takes, so a long label truncates before it rather than\n * putting the \"?\" on top of the chevron. The hint keeps its width; the label gives way.\n */\nexport function StaticLabelRow({ hint, children }: { hint: ReactNode; children: ReactNode }) {\n return (\n <div className=\"pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1\">\n {children}\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n}\n\n/**\n * A field with no label line and a FieldHint \"?\": the hint at the end edge, OUTSIDE the\n * box, as an unlabelled {@link Select} has placed it since 0.15.5. Without a hint the\n * field is returned exactly as it was, so the wrapper costs nothing where it does not\n * apply.\n */\nexport function EndHintRow({\n hint,\n className,\n children,\n}: {\n hint: ReactNode | undefined;\n className?: string;\n children: ReactNode;\n}) {\n if (hint === undefined) return <>{children}</>;\n return (\n <div className={cn(\"flex items-center gap-1.5\", className)}>\n <div className=\"min-w-0 flex-1\">{children}</div>\n <span className=\"flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n}\n\n/* ── the write lock on a control that saves itself (keksdose K3) ───────────── */\n\nexport interface LockReason {\n /** The reason in force — the lock's (with `commit` under a locked\n * `WriteLockProvider`), else the control's own `disabledReason`. */\n reason: ReactNode;\n /** There is a reason: the control is locked the focusable way. */\n locked: boolean;\n /** The id of the reason's hidden copy, for the control's `aria-describedby`. */\n reasonId: string;\n}\n\n/**\n * {@link Button}'s `commit` + `disabledReason`, for a control that IS its own commit — a\n * switch, a checkbox or a select that saves on change, a list row whose click writes.\n * The lock's reason wins over the control's own (`useCommitReason`).\n */\nexport function useLockReason(commit: boolean | undefined, disabledReason: ReactNode): LockReason {\n const reason = useCommitReason(commit, disabledReason);\n const reasonId = useId();\n return { reason, locked: hasMessage(reason), reasonId };\n}\n\n/**\n * The kit {@link Tooltip} around a locked control, with a `hidden` copy of the reason for\n * the control's `aria-describedby` — Button's anatomy. The control goes in a FRAGMENT so\n * the Tooltip does not also clone its bubble's id into the description: the hidden copy\n * already describes it, and the same sentence twice would be read on every focus.\n *\n * `className` is the wrapper's, which becomes the outermost element while locked — a\n * control moves its own layout classes here so a lock does not reflow the form.\n */\nexport function LockedReason({\n lock,\n className,\n children,\n}: {\n lock: LockReason;\n className?: string;\n children: ReactNode;\n}) {\n return (\n <Tooltip label={lock.reason} className={className}>\n <>\n {children}\n <span id={lock.reasonId} hidden>\n {lock.reason}\n </span>\n </>\n </Tooltip>\n );\n}\n\n/* ── groups ───────────────────────────────────────────────────────────────── */\n\n/**\n * Set by {@link CheckboxGroup}: its legend carries the one required star, so a box\n * inside it keeps the native `required` (a form still refuses an empty set) but draws no\n * star of its own — four stars on four languages would read as four separate demands.\n * ChoiceCardGroup's `InGroupContext`, for the plain checkbox.\n */\nexport const RequiredStarOnLegend = createContext(false);\n"],"mappings":";AAkEI,SA+C6B,UA/C7B,KAyBA,YAzBA;AAlEJ,SAAS,eAAe,aAAa;AAErC,SAAS,UAAU;AACnB,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,YAAY,wBAAwB;AAqBtC,SAAS,WAAW,MAA0C;AACnE,SAAQ,OAAO,SAAS,YAAY,SAAS,MAAO,OAAO,SAAS;AACtE;AAIO,MAAM,sBAAsB;AAmB5B,SAAS,aAAa,MAAiB,aAAiD;AAC7F,QAAM,YAAY,MAAM;AACxB,QAAM,OAAO,WAAW,IAAI;AAC5B,SAAO;AAAA,IACL,aAAa,OAAO,iBAAiB,aAAa,SAAS,IAAI;AAAA,IAC/D,WAAW,OAAO,YAAY;AAAA,IAC9B,aAAa,OAAO,OAAO;AAAA,IAC3B,WAAW,CAAC,QAAQ,WAAW,IAAI,IAAI,OAAO;AAAA,EAChD;AACF;AAGO,SAAS,aAAa,EAAE,OAAO,UAAU,GAAkD;AAChG,MAAI,MAAM,gBAAgB,OAAW,QAAO;AAC5C,SACE,oBAAC,OAAE,IAAI,MAAM,WAAW,WAAW,GAAG,qBAAqB,SAAS,GACjE,gBAAM,aACT;AAEJ;AAQO,MAAM,eAAe;AAWrB,SAAS,eAAe,EAAE,MAAM,SAAS,GAA6C;AAC3F,SACE,qBAAC,SAAI,WAAU,iFACZ;AAAA;AAAA,IACD,oBAAC,UAAK,WAAU,kDAAkD,gBAAK;AAAA,KACzE;AAEJ;AAQO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,SAAS,OAAW,QAAO,gCAAG,UAAS;AAC3C,SACE,qBAAC,SAAI,WAAW,GAAG,6BAA6B,SAAS,GACvD;AAAA,wBAAC,SAAI,WAAU,kBAAkB,UAAS;AAAA,IAC1C,oBAAC,UAAK,WAAU,8BAA8B,gBAAK;AAAA,KACrD;AAEJ;AAmBO,SAAS,cAAc,QAA6B,gBAAuC;AAChG,QAAM,SAAS,gBAAgB,QAAQ,cAAc;AACrD,QAAM,WAAW,MAAM;AACvB,SAAO,EAAE,QAAQ,QAAQ,WAAW,MAAM,GAAG,SAAS;AACxD;AAWO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,oBAAC,WAAQ,OAAO,KAAK,QAAQ,WAC3B,2CACG;AAAA;AAAA,IACD,oBAAC,UAAK,IAAI,KAAK,UAAU,QAAM,MAC5B,eAAK,QACR;AAAA,KACF,GACF;AAEJ;AAUO,MAAM,uBAAuB,cAAc,KAAK;","names":[]}
|