@cosmicdrift/kumiko-renderer-web 0.218.0 → 0.220.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.220.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.220.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.220.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.220.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",
|
|
@@ -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.
|
|
67
|
+
"@cosmicdrift/kumiko-locale-de": "0.220.0"
|
|
68
68
|
},
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
@@ -24,8 +24,8 @@ describe("DefaultForm Action-Footer", () => {
|
|
|
24
24
|
const actions = screen.getByTestId("f-actions");
|
|
25
25
|
expect(actions.className).toContain("justify-end");
|
|
26
26
|
expect(actions.className).toContain("border-t");
|
|
27
|
-
// Footer
|
|
28
|
-
expect(actions.closest(".max-w-
|
|
27
|
+
// Footer sits in the form shell (buttons align with the field edge).
|
|
28
|
+
expect(actions.closest(".max-w-full")).toBeTruthy();
|
|
29
29
|
// Titel ist ein eigenes Heading oben, NICHT mehr in der Action-Bar.
|
|
30
30
|
expect(actions.textContent).not.toContain("Titel");
|
|
31
31
|
expect(screen.getByTestId("f-title").textContent).toBe("Titel");
|
|
@@ -1570,8 +1570,8 @@ describe("KumikoScreen", () => {
|
|
|
1570
1570
|
expect(writeCalls[0]?.payload).toEqual({ title: "New Task", priority: 1 });
|
|
1571
1571
|
});
|
|
1572
1572
|
|
|
1573
|
-
test("actionForm mit redirect: nach success → nav.navigate({screenId: redirect})", async () => {
|
|
1574
|
-
const navigateCalls:
|
|
1573
|
+
test("actionForm mit redirect: nach success → nav.navigate({screenId: redirect, entityId})", async () => {
|
|
1574
|
+
const navigateCalls: NavTarget[] = [];
|
|
1575
1575
|
const dispatcher = makeDispatcher({
|
|
1576
1576
|
write: (async () => ({
|
|
1577
1577
|
isSuccess: true,
|
|
@@ -1613,11 +1613,13 @@ describe("KumikoScreen", () => {
|
|
|
1613
1613
|
fireEvent.change(titleInput, { target: { value: "go" } });
|
|
1614
1614
|
fireEvent.click(screen.getByTestId("render-edit-submit"));
|
|
1615
1615
|
await waitFor(() => expect(navigateCalls.length).toBe(1));
|
|
1616
|
-
|
|
1616
|
+
// #2416: the handler returns a created id (`{ id: "x" }`) — it must land
|
|
1617
|
+
// on the navigate as entityId, same as entityEdit-create.
|
|
1618
|
+
expect(navigateCalls[0]).toEqual({ screenId: "task-list", entityId: "x" });
|
|
1617
1619
|
});
|
|
1618
1620
|
|
|
1619
1621
|
test("actionForm mit Cross-Feature-QN als redirect: navigiert per Short-Id (#1946)", async () => {
|
|
1620
|
-
const navigateCalls:
|
|
1622
|
+
const navigateCalls: NavTarget[] = [];
|
|
1621
1623
|
const dispatcher = makeDispatcher({
|
|
1622
1624
|
write: (async () => ({
|
|
1623
1625
|
isSuccess: true,
|
|
@@ -1659,7 +1661,59 @@ describe("KumikoScreen", () => {
|
|
|
1659
1661
|
fireEvent.change(titleInput, { target: { value: "go" } });
|
|
1660
1662
|
fireEvent.click(screen.getByTestId("render-edit-submit"));
|
|
1661
1663
|
await waitFor(() => expect(navigateCalls.length).toBe(1));
|
|
1662
|
-
expect(navigateCalls[0]).toEqual({ screenId: "statement-upload-list" });
|
|
1664
|
+
expect(navigateCalls[0]).toEqual({ screenId: "statement-upload-list", entityId: "x" });
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// #2416: entityEdit-create passes the newly created record's id through to
|
|
1668
|
+
// the post-submit navigate as `entityId` (extractCreatedId); actionForm's
|
|
1669
|
+
// redirect did the same navigate but never extracted the id. The with-id
|
|
1670
|
+
// case is covered by the actionForm redirect success test above (now
|
|
1671
|
+
// asserting entityId); this pins the back-compat guarantee.
|
|
1672
|
+
test("actionForm mit redirect: Handler-Result ohne id → navigate ohne entityId-Key (Rückwärtskompatibilität, #2416)", async () => {
|
|
1673
|
+
const navigateCalls: NavTarget[] = [];
|
|
1674
|
+
const dispatcher = makeDispatcher({
|
|
1675
|
+
write: (async () => ({
|
|
1676
|
+
isSuccess: true,
|
|
1677
|
+
data: {},
|
|
1678
|
+
})) as unknown as Dispatcher["write"],
|
|
1679
|
+
});
|
|
1680
|
+
const memoryNav = {
|
|
1681
|
+
route: { screenId: "quick-add" },
|
|
1682
|
+
navigate: (target: NavTarget) => {
|
|
1683
|
+
if ("screenId" in target) navigateCalls.push(target);
|
|
1684
|
+
},
|
|
1685
|
+
replace: () => undefined,
|
|
1686
|
+
hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
|
|
1687
|
+
searchParams: {},
|
|
1688
|
+
setSearchParams: () => undefined,
|
|
1689
|
+
};
|
|
1690
|
+
const actionScreen: ActionFormScreenDefinition = {
|
|
1691
|
+
id: "quick-add",
|
|
1692
|
+
type: "actionForm",
|
|
1693
|
+
handler: "tasks:write:task:quick-add",
|
|
1694
|
+
fields: { title: { type: "text", required: true } },
|
|
1695
|
+
layout: { sections: [{ title: "x", fields: ["title"] }] },
|
|
1696
|
+
redirect: "task-list",
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
|
|
1700
|
+
render(
|
|
1701
|
+
<NavProvider value={memoryNav}>
|
|
1702
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
1703
|
+
<KumikoScreen
|
|
1704
|
+
schema={{ ...schema, screens: [actionScreen, listScreen] }}
|
|
1705
|
+
qn="tasks:screen:quick-add"
|
|
1706
|
+
/>
|
|
1707
|
+
</DispatcherProvider>
|
|
1708
|
+
</NavProvider>,
|
|
1709
|
+
);
|
|
1710
|
+
|
|
1711
|
+
const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
|
|
1712
|
+
fireEvent.change(titleInput, { target: { value: "go" } });
|
|
1713
|
+
fireEvent.click(screen.getByTestId("render-edit-submit"));
|
|
1714
|
+
await waitFor(() => expect(navigateCalls.length).toBe(1));
|
|
1715
|
+
expect(navigateCalls[0]).toEqual({ screenId: "task-list" });
|
|
1716
|
+
expect(Object.hasOwn(navigateCalls[0] ?? {}, "entityId")).toBe(false);
|
|
1663
1717
|
});
|
|
1664
1718
|
|
|
1665
1719
|
// cancelTarget (Bug-Bash 2026-06-07, Bug 9): redirect erzeugte
|
|
@@ -283,7 +283,7 @@ describe("RenderEdit", () => {
|
|
|
283
283
|
expect(seenResults[0]?.isSuccess).toBe(true);
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
-
test("layout.width defaults the form shell to max-w-
|
|
286
|
+
test("layout.width defaults the form shell to max-w-full when unset", () => {
|
|
287
287
|
render(
|
|
288
288
|
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
289
289
|
<RenderEdit<TestValues>
|
|
@@ -297,16 +297,16 @@ describe("RenderEdit", () => {
|
|
|
297
297
|
);
|
|
298
298
|
|
|
299
299
|
const shell = screen.getByTestId("render-edit-form").firstElementChild;
|
|
300
|
-
expect(shell?.className).toContain("max-w-
|
|
301
|
-
expect(shell?.className).not.toContain("max-w-
|
|
300
|
+
expect(shell?.className).toContain("max-w-full");
|
|
301
|
+
expect(shell?.className).not.toContain("max-w-3xl");
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
-
test("layout.width: '
|
|
304
|
+
test("layout.width: '3xl' narrows the form shell when set", () => {
|
|
305
305
|
const screenDef = makeScreen();
|
|
306
306
|
render(
|
|
307
307
|
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
308
308
|
<RenderEdit<TestValues>
|
|
309
|
-
screen={{ ...screenDef, layout: { ...screenDef.layout, width: "
|
|
309
|
+
screen={{ ...screenDef, layout: { ...screenDef.layout, width: "3xl" } }}
|
|
310
310
|
entity={orderEntity}
|
|
311
311
|
featureName="orders"
|
|
312
312
|
initial={{ title: "", count: 0, isUrgent: false }}
|
|
@@ -316,8 +316,8 @@ describe("RenderEdit", () => {
|
|
|
316
316
|
);
|
|
317
317
|
|
|
318
318
|
const shell = screen.getByTestId("render-edit-form").firstElementChild;
|
|
319
|
-
expect(shell?.className).toContain("max-w-
|
|
320
|
-
expect(shell?.className).not.toContain("max-w-
|
|
319
|
+
expect(shell?.className).toContain("max-w-3xl");
|
|
320
|
+
expect(shell?.className).not.toContain("max-w-full");
|
|
321
321
|
});
|
|
322
322
|
|
|
323
323
|
test("title resolved aus i18n-Key `screen:<id>.title` mit screenId als Fallback", () => {
|
package/src/primitives/index.tsx
CHANGED
|
@@ -1620,13 +1620,9 @@ function DefaultForm({
|
|
|
1620
1620
|
);
|
|
1621
1621
|
}
|
|
1622
1622
|
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
1625
|
-
//
|
|
1626
|
-
// bg-muted/30 als Farb-Trenner statt harter Linie. main hat kein Padding;
|
|
1627
|
-
// der max-w-3xl-Body rahmt die Card, mx-auto zentriert ihn im Main statt
|
|
1628
|
-
// linksbündig (sonst „eingesperrt" mit leerer rechter Hälfte auf breiten
|
|
1629
|
-
// Screens).
|
|
1623
|
+
// One card form: title as header (no divider under it), sections divided
|
|
1624
|
+
// between each other, muted action footer. Shell width defaults to full
|
|
1625
|
+
// (same chrome as lists); pass width to narrow (auth-adjacent / dense).
|
|
1630
1626
|
return (
|
|
1631
1627
|
<form
|
|
1632
1628
|
onSubmit={(e) => {
|
|
@@ -1698,13 +1694,10 @@ function DefaultForm({
|
|
|
1698
1694
|
);
|
|
1699
1695
|
}
|
|
1700
1696
|
|
|
1701
|
-
//
|
|
1702
|
-
//
|
|
1703
|
-
// (
|
|
1704
|
-
//
|
|
1705
|
-
// beliebiger max-w-*-Overrides: sm=schmale Auth-Forms, 3xl=Standard-Detail,
|
|
1706
|
-
// 4xl=tabellen-nahe Forms, full=volle Breite. Inhalt nutzt Card-Primitives;
|
|
1707
|
-
// `className` (z.B. "flex flex-col gap-6") für Multi-Card-Stacks.
|
|
1697
|
+
// Canonical form/settings shell shared by DefaultForm (configEdit/entityEdit)
|
|
1698
|
+
// and custom settings screens (profile, privacy-center). Default width matches
|
|
1699
|
+
// list chrome (full); override via maxWidth / layout.width when a screen
|
|
1700
|
+
// truly needs a narrow column (sm=auth-adjacent, 3xl/4xl=centered detail).
|
|
1708
1701
|
export type FormScreenShellWidth = FormWidth;
|
|
1709
1702
|
|
|
1710
1703
|
const formScreenShellWidth: Record<FormScreenShellWidth, string> = {
|
|
@@ -1718,7 +1711,7 @@ export function FormScreenShell({
|
|
|
1718
1711
|
children,
|
|
1719
1712
|
className,
|
|
1720
1713
|
testId,
|
|
1721
|
-
maxWidth = "
|
|
1714
|
+
maxWidth = "full",
|
|
1722
1715
|
}: {
|
|
1723
1716
|
readonly children: ReactNode;
|
|
1724
1717
|
readonly className?: string;
|