@cosmicdrift/kumiko-renderer-web 0.136.1 → 0.137.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.136.1",
3
+ "version": "0.137.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.136.1",
20
- "@cosmicdrift/kumiko-headless": "0.136.1",
21
- "@cosmicdrift/kumiko-renderer": "0.136.1",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.137.0",
20
+ "@cosmicdrift/kumiko-headless": "0.137.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.137.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",
@@ -110,6 +110,8 @@ const richScreen: DashboardScreenDefinition = {
110
110
  deltaField: "delta",
111
111
  deltaDirectionField: "deltaDirection",
112
112
  deltaToneField: "deltaTone",
113
+ icon: { react: { __component: "rich-dashboard-kpi-icon" } },
114
+ accentColor: "#123456",
113
115
  },
114
116
  {
115
117
  kind: "stat-group",
@@ -282,6 +284,50 @@ describe("KumikoScreen dashboard — neue Panel-Kinds", () => {
282
284
  expect(screen.getByText("↓23 %")).toBeTruthy();
283
285
  });
284
286
 
287
+ test("stat-Panel: registriertes Icon rendert, unregistriertes rendert nichts (kein Crash)", async () => {
288
+ function KpiIcon(): ReactNode {
289
+ return <svg data-testid="kpi-icon" />;
290
+ }
291
+ const dispatcher = createMockDispatcher({
292
+ query: (async (type: string) => {
293
+ if (type === "widgets:query:metrics:kpi") {
294
+ return { isSuccess: true, data: { value: "92.753 €" } };
295
+ }
296
+ return { isSuccess: true, data: {} };
297
+ }) as unknown as Dispatcher["query"],
298
+ });
299
+ render(
300
+ <DispatcherProvider dispatcher={dispatcher}>
301
+ <ExtensionSectionsProvider value={{ "rich-dashboard-kpi-icon": KpiIcon }}>
302
+ <DashboardBodyProvider value={WebDashboardBody}>
303
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
304
+ </DashboardBodyProvider>
305
+ </ExtensionSectionsProvider>
306
+ </DispatcherProvider>,
307
+ );
308
+ await waitFor(() => expect(screen.getByTestId("kpi-icon")).toBeTruthy());
309
+ });
310
+
311
+ test("stat-Panel: unregistriertes Icon rendert nichts, wirft nicht", async () => {
312
+ const dispatcher = createMockDispatcher({
313
+ query: (async (type: string) => {
314
+ if (type === "widgets:query:metrics:kpi") {
315
+ return { isSuccess: true, data: { value: "92.753 €" } };
316
+ }
317
+ return { isSuccess: true, data: {} };
318
+ }) as unknown as Dispatcher["query"],
319
+ });
320
+ render(
321
+ <DispatcherProvider dispatcher={dispatcher}>
322
+ <DashboardBodyProvider value={WebDashboardBody}>
323
+ <KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
324
+ </DashboardBodyProvider>
325
+ </DispatcherProvider>,
326
+ );
327
+ await waitFor(() => expect(screen.getByText("92.753 €")).toBeTruthy());
328
+ expect(screen.queryByTestId("kpi-icon")).toBeNull();
329
+ });
330
+
285
331
  test("stat-Panel: fehlende deltaDirection unterdrückt den Chip (kein Crash)", async () => {
286
332
  const dispatcher = createMockDispatcher({
287
333
  query: (async (type: string) => {
@@ -10,6 +10,9 @@
10
10
  // deltaField/deltaDirectionField(+deltaToneField) sind
11
11
  // optional — nur wenn BEIDE Felder gesetzt sind UND geliefert
12
12
  // werden, zeigt die Kachel einen Delta-Chip ("↓ 23 %").
13
+ // icon/accentColor sind statisch am Panel (keine Query-
14
+ // Felder) — icon über extensionSectionComponents wie bei
15
+ // custom-Panels, accentColor ein roher CSS-Farbwert.
13
16
  // stat-group → mehrere stat-Panels unter einem Sektions-Titel, jedes
14
17
  // Kind bleibt eine eigenständige Query.
15
18
  // chart → { points: { atMs, value | null }[], windowStartMs,
@@ -58,13 +61,41 @@ const STAT_TONES: ReadonlySet<string> = new Set(["default", "positive", "warn"])
58
61
  const WIDE_PANEL = "sm:col-span-2 lg:col-span-4";
59
62
  const HALF_PANEL = "sm:col-span-2 lg:col-span-2";
60
63
 
64
+ function StatPanelIcon({
65
+ panel,
66
+ screenId,
67
+ filterParams,
68
+ }: {
69
+ readonly panel: DashboardStatPanel;
70
+ readonly screenId: string;
71
+ readonly filterParams: Readonly<Record<string, unknown>>;
72
+ }): ReactNode {
73
+ const name = panel.icon !== undefined ? extensionSectionName(panel.icon) : undefined;
74
+ const Icon = useExtensionSectionComponent(name);
75
+ useEffect(() => {
76
+ if (panel.icon !== undefined && name !== undefined && Icon === undefined) {
77
+ // biome-ignore lint/suspicious/noConsole: dev-warning für Setup-Fehler
78
+ console.warn(
79
+ `[kumiko] Dashboard stat-panel "${panel.id}" on screen "${screenId}" references icon ` +
80
+ `"${name}", which is not registered in clientFeatures.extensionSectionComponents.`,
81
+ );
82
+ }
83
+ }, [panel.icon, panel.id, name, Icon, screenId]);
84
+ if (Icon === undefined) return null;
85
+ return (
86
+ <Icon entityName={screenId} entityId={null} screenId={screenId} filterParams={filterParams} />
87
+ );
88
+ }
89
+
61
90
  function StatPanelBody({
62
91
  panel,
63
92
  label,
93
+ screenId,
64
94
  filterParams,
65
95
  }: {
66
96
  readonly panel: DashboardStatPanel;
67
97
  readonly label: string;
98
+ readonly screenId: string;
68
99
  readonly filterParams: Readonly<Record<string, unknown>>;
69
100
  }): ReactNode {
70
101
  const { data, error, loading, refetch } = useQuery<Readonly<Record<string, unknown>>>(
@@ -82,9 +113,15 @@ function StatPanelBody({
82
113
  const delta = readDelta(panel, record);
83
114
  return (
84
115
  <StatCard
116
+ icon={
117
+ panel.icon !== undefined ? (
118
+ <StatPanelIcon panel={panel} screenId={screenId} filterParams={filterParams} />
119
+ ) : undefined
120
+ }
85
121
  label={label}
86
122
  value={String(record[panel.valueField] ?? "—")}
87
123
  tone={tone}
124
+ accentColor={panel.accentColor}
88
125
  {...(sub !== undefined && sub !== null && { sub: String(sub) })}
89
126
  {...(delta !== undefined && { delta })}
90
127
  testId={`dashboard-panel-${panel.id}`}
@@ -110,10 +147,12 @@ function readDelta(
110
147
  function StatGroupPanelBody({
111
148
  panel,
112
149
  label,
150
+ screenId,
113
151
  filterParams,
114
152
  }: {
115
153
  readonly panel: DashboardStatGroupPanel;
116
154
  readonly label: string;
155
+ readonly screenId: string;
117
156
  readonly filterParams: Readonly<Record<string, unknown>>;
118
157
  }): ReactNode {
119
158
  const t = useTranslation();
@@ -125,6 +164,7 @@ function StatGroupPanelBody({
125
164
  key={stat.id}
126
165
  panel={stat}
127
166
  label={t(stat.label)}
167
+ screenId={screenId}
128
168
  filterParams={filterParams}
129
169
  />
130
170
  ))}
@@ -365,9 +405,18 @@ function PanelBody({
365
405
  readonly filterParams: Readonly<Record<string, unknown>>;
366
406
  }): ReactNode {
367
407
  if (panel.kind === "stat")
368
- return <StatPanelBody panel={panel} label={label} filterParams={filterParams} />;
408
+ return (
409
+ <StatPanelBody panel={panel} label={label} screenId={screenId} filterParams={filterParams} />
410
+ );
369
411
  if (panel.kind === "stat-group") {
370
- return <StatGroupPanelBody panel={panel} label={label} filterParams={filterParams} />;
412
+ return (
413
+ <StatGroupPanelBody
414
+ panel={panel}
415
+ label={label}
416
+ screenId={screenId}
417
+ filterParams={filterParams}
418
+ />
419
+ );
371
420
  }
372
421
  if (panel.kind === "chart")
373
422
  return <ChartPanelBody panel={panel} label={label} filterParams={filterParams} />;