@cosmicdrift/kumiko-renderer-web 0.219.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.219.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.219.0",
20
- "@cosmicdrift/kumiko-headless": "0.219.0",
21
- "@cosmicdrift/kumiko-renderer": "0.219.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.219.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 liegt im max-w-3xl-Form-Body (Buttons enden an der Feld-Linie).
28
- expect(actions.closest(".max-w-3xl")).toBeTruthy();
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: { screenId: string }[] = [];
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
- expect(navigateCalls[0]).toEqual({ screenId: "task-list" });
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: { screenId: string }[] = [];
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-3xl when unset", () => {
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-3xl");
301
- expect(shell?.className).not.toContain("max-w-full");
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: 'full' widens the form shell to max-w-full (#1676)", () => {
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: "full" } }}
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-full");
320
- expect(shell?.className).not.toContain("max-w-3xl");
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", () => {
@@ -1620,13 +1620,9 @@ function DefaultForm({
1620
1620
  );
1621
1621
  }
1622
1622
 
1623
- // shadcn-Form-Muster: das Formular ist EINE Card Titel als Card-Header
1624
- // OHNE Trennlinie darunter (Titel fließt in die erste Section). Sections
1625
- // sind divide-y-getrennt (Linien nur ZWISCHEN ihnen). Action-Footer mit
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
- // Kanonische Form/Settings-Shell: zentrierte Spalte mit Standard-Screen-
1702
- // Padding. DefaultForm (configEdit/entityEdit) UND custom Settings-Screens
1703
- // (url-settings, privacy-center) teilen sie einheitliche Breite +
1704
- // Zentrierung statt per-Screen-Wildwuchs. Breite über `maxWidth`-Intent statt
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 = "3xl",
1714
+ maxWidth = "full",
1722
1715
  }: {
1723
1716
  readonly children: ReactNode;
1724
1717
  readonly className?: string;