@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.
Files changed (218) hide show
  1. package/components/ui/.gitkeep +0 -0
  2. package/components/ui/accordion.stories.tsx +231 -0
  3. package/components/ui/accordion.tsx +250 -0
  4. package/components/ui/app-shell.stories.tsx +270 -0
  5. package/components/ui/app-shell.tsx +491 -0
  6. package/components/ui/avatar.stories.tsx +174 -0
  7. package/components/ui/avatar.tsx +257 -0
  8. package/components/ui/badge.stories.tsx +127 -0
  9. package/components/ui/badge.tsx +146 -0
  10. package/components/ui/breadcrumb.stories.tsx +92 -0
  11. package/components/ui/breadcrumb.tsx +302 -0
  12. package/components/ui/button.stories.tsx +186 -0
  13. package/components/ui/button.tsx +128 -0
  14. package/components/ui/card.stories.tsx +279 -0
  15. package/components/ui/card.tsx +250 -0
  16. package/components/ui/checkbox.stories.tsx +93 -0
  17. package/components/ui/checkbox.tsx +131 -0
  18. package/components/ui/combobox.stories.tsx +489 -0
  19. package/components/ui/combobox.tsx +874 -0
  20. package/components/ui/context-menu.stories.tsx +202 -0
  21. package/components/ui/context-menu.tsx +309 -0
  22. package/components/ui/data-table.stories.tsx +227 -0
  23. package/components/ui/data-table.tsx +539 -0
  24. package/components/ui/date-picker.stories.tsx +225 -0
  25. package/components/ui/date-picker.tsx +597 -0
  26. package/components/ui/dialog.stories.tsx +193 -0
  27. package/components/ui/dialog.tsx +262 -0
  28. package/components/ui/divider.stories.tsx +84 -0
  29. package/components/ui/divider.tsx +135 -0
  30. package/components/ui/drawer.stories.tsx +218 -0
  31. package/components/ui/drawer.tsx +329 -0
  32. package/components/ui/dropdown-menu.stories.tsx +270 -0
  33. package/components/ui/dropdown-menu.tsx +353 -0
  34. package/components/ui/empty-state.stories.tsx +121 -0
  35. package/components/ui/empty-state.tsx +289 -0
  36. package/components/ui/field-group.stories.tsx +201 -0
  37. package/components/ui/field-group.tsx +276 -0
  38. package/components/ui/form.stories.tsx +219 -0
  39. package/components/ui/form.tsx +542 -0
  40. package/components/ui/input.stories.tsx +154 -0
  41. package/components/ui/input.tsx +208 -0
  42. package/components/ui/label.stories.tsx +84 -0
  43. package/components/ui/label.tsx +98 -0
  44. package/components/ui/page-header.stories.tsx +136 -0
  45. package/components/ui/page-header.tsx +315 -0
  46. package/components/ui/pagination.stories.tsx +136 -0
  47. package/components/ui/pagination.tsx +427 -0
  48. package/components/ui/popover.stories.tsx +212 -0
  49. package/components/ui/popover.tsx +167 -0
  50. package/components/ui/radio-group.stories.tsx +96 -0
  51. package/components/ui/radio-group.tsx +250 -0
  52. package/components/ui/select.stories.tsx +203 -0
  53. package/components/ui/select.tsx +318 -0
  54. package/components/ui/sidebar.stories.tsx +186 -0
  55. package/components/ui/sidebar.tsx +623 -0
  56. package/components/ui/skeleton.stories.tsx +131 -0
  57. package/components/ui/skeleton.tsx +311 -0
  58. package/components/ui/switch.stories.tsx +74 -0
  59. package/components/ui/switch.tsx +186 -0
  60. package/components/ui/table.stories.tsx +107 -0
  61. package/components/ui/table.tsx +285 -0
  62. package/components/ui/tabs.stories.tsx +222 -0
  63. package/components/ui/tabs.tsx +287 -0
  64. package/components/ui/textarea.stories.tsx +96 -0
  65. package/components/ui/textarea.tsx +182 -0
  66. package/components/ui/toast.stories.tsx +169 -0
  67. package/components/ui/toast.tsx +250 -0
  68. package/components/ui/tooltip.stories.tsx +146 -0
  69. package/components/ui/tooltip.tsx +156 -0
  70. package/components/ui/top-bar.stories.tsx +182 -0
  71. package/components/ui/top-bar.tsx +155 -0
  72. package/dist/components/ui/accordion.d.ts +45 -0
  73. package/dist/components/ui/accordion.d.ts.map +1 -0
  74. package/dist/components/ui/accordion.js +99 -0
  75. package/dist/components/ui/accordion.js.map +1 -0
  76. package/dist/components/ui/app-shell.d.ts +70 -0
  77. package/dist/components/ui/app-shell.d.ts.map +1 -0
  78. package/dist/components/ui/app-shell.js +199 -0
  79. package/dist/components/ui/app-shell.js.map +1 -0
  80. package/dist/components/ui/avatar.d.ts +41 -0
  81. package/dist/components/ui/avatar.d.ts.map +1 -0
  82. package/dist/components/ui/avatar.js +104 -0
  83. package/dist/components/ui/avatar.js.map +1 -0
  84. package/dist/components/ui/badge.d.ts +27 -0
  85. package/dist/components/ui/badge.d.ts.map +1 -0
  86. package/dist/components/ui/badge.js +65 -0
  87. package/dist/components/ui/badge.js.map +1 -0
  88. package/dist/components/ui/breadcrumb.d.ts +35 -0
  89. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  90. package/dist/components/ui/breadcrumb.js +88 -0
  91. package/dist/components/ui/breadcrumb.js.map +1 -0
  92. package/dist/components/ui/button.d.ts +26 -0
  93. package/dist/components/ui/button.d.ts.map +1 -0
  94. package/dist/components/ui/button.js +73 -0
  95. package/dist/components/ui/button.js.map +1 -0
  96. package/dist/components/ui/card.d.ts +52 -0
  97. package/dist/components/ui/card.d.ts.map +1 -0
  98. package/dist/components/ui/card.js +96 -0
  99. package/dist/components/ui/card.js.map +1 -0
  100. package/dist/components/ui/checkbox.d.ts +18 -0
  101. package/dist/components/ui/checkbox.d.ts.map +1 -0
  102. package/dist/components/ui/checkbox.js +59 -0
  103. package/dist/components/ui/checkbox.js.map +1 -0
  104. package/dist/components/ui/combobox.d.ts +194 -0
  105. package/dist/components/ui/combobox.d.ts.map +1 -0
  106. package/dist/components/ui/combobox.js +361 -0
  107. package/dist/components/ui/combobox.js.map +1 -0
  108. package/dist/components/ui/context-menu.d.ts +46 -0
  109. package/dist/components/ui/context-menu.d.ts.map +1 -0
  110. package/dist/components/ui/context-menu.js +95 -0
  111. package/dist/components/ui/context-menu.js.map +1 -0
  112. package/dist/components/ui/data-table.d.ts +53 -0
  113. package/dist/components/ui/data-table.d.ts.map +1 -0
  114. package/dist/components/ui/data-table.js +163 -0
  115. package/dist/components/ui/data-table.js.map +1 -0
  116. package/dist/components/ui/date-picker.d.ts +103 -0
  117. package/dist/components/ui/date-picker.d.ts.map +1 -0
  118. package/dist/components/ui/date-picker.js +306 -0
  119. package/dist/components/ui/date-picker.js.map +1 -0
  120. package/dist/components/ui/dialog.d.ts +40 -0
  121. package/dist/components/ui/dialog.d.ts.map +1 -0
  122. package/dist/components/ui/dialog.js +110 -0
  123. package/dist/components/ui/dialog.js.map +1 -0
  124. package/dist/components/ui/divider.d.ts +30 -0
  125. package/dist/components/ui/divider.d.ts.map +1 -0
  126. package/dist/components/ui/divider.js +62 -0
  127. package/dist/components/ui/divider.js.map +1 -0
  128. package/dist/components/ui/drawer.d.ts +56 -0
  129. package/dist/components/ui/drawer.d.ts.map +1 -0
  130. package/dist/components/ui/drawer.js +147 -0
  131. package/dist/components/ui/drawer.js.map +1 -0
  132. package/dist/components/ui/dropdown-menu.d.ts +63 -0
  133. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  134. package/dist/components/ui/dropdown-menu.js +116 -0
  135. package/dist/components/ui/dropdown-menu.js.map +1 -0
  136. package/dist/components/ui/empty-state.d.ts +43 -0
  137. package/dist/components/ui/empty-state.d.ts.map +1 -0
  138. package/dist/components/ui/empty-state.js +128 -0
  139. package/dist/components/ui/empty-state.js.map +1 -0
  140. package/dist/components/ui/field-group.d.ts +38 -0
  141. package/dist/components/ui/field-group.d.ts.map +1 -0
  142. package/dist/components/ui/field-group.js +107 -0
  143. package/dist/components/ui/field-group.js.map +1 -0
  144. package/dist/components/ui/form.d.ts +67 -0
  145. package/dist/components/ui/form.d.ts.map +1 -0
  146. package/dist/components/ui/form.js +286 -0
  147. package/dist/components/ui/form.js.map +1 -0
  148. package/dist/components/ui/input.d.ts +36 -0
  149. package/dist/components/ui/input.d.ts.map +1 -0
  150. package/dist/components/ui/input.js +99 -0
  151. package/dist/components/ui/input.js.map +1 -0
  152. package/dist/components/ui/label.d.ts +37 -0
  153. package/dist/components/ui/label.d.ts.map +1 -0
  154. package/dist/components/ui/label.js +34 -0
  155. package/dist/components/ui/label.js.map +1 -0
  156. package/dist/components/ui/page-header.d.ts +65 -0
  157. package/dist/components/ui/page-header.d.ts.map +1 -0
  158. package/dist/components/ui/page-header.js +140 -0
  159. package/dist/components/ui/page-header.js.map +1 -0
  160. package/dist/components/ui/pagination.d.ts +67 -0
  161. package/dist/components/ui/pagination.d.ts.map +1 -0
  162. package/dist/components/ui/pagination.js +109 -0
  163. package/dist/components/ui/pagination.js.map +1 -0
  164. package/dist/components/ui/popover.d.ts +28 -0
  165. package/dist/components/ui/popover.d.ts.map +1 -0
  166. package/dist/components/ui/popover.js +85 -0
  167. package/dist/components/ui/popover.js.map +1 -0
  168. package/dist/components/ui/radio-group.d.ts +35 -0
  169. package/dist/components/ui/radio-group.d.ts.map +1 -0
  170. package/dist/components/ui/radio-group.js +103 -0
  171. package/dist/components/ui/radio-group.js.map +1 -0
  172. package/dist/components/ui/select.d.ts +42 -0
  173. package/dist/components/ui/select.d.ts.map +1 -0
  174. package/dist/components/ui/select.js +86 -0
  175. package/dist/components/ui/select.js.map +1 -0
  176. package/dist/components/ui/sidebar.d.ts +59 -0
  177. package/dist/components/ui/sidebar.d.ts.map +1 -0
  178. package/dist/components/ui/sidebar.js +189 -0
  179. package/dist/components/ui/sidebar.js.map +1 -0
  180. package/dist/components/ui/skeleton.d.ts +77 -0
  181. package/dist/components/ui/skeleton.d.ts.map +1 -0
  182. package/dist/components/ui/skeleton.js +115 -0
  183. package/dist/components/ui/skeleton.js.map +1 -0
  184. package/dist/components/ui/switch.d.ts +26 -0
  185. package/dist/components/ui/switch.d.ts.map +1 -0
  186. package/dist/components/ui/switch.js +84 -0
  187. package/dist/components/ui/switch.js.map +1 -0
  188. package/dist/components/ui/table.d.ts +52 -0
  189. package/dist/components/ui/table.d.ts.map +1 -0
  190. package/dist/components/ui/table.js +109 -0
  191. package/dist/components/ui/table.js.map +1 -0
  192. package/dist/components/ui/tabs.d.ts +42 -0
  193. package/dist/components/ui/tabs.d.ts.map +1 -0
  194. package/dist/components/ui/tabs.js +163 -0
  195. package/dist/components/ui/tabs.js.map +1 -0
  196. package/dist/components/ui/textarea.d.ts +26 -0
  197. package/dist/components/ui/textarea.d.ts.map +1 -0
  198. package/dist/components/ui/textarea.js +96 -0
  199. package/dist/components/ui/textarea.js.map +1 -0
  200. package/dist/components/ui/toast.d.ts +77 -0
  201. package/dist/components/ui/toast.d.ts.map +1 -0
  202. package/dist/components/ui/toast.js +141 -0
  203. package/dist/components/ui/toast.js.map +1 -0
  204. package/dist/components/ui/tooltip.d.ts +31 -0
  205. package/dist/components/ui/tooltip.d.ts.map +1 -0
  206. package/dist/components/ui/tooltip.js +71 -0
  207. package/dist/components/ui/tooltip.js.map +1 -0
  208. package/dist/components/ui/top-bar.d.ts +30 -0
  209. package/dist/components/ui/top-bar.d.ts.map +1 -0
  210. package/dist/components/ui/top-bar.js +64 -0
  211. package/dist/components/ui/top-bar.js.map +1 -0
  212. package/dist/lib/utils.d.ts +3 -0
  213. package/dist/lib/utils.d.ts.map +1 -0
  214. package/dist/lib/utils.js +6 -0
  215. package/dist/lib/utils.js.map +1 -0
  216. package/lib/utils.ts +6 -0
  217. package/package.json +112 -0
  218. 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
+ };