@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 +2 -0
- package/dist/components/Card/Card.d.ts +49 -3
- package/dist/components/Card/Card.d.ts.map +1 -1
- package/dist/components/Card/Card.js +87 -4
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Card/index.d.ts +1 -1
- package/dist/components/Card/index.d.ts.map +1 -1
- package/dist/components/Card/index.js +1 -1
- package/dist/components/Card/index.js.map +1 -1
- package/dist/lib/contexts/OverlayContext/OverlayContext.d.ts +2 -2
- package/dist/lib/contexts/OverlayContext/OverlayContext.d.ts.map +1 -1
- package/dist/lib/contexts/OverlayContext/OverlayContext.js +17 -6
- package/dist/lib/contexts/OverlayContext/OverlayContext.js.map +1 -1
- package/package.json +5 -4
- package/src/components/Card/Card.tsx +102 -16
- package/src/components/Card/index.ts +1 -1
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +21 -10
- package/src/theme/recipes/card.css +6 -1
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
|
-
|
|
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
|
|
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;
|
|
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
|
|
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("
|
|
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;
|
|
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,
|
|
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
|
|
17
|
+
modalRoot?: FloatingPortalProps["root"];
|
|
18
18
|
modalStack: ReturnType<typeof useStack<OverlayStackItem>>;
|
|
19
|
-
toastRoot
|
|
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;
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
40
|
+
modalRoot,
|
|
30
41
|
modalStack,
|
|
31
|
-
toastRoot
|
|
42
|
+
toastRoot,
|
|
32
43
|
toastStack,
|
|
33
|
-
}), [
|
|
34
|
-
return (_jsxs(OverlayContext.Provider, { value: contextValue, children: [children, modals.length > 0 && (_jsx(FloatingPortal, {
|
|
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,
|
|
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.
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
<
|
|
137
|
+
<div className={headerVariants({ size })}>
|
|
52
138
|
{title && <div className="font-semibold">{title}</div>}
|
|
53
139
|
{headerAction && <div className="shrink-0">{headerAction}</div>}
|
|
54
|
-
</
|
|
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
|
|
21
|
+
modalRoot?: FloatingPortalProps["root"];
|
|
22
22
|
modalStack: ReturnType<typeof useStack<OverlayStackItem>>;
|
|
23
|
-
toastRoot
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
69
|
+
modalRoot,
|
|
59
70
|
modalStack,
|
|
60
|
-
toastRoot
|
|
71
|
+
toastRoot,
|
|
61
72
|
toastStack,
|
|
62
73
|
}),
|
|
63
|
-
[
|
|
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
|
|
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
|
|
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
|
|
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
|