@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.114.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.114.0",
20
- "@cosmicdrift/kumiko-headless": "0.114.0",
21
- "@cosmicdrift/kumiko-renderer": "0.114.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 { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
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, DefaultCard as Card, defaultPrimitives } from "./primitives";
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";
@@ -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>;
@@ -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={cn(cardSurface(), "overflow-hidden")}>
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={cn(cardFooter, cardFooterBorder)}
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
- <div data-testid={testId} className={cn(cardSurface(), "overflow-hidden")}>
1484
- <div className="flex flex-col gap-4 px-6 py-6">
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
- </div>
1476
+ </CardContent>
1488
1477
  {actions !== undefined && (
1489
1478
  <div
1490
1479
  data-testid={testId !== undefined ? `${testId}-actions` : undefined}
1491
- className={cn(cardFooter, cardFooterBorder)}
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
- </div>
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
- export function DefaultCard({ slots, options, className, testId, children }: CardProps): ReactNode {
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 data-testid={testId} className={cn(cardSurface({ radius }), "overflow-hidden", className)}>
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 className={cn(cardFooter, footerBordered && cardFooterBorder)}>{s.footer}</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
- }