@cosmicdrift/kumiko-renderer-web 0.252.0 → 0.252.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.252.0",
3
+ "version": "0.252.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.252.0",
20
- "@cosmicdrift/kumiko-headless": "0.252.0",
21
- "@cosmicdrift/kumiko-renderer": "0.252.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.252.1",
20
+ "@cosmicdrift/kumiko-headless": "0.252.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.252.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",
@@ -64,7 +64,7 @@
64
64
  "@types/react-dom": "^19.2.3",
65
65
  "jsdom": "^29.1.1",
66
66
  "tailwindcss": "^4.3.0",
67
- "@cosmicdrift/kumiko-locale-de": "0.252.0"
67
+ "@cosmicdrift/kumiko-locale-de": "0.252.1"
68
68
  },
69
69
  "repository": {
70
70
  "type": "git",
@@ -90,6 +90,76 @@ describe("KumikoScreen dashboard", () => {
90
90
  });
91
91
  });
92
92
 
93
+ // screen.description exists on DashboardScreenDefinition (types/src/
94
+ // screen.ts:713) but WebDashboardBody never read it — the panels grid
95
+ // rendered with no room for it at all.
96
+ describe("KumikoScreen dashboard — screen.description", () => {
97
+ test("a plain-text description renders above the panels", async () => {
98
+ const screenWithDescription: DashboardScreenDefinition = {
99
+ ...dashboardScreen,
100
+ description: "Live health signals across every monitored service.",
101
+ };
102
+ const schemaWithDescription: FeatureSchema = {
103
+ featureName: "status",
104
+ entities: {},
105
+ screens: [screenWithDescription],
106
+ };
107
+ render(
108
+ <DispatcherProvider dispatcher={makeDispatcher()}>
109
+ <DashboardBodyProvider value={WebDashboardBody}>
110
+ <KumikoScreen schema={schemaWithDescription} qn="status:screen:overview" />
111
+ </DashboardBodyProvider>
112
+ </DispatcherProvider>,
113
+ );
114
+
115
+ expect(screen.getByTestId("dashboard-overview-description").textContent).toBe(
116
+ "Live health signals across every monitored service.",
117
+ );
118
+ });
119
+
120
+ test("a description that is a known i18n key renders translated, not as the raw key", async () => {
121
+ const screenWithDescription: DashboardScreenDefinition = {
122
+ ...dashboardScreen,
123
+ description: "status:dashboard:explainer",
124
+ };
125
+ const schemaWithDescription: FeatureSchema = {
126
+ featureName: "status",
127
+ entities: {},
128
+ screens: [screenWithDescription],
129
+ };
130
+ render(
131
+ <DispatcherProvider dispatcher={makeDispatcher()}>
132
+ <DashboardBodyProvider value={WebDashboardBody}>
133
+ <KumikoScreen
134
+ schema={schemaWithDescription}
135
+ qn="status:screen:overview"
136
+ translate={(key) =>
137
+ key === "status:dashboard:explainer" ? "Refreshes every 60 seconds." : key
138
+ }
139
+ />
140
+ </DashboardBodyProvider>
141
+ </DispatcherProvider>,
142
+ );
143
+
144
+ const description = screen.getByTestId("dashboard-overview-description");
145
+ expect(description.textContent).toBe("Refreshes every 60 seconds.");
146
+ expect(description.textContent).not.toBe("status:dashboard:explainer");
147
+ });
148
+
149
+ test("a screen without description renders no description block", async () => {
150
+ render(
151
+ <DispatcherProvider dispatcher={makeDispatcher()}>
152
+ <DashboardBodyProvider value={WebDashboardBody}>
153
+ <KumikoScreen schema={schema} qn="status:screen:overview" />
154
+ </DashboardBodyProvider>
155
+ </DispatcherProvider>,
156
+ );
157
+
158
+ await waitFor(() => expect(screen.getByTestId("dashboard-overview")).toBeTruthy());
159
+ expect(screen.queryByTestId("dashboard-overview-description")).toBeNull();
160
+ });
161
+ });
162
+
93
163
  const richScreen: DashboardScreenDefinition = {
94
164
  id: "rich",
95
165
  type: "dashboard",
@@ -202,6 +202,45 @@ describe("RenderEdit", () => {
202
202
  expect(fieldEl.querySelector("input")?.className).toContain("pl-8");
203
203
  });
204
204
 
205
+ // Fall 2: tabs layout sets hideSectionTitles (kumiko-screen.tsx's
206
+ // ProjectionDetailBody, the only real caller) to blank out each section's
207
+ // OWN title — the active tab already names it. That gate used to also
208
+ // swallow the screen-level subtitle (screen.description), which has
209
+ // nothing to do with section titles and must survive tabs mode.
210
+ test("tabs layout (hideSectionTitles) keeps the section title hidden but still shows the screen's own subtitle", () => {
211
+ const entity = {
212
+ fields: { email: { type: "text", required: true } },
213
+ } as unknown as EntityDefinition;
214
+ const screenDef: EntityEditScreenDefinition = {
215
+ id: "orders:screen:order-detail",
216
+ type: "entityEdit",
217
+ entity: "order",
218
+ description: "Everything about this order in one place.",
219
+ layout: {
220
+ mode: "tabs",
221
+ sections: [{ id: "contact", title: "Contact", fields: [{ field: "email" }] }],
222
+ },
223
+ };
224
+ render(
225
+ <DispatcherProvider dispatcher={makeDispatcher()}>
226
+ <RenderEdit
227
+ screen={screenDef}
228
+ entity={entity}
229
+ featureName="orders"
230
+ initial={{ email: "" } as never}
231
+ writeCommand="order:create"
232
+ hideSectionTitles
233
+ />
234
+ </DispatcherProvider>,
235
+ );
236
+
237
+ expect(screen.queryByTestId("render-edit-form-title")).toBeNull();
238
+ expect(screen.queryByTestId("section-Contact-title")).toBeNull();
239
+ expect(screen.getByTestId("render-edit-form-subtitle").textContent).toBe(
240
+ "Everything about this order in one place.",
241
+ );
242
+ });
243
+
205
244
  // End-to-end-Routing: ein `type:"locatedTimestamp"`-Entity-Feld muss durch
206
245
  // computeEditViewModel → render-field → DefaultInput auf den Located-Picker
207
246
  // laufen (Datum + Uhrzeit + Zone), NICHT auf den Klartext-Fallthrough. Vor
@@ -0,0 +1,84 @@
1
+ // SecretsEditScreenDefinition.description exists (types/src/screen.ts:1205)
2
+ // to explain what the secrets on this screen are for, same doc-promise as
3
+ // every other description slot — but SecretsEditBody built its own <Form>
4
+ // without ever reading screen.description. Fixed by wiring it into the
5
+ // Form's established subtitle slot (same slot render-edit.tsx and
6
+ // write-form-section.tsx use), rendered through DefaultForm — real
7
+ // renderer-web primitives, not a stub, so this proves the actual DOM output.
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import type { SecretsEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
11
+ import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
12
+ import { DispatcherProvider } from "@cosmicdrift/kumiko-renderer";
13
+ import { SecretsEditBody } from "../../../renderer/src/app/secrets-edit-body";
14
+ import { createMockDispatcher, render, screen, waitFor } from "./test-utils";
15
+
16
+ function makeDispatcher(): Dispatcher {
17
+ return createMockDispatcher({
18
+ query: (async () => ({ isSuccess: true, data: [] })) as Dispatcher["query"],
19
+ });
20
+ }
21
+
22
+ function baseScreen(): SecretsEditScreenDefinition {
23
+ return {
24
+ id: "integrations:screen:secrets",
25
+ type: "secretsEdit",
26
+ secretKeys: { apiKey: "integrations:secret:api-key" },
27
+ fieldLabels: { apiKey: "API key" },
28
+ sections: [{ fields: ["apiKey"] }],
29
+ };
30
+ }
31
+
32
+ describe("SecretsEditBody — screen.description as form subtitle", () => {
33
+ test("a plain-text description renders as the form's subtitle", async () => {
34
+ const screenDef: SecretsEditScreenDefinition = {
35
+ ...baseScreen(),
36
+ description: "These credentials are shared across every integration on this workspace.",
37
+ };
38
+ render(
39
+ <DispatcherProvider dispatcher={makeDispatcher()}>
40
+ <SecretsEditBody screen={screenDef} />
41
+ </DispatcherProvider>,
42
+ );
43
+
44
+ await waitFor(() =>
45
+ expect(screen.getByTestId("secrets-edit-form-subtitle").textContent).toBe(
46
+ "These credentials are shared across every integration on this workspace.",
47
+ ),
48
+ );
49
+ });
50
+
51
+ test("a description that is a known i18n key renders translated, not as the raw key", async () => {
52
+ const screenDef: SecretsEditScreenDefinition = {
53
+ ...baseScreen(),
54
+ description: "integrations:secrets.explainer",
55
+ };
56
+ render(
57
+ <DispatcherProvider dispatcher={makeDispatcher()}>
58
+ <SecretsEditBody
59
+ screen={screenDef}
60
+ translate={(key) =>
61
+ key === "integrations:secrets.explainer" ? "Rotate these keys every 90 days." : key
62
+ }
63
+ />
64
+ </DispatcherProvider>,
65
+ );
66
+
67
+ await waitFor(() => {
68
+ const subtitle = screen.getByTestId("secrets-edit-form-subtitle");
69
+ expect(subtitle.textContent).toBe("Rotate these keys every 90 days.");
70
+ expect(subtitle.textContent).not.toBe("integrations:secrets.explainer");
71
+ });
72
+ });
73
+
74
+ test("a screen without description renders no subtitle at all", async () => {
75
+ render(
76
+ <DispatcherProvider dispatcher={makeDispatcher()}>
77
+ <SecretsEditBody screen={baseScreen()} />
78
+ </DispatcherProvider>,
79
+ );
80
+
81
+ await waitFor(() => expect(screen.getByTestId("secrets-edit-form")).toBeTruthy());
82
+ expect(screen.queryByTestId("secrets-edit-form-subtitle")).toBeNull();
83
+ });
84
+ });
@@ -437,9 +437,15 @@ function PanelBody({
437
437
  export function WebDashboardBody({ screen, translate }: DashboardBodyProps): ReactNode {
438
438
  const t = useTranslation();
439
439
  const effectiveTranslate = translate ?? t;
440
+ const { Text } = usePrimitives();
440
441
  const { params: filterParams, picker } = useFilterParams(screen);
441
442
  return (
442
443
  <PageSection className="flex flex-col gap-4" testId={`dashboard-${screen.id}`}>
444
+ {screen.description !== undefined && (
445
+ <Text variant="muted" testId={`dashboard-${screen.id}-description`}>
446
+ {effectiveTranslate(screen.description)}
447
+ </Text>
448
+ )}
443
449
  {picker}
444
450
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
445
451
  {screen.panels.map((panel) => {