@cosmicdrift/kumiko-renderer-web 0.246.0 → 0.247.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.246.0",
3
+ "version": "0.247.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.246.0",
20
- "@cosmicdrift/kumiko-headless": "0.246.0",
21
- "@cosmicdrift/kumiko-renderer": "0.246.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.247.0",
20
+ "@cosmicdrift/kumiko-headless": "0.247.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.247.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.246.0"
67
+ "@cosmicdrift/kumiko-locale-de": "0.247.0"
68
68
  },
69
69
  "repository": {
70
70
  "type": "git",
@@ -465,6 +465,43 @@ describe("DataTable", () => {
465
465
  });
466
466
  });
467
467
 
468
+ // scrollBody (fw#2722): a relatedList section in a tabs-mode Akte must not
469
+ // stretch the whole page with a long list, or leave dead space below a
470
+ // short one — bounds the table to a fixed height and scrolls rows inside
471
+ // it, regardless of how many rows there are.
472
+ describe("scrollBody", () => {
473
+ const cols = [{ field: "name", label: "Name", type: "string", sortable: false }] as const;
474
+ const shortRows = [{ id: "r1", values: { name: "Alice" } }];
475
+ const longRows = Array.from({ length: 50 }, (_, i) => ({
476
+ id: `r${i}`,
477
+ values: { name: `Row ${i}` },
478
+ }));
479
+
480
+ test("default: table has no fixed height and grows with its content", () => {
481
+ render(<DataTable columns={cols} rows={longRows} testId="t" />);
482
+ const wrapper = screen.getByTestId("t").parentElement?.parentElement;
483
+ expect(wrapper?.className).not.toContain("overflow-y-auto");
484
+ expect(wrapper?.className).toContain("overflow-hidden");
485
+ });
486
+
487
+ test("scrollBody: a long list fills its flex container and scrolls internally", () => {
488
+ render(<DataTable columns={cols} rows={longRows} testId="t" scrollBody />);
489
+ const wrapper = screen.getByTestId("t").parentElement?.parentElement;
490
+ expect(wrapper?.className).toContain("overflow-y-auto");
491
+ expect(wrapper?.className).toContain("flex-1");
492
+ expect(wrapper?.className).toContain("min-h-0");
493
+ expect(screen.getAllByTestId(/^row-/)).toHaveLength(50);
494
+ });
495
+
496
+ test("scrollBody: a short list gets the same flex-fill treatment (fills space instead of shrinking)", () => {
497
+ render(<DataTable columns={cols} rows={shortRows} testId="t" scrollBody />);
498
+ const wrapper = screen.getByTestId("t").parentElement?.parentElement;
499
+ expect(wrapper?.className).toContain("overflow-y-auto");
500
+ expect(wrapper?.className).toContain("flex-1");
501
+ expect(wrapper?.className).toContain("min-h-0");
502
+ });
503
+ });
504
+
468
505
  // Sort-Header pinnt das 3-State-Toggle-Verhalten + Visual-Indicator
469
506
  // + aria-sort. Renderer-Vertrag mit dem Caller (RenderList): jede
470
507
  // sortable-Column liefert beim Click den nächsten Sort-State zurück
@@ -1478,6 +1515,37 @@ describe("Form", () => {
1478
1515
  const contentContainer = footer.previousElementSibling as HTMLElement;
1479
1516
  expect(contentContainer.className).not.toContain("max-sm:pb-32");
1480
1517
  });
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", () => {
1523
+ render(
1524
+ <Form onSubmit={() => undefined} testId="form" fillHeight>
1525
+ <div>content</div>
1526
+ </Form>,
1527
+ );
1528
+ const form = screen.getByTestId("form");
1529
+ expect(form.className).toContain("h-full");
1530
+ 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.
1533
+ const card = form.firstElementChild?.firstElementChild as HTMLElement;
1534
+ expect(card.className).toContain("flex-1");
1535
+ expect(card.className).toContain("min-h-0");
1536
+ });
1537
+
1538
+ test("without fillHeight: form root keeps its normal, content-sized height", () => {
1539
+ render(
1540
+ <Form onSubmit={() => undefined} testId="form">
1541
+ <div>content</div>
1542
+ </Form>,
1543
+ );
1544
+ const form = screen.getByTestId("form");
1545
+ expect(form.className).not.toContain("h-full");
1546
+ const card = form.firstElementChild?.firstElementChild as HTMLElement;
1547
+ expect(card.className).not.toContain("flex-1");
1548
+ });
1481
1549
  });
1482
1550
 
1483
1551
  describe("Banner padded", () => {
@@ -25,6 +25,7 @@ import {
25
25
  type DataTableFacet,
26
26
  type DataTableProps,
27
27
  type FieldProps,
28
+ type FillContainerProps,
28
29
  type FormProps,
29
30
  type FormWidth,
30
31
  type GridCellProps,
@@ -904,6 +905,7 @@ function DefaultDataTable({
904
905
  getRowTestId,
905
906
  getCellTestId,
906
907
  chromeless,
908
+ scrollBody,
907
909
  }: DataTableProps): ReactNode {
908
910
  // One locale/translate subscription per table — not per cell (fw#2345).
909
911
  // Optional hooks: a bare DataTable outside LocaleProvider must not crash.
@@ -937,7 +939,19 @@ function DefaultDataTable({
937
939
  // sitzt auf derselben Card-Fläche wie Forms; auf Themes mit farbigem
938
940
  // Page-Background (z.B. Cream) matchen Listen sonst nicht die Cards.
939
941
  // `chromeless` drops that frame for a host with its own boundary already (a tab panel, fw#2722).
940
- <div className={cn("overflow-hidden", chromeless !== true && "rounded-lg border bg-card")}>
942
+ // `scrollBody` fills the remaining height of its flex-col ancestor
943
+ // (see DefaultForm/FormScreenShell's `fillHeight` and this table's own
944
+ // outer wrapper below) and scrolls rows internally, instead of
945
+ // `overflow-hidden` (document-flow height, grows with row count) — for
946
+ // a host where a long list must not stretch the whole page (a tab
947
+ // panel, fw#2722). `min-h-0` overrides flex's default min-height:auto,
948
+ // which would otherwise let this grow past its flex-1 share to fit content.
949
+ <div
950
+ className={cn(
951
+ scrollBody === true ? "flex-1 min-h-0 overflow-y-auto" : "overflow-hidden",
952
+ chromeless !== true && "rounded-lg border bg-card",
953
+ )}
954
+ >
941
955
  <Table data-testid={testId}>
942
956
  <TableHeader className="bg-muted">
943
957
  <TableRow className="hover:bg-transparent">
@@ -1139,7 +1153,10 @@ function DefaultDataTable({
1139
1153
  return (
1140
1154
  <div
1141
1155
  data-testid={testId !== undefined ? `${testId}-cards` : "render-list-cards"}
1142
- className="flex flex-col gap-3"
1156
+ className={cn(
1157
+ "flex flex-col gap-3",
1158
+ scrollBody === true && "flex-1 min-h-0 overflow-y-auto",
1159
+ )}
1143
1160
  >
1144
1161
  {onSortChange !== undefined && sortableColumns.length > 0 && (
1145
1162
  <select
@@ -1241,7 +1258,7 @@ function DefaultDataTable({
1241
1258
  // der Tabelle im selben Padding-Block — kein separater bg-Bar, kein Screen-
1242
1259
  // Titel (der steht im Breadcrumb der Shell).
1243
1260
  return (
1244
- <div className="flex flex-col gap-4 p-6 w-full">
1261
+ <div className={cn("flex flex-col gap-4 p-6 w-full", scrollBody === true && "flex-1 min-h-0")}>
1245
1262
  {hasToolbar && (
1246
1263
  <div
1247
1264
  data-testid={testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar"}
@@ -2134,6 +2151,7 @@ function DefaultForm({
2134
2151
  width,
2135
2152
  stickyActions,
2136
2153
  headerRegion,
2154
+ fillHeight,
2137
2155
  }: FormProps): ReactNode {
2138
2156
  // Eingebettet (AuthCard etc.): nacktes <form>, gestapelte Felder mit gap —
2139
2157
  // der Container trägt Card/Titel selbst, sonst Card-in-Card.
@@ -2164,12 +2182,25 @@ function DefaultForm({
2164
2182
  // between each other, muted action footer. Shell width defaults to full
2165
2183
  // (same chrome as lists); pass width to narrow (auth-adjacent / dense).
2166
2184
  return (
2167
- <FormRoot onSubmit={onSubmit} testId={testId} className="flex flex-col w-full">
2168
- <FormScreenShell {...(width !== undefined && { maxWidth: width })}>
2185
+ <FormRoot
2186
+ onSubmit={onSubmit}
2187
+ testId={testId}
2188
+ className={cn("flex flex-col w-full", fillHeight === true && "h-full min-h-0")}
2189
+ >
2190
+ <FormScreenShell
2191
+ {...(width !== undefined && { maxWidth: width })}
2192
+ {...(fillHeight === true && { fillHeight: true })}
2193
+ >
2169
2194
  {headerRegion !== undefined && (
2170
2195
  <div className="flex flex-col gap-6 mb-8">{headerRegion}</div>
2171
2196
  )}
2172
- <div className={cn(cardSurface(), "overflow-hidden")}>
2197
+ <div
2198
+ className={cn(
2199
+ cardSurface(),
2200
+ "overflow-hidden",
2201
+ fillHeight === true && "flex-1 min-h-0 flex flex-col",
2202
+ )}
2203
+ >
2173
2204
  {(title !== undefined || subtitle !== undefined) && (
2174
2205
  <div className={cn(cardHeaderBorder, "px-6 pb-4 pt-5")}>
2175
2206
  {title !== undefined && (
@@ -2203,6 +2234,7 @@ function DefaultForm({
2203
2234
  // safe-area, fw#2528) — widen further if a wizard step's last field
2204
2235
  // ever renders visibly clipped under three or more wrapped rows.
2205
2236
  stickyActions === true && "max-sm:pb-32",
2237
+ fillHeight === true && "flex-1 min-h-0",
2206
2238
  )}
2207
2239
  >
2208
2240
  <InsideFormContext.Provider value={true}>{children}</InsideFormContext.Provider>
@@ -2257,18 +2289,30 @@ export function FormScreenShell({
2257
2289
  className,
2258
2290
  testId,
2259
2291
  maxWidth,
2292
+ fillHeight,
2260
2293
  }: {
2261
2294
  readonly children: ReactNode;
2262
2295
  readonly className?: string;
2263
2296
  readonly testId?: string;
2264
2297
  readonly maxWidth?: FormScreenShellWidth;
2298
+ /** Stacks children in a `h-full` flex column instead of normal document
2299
+ * flow, so a `flex-1 min-h-0` child can claim the remaining height below
2300
+ * the others (fw#2722 — DefaultForm's tabs+relatedList case). Default
2301
+ * false: unchanged, content-sized height. */
2302
+ readonly fillHeight?: boolean;
2265
2303
  }): ReactNode {
2266
2304
  const contextWidth = useContext(ScreenWidthContext);
2267
2305
  const width = maxWidth ?? contextWidth;
2268
2306
  return (
2269
2307
  <div
2270
2308
  data-testid={testId}
2271
- className={cn(screenPaddingClassName, "w-full", screenWidthClassName[width], className)}
2309
+ className={cn(
2310
+ screenPaddingClassName,
2311
+ "w-full",
2312
+ screenWidthClassName[width],
2313
+ fillHeight === true && "h-full flex flex-col min-h-0",
2314
+ className,
2315
+ )}
2272
2316
  >
2273
2317
  {children}
2274
2318
  </div>
@@ -2376,6 +2420,14 @@ function DefaultSection({
2376
2420
  );
2377
2421
  }
2378
2422
 
2423
+ function DefaultFillContainer({ children, testId }: FillContainerProps): ReactNode {
2424
+ return (
2425
+ <div data-testid={testId} className="flex flex-1 min-h-0 flex-col">
2426
+ {children}
2427
+ </div>
2428
+ );
2429
+ }
2430
+
2379
2431
  function DefaultGrid({ columns, children, testId, maxRows }: GridProps): ReactNode {
2380
2432
  // "auto": content-sized items in a wrapping row (e.g. a metrics band of
2381
2433
  // self-sized tiles) instead of N equal-width, container-stretched tracks.
@@ -2631,4 +2683,5 @@ export const defaultPrimitives: CorePrimitives = {
2631
2683
  StatusBadge: DefaultStatusBadge,
2632
2684
  Metric: DefaultMetric,
2633
2685
  JsonView: DefaultJsonView,
2686
+ FillContainer: DefaultFillContainer,
2634
2687
  };