@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
|
File without changes
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { RiBookOpenLine, RiNotificationLine, RiUserLine } from "@remixicon/react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Accordion,
|
|
7
|
+
AccordionContent,
|
|
8
|
+
AccordionItem,
|
|
9
|
+
AccordionTrigger,
|
|
10
|
+
useAccordion,
|
|
11
|
+
} from "./accordion";
|
|
12
|
+
import { Badge } from "./badge";
|
|
13
|
+
import { Button } from "./button";
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof Accordion> = {
|
|
16
|
+
title: "Data/Accordion",
|
|
17
|
+
component: Accordion,
|
|
18
|
+
parameters: { layout: "padded" },
|
|
19
|
+
argTypes: {
|
|
20
|
+
variant: { control: "select", options: ["default", "bordered", "separated", "filled"] },
|
|
21
|
+
size: { control: "select", options: ["sm", "md", "lg"] },
|
|
22
|
+
iconPosition: { control: "select", options: ["start", "end"] },
|
|
23
|
+
type: { control: "select", options: ["single", "multiple"] },
|
|
24
|
+
collapsible: { control: "boolean" },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof Accordion>;
|
|
30
|
+
|
|
31
|
+
const ITEMS = [
|
|
32
|
+
{
|
|
33
|
+
value: "shipping",
|
|
34
|
+
title: "Shipping & Delivery",
|
|
35
|
+
body: "Orders ship in 1–2 business days. Free delivery over MAD 500.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
value: "returns",
|
|
39
|
+
title: "Returns & Refunds",
|
|
40
|
+
body: "Return any unused item within 30 days for a full refund.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: "support",
|
|
44
|
+
title: "Customer Support",
|
|
45
|
+
body: "Reach our team via chat 9am–6pm or email anytime.",
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const Default: Story = {
|
|
50
|
+
args: { type: "single", collapsible: true, variant: "default", defaultValue: "shipping" },
|
|
51
|
+
render: (args) => (
|
|
52
|
+
<Accordion {...args} className="w-[28rem]">
|
|
53
|
+
{ITEMS.map((it) => (
|
|
54
|
+
<AccordionItem key={it.value} value={it.value}>
|
|
55
|
+
<AccordionTrigger>{it.title}</AccordionTrigger>
|
|
56
|
+
<AccordionContent>{it.body}</AccordionContent>
|
|
57
|
+
</AccordionItem>
|
|
58
|
+
))}
|
|
59
|
+
</Accordion>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Bordered: Story = {
|
|
64
|
+
args: { type: "single", collapsible: true, variant: "bordered", defaultValue: "shipping" },
|
|
65
|
+
render: Default.render!,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Separated: Story = {
|
|
69
|
+
args: { type: "single", collapsible: true, variant: "separated", defaultValue: "shipping" },
|
|
70
|
+
render: Default.render!,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const Filled: Story = {
|
|
74
|
+
args: { type: "single", collapsible: true, variant: "filled", defaultValue: "shipping" },
|
|
75
|
+
render: Default.render!,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const IconPositionStart: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
type: "single",
|
|
81
|
+
collapsible: true,
|
|
82
|
+
variant: "default",
|
|
83
|
+
iconPosition: "start",
|
|
84
|
+
defaultValue: "shipping",
|
|
85
|
+
},
|
|
86
|
+
render: Default.render!,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Sizes: Story = {
|
|
90
|
+
render: () => (
|
|
91
|
+
<div className="w-[28rem] space-y-6">
|
|
92
|
+
{(["sm", "md", "lg"] as const).map((size) => (
|
|
93
|
+
<Accordion
|
|
94
|
+
key={size}
|
|
95
|
+
type="single"
|
|
96
|
+
collapsible
|
|
97
|
+
variant="bordered"
|
|
98
|
+
size={size}
|
|
99
|
+
defaultValue="a"
|
|
100
|
+
>
|
|
101
|
+
<AccordionItem value="a">
|
|
102
|
+
<AccordionTrigger>{size.toUpperCase()} item A</AccordionTrigger>
|
|
103
|
+
<AccordionContent>Sized for the {size} density.</AccordionContent>
|
|
104
|
+
</AccordionItem>
|
|
105
|
+
<AccordionItem value="b">
|
|
106
|
+
<AccordionTrigger>{size.toUpperCase()} item B</AccordionTrigger>
|
|
107
|
+
<AccordionContent>Sized for the {size} density.</AccordionContent>
|
|
108
|
+
</AccordionItem>
|
|
109
|
+
</Accordion>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const WithIconsBadgesDescriptions: Story = {
|
|
116
|
+
args: { type: "single", collapsible: true, variant: "bordered" },
|
|
117
|
+
render: (args) => (
|
|
118
|
+
<Accordion {...args} className="w-[32rem]">
|
|
119
|
+
<AccordionItem value="profile">
|
|
120
|
+
<AccordionTrigger
|
|
121
|
+
icon={<RiUserLine />}
|
|
122
|
+
badge={
|
|
123
|
+
<Badge size="xs" variant="info">
|
|
124
|
+
New
|
|
125
|
+
</Badge>
|
|
126
|
+
}
|
|
127
|
+
description="Name, photo, contact information"
|
|
128
|
+
>
|
|
129
|
+
Profile
|
|
130
|
+
</AccordionTrigger>
|
|
131
|
+
<AccordionContent>Manage your profile details.</AccordionContent>
|
|
132
|
+
</AccordionItem>
|
|
133
|
+
<AccordionItem value="notifications">
|
|
134
|
+
<AccordionTrigger
|
|
135
|
+
icon={<RiNotificationLine />}
|
|
136
|
+
badge={
|
|
137
|
+
<Badge size="xs" variant="warning">
|
|
138
|
+
3
|
|
139
|
+
</Badge>
|
|
140
|
+
}
|
|
141
|
+
description="Email and push preferences"
|
|
142
|
+
>
|
|
143
|
+
Notifications
|
|
144
|
+
</AccordionTrigger>
|
|
145
|
+
<AccordionContent>Control which notifications you receive.</AccordionContent>
|
|
146
|
+
</AccordionItem>
|
|
147
|
+
<AccordionItem value="docs">
|
|
148
|
+
<AccordionTrigger icon={<RiBookOpenLine />} description="Read more about your account">
|
|
149
|
+
Documentation
|
|
150
|
+
</AccordionTrigger>
|
|
151
|
+
<AccordionContent>Links to relevant help articles.</AccordionContent>
|
|
152
|
+
</AccordionItem>
|
|
153
|
+
</Accordion>
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const MultipleWithHelpers: Story = {
|
|
158
|
+
render: () => {
|
|
159
|
+
function Demo() {
|
|
160
|
+
const acc = useAccordion(["a"]);
|
|
161
|
+
const allValues = ["a", "b", "c"];
|
|
162
|
+
return (
|
|
163
|
+
<div className="w-[28rem] space-y-3">
|
|
164
|
+
<div className="flex gap-2">
|
|
165
|
+
<Button size="sm" variant="outline" onClick={() => acc.openAll(allValues)}>
|
|
166
|
+
Open all
|
|
167
|
+
</Button>
|
|
168
|
+
<Button size="sm" variant="outline" onClick={() => acc.closeAll()}>
|
|
169
|
+
Close all
|
|
170
|
+
</Button>
|
|
171
|
+
<Button size="sm" variant="outline" onClick={() => acc.toggle("b")}>
|
|
172
|
+
Toggle B
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
<Accordion
|
|
176
|
+
type="multiple"
|
|
177
|
+
value={acc.value}
|
|
178
|
+
onValueChange={acc.setValue}
|
|
179
|
+
variant="bordered"
|
|
180
|
+
>
|
|
181
|
+
<AccordionItem value="a">
|
|
182
|
+
<AccordionTrigger>Item A</AccordionTrigger>
|
|
183
|
+
<AccordionContent>Body A</AccordionContent>
|
|
184
|
+
</AccordionItem>
|
|
185
|
+
<AccordionItem value="b">
|
|
186
|
+
<AccordionTrigger>Item B</AccordionTrigger>
|
|
187
|
+
<AccordionContent>Body B</AccordionContent>
|
|
188
|
+
</AccordionItem>
|
|
189
|
+
<AccordionItem value="c">
|
|
190
|
+
<AccordionTrigger>Item C</AccordionTrigger>
|
|
191
|
+
<AccordionContent>Body C</AccordionContent>
|
|
192
|
+
</AccordionItem>
|
|
193
|
+
</Accordion>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return <Demo />;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const Controlled: Story = {
|
|
202
|
+
render: () => {
|
|
203
|
+
function Demo() {
|
|
204
|
+
const [value, setValue] = useState<string | undefined>("a");
|
|
205
|
+
return (
|
|
206
|
+
<div className="w-[28rem] space-y-3">
|
|
207
|
+
<p className="text-muted-foreground text-sm">
|
|
208
|
+
Open: <code>{String(value)}</code>
|
|
209
|
+
</p>
|
|
210
|
+
<Accordion
|
|
211
|
+
type="single"
|
|
212
|
+
collapsible
|
|
213
|
+
value={value ?? ""}
|
|
214
|
+
onValueChange={(v) => setValue(v || undefined)}
|
|
215
|
+
variant="bordered"
|
|
216
|
+
>
|
|
217
|
+
<AccordionItem value="a">
|
|
218
|
+
<AccordionTrigger>A</AccordionTrigger>
|
|
219
|
+
<AccordionContent>Body A</AccordionContent>
|
|
220
|
+
</AccordionItem>
|
|
221
|
+
<AccordionItem value="b">
|
|
222
|
+
<AccordionTrigger>B</AccordionTrigger>
|
|
223
|
+
<AccordionContent>Body B</AccordionContent>
|
|
224
|
+
</AccordionItem>
|
|
225
|
+
</Accordion>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return <Demo />;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, forwardRef, useCallback, useContext, useMemo, useState } from "react";
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
5
|
+
import { RiArrowDownSLine } from "@remixicon/react";
|
|
6
|
+
import { cva } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-4-components.md §4.
|
|
11
|
+
|
|
12
|
+
type AccordionVariant = "default" | "bordered" | "separated" | "filled";
|
|
13
|
+
type AccordionSize = "sm" | "md" | "lg";
|
|
14
|
+
type IconPosition = "start" | "end";
|
|
15
|
+
|
|
16
|
+
interface AccordionContextValue {
|
|
17
|
+
variant: AccordionVariant;
|
|
18
|
+
size: AccordionSize;
|
|
19
|
+
iconPosition: IconPosition;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null);
|
|
23
|
+
|
|
24
|
+
function useAccordionContext(): AccordionContextValue {
|
|
25
|
+
const ctx = useContext(AccordionContext);
|
|
26
|
+
if (!ctx) throw new Error("Accordion subcomponents must be used inside <Accordion>");
|
|
27
|
+
return ctx;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- useAccordion() (multi-mode helper) -------------------------------------
|
|
31
|
+
|
|
32
|
+
interface UseAccordionAPI {
|
|
33
|
+
value: string[];
|
|
34
|
+
setValue: (next: string[]) => void;
|
|
35
|
+
openAll: (allValues: string[]) => void;
|
|
36
|
+
closeAll: () => void;
|
|
37
|
+
toggle: (value: string) => void;
|
|
38
|
+
isOpen: (value: string) => boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function useAccordion(initial: string[] = []): UseAccordionAPI {
|
|
42
|
+
const [value, setValue] = useState<string[]>(initial);
|
|
43
|
+
const openAll = useCallback((all: string[]) => setValue([...new Set(all)]), []);
|
|
44
|
+
const closeAll = useCallback(() => setValue([]), []);
|
|
45
|
+
const toggle = useCallback(
|
|
46
|
+
(v: string) =>
|
|
47
|
+
setValue((prev) => (prev.includes(v) ? prev.filter((x) => x !== v) : [...prev, v])),
|
|
48
|
+
[],
|
|
49
|
+
);
|
|
50
|
+
const isOpen = useCallback((v: string) => value.includes(v), [value]);
|
|
51
|
+
return { value, setValue, openAll, closeAll, toggle, isOpen };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// --- Accordion (root) -------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const accordionVariants = cva("", {
|
|
57
|
+
variants: {
|
|
58
|
+
variant: {
|
|
59
|
+
default: "",
|
|
60
|
+
bordered: "rounded-md border border-border overflow-hidden",
|
|
61
|
+
separated: "space-y-2",
|
|
62
|
+
filled: "",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
defaultVariants: { variant: "default" },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
type AccordionRootProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root> & {
|
|
69
|
+
variant?: AccordionVariant;
|
|
70
|
+
size?: AccordionSize;
|
|
71
|
+
iconPosition?: IconPosition;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const Accordion = forwardRef<React.ElementRef<typeof AccordionPrimitive.Root>, AccordionRootProps>(
|
|
75
|
+
function Accordion(
|
|
76
|
+
{ className, variant = "default", size = "md", iconPosition = "end", ...props },
|
|
77
|
+
ref,
|
|
78
|
+
) {
|
|
79
|
+
const ctx = useMemo(() => ({ variant, size, iconPosition }), [variant, size, iconPosition]);
|
|
80
|
+
return (
|
|
81
|
+
<AccordionContext.Provider value={ctx}>
|
|
82
|
+
<AccordionPrimitive.Root
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={cn(accordionVariants({ variant }), className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
</AccordionContext.Provider>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
Accordion.displayName = "Accordion";
|
|
92
|
+
|
|
93
|
+
// --- AccordionItem ----------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
const accordionItemVariants = cva("", {
|
|
96
|
+
variants: {
|
|
97
|
+
variant: {
|
|
98
|
+
default: "border-b border-border last:border-b-0",
|
|
99
|
+
bordered: "border-b border-border last:border-b-0",
|
|
100
|
+
separated: "rounded-md border border-border overflow-hidden",
|
|
101
|
+
filled: ["rounded-md", "data-[state=open]:bg-muted"].join(" "),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
defaultVariants: { variant: "default" },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
type AccordionItemProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>;
|
|
108
|
+
|
|
109
|
+
const AccordionItem = forwardRef<
|
|
110
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
111
|
+
AccordionItemProps
|
|
112
|
+
>(function AccordionItem({ className, ...props }, ref) {
|
|
113
|
+
const { variant } = useAccordionContext();
|
|
114
|
+
return (
|
|
115
|
+
<AccordionPrimitive.Item
|
|
116
|
+
ref={ref}
|
|
117
|
+
className={cn(accordionItemVariants({ variant }), className)}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
AccordionItem.displayName = "AccordionItem";
|
|
123
|
+
|
|
124
|
+
// --- AccordionTrigger -------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const accordionTriggerVariants = cva(
|
|
127
|
+
[
|
|
128
|
+
"flex w-full items-center gap-3 font-medium text-foreground select-none cursor-pointer",
|
|
129
|
+
"transition-colors",
|
|
130
|
+
"hover:bg-accent/50",
|
|
131
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
132
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
133
|
+
"[&_svg]:size-4 [&_svg]:shrink-0",
|
|
134
|
+
].join(" "),
|
|
135
|
+
{
|
|
136
|
+
variants: {
|
|
137
|
+
size: {
|
|
138
|
+
sm: "px-3 py-2 text-xs",
|
|
139
|
+
md: "px-4 py-3 text-sm",
|
|
140
|
+
lg: "px-5 py-4 text-base",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
defaultVariants: { size: "md" },
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
type AccordionTriggerProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> & {
|
|
148
|
+
icon?: React.ReactNode;
|
|
149
|
+
badge?: React.ReactNode;
|
|
150
|
+
description?: React.ReactNode;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const AccordionTrigger = forwardRef<
|
|
154
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
155
|
+
AccordionTriggerProps
|
|
156
|
+
>(function AccordionTrigger({ className, icon, badge, description, children, ...props }, ref) {
|
|
157
|
+
const { size, iconPosition } = useAccordionContext();
|
|
158
|
+
return (
|
|
159
|
+
<AccordionPrimitive.Header className="flex">
|
|
160
|
+
<AccordionPrimitive.Trigger
|
|
161
|
+
ref={ref}
|
|
162
|
+
className={cn(
|
|
163
|
+
accordionTriggerVariants({ size }),
|
|
164
|
+
"group",
|
|
165
|
+
"data-[state=open]:[&_[data-chevron]]:rotate-180",
|
|
166
|
+
className,
|
|
167
|
+
)}
|
|
168
|
+
{...props}
|
|
169
|
+
>
|
|
170
|
+
{iconPosition === "start" ? (
|
|
171
|
+
<span data-chevron className="duration-normal shrink-0 transition-transform">
|
|
172
|
+
<RiArrowDownSLine />
|
|
173
|
+
</span>
|
|
174
|
+
) : null}
|
|
175
|
+
{icon ? <span className="shrink-0">{icon}</span> : null}
|
|
176
|
+
<span className="flex-1 text-start">
|
|
177
|
+
<span className="block">{children}</span>
|
|
178
|
+
{description ? (
|
|
179
|
+
<span className="text-muted-foreground mt-0.5 block text-xs font-normal">
|
|
180
|
+
{description}
|
|
181
|
+
</span>
|
|
182
|
+
) : null}
|
|
183
|
+
</span>
|
|
184
|
+
{badge ? <span className="shrink-0">{badge}</span> : null}
|
|
185
|
+
{iconPosition === "end" ? (
|
|
186
|
+
<span data-chevron className="duration-normal shrink-0 transition-transform">
|
|
187
|
+
<RiArrowDownSLine />
|
|
188
|
+
</span>
|
|
189
|
+
) : null}
|
|
190
|
+
</AccordionPrimitive.Trigger>
|
|
191
|
+
</AccordionPrimitive.Header>
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
AccordionTrigger.displayName = "AccordionTrigger";
|
|
195
|
+
|
|
196
|
+
// --- AccordionContent -------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
const accordionContentSizeVariants = cva("text-foreground", {
|
|
199
|
+
variants: {
|
|
200
|
+
size: {
|
|
201
|
+
sm: "px-3 pb-2 text-xs",
|
|
202
|
+
md: "px-4 pb-3 text-sm",
|
|
203
|
+
lg: "px-5 pb-4 text-base",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
defaultVariants: { size: "md" },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
type AccordionContentProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>;
|
|
210
|
+
|
|
211
|
+
const AccordionContent = forwardRef<
|
|
212
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
213
|
+
AccordionContentProps
|
|
214
|
+
>(function AccordionContent({ className, children, ...props }, ref) {
|
|
215
|
+
const { size } = useAccordionContext();
|
|
216
|
+
return (
|
|
217
|
+
<AccordionPrimitive.Content
|
|
218
|
+
ref={ref}
|
|
219
|
+
className={cn(
|
|
220
|
+
"overflow-hidden",
|
|
221
|
+
"data-[state=open]:animate-[collapse-down_var(--duration-normal,200ms)_var(--ease-out,ease-out)]",
|
|
222
|
+
"data-[state=closed]:animate-[collapse-up_var(--duration-normal,200ms)_var(--ease-in,ease-in)]",
|
|
223
|
+
// Expose the natural height to the keyframe via the variable set by Radix.
|
|
224
|
+
"[--collapsible-content-height:var(--radix-accordion-content-height)]",
|
|
225
|
+
)}
|
|
226
|
+
{...props}
|
|
227
|
+
>
|
|
228
|
+
<div className={cn(accordionContentSizeVariants({ size }), className)}>{children}</div>
|
|
229
|
+
</AccordionPrimitive.Content>
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
AccordionContent.displayName = "AccordionContent";
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
Accordion,
|
|
236
|
+
AccordionItem,
|
|
237
|
+
AccordionTrigger,
|
|
238
|
+
AccordionContent,
|
|
239
|
+
useAccordion,
|
|
240
|
+
accordionVariants,
|
|
241
|
+
accordionItemVariants,
|
|
242
|
+
accordionTriggerVariants,
|
|
243
|
+
};
|
|
244
|
+
export type {
|
|
245
|
+
AccordionRootProps,
|
|
246
|
+
AccordionItemProps,
|
|
247
|
+
AccordionTriggerProps,
|
|
248
|
+
AccordionContentProps,
|
|
249
|
+
UseAccordionAPI,
|
|
250
|
+
};
|