@cosmicdrift/kumiko-renderer 0.399.0 → 0.401.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 (75) hide show
  1. package/dist/app/entity-edit-prefill.js +7 -1
  2. package/dist/app/form-schema.js +42 -17
  3. package/dist/app/projection-detail-body.js +7 -0
  4. package/dist/app/projection-detail-header-model.d.ts +1 -1
  5. package/dist/app/projection-detail-header-model.js +2 -2
  6. package/dist/app/projection-detail-header-region.d.ts +1 -0
  7. package/dist/app/projection-detail-header-region.js +1 -1
  8. package/dist/app/projection-detail-header.js +4 -2
  9. package/dist/app/projection-detail-navigation.d.ts +1 -0
  10. package/dist/app/projection-detail-navigation.js +9 -1
  11. package/dist/app/use-related-list-tab-counts.d.ts +5 -0
  12. package/dist/app/use-related-list-tab-counts.js +68 -0
  13. package/dist/components/embedded-list-field.js +69 -91
  14. package/dist/components/embedded-list-paste.d.ts +2 -0
  15. package/dist/components/embedded-list-paste.js +73 -0
  16. package/dist/components/related-list-section.js +4 -8
  17. package/dist/components/render-edit-helpers.js +2 -2
  18. package/dist/components/render-field-choice-inputs.js +1 -1
  19. package/dist/components/use-reference-column-lookups.d.ts +6 -0
  20. package/dist/components/use-reference-column-lookups.js +36 -0
  21. package/dist/components/use-render-list-columns.d.ts +2 -3
  22. package/dist/components/use-render-list-columns.js +4 -31
  23. package/dist/embedded-list-types.d.ts +117 -0
  24. package/dist/embedded-list-types.js +1 -0
  25. package/dist/headless/form/form-controller.js +9 -1
  26. package/dist/headless/index.d.ts +1 -1
  27. package/dist/headless/index.js +1 -1
  28. package/dist/headless/view-model/edit.js +40 -50
  29. package/dist/headless/view-model/embedded-list-cells.d.ts +12 -0
  30. package/dist/headless/view-model/embedded-list-cells.js +49 -0
  31. package/dist/headless/view-model/embedded-list.d.ts +1 -1
  32. package/dist/headless/view-model/embedded-list.js +1 -1
  33. package/dist/headless/view-model/index.d.ts +1 -1
  34. package/dist/headless/view-model/index.js +1 -1
  35. package/dist/headless/view-model/list.js +2 -3
  36. package/dist/headless/view-model/types.d.ts +23 -6
  37. package/dist/hooks/related-list-parent-payload.d.ts +2 -0
  38. package/dist/hooks/related-list-parent-payload.js +7 -0
  39. package/dist/i18n-defaults.js +3 -0
  40. package/dist/primitives.d.ts +4 -88
  41. package/dist/web/app/dashboard-body.js +3 -3
  42. package/dist/web/app/dashboard-panels.d.ts +2 -1
  43. package/dist/web/app/dashboard-panels.js +11 -3
  44. package/dist/web/app/dashboard-reference-list-table.d.ts +17 -0
  45. package/dist/web/app/dashboard-reference-list-table.js +47 -0
  46. package/dist/web/lib/file-upload-form-data.d.ts +2 -0
  47. package/dist/web/lib/file-upload-form-data.js +2 -0
  48. package/dist/web/lib/upload-file-ref.d.ts +1 -1
  49. package/dist/web/primitives/data-table-cell.d.ts +2 -0
  50. package/dist/web/primitives/data-table-cell.js +1 -1
  51. package/dist/web/primitives/embedded-file-cell.d.ts +11 -0
  52. package/dist/web/primitives/embedded-file-cell.js +50 -0
  53. package/dist/web/primitives/embedded-list-cards.d.ts +38 -0
  54. package/dist/web/primitives/embedded-list-cards.js +71 -0
  55. package/dist/web/primitives/embedded-list-cell.d.ts +41 -0
  56. package/dist/web/primitives/embedded-list-cell.js +132 -0
  57. package/dist/web/primitives/embedded-list-input.d.ts +1 -1
  58. package/dist/web/primitives/embedded-list-input.js +29 -142
  59. package/dist/web/primitives/file-upload.d.ts +2 -1
  60. package/dist/web/primitives/file-upload.js +2 -1
  61. package/dist/web/primitives/input-choice-kinds.js +1 -1
  62. package/dist/web/primitives/input-optional-props.d.ts +2 -1
  63. package/dist/web/primitives/input-optional-props.js +2 -1
  64. package/dist/web/primitives/locale-number.d.ts +3 -0
  65. package/dist/web/primitives/locale-number.js +8 -0
  66. package/dist/web/primitives/number-input.d.ts +2 -1
  67. package/dist/web/primitives/number-input.js +4 -11
  68. package/dist/web/primitives/segmented-select.d.ts +6 -1
  69. package/dist/web/primitives/segmented-select.js +8 -4
  70. package/dist/web/widgets/dashboard-list.d.ts +2 -0
  71. package/dist/web/widgets/dashboard-list.js +2 -0
  72. package/dist/web/widgets/status-badge.d.ts +1 -0
  73. package/dist/web/widgets/status-badge.js +1 -1
  74. package/package.json +5 -5
  75. package/src/changes.json +6 -0
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from "react";
2
+ import { type FieldIssue } from "../../headless/index.ts";
3
+ import type { EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps } from "../../index.ts";
4
+ export declare function columnAlignClass(type: EmbeddedListCellType): string;
5
+ export declare function IssueMessages({ issues, testId, constrainWidth, }: {
6
+ readonly issues: readonly FieldIssue[] | undefined;
7
+ readonly testId?: string;
8
+ readonly constrainWidth?: boolean;
9
+ }): ReactNode;
10
+ export declare function readOnlyCellText(column: EmbeddedListColumn, value: unknown, currency: string, locale: string): string;
11
+ export declare function isDerivedCell(column: EmbeddedListColumn, row: Readonly<Record<string, unknown>>): boolean;
12
+ export declare function renderCellControl({ cellId, column, label, value, disabled, onChange, currency, locale, derived, touch, uploadTarget, }: {
13
+ readonly derived: boolean;
14
+ readonly touch: boolean;
15
+ readonly uploadTarget: EmbeddedListInputProps["uploadTarget"];
16
+ readonly cellId: string;
17
+ readonly label: string;
18
+ readonly column: EmbeddedListColumn;
19
+ readonly value: unknown;
20
+ readonly disabled: boolean;
21
+ readonly onChange: (value: unknown) => void;
22
+ readonly currency: string;
23
+ readonly locale: string;
24
+ }): ReactNode;
25
+ type RowActionsProps = {
26
+ readonly rowIndex: number;
27
+ readonly rowsLength: number;
28
+ readonly minItems: number | undefined;
29
+ readonly maxItems: number | undefined;
30
+ readonly onDuplicateRow: (rowIndex: number) => void;
31
+ readonly onMoveRow: (fromIndex: number, toIndex: number) => void;
32
+ readonly onRemoveRow: (rowIndex: number) => void;
33
+ readonly duplicateLabel: string;
34
+ readonly moveUpLabel: string;
35
+ readonly moveDownLabel: string;
36
+ readonly removeLabel: string;
37
+ readonly testIdPrefix: string | undefined;
38
+ readonly buttonSizeClass?: string;
39
+ };
40
+ export declare function RowActions({ rowIndex, rowsLength, minItems, maxItems, onDuplicateRow, onMoveRow, onRemoveRow, duplicateLabel, moveUpLabel, moveDownLabel, removeLabel, testIdPrefix, buttonSizeClass, }: RowActionsProps): ReactNode;
41
+ export {};
@@ -0,0 +1,132 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowDown, ArrowUp, Copy, Trash2 } from "lucide-react";
3
+ import { fieldIssueText } from "../../field-issue-text.js";
4
+ import { fileIdsOf } from "../../file-ids-of.js";
5
+ import { applyFormatSpec } from "../../headless/index.js";
6
+ import { useTranslation } from "../../index.js";
7
+ import { cn } from "../lib/cn.js";
8
+ import { Button as UiButton } from "../ui/button.js";
9
+ import { Checkbox } from "../ui/checkbox.js";
10
+ import { Input as UiInput } from "../ui/input.js";
11
+ import { ComboboxInput } from "./combobox.js";
12
+ import { selectPill } from "./data-table-cell.js";
13
+ import { DateInput } from "./date-input.js";
14
+ import { EmbeddedFileCell } from "./embedded-file-cell.js";
15
+ import { formatLocaleNumber } from "./locale-number.js";
16
+ import { formatMoney, MoneyInput } from "./money-input.js";
17
+ import { NumberInput } from "./number-input.js";
18
+ import { SegmentedControl } from "./segmented-select.js";
19
+ import { TimestampInput } from "./timestamp-input.js";
20
+ export function columnAlignClass(type) {
21
+ switch (type) {
22
+ case "money":
23
+ case "number":
24
+ case "decimal":
25
+ return "text-right";
26
+ case "boolean":
27
+ return "text-center";
28
+ case "date":
29
+ case "timestamp":
30
+ case "text":
31
+ case "select":
32
+ case "reference":
33
+ case "images":
34
+ case "files":
35
+ return "text-left";
36
+ default: {
37
+ const exhaustiveCheck = type;
38
+ return exhaustiveCheck;
39
+ }
40
+ }
41
+ }
42
+ export function IssueMessages({ issues, testId, constrainWidth, }) {
43
+ const t = useTranslation();
44
+ if (issues === undefined || issues.length === 0)
45
+ return null;
46
+ return (_jsx("div", { role: "alert", "data-testid": testId, className: cn("text-xs text-destructive", constrainWidth === true && "max-w-[16rem] whitespace-normal break-words"), children: issues.map((issue) => (_jsx("div", { children: fieldIssueText(t, issue) }, `${issue.path}:${issue.code}`))) }));
47
+ }
48
+ export function readOnlyCellText(column, value, currency, locale) {
49
+ if (value === null || value === undefined || value === "")
50
+ return "";
51
+ switch (column.type) {
52
+ case "boolean":
53
+ return value === true ? "✓" : "";
54
+ case "date":
55
+ case "timestamp":
56
+ return applyFormatSpec({ format: column.type, locale }, value);
57
+ case "number":
58
+ case "decimal":
59
+ return typeof value === "number"
60
+ ? formatLocaleNumber(value, locale, { grouping: true })
61
+ : String(value);
62
+ case "money":
63
+ return typeof value === "number" ? formatMoney(value, currency, locale) : String(value);
64
+ case "select":
65
+ return typeof value === "string" ? (column.optionLabels?.[value] ?? value) : String(value);
66
+ case "reference":
67
+ return typeof value === "string"
68
+ ? (column.referenceOptions?.find((o) => o.value === value)?.label ?? value)
69
+ : String(value);
70
+ case "text":
71
+ return String(value);
72
+ case "images":
73
+ case "files":
74
+ return String(fileIdsOf(value).length);
75
+ default: {
76
+ const exhaustiveCheck = column.type;
77
+ return exhaustiveCheck;
78
+ }
79
+ }
80
+ }
81
+ export function isDerivedCell(column, row) {
82
+ return column.isDerivedInRow?.(row) ?? column.derived;
83
+ }
84
+ export function renderCellControl({ cellId, column, label, value, disabled, onChange, currency, locale, derived, touch, uploadTarget, }) {
85
+ if (column.readOnly === true && !derived) {
86
+ return (_jsx("div", { "data-cell-id": cellId, "data-readonly-cell": "", className: cn("min-w-0 whitespace-normal break-words py-1.5 text-sm", columnAlignClass(column.type)), children: readOnlyCellText(column, value, currency, locale) }));
87
+ }
88
+ const isDisabled = disabled || derived;
89
+ const controlHeight = touch ? "h-11" : undefined;
90
+ switch (column.type) {
91
+ case "text":
92
+ return (_jsx(UiInput, { "data-cell-id": cellId, type: "text", "aria-label": label, className: cn("min-w-0", controlHeight), disabled: isDisabled, value: typeof value === "string" ? value : "", onChange: (e) => onChange(e.target.value) }));
93
+ case "number":
94
+ case "decimal":
95
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(NumberInput, { id: cellId, name: cellId, ariaLabel: label, locale: locale, value: typeof value === "number" ? value : "", onChange: (v) => onChange(v), disabled: isDisabled, className: cn("text-right", controlHeight) }) }));
96
+ case "boolean":
97
+ return (_jsx("div", { className: "flex justify-center", children: _jsx(Checkbox, { "data-cell-id": cellId, "aria-label": label, disabled: isDisabled, checked: value === true, onCheckedChange: (checked) => onChange(checked === true) }) }));
98
+ case "date":
99
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(DateInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), disabled: isDisabled }) }));
100
+ case "timestamp":
101
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(TimestampInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), disabled: isDisabled }) }));
102
+ case "money":
103
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(MoneyInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "number" ? value : "", onChange: (v) => onChange(v), currency: currency, disabled: isDisabled }) }));
104
+ case "images":
105
+ case "files":
106
+ return (_jsx(EmbeddedFileCell, { cellId: cellId, label: label, column: column, value: value, readOnly: isDisabled, uploadTarget: uploadTarget, onChange: onChange }));
107
+ case "select": {
108
+ if (derived) {
109
+ return (_jsx("div", { "data-cell-id": cellId, className: "min-w-0 py-1.5", children: selectPill(value, column.optionLabels, column.optionTones, locale, undefined) }));
110
+ }
111
+ const options = (column.options ?? []).map((opt) => ({
112
+ value: opt,
113
+ label: column.optionLabels?.[opt] ?? opt,
114
+ }));
115
+ if (column.display === "radio" && options.length > 0) {
116
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(SegmentedControl, { id: cellId, name: cellId, labelProps: { "aria-label": label }, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), options: options, disabled: isDisabled, ...(column.optionTones !== undefined && { tones: column.optionTones }), ...(touch && { size: "touch" }) }) }));
117
+ }
118
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(ComboboxInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), options: options, disabled: isDisabled }) }));
119
+ }
120
+ case "reference":
121
+ return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(ComboboxInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), options: column.referenceOptions ?? [], disabled: isDisabled, loading: column.referenceLoading }) }));
122
+ default: {
123
+ const exhaustiveCheck = column.type;
124
+ return exhaustiveCheck;
125
+ }
126
+ }
127
+ }
128
+ export function RowActions({ rowIndex, rowsLength, minItems, maxItems, onDuplicateRow, onMoveRow, onRemoveRow, duplicateLabel, moveUpLabel, moveDownLabel, removeLabel, testIdPrefix, buttonSizeClass = "size-7", }) {
129
+ const duplicateDisabled = maxItems !== undefined && rowsLength >= maxItems;
130
+ const removeDisabled = rowsLength <= (minItems ?? 0);
131
+ return (_jsxs("div", { className: "inline-flex items-center", children: [_jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": duplicateLabel, disabled: duplicateDisabled, onClick: () => onDuplicateRow(rowIndex), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-duplicate` : undefined, children: _jsx(Copy, { className: "size-4", "aria-hidden": "true" }) }), _jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": moveUpLabel, disabled: rowIndex === 0, onClick: () => onMoveRow(rowIndex, rowIndex - 1), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-move-up` : undefined, children: _jsx(ArrowUp, { className: "size-4", "aria-hidden": "true" }) }), _jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": moveDownLabel, disabled: rowIndex === rowsLength - 1, onClick: () => onMoveRow(rowIndex, rowIndex + 1), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-move-down` : undefined, children: _jsx(ArrowDown, { className: "size-4", "aria-hidden": "true" }) }), _jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": removeLabel, disabled: removeDisabled, onClick: () => onRemoveRow(rowIndex), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-remove` : undefined, children: _jsx(Trash2, { className: "size-4", "aria-hidden": "true" }) })] }));
132
+ }
@@ -5,5 +5,5 @@ type ScrollAffordance = {
5
5
  readonly canScrollRight: boolean;
6
6
  };
7
7
  export declare function computeScrollAffordance(scrollLeft: number, scrollWidth: number, clientWidth: number): ScrollAffordance;
8
- export declare function EmbeddedListInput({ id, columns, rows, totals, currency, disabled, fixedRows, minItems, maxItems, listIssues, rowIssues, cellIssues, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, onPasteCells, addLabel, actionsLabel, removeLabel, duplicateLabel, moveUpLabel, moveDownLabel, emptyLabel, emptyCtaLabel, testId, }: EmbeddedListInputProps): ReactNode;
8
+ export declare function EmbeddedListInput({ id, columns, rows, totals, currency, disabled, fixedRows, minItems, maxItems, listIssues, rowIssues, cellIssues, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, onPasteCells, addLabel, actionsLabel, removeLabel, duplicateLabel, moveUpLabel, moveDownLabel, emptyLabel, emptyCtaLabel, testId, uploadTarget, }: EmbeddedListInputProps): ReactNode;
9
9
  export {};
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // EmbeddedListInput — row/totals table for a createEmbeddedListField
3
3
  // (invoice-positions-style) field. Controlled: rows, derived values, and
4
4
  // issue groups are computed by the caller; this component only renders
@@ -19,21 +19,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
19
  // MoneyInput text, IME composition) and focus are lost — accepted trade-off
20
20
  // for the single-mount-per-side fix above. Upgrade: lift per-cell draft
21
21
  // state into a `draftsByCellId` map here that survives the layout swap.
22
- import { ArrowDown, ArrowUp, Copy, Plus, Trash2 } from "lucide-react";
22
+ import { Plus } from "lucide-react";
23
23
  import { Fragment, useEffect, useRef, useState, } from "react";
24
- import { fieldIssueText } from "../../field-issue-text.js";
25
- import { applyFormatSpec } from "../../headless/index.js";
26
24
  import { useLocale, useTranslation } from "../../index.js";
27
25
  import { cn } from "../lib/cn.js";
28
26
  import { Button as UiButton } from "../ui/button.js";
29
- import { Checkbox } from "../ui/checkbox.js";
30
- import { Input as UiInput } from "../ui/input.js";
31
27
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
32
- import { ComboboxInput } from "./combobox.js";
33
- import { DateInput } from "./date-input.js";
34
- import { formatMoney, MoneyInput } from "./money-input.js";
35
- import { SegmentedControl } from "./segmented-select.js";
36
- import { TimestampInput } from "./timestamp-input.js";
28
+ import { EmbeddedListCards } from "./embedded-list-cards.js";
29
+ import { columnAlignClass, IssueMessages, isDerivedCell, RowActions, renderCellControl, } from "./embedded-list-cell.js";
30
+ import { formatMoney } from "./money-input.js";
37
31
  import { useIsNarrowViewport } from "./use-narrow-viewport.js";
38
32
  // `input[type=hidden]` excluded — ComboboxInput renders one as a plain
39
33
  // name-carrier before its focusable trigger button.
@@ -59,6 +53,9 @@ function fixedColumnRem(type) {
59
53
  return 4;
60
54
  case "select":
61
55
  return 12;
56
+ case "images":
57
+ case "files":
58
+ return 14;
62
59
  case "text":
63
60
  case "reference":
64
61
  return undefined;
@@ -80,26 +77,6 @@ function tableMinWidthRem(columns, hasActions) {
80
77
  const columnsRem = columns.reduce((sum, column) => sum + (fixedColumnRem(column.type) ?? FLEXIBLE_COLUMN_MIN_REM), 0);
81
78
  return columnsRem + (hasActions ? ACTIONS_COLUMN_REM : 0);
82
79
  }
83
- function columnAlignClass(type) {
84
- switch (type) {
85
- case "money":
86
- case "number":
87
- case "decimal":
88
- return "text-right";
89
- case "boolean":
90
- return "text-center";
91
- case "date":
92
- case "timestamp":
93
- case "text":
94
- case "select":
95
- case "reference":
96
- return "text-left";
97
- default: {
98
- const exhaustiveCheck = type;
99
- return exhaustiveCheck;
100
- }
101
- }
102
- }
103
80
  // Exported (unlike columnWidthClass/columnAlignClass above) because
104
81
  // happy-dom performs no layout — scrollWidth/clientWidth/getBoundingClientRect
105
82
  // always read 0 in tests, so the only way to cover this decision at all is
@@ -128,88 +105,7 @@ function parsePasteGrid(text) {
128
105
  return withoutCr.split("\t");
129
106
  });
130
107
  }
131
- function IssueMessages({ issues, testId, constrainWidth, }) {
132
- const t = useTranslation();
133
- if (issues === undefined || issues.length === 0)
134
- return null;
135
- return (_jsx("div", { role: "alert", "data-testid": testId, className: cn("text-xs text-destructive", constrainWidth === true && "max-w-[16rem] whitespace-normal break-words"), children: issues.map((issue) => (_jsx("div", { children: fieldIssueText(t, issue) }, `${issue.path}:${issue.code}`))) }));
136
- }
137
- function readOnlyCellText(column, value, currency, locale) {
138
- if (value === null || value === undefined || value === "")
139
- return "";
140
- switch (column.type) {
141
- case "boolean":
142
- return value === true ? "✓" : "";
143
- case "date":
144
- case "timestamp":
145
- return applyFormatSpec({ format: column.type, locale }, value);
146
- case "number":
147
- case "decimal":
148
- return typeof value === "number"
149
- ? applyFormatSpec({ format: column.type, locale }, value)
150
- : String(value);
151
- case "money":
152
- return typeof value === "number" ? formatMoney(value, currency, locale) : String(value);
153
- case "select":
154
- return typeof value === "string" ? (column.optionLabels?.[value] ?? value) : String(value);
155
- case "reference":
156
- return typeof value === "string"
157
- ? (column.referenceOptions?.find((o) => o.value === value)?.label ?? value)
158
- : String(value);
159
- case "text":
160
- return String(value);
161
- default: {
162
- const exhaustiveCheck = column.type;
163
- return exhaustiveCheck;
164
- }
165
- }
166
- }
167
- function renderCellControl({ cellId, column, label, value, disabled, onChange, currency, locale, }) {
168
- if (column.readOnly === true && !column.derived) {
169
- return (_jsx("div", { "data-cell-id": cellId, "data-readonly-cell": "", className: cn("min-w-0 whitespace-normal break-words py-1.5 text-sm", columnAlignClass(column.type)), children: readOnlyCellText(column, value, currency, locale) }));
170
- }
171
- const isDisabled = disabled || column.derived;
172
- switch (column.type) {
173
- case "text":
174
- return (_jsx(UiInput, { "data-cell-id": cellId, type: "text", "aria-label": label, className: "min-w-0", disabled: isDisabled, value: typeof value === "string" ? value : "", onChange: (e) => onChange(e.target.value) }));
175
- case "number":
176
- case "decimal":
177
- return (_jsx(UiInput, { "data-cell-id": cellId, type: "number", "aria-label": label, disabled: isDisabled, className: "min-w-0 text-right tabular-nums", value: typeof value === "number" ? value : "", onChange: (e) => {
178
- const raw = e.target.value;
179
- onChange(raw === "" ? undefined : Number(raw));
180
- } }));
181
- case "boolean":
182
- return (_jsx("div", { className: "flex justify-center", children: _jsx(Checkbox, { "data-cell-id": cellId, "aria-label": label, disabled: isDisabled, checked: value === true, onCheckedChange: (checked) => onChange(checked === true) }) }));
183
- case "date":
184
- return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(DateInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), disabled: isDisabled }) }));
185
- case "timestamp":
186
- return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(TimestampInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), disabled: isDisabled }) }));
187
- case "money":
188
- return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(MoneyInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "number" ? value : "", onChange: (v) => onChange(v), currency: currency, disabled: isDisabled }) }));
189
- case "select": {
190
- const options = (column.options ?? []).map((opt) => ({
191
- value: opt,
192
- label: column.optionLabels?.[opt] ?? opt,
193
- }));
194
- if (column.display === "radio" && options.length > 0) {
195
- return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(SegmentedControl, { id: cellId, name: cellId, labelProps: { "aria-label": label }, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), options: options, disabled: isDisabled }) }));
196
- }
197
- return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(ComboboxInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), options: options, disabled: isDisabled }) }));
198
- }
199
- case "reference":
200
- return (_jsx("div", { "data-cell-id": cellId, className: "w-full min-w-0", children: _jsx(ComboboxInput, { id: cellId, name: cellId, ariaLabel: label, value: typeof value === "string" ? value : "", onChange: (v) => onChange(v), options: column.referenceOptions ?? [], disabled: isDisabled, loading: column.referenceLoading }) }));
201
- default: {
202
- const exhaustiveCheck = column.type;
203
- return exhaustiveCheck;
204
- }
205
- }
206
- }
207
- function RowActions({ rowIndex, rowsLength, minItems, maxItems, onDuplicateRow, onMoveRow, onRemoveRow, duplicateLabel, moveUpLabel, moveDownLabel, removeLabel, testIdPrefix, buttonSizeClass = "size-7", }) {
208
- const duplicateDisabled = maxItems !== undefined && rowsLength >= maxItems;
209
- const removeDisabled = rowsLength <= (minItems ?? 0);
210
- return (_jsxs("div", { className: "inline-flex items-center", children: [_jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": duplicateLabel, disabled: duplicateDisabled, onClick: () => onDuplicateRow(rowIndex), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-duplicate` : undefined, children: _jsx(Copy, { className: "size-4", "aria-hidden": "true" }) }), _jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": moveUpLabel, disabled: rowIndex === 0, onClick: () => onMoveRow(rowIndex, rowIndex - 1), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-move-up` : undefined, children: _jsx(ArrowUp, { className: "size-4", "aria-hidden": "true" }) }), _jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": moveDownLabel, disabled: rowIndex === rowsLength - 1, onClick: () => onMoveRow(rowIndex, rowIndex + 1), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-move-down` : undefined, children: _jsx(ArrowDown, { className: "size-4", "aria-hidden": "true" }) }), _jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: buttonSizeClass, "aria-label": removeLabel, disabled: removeDisabled, onClick: () => onRemoveRow(rowIndex), "data-testid": testIdPrefix !== undefined ? `${testIdPrefix}-remove` : undefined, children: _jsx(Trash2, { className: "size-4", "aria-hidden": "true" }) })] }));
211
- }
212
- export function EmbeddedListInput({ id, columns, rows, totals, currency, disabled, fixedRows, minItems, maxItems, listIssues, rowIssues, cellIssues, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, onPasteCells, addLabel, actionsLabel, removeLabel, duplicateLabel, moveUpLabel, moveDownLabel, emptyLabel, emptyCtaLabel, testId, }) {
108
+ export function EmbeddedListInput({ id, columns, rows, totals, currency, disabled, fixedRows, minItems, maxItems, listIssues, rowIssues, cellIssues, onCellChange, onAddRow, onRemoveRow, onDuplicateRow, onMoveRow, onPasteCells, addLabel, actionsLabel, removeLabel, duplicateLabel, moveUpLabel, moveDownLabel, emptyLabel, emptyCtaLabel, testId, uploadTarget, }) {
213
109
  // Callers that don't wire up the field-currency plumbing (or direct
214
110
  // callers/tests) keep getting the same "EUR" this
215
111
  // component always hardcoded.
@@ -339,6 +235,10 @@ export function EmbeddedListInput({ id, columns, rows, totals, currency, disable
339
235
  return (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { ref: containerRef, "data-testid": testId !== undefined ? `${testId}-empty` : "embedded-list-empty", className: "flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-10 text-center text-sm text-muted-foreground", children: [_jsx("span", { children: emptyLabel }), showControls && (_jsxs(UiButton, { type: "button", variant: "outline", size: "sm", onClick: onAddRow, "data-testid": testId !== undefined ? `${testId}-empty-add` : "embedded-list-empty-add", children: [_jsx(Plus, { className: "size-4", "aria-hidden": "true" }), emptyCtaLabel] }))] }), _jsx(IssueMessages, { issues: listIssues, testId: testIdFor("list-issues") })] }));
340
236
  }
341
237
  const hasTotals = totals !== undefined && totals.length > 0;
238
+ const firstColumn = columns[0];
239
+ const titleColumn = firstColumn?.type === "text" && firstColumn.readOnly === true && firstColumn.derived !== true
240
+ ? firstColumn
241
+ : undefined;
342
242
  return (_jsxs("div", { ref: containerRef, "data-testid": testId, className: "min-w-0", children: [!isMobile && (_jsxs("div", { "data-testid": testIdFor("desktop"), className: "hidden min-w-0 md:block", children: [_jsxs("div", { className: "min-w-0 overflow-hidden rounded-lg border bg-card", children: [_jsxs("div", { className: "relative min-w-0", children: [_jsx("div", { ref: setScrollContainerEl, "data-testid": testIdFor("desktop-scroll"), className: "overflow-x-auto [&_[data-slot=table-container]]:overflow-x-visible", children: _jsxs(Table, { className: "w-full table-fixed", style: { minWidth: `${tableMinWidthRem(columns, showControls)}rem` }, children: [_jsxs("colgroup", { children: [columns.map((column) => (_jsx("col", { style: columnWidthStyle(column.type) }, column.field))), showControls && _jsx("col", { style: { width: `${ACTIONS_COLUMN_REM}rem` } })] }), _jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { className: "hover:bg-transparent", children: [columns.map((column) => (_jsx(TableHead, { title: column.label, className: cn("truncate", columnAlignClass(column.type)), children: column.label }, column.field))), showControls && (_jsx(TableHead, { className: cn("sticky right-0 z-10 whitespace-nowrap bg-muted px-1 text-right", isHorizontallyScrollable &&
343
243
  "shadow-[inset_1px_0_0_var(--color-border)]"), "aria-label": actionsLabel }))] }) }), _jsx(TableBody, { children: rows.map((row, rowIndex) => {
344
244
  const isLastRow = rowIndex === rows.length - 1;
@@ -348,36 +248,23 @@ export function EmbeddedListInput({ id, columns, rows, totals, currency, disable
348
248
  , { children: [_jsxs(TableRow, { className: "group", "data-testid": testIdFor(`row-${rowIndex}`), children: [columns.map((column, columnIndex) => {
349
249
  const isLastCell = isLastRow && columnIndex === lastEditableIndex;
350
250
  const issues = cellIssues?.[`${rowIndex}.${column.field}`];
351
- return (_jsxs(TableCell, { "data-testid": testIdFor(`cell-${rowIndex}-${column.field}`), onPaste: onPasteCells !== undefined
251
+ const hidden = column.isHiddenInRow?.(row) === true;
252
+ return (_jsx(TableCell, { "data-testid": testIdFor(`cell-${rowIndex}-${column.field}`), onPaste: onPasteCells !== undefined
352
253
  ? handlePaste(rowIndex, columnIndex)
353
- : undefined, onKeyDown: isLastCell ? handleLastCellKeyDown : undefined, children: [renderCellControl({
354
- cellId: cellId(rowIndex, column.field),
355
- label: cellLabel(column, rowIndex),
356
- column,
357
- value: row[column.field],
358
- disabled: disabled === true,
359
- onChange: (value) => onCellChange(rowIndex, column.field, value),
360
- currency: effectiveCurrency,
361
- locale: resolvedLocale,
362
- }), _jsx(IssueMessages, { issues: issues, testId: testIdFor(`cell-${rowIndex}-${column.field}-errors`), constrainWidth: true })] }, column.field));
254
+ : undefined, onKeyDown: isLastCell ? handleLastCellKeyDown : undefined, children: !hidden && (_jsxs(_Fragment, { children: [renderCellControl({
255
+ cellId: cellId(rowIndex, column.field),
256
+ label: cellLabel(column, rowIndex),
257
+ column,
258
+ value: row[column.field],
259
+ disabled: disabled === true,
260
+ onChange: (value) => onCellChange(rowIndex, column.field, value),
261
+ currency: effectiveCurrency,
262
+ locale: resolvedLocale,
263
+ derived: isDerivedCell(column, row),
264
+ touch: false,
265
+ uploadTarget,
266
+ }), _jsx(IssueMessages, { issues: issues, testId: testIdFor(`cell-${rowIndex}-${column.field}-errors`), constrainWidth: true })] })) }, column.field));
363
267
  }), showControls && (_jsx(TableCell, { className: cn("sticky right-0 z-10 whitespace-nowrap bg-card px-1 text-right group-hover:bg-muted", isHorizontallyScrollable &&
364
268
  "shadow-[inset_1px_0_0_var(--color-border)]"), children: _jsx(RowActions, { rowIndex: rowIndex, rowsLength: rows.length, minItems: minItems, maxItems: maxItems, onDuplicateRow: onDuplicateRow, onMoveRow: onMoveRow, onRemoveRow: onRemoveRow, duplicateLabel: duplicateLabel, moveUpLabel: moveUpLabel, moveDownLabel: moveDownLabel, removeLabel: removeLabel, testIdPrefix: testIdFor(`row-${rowIndex}`) }) }))] }), rowIssuesForRow !== undefined && rowIssuesForRow.length > 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length + (showControls ? 1 : 0), children: _jsx(IssueMessages, { issues: rowIssuesForRow, testId: testIdFor(`row-${rowIndex}-issues`) }) }) }))] }, rowIndex));
365
- }) })] }) }), scrollAffordance.canScrollLeft && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-card to-transparent" })), scrollAffordance.canScrollRight && !showControls && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-card to-transparent" }))] }), showControls && (_jsx("div", { className: "border-t p-2", children: _jsxs(UiButton, { type: "button", variant: "ghost", size: "sm", onClick: onAddRow, disabled: addDisabled, "data-testid": testIdFor("add"), children: [_jsx(Plus, { className: "size-4", "aria-hidden": "true" }), addLabel] }) })), hasTotals && (_jsx("div", { "data-testid": testIdFor("totals"), className: "flex flex-wrap items-center justify-end gap-6 border-t bg-muted/30 px-4 py-3 text-sm", children: totals.map((total) => (_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: total.label }), _jsx("span", { className: "font-medium tabular-nums", children: formatTotalValue(total, columns, effectiveCurrency, resolvedLocale) })] }, total.field))) }))] }), _jsx(IssueMessages, { issues: listIssues, testId: testIdFor("list-issues") })] })), isMobile && (_jsxs("div", { "data-testid": testIdFor("mobile"), className: "md:hidden flex flex-col gap-3", children: [rows.map((row, rowIndex) => {
366
- const isLastRow = rowIndex === rows.length - 1;
367
- const rowIssuesForRow = rowIssues?.[rowIndex];
368
- return (_jsxs("div", { "data-testid": testIdFor(`row-${rowIndex}`), className: "flex flex-col gap-3 rounded-lg border bg-card p-4", children: [columns.map((column, columnIndex) => {
369
- const isLastCell = isLastRow && columnIndex === lastEditableIndex;
370
- const issues = cellIssues?.[`${rowIndex}.${column.field}`];
371
- return (_jsxs("div", { className: "flex flex-col gap-1", onPaste: onPasteCells !== undefined ? handlePaste(rowIndex, columnIndex) : undefined, onKeyDown: isLastCell ? handleLastCellKeyDown : undefined, children: [_jsx("span", { className: "text-xs font-medium text-muted-foreground", children: column.label }), renderCellControl({
372
- cellId: cellId(rowIndex, column.field),
373
- label: cellLabel(column, rowIndex),
374
- column,
375
- value: row[column.field],
376
- disabled: disabled === true,
377
- onChange: (value) => onCellChange(rowIndex, column.field, value),
378
- currency: effectiveCurrency,
379
- locale: resolvedLocale,
380
- }), _jsx(IssueMessages, { issues: issues, testId: testIdFor(`cell-${rowIndex}-${column.field}-errors`) })] }, column.field));
381
- }), rowIssuesForRow !== undefined && rowIssuesForRow.length > 0 && (_jsx(IssueMessages, { issues: rowIssuesForRow, testId: testIdFor(`row-${rowIndex}-issues`) })), showControls && (_jsx("div", { className: "flex items-center justify-end gap-1 border-t pt-3", children: _jsx(RowActions, { rowIndex: rowIndex, rowsLength: rows.length, minItems: minItems, maxItems: maxItems, onDuplicateRow: onDuplicateRow, onMoveRow: onMoveRow, onRemoveRow: onRemoveRow, duplicateLabel: duplicateLabel, moveUpLabel: moveUpLabel, moveDownLabel: moveDownLabel, removeLabel: removeLabel, testIdPrefix: testIdFor(`row-${rowIndex}`), buttonSizeClass: "size-10" }) }))] }, rowIndex));
382
- }), showControls && (_jsxs(UiButton, { type: "button", variant: "outline", onClick: onAddRow, disabled: addDisabled, "data-testid": testIdFor("add"), children: [_jsx(Plus, { className: "size-4", "aria-hidden": "true" }), addLabel] })), hasTotals && (_jsx("div", { "data-testid": testIdFor("totals"), className: "flex flex-col gap-2 rounded-lg border bg-muted/30 p-4 text-sm", children: totals.map((total) => (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: total.label }), _jsx("span", { className: "font-medium tabular-nums", children: formatTotalValue(total, columns, effectiveCurrency, resolvedLocale) })] }, total.field))) })), _jsx(IssueMessages, { issues: listIssues, testId: testIdFor("list-issues") })] }))] }));
269
+ }) })] }) }), scrollAffordance.canScrollLeft && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-card to-transparent" })), scrollAffordance.canScrollRight && !showControls && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-card to-transparent" }))] }), showControls && (_jsx("div", { className: "border-t p-2", children: _jsxs(UiButton, { type: "button", variant: "ghost", size: "sm", onClick: onAddRow, disabled: addDisabled, "data-testid": testIdFor("add"), children: [_jsx(Plus, { className: "size-4", "aria-hidden": "true" }), addLabel] }) })), hasTotals && (_jsx("div", { "data-testid": testIdFor("totals"), className: "flex flex-wrap items-center justify-end gap-6 border-t bg-muted/30 px-4 py-3 text-sm", children: totals.map((total) => (_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: total.label }), _jsx("span", { className: "font-medium tabular-nums", children: formatTotalValue(total, columns, effectiveCurrency, resolvedLocale) })] }, total.field))) }))] }), _jsx(IssueMessages, { issues: listIssues, testId: testIdFor("list-issues") })] })), isMobile && (_jsx(EmbeddedListCards, { columns: columns, rows: rows, titleColumn: titleColumn, lastEditableIndex: lastEditableIndex, rowIssues: rowIssues, cellIssues: cellIssues, listIssues: listIssues, totalsNode: hasTotals && (_jsx("div", { "data-testid": testIdFor("totals"), className: "flex flex-col gap-2 rounded-lg border bg-muted/30 p-4 text-sm", children: totals.map((total) => (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: total.label }), _jsx("span", { className: "font-medium tabular-nums", children: formatTotalValue(total, columns, effectiveCurrency, resolvedLocale) })] }, total.field))) })), disabled: disabled === true, showControls: showControls, addDisabled: addDisabled, minItems: minItems, maxItems: maxItems, currency: effectiveCurrency, locale: resolvedLocale, uploadTarget: uploadTarget, onCellChange: onCellChange, onAddRow: onAddRow, onRemoveRow: onRemoveRow, onDuplicateRow: onDuplicateRow, onMoveRow: onMoveRow, handlePaste: handlePaste, handleLastCellKeyDown: handleLastCellKeyDown, pasteEnabled: onPasteCells !== undefined, cellId: cellId, cellLabel: cellLabel, testId: testId, addLabel: addLabel, duplicateLabel: duplicateLabel, moveUpLabel: moveUpLabel, moveDownLabel: moveDownLabel, removeLabel: removeLabel }))] }));
383
270
  }
@@ -10,6 +10,7 @@ export type FileUploadInputProps = {
10
10
  readonly uploadActionForm?: string;
11
11
  readonly entityId?: string;
12
12
  readonly fieldName?: string;
13
+ readonly subField?: string;
13
14
  readonly imageVariant?: string;
14
15
  readonly capture?: "environment" | "user";
15
16
  /** Folder picker (web-only, `webkitdirectory`): hands the picked files with
@@ -19,4 +20,4 @@ export type FileUploadInputProps = {
19
20
  readonly pickLabel?: string;
20
21
  readonly testId?: string;
21
22
  };
22
- export declare function FileUploadInput({ kind, id, value, onChange, accept, disabled, entityType, uploadActionForm, entityId, fieldName, imageVariant, capture, directory, onFilesPicked, pickLabel, testId, }: FileUploadInputProps): ReactNode;
23
+ export declare function FileUploadInput({ kind, id, value, onChange, accept, disabled, entityType, uploadActionForm, entityId, fieldName, subField, imageVariant, capture, directory, onFilesPicked, pickLabel, testId, }: FileUploadInputProps): ReactNode;
@@ -15,7 +15,7 @@ import { Button as UiButton } from "../ui/button.js";
15
15
  import { FileNameLink, FileThumbnail } from "./file-thumbnail.js";
16
16
  // React's input typings have no `webkitdirectory`; spreading keeps it typed-cast-free.
17
17
  const DIRECTORY_INPUT_ATTRS = { webkitdirectory: "", multiple: true };
18
- export function FileUploadInput({ kind, id, value, onChange, accept, disabled, entityType, uploadActionForm, entityId, fieldName, imageVariant, capture, directory, onFilesPicked, pickLabel, testId, }) {
18
+ export function FileUploadInput({ kind, id, value, onChange, accept, disabled, entityType, uploadActionForm, entityId, fieldName, subField, imageVariant, capture, directory, onFilesPicked, pickLabel, testId, }) {
19
19
  const t = useTranslation();
20
20
  const sessionEnded = useSessionEndedSignal();
21
21
  const inputRef = useRef(null);
@@ -39,6 +39,7 @@ export function FileUploadInput({ kind, id, value, onChange, accept, disabled, e
39
39
  entityType,
40
40
  entityId,
41
41
  fieldName,
42
+ subField,
42
43
  actionForm: uploadActionForm,
43
44
  }));
44
45
  }
@@ -33,7 +33,7 @@ export const renderSelectKind = (props) => {
33
33
  return (_jsx(RadioListSelect, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: radioGroupOptions, ...(props.radioVariant !== undefined && { variant: props.radioVariant }), ...stateProps(props) }));
34
34
  }
35
35
  if (presentation === "segmented" && radioGroupOptions.length > 0) {
36
- return (_jsx(SegmentedSelect, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: radioGroupOptions, ...stateProps(props) }));
36
+ return (_jsx(SegmentedSelect, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: radioGroupOptions, ...(props.optionTones !== undefined && { tones: props.optionTones }), ...stateProps(props) }));
37
37
  }
38
38
  // The dropdown shows a disabled option's hint inline, so it stays readable in the trigger and search.
39
39
  return (_jsx(ComboboxInput, { id: props.id, name: props.name, value: props.value, onChange: props.onChange, options: comboOptions.map(withInlineDisabledHint), ...stateProps(props) }));
@@ -13,8 +13,9 @@ type UploadTargetInput = {
13
13
  readonly uploadActionForm?: string;
14
14
  readonly entityId?: string;
15
15
  readonly fieldName?: string;
16
+ readonly subField?: string;
16
17
  };
17
18
  export declare function stateProps({ disabled, required, hasError }: StateInput): StateInput;
18
19
  export declare function localeBoundsProps({ locale, min, max }: LocaleBoundsInput): LocaleBoundsInput;
19
- export declare function uploadTargetProps({ entityType, uploadActionForm, entityId, fieldName, }: UploadTargetInput): UploadTargetInput;
20
+ export declare function uploadTargetProps({ entityType, uploadActionForm, entityId, fieldName, subField, }: UploadTargetInput): UploadTargetInput;
20
21
  export {};
@@ -14,11 +14,12 @@ export function localeBoundsProps({ locale, min, max }) {
14
14
  ...(max !== undefined && { max }),
15
15
  };
16
16
  }
17
- export function uploadTargetProps({ entityType, uploadActionForm, entityId, fieldName, }) {
17
+ export function uploadTargetProps({ entityType, uploadActionForm, entityId, fieldName, subField, }) {
18
18
  return {
19
19
  ...(entityType !== undefined && { entityType }),
20
20
  ...(uploadActionForm !== undefined && { uploadActionForm }),
21
21
  ...(entityId !== undefined && { entityId }),
22
22
  ...(fieldName !== undefined && { fieldName }),
23
+ ...(subField !== undefined && { subField }),
23
24
  };
24
25
  }
@@ -1,2 +1,5 @@
1
1
  export declare function resolveSafeLocale(locale: string | undefined): string;
2
+ export declare function formatLocaleNumber(value: number, locale: string, options: {
3
+ readonly grouping: boolean;
4
+ }): string;
2
5
  export declare function parseLocaleNumber(raw: string, locale: string): number;
@@ -13,6 +13,14 @@ export function resolveSafeLocale(locale) {
13
13
  return FALLBACK_LOCALE;
14
14
  }
15
15
  }
16
+ // 20 is the Intl maximum: the display must never round a stored value.
17
+ const MAX_FRACTION_DIGITS = 20;
18
+ export function formatLocaleNumber(value, locale, options) {
19
+ return new Intl.NumberFormat(resolveSafeLocale(locale), {
20
+ maximumFractionDigits: MAX_FRACTION_DIGITS,
21
+ useGrouping: options.grouping,
22
+ }).format(value);
23
+ }
16
24
  // Separators come from Intl for the locale. A minus is only accepted at the
17
25
  // very start: "1-23" is invalid rather than -123, so a typo never turns into
18
26
  // a wrong amount. Anything besides digits and separators yields NaN so callers
@@ -14,5 +14,6 @@ export type NumberInputProps = {
14
14
  readonly testId?: string;
15
15
  readonly className?: string;
16
16
  readonly ariaDescribedBy?: string;
17
+ readonly ariaLabel?: string;
17
18
  };
18
- export declare function NumberInput({ id, name, value, onChange, locale, grouping, integer, disabled, required, hasError, placeholder, testId, className, ariaDescribedBy, }: NumberInputProps): ReactNode;
19
+ export declare function NumberInput({ id, name, value, onChange, locale, grouping, integer, disabled, required, hasError, placeholder, testId, className, ariaDescribedBy, ariaLabel, }: NumberInputProps): ReactNode;
@@ -9,10 +9,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
9
9
  import { useLayoutEffect, useRef, useState } from "react";
10
10
  import { cn } from "../lib/cn.js";
11
11
  import { Input as UiInput } from "../ui/input.js";
12
- import { parseLocaleNumber, resolveSafeLocale } from "./locale-number.js";
13
- // 20 is the Intl maximum: the display must never round a stored value.
14
- const MAX_FRACTION_DIGITS = 20;
15
- export function NumberInput({ id, name, value, onChange, locale, grouping = true, integer = false, disabled, required, hasError, placeholder, testId, className, ariaDescribedBy, }) {
12
+ import { formatLocaleNumber, parseLocaleNumber, resolveSafeLocale } from "./locale-number.js";
13
+ export function NumberInput({ id, name, value, onChange, locale, grouping = true, integer = false, disabled, required, hasError, placeholder, testId, className, ariaDescribedBy, ariaLabel, }) {
16
14
  const resolvedLocale = resolveSafeLocale(locale);
17
15
  const [focused, setFocused] = useState(false);
18
16
  const [draft, setDraft] = useState("");
@@ -25,12 +23,7 @@ export function NumberInput({ id, name, value, onChange, locale, grouping = true
25
23
  if (focused)
26
24
  inputRef.current?.select();
27
25
  }, [focused]);
28
- const format = (useGrouping) => value === ""
29
- ? ""
30
- : new Intl.NumberFormat(resolvedLocale, {
31
- maximumFractionDigits: MAX_FRACTION_DIGITS,
32
- useGrouping,
33
- }).format(value);
26
+ const format = (useGrouping) => value === "" ? "" : formatLocaleNumber(value, resolvedLocale, { grouping: useGrouping });
34
27
  const handleChange = (raw) => {
35
28
  setDraft(raw);
36
29
  if (raw.trim() === "") {
@@ -46,7 +39,7 @@ export function NumberInput({ id, name, value, onChange, locale, grouping = true
46
39
  }
47
40
  onChange(parsed);
48
41
  };
49
- return (_jsx(UiInput, { ref: inputRef, type: "text", inputMode: integer ? "numeric" : "decimal", autoComplete: "off", id: id, name: name, disabled: disabled, "aria-required": required, "aria-invalid": hasError === true ? true : undefined, "aria-describedby": ariaDescribedBy, "data-testid": testId, value: focused ? draft : format(grouping), onFocus: () => {
42
+ return (_jsx(UiInput, { ref: inputRef, type: "text", inputMode: integer ? "numeric" : "decimal", autoComplete: "off", id: id, name: name, disabled: disabled, "aria-required": required, "aria-invalid": hasError === true ? true : undefined, "aria-describedby": ariaDescribedBy, "aria-label": ariaLabel, "data-testid": testId, value: focused ? draft : format(grouping), onFocus: () => {
50
43
  setDraft(format(false));
51
44
  setFocused(true);
52
45
  }, onBlur: () => setFocused(false), onChange: (e) => handleChange(e.target.value), ...(placeholder !== undefined && { placeholder }), className: cn("text-left tabular-nums", className) }));
@@ -1,5 +1,6 @@
1
+ import type { SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
1
2
  import { type ReactNode } from "react";
2
- export declare function SegmentedControl({ id, name, value, onChange, options, disabled, required, hasError, labelProps, }: {
3
+ export declare function SegmentedControl({ id, name, value, onChange, options, disabled, required, hasError, labelProps, tones, size, }: {
3
4
  readonly id: string;
4
5
  readonly name: string;
5
6
  readonly value: string;
@@ -18,4 +19,8 @@ export declare function SegmentedControl({ id, name, value, onChange, options, d
18
19
  } | {
19
20
  "aria-label": string;
20
21
  };
22
+ /** Status tone per option value; the selected segment takes the tone's colors. */
23
+ readonly tones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
24
+ /** `"touch"` makes every segment at least 44px high for fingers. */
25
+ readonly size?: "touch";
21
26
  }): ReactNode;