@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 +107 -0
- package/package.json +38 -23
- package/src/__tests__/visual-tree-integration.test.tsx +315 -0
- package/src/__tests__/workspace-shell.test.tsx +142 -0
- package/src/app/client-plugin.tsx +37 -0
- package/src/app/create-app.tsx +49 -5
- package/src/app/resolvers-context.tsx +29 -0
- package/src/app/tree-providers-context.tsx +68 -0
- package/src/layout/__tests__/visual-tree.test.tsx +300 -0
- package/src/layout/avatar.tsx +1 -1
- package/src/layout/editor-panel.tsx +104 -0
- package/src/layout/target-resolver-stub.tsx +75 -0
- package/src/layout/target-url.ts +86 -0
- package/src/layout/tree-node-renderer.tsx +386 -0
- package/src/layout/visual-tree.tsx +398 -0
- package/src/layout/workspace-shell.tsx +31 -6
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.
|
|
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
|
-
".":
|
|
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.
|
|
17
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
-
"@radix-ui/react-dialog": "^1.1.
|
|
20
|
-
"@radix-ui/react-dropdown-menu": "^2.1.
|
|
21
|
-
"@radix-ui/react-label": "^2.1.
|
|
22
|
-
"@radix-ui/react-popover": "^1.1.
|
|
23
|
-
"@radix-ui/react-select": "^2.
|
|
24
|
-
"@radix-ui/react-slot": "^1.
|
|
25
|
-
"@radix-ui/react-toast": "^1.2.
|
|
26
|
-
"@radix-ui/react-tooltip": "^1.
|
|
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.
|
|
31
|
-
"react": "^19.2.
|
|
33
|
+
"lucide-react": "^1.14.0",
|
|
34
|
+
"react": "^19.2.6",
|
|
32
35
|
"react-day-picker": "^9.14.0",
|
|
33
|
-
"react-dom": "^19.2.
|
|
34
|
-
"tailwind-merge": "^3.0
|
|
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.
|
|
38
|
-
"@testing-library/react": "^16.3.
|
|
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.
|
|
41
|
-
"@types/react-dom": "^19.2.
|
|
42
|
-
"jsdom": "^29.1.
|
|
43
|
-
"tailwindcss": "^4.
|
|
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
|
+
});
|