@cosmicdrift/kumiko-renderer-web 0.250.0 → 0.252.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.250.0",
3
+ "version": "0.252.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.250.0",
20
- "@cosmicdrift/kumiko-headless": "0.250.0",
21
- "@cosmicdrift/kumiko-renderer": "0.250.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.252.0",
20
+ "@cosmicdrift/kumiko-headless": "0.252.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.252.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.250.0"
67
+ "@cosmicdrift/kumiko-locale-de": "0.252.0"
68
68
  },
69
69
  "repository": {
70
70
  "type": "git",
@@ -1516,10 +1516,15 @@ describe("Form", () => {
1516
1516
  expect(contentContainer.className).not.toContain("max-sm:pb-32");
1517
1517
  });
1518
1518
 
1519
- // fillHeight (fw#2722): the flex-fill chain RenderEdit opts a lone
1520
- // relatedList tab into, so its table can scroll inside the tab panel
1521
- // instead of the whole page stretching to the row count.
1522
- test("fillHeight: form root and its content container size to h-full/flex-1 min-h-0", () => {
1519
+ // fillHeight (fw#2722, height fw#2778): the flex chain RenderEdit opts a
1520
+ // lone relatedList tab into, so its table can scroll inside the tab panel
1521
+ // instead of the whole page stretching to the row count. The chain caps
1522
+ // at h-full but does NOT flex-1/grow past its content — a short table
1523
+ // must not stretch the card to the bottom of the panel (fw#2778); only
1524
+ // the innermost table wrapper (primitives.test.tsx's DataTable coverage)
1525
+ // keeps flex-1 to actually claim the leftover height once the ancestor
1526
+ // chain is force-shrunk.
1527
+ test("fillHeight: form root caps at h-full/min-h-0, its card sizes to content instead of growing (fw#2778)", () => {
1523
1528
  render(
1524
1529
  <Form onSubmit={() => undefined} testId="form" fillHeight>
1525
1530
  <div>content</div>
@@ -1528,10 +1533,11 @@ describe("Form", () => {
1528
1533
  const form = screen.getByTestId("form");
1529
1534
  expect(form.className).toContain("h-full");
1530
1535
  expect(form.className).toContain("min-h-0");
1531
- // form > FormScreenShell > card(overflow-hidden) — the card is the
1532
- // flex-1 min-h-0 child that claims the remaining height below headerRegion.
1536
+ // form > FormScreenShell > card(overflow-hidden) — the card sizes to
1537
+ // its content (no flex-1) and only shrinks (min-h-0) once the chain
1538
+ // above it is itself height-constrained.
1533
1539
  const card = form.firstElementChild?.firstElementChild as HTMLElement;
1534
- expect(card.className).toContain("flex-1");
1540
+ expect(card.className).not.toContain("flex-1");
1535
1541
  expect(card.className).toContain("min-h-0");
1536
1542
  });
1537
1543
 
@@ -1832,6 +1838,16 @@ describe("Stack", () => {
1832
1838
  });
1833
1839
  });
1834
1840
 
1841
+ // Padding utilities of one element, sorted — every screen container must
1842
+ // resolve to the same set (fw#2640).
1843
+ const paddingClasses = (el: Element | null | undefined): string[] =>
1844
+ (el?.className ?? "")
1845
+ .split(" ")
1846
+ .filter((c) => /^p[xytblr]?-/.test(c))
1847
+ .sort();
1848
+
1849
+ const paddingOf = (testId: string): string[] => paddingClasses(screen.getByTestId(testId));
1850
+
1835
1851
  describe("PageSection", () => {
1836
1852
  test("wrappt children mit einheitlichem Padding", () => {
1837
1853
  render(
@@ -1850,12 +1866,6 @@ describe("PageSection", () => {
1850
1866
  <FormScreenShell testId="shell-pad">x</FormScreenShell>
1851
1867
  </>,
1852
1868
  );
1853
- const paddingOf = (testId: string): string[] =>
1854
- screen
1855
- .getByTestId(testId)
1856
- .className.split(" ")
1857
- .filter((c) => /^p[xytblr]?-/.test(c))
1858
- .sort();
1859
1869
  expect(paddingOf("page-pad")).toEqual(["pb-12", "pt-6", "px-6"]);
1860
1870
  expect(paddingOf("page-pad")).toEqual(paddingOf("shell-pad"));
1861
1871
  });
@@ -1881,6 +1891,35 @@ describe("PageSection", () => {
1881
1891
  });
1882
1892
  });
1883
1893
 
1894
+ // A list screen has no PageSection/FormScreenShell around it — its screen
1895
+ // chrome is DataTable's own outer wrapper, which is why that wrapper had its
1896
+ // own inset and list screens ended 24px above the viewport edge instead of
1897
+ // 48px (fw#2640).
1898
+ describe("DataTable screenPadding", () => {
1899
+ const cols = [{ field: "name", label: "Name", type: "string", sortable: false }] as const;
1900
+ const oneRow = [{ id: "r1", values: { name: "Alice" } }];
1901
+ // table → [data-slot="table-container"] → card frame → padding wrapper.
1902
+ const paddingWrapperOf = (testId: string): Element | null | undefined =>
1903
+ screen.getByTestId(testId).parentElement?.parentElement?.parentElement;
1904
+
1905
+ test("screenPadding: the table's screen chrome matches PageSection and FormScreenShell", () => {
1906
+ render(
1907
+ <>
1908
+ <PageSection testId="page-pad">x</PageSection>
1909
+ <FormScreenShell testId="shell-pad">x</FormScreenShell>
1910
+ <DataTable columns={cols} rows={oneRow} testId="list-table" screenPadding />
1911
+ </>,
1912
+ );
1913
+ expect(paddingClasses(paddingWrapperOf("list-table"))).toEqual(paddingOf("page-pad"));
1914
+ expect(paddingClasses(paddingWrapperOf("list-table"))).toEqual(paddingOf("shell-pad"));
1915
+ });
1916
+
1917
+ test("default: an embedded table keeps its symmetric inset (relatedList unchanged)", () => {
1918
+ render(<DataTable columns={cols} rows={oneRow} testId="embedded-table" />);
1919
+ expect(paddingClasses(paddingWrapperOf("embedded-table"))).toEqual(["p-6"]);
1920
+ });
1921
+ });
1922
+
1884
1923
  describe("JsonView", () => {
1885
1924
  test("tokenisiert statt einen flachen String zu rendern — Keys/Strings/Zahlen/Booleans/null bekommen je eigene Klasse", () => {
1886
1925
  render(
@@ -904,6 +904,7 @@ function DefaultDataTable({
904
904
  getCellTestId,
905
905
  chromeless,
906
906
  scrollBody,
907
+ screenPadding,
907
908
  }: DataTableProps): ReactNode {
908
909
  // One locale/translate subscription per table — not per cell (fw#2345).
909
910
  // Optional hooks: a bare DataTable outside LocaleProvider must not crash.
@@ -937,13 +938,16 @@ function DefaultDataTable({
937
938
  // sitzt auf derselben Card-Fläche wie Forms; auf Themes mit farbigem
938
939
  // Page-Background (z.B. Cream) matchen Listen sonst nicht die Cards.
939
940
  // `chromeless` drops that frame for a host with its own boundary already (a tab panel, fw#2722).
940
- // `scrollBody` fills the remaining height of its flex-col ancestor
941
- // (see DefaultForm/FormScreenShell's `fillHeight` and this table's own
942
- // outer wrapper below) and scrolls rows internally, instead of
943
- // `overflow-hidden` (document-flow height, grows with row count) for
944
- // a host where a long list must not stretch the whole page (a tab
945
- // panel, fw#2722). `min-h-0` overrides flex's default min-height:auto,
946
- // which would otherwise let this grow past its flex-1 share to fit content.
941
+ // `scrollBody` is the terminal link in the DefaultForm/FormScreenShell
942
+ // `fillHeight` chain (fw#2722) every ancestor in that chain sizes to
943
+ // its own content (no flex-1) and only shrinks once the chain's
944
+ // `h-full` root runs out of room, so THIS is the one div that still
945
+ // claims the leftover height (`flex-1`) and scrolls rows internally
946
+ // instead of `overflow-hidden` (document-flow height, grows with row
947
+ // count) a short list keeps a content-sized card, a long one scrolls
948
+ // in place instead of stretching the whole page (fw#2722, fw#2778).
949
+ // `min-h-0` overrides flex's default min-height:auto, which would
950
+ // otherwise keep this at its content's height regardless of flex-shrink.
947
951
  <div
948
952
  className={cn(
949
953
  scrollBody === true ? "flex-1 min-h-0 overflow-y-auto" : "overflow-hidden",
@@ -1256,11 +1260,25 @@ function DefaultDataTable({
1256
1260
  // der Tabelle im selben Padding-Block — kein separater bg-Bar, kein Screen-
1257
1261
  // Titel (der steht im Breadcrumb der Shell).
1258
1262
  return (
1259
- <div className={cn("flex flex-col gap-4 p-6 w-full", scrollBody === true && "flex-1 min-h-0")}>
1263
+ // `screenPadding`: on a list screen this wrapper IS the screen container,
1264
+ // so it takes the same token as FormScreenShell/PageSection instead of its
1265
+ // own inset — a list ends at the same footer distance as a form (fw#2640).
1266
+ <div
1267
+ className={cn(
1268
+ "flex flex-col gap-4 w-full",
1269
+ screenPadding === true ? screenPaddingClassName : "p-6",
1270
+ // No flex-1: this wrapper sizes to its content (toolbar + table) and
1271
+ // only shrinks (min-h-0) once its ancestor chain is itself
1272
+ // height-constrained — the actual scroll surface is tableInner/
1273
+ // cardsInner below, which do keep flex-1 to claim whatever height
1274
+ // that shrink leaves them (fw#2778).
1275
+ scrollBody === true && "min-h-0",
1276
+ )}
1277
+ >
1260
1278
  {hasToolbar && (
1261
1279
  <div
1262
1280
  data-testid={testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar"}
1263
- className="flex flex-wrap items-center gap-3"
1281
+ className={cn("flex flex-wrap items-center gap-3", scrollBody === true && "shrink-0")}
1264
1282
  >
1265
1283
  {toolbarStart !== undefined && <div className="flex-1 max-w-sm">{toolbarStart}</div>}
1266
1284
  {facetCluster}
@@ -2193,17 +2211,27 @@ function DefaultForm({
2193
2211
  {...(fillHeight === true && { fillHeight: true })}
2194
2212
  >
2195
2213
  {headerRegion !== undefined && (
2196
- <div className="flex flex-col gap-6 mb-8">{headerRegion}</div>
2214
+ <div className={cn("flex flex-col gap-6 mb-8", fillHeight === true && "shrink-0")}>
2215
+ {headerRegion}
2216
+ </div>
2197
2217
  )}
2198
2218
  <div
2199
2219
  className={cn(
2200
2220
  cardSurface(),
2201
2221
  "overflow-hidden",
2202
- fillHeight === true && "flex-1 min-h-0 flex flex-col",
2222
+ // No flex-1: a fillHeight card sizes to its content (flex's
2223
+ // initial 0 1 auto) and only claims more than that once its
2224
+ // FormScreenShell ancestor is itself height-constrained and
2225
+ // shrinks it back down via min-h-0 — flex-1 would instead force
2226
+ // it to always fill the remaining height, stretching a short
2227
+ // relatedList tab to the bottom of the panel (fw#2778).
2228
+ fillHeight === true && "min-h-0 flex flex-col",
2203
2229
  )}
2204
2230
  >
2205
2231
  {(title !== undefined || subtitle !== undefined) && (
2206
- <div className={cn(cardHeaderBorder, "px-6 pb-4 pt-5")}>
2232
+ <div
2233
+ className={cn(cardHeaderBorder, "px-6 pb-4 pt-5", fillHeight === true && "shrink-0")}
2234
+ >
2207
2235
  {title !== undefined && (
2208
2236
  <h2
2209
2237
  data-testid={testId !== undefined ? `${testId}-title` : undefined}
@@ -2235,7 +2263,10 @@ function DefaultForm({
2235
2263
  // safe-area, fw#2528) — widen further if a wizard step's last field
2236
2264
  // ever renders visibly clipped under three or more wrapped rows.
2237
2265
  stickyActions === true && "max-sm:pb-32",
2238
- fillHeight === true && "flex-1 min-h-0",
2266
+ // Same "no flex-1" reasoning as the card above: this is the
2267
+ // one section allowed to shrink (min-h-0) inside the card, not
2268
+ // one forced to grow past its content.
2269
+ fillHeight === true && "min-h-0",
2239
2270
  )}
2240
2271
  >
2241
2272
  <InsideFormContext.Provider value={true}>{children}</InsideFormContext.Provider>
@@ -2245,6 +2276,7 @@ function DefaultForm({
2245
2276
  className={cn(
2246
2277
  "flex flex-col-reverse gap-3 px-[var(--card-padding)] py-3 sm:flex-row sm:items-center sm:justify-between sm:py-4",
2247
2278
  cardFooterBorder,
2279
+ fillHeight === true && "shrink-0",
2248
2280
  // Below sm (640px): pin to the viewport bottom instead of normal
2249
2281
  // flow, so a virtual keyboard shrinking the viewport can't push
2250
2282
  // this out of reach (fw#1918). `fixed` escapes the card's
@@ -2422,8 +2454,12 @@ function DefaultSection({
2422
2454
  }
2423
2455
 
2424
2456
  function DefaultFillContainer({ children, testId }: FillContainerProps): ReactNode {
2457
+ // No flex-1: sizes to its content (the relatedList table) and only
2458
+ // shrinks (min-h-0) once the ancestor FormScreenShell chain is itself
2459
+ // height-constrained — flex-1 would instead always stretch to fill the
2460
+ // remaining tab-panel height, even for a two-row table (fw#2778).
2425
2461
  return (
2426
- <div data-testid={testId} className="flex flex-1 min-h-0 flex-col">
2462
+ <div data-testid={testId} className="flex min-h-0 flex-col">
2427
2463
  {children}
2428
2464
  </div>
2429
2465
  );