@cosmicdrift/kumiko-renderer-web 0.2.3 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,112 @@
1
1
  # @cosmicdrift/kumiko-renderer-web
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 825e7d2: Visual-Tree V.1.4 → V.1.6 — Feature-complete Editor + Folder-Hierarchy + Roving-tabindex.
8
+
9
+ **V.1.4** — explicit `folder?: string` Schema-Field auf text-block-entity. Slug bleibt
10
+ kebab-only validiert, Folder explizit gesetzt. Tree gruppiert via `groupBlocksByFolder`
11
+ (ersetzt `groupBlocksBySlugPrefix`). `Subscribe<T>` Signature um optional `emitError`
12
+ erweitert für explicit async-error-Pfade. ProviderBranch zeigt Error-Banner mit
13
+ Retry-Button. Drift-Test pinnt seedTextBlock-vs-set.write Slug-Validation.
14
+
15
+ **V.1.4b** — URL-State-Routing für Editor-Target via `nav.searchParams`. F5 + Back-Button
16
+ stellen den Editor-State wieder her. Format: `?t=text-content:edit&a_slug=...&a_lang=...`.
17
+ Plus `useDispatchTarget` hook ersetzt globalen `dispatchTarget` als empfohlenen Production-
18
+ Pfad (legacy bleibt für Test-Hooks).
19
+
20
+ **V.1.5** — Arrow-Key-Navigation (`<aside role="tree">`, ARIA-tree-Pattern) + SSE-driven
21
+ Tree-Refresh. `ClientFeatureDefinition.treeEntities?: string[]` listet Entity-Namen pro
22
+ Provider; live-events triggern provider-re-mount → Stale-Tree-state="stub"→"filled"
23
+ flippt nach save automatisch.
24
+
25
+ **V.1.5c+d** — Active-Node-Highlight (explicit blue + 2px border-l + scrollIntoView),
26
+ VS-Code-Polish (compact spacing, focus-visible, folder-icon-color text-amber, indent-
27
+ guides per ancestor-depth), Folder-Wrapper für legal-pages ("📁 Legal" + slug-first
28
+ Verschachtelung) und text-content ("📁 Content").
29
+
30
+ **V.1.6** — Multi-level Folder-Splitting (`folder="page/marketing"` → nested folders,
31
+ walk-or-create-pattern, folder/leaf-collision-tolerant). Roving-tabindex (nur focused-
32
+ treeitem hat tabIndex=0, Tab cyclt aus dem Tree raus).
33
+
34
+ 35/35 kumiko check PASS, 13/13 group-blocks + 22/22 text-content integration tests grün.
35
+ Browser + Keyboard lokal validated.
36
+
37
+ **Breaking**: `TreeContext` Type entfernt (V.1.2 SR2-Rip — war nie genutzt). Provider sind
38
+ session-bound: `TreeChildrenSubscribe = () => Subscribe<T>` statt `(ctx) => Subscribe<T>`.
39
+
40
+ **V.1.7-Followups**: useEffect-deps in VisualTree-focus-init (Performance), Cancellation-
41
+ Token in TreeProvider's fetch (emit-after-unmount-warning), inline-rename, drag-drop,
42
+ file-icons per slug-extension, parent-jump bei ArrowLeft auf collapsed-item.
43
+
44
+ ### Patch Changes
45
+
46
+ - Updated dependencies [825e7d2]
47
+ - @cosmicdrift/kumiko-dispatcher-live@0.4.0
48
+ - @cosmicdrift/kumiko-headless@0.4.0
49
+ - @cosmicdrift/kumiko-renderer@0.4.0
50
+
51
+ ## 0.3.0
52
+
53
+ ### Minor Changes
54
+
55
+ - 0.3.0 bringt zwei neue Subsysteme (Step-Engine Tier-3 + Visual-Tree) plus
56
+ eine AST-Codemod-Pipeline als Vorarbeit für den L2-AI-Layer.
57
+
58
+ ### Breaking Changes
59
+
60
+ - `skipTransitionGuard` → `unsafeSkipTransitionGuard` (Rename in
61
+ feature-ast + engine). Der `unsafe`-Prefix macht die Tragweite des
62
+ Casts sichtbar und ist konsistent zur `unsafeProjectionUpsert`- und
63
+ `r.rawTable`-Konvention. Migration: 1:1-Ersetzung, keine Verhaltens-Änderung.
64
+
65
+ ### Features
66
+
67
+ - **Step-Engine M.4 — Tier-3 Workflow-Engine.** Neue Step-Vocabulary
68
+ `wait`, `waitForEvent`, `retry` ermöglicht persistierte Long-Running-Flows
69
+ über Job-Boundaries hinweg. Q7 Snapshot-at-Start hängt jedem Step-Run
70
+ einen SHA-256-Fingerprint des Aggregat-Zustands an, sodass Replays
71
+ deterministisch gegen den ursprünglichen Eingangszustand laufen.
72
+ - **Visual-Tree V.1.x — Tree-API + Editor-Panel.** Neue `VisualTree`-
73
+ Component plus TreeProvider-Pattern; erste TreeProviders für
74
+ `text-content` und `legal-pages` (CMS-light + Impressum/Privacy).
75
+ Fundament für den späteren No-Code-Designer (~3000 LOC, 98 Tests).
76
+ - **Codemod-Pipeline.** AST-basierte Patcher-Module für strukturelle
77
+ Feature-Edits — wird vom kommenden L2-AI-Layer als Tool-Surface
78
+ verwendet, ist aber eigenständig nutzbar für ts-morph-style Migrationen.
79
+ - **user-data-rights Sample-Recipe.** DSGVO Art. 15/17/18/20 vollständig
80
+ als Sample-Recipe (`samples/recipes/`) inklusive README — zeigt die
81
+ Export- und Forget-Pipeline gegen den `compliance-profiles`-Default
82
+ (`eu-dsgvo`).
83
+
84
+ ### Fixes
85
+
86
+ - `tier-engine`: auto-default-tier-Hook benutzt jetzt `ctx.db.raw` für
87
+ Event-Store-Operationen (#37, vorher: stiller Bug, 22 Tage live).
88
+ - `engine`: unsafe-projection-upsert nutzt `as never` statt `as any` —
89
+ schmaler Cast-Surface, weniger Compiler-Knebel.
90
+ - `visual-tree`: runtime-isolation marker für client-konsumierte Files,
91
+ damit der Multi-Entry-Build den richtigen Bundle-Split bekommt.
92
+ - `feature-ast`: vollständiger `unsafeSkipTransitionGuard`-Rename (war
93
+ in zwei Modulen noch der alte Name).
94
+ - `framework`: Error-Reasons + `noConsole`-Lint + No-Date-API-Guard
95
+ wieder push-ready.
96
+
97
+ ### Library-Updates
98
+
99
+ hono 4.12, jose 6.2, stripe 22.1, meilisearch 0.58, marked 18,
100
+ bun-types 1.3.13, lucide-react 1.14, bullmq 5.76, ioredis 5.10,
101
+ i18next 26.0, react + radix-ui-primitives auf aktuelle Minors.
102
+
103
+ ### Patch Changes
104
+
105
+ - Updated dependencies
106
+ - @cosmicdrift/kumiko-dispatcher-live@0.3.0
107
+ - @cosmicdrift/kumiko-headless@0.3.0
108
+ - @cosmicdrift/kumiko-renderer@0.3.0
109
+
3
110
  ## 0.2.3
4
111
 
5
112
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.2.3",
3
+ "version": "0.4.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>",
@@ -9,38 +9,53 @@
9
9
  "runtime": "client"
10
10
  },
11
11
  "exports": {
12
- ".": "./src/index.ts",
12
+ ".": {
13
+ "types": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ },
13
16
  "./styles.css": "./src/styles.css"
14
17
  },
15
18
  "dependencies": {
16
- "@cosmicdrift/kumiko-dispatcher-live": "0.2.3",
17
- "@cosmicdrift/kumiko-headless": "0.2.3",
18
- "@cosmicdrift/kumiko-renderer": "0.2.3",
19
- "@radix-ui/react-dialog": "^1.1.6",
20
- "@radix-ui/react-dropdown-menu": "^2.1.6",
21
- "@radix-ui/react-label": "^2.1.2",
22
- "@radix-ui/react-popover": "^1.1.6",
23
- "@radix-ui/react-select": "^2.1.6",
24
- "@radix-ui/react-slot": "^1.1.2",
25
- "@radix-ui/react-toast": "^1.2.6",
26
- "@radix-ui/react-tooltip": "^1.1.8",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.4.0",
20
+ "@cosmicdrift/kumiko-headless": "0.4.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.4.0",
22
+ "@radix-ui/react-dialog": "^1.1.15",
23
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
24
+ "@radix-ui/react-label": "^2.1.8",
25
+ "@radix-ui/react-popover": "^1.1.15",
26
+ "@radix-ui/react-select": "^2.2.6",
27
+ "@radix-ui/react-slot": "^1.2.4",
28
+ "@radix-ui/react-toast": "^1.2.15",
29
+ "@radix-ui/react-tooltip": "^1.2.8",
27
30
  "class-variance-authority": "^0.7.1",
28
31
  "clsx": "^2.1.1",
29
32
  "cmdk": "^1.1.1",
30
- "lucide-react": "^1.11.0",
31
- "react": "^19.2.0",
33
+ "lucide-react": "^1.14.0",
34
+ "react": "^19.2.6",
32
35
  "react-day-picker": "^9.14.0",
33
- "react-dom": "^19.2.0",
34
- "tailwind-merge": "^3.0.2"
36
+ "react-dom": "^19.2.6",
37
+ "tailwind-merge": "^3.6.0"
38
+ },
39
+ "peerDependencies": {
40
+ "@tailwindcss/cli": "^4.3.0",
41
+ "tailwindcss": "^4.3.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@tailwindcss/cli": {
45
+ "optional": false
46
+ },
47
+ "tailwindcss": {
48
+ "optional": false
49
+ }
35
50
  },
36
51
  "devDependencies": {
37
- "@tailwindcss/cli": "^4.0.0",
38
- "@testing-library/react": "^16.3.0",
52
+ "@tailwindcss/cli": "^4.3.0",
53
+ "@testing-library/react": "^16.3.2",
39
54
  "@testing-library/user-event": "^14.6.1",
40
- "@types/react": "^19.2.0",
41
- "@types/react-dom": "^19.2.0",
42
- "jsdom": "^29.1.0",
43
- "tailwindcss": "^4.0.0"
55
+ "@types/react": "^19.2.14",
56
+ "@types/react-dom": "^19.2.3",
57
+ "jsdom": "^29.1.1",
58
+ "tailwindcss": "^4.3.0"
44
59
  },
45
60
  "repository": {
46
61
  "type": "git",
@@ -0,0 +1,315 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // V.1.1-D Integration-Test — End-to-End-Beweis für die Schleife
4
+ // `clientFeatures.treeProvider → useTreeProviders → VisualTree →
5
+ // TreeNodeRenderer → Click → dispatchTarget`.
6
+ //
7
+ // Im Gegensatz zum visual-tree.test.tsx (isolierte VisualTree-Component
8
+ // mit Mock-Providers) mountet dieser Test den vollen WorkspaceShell mit
9
+ // zwei Workspaces (nav + tree) und zwei Tree-Provider-clientFeatures.
10
+ // Bewert wird:
11
+ // 1. Provider-Iteration: beide Top-Level-Knoten landen im DOM
12
+ // (alphabetisch nach featureName)
13
+ // 2. Subscribe-Update: zweiter Emit eines Providers re-rendert die
14
+ // Sidebar ohne Workspace-Switch
15
+ // 3. Stub-Dispatch: Click auf Knoten mit target ruft dispatch mit
16
+ // richtigem TargetRef
17
+ // 4. Workspace-Switch: nav-Workspace zeigt NavTree, tree-Workspace
18
+ // zeigt VisualTree mit beiden Provider-Beiträgen
19
+ //
20
+ // **Memory `[Kein Fake-Dispatcher]`-Note**: V.1.1 hat keine HTTP-Calls
21
+ // (Tree-Provider sind reine Client-Functions ohne Args). Echtes
22
+ // setupTestStack kommt mit V.1.2 wenn text-content's Slug-Liste durch
23
+ // die Server-Pipeline geht.
24
+
25
+ import type { TreeChildrenSubscribe, TreeNode } from "@cosmicdrift/kumiko-framework/engine";
26
+ import type { FeatureSchema, WorkspaceSchema } from "@cosmicdrift/kumiko-renderer";
27
+ import {
28
+ createStaticLocaleResolver,
29
+ LocaleProvider,
30
+ NavProvider,
31
+ PrimitivesProvider,
32
+ } from "@cosmicdrift/kumiko-renderer";
33
+ import { act, fireEvent, render, screen } from "@testing-library/react";
34
+ import type { ReactNode } from "react";
35
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
36
+ import { useBrowserNavApi } from "../app/nav";
37
+ import { TreeProvidersProvider } from "../app/tree-providers-context";
38
+ import { setDispatchListener } from "../layout/target-resolver-stub";
39
+ import { WorkspaceShell } from "../layout/workspace-shell";
40
+ import { defaultPrimitives } from "../primitives";
41
+
42
+ // localStorage-Mock (vitest+Bun-Runtime liefert nur partielles
43
+ // localStorage). Pro Test frische Map damit Tests sauber isoliert sind.
44
+ beforeEach(() => {
45
+ const store = new Map<string, string>();
46
+ Object.defineProperty(window, "localStorage", {
47
+ configurable: true,
48
+ value: {
49
+ getItem: (key: string): string | null => store.get(key) ?? null,
50
+ setItem: (key: string, value: string): void => {
51
+ store.set(key, value);
52
+ },
53
+ removeItem: (key: string): void => {
54
+ store.delete(key);
55
+ },
56
+ clear: (): void => store.clear(),
57
+ get length(): number {
58
+ return store.size;
59
+ },
60
+ key: (i: number): string | null => Array.from(store.keys())[i] ?? null,
61
+ },
62
+ });
63
+ window.history.replaceState(null, "", "/");
64
+ });
65
+
66
+ function ws(
67
+ id: string,
68
+ options: {
69
+ label: string;
70
+ isDefault?: boolean;
71
+ navigation?: "nav" | "tree";
72
+ navMembers?: readonly string[];
73
+ },
74
+ ): WorkspaceSchema {
75
+ return {
76
+ definition: {
77
+ id,
78
+ label: options.label,
79
+ access: { openToAll: true },
80
+ ...(options.isDefault === true && { default: true }),
81
+ ...(options.navigation !== undefined && { navigation: options.navigation }),
82
+ },
83
+ navMembers: options.navMembers ?? [],
84
+ };
85
+ }
86
+
87
+ function makeStaticProvider(nodes: readonly TreeNode[]): TreeChildrenSubscribe {
88
+ return () => (emit) => {
89
+ emit(nodes);
90
+ return () => {};
91
+ };
92
+ }
93
+
94
+ function makeMutableProvider(initial: readonly TreeNode[]): {
95
+ readonly provider: TreeChildrenSubscribe;
96
+ emit(nodes: readonly TreeNode[]): void;
97
+ } {
98
+ let listener: ((nodes: readonly TreeNode[]) => void) | undefined;
99
+ const provider: TreeChildrenSubscribe = () => (emit) => {
100
+ listener = emit;
101
+ emit(initial);
102
+ return () => {
103
+ listener = undefined;
104
+ };
105
+ };
106
+ return {
107
+ provider,
108
+ emit(nodes) {
109
+ if (listener !== undefined) listener(nodes);
110
+ },
111
+ };
112
+ }
113
+
114
+ function renderShellWithTreeProviders(
115
+ schema: FeatureSchema,
116
+ treeProviders: ReadonlyMap<string, TreeChildrenSubscribe>,
117
+ ): ReturnType<typeof render> {
118
+ function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
119
+ const nav = useBrowserNavApi({ hasWorkspaces: true });
120
+ return (
121
+ <LocaleProvider resolver={createStaticLocaleResolver()}>
122
+ <PrimitivesProvider value={defaultPrimitives}>
123
+ <NavProvider value={nav}>
124
+ <TreeProvidersProvider value={treeProviders}>{children}</TreeProvidersProvider>
125
+ </NavProvider>
126
+ </PrimitivesProvider>
127
+ </LocaleProvider>
128
+ );
129
+ }
130
+ return render(
131
+ <WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
132
+ <div>content</div>
133
+ </WorkspaceShell>,
134
+ { wrapper: Wrapper },
135
+ );
136
+ }
137
+
138
+ describe("V.1.1-D Integration — Provider-Iteration + Subscribe + Dispatch", () => {
139
+ let cleanup: (() => void) | undefined;
140
+ afterEach(() => {
141
+ cleanup?.();
142
+ cleanup = undefined;
143
+ });
144
+
145
+ test("Provider-Iteration: beide Features landen im DOM, alphabetisch sortiert", () => {
146
+ const schema = {
147
+ featureName: "demo",
148
+ entities: {},
149
+ screens: [],
150
+ navs: [],
151
+ workspaces: [
152
+ ws("visual", {
153
+ label: "Visual",
154
+ isDefault: true,
155
+ navigation: "tree",
156
+ }),
157
+ ],
158
+ } as const;
159
+
160
+ const treeProviders = new Map<string, TreeChildrenSubscribe>([
161
+ ["text-content", makeStaticProvider([{ label: "Marketing" }])],
162
+ ["legal-pages", makeStaticProvider([{ label: "Imprint" }])],
163
+ ]);
164
+
165
+ renderShellWithTreeProviders(schema, treeProviders);
166
+
167
+ expect(screen.getByText("Marketing")).toBeTruthy();
168
+ expect(screen.getByText("Imprint")).toBeTruthy();
169
+
170
+ // Reihenfolge: legal-pages (l < t) kommt vor text-content
171
+ const branches = document.querySelectorAll("[data-kumiko-tree-branch]");
172
+ expect(branches[0]?.getAttribute("data-kumiko-tree-branch")).toBe("legal-pages");
173
+ expect(branches[1]?.getAttribute("data-kumiko-tree-branch")).toBe("text-content");
174
+ });
175
+
176
+ test("Subscribe-Update ohne Workspace-Switch: zweiter Emit re-rendert", () => {
177
+ const { provider, emit } = makeMutableProvider([{ label: "Hero" }]);
178
+
179
+ const schema = {
180
+ featureName: "demo",
181
+ entities: {},
182
+ screens: [],
183
+ navs: [],
184
+ workspaces: [ws("visual", { label: "Visual", isDefault: true, navigation: "tree" })],
185
+ } as const;
186
+
187
+ const treeProviders = new Map([["text-content", provider]]);
188
+
189
+ renderShellWithTreeProviders(schema, treeProviders);
190
+ expect(screen.getByText("Hero")).toBeTruthy();
191
+
192
+ act(() => {
193
+ emit([{ label: "Hero" }, { label: "Pricing" }, { label: "Footer" }]);
194
+ });
195
+
196
+ expect(screen.getByText("Pricing")).toBeTruthy();
197
+ expect(screen.getByText("Footer")).toBeTruthy();
198
+ });
199
+
200
+ test("Stub-Dispatch: Click → setDispatchListener-Spy bekommt richtige TargetRef", () => {
201
+ const dispatched: unknown[] = [];
202
+ cleanup = setDispatchListener((target) => {
203
+ dispatched.push(target);
204
+ });
205
+
206
+ const schema = {
207
+ featureName: "demo",
208
+ entities: {},
209
+ screens: [],
210
+ navs: [],
211
+ workspaces: [ws("visual", { label: "Visual", isDefault: true, navigation: "tree" })],
212
+ } as const;
213
+
214
+ const treeProviders = new Map<string, TreeChildrenSubscribe>([
215
+ [
216
+ "text-content",
217
+ makeStaticProvider([
218
+ {
219
+ label: "Imprint",
220
+ target: {
221
+ featureId: "text-content",
222
+ action: "edit",
223
+ args: { slug: "imprint" },
224
+ },
225
+ },
226
+ ]),
227
+ ],
228
+ ]);
229
+
230
+ renderShellWithTreeProviders(schema, treeProviders);
231
+ fireEvent.click(screen.getByText("Imprint"));
232
+
233
+ expect(dispatched).toEqual([
234
+ {
235
+ featureId: "text-content",
236
+ action: "edit",
237
+ args: { slug: "imprint" },
238
+ },
239
+ ]);
240
+ });
241
+
242
+ test("Workspace-Switch: nav-Workspace zeigt NavTree, tree-Workspace zeigt VisualTree-Provider", () => {
243
+ const schema = {
244
+ featureName: "demo",
245
+ entities: {},
246
+ screens: [],
247
+ navs: [{ id: "list", label: "Builder-List" }],
248
+ workspaces: [
249
+ ws("admin", {
250
+ label: "Admin",
251
+ isDefault: true,
252
+ navigation: "nav",
253
+ navMembers: ["demo:nav:list"],
254
+ }),
255
+ ws("visual", {
256
+ label: "Visual",
257
+ navigation: "tree",
258
+ }),
259
+ ],
260
+ } as const;
261
+
262
+ const treeProviders = new Map<string, TreeChildrenSubscribe>([
263
+ ["text-content", makeStaticProvider([{ label: "Marketing" }])],
264
+ ]);
265
+
266
+ renderShellWithTreeProviders(schema, treeProviders);
267
+
268
+ // Initial: admin-Workspace → NavTree mit "Builder-List"
269
+ expect(screen.getByText("Builder-List")).toBeTruthy();
270
+ expect(screen.queryByText("Marketing")).toBeNull();
271
+
272
+ // Switch zu visual → VisualTree mit Provider-Beitrag
273
+ fireEvent.click(screen.getByTestId("workspace-tab-visual"));
274
+ expect(screen.getByText("Marketing")).toBeTruthy();
275
+ expect(screen.queryByText("Builder-List")).toBeNull();
276
+
277
+ // Zurück zu admin → NavTree wieder
278
+ fireEvent.click(screen.getByTestId("workspace-tab-admin"));
279
+ expect(screen.getByText("Builder-List")).toBeTruthy();
280
+ expect(screen.queryByText("Marketing")).toBeNull();
281
+ });
282
+
283
+ test("localStorage-Persistenz: Toggle persistiert pro Workspace, Re-Mount restored", () => {
284
+ const schema = {
285
+ featureName: "demo",
286
+ entities: {},
287
+ screens: [],
288
+ navs: [],
289
+ workspaces: [ws("visual", { label: "Visual", isDefault: true, navigation: "tree" })],
290
+ } as const;
291
+
292
+ const treeProviders = new Map<string, TreeChildrenSubscribe>([
293
+ ["text-content", makeStaticProvider([{ label: "Marketing", children: [{ label: "Hero" }] }])],
294
+ ]);
295
+
296
+ const result = renderShellWithTreeProviders(schema, treeProviders);
297
+
298
+ // Initial collapsed: Hero nicht sichtbar
299
+ expect(screen.queryByText("Hero")).toBeNull();
300
+
301
+ // Toggle: Marketing ausklappen
302
+ fireEvent.click(screen.getByText("Marketing"));
303
+ expect(screen.getByText("Hero")).toBeTruthy();
304
+
305
+ // localStorage hat den Persisted-State
306
+ const stored = window.localStorage.getItem("kumiko:visual-tree:expanded:visual");
307
+ expect(stored).not.toBeNull();
308
+ expect(JSON.parse(stored ?? "[]")).toContain("text-content/0-Marketing");
309
+
310
+ // Re-Mount mit gleichem Workspace → expanded restored
311
+ result.unmount();
312
+ renderShellWithTreeProviders(schema, treeProviders);
313
+ expect(screen.getByText("Hero")).toBeTruthy();
314
+ });
315
+ });
@@ -59,6 +59,7 @@ function ws(
59
59
  openToAll?: boolean;
60
60
  isDefault?: boolean;
61
61
  navMembers?: readonly string[];
62
+ navigation?: "nav" | "tree";
62
63
  } = {},
63
64
  ): WorkspaceSchema {
64
65
  const access = options.openToAll
@@ -73,6 +74,7 @@ function ws(
73
74
  ...(options.order !== undefined && { order: options.order }),
74
75
  ...(access !== undefined && { access }),
75
76
  ...(options.isDefault === true && { default: true }),
77
+ ...(options.navigation !== undefined && { navigation: options.navigation }),
76
78
  },
77
79
  navMembers: options.navMembers ?? [],
78
80
  };
@@ -770,3 +772,143 @@ describe("WorkspaceShell — AppSchema (multi-feature)", () => {
770
772
  expect(screen.getByText("List")).toBeTruthy();
771
773
  });
772
774
  });
775
+
776
+ // ---------------------------------------------------------------------------
777
+ // navigation-Mode-Switch — Phase 0 Schicht 3 / V.1.
778
+ // Workspace mit `navigation: "tree"` mountet den Visual-Tree-Stub statt
779
+ // NavTree. Default (kein navigation oder navigation="nav") hält das
780
+ // existing NavTree-Verhalten — kein Breaking-Change für Apps die Visual-
781
+ // Tree nicht aktivieren. Echte Tree-Component kommt in V.1.1.
782
+ // Siehe docs/plans/architecture/visual-tree.md A1.
783
+ // ---------------------------------------------------------------------------
784
+
785
+ describe("WorkspaceShell — navigation-Mode (Visual-Tree opt-in)", () => {
786
+ test('navigation: "tree" mountet den Visual-Tree-Stub statt NavTree', () => {
787
+ const schema = {
788
+ featureName: "demo",
789
+ entities: {},
790
+ screens: [],
791
+ navs: [{ id: "list", label: "List" }],
792
+ workspaces: [
793
+ ws("visual", {
794
+ label: "Visual",
795
+ openToAll: true,
796
+ isDefault: true,
797
+ navigation: "tree",
798
+ navMembers: ["demo:nav:list"],
799
+ }),
800
+ ],
801
+ } as const;
802
+
803
+ renderShell(
804
+ <WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
805
+ <div>content</div>
806
+ </WorkspaceShell>,
807
+ );
808
+ // Stub-Marker rendert
809
+ expect(screen.getByLabelText("Visual Tree (no providers)")).toBeTruthy();
810
+ // NavTree wäre durch das nav-Item "List" sichtbar — darf hier NICHT
811
+ // rendern (Tree-Mode ersetzt NavTree komplett)
812
+ expect(screen.queryByText("List")).toBeNull();
813
+ });
814
+
815
+ test('navigation: "nav" mountet NavTree (default-äquivalent)', () => {
816
+ const schema = {
817
+ featureName: "demo",
818
+ entities: {},
819
+ screens: [],
820
+ navs: [{ id: "list", label: "List" }],
821
+ workspaces: [
822
+ ws("admin", {
823
+ label: "Admin",
824
+ openToAll: true,
825
+ isDefault: true,
826
+ navigation: "nav",
827
+ navMembers: ["demo:nav:list"],
828
+ }),
829
+ ],
830
+ } as const;
831
+
832
+ renderShell(
833
+ <WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
834
+ <div>content</div>
835
+ </WorkspaceShell>,
836
+ );
837
+ expect(screen.getByText("List")).toBeTruthy();
838
+ expect(screen.queryByLabelText("Visual Tree (no providers)")).toBeNull();
839
+ });
840
+
841
+ test("workspace ohne navigation-Property defaultet zu NavTree (Backwards-Compat)", () => {
842
+ const schema = {
843
+ featureName: "demo",
844
+ entities: {},
845
+ screens: [],
846
+ navs: [{ id: "list", label: "List" }],
847
+ workspaces: [
848
+ // Keine `navigation`-Property → existing Apps müssen exakt so
849
+ // weiter laufen wie vor V.1
850
+ ws("admin", {
851
+ label: "Admin",
852
+ openToAll: true,
853
+ isDefault: true,
854
+ navMembers: ["demo:nav:list"],
855
+ }),
856
+ ],
857
+ } as const;
858
+
859
+ renderShell(
860
+ <WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
861
+ <div>content</div>
862
+ </WorkspaceShell>,
863
+ );
864
+ expect(screen.getByText("List")).toBeTruthy();
865
+ expect(screen.queryByLabelText("Visual Tree (no providers)")).toBeNull();
866
+ });
867
+
868
+ test("Switcher-Toggle re-rendert Sidebar zwischen NavTree und Stub", () => {
869
+ // Multi-Workspace-Setup: beide accessible, einer nav, einer tree.
870
+ // Single-mount-Tests können nicht beweisen dass der conditional
871
+ // render bei prop-change sauber re-rendert — das ist die echte UX
872
+ // (User klickt im Switcher zwischen Modi). Dieser Test pinnt das.
873
+ const schema = {
874
+ featureName: "demo",
875
+ entities: {},
876
+ screens: [],
877
+ navs: [{ id: "list", label: "List" }],
878
+ workspaces: [
879
+ ws("admin", {
880
+ label: "Admin",
881
+ openToAll: true,
882
+ isDefault: true,
883
+ navigation: "nav",
884
+ navMembers: ["demo:nav:list"],
885
+ }),
886
+ ws("visual", {
887
+ label: "Visual",
888
+ openToAll: true,
889
+ navigation: "tree",
890
+ }),
891
+ ],
892
+ } as const;
893
+
894
+ renderShell(
895
+ <WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
896
+ <div>content</div>
897
+ </WorkspaceShell>,
898
+ );
899
+
900
+ // Initial: admin-Workspace (default) aktiv → NavTree sichtbar, kein Stub
901
+ expect(screen.getByText("List")).toBeTruthy();
902
+ expect(screen.queryByLabelText("Visual Tree (no providers)")).toBeNull();
903
+
904
+ // Switch zu visual-Workspace via Switcher-Click
905
+ fireEvent.click(screen.getByTestId("workspace-tab-visual"));
906
+ expect(screen.getByLabelText("Visual Tree (no providers)")).toBeTruthy();
907
+ expect(screen.queryByText("List")).toBeNull();
908
+
909
+ // Zurück zu admin → NavTree wieder, Stub weg
910
+ fireEvent.click(screen.getByTestId("workspace-tab-admin"));
911
+ expect(screen.getByText("List")).toBeTruthy();
912
+ expect(screen.queryByLabelText("Visual Tree (no providers)")).toBeNull();
913
+ });
914
+ });