@cosmicdrift/kumiko-renderer 0.186.3 → 0.187.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.186.3",
3
+ "version": "0.187.0",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -15,8 +15,8 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.186.3",
19
- "@cosmicdrift/kumiko-headless": "0.186.3",
18
+ "@cosmicdrift/kumiko-framework": "0.187.0",
19
+ "@cosmicdrift/kumiko-headless": "0.187.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2"
22
22
  },
@@ -24,6 +24,10 @@ export type ColumnRendererProps = {
24
24
  readonly column: {
25
25
  readonly field: string;
26
26
  };
27
+ /** Set only when the caller (DataTable's `onCellChange`) wired an
28
+ * editable path for this cell — undefined means read-only. A renderer
29
+ * can branch its own markup (input vs. plain text) on presence alone. */
30
+ readonly onChange?: (value: unknown) => void;
27
31
  };
28
32
 
29
33
  export type ColumnRendererComponent = ComponentType<ColumnRendererProps>;
@@ -151,6 +151,21 @@ describe("EmbeddedListField — cell change recomputes derived", () => {
151
151
  { product: "p1", unit: "pcs", quantity: 4, unitPrice: 500, amount: 2000 },
152
152
  ]);
153
153
  });
154
+
155
+ test("a fractional product on the money-typed amount cell shows the rounded live preview, not the raw fraction", () => {
156
+ const rows = [{ product: "p1", unit: "pcs", quantity: 2, unitPrice: 923, amount: 1846 }];
157
+ let lastValue: unknown;
158
+ renderEmbeddedListField(invoiceLinesField({ value: rows }), (v) => {
159
+ lastValue = v;
160
+ });
161
+ // 2.5 * 923 = 2307.5 — a fractional minor-unit amount the money target
162
+ // can't store as-is. The live preview must show the rounded value
163
+ // (2308), matching what the server would compute and persist.
164
+ captured?.onCellChange(0, "quantity", 2.5);
165
+ expect(lastValue).toEqual([
166
+ { product: "p1", unit: "pcs", quantity: 2.5, unitPrice: 923, amount: 2308 },
167
+ ]);
168
+ });
154
169
  });
155
170
 
156
171
  describe("EmbeddedListField — row operations", () => {
@@ -1,7 +1,12 @@
1
- import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
1
+ import type {
2
+ EditFieldViewModel,
3
+ EmbeddedListCellViewModel,
4
+ FieldIssue,
5
+ } from "@cosmicdrift/kumiko-headless";
2
6
  import {
3
7
  computeDerivedCellValue,
4
8
  groupEmbeddedListIssues,
9
+ roundDerivedCellValue,
5
10
  sumEmbeddedListColumn,
6
11
  } from "@cosmicdrift/kumiko-headless";
7
12
  import type { ReactNode } from "react";
@@ -25,6 +30,7 @@ type EmbeddedRow = Readonly<Record<string, unknown>>;
25
30
  function withRecomputedDerived(
26
31
  row: EmbeddedRow,
27
32
  derived: EditFieldViewModel["embeddedListDerived"],
33
+ cells: readonly EmbeddedListCellViewModel[],
28
34
  ): EmbeddedRow {
29
35
  if (derived === undefined) return row;
30
36
  const result: Record<string, unknown> = { ...row };
@@ -33,7 +39,12 @@ function withRecomputedDerived(
33
39
  const v = result[src];
34
40
  return typeof v === "number" ? v : undefined;
35
41
  });
36
- result[derivedField] = computeDerivedCellValue(def.op, values);
42
+ const computed = computeDerivedCellValue(def.op, values);
43
+ const target = cells.find((c) => c.field === derivedField);
44
+ result[derivedField] =
45
+ computed !== undefined && target !== undefined
46
+ ? roundDerivedCellValue(computed, target)
47
+ : computed;
37
48
  }
38
49
  return result;
39
50
  }
@@ -154,11 +165,13 @@ export function EmbeddedListField({
154
165
  }
155
166
 
156
167
  function handleCellChange(rowIndex: number, cellField: string, value: unknown): void {
157
- replaceRow(rowIndex, (row) => withRecomputedDerived({ ...row, [cellField]: value }, derived));
168
+ replaceRow(rowIndex, (row) =>
169
+ withRecomputedDerived({ ...row, [cellField]: value }, derived, cells),
170
+ );
158
171
  }
159
172
 
160
173
  function handleAddRow(): void {
161
- onChange([...rows, withRecomputedDerived({}, derived)]);
174
+ onChange([...rows, withRecomputedDerived({}, derived, cells)]);
162
175
  }
163
176
 
164
177
  function handleRemoveRow(rowIndex: number): void {
@@ -210,7 +223,7 @@ export function EmbeddedListField({
210
223
  });
211
224
 
212
225
  const recomputed = nextRows.map((row, i) =>
213
- touchedIndices.has(i) ? withRecomputedDerived(row, derived) : row,
226
+ touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row,
214
227
  );
215
228
  onChange(recomputed);
216
229
  }
@@ -31,6 +31,9 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
31
31
 
32
32
  // Field — aria-Labels der Date/Timestamp-Primitives.
33
33
  "kumiko.field.open-calendar": "Kalender öffnen",
34
+ "kumiko.field.dateField.placeholderYear": "J",
35
+ "kumiko.field.dateField.placeholderMonth": "M",
36
+ "kumiko.field.dateField.placeholderDay": "T",
34
37
  "kumiko.field.time": "Uhrzeit",
35
38
  "kumiko.field.timezone": "Zeitzone",
36
39
  "kumiko.field.locatedTzHint": "Zeit lokal am angegebenen Ort",
@@ -195,6 +198,9 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
195
198
  "kumiko.toast.learn-more": "Learn more",
196
199
 
197
200
  "kumiko.field.open-calendar": "Open calendar",
201
+ "kumiko.field.dateField.placeholderYear": "Y",
202
+ "kumiko.field.dateField.placeholderMonth": "M",
203
+ "kumiko.field.dateField.placeholderDay": "D",
198
204
  "kumiko.field.time": "Time",
199
205
  "kumiko.field.timezone": "Time zone",
200
206
  "kumiko.field.locatedTzHint": "Time local to the given location",
@@ -548,6 +548,18 @@ export type DataTableProps = {
548
548
  * "Ende der Liste"-Hinweis statt des Sentinels. Default true. */
549
549
  readonly hasMore?: boolean;
550
550
  readonly testId?: string;
551
+ /** Wires an edit path into a component column-renderer: `DataTableCell`
552
+ * passes this down as `ColumnRendererProps.onChange` for that cell,
553
+ * bound to `(rowId, column.field)`. Renderers without a component
554
+ * renderer (format-spec, type-default) ignore it — read-only stays
555
+ * read-only unless the column has a custom renderer that reads
556
+ * `onChange`. */
557
+ readonly onCellChange?: (rowId: string, field: string, value: unknown) => void;
558
+ /** Overrides the row `data-testid` (default `row-${row.id}`) — for a
559
+ * screen that already has its own DOM-test/e2e naming scheme. */
560
+ readonly getRowTestId?: (row: ListRowViewModel) => string;
561
+ /** Overrides the cell `data-testid` (default `cell-${row.id}-${field}`). */
562
+ readonly getCellTestId?: (row: ListRowViewModel, field: string) => string;
551
563
  };
552
564
 
553
565
  // ---- EmbeddedListInput (createEmbeddedListField widget) ----