@cosmicdrift/kumiko-renderer-web 0.113.1 → 0.115.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 +32 -1
- package/src/index.ts +3 -1
- package/src/primitives/index.tsx +36 -24
- package/src/primitives/layout.tsx +39 -0
- package/src/styles.css +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.115.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.115.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.115.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.115.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 } =
|
|
@@ -979,7 +980,7 @@ describe("Card", () => {
|
|
|
979
980
|
<span>body</span>
|
|
980
981
|
</Card>,
|
|
981
982
|
);
|
|
982
|
-
expect(screen.getByTestId("c").innerHTML).toContain("p-
|
|
983
|
+
expect(screen.getByTestId("c").innerHTML).toContain("p-[var(--card-padding)]");
|
|
983
984
|
});
|
|
984
985
|
|
|
985
986
|
test("padded=false renders body without padding classes", () => {
|
|
@@ -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,22 @@ 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. Maße (Padding/Radius/Shadow) kommen aus --card-*
|
|
87
|
+
// CSS-Tokens (Defaults in styles.css), damit eine App sie einmal zentral in
|
|
88
|
+
// ihrer styles.css überschreiben kann — wie die Farben. Radius-Variante:
|
|
89
|
+
// xl = Card/Screen-Fläche (Default, token-getrieben), lg = "innen"-Fläche.
|
|
90
|
+
const cardSurface = cva(
|
|
91
|
+
"flex flex-col border bg-card text-card-foreground shadow-[var(--card-shadow)]",
|
|
92
|
+
{
|
|
93
|
+
variants: { radius: { xl: "rounded-[var(--card-radius)]", lg: "rounded-lg" } },
|
|
94
|
+
defaultVariants: { radius: "xl" },
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
const cardFooter = "flex items-center justify-end gap-2 px-[var(--card-padding)] py-4";
|
|
98
|
+
const cardFooterBorder = "border-t bg-muted/30";
|
|
99
|
+
|
|
84
100
|
// ---- Button (vendored shadcn ui/button) ----
|
|
85
101
|
|
|
86
102
|
// Contract-Variant → shadcn-Variant: secondary war schon immer der
|
|
@@ -1365,7 +1381,7 @@ function DefaultForm({
|
|
|
1365
1381
|
className="flex flex-col w-full"
|
|
1366
1382
|
>
|
|
1367
1383
|
<div className="px-6 pt-6 pb-12 max-w-3xl w-full">
|
|
1368
|
-
<div className="
|
|
1384
|
+
<div className={cn(cardSurface(), "overflow-hidden")}>
|
|
1369
1385
|
{(title !== undefined || subtitle !== undefined) && (
|
|
1370
1386
|
<div className="px-6 pb-2 pt-5">
|
|
1371
1387
|
{title !== undefined && (
|
|
@@ -1402,7 +1418,7 @@ function DefaultForm({
|
|
|
1402
1418
|
{actions !== undefined && (
|
|
1403
1419
|
<div
|
|
1404
1420
|
data-testid={testId !== undefined ? `${testId}-actions` : undefined}
|
|
1405
|
-
className=
|
|
1421
|
+
className={cn(cardFooter, cardFooterBorder)}
|
|
1406
1422
|
>
|
|
1407
1423
|
{actions}
|
|
1408
1424
|
</div>
|
|
@@ -1469,20 +1485,20 @@ function DefaultSection({ title, subtitle, children, actions, testId }: SectionP
|
|
|
1469
1485
|
// `children`) WOULD get silently clipped — verify this against any new
|
|
1470
1486
|
// standalone-section content that renders its own non-portaled overlay.
|
|
1471
1487
|
return (
|
|
1472
|
-
<
|
|
1473
|
-
<
|
|
1488
|
+
<div data-testid={testId} className={cn(cardSurface(), "overflow-hidden")}>
|
|
1489
|
+
<div className="flex flex-col gap-4 px-6 py-6">
|
|
1474
1490
|
{header}
|
|
1475
1491
|
{children}
|
|
1476
|
-
</
|
|
1492
|
+
</div>
|
|
1477
1493
|
{actions !== undefined && (
|
|
1478
1494
|
<div
|
|
1479
1495
|
data-testid={testId !== undefined ? `${testId}-actions` : undefined}
|
|
1480
|
-
className=
|
|
1496
|
+
className={cn(cardFooter, cardFooterBorder)}
|
|
1481
1497
|
>
|
|
1482
1498
|
{actions}
|
|
1483
1499
|
</div>
|
|
1484
1500
|
)}
|
|
1485
|
-
</
|
|
1501
|
+
</div>
|
|
1486
1502
|
);
|
|
1487
1503
|
}
|
|
1488
1504
|
|
|
@@ -1564,14 +1580,14 @@ import { ConfigSourceBadge as DefaultConfigSourceBadge } from "../components/con
|
|
|
1564
1580
|
|
|
1565
1581
|
// Generische Card-Chrome (rounded-xl wie die Entity-Card) — slot- + options-
|
|
1566
1582
|
// basiert, damit der Contract additiv wächst und Consumer nie migriert werden.
|
|
1567
|
-
function DefaultCard({ slots, options, className, testId, children }: CardProps): ReactNode {
|
|
1583
|
+
export function DefaultCard({ slots, options, className, testId, children }: CardProps): ReactNode {
|
|
1568
1584
|
const padded = options?.padded ?? true;
|
|
1569
1585
|
const radius = options?.radius ?? "xl";
|
|
1570
1586
|
const footerBordered = options?.footerBordered ?? true;
|
|
1571
1587
|
const s = slots ?? {};
|
|
1572
1588
|
const defaultHeader =
|
|
1573
1589
|
s.title !== undefined || s.subtitle !== undefined || s.headerActions !== undefined ? (
|
|
1574
|
-
<div className="flex flex-wrap items-start justify-between gap-3 px-
|
|
1590
|
+
<div className="flex flex-wrap items-start justify-between gap-3 px-[var(--card-padding)] pt-6 pb-4">
|
|
1575
1591
|
<div className="flex flex-col gap-1">
|
|
1576
1592
|
{s.title !== undefined && (
|
|
1577
1593
|
<h3 className="text-base font-semibold leading-none tracking-tight">{s.title}</h3>
|
|
@@ -1586,30 +1602,26 @@ function DefaultCard({ slots, options, className, testId, children }: CardProps)
|
|
|
1586
1602
|
const header = s.header ?? defaultHeader;
|
|
1587
1603
|
const hasHeader = header !== null && header !== undefined;
|
|
1588
1604
|
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
|
-
>
|
|
1605
|
+
<div data-testid={testId} className={cn(cardSurface({ radius }), "overflow-hidden", className)}>
|
|
1597
1606
|
{header}
|
|
1598
1607
|
{/* != null covers undefined AND explicit null; a `false` child (from
|
|
1599
1608
|
`cond && <El/>`) still renders no visible content either way. */}
|
|
1600
1609
|
{children != null && (
|
|
1601
|
-
<div className={cn("grow", padded && (hasHeader ? "px-6 pb-6" : "p-6"))}>{children}</div>
|
|
1602
|
-
)}
|
|
1603
|
-
{s.footer !== undefined && (
|
|
1604
1610
|
<div
|
|
1605
1611
|
className={cn(
|
|
1606
|
-
"
|
|
1607
|
-
|
|
1612
|
+
"grow",
|
|
1613
|
+
padded &&
|
|
1614
|
+
(hasHeader
|
|
1615
|
+
? "px-[var(--card-padding)] pb-[var(--card-padding)]"
|
|
1616
|
+
: "p-[var(--card-padding)]"),
|
|
1608
1617
|
)}
|
|
1609
1618
|
>
|
|
1610
|
-
{
|
|
1619
|
+
{children}
|
|
1611
1620
|
</div>
|
|
1612
1621
|
)}
|
|
1622
|
+
{s.footer !== undefined && (
|
|
1623
|
+
<div className={cn(cardFooter, footerBordered && cardFooterBorder)}>{s.footer}</div>
|
|
1624
|
+
)}
|
|
1613
1625
|
</div>
|
|
1614
1626
|
);
|
|
1615
1627
|
}
|
|
@@ -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
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -82,6 +82,17 @@
|
|
|
82
82
|
--radius-xl: calc(var(--radius) + 4px);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/* Card-Chrome-Maße — Framework-Defaults. Eine App überschreibt selektiv in
|
|
86
|
+
ihrer eigenen styles.css (@theme oder :root, kommt in der Cascade nach dem
|
|
87
|
+
@import und gewinnt); nicht-überschriebene bleiben auf diesen Werten.
|
|
88
|
+
Plain :root (nicht @theme) → immer emittiert, kein Tree-Shaking. Defaults
|
|
89
|
+
reproduzieren exakt p-6 / rounded-xl / shadow-sm. */
|
|
90
|
+
:root {
|
|
91
|
+
--card-padding: 1.5rem;
|
|
92
|
+
--card-radius: var(--radius-xl);
|
|
93
|
+
--card-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
/* Light-Mode-Overrides — aktiv wenn `<html>` KEINE `.dark` Klasse
|
|
86
97
|
hat. Dark ist der Default (siehe @theme oben); toggled der User
|
|
87
98
|
auf light, greifen diese Werte. */
|