@cosmicdrift/kumiko-renderer-web 0.115.1 → 0.116.1

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.1",
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.1",
20
+ "@cosmicdrift/kumiko-headless": "0.116.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.116.1",
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
  }
@@ -1370,7 +1370,9 @@ function DefaultForm({
1370
1370
  // OHNE Trennlinie darunter (Titel fließt in die erste Section). Sections
1371
1371
  // sind divide-y-getrennt (Linien nur ZWISCHEN ihnen). Action-Footer mit
1372
1372
  // bg-muted/30 als Farb-Trenner statt harter Linie. main hat kein Padding;
1373
- // der max-w-3xl-Body rahmt die Card.
1373
+ // der max-w-3xl-Body rahmt die Card, mx-auto zentriert ihn im Main statt
1374
+ // linksbündig (sonst „eingesperrt" mit leerer rechter Hälfte auf breiten
1375
+ // Screens).
1374
1376
  return (
1375
1377
  <form
1376
1378
  onSubmit={(e) => {
@@ -1380,7 +1382,7 @@ function DefaultForm({
1380
1382
  data-testid={testId}
1381
1383
  className="flex flex-col w-full"
1382
1384
  >
1383
- <div className="px-6 pt-6 pb-12 max-w-3xl w-full">
1385
+ <div className="px-6 pt-6 pb-12 max-w-3xl w-full mx-auto">
1384
1386
  <div className={cn(cardSurface(), "overflow-hidden")}>
1385
1387
  {(title !== undefined || subtitle !== undefined) && (
1386
1388
  <div className="px-6 pb-2 pt-5">