@cosmicdrift/kumiko-renderer-web 0.113.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.113.1",
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.113.1",
20
- "@cosmicdrift/kumiko-headless": "0.113.1",
21
- "@cosmicdrift/kumiko-renderer": "0.113.1",
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";
@@ -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="bg-card overflow-hidden rounded-xl border shadow-sm">
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="flex items-center justify-end gap-2 border-t bg-muted/30 px-6 py-4"
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
- <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">
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
- </CardContent>
1487
+ </div>
1477
1488
  {actions !== undefined && (
1478
1489
  <div
1479
1490
  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"
1491
+ className={cn(cardFooter, cardFooterBorder)}
1481
1492
  >
1482
1493
  {actions}
1483
1494
  </div>
1484
1495
  )}
1485
- </Card>
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
+ }