@cosmicdrift/kumiko-renderer-web 0.113.0 → 0.114.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 +4 -4
- package/src/__tests__/primitives.test.tsx +31 -0
- package/src/index.ts +3 -1
- package/src/primitives/index.tsx +22 -25
- package/src/primitives/layout.tsx +39 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.114.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": "0.114.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.114.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.114.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,6 +10,7 @@
|
|
|
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";
|
|
13
14
|
import { fireEvent, render, screen } from "./test-utils";
|
|
14
15
|
|
|
15
16
|
const { Button, Banner, Field, Input, DataTable, Form, Text, Heading, Dialog, Card } =
|
|
@@ -1053,3 +1054,33 @@ describe("Card", () => {
|
|
|
1053
1054
|
expect(screen.getByTestId("c").innerHTML).not.toContain("grow");
|
|
1054
1055
|
});
|
|
1055
1056
|
});
|
|
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
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type {
|
|
|
11
11
|
AppTokens,
|
|
12
12
|
BannerProps,
|
|
13
13
|
ButtonProps,
|
|
14
|
+
CardProps,
|
|
14
15
|
ColorTokens,
|
|
15
16
|
CorePrimitives,
|
|
16
17
|
CoreTokens,
|
|
@@ -123,7 +124,7 @@ export type { WorkspaceSwitcherProps } from "./layout/workspace-switcher";
|
|
|
123
124
|
export { WorkspaceSwitcher } from "./layout/workspace-switcher";
|
|
124
125
|
export { cn } from "./lib/cn";
|
|
125
126
|
export { postWithDownload } from "./lib/download";
|
|
126
|
-
export { BareFormProvider, defaultPrimitives } from "./primitives";
|
|
127
|
+
export { BareFormProvider, DefaultCard as Card, defaultPrimitives } from "./primitives";
|
|
127
128
|
export type { ActionMenuProps, MenuItemDef } from "./primitives/action-menu";
|
|
128
129
|
export { ActionMenu } from "./primitives/action-menu";
|
|
129
130
|
export {
|
|
@@ -136,6 +137,7 @@ export {
|
|
|
136
137
|
DropdownMenuSeparator,
|
|
137
138
|
DropdownMenuTrigger,
|
|
138
139
|
} from "./primitives/dropdown-menu";
|
|
140
|
+
export { PageSection, Stack } from "./primitives/layout";
|
|
139
141
|
export type { ToastOptions, ToastProviderProps, ToastVariant } from "./primitives/toast";
|
|
140
142
|
export { ToastProvider, useToast } from "./primitives/toast";
|
|
141
143
|
export type { CreateEventSourceLiveEventsOptions } from "./sse/live-events";
|
package/src/primitives/index.tsx
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
useTranslation,
|
|
36
36
|
WriteFailedError,
|
|
37
37
|
} from "@cosmicdrift/kumiko-renderer";
|
|
38
|
+
import { cva } from "class-variance-authority";
|
|
38
39
|
import {
|
|
39
40
|
ArrowDown,
|
|
40
41
|
ArrowUp,
|
|
@@ -59,7 +60,6 @@ import {
|
|
|
59
60
|
import { cn } from "../lib/cn";
|
|
60
61
|
import { Badge } from "../ui/badge";
|
|
61
62
|
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,6 +81,17 @@ 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
|
+
|
|
84
95
|
// ---- Button (vendored shadcn ui/button) ----
|
|
85
96
|
|
|
86
97
|
// Contract-Variant → shadcn-Variant: secondary war schon immer der
|
|
@@ -1365,7 +1376,7 @@ function DefaultForm({
|
|
|
1365
1376
|
className="flex flex-col w-full"
|
|
1366
1377
|
>
|
|
1367
1378
|
<div className="px-6 pt-6 pb-12 max-w-3xl w-full">
|
|
1368
|
-
<div className="
|
|
1379
|
+
<div className={cn(cardSurface(), "overflow-hidden")}>
|
|
1369
1380
|
{(title !== undefined || subtitle !== undefined) && (
|
|
1370
1381
|
<div className="px-6 pb-2 pt-5">
|
|
1371
1382
|
{title !== undefined && (
|
|
@@ -1402,7 +1413,7 @@ function DefaultForm({
|
|
|
1402
1413
|
{actions !== undefined && (
|
|
1403
1414
|
<div
|
|
1404
1415
|
data-testid={testId !== undefined ? `${testId}-actions` : undefined}
|
|
1405
|
-
className=
|
|
1416
|
+
className={cn(cardFooter, cardFooterBorder)}
|
|
1406
1417
|
>
|
|
1407
1418
|
{actions}
|
|
1408
1419
|
</div>
|
|
@@ -1469,20 +1480,20 @@ function DefaultSection({ title, subtitle, children, actions, testId }: SectionP
|
|
|
1469
1480
|
// `children`) WOULD get silently clipped — verify this against any new
|
|
1470
1481
|
// standalone-section content that renders its own non-portaled overlay.
|
|
1471
1482
|
return (
|
|
1472
|
-
<
|
|
1473
|
-
<
|
|
1483
|
+
<div data-testid={testId} className={cn(cardSurface(), "overflow-hidden")}>
|
|
1484
|
+
<div className="flex flex-col gap-4 px-6 py-6">
|
|
1474
1485
|
{header}
|
|
1475
1486
|
{children}
|
|
1476
|
-
</
|
|
1487
|
+
</div>
|
|
1477
1488
|
{actions !== undefined && (
|
|
1478
1489
|
<div
|
|
1479
1490
|
data-testid={testId !== undefined ? `${testId}-actions` : undefined}
|
|
1480
|
-
className=
|
|
1491
|
+
className={cn(cardFooter, cardFooterBorder)}
|
|
1481
1492
|
>
|
|
1482
1493
|
{actions}
|
|
1483
1494
|
</div>
|
|
1484
1495
|
)}
|
|
1485
|
-
</
|
|
1496
|
+
</div>
|
|
1486
1497
|
);
|
|
1487
1498
|
}
|
|
1488
1499
|
|
|
@@ -1564,7 +1575,7 @@ import { ConfigSourceBadge as DefaultConfigSourceBadge } from "../components/con
|
|
|
1564
1575
|
|
|
1565
1576
|
// Generische Card-Chrome (rounded-xl wie die Entity-Card) — slot- + options-
|
|
1566
1577
|
// basiert, damit der Contract additiv wächst und Consumer nie migriert werden.
|
|
1567
|
-
function DefaultCard({ slots, options, className, testId, children }: CardProps): ReactNode {
|
|
1578
|
+
export function DefaultCard({ slots, options, className, testId, children }: CardProps): ReactNode {
|
|
1568
1579
|
const padded = options?.padded ?? true;
|
|
1569
1580
|
const radius = options?.radius ?? "xl";
|
|
1570
1581
|
const footerBordered = options?.footerBordered ?? true;
|
|
@@ -1586,14 +1597,7 @@ function DefaultCard({ slots, options, className, testId, children }: CardProps)
|
|
|
1586
1597
|
const header = s.header ?? defaultHeader;
|
|
1587
1598
|
const hasHeader = header !== null && header !== undefined;
|
|
1588
1599
|
return (
|
|
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
|
-
>
|
|
1600
|
+
<div data-testid={testId} className={cn(cardSurface({ radius }), "overflow-hidden", className)}>
|
|
1597
1601
|
{header}
|
|
1598
1602
|
{/* != null covers undefined AND explicit null; a `false` child (from
|
|
1599
1603
|
`cond && <El/>`) still renders no visible content either way. */}
|
|
@@ -1601,14 +1605,7 @@ function DefaultCard({ slots, options, className, testId, children }: CardProps)
|
|
|
1601
1605
|
<div className={cn("grow", padded && (hasHeader ? "px-6 pb-6" : "p-6"))}>{children}</div>
|
|
1602
1606
|
)}
|
|
1603
1607
|
{s.footer !== undefined && (
|
|
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>
|
|
1608
|
+
<div className={cn(cardFooter, footerBordered && cardFooterBorder)}>{s.footer}</div>
|
|
1612
1609
|
)}
|
|
1613
1610
|
</div>
|
|
1614
1611
|
);
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
}
|