@emara/ui 1.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/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { RiArrowRightLine, RiCheckLine, RiMoreLine } from "@remixicon/react";
|
|
3
|
+
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
|
|
5
|
+
import { Badge } from "./badge";
|
|
6
|
+
import { Button } from "./button";
|
|
7
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof Card> = {
|
|
10
|
+
title: "Foundations/Card",
|
|
11
|
+
component: Card,
|
|
12
|
+
parameters: { layout: "centered" },
|
|
13
|
+
argTypes: {
|
|
14
|
+
variant: {
|
|
15
|
+
control: "select",
|
|
16
|
+
options: ["default", "outline", "elevated", "filled", "ghost"],
|
|
17
|
+
},
|
|
18
|
+
padding: { control: "select", options: ["none", "sm", "md", "lg"] },
|
|
19
|
+
interactive: { control: "boolean" },
|
|
20
|
+
selected: { control: "boolean" },
|
|
21
|
+
loading: { control: "boolean" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof Card>;
|
|
27
|
+
|
|
28
|
+
/** Standard Card with title, description, body, and footer actions. */
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<Card {...args} className="w-96">
|
|
32
|
+
<CardHeader>
|
|
33
|
+
<CardTitle>Email notifications</CardTitle>
|
|
34
|
+
<CardDescription>Choose which messages get delivered to your inbox.</CardDescription>
|
|
35
|
+
</CardHeader>
|
|
36
|
+
<CardContent>
|
|
37
|
+
<p className="text-sm leading-relaxed">
|
|
38
|
+
We'll only email you about the things you've opted in to. You can change this at
|
|
39
|
+
any time from your account settings.
|
|
40
|
+
</p>
|
|
41
|
+
</CardContent>
|
|
42
|
+
<CardFooter className="justify-end gap-2">
|
|
43
|
+
<Button variant="ghost">Cancel</Button>
|
|
44
|
+
<Button>Save changes</Button>
|
|
45
|
+
</CardFooter>
|
|
46
|
+
</Card>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Side-by-side comparison of the five spec variants. */
|
|
51
|
+
export const Variants: Story = {
|
|
52
|
+
parameters: { layout: "padded" },
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
55
|
+
{(["default", "outline", "elevated", "filled", "ghost"] as const).map((v) => (
|
|
56
|
+
<Card key={v} variant={v} className="w-72">
|
|
57
|
+
<CardHeader>
|
|
58
|
+
<CardTitle>{v}</CardTitle>
|
|
59
|
+
<CardDescription>variant="{v}"</CardDescription>
|
|
60
|
+
</CardHeader>
|
|
61
|
+
<CardContent>
|
|
62
|
+
<p className="text-sm">
|
|
63
|
+
{v === "default"
|
|
64
|
+
? "No border — assumes the host theme tints --card vs --background."
|
|
65
|
+
: v === "outline"
|
|
66
|
+
? "Border + card background. The safe default for most apps."
|
|
67
|
+
: v === "elevated"
|
|
68
|
+
? "Shadow-md, no border. Reads as floating above the page."
|
|
69
|
+
: v === "filled"
|
|
70
|
+
? "--muted background. Use for nested cards inside another Card."
|
|
71
|
+
: "Transparent. Pure spacing wrapper for headings + body."}
|
|
72
|
+
</p>
|
|
73
|
+
</CardContent>
|
|
74
|
+
</Card>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const PaddingScale: Story = {
|
|
81
|
+
parameters: { layout: "padded" },
|
|
82
|
+
render: () => (
|
|
83
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
84
|
+
{(["none", "sm", "md", "lg"] as const).map((p) => (
|
|
85
|
+
<Card key={p} padding={p} className="w-56">
|
|
86
|
+
<CardTitle>padding={p}</CardTitle>
|
|
87
|
+
</Card>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Whole card is a real `<button>` via `asChild`. Hover + focus styles applied. */
|
|
94
|
+
export const Interactive: Story = {
|
|
95
|
+
render: () => (
|
|
96
|
+
<Card asChild interactive className="w-80">
|
|
97
|
+
<button type="button" onClick={() => console.log("card clicked")} className="text-start">
|
|
98
|
+
<CardHeader>
|
|
99
|
+
<CardTitle>Click anywhere on this card</CardTitle>
|
|
100
|
+
<CardDescription>The whole surface is a real button.</CardDescription>
|
|
101
|
+
</CardHeader>
|
|
102
|
+
<CardContent className="flex items-center justify-between">
|
|
103
|
+
<span className="text-muted-foreground text-sm">View details</span>
|
|
104
|
+
<RiArrowRightLine className="rtl-mirror text-muted-foreground size-4" />
|
|
105
|
+
</CardContent>
|
|
106
|
+
</button>
|
|
107
|
+
</Card>
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Selected card — used for grid selection states (filter chips, plan picker, etc.). */
|
|
112
|
+
export const Selected: Story = {
|
|
113
|
+
render: () => (
|
|
114
|
+
<div className="flex gap-4">
|
|
115
|
+
<Card className="w-56">
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<CardTitle>Starter</CardTitle>
|
|
118
|
+
<CardDescription>$0 / month</CardDescription>
|
|
119
|
+
</CardHeader>
|
|
120
|
+
</Card>
|
|
121
|
+
<Card selected className="w-56">
|
|
122
|
+
<CardHeader>
|
|
123
|
+
<CardTitle>Pro</CardTitle>
|
|
124
|
+
<CardDescription>$12 / month</CardDescription>
|
|
125
|
+
</CardHeader>
|
|
126
|
+
</Card>
|
|
127
|
+
<Card className="w-56">
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<CardTitle>Team</CardTitle>
|
|
130
|
+
<CardDescription>$48 / month</CardDescription>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
</Card>
|
|
133
|
+
</div>
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Skeleton placeholder while data loads. Carries `aria-busy` / `aria-live`. */
|
|
138
|
+
export const Loading: Story = {
|
|
139
|
+
args: { loading: true },
|
|
140
|
+
render: (args) => <Card {...args} className="w-96" />,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** `CardTitle as="h2"` keeps the document outline intact when a Card sits
|
|
144
|
+
* directly under a `PageHeader` whose title is an `<h1>`. */
|
|
145
|
+
export const TitleAsH2: Story = {
|
|
146
|
+
render: () => (
|
|
147
|
+
<Card className="w-96">
|
|
148
|
+
<CardHeader>
|
|
149
|
+
<CardTitle as="h2">Title rendered as <h2></CardTitle>
|
|
150
|
+
<CardDescription>
|
|
151
|
+
Use <code>as="h2"</code> when the Card sits directly under a PageHeader.
|
|
152
|
+
</CardDescription>
|
|
153
|
+
</CardHeader>
|
|
154
|
+
</Card>
|
|
155
|
+
),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ----------------------------------------------------------------------------
|
|
159
|
+
// Real-world composition examples
|
|
160
|
+
// ----------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/** Profile-style card composing Avatar, Badge, and CardTitle in a header row. */
|
|
163
|
+
export const ProfileCard: Story = {
|
|
164
|
+
render: () => (
|
|
165
|
+
<Card className="w-96">
|
|
166
|
+
<CardHeader className="flex flex-row items-center gap-3 space-y-0">
|
|
167
|
+
<Avatar size="lg">
|
|
168
|
+
<AvatarImage src="https://i.pravatar.cc/96?img=12" alt="" />
|
|
169
|
+
<AvatarFallback>AB</AvatarFallback>
|
|
170
|
+
</Avatar>
|
|
171
|
+
<div className="flex-1">
|
|
172
|
+
<CardTitle>Alice Bouchaib</CardTitle>
|
|
173
|
+
<CardDescription>alice@example.com</CardDescription>
|
|
174
|
+
</div>
|
|
175
|
+
<Badge dot variant="success">
|
|
176
|
+
Active
|
|
177
|
+
</Badge>
|
|
178
|
+
</CardHeader>
|
|
179
|
+
<CardContent>
|
|
180
|
+
<dl className="grid grid-cols-2 gap-3 text-sm">
|
|
181
|
+
<div>
|
|
182
|
+
<dt className="text-muted-foreground">Role</dt>
|
|
183
|
+
<dd className="font-medium">Designer</dd>
|
|
184
|
+
</div>
|
|
185
|
+
<div>
|
|
186
|
+
<dt className="text-muted-foreground">Joined</dt>
|
|
187
|
+
<dd className="font-medium">2024-03-12</dd>
|
|
188
|
+
</div>
|
|
189
|
+
</dl>
|
|
190
|
+
</CardContent>
|
|
191
|
+
<CardFooter className="justify-between">
|
|
192
|
+
<Button variant="ghost" size="sm">
|
|
193
|
+
Message
|
|
194
|
+
</Button>
|
|
195
|
+
<Button size="sm">View profile</Button>
|
|
196
|
+
</CardFooter>
|
|
197
|
+
</Card>
|
|
198
|
+
),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** Stat-style card with a primary metric, secondary detail, and a trend badge. */
|
|
202
|
+
export const StatCard: Story = {
|
|
203
|
+
render: () => (
|
|
204
|
+
<Card className="w-72">
|
|
205
|
+
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
|
206
|
+
<div>
|
|
207
|
+
<CardDescription>Monthly recurring revenue</CardDescription>
|
|
208
|
+
<CardTitle className="mt-1 text-3xl tabular-nums">$24,580</CardTitle>
|
|
209
|
+
</div>
|
|
210
|
+
<Badge variant="success" icon={<RiCheckLine />}>
|
|
211
|
+
+12.3%
|
|
212
|
+
</Badge>
|
|
213
|
+
</CardHeader>
|
|
214
|
+
<CardContent>
|
|
215
|
+
<p className="text-muted-foreground text-xs">vs. $21,890 last month</p>
|
|
216
|
+
</CardContent>
|
|
217
|
+
</Card>
|
|
218
|
+
),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** Header with a leading icon-button action — common for cards that get
|
|
222
|
+
* dismissed, pinned, or expanded individually. */
|
|
223
|
+
export const WithHeaderAction: Story = {
|
|
224
|
+
render: () => (
|
|
225
|
+
<Card className="w-96">
|
|
226
|
+
<CardHeader className="flex flex-row items-start justify-between space-y-0">
|
|
227
|
+
<div className="space-y-1">
|
|
228
|
+
<CardTitle>API keys</CardTitle>
|
|
229
|
+
<CardDescription>Manage authentication tokens for this workspace.</CardDescription>
|
|
230
|
+
</div>
|
|
231
|
+
<Button variant="ghost" size="icon-sm" aria-label="More actions">
|
|
232
|
+
<RiMoreLine />
|
|
233
|
+
</Button>
|
|
234
|
+
</CardHeader>
|
|
235
|
+
<CardContent>
|
|
236
|
+
<p className="text-muted-foreground text-sm">3 active keys, 1 revoked.</p>
|
|
237
|
+
</CardContent>
|
|
238
|
+
</Card>
|
|
239
|
+
),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Grid of selectable plan cards — demonstrates the selected state in a real
|
|
243
|
+
* picker pattern with `asChild` + `<button>` for keyboard accessibility. */
|
|
244
|
+
export const PlanPicker: Story = {
|
|
245
|
+
parameters: { layout: "padded" },
|
|
246
|
+
render: () => {
|
|
247
|
+
const plans = [
|
|
248
|
+
{ id: "starter", name: "Starter", price: "$0", perks: "Up to 3 projects" },
|
|
249
|
+
{ id: "pro", name: "Pro", price: "$12", perks: "Unlimited projects", selected: true },
|
|
250
|
+
{ id: "team", name: "Team", price: "$48", perks: "Seats + roles" },
|
|
251
|
+
];
|
|
252
|
+
return (
|
|
253
|
+
<div className="grid grid-cols-3 gap-4">
|
|
254
|
+
{plans.map((plan) => (
|
|
255
|
+
<Card
|
|
256
|
+
key={plan.id}
|
|
257
|
+
asChild
|
|
258
|
+
interactive
|
|
259
|
+
selected={plan.selected}
|
|
260
|
+
className="w-full text-start"
|
|
261
|
+
>
|
|
262
|
+
<button type="button">
|
|
263
|
+
<CardHeader>
|
|
264
|
+
<CardTitle>{plan.name}</CardTitle>
|
|
265
|
+
<CardDescription>{plan.perks}</CardDescription>
|
|
266
|
+
</CardHeader>
|
|
267
|
+
<CardContent>
|
|
268
|
+
<p className="text-2xl font-semibold tabular-nums">
|
|
269
|
+
{plan.price}
|
|
270
|
+
<span className="text-muted-foreground ms-1 text-sm font-normal">/mo</span>
|
|
271
|
+
</p>
|
|
272
|
+
</CardContent>
|
|
273
|
+
</button>
|
|
274
|
+
</Card>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
import { Skeleton } from "./skeleton";
|
|
10
|
+
|
|
11
|
+
// Per docs/emara-ui-phase-1-components.md §5.
|
|
12
|
+
|
|
13
|
+
// ----------------------------------------------------------------------------
|
|
14
|
+
// Card root
|
|
15
|
+
// ----------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const cardVariants = cva(
|
|
18
|
+
"rounded-lg transition-colors transition-shadow",
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
// Spec-faithful per docs/emara-ui-phase-1-components.md §5. Note that
|
|
22
|
+
// `default` carries no border because the spec assumes `--card` differs
|
|
23
|
+
// from `--background` in the host theme. Emara's stock palette has both
|
|
24
|
+
// as white in light mode, so `defaultVariants.variant` (below) is set
|
|
25
|
+
// to `outline` — a bare `<Card>` should still look like a card.
|
|
26
|
+
variant: {
|
|
27
|
+
default: "bg-card text-card-foreground",
|
|
28
|
+
outline: "bg-card text-card-foreground border border-border",
|
|
29
|
+
elevated: "bg-card text-card-foreground shadow-md",
|
|
30
|
+
filled: "bg-muted text-foreground",
|
|
31
|
+
ghost: "bg-transparent text-foreground",
|
|
32
|
+
},
|
|
33
|
+
padding: {
|
|
34
|
+
none: "p-0",
|
|
35
|
+
sm: "p-3",
|
|
36
|
+
md: "p-4",
|
|
37
|
+
lg: "p-6",
|
|
38
|
+
},
|
|
39
|
+
interactive: {
|
|
40
|
+
true: [
|
|
41
|
+
"cursor-pointer",
|
|
42
|
+
// Subtle hover — only the surface tone shifts, text stays put.
|
|
43
|
+
// For the default/outline variants the border also gets slightly
|
|
44
|
+
// stronger so the lift reads even without a background change.
|
|
45
|
+
"hover:bg-accent/40 hover:border-foreground/20",
|
|
46
|
+
"active:bg-accent/60",
|
|
47
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
48
|
+
].join(" "),
|
|
49
|
+
false: "",
|
|
50
|
+
},
|
|
51
|
+
selected: {
|
|
52
|
+
true: "ring-2 ring-primary ring-offset-2 ring-offset-background",
|
|
53
|
+
false: "",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultVariants: {
|
|
57
|
+
variant: "outline",
|
|
58
|
+
padding: "md",
|
|
59
|
+
interactive: false,
|
|
60
|
+
selected: false,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
type CardVariants = VariantProps<typeof cardVariants>;
|
|
66
|
+
|
|
67
|
+
type CardProps = React.HTMLAttributes<HTMLDivElement> &
|
|
68
|
+
CardVariants & {
|
|
69
|
+
asChild?: boolean;
|
|
70
|
+
loading?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const Card = forwardRef<HTMLDivElement, CardProps>(function Card(
|
|
74
|
+
{
|
|
75
|
+
className,
|
|
76
|
+
variant,
|
|
77
|
+
padding,
|
|
78
|
+
interactive,
|
|
79
|
+
selected,
|
|
80
|
+
asChild = false,
|
|
81
|
+
loading = false,
|
|
82
|
+
children,
|
|
83
|
+
...props
|
|
84
|
+
},
|
|
85
|
+
ref,
|
|
86
|
+
) {
|
|
87
|
+
const Comp = asChild ? Slot : "div";
|
|
88
|
+
|
|
89
|
+
// Loading state mirrors a typical Card: header (title + description), body
|
|
90
|
+
// (3 lines), and a footer action. The container carries `aria-busy` /
|
|
91
|
+
// `aria-live` so assistive tech announces the loading region.
|
|
92
|
+
const loadingContent = (
|
|
93
|
+
<div className="space-y-4" aria-busy="true" aria-live="polite">
|
|
94
|
+
<div className="space-y-2">
|
|
95
|
+
<Skeleton className="h-5 w-2/5" />
|
|
96
|
+
<Skeleton className="h-4 w-3/5" />
|
|
97
|
+
</div>
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<Skeleton className="h-4 w-full" />
|
|
100
|
+
<Skeleton className="h-4 w-10/12" />
|
|
101
|
+
<Skeleton className="h-4 w-3/5" />
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex justify-end">
|
|
104
|
+
<Skeleton className="h-9 w-24" />
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Comp
|
|
111
|
+
ref={ref}
|
|
112
|
+
data-selected={selected ? "true" : undefined}
|
|
113
|
+
className={cn(cardVariants({ variant, padding, interactive, selected }), className)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
{loading ? loadingContent : children}
|
|
117
|
+
</Comp>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
Card.displayName = "Card";
|
|
121
|
+
|
|
122
|
+
// ----------------------------------------------------------------------------
|
|
123
|
+
// CardHeader
|
|
124
|
+
// ----------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
type CardHeaderProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
127
|
+
asChild?: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(function CardHeader(
|
|
131
|
+
{ className, asChild = false, ...props },
|
|
132
|
+
ref,
|
|
133
|
+
) {
|
|
134
|
+
const Comp = asChild ? Slot : "div";
|
|
135
|
+
// Block layout with vertical rhythm. `space-y-1` is 4px — tight, but the
|
|
136
|
+
// CardTitle now carries its own `leading-snug` so visually there's already
|
|
137
|
+
// breathing room between title and description.
|
|
138
|
+
return <Comp ref={ref} className={cn("space-y-1", className)} {...props} />;
|
|
139
|
+
});
|
|
140
|
+
CardHeader.displayName = "CardHeader";
|
|
141
|
+
|
|
142
|
+
// ----------------------------------------------------------------------------
|
|
143
|
+
// CardTitle — polymorphic via `as` prop (h2..h6, default h3).
|
|
144
|
+
// ----------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
type CardTitleAs = "h2" | "h3" | "h4" | "h5" | "h6";
|
|
147
|
+
|
|
148
|
+
type CardTitleProps = React.HTMLAttributes<HTMLHeadingElement> & {
|
|
149
|
+
as?: CardTitleAs;
|
|
150
|
+
asChild?: boolean;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const CardTitle = forwardRef<HTMLHeadingElement, CardTitleProps>(function CardTitle(
|
|
154
|
+
{ className, as = "h3", asChild = false, ...props },
|
|
155
|
+
ref,
|
|
156
|
+
) {
|
|
157
|
+
const Comp = asChild ? Slot : as;
|
|
158
|
+
// 18px / leading-snug / tight letter-spacing — establishes a clear visual
|
|
159
|
+
// step above the 14px CardDescription and 16px body text.
|
|
160
|
+
return (
|
|
161
|
+
<Comp
|
|
162
|
+
ref={ref}
|
|
163
|
+
className={cn("text-lg font-semibold leading-snug tracking-tight", className)}
|
|
164
|
+
{...props}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
CardTitle.displayName = "CardTitle";
|
|
169
|
+
|
|
170
|
+
// ----------------------------------------------------------------------------
|
|
171
|
+
// CardDescription
|
|
172
|
+
// ----------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
type CardDescriptionProps = React.HTMLAttributes<HTMLParagraphElement> & {
|
|
175
|
+
asChild?: boolean;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const CardDescription = forwardRef<HTMLParagraphElement, CardDescriptionProps>(function CardDescription(
|
|
179
|
+
{ className, asChild = false, ...props },
|
|
180
|
+
ref,
|
|
181
|
+
) {
|
|
182
|
+
const Comp = asChild ? Slot : "p";
|
|
183
|
+
return (
|
|
184
|
+
<Comp
|
|
185
|
+
ref={ref}
|
|
186
|
+
className={cn("text-sm leading-snug text-muted-foreground", className)}
|
|
187
|
+
{...props}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
CardDescription.displayName = "CardDescription";
|
|
192
|
+
|
|
193
|
+
// ----------------------------------------------------------------------------
|
|
194
|
+
// CardContent — `first:pt-0` so a Card with content-only renders flush.
|
|
195
|
+
// ----------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
type CardContentProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
198
|
+
asChild?: boolean;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const CardContent = forwardRef<HTMLDivElement, CardContentProps>(function CardContent(
|
|
202
|
+
{ className, asChild = false, ...props },
|
|
203
|
+
ref,
|
|
204
|
+
) {
|
|
205
|
+
const Comp = asChild ? Slot : "div";
|
|
206
|
+
return <Comp ref={ref} className={cn("pt-4 first:pt-0", className)} {...props} />;
|
|
207
|
+
});
|
|
208
|
+
CardContent.displayName = "CardContent";
|
|
209
|
+
|
|
210
|
+
// ----------------------------------------------------------------------------
|
|
211
|
+
// CardFooter — same `first:pt-0` guard so a footer-only Card doesn't get a
|
|
212
|
+
// stray top gap.
|
|
213
|
+
// ----------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
type CardFooterProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
216
|
+
asChild?: boolean;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(function CardFooter(
|
|
220
|
+
{ className, asChild = false, ...props },
|
|
221
|
+
ref,
|
|
222
|
+
) {
|
|
223
|
+
const Comp = asChild ? Slot : "div";
|
|
224
|
+
return (
|
|
225
|
+
<Comp
|
|
226
|
+
ref={ref}
|
|
227
|
+
className={cn("flex items-center pt-4 first:pt-0", className)}
|
|
228
|
+
{...props}
|
|
229
|
+
/>
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
CardFooter.displayName = "CardFooter";
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
Card,
|
|
236
|
+
CardHeader,
|
|
237
|
+
CardTitle,
|
|
238
|
+
CardDescription,
|
|
239
|
+
CardContent,
|
|
240
|
+
CardFooter,
|
|
241
|
+
cardVariants,
|
|
242
|
+
};
|
|
243
|
+
export type {
|
|
244
|
+
CardProps,
|
|
245
|
+
CardHeaderProps,
|
|
246
|
+
CardTitleProps,
|
|
247
|
+
CardDescriptionProps,
|
|
248
|
+
CardContentProps,
|
|
249
|
+
CardFooterProps,
|
|
250
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Checkbox } from "./checkbox";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Checkbox> = {
|
|
7
|
+
title: "Forms/Checkbox",
|
|
8
|
+
component: Checkbox,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: { control: "select", options: ["sm", "md", "lg"] },
|
|
12
|
+
invalid: { control: "boolean" },
|
|
13
|
+
disabled: { control: "boolean" },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Checkbox>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {};
|
|
21
|
+
|
|
22
|
+
export const Sizes: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="flex items-center gap-6">
|
|
25
|
+
<Checkbox size="sm" defaultChecked label="Small" />
|
|
26
|
+
<Checkbox size="md" defaultChecked label="Medium" />
|
|
27
|
+
<Checkbox size="lg" defaultChecked label="Large" />
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const WithLabel: Story = {
|
|
33
|
+
args: { label: "Accept the terms and conditions" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const WithDescription: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
label: "Email notifications",
|
|
39
|
+
description: "Receive weekly digest emails about your account activity.",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Checked: Story = {
|
|
44
|
+
args: { defaultChecked: true, label: "Default checked" },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Indeterminate: Story = {
|
|
48
|
+
render: () => {
|
|
49
|
+
const Wrapper = () => {
|
|
50
|
+
const [state, setState] = useState<"indeterminate" | boolean>("indeterminate");
|
|
51
|
+
return (
|
|
52
|
+
<Checkbox
|
|
53
|
+
checked={state}
|
|
54
|
+
onCheckedChange={(v) => setState(v)}
|
|
55
|
+
label="Select all"
|
|
56
|
+
description={`Current state: ${String(state)}`}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
return <Wrapper />;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Invalid: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
invalid: true,
|
|
67
|
+
label: "You must accept to continue",
|
|
68
|
+
description: "This field is required.",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Disabled: Story = {
|
|
73
|
+
render: () => (
|
|
74
|
+
<div className="space-y-2">
|
|
75
|
+
<Checkbox disabled label="Disabled unchecked" />
|
|
76
|
+
<Checkbox disabled defaultChecked label="Disabled checked" />
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const ListGroup: Story = {
|
|
82
|
+
render: () => (
|
|
83
|
+
<div className="w-72 space-y-3">
|
|
84
|
+
<Checkbox label="Marketing emails" description="Product updates and offers." />
|
|
85
|
+
<Checkbox
|
|
86
|
+
label="Security alerts"
|
|
87
|
+
description="Sign-in attempts and password changes."
|
|
88
|
+
defaultChecked
|
|
89
|
+
/>
|
|
90
|
+
<Checkbox label="Newsletter" description="Monthly company updates." />
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
};
|