@eifi1/ui-kit 0.56.0 → 0.57.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.
Files changed (63) hide show
  1. package/README.md +9 -6
  2. package/dist/components/data-table.d.ts +16 -0
  3. package/dist/components/data-table.js +17 -0
  4. package/dist/components/data-table.js.map +1 -1
  5. package/dist/components/quantity-field.d.ts +61 -0
  6. package/dist/components/quantity-field.js +95 -0
  7. package/dist/components/quantity-field.js.map +1 -0
  8. package/dist/components/quantity-input.d.ts +139 -0
  9. package/dist/components/quantity-input.js +188 -0
  10. package/dist/components/quantity-input.js.map +1 -0
  11. package/dist/components/quantity-labels.d.ts +2 -0
  12. package/dist/components/quantity-labels.js +11 -0
  13. package/dist/components/quantity-labels.js.map +1 -0
  14. package/dist/eslint/index.js +1 -1
  15. package/dist/feedback/feedback-inbox-bulk.d.ts +88 -0
  16. package/dist/feedback/feedback-inbox-bulk.js +316 -0
  17. package/dist/feedback/feedback-inbox-bulk.js.map +1 -0
  18. package/dist/feedback/feedback-inbox-page.d.ts +47 -3
  19. package/dist/feedback/feedback-inbox-page.js +19 -4
  20. package/dist/feedback/feedback-inbox-page.js.map +1 -1
  21. package/dist/feedback/feedback-row-detail.d.ts +5 -1
  22. package/dist/feedback/feedback-row-detail.js.map +1 -1
  23. package/dist/feedback/feedback-table-labels.js +10 -1
  24. package/dist/feedback/feedback-table-labels.js.map +1 -1
  25. package/dist/feedback/feedback-table.d.ts +26 -0
  26. package/dist/feedback/feedback-table.js.map +1 -1
  27. package/dist/feedback.d.ts +1 -1
  28. package/dist/feedback.js.map +1 -1
  29. package/dist/i18n/defaults.js +3 -1
  30. package/dist/i18n/defaults.js.map +1 -1
  31. package/dist/i18n/german.js +20 -1
  32. package/dist/i18n/german.js.map +1 -1
  33. package/dist/i18n/kit-labels.d.ts +4 -0
  34. package/dist/i18n/kit-labels.js.map +1 -1
  35. package/dist/i18n/locales/en.js +26 -3
  36. package/dist/i18n/locales/en.js.map +3 -3
  37. package/dist/i18n/locales/es.js +20 -1
  38. package/dist/i18n/locales/es.js.map +1 -1
  39. package/dist/i18n/locales/fr.js +25 -1
  40. package/dist/i18n/locales/fr.js.map +1 -1
  41. package/dist/i18n/locales/hu.js +22 -1
  42. package/dist/i18n/locales/hu.js.map +1 -1
  43. package/dist/i18n/locales/it.js +21 -1
  44. package/dist/i18n/locales/it.js.map +1 -1
  45. package/dist/i18n/locales/zh.js +20 -1
  46. package/dist/i18n/locales/zh.js.map +1 -1
  47. package/dist/i18n/review.js +12 -1
  48. package/dist/i18n/review.js.map +1 -1
  49. package/dist/index.d.ts +6 -0
  50. package/dist/index.js +23 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/lib/quantity.d.ts +107 -0
  53. package/dist/lib/quantity.js +166 -0
  54. package/dist/lib/quantity.js.map +1 -0
  55. package/dist/rhf/fields.d.ts +28 -0
  56. package/dist/rhf/fields.js +82 -0
  57. package/dist/rhf/fields.js.map +1 -1
  58. package/dist/rhf.js.map +1 -1
  59. package/dist/theme/contrast.d.ts +1 -1
  60. package/dist/theme/palette-store.d.ts +1 -1
  61. package/dist/theme/text-size.d.ts +1 -1
  62. package/dist/theme/theme-store.d.ts +1 -1
  63. package/package.json +2 -3
@@ -0,0 +1,61 @@
1
+ import type { ReactNode } from "react";
2
+ import { type QuantityInputProps } from "./quantity-input.js";
3
+ import { type FieldHelpProps } from "./field-parts.js";
4
+ import { type Quantity } from "../lib/quantity.js";
5
+ /** {@link QuantityField}'s props: {@link QuantityInput}'s, and a field's anatomy. */
6
+ export interface QuantityFieldProps extends QuantityInputProps, FieldHelpProps {
7
+ /** The visible label, floating in the amount's box as NumberField's does. Without one,
8
+ * name the field with `aria-label`. */
9
+ label?: ReactNode;
10
+ /** A fact under the field, on its description — or a `FieldHint` on the label's line
11
+ * (its words at Large and on touch, §4) — as on every kit field. */
12
+ hint?: ReactNode;
13
+ /** What is wrong, in the caller's words: under the field, on its description, and it
14
+ * paints both controls. `null`, `false` and `""` are no message. */
15
+ error?: ReactNode;
16
+ /** Announced as required (`aria-required` on the amount). Whether it IS required is the
17
+ * form's validation to say. */
18
+ required?: boolean;
19
+ /**
20
+ * A quantity to take with one tap — the household's usual pack, the last one bought: a
21
+ * line under the field, "Suggested · 500 g", whose quantity is a button that sets the
22
+ * value to it (amount and unit). Shown while it is passed and has an amount; the app
23
+ * stops passing it when it no longer applies. Taking it when the value already is it
24
+ * changes nothing, so the button keeps its place and its focus.
25
+ */
26
+ suggestion?: Quantity;
27
+ /** Where the suggestion comes from, in the app's words ("Usual pack", "Last time") —
28
+ * FieldSuggestion's `from`. Default the kit's "Suggested" (`quantity.suggested`). */
29
+ suggestionFrom?: ReactNode;
30
+ /** The field's box — a width, a grid cell. `inputClassName` and `selectClassName`
31
+ * reach the two controls. */
32
+ className?: string;
33
+ }
34
+ /**
35
+ * {@link QuantityInput} as a labelled field: the label floating in the amount's box, the
36
+ * unit select beside it, and under both the suggestion to take, the caption and the error.
37
+ *
38
+ * ```tsx
39
+ * <QuantityField
40
+ * label="Flour"
41
+ * family="mass"
42
+ * value={qty}
43
+ * onChange={setQty}
44
+ * suggestion={{ amount: 1, unit: "kg" }}
45
+ * suggestionFrom="Usual pack"
46
+ * />
47
+ * ```
48
+ *
49
+ * The amount and the unit are one field to a screen reader: the amount is named by the
50
+ * label, the select "Unit for “<label>”", and both are described by the same suggestion,
51
+ * caption and error. Tab goes amount → unit → the suggestion's button.
52
+ *
53
+ * `className` is the field's box (a width, a grid cell); `inputClassName` and
54
+ * `selectClassName` reach the two controls.
55
+ */
56
+ export declare function QuantityField(props: QuantityFieldProps): import("react").JSX.Element;
57
+ /** @internal QuantityField, with the name its unit select is called by when the label is
58
+ * drawn outside it — RhfQuantityField's, above. Not on the barrel. */
59
+ export declare function QuantityFieldBody(props: QuantityFieldProps & {
60
+ fieldName?: string;
61
+ }): import("react").JSX.Element;
@@ -0,0 +1,95 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { QuantityRow, DEFAULT_QUANTITY_LABELS } from "./quantity-input.js";
5
+ import { FIELD_CAPTION_CLASS, mergeDescribedBy, useFieldMessages } from "./field-parts.js";
6
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
7
+ import { cn } from "../lib/cn.js";
8
+ import { dataAttributes } from "../lib/data-attributes.js";
9
+ import { hasContent } from "../lib/presence.js";
10
+ import { formatQuantity } from "../lib/quantity.js";
11
+ function same(a, b) {
12
+ return a.unit === b.unit && Object.is(a.amount, b.amount);
13
+ }
14
+ function QuantityField(props) {
15
+ return /* @__PURE__ */ jsx(QuantityFieldBody, { ...props });
16
+ }
17
+ function QuantityFieldBody(props) {
18
+ const {
19
+ ref,
20
+ label,
21
+ hint,
22
+ help,
23
+ error,
24
+ required,
25
+ suggestion,
26
+ suggestionFrom,
27
+ fieldName,
28
+ className,
29
+ invalid,
30
+ "aria-describedby": ariaDescribedBy,
31
+ "aria-invalid": ariaInvalid,
32
+ "aria-required": ariaRequired,
33
+ ...rest
34
+ } = props;
35
+ const labels = useKitLabels("quantity", DEFAULT_QUANTITY_LABELS, rest.labels);
36
+ const locale = useKitLocale(rest.locale);
37
+ const name = label ?? rest["aria-label"];
38
+ const offer = suggestion !== void 0 && suggestion.amount !== null && Number.isFinite(suggestion.amount);
39
+ const suggestionId = useId();
40
+ const messages = useFieldMessages({
41
+ hint,
42
+ help,
43
+ name,
44
+ error,
45
+ invalid,
46
+ describedBy: mergeDescribedBy(ariaDescribedBy, offer && suggestionId),
47
+ ariaInvalid
48
+ });
49
+ const shown = offer ? formatQuantity(suggestion, locale, { labels }) : "";
50
+ const take = () => {
51
+ if (!offer || rest.disabled) return;
52
+ if (!same(suggestion, rest.value)) rest.onChange({ amount: suggestion.amount, unit: suggestion.unit });
53
+ };
54
+ const rowProps = Object.fromEntries(Object.entries(rest).filter(([key]) => !key.startsWith("data-")));
55
+ return /* @__PURE__ */ jsxs("div", { ...dataAttributes(props), className, children: [
56
+ /* @__PURE__ */ jsx(
57
+ QuantityRow,
58
+ {
59
+ ...rowProps,
60
+ ref,
61
+ label,
62
+ labelHint: messages.labelHint,
63
+ fieldName,
64
+ "aria-describedby": messages.describedBy,
65
+ "aria-required": required || ariaRequired || void 0,
66
+ invalid: messages.isInvalid
67
+ }
68
+ ),
69
+ offer && /* @__PURE__ */ jsxs("p", { id: suggestionId, "data-slot": "quantity-suggestion", className: FIELD_CAPTION_CLASS, children: [
70
+ hasContent(suggestionFrom) ? suggestionFrom : labels.suggested,
71
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: " \xB7 " }),
72
+ /* @__PURE__ */ jsx(
73
+ "button",
74
+ {
75
+ type: "button",
76
+ onClick: take,
77
+ disabled: rest.disabled,
78
+ "aria-label": labels.takeSuggestion(shown),
79
+ className: cn(
80
+ "rounded-sm font-medium text-secondary underline underline-offset-2 transition-colors hover:text-primary",
81
+ "focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand",
82
+ "disabled:cursor-default disabled:no-underline disabled:opacity-50"
83
+ ),
84
+ children: shown
85
+ }
86
+ )
87
+ ] }),
88
+ messages.below
89
+ ] });
90
+ }
91
+ export {
92
+ QuantityField,
93
+ QuantityFieldBody
94
+ };
95
+ //# sourceMappingURL=quantity-field.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/quantity-field.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { QuantityRow, DEFAULT_QUANTITY_LABELS, type QuantityInputProps } from \"./quantity-input\";\nimport { FIELD_CAPTION_CLASS, mergeDescribedBy, useFieldMessages, type FieldHelpProps } from \"./field-parts\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { dataAttributes } from \"../lib/data-attributes\";\nimport { hasContent } from \"../lib/presence\";\nimport { formatQuantity, type Quantity } from \"../lib/quantity\";\n\n/** {@link QuantityField}'s props: {@link QuantityInput}'s, and a field's anatomy. */\nexport interface QuantityFieldProps extends QuantityInputProps, FieldHelpProps {\n /** The visible label, floating in the amount's box as NumberField's does. Without one,\n * name the field with `aria-label`. */\n label?: ReactNode;\n /** A fact under the field, on its description — or a `FieldHint` on the label's line\n * (its words at Large and on touch, §4) — as on every kit field. */\n hint?: ReactNode;\n /** What is wrong, in the caller's words: under the field, on its description, and it\n * paints both controls. `null`, `false` and `\"\"` are no message. */\n error?: ReactNode;\n /** Announced as required (`aria-required` on the amount). Whether it IS required is the\n * form's validation to say. */\n required?: boolean;\n /**\n * A quantity to take with one tap — the household's usual pack, the last one bought: a\n * line under the field, \"Suggested · 500 g\", whose quantity is a button that sets the\n * value to it (amount and unit). Shown while it is passed and has an amount; the app\n * stops passing it when it no longer applies. Taking it when the value already is it\n * changes nothing, so the button keeps its place and its focus.\n */\n suggestion?: Quantity;\n /** Where the suggestion comes from, in the app's words (\"Usual pack\", \"Last time\") —\n * FieldSuggestion's `from`. Default the kit's \"Suggested\" (`quantity.suggested`). */\n suggestionFrom?: ReactNode;\n /** The field's box — a width, a grid cell. `inputClassName` and `selectClassName`\n * reach the two controls. */\n className?: string;\n}\n\n/** Two quantities that say the same: same unit, same amount. */\nfunction same(a: Quantity, b: Quantity): boolean {\n return a.unit === b.unit && Object.is(a.amount, b.amount);\n}\n\n/**\n * {@link QuantityInput} as a labelled field: the label floating in the amount's box, the\n * unit select beside it, and under both the suggestion to take, the caption and the error.\n *\n * ```tsx\n * <QuantityField\n * label=\"Flour\"\n * family=\"mass\"\n * value={qty}\n * onChange={setQty}\n * suggestion={{ amount: 1, unit: \"kg\" }}\n * suggestionFrom=\"Usual pack\"\n * />\n * ```\n *\n * The amount and the unit are one field to a screen reader: the amount is named by the\n * label, the select \"Unit for “<label>”\", and both are described by the same suggestion,\n * caption and error. Tab goes amount → unit → the suggestion's button.\n *\n * `className` is the field's box (a width, a grid cell); `inputClassName` and\n * `selectClassName` reach the two controls.\n */\nexport function QuantityField(props: QuantityFieldProps) {\n return <QuantityFieldBody {...props} />;\n}\n\n/** @internal QuantityField, with the name its unit select is called by when the label is\n * drawn outside it — RhfQuantityField's, above. Not on the barrel. */\nexport function QuantityFieldBody(props: QuantityFieldProps & { fieldName?: string }) {\n const {\n ref,\n label,\n hint,\n help,\n error,\n required,\n suggestion,\n suggestionFrom,\n fieldName,\n className,\n invalid,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n ...rest\n } = props;\n const labels = useKitLabels(\"quantity\", DEFAULT_QUANTITY_LABELS, rest.labels);\n const locale = useKitLocale(rest.locale);\n const name = label ?? rest[\"aria-label\"];\n\n const offer = suggestion !== undefined && suggestion.amount !== null && Number.isFinite(suggestion.amount);\n const suggestionId = useId();\n // The suggestion first — it is about the value, nearest the control — then the caption\n // and the error, the order every kit field reads its description in.\n const messages = useFieldMessages({\n hint,\n help,\n name,\n error,\n invalid,\n describedBy: mergeDescribedBy(ariaDescribedBy, offer && suggestionId),\n ariaInvalid,\n });\n\n const shown = offer ? formatQuantity(suggestion, locale, { labels }) : \"\";\n const take = () => {\n if (!offer || rest.disabled) return;\n if (!same(suggestion, rest.value)) rest.onChange({ amount: suggestion.amount, unit: suggestion.unit });\n };\n\n // The data-* props tag the field's box, not the row inside it.\n const rowProps = Object.fromEntries(Object.entries(rest).filter(([key]) => !key.startsWith(\"data-\"))) as typeof rest;\n\n return (\n <div {...dataAttributes(props)} className={className}>\n <QuantityRow\n {...rowProps}\n ref={ref}\n label={label}\n labelHint={messages.labelHint}\n fieldName={fieldName}\n aria-describedby={messages.describedBy}\n aria-required={required || ariaRequired || undefined}\n invalid={messages.isInvalid}\n />\n {offer && (\n <p id={suggestionId} data-slot=\"quantity-suggestion\" className={FIELD_CAPTION_CLASS}>\n {hasContent(suggestionFrom) ? suggestionFrom : labels.suggested}\n <span aria-hidden=\"true\"> · </span>\n <button\n type=\"button\"\n onClick={take}\n disabled={rest.disabled}\n aria-label={labels.takeSuggestion(shown)}\n // FieldSuggestion's text button: the caption's type, underlined as the kit's\n // secondary TextLink is; the focus outline follows a wrapped line.\n className={cn(\n \"rounded-sm font-medium text-secondary underline underline-offset-2 transition-colors hover:text-primary\",\n \"focus-visible:outline-focus focus-visible:outline-offset-2 focus-visible:outline-brand\",\n \"disabled:cursor-default disabled:no-underline disabled:opacity-50\",\n )}\n >\n {shown}\n </button>\n </p>\n )}\n {messages.below}\n </div>\n );\n}\n"],"mappings":";AAoES,cA+DD,YA/DC;AApET,SAAS,aAAa;AAEtB,SAAS,aAAa,+BAAwD;AAC9E,SAAS,qBAAqB,kBAAkB,wBAA6C;AAC7F,SAAS,cAAc,oBAAoB;AAC3C,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,sBAAqC;AAiC9C,SAAS,KAAK,GAAa,GAAsB;AAC/C,SAAO,EAAE,SAAS,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,EAAE,MAAM;AAC1D;AAwBO,SAAS,cAAc,OAA2B;AACvD,SAAO,oBAAC,qBAAmB,GAAG,OAAO;AACvC;AAIO,SAAS,kBAAkB,OAAoD;AACpF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,SAAS,aAAa,YAAY,yBAAyB,KAAK,MAAM;AAC5E,QAAM,SAAS,aAAa,KAAK,MAAM;AACvC,QAAM,OAAO,SAAS,KAAK,YAAY;AAEvC,QAAM,QAAQ,eAAe,UAAa,WAAW,WAAW,QAAQ,OAAO,SAAS,WAAW,MAAM;AACzG,QAAM,eAAe,MAAM;AAG3B,QAAM,WAAW,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,iBAAiB,iBAAiB,SAAS,YAAY;AAAA,IACpE;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,QAAQ,eAAe,YAAY,QAAQ,EAAE,OAAO,CAAC,IAAI;AACvE,QAAM,OAAO,MAAM;AACjB,QAAI,CAAC,SAAS,KAAK,SAAU;AAC7B,QAAI,CAAC,KAAK,YAAY,KAAK,KAAK,EAAG,MAAK,SAAS,EAAE,QAAQ,WAAW,QAAQ,MAAM,WAAW,KAAK,CAAC;AAAA,EACvG;AAGA,QAAM,WAAW,OAAO,YAAY,OAAO,QAAQ,IAAI,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,IAAI,WAAW,OAAO,CAAC,CAAC;AAEpG,SACE,qBAAC,SAAK,GAAG,eAAe,KAAK,GAAG,WAC9B;AAAA;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA;AAAA,QACA,WAAW,SAAS;AAAA,QACpB;AAAA,QACA,oBAAkB,SAAS;AAAA,QAC3B,iBAAe,YAAY,gBAAgB;AAAA,QAC3C,SAAS,SAAS;AAAA;AAAA,IACpB;AAAA,IACC,SACC,qBAAC,OAAE,IAAI,cAAc,aAAU,uBAAsB,WAAW,qBAC7D;AAAA,iBAAW,cAAc,IAAI,iBAAiB,OAAO;AAAA,MACtD,oBAAC,UAAK,eAAY,QAAO,oBAAG;AAAA,MAC5B;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,SAAS;AAAA,UACT,UAAU,KAAK;AAAA,UACf,cAAY,OAAO,eAAe,KAAK;AAAA,UAGvC,WAAW;AAAA,YACT;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,OACF;AAAA,IAED,SAAS;AAAA,KACZ;AAEJ;","names":[]}
@@ -0,0 +1,139 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import { DEFAULT_QUANTITY_LABELS } from "./quantity-labels.js";
3
+ import { type LabelOverride } from "../i18n/kit-labels.js";
4
+ import { type DataAttributes } from "../lib/data-attributes.js";
5
+ import { type Quantity, type QuantityFamily, type QuantityUnit } from "../lib/quantity.js";
6
+ /** The words of {@link QuantityInput} and {@link QuantityField} — the `quantity` namespace. */
7
+ export interface QuantityLabels {
8
+ /** The pieces unit, in the select and after a count: "pcs", "Stk.", "pce", "pz.". The
9
+ * one unit with a word — g, kg, ml, cl, dl and l are SI symbols in every language. */
10
+ pcs: string;
11
+ /** The unit select's accessible name — with the field's name when it has one ("Unit
12
+ * for “Flour”"), else alone. */
13
+ unit: (field?: string) => string;
14
+ /** The suggestion line's lead when the app names no source ({@link QuantityField}'s
15
+ * `suggestionFrom`). */
16
+ suggested: string;
17
+ /** The suggestion button's accessible name: take this quantity ("Use 500 g"). It holds
18
+ * the quantity the button shows, the words a voice user says (WCAG 2.5.3). */
19
+ takeSuggestion: (quantity: string) => string;
20
+ }
21
+ export { DEFAULT_QUANTITY_LABELS };
22
+ /** {@link QuantityInput}'s props. */
23
+ export interface QuantityInputProps extends DataAttributes {
24
+ /** The quantity: an amount (`null` for none yet) and its unit. */
25
+ value: Quantity;
26
+ /**
27
+ * The next quantity — when a typed amount commits (blur or Enter, as NumberField
28
+ * commits; never per keystroke) and when another unit is picked. The amount is
29
+ * already rounded to the unit's `digits` and clamped to `max`.
30
+ */
31
+ onChange: (next: Quantity) => void;
32
+ /** Offer only this family's units — `"mass"` (g, kg), `"volume"` (ml, cl, dl, l) or
33
+ * `"count"` (pcs). Without it (and without `units`), every unit. */
34
+ family?: QuantityFamily;
35
+ /**
36
+ * The units to offer, explicitly — WINS over `family`, and is listed small to large
37
+ * whatever its order (`["ml", "dl", "l"]` is a volume list without centilitres). Each
38
+ * should lie inside `family` when both are given; one that does not is still offered,
39
+ * and a development build says so on the console. A value whose unit is not in the
40
+ * list (a 75 cl that `parseQuantity` read off a label) is still shown: the select lists
41
+ * its unit for as long as it is the value's.
42
+ */
43
+ units?: readonly QuantityUnit[];
44
+ /** An emptied amount commits `null`. Without it an empty amount is no answer and snaps
45
+ * back to the last one, as NumberField's does. */
46
+ nullable?: boolean;
47
+ /** Refuse 0: it snaps back like text that is no number (default `true` — a shopping
48
+ * list has no "0 g"). A negative amount is refused either way. */
49
+ positive?: boolean;
50
+ /** The largest amount, in whichever unit is chosen — NumberField's `max`: a typed amount
51
+ * above it commits as `max`. A limit in a physical unit ("at most 25 kg") is the form's
52
+ * validation, which sees the unit too. */
53
+ max?: number;
54
+ /**
55
+ * Decimals per unit, over {@link QUANTITY_DIGITS} (g 0, kg 3, ml 0, cl 1, dl 2, l 3,
56
+ * pcs 0): `{ kg: 2 }`, `{ pcs: 1 }`. A typed amount is rounded to them on commit, and
57
+ * shown with at most that many. Cut to the unit's milli grid (g and ml none, cl 1, dl 2,
58
+ * kg, l and pcs 3), so every amount still sums exactly.
59
+ */
60
+ digits?: Partial<Record<QuantityUnit, number>>;
61
+ /** BCP 47 tag for the amount's decimal mark ("1,5" in de-DE, "1.5" in de-CH); default
62
+ * the `<UiKitProvider locale>`, else the runtime's — NumberField's `locale`. */
63
+ locale?: string;
64
+ /**
65
+ * Another unit converts the amount: `true` turns 500 g into 0.5 kg. `false` (default)
66
+ * keeps the amount as typed — 500 g becomes 500 kg — because the usual reason to change
67
+ * the unit is that the unit was wrong, not the weight. Either way the amount lands on
68
+ * the new unit's grid (1.5 kg kept as typed in grams is 2 g); a conversion within a
69
+ * family is exact, and across families (`units` mixing them) the amount is kept.
70
+ */
71
+ convertOnUnitChange?: boolean;
72
+ /** Both controls, the amount and the unit. */
73
+ disabled?: boolean;
74
+ /** Paints both controls as wrong — {@link Input}'s `invalid`. */
75
+ invalid?: boolean;
76
+ /** The amount `<input>`'s id — what a label's `htmlFor` points at. */
77
+ id?: string;
78
+ /** Names the amount when there is no label (a table cell). The unit select is named
79
+ * after it: "Unit for “<aria-label>”". */
80
+ "aria-label"?: string;
81
+ /** Set on the amount input (and the unit select, which the same caption and error
82
+ * describe) — a form library's control slot spreads these. */
83
+ "aria-describedby"?: string;
84
+ "aria-invalid"?: boolean | "true" | "false";
85
+ "aria-required"?: boolean | "true" | "false";
86
+ placeholder?: string;
87
+ /** Prop > `<UiKitProvider labels={{ quantity }}>` > English. */
88
+ labels?: LabelOverride<QuantityLabels>;
89
+ /** The root: the row holding the amount and the unit. */
90
+ className?: string;
91
+ /** The amount `<input>`. */
92
+ inputClassName?: string;
93
+ /** The unit `<select>`. */
94
+ selectClassName?: string;
95
+ /** The amount `<input>` — focus on error, `select()`. */
96
+ ref?: Ref<HTMLInputElement>;
97
+ }
98
+ /** @internal What {@link QuantityField} hands the row on top of the bare control's props. */
99
+ export interface QuantityRowProps extends QuantityInputProps {
100
+ /** The floating label in the amount's box. */
101
+ label?: ReactNode;
102
+ /** What rides the label's line — a resolved `labelHint` (a FieldHint, a help "?"). */
103
+ labelHint?: ReactNode;
104
+ /** The field's name for the unit select's ("Unit for “Flour”") where the label is
105
+ * drawn outside the row — RhfQuantityField's, above it. Default the label's text, else
106
+ * `aria-label`. */
107
+ fieldName?: string;
108
+ }
109
+ /** @internal The unit a value with none starts in — the first one the select would offer
110
+ * (RhfQuantityField's, for a form whose default is `null`). */
111
+ export declare function firstOfferedUnit(units: readonly QuantityUnit[] | undefined, family: QuantityFamily | undefined): QuantityUnit;
112
+ /** @internal The amount and unit row, with the floating label {@link QuantityField} gives
113
+ * it. Not on the barrel: QuantityInput is this without a label. */
114
+ export declare function QuantityRow(props: QuantityRowProps): import("react").JSX.Element;
115
+ /**
116
+ * A quantity — an amount and its unit — as one control: the amount, typed in the
117
+ * locale's decimal mark, beside a select of units. The bare control, for a table row or
118
+ * a line of a list; {@link QuantityField} is the labelled field with a caption, an error
119
+ * and a suggestion to take.
120
+ *
121
+ * ```tsx
122
+ * <QuantityInput aria-label="Flour" family="mass" value={qty} onChange={setQty} />
123
+ * ```
124
+ *
125
+ * THE AMOUNT is NumberField's loop over {@link NumberInput}: a draft while it is typed
126
+ * (grouping read off, the locale's mark kept), resolved on blur or Enter — rounded to the
127
+ * unit's `digits`, clamped to `max` — and `onChange` only then. Empty, or text that is no
128
+ * number, snaps back (empty commits `null` with `nullable`); so does 0 under `positive`
129
+ * (the default), and a negative amount always.
130
+ *
131
+ * THE UNIT is a native select of {@link QUANTITY_UNITS} — `family`'s, or `units`' — with
132
+ * pieces in the reader's word (`quantity.pcs`). Picking one is a commit of its own;
133
+ * `convertOnUnitChange` decides whether the amount is converted or kept as typed.
134
+ *
135
+ * The value's arithmetic, formatting and parsing are `lib/quantity`'s — {@link toBaseQuantity},
136
+ * {@link convertQuantity}, {@link sumQuantities}, {@link formatQuantity}, {@link parseQuantity} —
137
+ * for the list's totals and a comparison table outside the field.
138
+ */
139
+ export declare function QuantityInput(props: QuantityInputProps): import("react").JSX.Element;
@@ -0,0 +1,188 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { NumberInput } from "./number-input.js";
5
+ import { Select } from "./select.js";
6
+ import { FIELD_FLOATING_PAD } from "./field-base.js";
7
+ import { DEFAULT_QUANTITY_LABELS } from "./quantity-labels.js";
8
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
9
+ import { cn } from "../lib/cn.js";
10
+ import { formatResult } from "../lib/calc.js";
11
+ import { dataAttributes } from "../lib/data-attributes.js";
12
+ import { decimalMark, localizeMark } from "../lib/decimal-marks.js";
13
+ import { plainText } from "../lib/plain-text.js";
14
+ import {
15
+ QUANTITY_DIGITS,
16
+ QUANTITY_UNITS,
17
+ convertQuantity,
18
+ quantityFamily,
19
+ quantityMaxDigits
20
+ } from "../lib/quantity.js";
21
+ import { roundHalfAway } from "../lib/round.js";
22
+ function devWarnings() {
23
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
24
+ }
25
+ function isUnit(unit) {
26
+ return typeof unit === "string" && QUANTITY_UNITS.includes(unit);
27
+ }
28
+ function digitsFor(unit, digits) {
29
+ const wanted = digits?.[unit] ?? QUANTITY_DIGITS[unit] ?? 3;
30
+ const whole = Number.isFinite(wanted) ? Math.trunc(wanted) : QUANTITY_DIGITS[unit];
31
+ return Math.min(Math.max(whole, 0), isUnit(unit) ? quantityMaxDigits(unit) : 3);
32
+ }
33
+ function onGrid(amount, unit, positive) {
34
+ const places = isUnit(unit) ? quantityMaxDigits(unit) : 3;
35
+ const rounded = roundHalfAway(amount, places);
36
+ return positive && rounded <= 0 && amount > 0 ? 10 ** -places : rounded;
37
+ }
38
+ function offeredUnits(units, family, current) {
39
+ const known = units?.filter(isUnit) ?? [];
40
+ const allowed = known.length > 0 ? known : QUANTITY_UNITS.filter((u) => !family || quantityFamily(u) === family);
41
+ return QUANTITY_UNITS.filter((u) => allowed.includes(u) || u === current);
42
+ }
43
+ function firstOfferedUnit(units, family) {
44
+ const known = units?.filter(isUnit) ?? [];
45
+ return QUANTITY_UNITS.find((u) => known.length > 0 ? known.includes(u) : !family || quantityFamily(u) === family);
46
+ }
47
+ function QuantityRow(props) {
48
+ const {
49
+ ref,
50
+ value,
51
+ onChange,
52
+ family,
53
+ units,
54
+ nullable = false,
55
+ positive = true,
56
+ max,
57
+ digits,
58
+ locale: localeProp,
59
+ convertOnUnitChange = false,
60
+ disabled,
61
+ invalid,
62
+ id,
63
+ "aria-label": ariaLabel,
64
+ "aria-describedby": ariaDescribedBy,
65
+ "aria-invalid": ariaInvalid,
66
+ "aria-required": ariaRequired,
67
+ placeholder,
68
+ labels: labelsProp,
69
+ className,
70
+ inputClassName,
71
+ selectClassName,
72
+ label,
73
+ labelHint,
74
+ fieldName
75
+ } = props;
76
+ const labels = useKitLabels("quantity", DEFAULT_QUANTITY_LABELS, labelsProp);
77
+ const locale = useKitLocale(localeProp);
78
+ const mark = decimalMark(locale);
79
+ const amount = value.amount !== null && Number.isFinite(value.amount) ? value.amount : null;
80
+ const unit = value.unit;
81
+ const places = digitsFor(unit, digits);
82
+ const format = (n, decimals) => n === null ? "" : localizeMark(formatResult(roundHalfAway(n, decimals)), mark);
83
+ const [draft, setDraft] = useState(() => format(amount, places));
84
+ const [seen, setSeen] = useState({ amount, unit, mark, places });
85
+ if (!Object.is(seen.amount, amount) || seen.unit !== unit || seen.mark !== mark || seen.places !== places) {
86
+ setSeen({ amount, unit, mark, places });
87
+ setDraft(format(amount, places));
88
+ }
89
+ const commit = (text) => {
90
+ const trimmed = text.trim();
91
+ const snapBack = () => setDraft(format(amount, places));
92
+ let next;
93
+ if (trimmed === "") {
94
+ if (!nullable) return snapBack();
95
+ next = null;
96
+ } else {
97
+ const parsed = Number(trimmed.replace(/,/g, "."));
98
+ if (!Number.isFinite(parsed)) return snapBack();
99
+ next = roundHalfAway(parsed, places);
100
+ if (max !== void 0 && next > max) next = max;
101
+ if (next < 0 || positive && next === 0) return snapBack();
102
+ }
103
+ setDraft(format(next, places));
104
+ if (!Object.is(next, amount)) onChange({ amount: next, unit });
105
+ };
106
+ const pickUnit = (picked) => {
107
+ if (!isUnit(picked) || picked === unit) return;
108
+ let next = amount;
109
+ if (next !== null) {
110
+ if (convertOnUnitChange) next = convertQuantity({ amount: next, unit }, picked)?.amount ?? next;
111
+ next = onGrid(next, picked, positive);
112
+ }
113
+ onChange({ amount: next, unit: picked });
114
+ };
115
+ const offered = offeredUnits(units, family, unit);
116
+ const warned = useRef(false);
117
+ const unitsKey = units === void 0 ? void 0 : JSON.stringify(units);
118
+ useEffect(() => {
119
+ if (warned.current || !devWarnings() || unitsKey === void 0) return;
120
+ const list = JSON.parse(unitsKey);
121
+ const unknown = list.filter((u) => !isUnit(u));
122
+ const outside = family ? list.filter((u) => isUnit(u) && quantityFamily(u) !== family) : [];
123
+ if (unknown.length === 0 && outside.length === 0 && list.length > 0) return;
124
+ warned.current = true;
125
+ const problem = list.length === 0 ? "is empty, so the field offers its family's units" : unknown.length > 0 ? `names ${JSON.stringify(unknown)}, which no quantity is measured in` : `${JSON.stringify(outside)} ${outside.length === 1 ? "lies" : "lie"} outside family "${family}"`;
126
+ console.warn(
127
+ `[ui-kit] QuantityInput: \`units\` ${problem}. \`units\` wins over \`family\`: give units of the family, or leave \`family\` out. (Development builds only; once per field.)`
128
+ );
129
+ }, [unitsKey, family]);
130
+ const generatedId = useId();
131
+ const fieldId = id ?? generatedId;
132
+ const name = fieldName ?? plainText(label) ?? ariaLabel;
133
+ const isInvalid = Boolean(invalid) || ariaInvalid === true || ariaInvalid === "true";
134
+ const labelled = label !== void 0;
135
+ return (
136
+ // `items-end`: at Large the amount's label stands above its box (§10.17), and the
137
+ // select lines up with the box, not with the label.
138
+ /* @__PURE__ */ jsxs("div", { ...dataAttributes(props), className: cn("flex items-end", className), children: [
139
+ /* @__PURE__ */ jsx(
140
+ NumberInput,
141
+ {
142
+ ref,
143
+ id: fieldId,
144
+ label,
145
+ ...labelHint !== void 0 ? { hint: labelHint } : {},
146
+ "aria-label": ariaLabel,
147
+ "aria-describedby": ariaDescribedBy,
148
+ "aria-required": ariaRequired,
149
+ value: draft,
150
+ onChange: (text) => setDraft(localizeMark(text, mark)),
151
+ onCommit: commit,
152
+ locale,
153
+ placeholder,
154
+ disabled,
155
+ invalid: isInvalid,
156
+ calculator: false,
157
+ className: "min-w-0 flex-1 focus-within:z-10",
158
+ inputClassName: cn("rounded-e-none", inputClassName)
159
+ }
160
+ ),
161
+ /* @__PURE__ */ jsx(
162
+ Select,
163
+ {
164
+ id: `${fieldId}-unit`,
165
+ "aria-label": labels.unit(name),
166
+ "aria-describedby": ariaDescribedBy,
167
+ value: unit,
168
+ onChange: (e) => pickUnit(e.target.value),
169
+ disabled,
170
+ invalid: isInvalid,
171
+ className: "-ms-px shrink-0 focus-within:z-10",
172
+ selectClassName: cn("w-auto rounded-s-none", labelled && FIELD_FLOATING_PAD, selectClassName),
173
+ children: offered.map((u) => /* @__PURE__ */ jsx("option", { value: u, children: u === "pcs" ? labels.pcs : u }, u))
174
+ }
175
+ )
176
+ ] })
177
+ );
178
+ }
179
+ function QuantityInput(props) {
180
+ return /* @__PURE__ */ jsx(QuantityRow, { ...props });
181
+ }
182
+ export {
183
+ DEFAULT_QUANTITY_LABELS,
184
+ QuantityInput,
185
+ QuantityRow,
186
+ firstOfferedUnit
187
+ };
188
+ //# sourceMappingURL=quantity-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/quantity-input.tsx"],"sourcesContent":["import { useEffect, useId, useRef, useState } from \"react\";\nimport type { ReactNode, Ref } from \"react\";\nimport { NumberInput } from \"./number-input\";\nimport { Select } from \"./select\";\nimport { FIELD_FLOATING_PAD } from \"./field-base\";\nimport { DEFAULT_QUANTITY_LABELS } from \"./quantity-labels\";\nimport { useKitLabels, useKitLocale, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { formatResult } from \"../lib/calc\";\nimport { dataAttributes, type DataAttributes } from \"../lib/data-attributes\";\nimport { decimalMark, localizeMark } from \"../lib/decimal-marks\";\nimport { plainText } from \"../lib/plain-text\";\nimport {\n QUANTITY_DIGITS,\n QUANTITY_UNITS,\n convertQuantity,\n quantityFamily,\n quantityMaxDigits,\n type Quantity,\n type QuantityFamily,\n type QuantityUnit,\n} from \"../lib/quantity\";\nimport { roundHalfAway } from \"../lib/round\";\n\n/** The words of {@link QuantityInput} and {@link QuantityField} — the `quantity` namespace. */\nexport interface QuantityLabels {\n /** The pieces unit, in the select and after a count: \"pcs\", \"Stk.\", \"pce\", \"pz.\". The\n * one unit with a word — g, kg, ml, cl, dl and l are SI symbols in every language. */\n pcs: string;\n /** The unit select's accessible name — with the field's name when it has one (\"Unit\n * for “Flour”\"), else alone. */\n unit: (field?: string) => string;\n /** The suggestion line's lead when the app names no source ({@link QuantityField}'s\n * `suggestionFrom`). */\n suggested: string;\n /** The suggestion button's accessible name: take this quantity (\"Use 500 g\"). It holds\n * the quantity the button shows, the words a voice user says (WCAG 2.5.3). */\n takeSuggestion: (quantity: string) => string;\n}\n\n// Defined in ./quantity-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_QUANTITY_LABELS };\n\n/** {@link QuantityInput}'s props. */\nexport interface QuantityInputProps extends DataAttributes {\n /** The quantity: an amount (`null` for none yet) and its unit. */\n value: Quantity;\n /**\n * The next quantity — when a typed amount commits (blur or Enter, as NumberField\n * commits; never per keystroke) and when another unit is picked. The amount is\n * already rounded to the unit's `digits` and clamped to `max`.\n */\n onChange: (next: Quantity) => void;\n /** Offer only this family's units — `\"mass\"` (g, kg), `\"volume\"` (ml, cl, dl, l) or\n * `\"count\"` (pcs). Without it (and without `units`), every unit. */\n family?: QuantityFamily;\n /**\n * The units to offer, explicitly — WINS over `family`, and is listed small to large\n * whatever its order (`[\"ml\", \"dl\", \"l\"]` is a volume list without centilitres). Each\n * should lie inside `family` when both are given; one that does not is still offered,\n * and a development build says so on the console. A value whose unit is not in the\n * list (a 75 cl that `parseQuantity` read off a label) is still shown: the select lists\n * its unit for as long as it is the value's.\n */\n units?: readonly QuantityUnit[];\n /** An emptied amount commits `null`. Without it an empty amount is no answer and snaps\n * back to the last one, as NumberField's does. */\n nullable?: boolean;\n /** Refuse 0: it snaps back like text that is no number (default `true` — a shopping\n * list has no \"0 g\"). A negative amount is refused either way. */\n positive?: boolean;\n /** The largest amount, in whichever unit is chosen — NumberField's `max`: a typed amount\n * above it commits as `max`. A limit in a physical unit (\"at most 25 kg\") is the form's\n * validation, which sees the unit too. */\n max?: number;\n /**\n * Decimals per unit, over {@link QUANTITY_DIGITS} (g 0, kg 3, ml 0, cl 1, dl 2, l 3,\n * pcs 0): `{ kg: 2 }`, `{ pcs: 1 }`. A typed amount is rounded to them on commit, and\n * shown with at most that many. Cut to the unit's milli grid (g and ml none, cl 1, dl 2,\n * kg, l and pcs 3), so every amount still sums exactly.\n */\n digits?: Partial<Record<QuantityUnit, number>>;\n /** BCP 47 tag for the amount's decimal mark (\"1,5\" in de-DE, \"1.5\" in de-CH); default\n * the `<UiKitProvider locale>`, else the runtime's — NumberField's `locale`. */\n locale?: string;\n /**\n * Another unit converts the amount: `true` turns 500 g into 0.5 kg. `false` (default)\n * keeps the amount as typed — 500 g becomes 500 kg — because the usual reason to change\n * the unit is that the unit was wrong, not the weight. Either way the amount lands on\n * the new unit's grid (1.5 kg kept as typed in grams is 2 g); a conversion within a\n * family is exact, and across families (`units` mixing them) the amount is kept.\n */\n convertOnUnitChange?: boolean;\n /** Both controls, the amount and the unit. */\n disabled?: boolean;\n /** Paints both controls as wrong — {@link Input}'s `invalid`. */\n invalid?: boolean;\n /** The amount `<input>`'s id — what a label's `htmlFor` points at. */\n id?: string;\n /** Names the amount when there is no label (a table cell). The unit select is named\n * after it: \"Unit for “<aria-label>”\". */\n \"aria-label\"?: string;\n /** Set on the amount input (and the unit select, which the same caption and error\n * describe) — a form library's control slot spreads these. */\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean | \"true\" | \"false\";\n \"aria-required\"?: boolean | \"true\" | \"false\";\n placeholder?: string;\n /** Prop > `<UiKitProvider labels={{ quantity }}>` > English. */\n labels?: LabelOverride<QuantityLabels>;\n /** The root: the row holding the amount and the unit. */\n className?: string;\n /** The amount `<input>`. */\n inputClassName?: string;\n /** The unit `<select>`. */\n selectClassName?: string;\n /** The amount `<input>` — focus on error, `select()`. */\n ref?: Ref<HTMLInputElement>;\n}\n\n/** @internal What {@link QuantityField} hands the row on top of the bare control's props. */\nexport interface QuantityRowProps extends QuantityInputProps {\n /** The floating label in the amount's box. */\n label?: ReactNode;\n /** What rides the label's line — a resolved `labelHint` (a FieldHint, a help \"?\"). */\n labelHint?: ReactNode;\n /** The field's name for the unit select's (\"Unit for “Flour”\") where the label is\n * drawn outside the row — RhfQuantityField's, above it. Default the label's text, else\n * `aria-label`. */\n fieldName?: string;\n}\n\n/** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a\n * production one, and never under vitest. Read per call so a test can switch it on with\n * `vi.stubEnv`. Optional-chained: `import.meta.env` is a Vite injection. */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\n/** A unit the kit knows — a `units` entry may be a typo, or a string off an API. */\nfunction isUnit(unit: unknown): unit is QuantityUnit {\n return typeof unit === \"string\" && (QUANTITY_UNITS as readonly string[]).includes(unit);\n}\n\n/** The decimals `unit` is entered with: the caller's, else the default, cut to the grid. */\nfunction digitsFor(unit: QuantityUnit, digits: QuantityInputProps[\"digits\"]): number {\n const wanted = digits?.[unit] ?? QUANTITY_DIGITS[unit] ?? 3;\n const whole = Number.isFinite(wanted) ? Math.trunc(wanted) : QUANTITY_DIGITS[unit];\n return Math.min(Math.max(whole, 0), isUnit(unit) ? quantityMaxDigits(unit) : 3);\n}\n\n/**\n * `amount` on `unit`'s milli grid, for a unit switch: half away from zero, except that a\n * positive amount never rounds to 0 under `positive` — 0.4 kg kept as typed in grams is\n * 1 g, the smallest amount the field would accept, not a 0 it would refuse.\n */\nfunction onGrid(amount: number, unit: QuantityUnit, positive: boolean): number {\n const places = isUnit(unit) ? quantityMaxDigits(unit) : 3;\n const rounded = roundHalfAway(amount, places);\n return positive && rounded <= 0 && amount > 0 ? 10 ** -places : rounded;\n}\n\n/** The units the select offers: `units` (known ones, deduplicated), else `family`'s,\n * else all — plus the value's own unit while it is outside them. Small to large. */\nfunction offeredUnits(\n units: readonly QuantityUnit[] | undefined,\n family: QuantityFamily | undefined,\n current: QuantityUnit,\n): QuantityUnit[] {\n const known = units?.filter(isUnit) ?? [];\n const allowed = known.length > 0 ? known : QUANTITY_UNITS.filter((u) => !family || quantityFamily(u) === family);\n return QUANTITY_UNITS.filter((u) => allowed.includes(u) || u === current);\n}\n\n/** @internal The unit a value with none starts in — the first one the select would offer\n * (RhfQuantityField's, for a form whose default is `null`). */\nexport function firstOfferedUnit(units: readonly QuantityUnit[] | undefined, family: QuantityFamily | undefined) {\n const known = units?.filter(isUnit) ?? [];\n return QUANTITY_UNITS.find((u) => (known.length > 0 ? known.includes(u) : !family || quantityFamily(u) === family))!;\n}\n\n/** @internal The amount and unit row, with the floating label {@link QuantityField} gives\n * it. Not on the barrel: QuantityInput is this without a label. */\nexport function QuantityRow(props: QuantityRowProps) {\n const {\n ref,\n value,\n onChange,\n family,\n units,\n nullable = false,\n positive = true,\n max,\n digits,\n locale: localeProp,\n convertOnUnitChange = false,\n disabled,\n invalid,\n id,\n \"aria-label\": ariaLabel,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-required\": ariaRequired,\n placeholder,\n labels: labelsProp,\n className,\n inputClassName,\n selectClassName,\n label,\n labelHint,\n fieldName,\n } = props;\n const labels = useKitLabels(\"quantity\", DEFAULT_QUANTITY_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const mark = decimalMark(locale);\n // A number that is not finite is no amount, as NumberField reads its value (INP-11).\n const amount = value.amount !== null && Number.isFinite(value.amount) ? value.amount : null;\n const unit = value.unit;\n const places = digitsFor(unit, digits);\n // `formatResult` for fixed notation (\"1e-7\" would read back as 17), in the locale's mark.\n const format = (n: number | null, decimals: number) =>\n n === null ? \"\" : localizeMark(formatResult(roundHalfAway(n, decimals)), mark);\n\n // The draft the amount shows while it is typed — NumberField's loop: kept as typed,\n // resolved on commit, re-rendered from the value when that changes from outside (a\n // reset, a unit switch, a suggestion taken) or when the mark or the digits do.\n const [draft, setDraft] = useState(() => format(amount, places));\n const [seen, setSeen] = useState({ amount, unit, mark, places });\n if (!Object.is(seen.amount, amount) || seen.unit !== unit || seen.mark !== mark || seen.places !== places) {\n setSeen({ amount, unit, mark, places });\n setDraft(format(amount, places));\n }\n\n const commit = (text: string) => {\n const trimmed = text.trim();\n // Unparsable, refused or emptied without `nullable`: back to the value, as NumberField\n // snaps back — never a NaN, a 0 or a negative sent on for the server to reject.\n const snapBack = () => setDraft(format(amount, places));\n let next: number | null;\n if (trimmed === \"\") {\n if (!nullable) return snapBack();\n next = null;\n } else {\n // NumberInput's committed text: an evaluated calculation, dot-decimal.\n const parsed = Number(trimmed.replace(/,/g, \".\"));\n if (!Number.isFinite(parsed)) return snapBack();\n next = roundHalfAway(parsed, places);\n if (max !== undefined && next > max) next = max;\n if (next < 0 || (positive && next === 0)) return snapBack();\n }\n setDraft(format(next, places));\n if (!Object.is(next, amount)) onChange({ amount: next, unit });\n };\n\n const pickUnit = (picked: string) => {\n if (!isUnit(picked) || picked === unit) return;\n let next = amount;\n if (next !== null) {\n if (convertOnUnitChange) next = convertQuantity({ amount: next, unit }, picked)?.amount ?? next;\n next = onGrid(next, picked, positive);\n }\n onChange({ amount: next, unit: picked });\n };\n\n const offered = offeredUnits(units, family, unit);\n // The units warning, once per field, development only. Keyed on the list's text: a\n // caller's inline `units={[\"ml\", \"dl\", \"l\"]}` is a new array on every render.\n const warned = useRef(false);\n const unitsKey = units === undefined ? undefined : JSON.stringify(units);\n useEffect(() => {\n if (warned.current || !devWarnings() || unitsKey === undefined) return;\n const list = JSON.parse(unitsKey) as unknown[];\n const unknown = list.filter((u) => !isUnit(u));\n const outside = family ? list.filter((u) => isUnit(u) && quantityFamily(u) !== family) : [];\n if (unknown.length === 0 && outside.length === 0 && list.length > 0) return;\n warned.current = true;\n const problem =\n list.length === 0\n ? \"is empty, so the field offers its family's units\"\n : unknown.length > 0\n ? `names ${JSON.stringify(unknown)}, which no quantity is measured in`\n : `${JSON.stringify(outside)} ${outside.length === 1 ? \"lies\" : \"lie\"} outside family \"${family}\"`;\n console.warn(\n `[ui-kit] QuantityInput: \\`units\\` ${problem}. \\`units\\` wins over \\`family\\`: give units of the family, ` +\n `or leave \\`family\\` out. (Development builds only; once per field.)`,\n );\n }, [unitsKey, family]);\n\n const generatedId = useId();\n const fieldId = id ?? generatedId;\n const name = fieldName ?? plainText(label) ?? ariaLabel;\n const isInvalid = Boolean(invalid) || ariaInvalid === true || ariaInvalid === \"true\";\n const labelled = label !== undefined;\n\n return (\n // `items-end`: at Large the amount's label stands above its box (§10.17), and the\n // select lines up with the box, not with the label.\n <div {...dataAttributes(props)} className={cn(\"flex items-end\", className)}>\n <NumberInput\n ref={ref}\n id={fieldId}\n label={label}\n // Only when there is one: NumberInput keeps a wrapper box for a passed `hint` key,\n // and the box would take the flex item's place in this row.\n {...(labelHint !== undefined ? { hint: labelHint } : {})}\n aria-label={ariaLabel}\n aria-describedby={ariaDescribedBy}\n aria-required={ariaRequired}\n value={draft}\n onChange={(text) => setDraft(localizeMark(text, mark))}\n onCommit={commit}\n locale={locale}\n placeholder={placeholder}\n disabled={disabled}\n invalid={isInvalid}\n // A count or a weight is typed, not worked out; the trigger beside a unit select\n // in a table cell is noise. A typed \"2*500\" still evaluates on commit.\n calculator={false}\n // The focused half paints its whole frame over the shared border.\n className=\"min-w-0 flex-1 focus-within:z-10\"\n inputClassName={cn(\"rounded-e-none\", inputClassName)}\n />\n <Select\n id={`${fieldId}-unit`}\n aria-label={labels.unit(name)}\n aria-describedby={ariaDescribedBy}\n value={unit}\n onChange={(e) => pickUnit(e.target.value)}\n disabled={disabled}\n invalid={isInvalid}\n // One border between the two halves, not two side by side.\n className=\"-ms-px shrink-0 focus-within:z-10\"\n // As tall as the floating field beside it, its value on the same line.\n selectClassName={cn(\"w-auto rounded-s-none\", labelled && FIELD_FLOATING_PAD, selectClassName)}\n >\n {offered.map((u) => (\n <option key={u} value={u}>\n {u === \"pcs\" ? labels.pcs : u}\n </option>\n ))}\n </Select>\n </div>\n );\n}\n\n/**\n * A quantity — an amount and its unit — as one control: the amount, typed in the\n * locale's decimal mark, beside a select of units. The bare control, for a table row or\n * a line of a list; {@link QuantityField} is the labelled field with a caption, an error\n * and a suggestion to take.\n *\n * ```tsx\n * <QuantityInput aria-label=\"Flour\" family=\"mass\" value={qty} onChange={setQty} />\n * ```\n *\n * THE AMOUNT is NumberField's loop over {@link NumberInput}: a draft while it is typed\n * (grouping read off, the locale's mark kept), resolved on blur or Enter — rounded to the\n * unit's `digits`, clamped to `max` — and `onChange` only then. Empty, or text that is no\n * number, snaps back (empty commits `null` with `nullable`); so does 0 under `positive`\n * (the default), and a negative amount always.\n *\n * THE UNIT is a native select of {@link QUANTITY_UNITS} — `family`'s, or `units`' — with\n * pieces in the reader's word (`quantity.pcs`). Picking one is a commit of its own;\n * `convertOnUnitChange` decides whether the amount is converted or kept as typed.\n *\n * The value's arithmetic, formatting and parsing are `lib/quantity`'s — {@link toBaseQuantity},\n * {@link convertQuantity}, {@link sumQuantities}, {@link formatQuantity}, {@link parseQuantity} —\n * for the list's totals and a comparison table outside the field.\n */\nexport function QuantityInput(props: QuantityInputProps) {\n return <QuantityRow {...props} />;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,2 @@
1
+ import type { QuantityLabels } from "./quantity-input.js";
2
+ export declare const DEFAULT_QUANTITY_LABELS: QuantityLabels;
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ const DEFAULT_QUANTITY_LABELS = {
3
+ pcs: "pcs",
4
+ unit: (field) => field ? `Unit for \u201C${field}\u201D` : "Unit",
5
+ suggested: "Suggested",
6
+ takeSuggestion: (quantity) => `Use ${quantity}`
7
+ };
8
+ export {
9
+ DEFAULT_QUANTITY_LABELS
10
+ };
11
+ //# sourceMappingURL=quantity-labels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/quantity-labels.ts"],"sourcesContent":["// The English words of ./quantity-input's QuantityInput and QuantityField, in a module with\n// no component code: the English catalogue (src/i18n/defaults.ts) imports them without the\n// component, so `en` loads as one chunk like the other languages (0.34, KS U8).\n// ./quantity-input re-exports them.\n\nimport type { QuantityLabels } from \"./quantity-input\";\n\nexport const DEFAULT_QUANTITY_LABELS: QuantityLabels = {\n pcs: \"pcs\",\n unit: (field) => (field ? `Unit for “${field}”` : \"Unit\"),\n suggested: \"Suggested\",\n takeSuggestion: (quantity) => `Use ${quantity}`,\n};\n"],"mappings":";AAOO,MAAM,0BAA0C;AAAA,EACrD,KAAK;AAAA,EACL,MAAM,CAAC,UAAW,QAAQ,kBAAa,KAAK,WAAM;AAAA,EAClD,WAAW;AAAA,EACX,gBAAgB,CAAC,aAAa,OAAO,QAAQ;AAC/C;","names":[]}
@@ -47,7 +47,7 @@ const roleUtilities = {
47
47
  };
48
48
  }
49
49
  };
50
- const VERSION = true ? "0.56.0" : "0.0.0-source";
50
+ const VERSION = true ? "0.57.0" : "0.0.0-source";
51
51
  const recommended = {
52
52
  name: "@eifi1/ui-kit/recommended",
53
53
  // Not the tests, as check:tokens skips the kit's: a test names the old class to assert it