@cosmicdrift/kumiko-renderer-web 0.232.0 → 0.234.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.
@@ -80,7 +80,8 @@ export type WorkspaceShellProps = {
80
80
  /** Runtime badge slot per nav leaf (bare nav id), same as DefaultAppShell. */
81
81
  readonly navBadges?: ReadonlyMap<string, ReactNode>;
82
82
  /** Viewport-fit shell. true → fixed viewport height (`h-svh`), content
83
- * scrolls INSIDE instead of the whole page. Default false (page scroll). */
83
+ * scrolls INSIDE instead of the whole page. Default true set
84
+ * `fill={false}` to opt back into page scroll. */
84
85
  readonly fill?: boolean;
85
86
  /** Screen content. */
86
87
  readonly children: ReactNode;
@@ -94,7 +95,7 @@ export function WorkspaceShell({
94
95
  initialWorkspaceId,
95
96
  sidebarFooter,
96
97
  navBadges,
97
- fill,
98
+ fill = true,
98
99
  children,
99
100
  }: WorkspaceShellProps): ReactNode {
100
101
  const app = useMemo(() => toAppSchema(schema), [schema]);
@@ -25,3 +25,40 @@ describe("DefaultButton className/ref (fw#1831)", () => {
25
25
  expect(ref.current).toBe(screen.getByTestId("btn"));
26
26
  });
27
27
  });
28
+
29
+ describe("DefaultButton icon (fw-ui-defaults)", () => {
30
+ test("icon prop renders the resolved icon left of the text", () => {
31
+ render(
32
+ <Button icon="trash" testId="btn">
33
+ Delete
34
+ </Button>,
35
+ );
36
+ const btn = screen.getByTestId("btn");
37
+ expect(btn.querySelector("svg")).not.toBeNull();
38
+ expect(btn.textContent).toBe("Delete");
39
+ });
40
+
41
+ test("size='icon' + a resolved icon still renders icon, children and aria-label", () => {
42
+ render(
43
+ <Button icon="trash" size="icon" ariaLabel="Delete" testId="btn">
44
+ Delete
45
+ </Button>,
46
+ );
47
+ const btn = screen.getByTestId("btn");
48
+ expect(btn.textContent).toBe("Delete");
49
+ expect(btn.querySelector("svg")).not.toBeNull();
50
+ expect(btn.getAttribute("aria-label")).toBe("Delete");
51
+ });
52
+
53
+ test("unknown icon key: no crash, falls back to rendering children only", () => {
54
+ render(
55
+ // @ts-expect-error — exercising the runtime fallback for a schema-supplied key outside the closed IconKey union
56
+ <Button icon="not-a-real-icon" testId="btn">
57
+ Delete
58
+ </Button>,
59
+ );
60
+ const btn = screen.getByTestId("btn");
61
+ expect(btn.querySelector("svg")).toBeNull();
62
+ expect(btn.textContent).toBe("Delete");
63
+ });
64
+ });
@@ -216,6 +216,39 @@ describe("defaultCellRender", () => {
216
216
  expect(defaultCellRender("hallo", "text")).toBe("hallo");
217
217
  });
218
218
 
219
+ test("text-Spalte mit vollem ISO-8601-Zeitstempel warnt einmal pro Spalte, Wert bleibt unverändert (fw#2569)", () => {
220
+ const prevNodeEnv = process.env.NODE_ENV;
221
+ process.env.NODE_ENV = "development";
222
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
223
+ try {
224
+ const iso = "2026-09-04T12:30:00.000Z";
225
+ expect(defaultCellRender(iso, "text", undefined, undefined, "createdAt")).toBe(iso);
226
+ expect(warn).toHaveBeenCalledTimes(1);
227
+ expect(warn.mock.calls[0]?.[0]).toContain("createdAt");
228
+ // Same column, second row with a different ISO value → no second warning.
229
+ expect(
230
+ defaultCellRender("2026-09-05T08:00:00Z", "text", undefined, undefined, "createdAt"),
231
+ ).toBe("2026-09-05T08:00:00Z");
232
+ expect(warn).toHaveBeenCalledTimes(1);
233
+ // A different column still gets its own warning.
234
+ expect(defaultCellRender(iso, "text", undefined, undefined, "expiresAt")).toBe(iso);
235
+ expect(warn).toHaveBeenCalledTimes(2);
236
+ // Plain text and date-only strings never trigger the warning.
237
+ expect(defaultCellRender("hallo", "text", undefined, undefined, "name")).toBe("hallo");
238
+ expect(defaultCellRender("2026-09-04", "text", undefined, undefined, "day")).toBe(
239
+ "2026-09-04",
240
+ );
241
+ expect(warn).toHaveBeenCalledTimes(2);
242
+ } finally {
243
+ warn.mockRestore();
244
+ if (prevNodeEnv === undefined) {
245
+ delete process.env.NODE_ENV;
246
+ } else {
247
+ process.env.NODE_ENV = prevNodeEnv;
248
+ }
249
+ }
250
+ });
251
+
219
252
  test("number/decimal → locale-formatiert, kein roher Dezimalpunkt (fw#2160)", () => {
220
253
  expect(defaultCellRender(42, "number")).toBe(new Intl.NumberFormat(undefined).format(42));
221
254
  expect(defaultCellRender(245.5, "decimal")).toBe(
@@ -0,0 +1,84 @@
1
+ // Segment control for `kind: "select"` with a small closed option set
2
+ // (edit-existing screenshot feedback): a Status field with 3 short values
3
+ // no longer stretches into a full-width dropdown. Threshold: ≤4 options,
4
+ // each label ≤14 chars — otherwise the unchanged ComboboxInput dropdown.
5
+
6
+ import { describe, expect, test } from "bun:test";
7
+ import { fireEvent } from "@testing-library/react";
8
+ import { render, screen } from "../../__tests__/test-utils";
9
+ import { defaultPrimitives } from "../index";
10
+
11
+ const { Field, Input } = defaultPrimitives;
12
+
13
+ function renderSelect(
14
+ options: readonly string[],
15
+ overrides: { readonly value?: string; readonly disabled?: boolean } = {},
16
+ ): string[] {
17
+ const changes: string[] = [];
18
+ render(
19
+ <Field id="status" label="Status" testId="field-status">
20
+ <Input
21
+ kind="select"
22
+ id="status"
23
+ name="status"
24
+ value={overrides.value ?? options[0] ?? ""}
25
+ onChange={(v) => changes.push(v)}
26
+ options={options}
27
+ {...(overrides.disabled !== undefined && { disabled: overrides.disabled })}
28
+ />
29
+ </Field>,
30
+ );
31
+ return changes;
32
+ }
33
+
34
+ describe("DefaultInput select → segmented control (edit-existing feedback)", () => {
35
+ test("≤4 short options render the segmented control, not the dropdown", () => {
36
+ renderSelect(["Draft", "Review", "Published"]);
37
+ expect(screen.queryByTestId("combobox-status")).toBeNull();
38
+ expect(screen.getByRole("radiogroup")).toBeTruthy();
39
+ expect(screen.getAllByRole("radio")).toHaveLength(3);
40
+ });
41
+
42
+ test("4 options render the segmented control", () => {
43
+ renderSelect(["Draft", "Review", "Published", "Archived"]);
44
+ expect(screen.queryByTestId("combobox-status")).toBeNull();
45
+ expect(screen.getByRole("radiogroup")).toBeTruthy();
46
+ expect(screen.getAllByRole("radio")).toHaveLength(4);
47
+ });
48
+
49
+ test("5 options keep the dropdown", () => {
50
+ renderSelect(["Draft", "Review", "Published", "Archived", "Deleted"]);
51
+ expect(screen.queryByRole("radiogroup")).toBeNull();
52
+ expect(screen.getByTestId("combobox-status")).toBeTruthy();
53
+ });
54
+
55
+ test("3 options with one long label (>14 chars) keep the dropdown", () => {
56
+ renderSelect(["Draft", "Review", "Published Status"]);
57
+ expect(screen.queryByRole("radiogroup")).toBeNull();
58
+ expect(screen.getByTestId("combobox-status")).toBeTruthy();
59
+ });
60
+
61
+ test("clicking a segment reports the same value the dropdown's onChange would give", () => {
62
+ const changes = renderSelect(["Draft", "Review", "Published"], { value: "Draft" });
63
+ fireEvent.click(screen.getByRole("radio", { name: "Review" }));
64
+ expect(changes).toEqual(["Review"]);
65
+ });
66
+
67
+ test("group has an accessible name and the selected segment is aria-checked", () => {
68
+ renderSelect(["Draft", "Review", "Published"], { value: "Review" });
69
+ expect(screen.getByRole("radiogroup", { name: "Status" })).toBeTruthy();
70
+ expect(screen.getByRole("radio", { name: "Review" }).getAttribute("aria-checked")).toBe("true");
71
+ expect(screen.getByRole("radio", { name: "Draft" }).getAttribute("aria-checked")).toBe("false");
72
+ });
73
+
74
+ test("disabled prevents selection", () => {
75
+ const changes = renderSelect(["Draft", "Review", "Published"], {
76
+ value: "Draft",
77
+ disabled: true,
78
+ });
79
+ const review = screen.getByRole("radio", { name: "Review" });
80
+ expect((review as HTMLButtonElement).disabled).toBe(true);
81
+ fireEvent.click(review);
82
+ expect(changes).toEqual([]);
83
+ });
84
+ });