@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.
- package/package.json +5 -5
- package/src/__tests__/app-layout.test.tsx +13 -12
- package/src/__tests__/default-app-shell.test.tsx +33 -0
- package/src/__tests__/form-action-bar.test.tsx +53 -4
- package/src/__tests__/kumiko-screen.test.tsx +293 -3
- package/src/__tests__/primitives.test.tsx +245 -15
- package/src/__tests__/projection-detail-record-layout.test.tsx +91 -0
- package/src/__tests__/render-edit.test.tsx +29 -10
- package/src/__tests__/render-list-narrow-cards.test.tsx +187 -0
- package/src/__tests__/workspace-shell.test.tsx +5 -4
- package/src/icons.tsx +199 -0
- package/src/layout/app-layout.tsx +7 -7
- package/src/layout/default-app-shell.tsx +3 -2
- package/src/layout/nav-tree.tsx +25 -122
- package/src/layout/workspace-shell.tsx +3 -2
- package/src/primitives/__tests__/button.test.tsx +37 -0
- package/src/primitives/__tests__/data-table-logic.test.ts +33 -0
- package/src/primitives/__tests__/select-segmented.test.tsx +84 -0
- package/src/primitives/index.tsx +475 -50
- package/src/primitives/metric.tsx +18 -10
- package/src/styles.css +21 -17
- package/src/ui/switch.tsx +35 -0
|
@@ -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
|
|
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
|
+
});
|