@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.113.1",
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.113.1",
20
- "@cosmicdrift/kumiko-headless": "0.113.1",
21
- "@cosmicdrift/kumiko-renderer": "0.113.1",
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-6");
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";
@@ -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="bg-card overflow-hidden rounded-xl border shadow-sm">
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="flex items-center justify-end gap-2 border-t bg-muted/30 px-6 py-4"
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
- <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">
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
- </CardContent>
1492
+ </div>
1477
1493
  {actions !== undefined && (
1478
1494
  <div
1479
1495
  data-testid={testId !== undefined ? `${testId}-actions` : undefined}
1480
- className="flex items-center justify-end gap-2 border-t bg-muted/30 px-6 py-4"
1496
+ className={cn(cardFooter, cardFooterBorder)}
1481
1497
  >
1482
1498
  {actions}
1483
1499
  </div>
1484
1500
  )}
1485
- </Card>
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-6 pt-6 pb-4">
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
- "flex items-center justify-end gap-2 px-6 py-4",
1607
- footerBordered && "border-t bg-muted/30",
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
- {s.footer}
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. */