@cosmicdrift/kumiko-renderer 0.399.0 → 0.400.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/entity-edit-prefill.js +7 -1
- package/dist/app/form-schema.js +42 -17
- package/dist/app/projection-detail-body.js +7 -0
- package/dist/app/projection-detail-header-model.d.ts +1 -1
- package/dist/app/projection-detail-header-model.js +2 -2
- package/dist/app/projection-detail-header-region.d.ts +1 -0
- package/dist/app/projection-detail-header-region.js +1 -1
- package/dist/app/projection-detail-header.js +4 -2
- package/dist/app/projection-detail-navigation.d.ts +1 -0
- package/dist/app/projection-detail-navigation.js +9 -1
- package/dist/app/use-related-list-tab-counts.d.ts +5 -0
- package/dist/app/use-related-list-tab-counts.js +68 -0
- package/dist/components/embedded-list-field.js +69 -91
- package/dist/components/embedded-list-paste.d.ts +2 -0
- package/dist/components/embedded-list-paste.js +73 -0
- package/dist/components/related-list-section.js +4 -8
- package/dist/components/render-edit-helpers.js +2 -2
- package/dist/components/render-field-choice-inputs.js +1 -1
- package/dist/embedded-list-types.d.ts +117 -0
- package/dist/embedded-list-types.js +1 -0
- package/dist/headless/form/form-controller.js +9 -1
- package/dist/headless/index.d.ts +1 -1
- package/dist/headless/index.js +1 -1
- package/dist/headless/view-model/edit.js +40 -50
- package/dist/headless/view-model/embedded-list-cells.d.ts +12 -0
- package/dist/headless/view-model/embedded-list-cells.js +49 -0
- package/dist/headless/view-model/embedded-list.d.ts +1 -1
- package/dist/headless/view-model/embedded-list.js +1 -1
- package/dist/headless/view-model/index.d.ts +1 -1
- package/dist/headless/view-model/index.js +1 -1
- package/dist/headless/view-model/list.js +2 -3
- package/dist/headless/view-model/types.d.ts +23 -6
- package/dist/hooks/related-list-parent-payload.d.ts +2 -0
- package/dist/hooks/related-list-parent-payload.js +7 -0
- package/dist/i18n-defaults.js +3 -0
- package/dist/primitives.d.ts +4 -88
- package/dist/web/lib/file-upload-form-data.d.ts +2 -0
- package/dist/web/lib/file-upload-form-data.js +2 -0
- package/dist/web/lib/upload-file-ref.d.ts +1 -1
- package/dist/web/primitives/data-table-cell.d.ts +2 -0
- package/dist/web/primitives/data-table-cell.js +1 -1
- package/dist/web/primitives/embedded-file-cell.d.ts +11 -0
- package/dist/web/primitives/embedded-file-cell.js +50 -0
- package/dist/web/primitives/embedded-list-cards.d.ts +38 -0
- package/dist/web/primitives/embedded-list-cards.js +71 -0
- package/dist/web/primitives/embedded-list-cell.d.ts +41 -0
- package/dist/web/primitives/embedded-list-cell.js +132 -0
- package/dist/web/primitives/embedded-list-input.d.ts +1 -1
- package/dist/web/primitives/embedded-list-input.js +29 -142
- package/dist/web/primitives/file-upload.d.ts +2 -1
- package/dist/web/primitives/file-upload.js +2 -1
- package/dist/web/primitives/input-choice-kinds.js +1 -1
- package/dist/web/primitives/input-optional-props.d.ts +2 -1
- package/dist/web/primitives/input-optional-props.js +2 -1
- package/dist/web/primitives/locale-number.d.ts +3 -0
- package/dist/web/primitives/locale-number.js +8 -0
- package/dist/web/primitives/number-input.d.ts +2 -1
- package/dist/web/primitives/number-input.js +4 -11
- package/dist/web/primitives/segmented-select.d.ts +6 -1
- package/dist/web/primitives/segmented-select.js +8 -4
- package/dist/web/widgets/status-badge.d.ts +1 -0
- package/dist/web/widgets/status-badge.js +1 -1
- package/package.json +5 -5
|
@@ -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 {
|
|
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 {
|
|
33
|
-
import {
|
|
34
|
-
import { formatMoney
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 && (
|
|
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
|
-
|
|
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;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useRef } from "react";
|
|
3
|
+
import { statusToneForOptionTone } from "../../index.js";
|
|
3
4
|
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { TONE_PILL } from "../widgets/status-badge.js";
|
|
4
6
|
const EQUAL_WIDTH_MAX_SEGMENTS = 4;
|
|
5
7
|
// WAI-ARIA radiogroup pattern (role="radiogroup" + role="radio" children):
|
|
6
8
|
// arrow keys move focus AND selection in the same step, only the checked
|
|
7
9
|
// segment (or the first when none is checked) sits in the tab order.
|
|
8
|
-
export function SegmentedControl({ id, name, value, onChange, options, disabled, required, hasError, labelProps, }) {
|
|
10
|
+
export function SegmentedControl({ id, name, value, onChange, options, disabled, required, hasError, labelProps, tones, size, }) {
|
|
9
11
|
const buttonRefs = useRef([]);
|
|
10
12
|
// An unknown/legacy value checks no segment; the first one must still be tabbable.
|
|
11
13
|
const selectedIndex = options.findIndex((opt) => opt.value === value);
|
|
@@ -41,8 +43,10 @@ export function SegmentedControl({ id, name, value, onChange, options, disabled,
|
|
|
41
43
|
// their segment instead of leaving one tall and its siblings short.
|
|
42
44
|
options.length <= EQUAL_WIDTH_MAX_SEGMENTS
|
|
43
45
|
? "inline-grid max-w-full grid-flow-col auto-cols-fr"
|
|
44
|
-
: "inline-flex w-fit flex-wrap", "min-h-9 overflow-hidden rounded-md border", hasError === true ? "border-destructive" : "border-input"), children: [_jsx("input", { type: "hidden", name: name, value: value }), options.map((opt, index) => {
|
|
46
|
+
: "inline-flex w-fit flex-wrap", size === "touch" ? "min-h-11" : "min-h-9", "overflow-hidden rounded-md border", hasError === true ? "border-destructive" : "border-input"), children: [_jsx("input", { type: "hidden", name: name, value: value }), options.map((opt, index) => {
|
|
45
47
|
const checked = opt.value === value;
|
|
48
|
+
const declaredTone = tones !== undefined && Object.hasOwn(tones, opt.value) ? tones[opt.value] : undefined;
|
|
49
|
+
const badgeTone = declaredTone !== undefined ? statusToneForOptionTone(declaredTone) : undefined;
|
|
46
50
|
return (_jsx("button", { ref: (node) => {
|
|
47
51
|
buttonRefs.current[index] = node;
|
|
48
52
|
}, type: "button", role: "radio", "aria-checked": checked, tabIndex: index === tabStopIndex ? 0 : -1, disabled: disabled === true || opt.disabled === true, "data-testid": `segmented-${id}-${opt.value}`, onClick: () => onChange(opt.value), onKeyDown: (e) => handleKeyDown(e, index), className: cn(
|
|
@@ -61,8 +65,8 @@ export function SegmentedControl({ id, name, value, onChange, options, disabled,
|
|
|
61
65
|
// and the next row's top border cut a notch into the row above.
|
|
62
66
|
// Harmless on a single-row layout: there's no leftover space to
|
|
63
67
|
// grow into, so `w-fit` on the container still holds.
|
|
64
|
-
"-ml-px -mt-px grow border-l border-t px-3 py-1.5 text-center text-sm transition-colors", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-50", hasError === true ? "border-destructive/50" : "border-border", checked
|
|
65
|
-
? "bg-primary/10
|
|
68
|
+
"-ml-px -mt-px grow border-l border-t px-3 py-1.5 text-center text-sm transition-colors", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-50", hasError === true ? "border-destructive/50" : "border-border", size === "touch" && "min-h-11", checked
|
|
69
|
+
? cn("font-semibold", badgeTone !== undefined ? TONE_PILL[badgeTone] : "bg-primary/10 text-primary")
|
|
66
70
|
: "bg-transparent font-medium text-foreground hover:bg-accent"), children: opt.label }, opt.value));
|
|
67
71
|
})] }));
|
|
68
72
|
}
|
|
@@ -4,6 +4,7 @@ export declare const STATUS_TONE_TEXT: Record<StatusTone, string>;
|
|
|
4
4
|
/** `accent` is a brand-colored call-out (e.g. "3 suggestions"), not a status —
|
|
5
5
|
* hence not part of `StatusTone`, which toasts and select options share. */
|
|
6
6
|
export type StatusBadgeTone = StatusTone | "accent";
|
|
7
|
+
export declare const TONE_PILL: Record<StatusBadgeTone, string>;
|
|
7
8
|
/** Pill badge with a status dot. Caller maps domain values to a tone
|
|
8
9
|
* (e.g. operational→ok, investigating→warn) and passes the translated
|
|
9
10
|
* label as children. */
|
|
@@ -8,7 +8,7 @@ export const STATUS_TONE_TEXT = {
|
|
|
8
8
|
muted: "text-muted-foreground",
|
|
9
9
|
};
|
|
10
10
|
// `muted` maps to the neutral tokens, not a status-* palette.
|
|
11
|
-
const TONE_PILL = {
|
|
11
|
+
export const TONE_PILL = {
|
|
12
12
|
accent: "bg-primary/10 text-primary",
|
|
13
13
|
ok: "bg-status-ok-surface text-status-ok",
|
|
14
14
|
warn: "bg-status-warn-surface text-status-warn",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.400.0",
|
|
4
4
|
"description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
52
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-framework": "0.400.0",
|
|
52
|
+
"@cosmicdrift/kumiko-types": "0.400.0",
|
|
53
53
|
"@types/qrcode": "^1.5.5",
|
|
54
54
|
"class-variance-authority": "^0.7.1",
|
|
55
55
|
"clsx": "^2.1.1",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
83
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
82
|
+
"@cosmicdrift/kumiko-locale-de": "0.400.0",
|
|
83
|
+
"@cosmicdrift/kumiko-testing": "0.400.0",
|
|
84
84
|
"@tailwindcss/cli": "^4.3.3",
|
|
85
85
|
"@testing-library/react": "^16.3.3",
|
|
86
86
|
"@testing-library/user-event": "^14.6.8",
|