@sixthshift/design-system 0.4.0 → 0.5.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/README.md CHANGED
@@ -4,6 +4,8 @@ Personal design system: design tokens, theme pipeline, and a React component lib
4
4
 
5
5
  A single package, `@sixthshift/design-system`: ~80 components (primitives, overlays, forms, pickers, charts, typography) + a JSON-driven token/theme pipeline + Tailwind config. Built on [shadcn/ui](https://ui.shadcn.com) patterns.
6
6
 
7
+ Browse the components: **[sixthshift.github.io/design-system](https://sixthshift.github.io/design-system/)** — Storybook, rebuilt from `main` by `.github/workflows/pages.yml`.
8
+
7
9
  ## Orientation
8
10
 
9
11
  **Owns:** The component library — 80+ exports via subpath imports, design tokens, theme system (JSON-driven CSS generation), Tailwind config, and Storybook stories. Components span primitives, composites, typography, charts, and overlays.
@@ -1,5 +1,44 @@
1
+ import { type VariantProps } from "class-variance-authority";
1
2
  import * as React from "react";
2
- export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
3
+ /**
4
+ * Geometry only, plus the five `--card-*` component tokens.
5
+ *
6
+ * Every colour reads a `--card-*` token whose value is decided by
7
+ * src/theme/recipes/card.css. `size` stays here: it is pure geometry
8
+ * (padding, header gap, corner radius), not colour, so it has no reason to
9
+ * move — the same split Message.tsx documents.
10
+ *
11
+ * The ramp is a density axis, not a padding setting. `md` is the middle and
12
+ * the default at 16px, which is also where MUI's `CardContent` and
13
+ * Bootstrap's `.card-body` sit; two steps exist below it for surfaces that
14
+ * repeat dozens of times, and two above for surfaces that are the page.
15
+ *
16
+ * xs 8px dense data — table-row cards, compact tiles, many at once
17
+ * sm 12px list rows and secondary cards; a line or two of content
18
+ * md 16px a standalone content card. The one to reach for by default
19
+ * lg 24px primary or feature cards — a main panel, a form container
20
+ * xl 32px hero, empty-state and marketing surfaces
21
+ *
22
+ * Steps are 8 · 12 · 16 · 24 · 32, alternating x1.5 and x1.33; 20px is
23
+ * skipped deliberately as the value that breaks that rhythm.
24
+ *
25
+ * Radius scales with padding and never exceeds it — at `xs` a 12px corner
26
+ * against 8px of padding visibly crowds the content. The header gap flattens
27
+ * at both ends instead of tracking padding: it separates a title from an
28
+ * action control, so it has a usability floor and nothing to gain above 16px.
29
+ *
30
+ * Type is deliberately absent from the ramp. Message's `size` carries
31
+ * `text-xs` because Message knows what it holds; Card is a container for
32
+ * arbitrary children, `Heading` and `Body` included, so setting type here
33
+ * would fight the typography ramp at every call site. Containers set
34
+ * spacing; content components set type.
35
+ */
36
+ export declare const cardVariants: (props?: ({
37
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
38
+ interactive?: boolean | null | undefined;
39
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
40
+ export type CardSize = NonNullable<VariantProps<typeof cardVariants>["size"]>;
41
+ export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & Pick<VariantProps<typeof cardVariants>, "size"> & {
3
42
  /** Optional header title - string or ReactNode */
4
43
  title?: React.ReactNode;
5
44
  /** Optional action element displayed on the right side of the header */
@@ -8,7 +47,11 @@ export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
8
47
  /**
9
48
  * Bordered, elevated container — the "this contains data" building block
10
49
  * (see docs/visual-hierarchy.md's Elevated tier). Renders a `<div>`, with an
11
- * optional `<header>` when `title` and/or `headerAction` is passed.
50
+ * optional header row when `title` and/or `headerAction` is passed.
51
+ *
52
+ * `size` is a density axis covering padding, header spacing and corner
53
+ * radius; `md` (16px) is the default. See `cardVariants` above for what each
54
+ * step is for and why type is not part of it.
12
55
  *
13
56
  * Passing `onClick` turns the whole card into a button: it gets
14
57
  * `role="button"`, `tabIndex={0}`, and both Enter and Space activate it, not
@@ -18,7 +61,10 @@ export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
18
61
  * the same seam Button and Badge use — so a consumer can re-point them
19
62
  * without a release.
20
63
  */
21
- export declare const Card: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
64
+ export declare const Card: React.ForwardRefExoticComponent<Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & Pick<VariantProps<(props?: ({
65
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
66
+ interactive?: boolean | null | undefined;
67
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string>, "size"> & {
22
68
  /** Optional header title - string or ReactNode */
23
69
  title?: React.ReactNode;
24
70
  /** Optional action element displayed on the right side of the header */
@@ -1 +1 @@
1
- {"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../../src/components/Card/Card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,GAAG;IAC5E,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,wEAAwE;IACxE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAChC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,IAAI;IAnBf,kDAAkD;YAC1C,KAAK,CAAC,SAAS;IACvB,wEAAwE;mBACzD,KAAK,CAAC,SAAS;wCAmD9B,CAAC"}
1
+ {"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../../src/components/Card/Card.tsx"],"names":[],"mappings":"AACA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,YAAY;;;8EAwBxB,CAAC;AA0BF,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAC,YAAY,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAE9E,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC,GACzE,IAAI,CAAC,YAAY,CAAC,OAAO,YAAY,CAAC,EAAE,MAAM,CAAC,GAAG;IAChD,kDAAkD;IAClD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,wEAAwE;IACxE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAChC,CAAC;AAEJ;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,IAAI;;;;IAvBb,kDAAkD;YAC1C,KAAK,CAAC,SAAS;IACvB,wEAAwE;mBACzD,KAAK,CAAC,SAAS;wCAkDhC,CAAC"}
@@ -1,10 +1,94 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "@sixthshift/design-system/utils";
3
+ import { cva } from "class-variance-authority";
3
4
  import * as React from "react";
5
+ /**
6
+ * Geometry only, plus the five `--card-*` component tokens.
7
+ *
8
+ * Every colour reads a `--card-*` token whose value is decided by
9
+ * src/theme/recipes/card.css. `size` stays here: it is pure geometry
10
+ * (padding, header gap, corner radius), not colour, so it has no reason to
11
+ * move — the same split Message.tsx documents.
12
+ *
13
+ * The ramp is a density axis, not a padding setting. `md` is the middle and
14
+ * the default at 16px, which is also where MUI's `CardContent` and
15
+ * Bootstrap's `.card-body` sit; two steps exist below it for surfaces that
16
+ * repeat dozens of times, and two above for surfaces that are the page.
17
+ *
18
+ * xs 8px dense data — table-row cards, compact tiles, many at once
19
+ * sm 12px list rows and secondary cards; a line or two of content
20
+ * md 16px a standalone content card. The one to reach for by default
21
+ * lg 24px primary or feature cards — a main panel, a form container
22
+ * xl 32px hero, empty-state and marketing surfaces
23
+ *
24
+ * Steps are 8 · 12 · 16 · 24 · 32, alternating x1.5 and x1.33; 20px is
25
+ * skipped deliberately as the value that breaks that rhythm.
26
+ *
27
+ * Radius scales with padding and never exceeds it — at `xs` a 12px corner
28
+ * against 8px of padding visibly crowds the content. The header gap flattens
29
+ * at both ends instead of tracking padding: it separates a title from an
30
+ * action control, so it has a usability floor and nothing to gain above 16px.
31
+ *
32
+ * Type is deliberately absent from the ramp. Message's `size` carries
33
+ * `text-xs` because Message knows what it holds; Card is a container for
34
+ * arbitrary children, `Heading` and `Body` included, so setting type here
35
+ * would fight the typography ramp at every call site. Containers set
36
+ * spacing; content components set type.
37
+ */
38
+ export const cardVariants = cva(
39
+ // One literal, deliberately: `useSortedClasses --unsafe` strips the trailing
40
+ // space before a `+`, silently welding the last class of one fragment to the
41
+ // first of the next. A single string means the sorter has nothing to break.
42
+ "card border-(color:--card-border) border bg-(--card-bg) text-(--card-fg) shadow", {
43
+ variants: {
44
+ size: {
45
+ xs: "rounded-md p-2",
46
+ sm: "rounded-lg p-3",
47
+ md: "rounded-xl p-4",
48
+ lg: "rounded-xl p-6",
49
+ xl: "rounded-2xl p-8",
50
+ },
51
+ interactive: {
52
+ true: "hover:border-(color:--card-border-hovered) focus-visible:outline-(color:--card-ring) cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2",
53
+ false: "",
54
+ },
55
+ },
56
+ defaultVariants: {
57
+ size: "md",
58
+ interactive: false,
59
+ },
60
+ });
61
+ /**
62
+ * Header geometry per size: the gap between title and action, and the space
63
+ * below it.
64
+ *
65
+ * A `<div>`, not a `<header>`. A `<header>` whose nearest sectioning ancestor
66
+ * is the body becomes a `banner` landmark, so two titled cards on one page
67
+ * produced two banners — axe's `landmark-no-duplicate-banner`. A card's header
68
+ * is not the page's banner, and the element bought nothing else here.
69
+ */
70
+ const headerVariants = cva("flex items-start justify-between", {
71
+ variants: {
72
+ size: {
73
+ xs: "mb-2 gap-2",
74
+ sm: "mb-3 gap-2",
75
+ md: "mb-4 gap-3",
76
+ lg: "mb-4 gap-4",
77
+ xl: "mb-6 gap-4",
78
+ },
79
+ },
80
+ defaultVariants: {
81
+ size: "md",
82
+ },
83
+ });
4
84
  /**
5
85
  * Bordered, elevated container — the "this contains data" building block
6
86
  * (see docs/visual-hierarchy.md's Elevated tier). Renders a `<div>`, with an
7
- * optional `<header>` when `title` and/or `headerAction` is passed.
87
+ * optional header row when `title` and/or `headerAction` is passed.
88
+ *
89
+ * `size` is a density axis covering padding, header spacing and corner
90
+ * radius; `md` (16px) is the default. See `cardVariants` above for what each
91
+ * step is for and why type is not part of it.
8
92
  *
9
93
  * Passing `onClick` turns the whole card into a button: it gets
10
94
  * `role="button"`, `tabIndex={0}`, and both Enter and Space activate it, not
@@ -14,7 +98,7 @@ import * as React from "react";
14
98
  * the same seam Button and Badge use — so a consumer can re-point them
15
99
  * without a release.
16
100
  */
17
- export const Card = React.forwardRef(({ className, title, headerAction, children, onClick, onKeyDown, ...props }, ref) => {
101
+ export const Card = React.forwardRef(({ className, size, title, headerAction, children, onClick, onKeyDown, ...props }, ref) => {
18
102
  const isInteractive = !!onClick;
19
103
  const handleKeyDown = (e) => {
20
104
  if (isInteractive && (e.key === "Enter" || e.key === " ")) {
@@ -25,8 +109,7 @@ export const Card = React.forwardRef(({ className, title, headerAction, children
25
109
  };
26
110
  return (
27
111
  // biome-ignore lint/a11y/noStaticElementInteractions: role="button" + tabIndex are added dynamically when onClick is present
28
- _jsxs("div", { ref: ref, className: cn("card border-(color:--card-border) rounded-xl border bg-(--card-bg) p-6 text-(--card-fg) shadow", isInteractive &&
29
- "hover:border-(color:--card-border-hovered) focus-visible:outline-(color:--card-ring) cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2", className), onClick: onClick, onKeyDown: handleKeyDown, ...(isInteractive && { role: "button", tabIndex: 0 }), ...props, children: [(title || headerAction) && (_jsxs("header", { className: "mb-4 flex items-start justify-between gap-4", children: [title && _jsx("div", { className: "font-semibold", children: title }), headerAction && _jsx("div", { className: "shrink-0", children: headerAction })] })), children] }));
112
+ _jsxs("div", { ref: ref, className: cn(cardVariants({ size, interactive: isInteractive }), className), onClick: onClick, onKeyDown: handleKeyDown, ...(isInteractive && { role: "button", tabIndex: 0 }), ...props, children: [(title || headerAction) && (_jsxs("div", { className: headerVariants({ size }), children: [title && _jsx("div", { className: "font-semibold", children: title }), headerAction && _jsx("div", { className: "shrink-0", children: headerAction })] })), children] }));
30
113
  });
31
114
  Card.displayName = "Card";
32
115
  //# sourceMappingURL=Card.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Card.js","sourceRoot":"","sources":["../../../src/components/Card/Card.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,iCAAiC,CAAC;AACrD,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAA4B,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE;IAClJ,MAAM,aAAa,GAAG,CAAC,CAAC,OAAO,CAAC;IAEhC,MAAM,aAAa,GAAG,CAAC,CAAsC,EAAE,EAAE;QAC/D,IAAI,aAAa,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1D,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC,CAAgD,CAAC,CAAC;QAC9D,CAAC;QACD,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO;IACL,6HAA6H;IAC7H,eACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,EAAE,CACX,gGAAgG,EAChG,aAAa;YACX,8KAA8K,EAChL,SAAS,CACV,EACD,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,aAAa,KACpB,CAAC,aAAa,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,KAClD,KAAK,aAER,CAAC,KAAK,IAAI,YAAY,CAAC,IAAI,CAC1B,kBAAQ,SAAS,EAAC,6CAA6C,aAC5D,KAAK,IAAI,cAAK,SAAS,EAAC,eAAe,YAAE,KAAK,GAAO,EACrD,YAAY,IAAI,cAAK,SAAS,EAAC,UAAU,YAAE,YAAY,GAAO,IACxD,CACV,EACA,QAAQ,IACL,CACP,CAAC;AACJ,CAAC,CAAC,CAAC;AACH,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC"}
1
+ {"version":3,"file":"Card.js","sourceRoot":"","sources":["../../../src/components/Card/Card.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,iCAAiC,CAAC;AACrD,OAAO,EAAE,GAAG,EAAqB,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG;AAC7B,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,iFAAiF,EACjF;IACE,QAAQ,EAAE;QACR,IAAI,EAAE;YACJ,EAAE,EAAE,gBAAgB;YACpB,EAAE,EAAE,gBAAgB;YACpB,EAAE,EAAE,gBAAgB;YACpB,EAAE,EAAE,gBAAgB;YACpB,EAAE,EAAE,iBAAiB;SACtB;QACD,WAAW,EAAE;YACX,IAAI,EAAE,8KAA8K;YACpL,KAAK,EAAE,EAAE;SACV;KACF;IACD,eAAe,EAAE;QACf,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,KAAK;KACnB;CACF,CACF,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,GAAG,CAAC,kCAAkC,EAAE;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE;YACJ,EAAE,EAAE,YAAY;YAChB,EAAE,EAAE,YAAY;YAChB,EAAE,EAAE,YAAY;YAChB,EAAE,EAAE,YAAY;YAChB,EAAE,EAAE,YAAY;SACjB;KACF;IACD,eAAe,EAAE;QACf,IAAI,EAAE,IAAI;KACX;CACF,CAAC,CAAC;AAYH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAA4B,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE;IACxJ,MAAM,aAAa,GAAG,CAAC,CAAC,OAAO,CAAC;IAEhC,MAAM,aAAa,GAAG,CAAC,CAAsC,EAAE,EAAE;QAC/D,IAAI,aAAa,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;YAC1D,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC,CAAgD,CAAC,CAAC;QAC9D,CAAC;QACD,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,CAAC;IAEF,OAAO;IACL,6HAA6H;IAC7H,eACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC,EAAE,SAAS,CAAC,EAC5E,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,aAAa,KACpB,CAAC,aAAa,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,KAClD,KAAK,aAER,CAAC,KAAK,IAAI,YAAY,CAAC,IAAI,CAC1B,eAAK,SAAS,EAAE,cAAc,CAAC,EAAE,IAAI,EAAE,CAAC,aACrC,KAAK,IAAI,cAAK,SAAS,EAAC,eAAe,YAAE,KAAK,GAAO,EACrD,YAAY,IAAI,cAAK,SAAS,EAAC,UAAU,YAAE,YAAY,GAAO,IAC3D,CACP,EACA,QAAQ,IACL,CACP,CAAC;AACJ,CAAC,CAAC,CAAC;AACH,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC"}
@@ -1,2 +1,2 @@
1
- export { Card, type CardProps } from "./Card.js";
1
+ export { Card, type CardProps, type CardSize, cardVariants } from "./Card.js";
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Card/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Card/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC"}
@@ -1,2 +1,2 @@
1
- export { Card } from "./Card.js";
1
+ export { Card, cardVariants } from "./Card.js";
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Card/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAkB,MAAM,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Card/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAiC,YAAY,EAAE,MAAM,QAAQ,CAAC"}
@@ -14,9 +14,9 @@ type OverlayStackItem = StackItem & {
14
14
  onClose?: (() => void) | undefined;
15
15
  };
16
16
  type OverlayContextType = {
17
- modalRoot: FloatingPortalProps["root"];
17
+ modalRoot?: FloatingPortalProps["root"];
18
18
  modalStack: ReturnType<typeof useStack<OverlayStackItem>>;
19
- toastRoot: FloatingPortalProps["root"];
19
+ toastRoot?: FloatingPortalProps["root"];
20
20
  toastStack: ReturnType<typeof useStack<OverlayStackItem>>;
21
21
  };
22
22
  export declare const useOverlayContext: () => OverlayContextType;
@@ -1 +1 @@
1
- {"version":3,"file":"OverlayContext.d.ts","sourceRoot":"","sources":["../../../../src/lib/contexts/OverlayContext/OverlayContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,KAAK,SAAS,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAiB,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAkC,MAAM,OAAO,CAAC;AAEtH,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;CACrC,CAAC;AAEF,sEAAsE;AACtE,KAAK,gBAAgB,GAAG,SAAS,GAAG;IAElC,SAAS,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAClC,qDAAqD;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4CAA4C;IAC5C,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,SAAS,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACvC,UAAU,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC1D,SAAS,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACvC,UAAU,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC;CAC3D,CAAC;AAGF,eAAO,MAAM,iBAAiB,0BAAmC,CAAC;AAElE,eAAO,MAAM,eAAe,qDAAsD,iBAAiB,CAAC,mBAAmB,CAAC,gCA+DvH,CAAC"}
1
+ {"version":3,"file":"OverlayContext.d.ts","sourceRoot":"","sources":["../../../../src/lib/contexts/OverlayContext/OverlayContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,KAAK,SAAS,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAiB,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAkC,MAAM,OAAO,CAAC;AAEtH,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;CACrC,CAAC;AAEF,sEAAsE;AACtE,KAAK,gBAAgB,GAAG,SAAS,GAAG;IAElC,SAAS,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAClC,qDAAqD;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4CAA4C;IAC5C,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CACpC,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,SAAS,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC;IAC1D,SAAS,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,gBAAgB,CAAC,CAAC,CAAC;CAC3D,CAAC;AAWF,eAAO,MAAM,iBAAiB,0BAAmC,CAAC;AAElE,eAAO,MAAM,eAAe,qDAAsD,iBAAiB,CAAC,mBAAmB,CAAC,gCAkEvH,CAAC"}
@@ -2,11 +2,22 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { FloatingPortal } from "@floating-ui/react";
3
3
  import { useStack } from "@sixthshift/design-system/hooks";
4
4
  import { createContext, useContext, useEffect, useMemo } from "react";
5
+ /**
6
+ * `root` omitted when the caller gave none, rather than passed as `undefined`
7
+ * or `null`. The distinction is load-bearing: `FloatingPortal` bails out of
8
+ * creating its portal node entirely on an explicit `null` ("wait for the root
9
+ * to exist"), and only a missing `root` falls through to `document.body`.
10
+ */
11
+ const rootProp = (root) => (root === undefined ? {} : { root });
5
12
  const OverlayContext = createContext(undefined);
6
13
  export const useOverlayContext = () => useContext(OverlayContext);
7
14
  export const OverlayProvider = ({ modal: modalRoot, toast: toastRoot, children }) => {
8
- const resolvedModalRoot = modalRoot ?? document.body;
9
- const resolvedToastRoot = toastRoot ?? document.body;
15
+ // The roots pass through unresolved, deliberately. Defaulting them to
16
+ // `document.body` here read the DOM during render, which threw the whole
17
+ // tree under SSR — and it bought nothing: `FloatingPortal` already falls
18
+ // back to `document.body` when no `root` is given, on the client, where a
19
+ // body exists. Both portals below are behind a non-empty stack, and both
20
+ // stacks start empty, so nothing portals on the server either way.
10
21
  const modalStack = useStack([]);
11
22
  const toastStack = useStack([]);
12
23
  const [modals] = modalStack;
@@ -26,11 +37,11 @@ export const OverlayProvider = ({ modal: modalRoot, toast: toastRoot, children }
26
37
  }, [modals]);
27
38
  // Memoize context value to prevent unnecessary re-renders in consumers
28
39
  const contextValue = useMemo(() => ({
29
- modalRoot: resolvedModalRoot,
40
+ modalRoot,
30
41
  modalStack,
31
- toastRoot: resolvedToastRoot,
42
+ toastRoot,
32
43
  toastStack,
33
- }), [resolvedModalRoot, modalStack, resolvedToastRoot, toastStack]);
34
- return (_jsxs(OverlayContext.Provider, { value: contextValue, children: [children, modals.length > 0 && (_jsx(FloatingPortal, { root: resolvedModalRoot, children: modals.map(({ id, component: Component, data }) => (_jsx(Component, { data: data }, id))) })), toasts.length > 0 && (_jsx(FloatingPortal, { root: resolvedToastRoot, children: _jsx("div", { className: "pointer-events-none fixed bottom-6 left-1/2 z-toast flex -translate-x-1/2 flex-col-reverse items-center gap-3", children: toasts.map(({ id, component: Component, onClose, ...props }) => (_jsx("div", { className: "pointer-events-auto", children: _jsx(Component, { onClose: onClose, ...props }) }, id))) }) }))] }));
44
+ }), [modalRoot, modalStack, toastRoot, toastStack]);
45
+ return (_jsxs(OverlayContext.Provider, { value: contextValue, children: [children, modals.length > 0 && (_jsx(FloatingPortal, { ...rootProp(modalRoot), children: modals.map(({ id, component: Component, data }) => (_jsx(Component, { data: data }, id))) })), toasts.length > 0 && (_jsx(FloatingPortal, { ...rootProp(toastRoot), children: _jsx("div", { className: "pointer-events-none fixed bottom-6 left-1/2 z-toast flex -translate-x-1/2 flex-col-reverse items-center gap-3", children: toasts.map(({ id, component: Component, onClose, ...props }) => (_jsx("div", { className: "pointer-events-auto", children: _jsx(Component, { onClose: onClose, ...props }) }, id))) }) }))] }));
35
46
  };
36
47
  //# sourceMappingURL=OverlayContext.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"OverlayContext.js","sourceRoot":"","sources":["../../../../src/lib/contexts/OverlayContext/OverlayContext.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAA4B,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAkB,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAkD,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAwBtH,MAAM,cAAc,GAAG,aAAa,CAAqB,SAA0C,CAAC,CAAC;AACrG,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAA0C,EAAE,EAAE;IAC1H,MAAM,iBAAiB,GAAG,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC;IACrD,MAAM,iBAAiB,GAAG,SAAS,IAAI,QAAQ,CAAC,IAAI,CAAC;IAErD,MAAM,UAAU,GAAG,QAAQ,CAAmB,EAAE,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,QAAQ,CAAmB,EAAE,CAAC,CAAC;IAElD,MAAM,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC;IAC5B,MAAM,CAAC,MAAM,CAAC,GAAG,UAAU,CAAC;IAE5B,mDAAmD;IACnD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAEhC,MAAM,YAAY,GAAG,CAAC,KAAoB,EAAE,EAAE;YAC5C,IAAI,KAAK,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/B,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC;YACxB,CAAC;QACH,CAAC,CAAC;QAEF,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QACnD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACrE,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,uEAAuE;IACvE,MAAM,YAAY,GAAG,OAAO,CAC1B,GAAG,EAAE,CAAC,CAAC;QACL,SAAS,EAAE,iBAAiB;QAC5B,UAAU;QACV,SAAS,EAAE,iBAAiB;QAC5B,UAAU;KACX,CAAC,EACF,CAAC,iBAAiB,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,CAAC,CAC/D,CAAC;IAEF,OAAO,CACL,MAAC,cAAc,CAAC,QAAQ,IAAC,KAAK,EAAE,YAAY,aACzC,QAAQ,EAGR,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CACpB,KAAC,cAAc,IAAC,IAAI,EAAE,iBAAiB,YACpC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAClD,KAAC,SAAS,IAAU,IAAI,EAAE,IAAI,IAAd,EAAE,CAAgB,CACnC,CAAC,GACa,CAClB,EAGA,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CACpB,KAAC,cAAc,IAAC,IAAI,EAAE,iBAAiB,YACrC,cAAK,SAAS,EAAC,+GAA+G,YAC3H,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,CAAC,CAC/D,cAAc,SAAS,EAAC,qBAAqB,YAC3C,KAAC,SAAS,IAAC,OAAO,EAAE,OAAO,KAAM,KAAK,GAAI,IADlC,EAAE,CAEN,CACP,CAAC,GACE,GACS,CAClB,IACuB,CAC3B,CAAC;AACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"OverlayContext.js","sourceRoot":"","sources":["../../../../src/lib/contexts/OverlayContext/OverlayContext.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixthshift/design-system",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Personal design system - tokens, theme, and component library",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -322,9 +322,10 @@
322
322
  "build:js": "tsc -p tsconfig.build.json && bun run scripts/fix-extensions.ts",
323
323
  "dev": "storybook dev -p 6006 --no-open",
324
324
  "build:storybook": "storybook build",
325
- "test": "vitest run --project unit --project date-time --reporter=dot",
326
- "test:verbose": "vitest run --project unit --project date-time",
327
- "test:watch": "vitest --project unit --project date-time",
325
+ "test": "vitest run --project unit --project date-time --project ssr --reporter=dot",
326
+ "test:verbose": "vitest run --project unit --project date-time --project ssr",
327
+ "test:watch": "vitest --project unit --project date-time --project ssr",
328
+ "test:ssr": "vitest run --project ssr",
328
329
  "test:visual": "vitest run --project visual",
329
330
  "test:visual:update": "vitest run --project visual --update",
330
331
  "test:stories": "vitest run --project storybook --project storybook-dark",
@@ -1,17 +1,108 @@
1
1
  import { cn } from "@sixthshift/design-system/utils";
2
+ import { cva, type VariantProps } from "class-variance-authority";
2
3
  import * as React from "react";
3
4
 
4
- export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
5
- /** Optional header title - string or ReactNode */
6
- title?: React.ReactNode;
7
- /** Optional action element displayed on the right side of the header */
8
- headerAction?: React.ReactNode;
9
- };
5
+ /**
6
+ * Geometry only, plus the five `--card-*` component tokens.
7
+ *
8
+ * Every colour reads a `--card-*` token whose value is decided by
9
+ * src/theme/recipes/card.css. `size` stays here: it is pure geometry
10
+ * (padding, header gap, corner radius), not colour, so it has no reason to
11
+ * move — the same split Message.tsx documents.
12
+ *
13
+ * The ramp is a density axis, not a padding setting. `md` is the middle and
14
+ * the default at 16px, which is also where MUI's `CardContent` and
15
+ * Bootstrap's `.card-body` sit; two steps exist below it for surfaces that
16
+ * repeat dozens of times, and two above for surfaces that are the page.
17
+ *
18
+ * xs 8px dense data — table-row cards, compact tiles, many at once
19
+ * sm 12px list rows and secondary cards; a line or two of content
20
+ * md 16px a standalone content card. The one to reach for by default
21
+ * lg 24px primary or feature cards — a main panel, a form container
22
+ * xl 32px hero, empty-state and marketing surfaces
23
+ *
24
+ * Steps are 8 · 12 · 16 · 24 · 32, alternating x1.5 and x1.33; 20px is
25
+ * skipped deliberately as the value that breaks that rhythm.
26
+ *
27
+ * Radius scales with padding and never exceeds it — at `xs` a 12px corner
28
+ * against 8px of padding visibly crowds the content. The header gap flattens
29
+ * at both ends instead of tracking padding: it separates a title from an
30
+ * action control, so it has a usability floor and nothing to gain above 16px.
31
+ *
32
+ * Type is deliberately absent from the ramp. Message's `size` carries
33
+ * `text-xs` because Message knows what it holds; Card is a container for
34
+ * arbitrary children, `Heading` and `Body` included, so setting type here
35
+ * would fight the typography ramp at every call site. Containers set
36
+ * spacing; content components set type.
37
+ */
38
+ export const cardVariants = cva(
39
+ // One literal, deliberately: `useSortedClasses --unsafe` strips the trailing
40
+ // space before a `+`, silently welding the last class of one fragment to the
41
+ // first of the next. A single string means the sorter has nothing to break.
42
+ "card border-(color:--card-border) border bg-(--card-bg) text-(--card-fg) shadow",
43
+ {
44
+ variants: {
45
+ size: {
46
+ xs: "rounded-md p-2",
47
+ sm: "rounded-lg p-3",
48
+ md: "rounded-xl p-4",
49
+ lg: "rounded-xl p-6",
50
+ xl: "rounded-2xl p-8",
51
+ },
52
+ interactive: {
53
+ true: "hover:border-(color:--card-border-hovered) focus-visible:outline-(color:--card-ring) cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2",
54
+ false: "",
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ size: "md",
59
+ interactive: false,
60
+ },
61
+ }
62
+ );
63
+
64
+ /**
65
+ * Header geometry per size: the gap between title and action, and the space
66
+ * below it.
67
+ *
68
+ * A `<div>`, not a `<header>`. A `<header>` whose nearest sectioning ancestor
69
+ * is the body becomes a `banner` landmark, so two titled cards on one page
70
+ * produced two banners — axe's `landmark-no-duplicate-banner`. A card's header
71
+ * is not the page's banner, and the element bought nothing else here.
72
+ */
73
+ const headerVariants = cva("flex items-start justify-between", {
74
+ variants: {
75
+ size: {
76
+ xs: "mb-2 gap-2",
77
+ sm: "mb-3 gap-2",
78
+ md: "mb-4 gap-3",
79
+ lg: "mb-4 gap-4",
80
+ xl: "mb-6 gap-4",
81
+ },
82
+ },
83
+ defaultVariants: {
84
+ size: "md",
85
+ },
86
+ });
87
+
88
+ export type CardSize = NonNullable<VariantProps<typeof cardVariants>["size"]>;
89
+
90
+ export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> &
91
+ Pick<VariantProps<typeof cardVariants>, "size"> & {
92
+ /** Optional header title - string or ReactNode */
93
+ title?: React.ReactNode;
94
+ /** Optional action element displayed on the right side of the header */
95
+ headerAction?: React.ReactNode;
96
+ };
10
97
 
11
98
  /**
12
99
  * Bordered, elevated container — the "this contains data" building block
13
100
  * (see docs/visual-hierarchy.md's Elevated tier). Renders a `<div>`, with an
14
- * optional `<header>` when `title` and/or `headerAction` is passed.
101
+ * optional header row when `title` and/or `headerAction` is passed.
102
+ *
103
+ * `size` is a density axis covering padding, header spacing and corner
104
+ * radius; `md` (16px) is the default. See `cardVariants` above for what each
105
+ * step is for and why type is not part of it.
15
106
  *
16
107
  * Passing `onClick` turns the whole card into a button: it gets
17
108
  * `role="button"`, `tabIndex={0}`, and both Enter and Space activate it, not
@@ -21,7 +112,7 @@ export type CardProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title"> & {
21
112
  * the same seam Button and Badge use — so a consumer can re-point them
22
113
  * without a release.
23
114
  */
24
- export const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, title, headerAction, children, onClick, onKeyDown, ...props }, ref) => {
115
+ export const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, size, title, headerAction, children, onClick, onKeyDown, ...props }, ref) => {
25
116
  const isInteractive = !!onClick;
26
117
 
27
118
  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
@@ -36,22 +127,17 @@ export const Card = React.forwardRef<HTMLDivElement, CardProps>(({ className, ti
36
127
  // biome-ignore lint/a11y/noStaticElementInteractions: role="button" + tabIndex are added dynamically when onClick is present
37
128
  <div
38
129
  ref={ref}
39
- className={cn(
40
- "card border-(color:--card-border) rounded-xl border bg-(--card-bg) p-6 text-(--card-fg) shadow",
41
- isInteractive &&
42
- "hover:border-(color:--card-border-hovered) focus-visible:outline-(color:--card-ring) cursor-pointer transition-colors focus-visible:outline-2 focus-visible:outline-offset-2",
43
- className
44
- )}
130
+ className={cn(cardVariants({ size, interactive: isInteractive }), className)}
45
131
  onClick={onClick}
46
132
  onKeyDown={handleKeyDown}
47
133
  {...(isInteractive && { role: "button", tabIndex: 0 })}
48
134
  {...props}
49
135
  >
50
136
  {(title || headerAction) && (
51
- <header className="mb-4 flex items-start justify-between gap-4">
137
+ <div className={headerVariants({ size })}>
52
138
  {title && <div className="font-semibold">{title}</div>}
53
139
  {headerAction && <div className="shrink-0">{headerAction}</div>}
54
- </header>
140
+ </div>
55
141
  )}
56
142
  {children}
57
143
  </div>
@@ -1 +1 @@
1
- export { Card, type CardProps } from "./Card";
1
+ export { Card, type CardProps, type CardSize, cardVariants } from "./Card";
@@ -18,19 +18,30 @@ type OverlayStackItem = StackItem & {
18
18
  };
19
19
 
20
20
  type OverlayContextType = {
21
- modalRoot: FloatingPortalProps["root"];
21
+ modalRoot?: FloatingPortalProps["root"];
22
22
  modalStack: ReturnType<typeof useStack<OverlayStackItem>>;
23
- toastRoot: FloatingPortalProps["root"];
23
+ toastRoot?: FloatingPortalProps["root"];
24
24
  toastStack: ReturnType<typeof useStack<OverlayStackItem>>;
25
25
  };
26
26
 
27
+ /**
28
+ * `root` omitted when the caller gave none, rather than passed as `undefined`
29
+ * or `null`. The distinction is load-bearing: `FloatingPortal` bails out of
30
+ * creating its portal node entirely on an explicit `null` ("wait for the root
31
+ * to exist"), and only a missing `root` falls through to `document.body`.
32
+ */
33
+ const rootProp = (root: FloatingPortalProps["root"] | undefined) => (root === undefined ? {} : { root });
34
+
27
35
  const OverlayContext = createContext<OverlayContextType>(undefined as unknown as OverlayContextType);
28
36
  export const useOverlayContext = () => useContext(OverlayContext);
29
37
 
30
38
  export const OverlayProvider = ({ modal: modalRoot, toast: toastRoot, children }: PropsWithChildren<OverlayContextProps>) => {
31
- const resolvedModalRoot = modalRoot ?? document.body;
32
- const resolvedToastRoot = toastRoot ?? document.body;
33
-
39
+ // The roots pass through unresolved, deliberately. Defaulting them to
40
+ // `document.body` here read the DOM during render, which threw the whole
41
+ // tree under SSR — and it bought nothing: `FloatingPortal` already falls
42
+ // back to `document.body` when no `root` is given, on the client, where a
43
+ // body exists. Both portals below are behind a non-empty stack, and both
44
+ // stacks start empty, so nothing portals on the server either way.
34
45
  const modalStack = useStack<OverlayStackItem>([]);
35
46
  const toastStack = useStack<OverlayStackItem>([]);
36
47
 
@@ -55,12 +66,12 @@ export const OverlayProvider = ({ modal: modalRoot, toast: toastRoot, children }
55
66
  // Memoize context value to prevent unnecessary re-renders in consumers
56
67
  const contextValue = useMemo(
57
68
  () => ({
58
- modalRoot: resolvedModalRoot,
69
+ modalRoot,
59
70
  modalStack,
60
- toastRoot: resolvedToastRoot,
71
+ toastRoot,
61
72
  toastStack,
62
73
  }),
63
- [resolvedModalRoot, modalStack, resolvedToastRoot, toastStack]
74
+ [modalRoot, modalStack, toastRoot, toastStack]
64
75
  );
65
76
 
66
77
  return (
@@ -69,7 +80,7 @@ export const OverlayProvider = ({ modal: modalRoot, toast: toastRoot, children }
69
80
 
70
81
  {/* Modal stack - rendered in portal */}
71
82
  {modals.length > 0 && (
72
- <FloatingPortal root={resolvedModalRoot}>
83
+ <FloatingPortal {...rootProp(modalRoot)}>
73
84
  {modals.map(({ id, component: Component, data }) => (
74
85
  <Component key={id} data={data} />
75
86
  ))}
@@ -78,7 +89,7 @@ export const OverlayProvider = ({ modal: modalRoot, toast: toastRoot, children }
78
89
 
79
90
  {/* Toast stack - rendered in portal */}
80
91
  {toasts.length > 0 && (
81
- <FloatingPortal root={resolvedToastRoot}>
92
+ <FloatingPortal {...rootProp(toastRoot)}>
82
93
  <div className="pointer-events-none fixed bottom-6 left-1/2 z-toast flex -translate-x-1/2 flex-col-reverse items-center gap-3">
83
94
  {toasts.map(({ id, component: Component, onClose, ...props }) => (
84
95
  <div key={id} className="pointer-events-auto">
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Card recipe — the component-token layer for Card.
3
3
  *
4
- * Card has no variant/intent axis — every instance uses the same fixed
4
+ * Card has no colour-bearing variant — every instance uses the same fixed
5
5
  * semantic tokens the old plain class string named directly. This file just
6
6
  * gives those tokens a name, so a consumer can re-point a cell or scope a
7
7
  * change to a subtree without a release.
8
8
  *
9
+ * Card's `size` axis is pure geometry (padding, header gap, radius) and stays
10
+ * in Card.tsx's `cva` call; nothing here concerns it. Same split as
11
+ * message.css — this layer is colour, and a consumer wanting a different
12
+ * spacing rhythm re-points Tailwind's `--spacing`, not a token here.
13
+ *
9
14
  * The grammar is `--card-{context}[-{state}]`, context in {bg, fg, border,
10
15
  * ring} — the same words the semantic layer uses. `--card-ring` backs the
11
16
  * `outline-*` utilities on the interactive (clickable) variant: it plays the