@cosmicdrift/kumiko-renderer-web 0.157.2 → 0.158.2

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-web",
3
- "version": "0.157.2",
3
+ "version": "0.158.2",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.157.2",
20
- "@cosmicdrift/kumiko-headless": "0.157.2",
21
- "@cosmicdrift/kumiko-renderer": "0.157.2",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.158.2",
20
+ "@cosmicdrift/kumiko-headless": "0.158.2",
21
+ "@cosmicdrift/kumiko-renderer": "0.158.2",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -61,6 +61,7 @@ const baseSchema: FeatureSchema = {
61
61
  featureName: "tasks",
62
62
  entities: { task: taskEntity },
63
63
  screens: [editScreen, listScreen],
64
+ navs: [{ id: "task-edit", label: "tasks:nav.task-edit", screen: "tasks:screen:task-edit" }],
64
65
  };
65
66
 
66
67
  // createKumikoApp ruft createRoot(...).render(...) direkt auf — React 18+
@@ -181,6 +182,7 @@ describe("createKumikoApp", () => {
181
182
  featureName: "tasks",
182
183
  entities: { task: taskEntity },
183
184
  screens: [{ ...listScreen, access: { openToAll: true } }],
185
+ navs: [{ id: "task-list", label: "tasks:nav.task-list", screen: "tasks:screen:task-list" }],
184
186
  };
185
187
  const schema: AppSchema = { features: [restrictedFeature, openFeature] };
186
188
  await mountApp({ schema, dispatcher: makeDispatcher() });
@@ -230,6 +232,9 @@ describe("createKumikoApp", () => {
230
232
  columns: [{ field: "color", renderer: { react: { __component: "Swatch" } } }],
231
233
  },
232
234
  ],
235
+ navs: [
236
+ { id: "color-list", label: "tasks:nav.color-list", screen: "tasks:screen:color-list" },
237
+ ],
233
238
  };
234
239
 
235
240
  mountRoot();
@@ -283,6 +288,9 @@ describe("createKumikoApp", () => {
283
288
  featureName: "tasks",
284
289
  entities: { task: colorEntity },
285
290
  screens: [colorListScreen],
291
+ navs: [
292
+ { id: "color-list", label: "tasks:nav.color-list", screen: "tasks:screen:color-list" },
293
+ ],
286
294
  };
287
295
  const dispatcher = createMockDispatcher({
288
296
  query: (async () => ({
@@ -328,6 +336,13 @@ describe("createKumikoApp", () => {
328
336
  featureName: "cap-counter",
329
337
  entities: {},
330
338
  screens: [dashboardScreen],
339
+ navs: [
340
+ {
341
+ id: "overview",
342
+ label: "cap-counter:nav.cap-list",
343
+ screen: "cap-counter:screen:overview",
344
+ },
345
+ ],
331
346
  translations: {
332
347
  "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
333
348
  },
@@ -371,6 +386,13 @@ describe("createKumikoApp", () => {
371
386
  featureName: "cap-counter",
372
387
  entities: {},
373
388
  screens: [dashboardScreen],
389
+ navs: [
390
+ {
391
+ id: "overview",
392
+ label: "cap-counter:nav.cap-list",
393
+ screen: "cap-counter:screen:overview",
394
+ },
395
+ ],
374
396
  translations: {
375
397
  "cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
376
398
  },
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CustomScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
4
+ import { firstOpenScreenQn } from "../create-app";
5
+
6
+ function feature(
7
+ overrides: Partial<FeatureSchema> & { readonly featureName: string },
8
+ ): FeatureSchema {
9
+ return { entities: {}, screens: [], ...overrides };
10
+ }
11
+
12
+ function customScreen(
13
+ overrides: Partial<CustomScreenDefinition> & { readonly id: string },
14
+ ): CustomScreenDefinition {
15
+ return { type: "custom", renderer: {}, ...overrides };
16
+ }
17
+
18
+ describe("firstOpenScreenQn", () => {
19
+ test("picks an open screen that is placed in nav", () => {
20
+ const features: readonly FeatureSchema[] = [
21
+ feature({
22
+ featureName: "shop",
23
+ screens: [customScreen({ id: "catalog" })],
24
+ navs: [{ id: "catalog", label: "shop:nav.catalog", screen: "shop:screen:catalog" }],
25
+ }),
26
+ ];
27
+ expect(firstOpenScreenQn(features)).toBe("shop:screen:catalog");
28
+ });
29
+
30
+ test("skips a dormant open screen that has no nav entry (#1258)", () => {
31
+ const features: readonly FeatureSchema[] = [
32
+ feature({
33
+ featureName: "auth-mfa",
34
+ screens: [customScreen({ id: "auth-mfa-enable", access: { openToAll: true } })],
35
+ // No nav entry — this is auth-mfa's dormant custom-screen convention.
36
+ }),
37
+ feature({
38
+ featureName: "shop",
39
+ screens: [customScreen({ id: "catalog" })],
40
+ navs: [{ id: "catalog", label: "shop:nav.catalog", screen: "shop:screen:catalog" }],
41
+ }),
42
+ ];
43
+ expect(firstOpenScreenQn(features)).toBe("shop:screen:catalog");
44
+ });
45
+
46
+ test("skips role-restricted screens even when placed in nav", () => {
47
+ const features: readonly FeatureSchema[] = [
48
+ feature({
49
+ featureName: "admin",
50
+ screens: [customScreen({ id: "dashboard", access: { roles: ["Admin"] } })],
51
+ navs: [{ id: "dashboard", label: "admin:nav.dashboard", screen: "admin:screen:dashboard" }],
52
+ }),
53
+ ];
54
+ expect(firstOpenScreenQn(features)).toBeUndefined();
55
+ });
56
+
57
+ test("returns undefined when no screen is both open and nav-placed", () => {
58
+ const features: readonly FeatureSchema[] = [
59
+ feature({
60
+ featureName: "auth-mfa",
61
+ screens: [customScreen({ id: "auth-mfa-enable", access: { openToAll: true } })],
62
+ }),
63
+ ];
64
+ expect(firstOpenScreenQn(features)).toBeUndefined();
65
+ });
66
+ });
@@ -135,10 +135,30 @@ function readInjectedSchema(): AppSchema | FeatureSchema | undefined {
135
135
  // ein role-restricted Screen (z.B. bundled user/tenant, SystemAdmin-only)
136
136
  // darf hier nie gewinnen, sonst landet jeder Nicht-Admin auf einem
137
137
  // Access-Denied-Screen (#1176).
138
- function firstOpenScreenQn(features: readonly FeatureSchema[]): string | undefined {
138
+ // Also requires the screen be reachable via r.nav, otherwise a dormant
139
+ // `type: "custom"` screen a feature only registers for manual app-side
140
+ // placement (e.g. auth-mfa's enable screen) can win by declaration order
141
+ // alone, landing every app without an explicit `screenQn` on a screen
142
+ // nobody wired a component for (#1258).
143
+ export function firstOpenScreenQn(features: readonly FeatureSchema[]): string | undefined {
144
+ // NavDefinition.screen carries two shapes in practice: most bundled
145
+ // features author it pre-qualified ("tenant:screen:members"), but the
146
+ // config settings-hub generator emits the bare short id. Index both
147
+ // forms so nav-reachability doesn't depend on which convention a given
148
+ // feature happens to use.
149
+ const navScreenQns = new Set<string>();
150
+ for (const f of features) {
151
+ for (const n of f.navs ?? []) {
152
+ if (n.screen === undefined) continue;
153
+ navScreenQns.add(n.screen);
154
+ navScreenQns.add(qualifyScreenId(f.featureName, n.screen));
155
+ }
156
+ }
139
157
  for (const feature of features) {
140
158
  const openScreen = feature.screens.find(
141
- (s) => s.access === undefined || "openToAll" in s.access,
159
+ (s) =>
160
+ (s.access === undefined || "openToAll" in s.access) &&
161
+ navScreenQns.has(qualifyScreenId(feature.featureName, s.id)),
142
162
  );
143
163
  if (openScreen !== undefined) return qualifyScreenId(feature.featureName, openScreen.id);
144
164
  }