@cosmicdrift/kumiko-renderer-web 0.133.0 → 0.135.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.133.0",
3
+ "version": "0.135.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.133.0",
20
- "@cosmicdrift/kumiko-headless": "0.133.0",
21
- "@cosmicdrift/kumiko-renderer": "0.133.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.135.0",
20
+ "@cosmicdrift/kumiko-headless": "0.135.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.135.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",
@@ -1,12 +1,15 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
3
3
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
4
- import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
4
+ import type { ExtensionSectionProps, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
5
5
  import {
6
6
  DashboardBodyProvider,
7
7
  DispatcherProvider,
8
+ ExtensionSectionsProvider,
8
9
  KumikoScreen,
9
10
  } from "@cosmicdrift/kumiko-renderer";
11
+ import userEvent from "@testing-library/user-event";
12
+ import type { ReactNode } from "react";
10
13
  import { WebDashboardBody } from "../app/dashboard-body";
11
14
  import { createMockDispatcher, render, screen, waitFor } from "./test-utils";
12
15
 
@@ -84,3 +87,206 @@ describe("KumikoScreen dashboard", () => {
84
87
  await waitFor(() => expect(screen.getByText("API-Ausfall")).toBeTruthy());
85
88
  });
86
89
  });
90
+
91
+ const richScreen: DashboardScreenDefinition = {
92
+ id: "rich",
93
+ type: "dashboard",
94
+ filter: {
95
+ id: "region",
96
+ label: "widgets:dashboard:filter-region",
97
+ kind: "select",
98
+ options: [
99
+ { value: "eu", label: "widgets:dashboard:filter-region-eu" },
100
+ { value: "us", label: "widgets:dashboard:filter-region-us" },
101
+ ],
102
+ },
103
+ panels: [
104
+ {
105
+ kind: "stat",
106
+ id: "kpi",
107
+ label: "widgets:dashboard:kpi",
108
+ query: "widgets:query:metrics:kpi",
109
+ valueField: "value",
110
+ },
111
+ {
112
+ kind: "stat-group",
113
+ id: "net-worth",
114
+ label: "widgets:dashboard:net-worth",
115
+ stats: [
116
+ {
117
+ kind: "stat",
118
+ id: "assets",
119
+ label: "widgets:dashboard:assets",
120
+ query: "widgets:query:metrics:assets",
121
+ valueField: "value",
122
+ },
123
+ ],
124
+ },
125
+ {
126
+ kind: "feed",
127
+ id: "upcoming",
128
+ label: "widgets:dashboard:upcoming",
129
+ query: "widgets:query:metrics:upcoming",
130
+ },
131
+ {
132
+ kind: "progress-list",
133
+ id: "goal-progress",
134
+ label: "widgets:dashboard:goal-progress",
135
+ query: "widgets:query:metrics:goal-progress",
136
+ },
137
+ {
138
+ kind: "custom",
139
+ id: "custom-panel",
140
+ component: { react: { __component: "rich-dashboard-custom" } },
141
+ },
142
+ ],
143
+ };
144
+
145
+ const richSchema: FeatureSchema = {
146
+ featureName: "widgets",
147
+ entities: {},
148
+ screens: [richScreen],
149
+ };
150
+
151
+ describe("KumikoScreen dashboard — neue Panel-Kinds", () => {
152
+ test("stat-group rendert Sektions-Titel + genestete Stat-Panels", async () => {
153
+ const dispatcher = createMockDispatcher({
154
+ query: (async (type: string) => {
155
+ if (type === "widgets:query:metrics:assets") {
156
+ return { isSuccess: true, data: { value: "120.000 €" } };
157
+ }
158
+ return { isSuccess: true, data: {} };
159
+ }) as unknown as Dispatcher["query"],
160
+ });
161
+ render(
162
+ <DispatcherProvider dispatcher={dispatcher}>
163
+ <DashboardBodyProvider value={WebDashboardBody}>
164
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
165
+ </DashboardBodyProvider>
166
+ </DispatcherProvider>,
167
+ );
168
+ expect(screen.getByText("widgets:dashboard:net-worth")).toBeTruthy();
169
+ await waitFor(() => expect(screen.getByText("120.000 €")).toBeTruthy());
170
+ });
171
+
172
+ test("feed-Panel rendert primary/trailing-Zeilen", async () => {
173
+ const dispatcher = createMockDispatcher({
174
+ query: (async (type: string) => {
175
+ if (type === "widgets:query:metrics:upcoming") {
176
+ return {
177
+ isSuccess: true,
178
+ data: { rows: [{ primary: "Zinsanpassung", trailing: "Aug 2026" }] },
179
+ };
180
+ }
181
+ return { isSuccess: true, data: {} };
182
+ }) as unknown as Dispatcher["query"],
183
+ });
184
+ render(
185
+ <DispatcherProvider dispatcher={dispatcher}>
186
+ <DashboardBodyProvider value={WebDashboardBody}>
187
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
188
+ </DashboardBodyProvider>
189
+ </DispatcherProvider>,
190
+ );
191
+ await waitFor(() => expect(screen.getByText("Zinsanpassung")).toBeTruthy());
192
+ expect(screen.getByText("Aug 2026")).toBeTruthy();
193
+ });
194
+
195
+ test("progress-list-Panel rendert Label/Wert + Fortschrittsbalken", async () => {
196
+ const dispatcher = createMockDispatcher({
197
+ query: (async (type: string) => {
198
+ if (type === "widgets:query:metrics:goal-progress") {
199
+ return {
200
+ isSuccess: true,
201
+ data: { rows: [{ label: "Baudarlehen", value: "42.000 € offen", fraction: 0.71 }] },
202
+ };
203
+ }
204
+ return { isSuccess: true, data: {} };
205
+ }) as unknown as Dispatcher["query"],
206
+ });
207
+ render(
208
+ <DispatcherProvider dispatcher={dispatcher}>
209
+ <DashboardBodyProvider value={WebDashboardBody}>
210
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
211
+ </DashboardBodyProvider>
212
+ </DispatcherProvider>,
213
+ );
214
+ await waitFor(() => expect(screen.getByText("Baudarlehen")).toBeTruthy());
215
+ const bar = screen.getByRole("progressbar");
216
+ expect(bar.getAttribute("aria-valuenow")).toBe("71");
217
+ });
218
+
219
+ test("custom-Panel: registrierte Komponente rendert mit screenId + filterParams", async () => {
220
+ function CustomEcho({ screenId, filterParams }: ExtensionSectionProps): ReactNode {
221
+ return (
222
+ <div data-testid="custom-echo">
223
+ {screenId}:{String(filterParams?.["region"] ?? "none")}
224
+ </div>
225
+ );
226
+ }
227
+ const dispatcher = createMockDispatcher();
228
+ render(
229
+ <DispatcherProvider dispatcher={dispatcher}>
230
+ <ExtensionSectionsProvider value={{ "rich-dashboard-custom": CustomEcho }}>
231
+ <DashboardBodyProvider value={WebDashboardBody}>
232
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
233
+ </DashboardBodyProvider>
234
+ </ExtensionSectionsProvider>
235
+ </DispatcherProvider>,
236
+ );
237
+ await waitFor(() => expect(screen.getByTestId("custom-echo")).toBeTruthy());
238
+ expect(screen.getByTestId("custom-echo").textContent).toBe("rich:none");
239
+ });
240
+
241
+ test("custom-Panel: unregistrierte Komponente rendert nichts, wirft nicht", async () => {
242
+ const dispatcher = createMockDispatcher();
243
+ render(
244
+ <DispatcherProvider dispatcher={dispatcher}>
245
+ <DashboardBodyProvider value={WebDashboardBody}>
246
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
247
+ </DashboardBodyProvider>
248
+ </DispatcherProvider>,
249
+ );
250
+ await waitFor(() => expect(screen.getByTestId("dashboard-panel-kpi")).toBeTruthy());
251
+ expect(screen.queryByTestId("custom-echo")).toBeNull();
252
+ });
253
+
254
+ test("Filter-Wechsel refetcht Stat- UND Feed-Panel mit neuem Payload", async () => {
255
+ const calls: { readonly type: string; readonly payload: unknown }[] = [];
256
+ const dispatcher = createMockDispatcher({
257
+ query: (async (type: string, payload: unknown) => {
258
+ calls.push({ type, payload });
259
+ const region = (payload as { readonly region?: string } | undefined)?.region;
260
+ if (type === "widgets:query:metrics:kpi") {
261
+ return { isSuccess: true, data: { value: region === "us" ? "38.120 $" : "92.753 €" } };
262
+ }
263
+ if (type === "widgets:query:metrics:upcoming") {
264
+ return {
265
+ isSuccess: true,
266
+ data: {
267
+ rows: [{ primary: region === "us" ? "US-Event" : "EU-Event" }],
268
+ },
269
+ };
270
+ }
271
+ return { isSuccess: true, data: {} };
272
+ }) as unknown as Dispatcher["query"],
273
+ });
274
+ const user = userEvent.setup();
275
+ render(
276
+ <DispatcherProvider dispatcher={dispatcher}>
277
+ <DashboardBodyProvider value={WebDashboardBody}>
278
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
279
+ </DashboardBodyProvider>
280
+ </DispatcherProvider>,
281
+ );
282
+ await waitFor(() => expect(screen.getByText("92.753 €")).toBeTruthy());
283
+ await waitFor(() => expect(screen.getByText("EU-Event")).toBeTruthy());
284
+
285
+ await user.click(screen.getByTestId("combobox-dashboard-filter-region"));
286
+ const usOption = await screen.findByText("widgets:dashboard:filter-region-us");
287
+ await user.click(usOption);
288
+
289
+ await waitFor(() => expect(screen.getByText("38.120 $")).toBeTruthy());
290
+ await waitFor(() => expect(screen.getByText("US-Event")).toBeTruthy());
291
+ });
292
+ });
@@ -57,7 +57,6 @@ function ws(
57
57
  openToAll?: boolean;
58
58
  isDefault?: boolean;
59
59
  navMembers?: readonly string[];
60
- navigation?: "nav" | "tree";
61
60
  } = {},
62
61
  ): WorkspaceSchema {
63
62
  const access = options.openToAll
@@ -72,7 +71,6 @@ function ws(
72
71
  ...(options.order !== undefined && { order: options.order }),
73
72
  ...(access !== undefined && { access }),
74
73
  ...(options.isDefault === true && { default: true }),
75
- ...(options.navigation !== undefined && { navigation: options.navigation }),
76
74
  },
77
75
  navMembers: options.navMembers ?? [],
78
76
  };
@@ -774,43 +772,13 @@ describe("WorkspaceShell — AppSchema (multi-feature)", () => {
774
772
  });
775
773
 
776
774
  // ---------------------------------------------------------------------------
777
- // EINE Nav (Visual-Tree-Merge). Der frühere navigation:"tree"/VisualTree-
778
- // Zweig ist gefaltet: WorkspaceShell rendert IMMER NavTree. Die
779
- // navigation-Property ist seitdem ein No-op (deferred removal aus
780
- // WorkspaceDefinition). Im Content-Bereich ersetzt ein aktives target
781
- // (aus den nav.searchParams) den RoutedScreen durch den EditorPanel.
775
+ // EINE Nav. WorkspaceShell rendert IMMER NavTree. Im Content-Bereich
776
+ // ersetzt ein aktives target (aus den nav.searchParams) den RoutedScreen
777
+ // durch den EditorPanel.
782
778
  // ---------------------------------------------------------------------------
783
779
 
784
- describe("WorkspaceShell — EINE Nav (Visual-Tree gefaltet)", () => {
785
- test('navigation: "tree" rendert NavTree (Tree-Stub ist gefaltet, kein Switch mehr)', () => {
786
- const schema = {
787
- featureName: "demo",
788
- entities: {},
789
- screens: [],
790
- navs: [{ id: "list", label: "List" }],
791
- workspaces: [
792
- ws("visual", {
793
- label: "Visual",
794
- openToAll: true,
795
- isDefault: true,
796
- navigation: "tree",
797
- navMembers: ["demo:nav:list"],
798
- }),
799
- ],
800
- } as const;
801
-
802
- renderShell(
803
- <WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
804
- <div>content</div>
805
- </WorkspaceShell>,
806
- );
807
- // NavTree rendert das nav-Item — auch bei navigation:"tree" (No-op).
808
- expect(screen.getByText("List")).toBeTruthy();
809
- // Der alte VisualTree-Stub existiert nicht mehr.
810
- expect(screen.queryByLabelText("Visual Tree (no providers)")).toBeNull();
811
- });
812
-
813
- test("workspace ohne navigation-Property rendert NavTree (Backwards-Compat)", () => {
780
+ describe("WorkspaceShell — EINE Nav", () => {
781
+ test("workspace rendert NavTree", () => {
814
782
  const schema = {
815
783
  featureName: "demo",
816
784
  entities: {},