@cosmicdrift/kumiko-renderer-web 0.250.0 → 0.251.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 +5 -5
- package/src/__tests__/primitives.test.tsx +52 -13
- package/src/primitives/index.tsx +50 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.251.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.251.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.251.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.251.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.251.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
|
|
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
|
-
|
|
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
|
|
1532
|
-
// flex-1 min-h-0
|
|
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(
|
package/src/primitives/index.tsx
CHANGED
|
@@ -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`
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
// `
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2462
|
+
<div data-testid={testId} className="flex min-h-0 flex-col">
|
|
2427
2463
|
{children}
|
|
2428
2464
|
</div>
|
|
2429
2465
|
);
|