@cosmicdrift/kumiko-renderer-web 0.172.0 → 0.173.1

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.172.0",
3
+ "version": "0.173.1",
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.172.0",
20
- "@cosmicdrift/kumiko-headless": "0.172.0",
21
- "@cosmicdrift/kumiko-renderer": "0.172.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.173.1",
20
+ "@cosmicdrift/kumiko-headless": "0.173.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.173.1",
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
  // ---------------------------------------------------------------------------
@@ -108,4 +108,15 @@ describe("formatDateForInput", () => {
108
108
  expect(roundtrip).toBeDefined();
109
109
  if (roundtrip !== undefined) expect(toIso(roundtrip)).toBe("2026-04-25");
110
110
  });
111
+
112
+ // #1659: y/m/d locale branch of localeDateOrder (ISO-like)
113
+ test("ja-JP locale (y/m/d order), re-parseable", () => {
114
+ const formatted = formatDateForInput(
115
+ Temporal.PlainDate.from({ year: 2026, month: 4, day: 25 }),
116
+ "ja-JP",
117
+ );
118
+ const roundtrip = parseTypedDate(formatted, "ja-JP");
119
+ expect(roundtrip).toBeDefined();
120
+ if (roundtrip !== undefined) expect(toIso(roundtrip)).toBe("2026-04-25");
121
+ });
111
122
  });
@@ -1019,11 +1019,8 @@ function RowActionsKebab({
1019
1019
  // lädt, ignorieren wir weitere Sichtbar-Events). Kein observer in
1020
1020
  // Server-Side-Render, kein observer wenn hasMore=false — dann zeigt
1021
1021
  // der Sentinel nur den End-of-list-Hinweis.
1022
- // ponytail: End-Marker nur ab END_LABEL_MIN_ROWS Zeilen eine kurze
1023
- // Liste (eine einzige Page) zeigt ihr Ende selbst, da wäre der Marker
1024
- // nur Lärm. Schwellwert = eine Page; per Prop übersteuerbar falls Apps
1025
- // andere Page-Größen fahren.
1026
- const END_LABEL_MIN_ROWS = 20;
1022
+ // ponytail: Short lists do not need an end marker.
1023
+ export const END_LABEL_MIN_ROWS = 20;
1027
1024
 
1028
1025
  function InfiniteSentinel({
1029
1026
  onReachEnd,