@cosmicdrift/kumiko-renderer-web 0.115.1 → 0.116.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.115.1",
3
+ "version": "0.116.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.115.1",
20
- "@cosmicdrift/kumiko-headless": "0.115.1",
21
- "@cosmicdrift/kumiko-renderer": "0.115.1",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.116.0",
20
+ "@cosmicdrift/kumiko-headless": "0.116.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.116.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",
@@ -329,10 +329,11 @@ describe("WorkspaceShell", () => {
329
329
  <div>content</div>
330
330
  </WorkspaceShell>,
331
331
  );
332
- // admin → system + orders, NOT tours
333
- expect(screen.getByText("System")).toBeTruthy();
334
- expect(screen.getByText("Orders")).toBeTruthy();
335
- expect(screen.queryByText("Tours")).toBeNull();
332
+ // admin → system + orders, NOT tours. getAllByText: die shadcn-Sidebar
333
+ // rendert Desktop + Mobile-Sheet, jeder Nav-Eintrag kommt also 2× vor.
334
+ expect(screen.getAllByText("System").length).toBeGreaterThan(0);
335
+ expect(screen.getAllByText("Orders").length).toBeGreaterThan(0);
336
+ expect(screen.queryAllByText("Tours").length).toBe(0);
336
337
  });
337
338
 
338
339
  test("clicking the dispatch tab swaps the visible nav set", () => {
@@ -346,10 +347,10 @@ describe("WorkspaceShell", () => {
346
347
  </WorkspaceShell>,
347
348
  );
348
349
  fireEvent.click(screen.getByTestId("workspace-tab-dispatch"));
349
- // dispatch → orders + tours, NOT system
350
- expect(screen.getByText("Orders")).toBeTruthy();
351
- expect(screen.getByText("Tours")).toBeTruthy();
352
- expect(screen.queryByText("System")).toBeNull();
350
+ // dispatch → orders + tours, NOT system (getAllByText: Desktop + Mobile-Sheet)
351
+ expect(screen.getAllByText("Orders").length).toBeGreaterThan(0);
352
+ expect(screen.getAllByText("Tours").length).toBeGreaterThan(0);
353
+ expect(screen.queryAllByText("System").length).toBe(0);
353
354
  });
354
355
 
355
356
  test("a driver lands on the driver workspace (their only one)", () => {
@@ -365,9 +366,9 @@ describe("WorkspaceShell", () => {
365
366
  // Only one workspace → switcher renders nothing, but the nav still
366
367
  // shows that workspace's members.
367
368
  expect(screen.queryByTestId("workspace-tab-driver")).toBeNull();
368
- expect(screen.getByText("Tours")).toBeTruthy();
369
- expect(screen.queryByText("System")).toBeNull();
370
- expect(screen.queryByText("Orders")).toBeNull();
369
+ expect(screen.getAllByText("Tours").length).toBeGreaterThan(0);
370
+ expect(screen.queryAllByText("System").length).toBe(0);
371
+ expect(screen.queryAllByText("Orders").length).toBe(0);
371
372
  });
372
373
 
373
374
  test("schema without workspaces falls back to plain rendering (all navs visible)", () => {
@@ -710,8 +711,9 @@ describe("WorkspaceShell — AppSchema (multi-feature)", () => {
710
711
  <div>content</div>
711
712
  </WorkspaceShell>,
712
713
  );
713
- expect(screen.getByTestId("sidebar-footer")).toBeTruthy();
714
- expect(screen.getByTestId("sidebar-footer").textContent).toBe("v1.2.3");
714
+ const footers = screen.getAllByTestId("sidebar-footer");
715
+ expect(footers.length).toBeGreaterThan(0);
716
+ expect(footers[0]?.textContent).toBe("v1.2.3");
715
717
  });
716
718
 
717
719
  test("ohne sidebarFooter-Prop rendert die Sidebar ohne Footer-Slot (default)", () => {
@@ -14,13 +14,8 @@
14
14
  // Apps die feingranulare Kontrolle wollen, gehen direkt auf die ui/-Teile.
15
15
  // Wer ein Topbar mit Workspace-Switcher braucht, nutzt <WorkspaceShell>.
16
16
 
17
- import type { NavNode } from "@cosmicdrift/kumiko-headless";
18
- import { resolveNavigation } from "@cosmicdrift/kumiko-headless";
19
17
  import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
20
- import { toAppSchema, useNav, useTranslation } from "@cosmicdrift/kumiko-renderer";
21
- import { type ReactNode, useMemo } from "react";
22
- import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from "../ui/breadcrumb";
23
- import { Separator } from "../ui/separator";
18
+ import type { ReactNode } from "react";
24
19
  import {
25
20
  Sidebar,
26
21
  SidebarContent,
@@ -30,9 +25,9 @@ import {
30
25
  SidebarInset,
31
26
  SidebarProvider,
32
27
  SidebarRail,
33
- SidebarTrigger,
34
28
  } from "../ui/sidebar";
35
- import { buildNavRegistrySliceForApp, lastSegment, NavTree } from "./nav-tree";
29
+ import { NavTree } from "./nav-tree";
30
+ import { ShellHeader } from "./shell-header";
36
31
 
37
32
  export type DefaultAppShellUser = {
38
33
  readonly id: string;
@@ -123,64 +118,3 @@ export function DefaultAppShell({
123
118
  </SidebarProvider>
124
119
  );
125
120
  }
126
-
127
- // Inset-Header: hostet den SidebarTrigger (öffnet das Mobile-Sheet / toggled
128
- // die Rail) und einen Breadcrumb mit dem aktiven Screen. Höhe == Sidebar-
129
- // Header (h-12), damit Rail und Panel oben bündig sind.
130
- function ShellHeader({
131
- schema,
132
- user,
133
- headerActions,
134
- }: {
135
- readonly schema: AppSchema | FeatureSchema;
136
- readonly user?: DefaultAppShellUser;
137
- readonly headerActions?: ReactNode;
138
- }): ReactNode {
139
- const nav = useNav();
140
- const t = useTranslation();
141
- const tree = useMemo(() => {
142
- const source = buildNavRegistrySliceForApp(toAppSchema(schema));
143
- return resolveNavigation({ source, ...(user !== undefined && { user }) });
144
- }, [schema, user]);
145
-
146
- const screenId = nav.route?.screenId;
147
- const label = screenId !== undefined ? activeNavLabel(tree, screenId, (k) => t(k)) : undefined;
148
-
149
- return (
150
- <header className="flex h-16 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
151
- <div className="flex items-center gap-2 px-4">
152
- <SidebarTrigger className="-ml-1" />
153
- <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
154
- {label !== undefined && (
155
- <Breadcrumb>
156
- <BreadcrumbList>
157
- <BreadcrumbItem>
158
- <BreadcrumbPage>{label}</BreadcrumbPage>
159
- </BreadcrumbItem>
160
- </BreadcrumbList>
161
- </Breadcrumb>
162
- )}
163
- </div>
164
- {headerActions !== undefined && (
165
- <div data-kumiko-layout="header-actions" className="ml-auto flex items-center gap-2 px-4">
166
- {headerActions}
167
- </div>
168
- )}
169
- </header>
170
- );
171
- }
172
-
173
- function activeNavLabel(
174
- nodes: readonly NavNode[],
175
- screenId: string,
176
- t: (key: string) => string,
177
- ): string | undefined {
178
- for (const node of nodes) {
179
- if (node.screen !== undefined && lastSegment(node.screen) === screenId) {
180
- return node.label.includes(".") ? t(node.label) : node.label;
181
- }
182
- const child = activeNavLabel(node.children, screenId, t);
183
- if (child !== undefined) return child;
184
- }
185
- return undefined;
186
- }
@@ -0,0 +1,78 @@
1
+ // ShellHeader — die Inset-Kopfzeile für Sidebar-basierte Shells: SidebarTrigger
2
+ // (Rail-/Mobile-Sheet-Toggle) + Breadcrumb mit dem aktiven Screen + optionale
3
+ // rechtsbündige headerActions. Geteilt von DefaultAppShell und WorkspaceShell,
4
+ // damit beide dieselbe Kopfzeile tragen (Höhe h-16, kollabiert auf h-12 mit
5
+ // der Icon-Rail).
6
+
7
+ import type { NavNode } from "@cosmicdrift/kumiko-headless";
8
+ import { resolveNavigation } from "@cosmicdrift/kumiko-headless";
9
+ import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
10
+ import { toAppSchema, useNav, useTranslation } from "@cosmicdrift/kumiko-renderer";
11
+ import { type ReactNode, useMemo } from "react";
12
+ import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from "../ui/breadcrumb";
13
+ import { Separator } from "../ui/separator";
14
+ import { SidebarTrigger } from "../ui/sidebar";
15
+ import { buildNavRegistrySliceForApp, lastSegment } from "./nav-tree";
16
+
17
+ type ShellHeaderUser = {
18
+ readonly id: string;
19
+ readonly roles: readonly string[];
20
+ };
21
+
22
+ export function ShellHeader({
23
+ schema,
24
+ user,
25
+ headerActions,
26
+ }: {
27
+ readonly schema: AppSchema | FeatureSchema;
28
+ readonly user?: ShellHeaderUser;
29
+ readonly headerActions?: ReactNode;
30
+ }): ReactNode {
31
+ const nav = useNav();
32
+ const t = useTranslation();
33
+ const tree = useMemo(() => {
34
+ const source = buildNavRegistrySliceForApp(toAppSchema(schema));
35
+ return resolveNavigation({ source, ...(user !== undefined && { user }) });
36
+ }, [schema, user]);
37
+
38
+ const screenId = nav.route?.screenId;
39
+ const label = screenId !== undefined ? activeNavLabel(tree, screenId, (k) => t(k)) : undefined;
40
+
41
+ return (
42
+ <header className="flex h-16 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
43
+ <div className="flex items-center gap-2 px-4">
44
+ <SidebarTrigger className="-ml-1" />
45
+ <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
46
+ {label !== undefined && (
47
+ <Breadcrumb>
48
+ <BreadcrumbList>
49
+ <BreadcrumbItem>
50
+ <BreadcrumbPage>{label}</BreadcrumbPage>
51
+ </BreadcrumbItem>
52
+ </BreadcrumbList>
53
+ </Breadcrumb>
54
+ )}
55
+ </div>
56
+ {headerActions !== undefined && (
57
+ <div data-kumiko-layout="header-actions" className="ml-auto flex items-center gap-2 px-4">
58
+ {headerActions}
59
+ </div>
60
+ )}
61
+ </header>
62
+ );
63
+ }
64
+
65
+ function activeNavLabel(
66
+ nodes: readonly NavNode[],
67
+ screenId: string,
68
+ t: (key: string) => string,
69
+ ): string | undefined {
70
+ for (const node of nodes) {
71
+ if (node.screen !== undefined && lastSegment(node.screen) === screenId) {
72
+ return node.label.includes(".") ? t(node.label) : node.label;
73
+ }
74
+ const child = activeNavLabel(node.children, screenId, t);
75
+ if (child !== undefined) return child;
76
+ }
77
+ return undefined;
78
+ }
@@ -34,13 +34,19 @@ import type { AppSchema, FeatureSchema, WorkspaceSchema } from "@cosmicdrift/kum
34
34
  import { qualifyNavId, toAppSchema, useNav } from "@cosmicdrift/kumiko-renderer";
35
35
  import { type ReactNode, useCallback, useLayoutEffect, useMemo } from "react";
36
36
  import { useResolvers } from "../app/resolvers-context";
37
- import { SidebarProvider } from "../ui/sidebar";
38
- import { AppLayout } from "./app-layout";
37
+ import {
38
+ Sidebar,
39
+ SidebarContent,
40
+ SidebarFooter,
41
+ SidebarHeader,
42
+ SidebarInset,
43
+ SidebarProvider,
44
+ SidebarRail,
45
+ } from "../ui/sidebar";
39
46
  import { EditorPanel } from "./editor-panel";
40
47
  import { lastSegment, NavTree } from "./nav-tree";
41
- import { Sidebar } from "./sidebar";
48
+ import { ShellHeader } from "./shell-header";
42
49
  import { parseTargetFromSearchParams } from "./target-url";
43
- import { Topbar } from "./topbar";
44
50
  import { WorkspaceSwitcher } from "./workspace-switcher";
45
51
 
46
52
  export type WorkspaceShellUser = {
@@ -204,21 +210,34 @@ export function WorkspaceShell({
204
210
  />
205
211
  );
206
212
 
207
- // SidebarProvider: NavTree rendert vendored shadcn-SidebarMenuButtons, die
208
- // useSidebar() rufen. WorkspaceShell behält sein eigenes Topbar-Layout
209
- // (AppLayout + Sidebar), braucht aber den Context-Wrapper drumherum.
213
+ // Modern shell (wie DefaultAppShell): collapsible Icon-Rail-Sidebar trägt
214
+ // Brand + Workspace-Switcher + Nav + Footer; der SidebarInset rendert die
215
+ // geteilte ShellHeader (Panel-Toggle + Breadcrumb + Actions) über dem
216
+ // Content. topbarActions landen rechts in der Header-Zeile statt in einer
217
+ // separaten Topbar — eine Kopfzeile statt zwei.
210
218
  return (
211
219
  <SidebarProvider>
212
- <AppLayout
213
- topbar={<Topbar start={brand} center={switcher || undefined} end={topbarActions} />}
214
- sidebar={
215
- <Sidebar {...(sidebarFooter !== undefined && { footer: sidebarFooter })}>
216
- {sidebarContent}
217
- </Sidebar>
218
- }
219
- >
220
- {activeTarget !== undefined ? <EditorPanel resolvers={resolvers} /> : children}
221
- </AppLayout>
220
+ <Sidebar collapsible="icon">
221
+ <SidebarHeader data-kumiko-layout="sidebar-header">
222
+ {brand}
223
+ {switcher}
224
+ </SidebarHeader>
225
+ <SidebarContent data-kumiko-layout="sidebar-nav">{sidebarContent}</SidebarContent>
226
+ {sidebarFooter !== undefined && (
227
+ <SidebarFooter data-kumiko-layout="sidebar-footer">{sidebarFooter}</SidebarFooter>
228
+ )}
229
+ <SidebarRail />
230
+ </Sidebar>
231
+ <SidebarInset>
232
+ <ShellHeader
233
+ schema={app}
234
+ {...(user !== undefined && { user })}
235
+ {...(topbarActions !== undefined && { headerActions: topbarActions })}
236
+ />
237
+ <main className="flex-1 overflow-auto">
238
+ {activeTarget !== undefined ? <EditorPanel resolvers={resolvers} /> : children}
239
+ </main>
240
+ </SidebarInset>
222
241
  </SidebarProvider>
223
242
  );
224
243
  }