@cosmicdrift/kumiko-renderer-web 0.173.0 → 0.174.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.173.0",
3
+ "version": "0.174.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.173.0",
20
- "@cosmicdrift/kumiko-headless": "0.173.0",
21
- "@cosmicdrift/kumiko-renderer": "0.173.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.174.0",
20
+ "@cosmicdrift/kumiko-headless": "0.174.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.174.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",
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { describe, expect, mock, test } from "bun:test";
11
11
  import userEvent from "@testing-library/user-event";
12
- import { defaultPrimitives } from "../primitives";
12
+ import { defaultPrimitives, END_LABEL_MIN_ROWS } from "../primitives";
13
13
  import { PageSection, Stack } from "../primitives/layout";
14
14
  import { fireEvent, render, screen } from "./test-utils";
15
15
 
@@ -483,6 +483,8 @@ describe("DataTable", () => {
483
483
  id: `r${i}`,
484
484
  values: { name: `A${i}` },
485
485
  }));
486
+ const rowsOfCount = (n: number) =>
487
+ Array.from({ length: n }, (_, i) => ({ id: `r${i}`, values: { name: `A${i}` } }));
486
488
 
487
489
  test("ohne onReachEnd: kein Sentinel im DOM", () => {
488
490
  render(<DataTable columns={cols} rows={oneRow} testId="dt" />);
@@ -551,6 +553,34 @@ describe("DataTable", () => {
551
553
  expect(screen.getByTestId("dt-sentinel").textContent).toBe("");
552
554
  });
553
555
 
556
+ test(`hasMore=false + ${END_LABEL_MIN_ROWS - 1} Zeilen: kein End-Marker (Grenzfall unter Threshold)`, () => {
557
+ render(
558
+ <DataTable
559
+ columns={cols}
560
+ rows={rowsOfCount(END_LABEL_MIN_ROWS - 1)}
561
+ testId="dt"
562
+ onReachEnd={mock()}
563
+ loadingMore={false}
564
+ hasMore={false}
565
+ />,
566
+ );
567
+ expect(screen.queryByTestId("dt-sentinel-end")).toBeNull();
568
+ });
569
+
570
+ test(`hasMore=false + ${END_LABEL_MIN_ROWS} Zeilen: End-Marker (Grenzfall auf Threshold)`, () => {
571
+ render(
572
+ <DataTable
573
+ columns={cols}
574
+ rows={rowsOfCount(END_LABEL_MIN_ROWS)}
575
+ testId="dt"
576
+ onReachEnd={mock()}
577
+ loadingMore={false}
578
+ hasMore={false}
579
+ />,
580
+ );
581
+ expect(screen.getByTestId("dt-sentinel-end")).not.toBeNull();
582
+ });
583
+
554
584
  test("hasMore=false + de-Locale: Marker kommt aus i18n statt hartcodiert", async () => {
555
585
  const { LocaleProvider, createStaticLocaleResolver, kumikoDefaultTranslations } =
556
586
  await import("@cosmicdrift/kumiko-renderer");
@@ -178,4 +178,71 @@ describe("Reference-field create-in-place (#1681)", () => {
178
178
  expect(dispatcher.writes).toHaveLength(0);
179
179
  expect(screen.getByTestId("combobox-kumiko-edit-assignee").textContent).not.toContain("widget");
180
180
  });
181
+
182
+ // The three negative gates that decide whether the "+ Create" footer
183
+ // renders at all (render-field.tsx ReferenceInput.canCreate). A regression
184
+ // here silently shows a create button the server would reject.
185
+ describe("create-footer gates: no combobox-*-create in the DOM", () => {
186
+ test("allowCreate: false on the target screen", async () => {
187
+ const noCreateSchema: FeatureSchema = {
188
+ ...catalogSchema,
189
+ screens: [{ ...widgetCreateScreen, allowCreate: false }],
190
+ };
191
+ render(
192
+ <AppFeaturesProvider features={[tasksSchema, noCreateSchema]}>
193
+ <DispatcherProvider dispatcher={makeDispatcher()}>
194
+ <KumikoScreen schema={tasksSchema} qn="tasks:screen:task-edit" />
195
+ </DispatcherProvider>
196
+ </AppFeaturesProvider>,
197
+ );
198
+ await waitFor(() => screen.getByTestId("render-edit-form"));
199
+ await userEvent.setup().click(screen.getByTestId("combobox-kumiko-edit-assignee"));
200
+ expect(screen.queryByTestId("combobox-kumiko-edit-assignee-create")).toBeNull();
201
+ });
202
+
203
+ test("target screen gated by a role the user doesn't have (no UserRolesProvider mounted)", async () => {
204
+ const roleGatedSchema: FeatureSchema = {
205
+ ...catalogSchema,
206
+ screens: [{ ...widgetCreateScreen, access: { roles: ["Admin"] } }],
207
+ };
208
+ render(
209
+ <AppFeaturesProvider features={[tasksSchema, roleGatedSchema]}>
210
+ <DispatcherProvider dispatcher={makeDispatcher()}>
211
+ <KumikoScreen schema={tasksSchema} qn="tasks:screen:task-edit" />
212
+ </DispatcherProvider>
213
+ </AppFeaturesProvider>,
214
+ );
215
+ await waitFor(() => screen.getByTestId("render-edit-form"));
216
+ await userEvent.setup().click(screen.getByTestId("combobox-kumiko-edit-assignee"));
217
+ expect(screen.queryByTestId("combobox-kumiko-edit-assignee-create")).toBeNull();
218
+ });
219
+
220
+ test("field.readOnly on the reference field itself", async () => {
221
+ const readOnlySchema: FeatureSchema = {
222
+ ...tasksSchema,
223
+ screens: [
224
+ {
225
+ ...taskEditScreen,
226
+ layout: {
227
+ sections: [
228
+ {
229
+ title: "Basics",
230
+ fields: ["title", { field: "assignee", readOnly: true }, "tags"],
231
+ },
232
+ ],
233
+ },
234
+ },
235
+ ],
236
+ };
237
+ render(
238
+ <AppFeaturesProvider features={[readOnlySchema, catalogSchema]}>
239
+ <DispatcherProvider dispatcher={makeDispatcher()}>
240
+ <KumikoScreen schema={readOnlySchema} qn="tasks:screen:task-edit" />
241
+ </DispatcherProvider>
242
+ </AppFeaturesProvider>,
243
+ );
244
+ await waitFor(() => screen.getByTestId("render-edit-form"));
245
+ expect(screen.queryByTestId("combobox-kumiko-edit-assignee-create")).toBeNull();
246
+ });
247
+ });
181
248
  });
@@ -443,6 +443,23 @@ describe("WorkspaceShell", () => {
443
443
  expect(innerMain?.classList.contains("flex-1")).toBe(true);
444
444
  expect(innerMain?.classList.contains("overflow-auto")).toBe(true);
445
445
  });
446
+
447
+ // #1692: default (no fill) must not force viewport-lock classes
448
+ test("without fill does not apply h-svh or min-h-0", () => {
449
+ renderShell(
450
+ <WorkspaceShell
451
+ brand={<div>Brand</div>}
452
+ schema={schema}
453
+ user={{ id: "u1", roles: ["admin"] }}
454
+ >
455
+ <div>content</div>
456
+ </WorkspaceShell>,
457
+ );
458
+ const root = document.querySelector('[data-slot="sidebar-wrapper"]');
459
+ expect(root?.classList.contains("h-svh")).toBe(false);
460
+ const inset = document.querySelector('[data-slot="sidebar-inset"]');
461
+ expect(inset?.classList.contains("min-h-0")).toBe(false);
462
+ });
446
463
  });
447
464
 
448
465
  // ---------------------------------------------------------------------------
@@ -36,6 +36,17 @@ describe("parseIso", () => {
36
36
  expect(parseIso("0026-04-25")).toBeUndefined();
37
37
  expect(parseIso("26-04-25")).toBeUndefined();
38
38
  });
39
+
40
+ test("year above 9999 → undefined (Temporal.toString() would switch to signed extended ISO)", () => {
41
+ expect(parseIso("10000-04-25")).toBeUndefined();
42
+ expect(parseIso("999999-04-25")).toBeUndefined();
43
+ });
44
+
45
+ test("year 9999 is still accepted (upper boundary)", () => {
46
+ const d = parseIso("9999-04-25");
47
+ expect(d).toBeDefined();
48
+ expect(toIso(d as Temporal.PlainDate)).toBe("9999-04-25");
49
+ });
39
50
  });
40
51
 
41
52
  describe("toIso", () => {
@@ -80,6 +91,10 @@ describe("parseTypedDate", () => {
80
91
  if (d !== undefined) expect(toIso(d)).toBe("2026-04-25");
81
92
  });
82
93
 
94
+ test("six-digit year → undefined, not a signed extended-ISO wire value", () => {
95
+ expect(parseTypedDate("25.04.999999", "de-DE")).toBeUndefined();
96
+ });
97
+
83
98
  test("partial/invalid input → undefined", () => {
84
99
  expect(parseTypedDate("", "de-DE")).toBeUndefined();
85
100
  expect(parseTypedDate("25.04", "de-DE")).toBeUndefined();
@@ -108,4 +123,15 @@ describe("formatDateForInput", () => {
108
123
  expect(roundtrip).toBeDefined();
109
124
  if (roundtrip !== undefined) expect(toIso(roundtrip)).toBe("2026-04-25");
110
125
  });
126
+
127
+ // #1659: y/m/d locale branch of localeDateOrder (ISO-like)
128
+ test("ja-JP locale (y/m/d order), re-parseable", () => {
129
+ const formatted = formatDateForInput(
130
+ Temporal.PlainDate.from({ year: 2026, month: 4, day: 25 }),
131
+ "ja-JP",
132
+ );
133
+ const roundtrip = parseTypedDate(formatted, "ja-JP");
134
+ expect(roundtrip).toBeDefined();
135
+ if (roundtrip !== undefined) expect(toIso(roundtrip)).toBe("2026-04-25");
136
+ });
111
137
  });
@@ -37,7 +37,11 @@ export function parseIso(v: string): Temporal.PlainDate | undefined {
37
37
  Number.isNaN(d) ||
38
38
  // Reject 2-digit years — matches the pre-PlainDate behavior where
39
39
  // `new Date`'s 1900+y quirk made a round-trip check fail on them.
40
- y < 100
40
+ y < 100 ||
41
+ // Reject years outside 0000-9999 — Temporal.toString() switches to the
42
+ // signed extended ISO format above that range (e.g. "+010000-04-25"),
43
+ // breaking the "unchanged ISO string" wire contract (toIso below).
44
+ y > 9999
41
45
  ) {
42
46
  return undefined;
43
47
  }
@@ -106,6 +110,7 @@ export function parseTypedDate(input: string, locale: string): Temporal.PlainDat
106
110
  else d = val;
107
111
  });
108
112
  if (y < 100) y += 2000;
113
+ if (y > 9999) return undefined;
109
114
 
110
115
  return makePlainDate(y, m, d);
111
116
  }
@@ -1020,7 +1020,7 @@ function RowActionsKebab({
1020
1020
  // Server-Side-Render, kein observer wenn hasMore=false — dann zeigt
1021
1021
  // der Sentinel nur den End-of-list-Hinweis.
1022
1022
  // ponytail: Short lists do not need an end marker.
1023
- const END_LABEL_MIN_ROWS = 20;
1023
+ export const END_LABEL_MIN_ROWS = 20;
1024
1024
 
1025
1025
  function InfiniteSentinel({
1026
1026
  onReachEnd,