@cosmicdrift/kumiko-renderer-web 0.114.0 → 1.0.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.
|
|
3
|
+
"version": "1.0.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": "1.0.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "1.0.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "1.0.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",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
import { describe, expect, mock, test } from "bun:test";
|
|
11
11
|
import userEvent from "@testing-library/user-event";
|
|
12
12
|
import { defaultPrimitives } from "../primitives";
|
|
13
|
-
import { PageSection, Stack } from "../primitives/layout";
|
|
14
13
|
import { fireEvent, render, screen } from "./test-utils";
|
|
15
14
|
|
|
16
15
|
const { Button, Banner, Field, Input, DataTable, Form, Text, Heading, Dialog, Card } =
|
|
@@ -1054,33 +1053,3 @@ describe("Card", () => {
|
|
|
1054
1053
|
expect(screen.getByTestId("c").innerHTML).not.toContain("grow");
|
|
1055
1054
|
});
|
|
1056
1055
|
});
|
|
1057
|
-
|
|
1058
|
-
describe("Stack", () => {
|
|
1059
|
-
test("gap-Variante bildet auf die Tailwind-gap-Klasse ab", () => {
|
|
1060
|
-
render(
|
|
1061
|
-
<Stack testId="s" gap="lg">
|
|
1062
|
-
<span>a</span>
|
|
1063
|
-
</Stack>,
|
|
1064
|
-
);
|
|
1065
|
-
const el = screen.getByTestId("s");
|
|
1066
|
-
expect(el.className).toContain("flex flex-col");
|
|
1067
|
-
expect(el.className).toContain("gap-6");
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
test("default gap = md", () => {
|
|
1071
|
-
render(<Stack testId="s">x</Stack>);
|
|
1072
|
-
expect(screen.getByTestId("s").className).toContain("gap-4");
|
|
1073
|
-
});
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
describe("PageSection", () => {
|
|
1077
|
-
test("wrappt children mit einheitlichem Padding", () => {
|
|
1078
|
-
render(
|
|
1079
|
-
<PageSection testId="p">
|
|
1080
|
-
<span data-testid="child">x</span>
|
|
1081
|
-
</PageSection>,
|
|
1082
|
-
);
|
|
1083
|
-
expect(screen.getByTestId("p").className).toContain("p-6");
|
|
1084
|
-
expect(screen.getByTestId("child")).toBeDefined();
|
|
1085
|
-
});
|
|
1086
|
-
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, mock, spyOn, test } from "bun:test";
|
|
2
2
|
import type {
|
|
3
3
|
EntityDefinition,
|
|
4
4
|
EntityListScreenDefinition,
|
|
@@ -188,21 +188,13 @@ describe("RenderList — slots.header", () => {
|
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
test("slots.header gesetzt, aber Component nicht registriert → kein Crash, kein Header", () => {
|
|
191
|
-
spyOn(console, "warn").mockImplementation(() => {});
|
|
191
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
192
192
|
render(
|
|
193
193
|
<ExtensionSectionsProvider value={{}}>
|
|
194
194
|
<RenderList screen={screenWithHeader} entity={taskEntity} rows={[]} featureName="tasks" />
|
|
195
195
|
</ExtensionSectionsProvider>,
|
|
196
196
|
);
|
|
197
197
|
expect(screen.queryByTestId("list-header-slot")).toBeNull();
|
|
198
|
+
warn.mockRestore();
|
|
198
199
|
});
|
|
199
200
|
});
|
|
200
|
-
|
|
201
|
-
// 573/1: a mid-test assertion throw would skip the trailing warn.mockRestore()
|
|
202
|
-
// above, leaking the spy into every later test — this backstop restores
|
|
203
|
-
// unconditionally regardless of how the test exited.
|
|
204
|
-
afterEach(() => {
|
|
205
|
-
// biome-ignore lint/suspicious/noConsole: reading the spy handle to restore it, not logging
|
|
206
|
-
const warn = console.warn as unknown as { mockRestore?: () => void };
|
|
207
|
-
warn.mockRestore?.();
|
|
208
|
-
});
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,6 @@ export type {
|
|
|
11
11
|
AppTokens,
|
|
12
12
|
BannerProps,
|
|
13
13
|
ButtonProps,
|
|
14
|
-
CardProps,
|
|
15
14
|
ColorTokens,
|
|
16
15
|
CorePrimitives,
|
|
17
16
|
CoreTokens,
|
|
@@ -124,7 +123,7 @@ export type { WorkspaceSwitcherProps } from "./layout/workspace-switcher";
|
|
|
124
123
|
export { WorkspaceSwitcher } from "./layout/workspace-switcher";
|
|
125
124
|
export { cn } from "./lib/cn";
|
|
126
125
|
export { postWithDownload } from "./lib/download";
|
|
127
|
-
export { BareFormProvider,
|
|
126
|
+
export { BareFormProvider, defaultPrimitives } from "./primitives";
|
|
128
127
|
export type { ActionMenuProps, MenuItemDef } from "./primitives/action-menu";
|
|
129
128
|
export { ActionMenu } from "./primitives/action-menu";
|
|
130
129
|
export {
|
|
@@ -137,7 +136,6 @@ export {
|
|
|
137
136
|
DropdownMenuSeparator,
|
|
138
137
|
DropdownMenuTrigger,
|
|
139
138
|
} from "./primitives/dropdown-menu";
|
|
140
|
-
export { PageSection, Stack } from "./primitives/layout";
|
|
141
139
|
export type { ToastOptions, ToastProviderProps, ToastVariant } from "./primitives/toast";
|
|
142
140
|
export { ToastProvider, useToast } from "./primitives/toast";
|
|
143
141
|
export type { CreateEventSourceLiveEventsOptions } from "./sse/live-events";
|
package/src/layout/nav-tree.tsx
CHANGED
|
@@ -192,14 +192,8 @@ export function NavTree({
|
|
|
192
192
|
// schiebt ihn ans rechte Ende; das truncate-Label gibt nach.
|
|
193
193
|
// ponytail: nur an screen/target-Leaves verdrahtet, nicht an Container-
|
|
194
194
|
// Knoten (die tragen den ml-auto-Chevron) — Tier-Badge ist ein Leaf.
|
|
195
|
-
// Defense-in-depth (555/4): every current call site already guards on
|
|
196
|
-
// node.screen/node.target !== undefined before rendering <NavBadge>, so this
|
|
197
|
-
// early-return changes no current behavior — it's here so a future call
|
|
198
|
-
// site added without that guard can't silently show a leaf's badge on an
|
|
199
|
-
// unrelated container node that happens to share its last-segment.
|
|
200
195
|
function NavBadge({ node }: { readonly node: NavNode }): ReactNode {
|
|
201
196
|
const badges = useContext(NavBadgesContext);
|
|
202
|
-
if (node.screen === undefined && node.target === undefined) return null;
|
|
203
197
|
const badge = badges.get(lastSegment(node.qualifiedName));
|
|
204
198
|
if (badge === undefined) return null;
|
|
205
199
|
return <span className="ml-auto shrink-0">{badge}</span>;
|
package/src/primitives/index.tsx
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
useTranslation,
|
|
36
36
|
WriteFailedError,
|
|
37
37
|
} from "@cosmicdrift/kumiko-renderer";
|
|
38
|
-
import { cva } from "class-variance-authority";
|
|
39
38
|
import {
|
|
40
39
|
ArrowDown,
|
|
41
40
|
ArrowUp,
|
|
@@ -60,6 +59,7 @@ import {
|
|
|
60
59
|
import { cn } from "../lib/cn";
|
|
61
60
|
import { Badge } from "../ui/badge";
|
|
62
61
|
import { Button as UiButton } from "../ui/button";
|
|
62
|
+
import { Card, CardContent } from "../ui/card";
|
|
63
63
|
import { Checkbox } from "../ui/checkbox";
|
|
64
64
|
import { Input as UiInput } from "../ui/input";
|
|
65
65
|
import { Label as UiLabel } from "../ui/label";
|
|
@@ -81,17 +81,6 @@ import { MoneyInput } from "./money-input";
|
|
|
81
81
|
import { TimestampInput } from "./timestamp-input";
|
|
82
82
|
import { useToast } from "./toast";
|
|
83
83
|
|
|
84
|
-
// ---- Card-Chrome (eine Definition für Form/Section/Card) ----
|
|
85
|
-
|
|
86
|
-
// Die eine Card-Surface. Radius ist die einzige real gebrauchte Variante:
|
|
87
|
-
// xl = Card/Screen-Fläche (Default), lg = "innen"-Fläche wie Tabellen.
|
|
88
|
-
const cardSurface = cva("flex flex-col border bg-card text-card-foreground shadow-sm", {
|
|
89
|
-
variants: { radius: { xl: "rounded-xl", lg: "rounded-lg" } },
|
|
90
|
-
defaultVariants: { radius: "xl" },
|
|
91
|
-
});
|
|
92
|
-
const cardFooter = "flex items-center justify-end gap-2 px-6 py-4";
|
|
93
|
-
const cardFooterBorder = "border-t bg-muted/30";
|
|
94
|
-
|
|
95
84
|
// ---- Button (vendored shadcn ui/button) ----
|
|
96
85
|
|
|
97
86
|
// Contract-Variant → shadcn-Variant: secondary war schon immer der
|
|
@@ -1376,7 +1365,7 @@ function DefaultForm({
|
|
|
1376
1365
|
className="flex flex-col w-full"
|
|
1377
1366
|
>
|
|
1378
1367
|
<div className="px-6 pt-6 pb-12 max-w-3xl w-full">
|
|
1379
|
-
<div className=
|
|
1368
|
+
<div className="bg-card overflow-hidden rounded-xl border shadow-sm">
|
|
1380
1369
|
{(title !== undefined || subtitle !== undefined) && (
|
|
1381
1370
|
<div className="px-6 pb-2 pt-5">
|
|
1382
1371
|
{title !== undefined && (
|
|
@@ -1413,7 +1402,7 @@ function DefaultForm({
|
|
|
1413
1402
|
{actions !== undefined && (
|
|
1414
1403
|
<div
|
|
1415
1404
|
data-testid={testId !== undefined ? `${testId}-actions` : undefined}
|
|
1416
|
-
className=
|
|
1405
|
+
className="flex items-center justify-end gap-2 border-t bg-muted/30 px-6 py-4"
|
|
1417
1406
|
>
|
|
1418
1407
|
{actions}
|
|
1419
1408
|
</div>
|
|
@@ -1480,20 +1469,20 @@ function DefaultSection({ title, subtitle, children, actions, testId }: SectionP
|
|
|
1480
1469
|
// `children`) WOULD get silently clipped — verify this against any new
|
|
1481
1470
|
// standalone-section content that renders its own non-portaled overlay.
|
|
1482
1471
|
return (
|
|
1483
|
-
<
|
|
1484
|
-
<
|
|
1472
|
+
<Card data-testid={testId} className="gap-0 overflow-hidden rounded-lg py-0">
|
|
1473
|
+
<CardContent className="flex flex-col gap-4 px-6 py-6">
|
|
1485
1474
|
{header}
|
|
1486
1475
|
{children}
|
|
1487
|
-
</
|
|
1476
|
+
</CardContent>
|
|
1488
1477
|
{actions !== undefined && (
|
|
1489
1478
|
<div
|
|
1490
1479
|
data-testid={testId !== undefined ? `${testId}-actions` : undefined}
|
|
1491
|
-
className=
|
|
1480
|
+
className="flex items-center justify-end gap-2 border-t bg-muted/30 px-6 py-4"
|
|
1492
1481
|
>
|
|
1493
1482
|
{actions}
|
|
1494
1483
|
</div>
|
|
1495
1484
|
)}
|
|
1496
|
-
</
|
|
1485
|
+
</Card>
|
|
1497
1486
|
);
|
|
1498
1487
|
}
|
|
1499
1488
|
|
|
@@ -1575,7 +1564,7 @@ import { ConfigSourceBadge as DefaultConfigSourceBadge } from "../components/con
|
|
|
1575
1564
|
|
|
1576
1565
|
// Generische Card-Chrome (rounded-xl wie die Entity-Card) — slot- + options-
|
|
1577
1566
|
// basiert, damit der Contract additiv wächst und Consumer nie migriert werden.
|
|
1578
|
-
|
|
1567
|
+
function DefaultCard({ slots, options, className, testId, children }: CardProps): ReactNode {
|
|
1579
1568
|
const padded = options?.padded ?? true;
|
|
1580
1569
|
const radius = options?.radius ?? "xl";
|
|
1581
1570
|
const footerBordered = options?.footerBordered ?? true;
|
|
@@ -1597,7 +1586,14 @@ export function DefaultCard({ slots, options, className, testId, children }: Car
|
|
|
1597
1586
|
const header = s.header ?? defaultHeader;
|
|
1598
1587
|
const hasHeader = header !== null && header !== undefined;
|
|
1599
1588
|
return (
|
|
1600
|
-
<div
|
|
1589
|
+
<div
|
|
1590
|
+
data-testid={testId}
|
|
1591
|
+
className={cn(
|
|
1592
|
+
"flex flex-col overflow-hidden border bg-card text-card-foreground shadow-sm",
|
|
1593
|
+
radius === "xl" ? "rounded-xl" : "rounded-lg",
|
|
1594
|
+
className,
|
|
1595
|
+
)}
|
|
1596
|
+
>
|
|
1601
1597
|
{header}
|
|
1602
1598
|
{/* != null covers undefined AND explicit null; a `false` child (from
|
|
1603
1599
|
`cond && <El/>`) still renders no visible content either way. */}
|
|
@@ -1605,7 +1601,14 @@ export function DefaultCard({ slots, options, className, testId, children }: Car
|
|
|
1605
1601
|
<div className={cn("grow", padded && (hasHeader ? "px-6 pb-6" : "p-6"))}>{children}</div>
|
|
1606
1602
|
)}
|
|
1607
1603
|
{s.footer !== undefined && (
|
|
1608
|
-
<div
|
|
1604
|
+
<div
|
|
1605
|
+
className={cn(
|
|
1606
|
+
"flex items-center justify-end gap-2 px-6 py-4",
|
|
1607
|
+
footerBordered && "border-t bg-muted/30",
|
|
1608
|
+
)}
|
|
1609
|
+
>
|
|
1610
|
+
{s.footer}
|
|
1611
|
+
</div>
|
|
1609
1612
|
)}
|
|
1610
1613
|
</div>
|
|
1611
1614
|
);
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// Layout-Primitives für Custom-Screens: ein Ort für vertikale Abstände und
|
|
2
|
-
// Screen-Padding, damit Consumer nicht `flex flex-col gap-*` / `p-6` per Hand
|
|
3
|
-
// streuen. Bewusst dünn — kein generisches Box-mit-20-props-System.
|
|
4
|
-
|
|
5
|
-
import type { ReactNode } from "react";
|
|
6
|
-
import { cn } from "../lib/cn";
|
|
7
|
-
|
|
8
|
-
const STACK_GAP = { sm: "gap-2", md: "gap-4", lg: "gap-6" } as const;
|
|
9
|
-
|
|
10
|
-
type StackGap = keyof typeof STACK_GAP;
|
|
11
|
-
|
|
12
|
-
type StackProps = {
|
|
13
|
-
readonly gap?: StackGap;
|
|
14
|
-
readonly className?: string;
|
|
15
|
-
readonly children?: ReactNode;
|
|
16
|
-
readonly testId?: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function Stack({ gap = "md", className, children, testId }: StackProps): ReactNode {
|
|
20
|
-
return (
|
|
21
|
-
<div data-testid={testId} className={cn("flex flex-col", STACK_GAP[gap], className)}>
|
|
22
|
-
{children}
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type PageSectionProps = {
|
|
28
|
-
readonly className?: string;
|
|
29
|
-
readonly children?: ReactNode;
|
|
30
|
-
readonly testId?: string;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export function PageSection({ className, children, testId }: PageSectionProps): ReactNode {
|
|
34
|
-
return (
|
|
35
|
-
<div data-testid={testId} className={cn("p-6", className)}>
|
|
36
|
-
{children}
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|