@cosmicdrift/kumiko-renderer-web 0.286.0 → 0.287.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-web",
3
- "version": "0.286.0",
3
+ "version": "0.287.0",
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.286.0",
20
- "@cosmicdrift/kumiko-headless": "0.286.0",
21
- "@cosmicdrift/kumiko-renderer": "0.286.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.287.0",
20
+ "@cosmicdrift/kumiko-headless": "0.287.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.287.0",
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",
@@ -66,7 +66,7 @@
66
66
  "@types/react-dom": "^19.2.3",
67
67
  "jsdom": "^29.1.1",
68
68
  "tailwindcss": "^4.3.0",
69
- "@cosmicdrift/kumiko-locale-de": "0.286.0"
69
+ "@cosmicdrift/kumiko-locale-de": "0.287.0"
70
70
  },
71
71
  "repository": {
72
72
  "type": "git",
package/src/index.ts CHANGED
@@ -112,6 +112,8 @@ export type { DefaultAppShellProps } from "./layout/default-app-shell";
112
112
  export { DefaultAppShell } from "./layout/default-app-shell";
113
113
  export type { EditorPanelProps, ResolverComponent } from "./layout/editor-panel";
114
114
  export { EditorPanel } from "./layout/editor-panel";
115
+ export type { NavReparentOverride } from "./layout/filter-app-schema-navs";
116
+ export { filterAppSchemaNavsByAllowlist } from "./layout/filter-app-schema-navs";
115
117
  export type { LanguageSwitcherProps, LocaleOption } from "./layout/language-switcher";
116
118
  export { LanguageSwitcher } from "./layout/language-switcher";
117
119
  export type { NavTreeProps } from "./layout/nav-tree";
@@ -0,0 +1,113 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
3
+ import { filterAppSchemaNavsByAllowlist } from "../filter-app-schema-navs";
4
+
5
+ function feature(overrides: Partial<FeatureSchema> & { featureName: string }): FeatureSchema {
6
+ return {
7
+ entities: {},
8
+ screens: [],
9
+ ...overrides,
10
+ } as FeatureSchema;
11
+ }
12
+
13
+ describe("filterAppSchemaNavsByAllowlist", () => {
14
+ test("keeps only navs whose qualified QN is in the allowlist", () => {
15
+ const schema: AppSchema = {
16
+ features: [
17
+ feature({
18
+ featureName: "vehicles",
19
+ navs: [{ id: "vehicle-list", label: "x", screen: "vehicles:screen:vehicle-list" }],
20
+ }),
21
+ feature({
22
+ featureName: "app-shell",
23
+ navs: [{ id: "vehicle-list", label: "x", screen: "vehicles:screen:vehicle-list" }],
24
+ }),
25
+ ],
26
+ };
27
+
28
+ const filtered = filterAppSchemaNavsByAllowlist(
29
+ schema,
30
+ new Set(["app-shell:nav:vehicle-list"]),
31
+ );
32
+
33
+ expect(filtered.features.find((f) => f.featureName === "vehicles")?.navs).toEqual([]);
34
+ expect(filtered.features.find((f) => f.featureName === "app-shell")?.navs).toHaveLength(1);
35
+ });
36
+
37
+ test("passes through an already-qualified id unchanged when allowed", () => {
38
+ const schema: AppSchema = {
39
+ features: [
40
+ feature({
41
+ featureName: "config",
42
+ navs: [{ id: "config:nav:audience-tenant", label: "x" }],
43
+ }),
44
+ ],
45
+ };
46
+
47
+ const filtered = filterAppSchemaNavsByAllowlist(
48
+ schema,
49
+ new Set(["config:nav:audience-tenant"]),
50
+ );
51
+
52
+ expect(filtered.features[0]?.navs).toHaveLength(1);
53
+ });
54
+
55
+ test("a feature without navs passes through unchanged", () => {
56
+ const schema: AppSchema = {
57
+ features: [feature({ featureName: "no-nav-feature" })],
58
+ };
59
+
60
+ const filtered = filterAppSchemaNavsByAllowlist(schema, new Set());
61
+
62
+ expect(filtered.features[0]?.navs).toBeUndefined();
63
+ });
64
+
65
+ test("an empty allowlist yields empty navs arrays, not a broken schema", () => {
66
+ const schema: AppSchema = {
67
+ features: [feature({ featureName: "vehicles", navs: [{ id: "vehicle-list", label: "x" }] })],
68
+ };
69
+
70
+ const filtered = filterAppSchemaNavsByAllowlist(schema, new Set());
71
+
72
+ expect(filtered.features[0]?.navs).toEqual([]);
73
+ expect(filtered.features).toHaveLength(1);
74
+ });
75
+
76
+ test("reparents an adopted foreign nav onto the given section, leaving other fields intact", () => {
77
+ const schema: AppSchema = {
78
+ features: [
79
+ feature({
80
+ featureName: "config",
81
+ navs: [
82
+ { id: "audience-tenant", label: "config.settings.tenant", order: 0 },
83
+ {
84
+ id: "dealers-tenant",
85
+ label: "dealers.settings",
86
+ icon: "building",
87
+ parent: "audience-tenant",
88
+ screen: "config:screen:dealers-tenant",
89
+ order: 1,
90
+ },
91
+ ],
92
+ }),
93
+ ],
94
+ };
95
+
96
+ const filtered = filterAppSchemaNavsByAllowlist(
97
+ schema,
98
+ new Set(["config:nav:dealers-tenant"]),
99
+ new Map([["config:nav:dealers-tenant", { parent: "app-shell:nav:tenant", order: 10 }]]),
100
+ );
101
+
102
+ expect(filtered.features[0]?.navs).toEqual([
103
+ {
104
+ id: "dealers-tenant",
105
+ label: "dealers.settings",
106
+ icon: "building",
107
+ parent: "app-shell:nav:tenant",
108
+ screen: "config:screen:dealers-tenant",
109
+ order: 10,
110
+ },
111
+ ]);
112
+ });
113
+ });
@@ -0,0 +1,40 @@
1
+ import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
2
+ import { qualifyNavId } from "./nav-tree";
3
+
4
+ export type NavReparentOverride = { readonly parent: string; readonly order: number };
5
+
6
+ // Reduces every feature's `navs` to the app's sidebar allowlist, leaving
7
+ // screens/entities/handlers/translations and every other schema field
8
+ // untouched. Pure and defensive: a feature with no `navs` passes through
9
+ // unchanged, and a feature whose every nav gets stripped ends up with an
10
+ // empty `navs` array, never `undefined` turned into something broken.
11
+ //
12
+ // `reparentOverrides` (keyed by the nav's qualified QN) lets a foreign,
13
+ // otherwise-unmodifiable nav (e.g. a settings-hub-synthesized entry that
14
+ // can't be hand-re-registered under a new parent at boot-validation time)
15
+ // be "adopted" into a different section by rewriting its parent/order as
16
+ // it survives the allowlist filter. Without an override, a surviving nav's
17
+ // parent/order pass through unchanged.
18
+ export function filterAppSchemaNavsByAllowlist(
19
+ schema: AppSchema,
20
+ allowedNavQns: ReadonlySet<string>,
21
+ reparentOverrides: ReadonlyMap<string, NavReparentOverride> = new Map(),
22
+ ): AppSchema {
23
+ return {
24
+ ...schema,
25
+ features: schema.features.map((feature): FeatureSchema => {
26
+ if (feature.navs === undefined) return feature;
27
+ return {
28
+ ...feature,
29
+ navs: feature.navs.flatMap((nav) => {
30
+ const qn = qualifyNavId(feature.featureName, nav.id);
31
+ if (!allowedNavQns.has(qn)) return [];
32
+ const override = reparentOverrides.get(qn);
33
+ return override === undefined
34
+ ? [nav]
35
+ : [{ ...nav, parent: override.parent, order: override.order }];
36
+ }),
37
+ };
38
+ }),
39
+ };
40
+ }
@@ -970,7 +970,10 @@ export function buildNavRegistrySliceForApp(
970
970
  };
971
971
  }
972
972
 
973
- function qualifyNavId(feature: string, id: string): string {
973
+ // Exported (not part of the package's public index.ts surface) so other
974
+ // modules under layout/ — e.g. filter-app-schema-navs.ts — qualify nav ids
975
+ // the same way instead of keeping a second copy in sync.
976
+ export function qualifyNavId(feature: string, id: string): string {
974
977
  return id.includes(":nav:") ? id : `${feature}:nav:${id}`;
975
978
  }
976
979