@cosmicdrift/kumiko-renderer-web 0.135.0 → 0.136.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.
|
|
3
|
+
"version": "0.136.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.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.136.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.136.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.136.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",
|
|
@@ -107,6 +107,9 @@ const richScreen: DashboardScreenDefinition = {
|
|
|
107
107
|
label: "widgets:dashboard:kpi",
|
|
108
108
|
query: "widgets:query:metrics:kpi",
|
|
109
109
|
valueField: "value",
|
|
110
|
+
deltaField: "delta",
|
|
111
|
+
deltaDirectionField: "deltaDirection",
|
|
112
|
+
deltaToneField: "deltaTone",
|
|
110
113
|
},
|
|
111
114
|
{
|
|
112
115
|
kind: "stat-group",
|
|
@@ -251,6 +254,54 @@ describe("KumikoScreen dashboard — neue Panel-Kinds", () => {
|
|
|
251
254
|
expect(screen.queryByTestId("custom-echo")).toBeNull();
|
|
252
255
|
});
|
|
253
256
|
|
|
257
|
+
test("stat-Panel: Delta-Chip rendert nur wenn value+direction geliefert werden", async () => {
|
|
258
|
+
const dispatcher = createMockDispatcher({
|
|
259
|
+
query: (async (type: string) => {
|
|
260
|
+
if (type === "widgets:query:metrics:kpi") {
|
|
261
|
+
return {
|
|
262
|
+
isSuccess: true,
|
|
263
|
+
data: {
|
|
264
|
+
value: "92.753 €",
|
|
265
|
+
delta: "23 %",
|
|
266
|
+
deltaDirection: "down",
|
|
267
|
+
deltaTone: "positive",
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { isSuccess: true, data: {} };
|
|
272
|
+
}) as unknown as Dispatcher["query"],
|
|
273
|
+
});
|
|
274
|
+
render(
|
|
275
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
276
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
277
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
278
|
+
</DashboardBodyProvider>
|
|
279
|
+
</DispatcherProvider>,
|
|
280
|
+
);
|
|
281
|
+
await waitFor(() => expect(screen.getByText("92.753 €")).toBeTruthy());
|
|
282
|
+
expect(screen.getByText("↓23 %")).toBeTruthy();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("stat-Panel: fehlende deltaDirection unterdrückt den Chip (kein Crash)", async () => {
|
|
286
|
+
const dispatcher = createMockDispatcher({
|
|
287
|
+
query: (async (type: string) => {
|
|
288
|
+
if (type === "widgets:query:metrics:kpi") {
|
|
289
|
+
return { isSuccess: true, data: { value: "92.753 €" } };
|
|
290
|
+
}
|
|
291
|
+
return { isSuccess: true, data: {} };
|
|
292
|
+
}) as unknown as Dispatcher["query"],
|
|
293
|
+
});
|
|
294
|
+
render(
|
|
295
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
296
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
297
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
298
|
+
</DashboardBodyProvider>
|
|
299
|
+
</DispatcherProvider>,
|
|
300
|
+
);
|
|
301
|
+
await waitFor(() => expect(screen.getByText("92.753 €")).toBeTruthy());
|
|
302
|
+
expect(screen.queryByText("23 %")).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
|
|
254
305
|
test("Filter-Wechsel refetcht Stat- UND Feed-Panel mit neuem Payload", async () => {
|
|
255
306
|
const calls: { readonly type: string; readonly payload: unknown }[] = [];
|
|
256
307
|
const dispatcher = createMockDispatcher({
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
// Panel-Daten-Contracts (siehe DashboardPanelDefinition in kumiko-framework):
|
|
8
8
|
// stat → flaches Record, valueField/subField/toneField zeigen auf
|
|
9
9
|
// anzeigefertige Werte (der Query-Handler formatiert).
|
|
10
|
+
// deltaField/deltaDirectionField(+deltaToneField) sind
|
|
11
|
+
// optional — nur wenn BEIDE Felder gesetzt sind UND geliefert
|
|
12
|
+
// werden, zeigt die Kachel einen Delta-Chip ("↓ 23 %").
|
|
10
13
|
// stat-group → mehrere stat-Panels unter einem Sektions-Titel, jedes
|
|
11
14
|
// Kind bleibt eine eigenständige Query.
|
|
12
15
|
// chart → { points: { atMs, value | null }[], windowStartMs,
|
|
@@ -48,7 +51,7 @@ import { FeedList, type FeedRow } from "../widgets/feed-list";
|
|
|
48
51
|
import { ProgressList, type ProgressListRow } from "../widgets/progress-list";
|
|
49
52
|
import { QueryTable } from "../widgets/query-table";
|
|
50
53
|
import { SectionCard } from "../widgets/section-card";
|
|
51
|
-
import { StatCard, type StatTone } from "../widgets/stat";
|
|
54
|
+
import { StatCard, type StatDelta, type StatTone } from "../widgets/stat";
|
|
52
55
|
import { ErrorState, LoadingState } from "../widgets/states";
|
|
53
56
|
|
|
54
57
|
const STAT_TONES: ReadonlySet<string> = new Set(["default", "positive", "warn"]);
|
|
@@ -76,17 +79,34 @@ function StatPanelBody({
|
|
|
76
79
|
const tone =
|
|
77
80
|
typeof rawTone === "string" && STAT_TONES.has(rawTone) ? (rawTone as StatTone) : "default";
|
|
78
81
|
const sub = panel.subField !== undefined ? record[panel.subField] : undefined;
|
|
82
|
+
const delta = readDelta(panel, record);
|
|
79
83
|
return (
|
|
80
84
|
<StatCard
|
|
81
85
|
label={label}
|
|
82
86
|
value={String(record[panel.valueField] ?? "—")}
|
|
83
87
|
tone={tone}
|
|
84
88
|
{...(sub !== undefined && sub !== null && { sub: String(sub) })}
|
|
89
|
+
{...(delta !== undefined && { delta })}
|
|
85
90
|
testId={`dashboard-panel-${panel.id}`}
|
|
86
91
|
/>
|
|
87
92
|
);
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
function readDelta(
|
|
96
|
+
panel: DashboardStatPanel,
|
|
97
|
+
record: Readonly<Record<string, unknown>>,
|
|
98
|
+
): StatDelta | undefined {
|
|
99
|
+
if (panel.deltaField === undefined || panel.deltaDirectionField === undefined) return undefined;
|
|
100
|
+
const value = record[panel.deltaField];
|
|
101
|
+
const direction = record[panel.deltaDirectionField];
|
|
102
|
+
if (value === undefined || value === null) return undefined;
|
|
103
|
+
if (direction !== "up" && direction !== "down") return undefined;
|
|
104
|
+
const rawTone = panel.deltaToneField !== undefined ? record[panel.deltaToneField] : undefined;
|
|
105
|
+
const tone =
|
|
106
|
+
typeof rawTone === "string" && STAT_TONES.has(rawTone) ? (rawTone as StatTone) : undefined;
|
|
107
|
+
return { value: String(value), direction, ...(tone !== undefined && { tone }) };
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
function StatGroupPanelBody({
|
|
91
111
|
panel,
|
|
92
112
|
label,
|