@varialkit/card 0.1.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/docs.md ADDED
@@ -0,0 +1,73 @@
1
+ # Card
2
+
3
+ Cards are flexible containers for grouping related content, headers, and actions. They support optional headers,
4
+ padding control, and visual variants like borders, shadows, and hover elevation.
5
+
6
+ ## Usage
7
+
8
+ ```tsx
9
+ import { Card } from "@solara/card";
10
+
11
+ export function Example() {
12
+ return (
13
+ <Card title="Summary" headerActions={<button type="button">Edit</button>}>
14
+ <p>Cards keep content grouped and readable.</p>
15
+ </Card>
16
+ );
17
+ }
18
+ ```
19
+
20
+ ## Props
21
+
22
+ | Prop | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | `title` | `React.ReactNode` | — | Optional header title. |
25
+ | `headerActions` | `React.ReactNode` | — | Actions rendered on the right side of the header. |
26
+ | `bordered` | `boolean` | `false` | Adds a border. |
27
+ | `shadow` | `boolean \| '1' \| '2' \| '3' \| '4'` | `true` | The elevation shadow of the card. If `true`, it defaults to level `1`. |
28
+ | `hoverable` | `boolean` | `false` | Adds hover elevation. |
29
+ | `size` | `"small" | "medium" | "large"` | `"medium"` | Controls header sizing. |
30
+ | `padding` | `"none" | "small" | "medium" | "large"` | `"medium"` | Controls body padding. |
31
+ | `fill` | `boolean` | `false` | Forces the card content to fill available height. |
32
+ | `selected` | `boolean` | `false` | Adds a visual indicator for the selected state. |
33
+ | `dragging` | `boolean` | `false` | Adds a visual indicator for the dragging state. |
34
+ | `radius` | `"none" | "small" | "medium" | "large" | "xlarge"` | `--radius-2` scaled by `--radius-multiplier` | Controls the border radius. When omitted, Card follows the generic radius scale. |
35
+ | `surface` | `"0" | "100" | "200" | "300" | "400" | "500"` | `"0"` | Controls the background color. |
36
+
37
+ Card also supports `Card.Header`, `Card.Title`, and `Card.Content` for custom layouts.
38
+
39
+ ## Selected and Dragging States
40
+
41
+ The `Card` component can be used to indicate a selected or dragging state. These states can be controlled by passing the `selected` or `dragging` props.
42
+
43
+ ```tsx
44
+ import { Card } from "@solara/card";
45
+ import { useState } from "react";
46
+
47
+ export function DraggableCard() {
48
+ const [isSelected, setIsSelected] = useState(false);
49
+ const [isDragging, setIsDragging] = useState(false);
50
+
51
+ const handlePointerDown = () => {
52
+ setIsDragging(true);
53
+ };
54
+
55
+ const handlePointerUp = () => {
56
+ setIsDragging(false);
57
+ };
58
+
59
+ return (
60
+ <Card
61
+ title="Draggable Card"
62
+ selected={isSelected}
63
+ dragging={isDragging}
64
+ onClick={() => setIsSelected(!isSelected)}
65
+ onPointerDown={handlePointerDown}
66
+ onPointerUp={handlePointerUp}
67
+ hoverable
68
+ >
69
+ <p>This card can be dragged and selected.</p>
70
+ </Card>
71
+ );
72
+ }
73
+ ```
package/examples.tsx ADDED
@@ -0,0 +1,216 @@
1
+ import React from "react";
2
+ import { Card } from "./src/Card";
3
+ import { Button } from "../button/src/Button";
4
+ import type { CardPadding, CardRadius, CardSize, CardSurface } from "./src/Card.types";
5
+
6
+ export const stories = {
7
+ playground: {
8
+ title: "Playground",
9
+ description: "Tweak the props to explore the Card API.",
10
+ render: (props: {
11
+ title?: string;
12
+ bordered?: boolean;
13
+ shadow?: "true" | "false" | "1" | "2" | "3" | "4";
14
+ hoverable?: boolean;
15
+ size?: CardSize;
16
+ padding?: CardPadding;
17
+ fill?: boolean;
18
+ showActions?: boolean;
19
+ selected?: boolean;
20
+ dragging?: boolean;
21
+ radius?: CardRadius | "theme";
22
+ surface?: CardSurface;
23
+ }) => (
24
+ <Card
25
+ title={props.title || "Card Title"}
26
+ headerActions={
27
+ props.showActions ? (
28
+ <Button label="Action" />
29
+ ) : null
30
+ }
31
+ bordered={props.bordered}
32
+ shadow={
33
+ props.shadow === "true"
34
+ ? true
35
+ : props.shadow === "false"
36
+ ? false
37
+ : props.shadow
38
+ }
39
+ hoverable={props.hoverable}
40
+ size={props.size}
41
+ padding={props.padding}
42
+ fill={props.fill}
43
+ selected={props.selected}
44
+ dragging={props.dragging}
45
+ radius={props.radius === "theme" ? undefined : props.radius}
46
+ surface={props.surface}
47
+ style={{ maxWidth: "480px" }}
48
+ >
49
+ <p>
50
+ Cards are flexible containers for grouping related content and actions.
51
+ </p>
52
+ </Card>
53
+ ),
54
+ controls: [
55
+ { name: "title", type: "text", label: "Title" },
56
+ { name: "showActions", type: "boolean", label: "Header Actions" },
57
+ {
58
+ name: "size",
59
+ type: "select",
60
+ options: ["small", "medium", "large"],
61
+ },
62
+ {
63
+ name: "padding",
64
+ type: "select",
65
+ options: ["none", "small", "medium", "large"],
66
+ },
67
+ { name: "bordered", type: "boolean" },
68
+ {
69
+ name: "shadow",
70
+ type: "select",
71
+ options: ["false", "true", "1", "2", "3", "4"],
72
+ },
73
+ { name: "hoverable", type: "boolean" },
74
+ { name: "fill", type: "boolean" },
75
+ { name: "selected", type: "boolean" },
76
+ { name: "dragging", type: "boolean" },
77
+ {
78
+ name: "radius",
79
+ type: "select",
80
+ options: ["theme", "none", "small", "medium", "large", "xlarge"],
81
+ },
82
+ {
83
+ name: "surface",
84
+ type: "select",
85
+ options: ["0", "100", "200", "300", "400", "500"],
86
+ },
87
+ ],
88
+ initialProps: {
89
+ title: "Card Title",
90
+ showActions: true,
91
+ size: "medium",
92
+ padding: "medium",
93
+ bordered: false,
94
+ shadow: true,
95
+ hoverable: false,
96
+ fill: false,
97
+ selected: false,
98
+ dragging: false,
99
+ radius: "theme",
100
+ surface: "0",
101
+ },
102
+ },
103
+ simple: {
104
+ title: "Simple",
105
+ description: "A basic card with a header and content.",
106
+ showProps: false,
107
+ render: () => (
108
+ <Card title="Summary" style={{ maxWidth: "420px" }}>
109
+ <p>Use cards to group related information.</p>
110
+ </Card>
111
+ ),
112
+ code: `import { Card } from "@solara/card";
113
+
114
+ export function Example() {
115
+ return (
116
+ <Card title="Summary" style={{ maxWidth: "420px" }}>
117
+ <p>Use cards to group related information.</p>
118
+ </Card>
119
+ );
120
+ }
121
+ `,
122
+ },
123
+ sections: {
124
+ title: "Sections",
125
+ description: "Use the compound components for custom layouts.",
126
+ showProps: false,
127
+ render: () => (
128
+ <Card style={{ maxWidth: "520px" }}>
129
+ <Card.Header actions={<Button label="Edit" />}>
130
+ <Card.Title>Project Health</Card.Title>
131
+ </Card.Header>
132
+ <Card.Content padding="medium">
133
+ <p>All systems operational.</p>
134
+ </Card.Content>
135
+ </Card>
136
+ ),
137
+ code: `import { Card } from "@solara/card";
138
+ import { Button } from "@solara/button";
139
+
140
+ export function Example() {
141
+ return (
142
+ <Card style={{ maxWidth: "520px" }}>
143
+ <Card.Header actions={<Button label="Edit" />}>
144
+ <Card.Title>Project Health</Card.Title>
145
+ </Card.Header>
146
+ <Card.Content padding="medium">
147
+ <p>All systems operational.</p>
148
+ </Card.Content>
149
+ </Card>
150
+ );
151
+ }
152
+ `,
153
+ },
154
+ states: {
155
+ title: "States",
156
+ description: "A card can be selected or dragging.",
157
+ showProps: false,
158
+ render: () => {
159
+ const [isSelected, setIsSelected] = React.useState(false);
160
+ const [isDragging, setIsDragging] = React.useState(false);
161
+
162
+ const handlePointerDown = () => {
163
+ setIsDragging(true);
164
+ };
165
+
166
+ const handlePointerUp = () => {
167
+ setIsDragging(false);
168
+ };
169
+ return (
170
+ <Card
171
+ title="Draggable Card"
172
+ selected={isSelected}
173
+ dragging={isDragging}
174
+ onClick={() => setIsSelected(!isSelected)}
175
+ onPointerDown={handlePointerDown}
176
+ onPointerUp={handlePointerUp}
177
+ hoverable
178
+ style={{ maxWidth: "420px" }}
179
+ >
180
+ <p>This card can be dragged and selected.</p>
181
+ </Card>
182
+ );
183
+ },
184
+ code: `import { Card } from "@solara/card";
185
+ import { useState } from "react";
186
+
187
+ export function DraggableCard() {
188
+ const [isSelected, setIsSelected] = useState(false);
189
+ const [isDragging, setIsDragging] = useState(false);
190
+
191
+ const handlePointerDown = () => {
192
+ setIsDragging(true);
193
+ };
194
+
195
+ const handlePointerUp = () => {
196
+ setIsDragging(false);
197
+ };
198
+
199
+ return (
200
+ <Card
201
+ title="Draggable Card"
202
+ selected={isSelected}
203
+ dragging={isDragging}
204
+ onClick={() => setIsSelected(!isSelected)}
205
+ onPointerDown={handlePointerDown}
206
+ onPointerUp={handlePointerUp}
207
+ hoverable
208
+ style={{ maxWidth: "420px" }}
209
+ >
210
+ <p>This card can be dragged and selected.</p>
211
+ </Card>
212
+ );
213
+ }
214
+ `,
215
+ },
216
+ };
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@varialkit/card",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./examples": "./examples/index.tsx"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "docs.md",
14
+ "examples",
15
+ "examples.tsx"
16
+ ],
17
+ "peerDependencies": {
18
+ "react": "^19.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/react": "19.0.10",
22
+ "react": "19.0.0"
23
+ }
24
+ }
package/src/Card.scss ADDED
@@ -0,0 +1,232 @@
1
+ .solara-card {
2
+ --card-padding-y: var(--space-3);
3
+ --card-padding-x: var(--space-4);
4
+ --card-header-padding-y: var(--space-3);
5
+ --card-header-padding-x: var(--space-4);
6
+ --card-title-font-size: var(--font-size-body-scaled);
7
+ --card-title-line-height: var(--line-height-body-scaled);
8
+ --card-surface-color: var(--color-surface-0);
9
+ --card-surface-color-rgb: var(--color-surface-0-rgb);
10
+ --surface-border-color: var(--color-border-secondary);
11
+ --surface-border-color-rgb: var(--color-divider-secondary-rgb);
12
+ --surface-opacity: 1;
13
+ --surface-blur: 0px;
14
+ --surface-shadow: none;
15
+ --card-border-radius: var(--radius-2);
16
+ background-color: rgba(var(--card-surface-color-rgb), var(--surface-opacity));
17
+ backdrop-filter: blur(var(--surface-blur));
18
+ border-radius: calc(var(--card-border-radius) * var(--radius-multiplier));
19
+ border: 1px solid transparent;
20
+ box-shadow: var(--surface-shadow);
21
+ display: flex;
22
+ flex-direction: column;
23
+ height: 100%;
24
+ width: 100%;
25
+ transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
26
+ }
27
+
28
+ .solara-card--size-small {
29
+ --card-padding-y: var(--space-2);
30
+ --card-padding-x: var(--space-3);
31
+ --card-header-padding-y: var(--space-2);
32
+ --card-header-padding-x: var(--space-3);
33
+ --card-title-font-size: var(--font-size-caption-scaled);
34
+ --card-title-line-height: var(--line-height-caption-scaled);
35
+ }
36
+
37
+ .solara-card--size-large {
38
+ --card-padding-y: var(--space-4);
39
+ --card-padding-x: var(--space-5);
40
+ --card-header-padding-y: var(--space-4);
41
+ --card-header-padding-x: var(--space-5);
42
+ --card-title-font-size: var(--font-size-h5-scaled);
43
+ --card-title-line-height: var(--line-height-body-scaled);
44
+ }
45
+
46
+ .solara-card--bordered {
47
+ border-color: var(--surface-border-color);
48
+ }
49
+
50
+ .solara-card--shadow-1 {
51
+ --surface-shadow: var(--elevation-1);
52
+ }
53
+
54
+ .solara-card--shadow-2 {
55
+ --surface-shadow: var(--elevation-2);
56
+ }
57
+
58
+ .solara-card--shadow-3 {
59
+ --surface-shadow: var(--elevation-3);
60
+ }
61
+
62
+ .solara-card--shadow-4 {
63
+ --surface-shadow: var(--elevation-4);
64
+ }
65
+
66
+ .solara-card--radius-none {
67
+ border-radius: 0;
68
+ }
69
+
70
+ .solara-card--radius-small {
71
+ --card-border-radius: var(--radius-1);
72
+ }
73
+
74
+ .solara-card--radius-medium {
75
+ --card-border-radius: var(--radius-2);
76
+ }
77
+
78
+ .solara-card--radius-large {
79
+ --card-border-radius: var(--radius-3);
80
+ }
81
+
82
+ .solara-card--radius-xlarge {
83
+ --card-border-radius: var(--radius-4);
84
+ }
85
+
86
+ .solara-card--hoverable {
87
+ cursor: pointer;
88
+
89
+ &:hover {
90
+ --surface-shadow: var(--elevation-2);
91
+ transform: translateY(-2px);
92
+ }
93
+ }
94
+
95
+ .solara-card--fill {
96
+ height: 100%;
97
+ width: 100%;
98
+
99
+ .solara-card__body {
100
+ flex: 1;
101
+ min-height: 0;
102
+ }
103
+ }
104
+
105
+ .solara-card__content {
106
+ display: flex;
107
+ flex-direction: column;
108
+ flex: 1;
109
+ min-height: 0;
110
+ }
111
+
112
+ .solara-card__header {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: calc(var(--space-2) * var(--spacing-multiplier));
116
+ padding: calc(var(--card-header-padding-y) * var(--spacing-multiplier)) calc(var(--card-header-padding-x) * var(--spacing-multiplier));
117
+ border-bottom: 1px solid var(--surface-border-color);
118
+ }
119
+
120
+ .solara-card__header-content {
121
+ flex: 1;
122
+ min-width: 0;
123
+ }
124
+
125
+ .solara-card__title {
126
+ margin: 0;
127
+ font-size: var(--card-title-font-size);
128
+ line-height: var(--card-title-line-height);
129
+ font-weight: 600;
130
+ color: var(--color-text-primary);
131
+ }
132
+
133
+ .solara-card__actions {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: calc(var(--space-2) * var(--spacing-multiplier));
137
+ margin-left: auto;
138
+ }
139
+
140
+ .solara-card__body {
141
+ flex: 1;
142
+
143
+ > :first-child {
144
+ margin-top: 0;
145
+ }
146
+
147
+ > :last-child {
148
+ margin-bottom: 0;
149
+ }
150
+ }
151
+
152
+ .solara-card__body--padding-none {
153
+ padding: 0;
154
+ }
155
+
156
+ .solara-card__body--padding-small {
157
+ padding: calc(var(--space-2) * var(--spacing-multiplier)) calc(var(--space-3) * var(--spacing-multiplier));
158
+ }
159
+
160
+ .solara-card__body--padding-medium {
161
+ padding: calc(var(--space-3) * var(--spacing-multiplier)) calc(var(--space-4) * var(--spacing-multiplier));
162
+ }
163
+
164
+ .solara-card__body--padding-large {
165
+ padding: calc(var(--space-4) * var(--spacing-multiplier)) calc(var(--space-5) * var(--spacing-multiplier));
166
+ }
167
+
168
+ .solara-card--selected {
169
+ border-color: var(--color-border-accent);
170
+ }
171
+
172
+ .solara-card--dragging {
173
+ border: 2px solid var(--color-border-accent);
174
+ --surface-shadow: var(--elevation-3);
175
+ }
176
+
177
+ .solara-card--surface-0 {
178
+ --card-surface-color: var(--color-surface-0);
179
+ --card-surface-color-rgb: var(--color-surface-0-rgb);
180
+ }
181
+
182
+ .solara-card--surface-100 {
183
+ --card-surface-color: var(--color-surface-100);
184
+ --card-surface-color-rgb: var(--color-surface-100-rgb);
185
+ }
186
+
187
+ .solara-card--surface-200 {
188
+ --card-surface-color: var(--color-surface-200);
189
+ --card-surface-color-rgb: var(--color-surface-200-rgb);
190
+ }
191
+
192
+ .solara-card--surface-300 {
193
+ --card-surface-color: var(--color-surface-300);
194
+ --card-surface-color-rgb: var(--color-surface-300-rgb);
195
+ }
196
+
197
+ .solara-card--surface-400 {
198
+ --card-surface-color: var(--color-surface-400);
199
+ --card-surface-color-rgb: var(--color-surface-400-rgb);
200
+ }
201
+
202
+ .solara-card--surface-500 {
203
+ --card-surface-color: var(--color-surface-500);
204
+ --card-surface-color-rgb: var(--color-surface-500-rgb);
205
+ }
206
+
207
+ :root[data-surface-default="translucent"] .solara-card,
208
+ :root[data-surface-card="translucent"] .solara-card,
209
+ .solara-card[data-surface-style="translucent"] {
210
+ --surface-opacity: var(--opacity-translucent-medium);
211
+ --surface-blur: 0px;
212
+ }
213
+
214
+ :root[data-surface-default="glass"] .solara-card,
215
+ :root[data-surface-card="glass"] .solara-card,
216
+ .solara-card[data-surface-style="glass"] {
217
+ --surface-opacity: var(--opacity-translucent-heavy);
218
+ --surface-blur: var(--blur-medium);
219
+ --surface-border-color: rgba(var(--surface-border-color-rgb), var(--surface-border-alpha-glass));
220
+ }
221
+
222
+ .solara-card[data-surface-style="solid"] {
223
+ --surface-opacity: 1;
224
+ --surface-blur: 0px;
225
+ --surface-border-color: var(--color-border-secondary);
226
+ }
227
+
228
+ :root[data-surface-card="solid"] .solara-card {
229
+ --surface-opacity: 1;
230
+ --surface-blur: 0px;
231
+ --surface-border-color: var(--color-border-secondary);
232
+ }
package/src/Card.tsx ADDED
@@ -0,0 +1,214 @@
1
+ import React, { forwardRef } from "react";
2
+ import type {
3
+ CardContentProps,
4
+ CardHeaderProps,
5
+ CardPadding,
6
+ CardProps,
7
+ CardRadius,
8
+ CardSurface,
9
+ CardSize,
10
+ CardTitleProps,
11
+ } from "./Card.types";
12
+ import "./Card.scss";
13
+
14
+ const sizeAliasMap: Record<CardSize, "small" | "medium" | "large"> = {
15
+ sm: "small",
16
+ md: "medium",
17
+ lg: "large",
18
+ small: "small",
19
+ medium: "medium",
20
+ large: "large",
21
+ };
22
+
23
+ const paddingAliasMap: Record<CardPadding, "none" | "small" | "medium" | "large"> = {
24
+ none: "none",
25
+ small: "small",
26
+ medium: "medium",
27
+ large: "large",
28
+ };
29
+
30
+ const radiusAliasMap: Record<CardRadius, "none" | "small" | "medium" | "large" | "xlarge"> = {
31
+ none: "none",
32
+ small: "small",
33
+ medium: "medium",
34
+ large: "large",
35
+ xlarge: "xlarge",
36
+ };
37
+
38
+ const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
39
+ ({ className, children, actions, ...props }, ref) => (
40
+ <div
41
+ className={["solara-card__header", className].filter(Boolean).join(" ")}
42
+ ref={ref}
43
+ {...props}
44
+ >
45
+ <div className="solara-card__header-content">{children}</div>
46
+ {actions ? (
47
+ <div className="solara-card__actions">{actions}</div>
48
+ ) : null}
49
+ </div>
50
+ )
51
+ );
52
+ CardHeader.displayName = "CardHeader";
53
+
54
+ const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(
55
+ ({ className, ...props }, ref) => (
56
+ <h3
57
+ className={["solara-card__title", className].filter(Boolean).join(" ")}
58
+ ref={ref}
59
+ {...props}
60
+ />
61
+ )
62
+ );
63
+ CardTitle.displayName = "CardTitle";
64
+
65
+ const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
66
+ ({ className, padding = "medium", ...props }, ref) => {
67
+ const resolvedPadding = paddingAliasMap[padding];
68
+ return (
69
+ <div
70
+ className={[
71
+ "solara-card__body",
72
+ `solara-card__body--padding-${resolvedPadding}`,
73
+ className,
74
+ ]
75
+ .filter(Boolean)
76
+ .join(" ")}
77
+ ref={ref}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+ );
83
+ CardContent.displayName = "CardContent";
84
+
85
+ const Card = forwardRef<HTMLDivElement, CardProps>(
86
+ (
87
+ {
88
+ children,
89
+ className,
90
+ title,
91
+ headerActions,
92
+ headerClassName,
93
+ bodyClassName,
94
+ titleClassName,
95
+ bordered = false,
96
+ shadow = true,
97
+ hoverable = false,
98
+ selected = false,
99
+ dragging = false,
100
+ radius,
101
+ surface,
102
+ surfaceStyle,
103
+ size = "medium",
104
+ padding = "medium",
105
+ fill = false,
106
+ style,
107
+ ...restProps
108
+ },
109
+ ref
110
+ ) => {
111
+ const resolvedSize = sizeAliasMap[size];
112
+ const resolvedRadius = radius ? radiusAliasMap[radius] : undefined;
113
+
114
+ const rootClasses = [
115
+ "solara-card",
116
+ `solara-card--size-${resolvedSize}`,
117
+ bordered ? "solara-card--bordered" : null,
118
+ shadow ? `solara-card--shadow-${shadow === true ? "1" : shadow}` : null,
119
+ resolvedRadius ? `solara-card--radius-${resolvedRadius}` : null,
120
+ hoverable ? "solara-card--hoverable" : null,
121
+ selected ? "solara-card--selected" : null,
122
+ dragging ? "solara-card--dragging" : null,
123
+ surface ? `solara-card--surface-${surface}` : null,
124
+ fill ? "solara-card--fill" : null,
125
+ className,
126
+ ]
127
+ .filter(Boolean)
128
+ .join(" ");
129
+
130
+ // Surface styling is applied via data attributes + CSS variables so themes can stay centralized.
131
+ const surfaceAttributes: Record<string, string | undefined> = {};
132
+ const surfaceStyleVars: React.CSSProperties = {};
133
+
134
+ if (surfaceStyle) {
135
+ if (typeof surfaceStyle === "string") {
136
+ surfaceAttributes["data-surface-style"] = surfaceStyle;
137
+ } else {
138
+ surfaceAttributes["data-surface-style"] = "custom";
139
+ if (surfaceStyle.opacity !== undefined) {
140
+ surfaceStyleVars["--surface-opacity"] = surfaceStyle.opacity.toString();
141
+ }
142
+ if (surfaceStyle.blur !== undefined) {
143
+ surfaceStyleVars["--surface-blur"] = `${surfaceStyle.blur}px`;
144
+ }
145
+ if (surfaceStyle.borderColor !== undefined) {
146
+ surfaceStyleVars["--surface-border-color"] = surfaceStyle.borderColor;
147
+ }
148
+ if (surfaceStyle.shadow !== undefined) {
149
+ surfaceStyleVars["--surface-shadow"] = surfaceStyle.shadow;
150
+ }
151
+ }
152
+ }
153
+
154
+ const childArray = React.Children.toArray(children);
155
+ const hasStructuredChildren = childArray.some(
156
+ (child) =>
157
+ React.isValidElement(child) &&
158
+ (child.type === CardHeader ||
159
+ child.type === CardContent ||
160
+ child.type === CardTitle)
161
+ );
162
+
163
+ const content = children
164
+ ? hasStructuredChildren
165
+ ? children
166
+ : (
167
+ <CardContent padding={padding} className={bodyClassName}>
168
+ {children}
169
+ </CardContent>
170
+ )
171
+ : null;
172
+
173
+ return (
174
+ <div
175
+ ref={ref}
176
+ className={rootClasses}
177
+ style={{ ...surfaceStyleVars, ...style }}
178
+ {...surfaceAttributes}
179
+ {...restProps}
180
+ >
181
+ <div className="solara-card__content">
182
+ {title || headerActions ? (
183
+ <CardHeader className={headerClassName} actions={headerActions}>
184
+ {title ? (
185
+ <CardTitle className={titleClassName}>{title}</CardTitle>
186
+ ) : null}
187
+ </CardHeader>
188
+ ) : null}
189
+ {content}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
194
+ );
195
+ Card.displayName = "Card";
196
+
197
+ interface CardCompound
198
+ extends React.ForwardRefExoticComponent<
199
+ CardProps & React.RefAttributes<HTMLDivElement>
200
+ > {
201
+ Header: typeof CardHeader;
202
+ Title: typeof CardTitle;
203
+ Content: typeof CardContent;
204
+ }
205
+
206
+ const CardWithSubcomponents = Object.assign(Card, {
207
+ Header: CardHeader,
208
+ Title: CardTitle,
209
+ Content: CardContent,
210
+ }) as CardCompound;
211
+
212
+ CardWithSubcomponents.displayName = "Card";
213
+
214
+ export { CardWithSubcomponents as Card, CardHeader, CardTitle, CardContent };
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+
3
+ export type CardSize = "small" | "medium" | "large" | "sm" | "md" | "lg";
4
+
5
+ export type CardRadius = "none" | "small" | "medium" | "large" | "xlarge";
6
+
7
+ export type CardSurface = "0" | "100" | "200" | "300" | "400" | "500";
8
+
9
+ export type CardSurfaceStyle =
10
+ | "solid"
11
+ | "translucent"
12
+ | "glass"
13
+ | {
14
+ opacity?: number;
15
+ blur?: number;
16
+ borderColor?: string;
17
+ shadow?: string;
18
+ };
19
+
20
+ export type CardPadding = "none" | "small" | "medium" | "large";
21
+
22
+ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
23
+ /**
24
+ * Optional actions to display on the right side of the header.
25
+ */
26
+ actions?: React.ReactNode;
27
+ }
28
+
29
+ export interface CardTitleProps
30
+ extends React.HTMLAttributes<HTMLHeadingElement> { }
31
+
32
+ export interface CardContentProps
33
+ extends React.HTMLAttributes<HTMLDivElement> {
34
+ /**
35
+ * Padding size for the card content.
36
+ */
37
+ padding?: CardPadding;
38
+ }
39
+
40
+ export interface CardProps
41
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
42
+ /**
43
+ * The title of the card.
44
+ */
45
+ title?: React.ReactNode;
46
+ /**
47
+ * Optional header actions displayed to the right of the title.
48
+ */
49
+ headerActions?: React.ReactNode;
50
+ /**
51
+ * Whether the card has a border.
52
+ */
53
+ bordered?: boolean;
54
+ /**
55
+ * The elevation shadow of the card.
56
+ * @type {boolean | '1' | '2' | '3' | '4'}
57
+ */
58
+ shadow?: boolean | '1' | '2' | '3' | '4';
59
+ /**
60
+ * The radius of the card's corners.
61
+ */
62
+ radius?: CardRadius;
63
+ /**
64
+ * The surface color of the card.
65
+ */
66
+ surface?: CardSurface;
67
+ /**
68
+ * Controls surface material treatment without changing layout tokens.
69
+ */
70
+ surfaceStyle?: CardSurfaceStyle;
71
+ /**
72
+ * Whether the card shows a hover elevation.
73
+ */
74
+ hoverable?: boolean;
75
+ /**
76
+ * The size of the card.
77
+ */
78
+ size?: CardSize;
79
+ /**
80
+ * The padding of the card content.
81
+ */
82
+ padding?: CardPadding;
83
+ /**
84
+ * Whether to force the card content to fill the available space.
85
+ */
86
+ fill?: boolean;
87
+ /**
88
+ * Custom class name for the card header.
89
+ */
90
+ headerClassName?: string;
91
+ /**
92
+ * Custom class name for the card body.
93
+ */
94
+ bodyClassName?: string;
95
+ /**
96
+ * Custom class name for the card title.
97
+ */
98
+ titleClassName?: string;
99
+ /**
100
+ * Whether the card is selected.
101
+ */
102
+ selected?: boolean;
103
+ /**
104
+ * Whether the card is being dragged.
105
+ */
106
+ dragging?: boolean;
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./Card";
2
+ export type { CardProps, CardContentProps, CardHeaderProps, CardTitleProps } from "./Card.types";