@cosmicdrift/kumiko-renderer 0.247.0 → 0.249.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.247.0",
3
+ "version": "0.249.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.247.0",
19
- "@cosmicdrift/kumiko-headless": "0.247.0",
18
+ "@cosmicdrift/kumiko-framework": "0.249.0",
19
+ "@cosmicdrift/kumiko-headless": "0.249.0",
20
20
  "react": "^19.2.6",
21
21
  "temporal-polyfill": "^0.3.2",
22
22
  "zod": "^4.4.3"
@@ -27,7 +27,7 @@
27
27
  "@types/react-dom": "^19.2.3",
28
28
  "jsdom": "^29.1.1",
29
29
  "react-dom": "^19.2.6",
30
- "@cosmicdrift/kumiko-locale-de": "0.247.0"
30
+ "@cosmicdrift/kumiko-locale-de": "0.249.0"
31
31
  },
32
32
  "repository": {
33
33
  "type": "git",
@@ -3,8 +3,9 @@
3
3
  // renders the real entityList pipeline (KumikoScreen → EntityListBody) and
4
4
  // asserts on the resolved `icon` KumikoScreen attaches to each
5
5
  // DataTableRowAction — the id-derived default (ACTION_ICON_BY_ID in
6
- // kumiko-screen.tsx) for an action with no declared icon, and the declared
7
- // icon overriding that default for one that has it.
6
+ // kumiko-screen.tsx) for an action with no declared icon, the declared icon
7
+ // overriding that default for one that has it, and the full id / last
8
+ // segment / first segment fallback order for compound ids (fw#2749).
8
9
 
9
10
  import { describe, expect, test } from "bun:test";
10
11
  import type {
@@ -101,6 +102,36 @@ function buildSchema(): FeatureSchema {
101
102
  screen: "widget-edit",
102
103
  icon: "archive",
103
104
  },
105
+ // Full id and last segment ("item") have no entry — falls through to
106
+ // the first segment ("add" -> plus).
107
+ {
108
+ id: "add-item",
109
+ label: "Add Item",
110
+ handler: "icon-fixture:write:widget:add-item",
111
+ },
112
+ // Same fallback, different verb ("open" -> eye).
113
+ {
114
+ kind: "navigate",
115
+ id: "open-lease",
116
+ label: "Open Lease",
117
+ screen: "widget-edit",
118
+ },
119
+ // Neither the full id nor either segment ("order", "ship") has an
120
+ // entry — the new stage must not invent an icon.
121
+ {
122
+ kind: "navigate",
123
+ id: "order-ship",
124
+ label: "Order Ship",
125
+ screen: "widget-edit",
126
+ },
127
+ // Last segment ("copy") and first segment ("send") both have entries —
128
+ // the last segment must keep priority over the new first-segment stage.
129
+ {
130
+ kind: "navigate",
131
+ id: "send-copy",
132
+ label: "Send Copy",
133
+ screen: "widget-edit",
134
+ },
104
135
  ],
105
136
  };
106
137
  return {
@@ -166,4 +197,52 @@ describe("entityList rowActions resolve an icon (fw-ui-defaults)", () => {
166
197
 
167
198
  expect(requireAction("edit").icon).toBe("archive");
168
199
  });
200
+
201
+ test("an id whose full form and last segment miss falls back to its first segment ('add-item' -> plus)", async () => {
202
+ capturedRowActions = undefined;
203
+
204
+ renderListScreen();
205
+
206
+ await waitFor(() => {
207
+ expect(capturedRowActions).toBeDefined();
208
+ });
209
+
210
+ expect(requireAction("add-item").icon).toBe("plus");
211
+ });
212
+
213
+ test("the first-segment fallback also applies to 'open-lease' -> eye", async () => {
214
+ capturedRowActions = undefined;
215
+
216
+ renderListScreen();
217
+
218
+ await waitFor(() => {
219
+ expect(capturedRowActions).toBeDefined();
220
+ });
221
+
222
+ expect(requireAction("open-lease").icon).toBe("eye");
223
+ });
224
+
225
+ test("an id with no matching segment stays without an icon ('order-ship')", async () => {
226
+ capturedRowActions = undefined;
227
+
228
+ renderListScreen();
229
+
230
+ await waitFor(() => {
231
+ expect(capturedRowActions).toBeDefined();
232
+ });
233
+
234
+ expect(requireAction("order-ship").icon).toBeUndefined();
235
+ });
236
+
237
+ test("the last segment keeps priority over the first ('send-copy' -> copy, not send)", async () => {
238
+ capturedRowActions = undefined;
239
+
240
+ renderListScreen();
241
+
242
+ await waitFor(() => {
243
+ expect(capturedRowActions).toBeDefined();
244
+ });
245
+
246
+ expect(requireAction("send-copy").icon).toBe("copy");
247
+ });
169
248
  });
@@ -57,19 +57,29 @@ const ACTION_ICON_BY_ID: Readonly<Partial<Record<string, IconKey>>> = {
57
57
  send: "send",
58
58
  };
59
59
 
60
- // Ids are kebab-case (RowAction.id doc) — a compound id whose full form has
61
- // no entry falls back to its last segment ("order-ship" -> "ship").
60
+ // Ids are kebab-case (RowAction.id doc): aggregate-object-verb
61
+ // ("order-ship") or verb-prefix ("add-item").
62
62
  function kebabLastSegment(id: string): string {
63
63
  const idx = id.lastIndexOf("-");
64
64
  return idx === -1 ? id : id.slice(idx + 1);
65
65
  }
66
66
 
67
+ function kebabFirstSegment(id: string): string {
68
+ const idx = id.indexOf("-");
69
+ return idx === -1 ? id : id.slice(0, idx);
70
+ }
71
+
67
72
  // Resolution order: author-declared `icon` wins, then the id-derived
68
- // default (full id, then its last kebab segment). `declared` is `undefined`
69
- // for ToolbarAction, which has no author-facing icon field.
73
+ // default (full id, then its last kebab segment, then its first kebab
74
+ // segment). `declared` is `undefined` for ToolbarAction, which has no
75
+ // author-facing icon field.
70
76
  export function resolveActionIcon(id: string, declared?: IconKey): IconKey | undefined {
71
77
  if (declared !== undefined) return declared;
72
- return ACTION_ICON_BY_ID[id] ?? ACTION_ICON_BY_ID[kebabLastSegment(id)];
78
+ return (
79
+ ACTION_ICON_BY_ID[id] ??
80
+ ACTION_ICON_BY_ID[kebabLastSegment(id)] ??
81
+ ACTION_ICON_BY_ID[kebabFirstSegment(id)]
82
+ );
73
83
  }
74
84
 
75
85
  // Row-action column mode for a resolved action set: a group where every