@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
@@ -0,0 +1,302 @@
1
+ "use client";
2
+
3
+ import { Fragment, forwardRef, useMemo } from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { RiArrowRightSLine, RiMoreLine } from "@remixicon/react";
6
+ import { cva, type VariantProps } from "class-variance-authority";
7
+
8
+ import { cn } from "@/lib/utils";
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuTrigger,
14
+ } from "./dropdown-menu";
15
+
16
+ // Per docs/emara-ui-phase-5-components.md §5.
17
+
18
+ // ===========================================================================
19
+ // Primitives (anatomy mirrors shadcn)
20
+ // ===========================================================================
21
+
22
+ const breadcrumbVariants = cva("flex items-center w-full", {
23
+ variants: {
24
+ size: {
25
+ sm: "text-xs",
26
+ md: "text-sm",
27
+ lg: "text-base",
28
+ },
29
+ },
30
+ defaultVariants: { size: "md" },
31
+ });
32
+
33
+ type BreadcrumbVariants = VariantProps<typeof breadcrumbVariants>;
34
+
35
+ type BreadcrumbProps = React.HTMLAttributes<HTMLElement> & BreadcrumbVariants;
36
+
37
+ const Breadcrumb = forwardRef<HTMLElement, BreadcrumbProps>(function Breadcrumb(
38
+ { className, size, ...props },
39
+ ref,
40
+ ) {
41
+ return (
42
+ <nav
43
+ ref={ref}
44
+ aria-label="Breadcrumb"
45
+ className={cn(breadcrumbVariants({ size }), className)}
46
+ {...props}
47
+ />
48
+ );
49
+ });
50
+ Breadcrumb.displayName = "Breadcrumb";
51
+
52
+ const BreadcrumbList = forwardRef<HTMLOListElement, React.OlHTMLAttributes<HTMLOListElement>>(
53
+ function BreadcrumbList({ className, ...props }, ref) {
54
+ return (
55
+ <ol
56
+ ref={ref}
57
+ className={cn(
58
+ "flex flex-wrap items-center gap-1.5 text-muted-foreground",
59
+ className,
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ },
65
+ );
66
+ BreadcrumbList.displayName = "BreadcrumbList";
67
+
68
+ const BreadcrumbItem = forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
69
+ function BreadcrumbItem({ className, ...props }, ref) {
70
+ return (
71
+ <li
72
+ ref={ref}
73
+ className={cn("inline-flex items-center gap-1.5", className)}
74
+ {...props}
75
+ />
76
+ );
77
+ },
78
+ );
79
+ BreadcrumbItem.displayName = "BreadcrumbItem";
80
+
81
+ type BreadcrumbLinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
82
+ asChild?: boolean;
83
+ };
84
+
85
+ const BreadcrumbLink = forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
86
+ function BreadcrumbLink({ className, asChild = false, ...props }, ref) {
87
+ const Comp = asChild ? Slot : "a";
88
+ return (
89
+ <Comp
90
+ ref={ref}
91
+ className={cn(
92
+ "inline-flex items-center gap-1 transition-colors hover:text-foreground hover:underline underline-offset-2 cursor-pointer",
93
+ "[&_svg]:size-3.5 [&_svg]:shrink-0",
94
+ className,
95
+ )}
96
+ {...props}
97
+ />
98
+ );
99
+ },
100
+ );
101
+ BreadcrumbLink.displayName = "BreadcrumbLink";
102
+
103
+ const BreadcrumbPage = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
104
+ function BreadcrumbPage({ className, ...props }, ref) {
105
+ return (
106
+ <span
107
+ ref={ref}
108
+ role="link"
109
+ aria-disabled="true"
110
+ aria-current="page"
111
+ className={cn(
112
+ "inline-flex items-center gap-1 font-medium text-foreground",
113
+ "[&_svg]:size-3.5 [&_svg]:shrink-0",
114
+ className,
115
+ )}
116
+ {...props}
117
+ />
118
+ );
119
+ },
120
+ );
121
+ BreadcrumbPage.displayName = "BreadcrumbPage";
122
+
123
+ const BreadcrumbSeparator = forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(
124
+ function BreadcrumbSeparator({ className, children, ...props }, ref) {
125
+ return (
126
+ <li
127
+ ref={ref}
128
+ role="presentation"
129
+ aria-hidden="true"
130
+ className={cn(
131
+ "[&_svg]:size-3.5 [&_svg]:shrink-0 text-muted-foreground",
132
+ className,
133
+ )}
134
+ {...props}
135
+ >
136
+ {children ?? <RiArrowRightSLine className="rtl-mirror" />}
137
+ </li>
138
+ );
139
+ },
140
+ );
141
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
142
+
143
+ const BreadcrumbEllipsis = forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
144
+ function BreadcrumbEllipsis({ className, ...props }, ref) {
145
+ return (
146
+ <span
147
+ ref={ref}
148
+ role="presentation"
149
+ aria-hidden="true"
150
+ className={cn(
151
+ "inline-flex size-6 items-center justify-center [&_svg]:size-4 [&_svg]:shrink-0",
152
+ className,
153
+ )}
154
+ {...props}
155
+ >
156
+ <RiMoreLine />
157
+ <span className="sr-only">More items</span>
158
+ </span>
159
+ );
160
+ },
161
+ );
162
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
163
+
164
+ // ===========================================================================
165
+ // Smart Breadcrumbs (Emara addition)
166
+ // ===========================================================================
167
+
168
+ interface BreadcrumbsItem {
169
+ label: React.ReactNode;
170
+ href?: string;
171
+ icon?: React.ReactNode;
172
+ }
173
+
174
+ interface BreadcrumbsProps extends BreadcrumbVariants {
175
+ items: BreadcrumbsItem[];
176
+ separator?: React.ReactNode;
177
+ maxItems?: number;
178
+ itemsBeforeCollapse?: number;
179
+ itemsAfterCollapse?: number;
180
+ className?: string;
181
+ }
182
+
183
+ const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(function Breadcrumbs(
184
+ {
185
+ items,
186
+ separator,
187
+ maxItems,
188
+ itemsBeforeCollapse = 1,
189
+ itemsAfterCollapse = 1,
190
+ size,
191
+ className,
192
+ },
193
+ ref,
194
+ ) {
195
+ const shouldCollapse =
196
+ maxItems !== undefined &&
197
+ items.length > maxItems &&
198
+ items.length > itemsBeforeCollapse + itemsAfterCollapse;
199
+
200
+ const { start, hidden, end } = useMemo(() => {
201
+ if (!shouldCollapse) {
202
+ return { start: items, hidden: [] as BreadcrumbsItem[], end: [] as BreadcrumbsItem[] };
203
+ }
204
+ return {
205
+ start: items.slice(0, itemsBeforeCollapse),
206
+ hidden: items.slice(itemsBeforeCollapse, items.length - itemsAfterCollapse),
207
+ end: items.slice(items.length - itemsAfterCollapse),
208
+ };
209
+ }, [items, itemsBeforeCollapse, itemsAfterCollapse, shouldCollapse]);
210
+
211
+ function renderItem(item: BreadcrumbsItem, isLast: boolean) {
212
+ const content = (
213
+ <>
214
+ {item.icon ? <span aria-hidden="true">{item.icon}</span> : null}
215
+ <span>{item.label}</span>
216
+ </>
217
+ );
218
+ if (!item.href || isLast) {
219
+ return <BreadcrumbPage>{content}</BreadcrumbPage>;
220
+ }
221
+ return <BreadcrumbLink href={item.href}>{content}</BreadcrumbLink>;
222
+ }
223
+
224
+ return (
225
+ <Breadcrumb ref={ref} {...(size !== undefined ? { size } : {})} className={className}>
226
+ <BreadcrumbList>
227
+ {start.map((item, idx) => {
228
+ const isLast = !shouldCollapse && idx === items.length - 1;
229
+ const showSeparator = idx < start.length - 1 || shouldCollapse || end.length > 0;
230
+ return (
231
+ <Fragment key={`s-${idx}`}>
232
+ <BreadcrumbItem>{renderItem(item, isLast)}</BreadcrumbItem>
233
+ {showSeparator ? (
234
+ <BreadcrumbSeparator>{separator}</BreadcrumbSeparator>
235
+ ) : null}
236
+ </Fragment>
237
+ );
238
+ })}
239
+
240
+ {shouldCollapse && hidden.length > 0 ? (
241
+ <>
242
+ <BreadcrumbItem>
243
+ <DropdownMenu>
244
+ <DropdownMenuTrigger
245
+ aria-label="Show more"
246
+ className="inline-flex items-center justify-center rounded p-0.5 cursor-pointer hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
247
+ >
248
+ <BreadcrumbEllipsis />
249
+ </DropdownMenuTrigger>
250
+ <DropdownMenuContent align="start">
251
+ {hidden.map((item, idx) => (
252
+ <DropdownMenuItem
253
+ key={idx}
254
+ {...(item.icon ? { icon: item.icon } : {})}
255
+ onSelect={() => {
256
+ if (item.href && typeof window !== "undefined") {
257
+ window.location.href = item.href;
258
+ }
259
+ }}
260
+ >
261
+ {item.label}
262
+ </DropdownMenuItem>
263
+ ))}
264
+ </DropdownMenuContent>
265
+ </DropdownMenu>
266
+ </BreadcrumbItem>
267
+ <BreadcrumbSeparator>{separator}</BreadcrumbSeparator>
268
+ </>
269
+ ) : null}
270
+
271
+ {end.map((item, idx) => {
272
+ const isLast = idx === end.length - 1;
273
+ return (
274
+ <Fragment key={`e-${idx}`}>
275
+ <BreadcrumbItem>{renderItem(item, isLast)}</BreadcrumbItem>
276
+ {!isLast ? <BreadcrumbSeparator>{separator}</BreadcrumbSeparator> : null}
277
+ </Fragment>
278
+ );
279
+ })}
280
+ </BreadcrumbList>
281
+ </Breadcrumb>
282
+ );
283
+ });
284
+ Breadcrumbs.displayName = "Breadcrumbs";
285
+
286
+ export {
287
+ Breadcrumb,
288
+ BreadcrumbList,
289
+ BreadcrumbItem,
290
+ BreadcrumbLink,
291
+ BreadcrumbPage,
292
+ BreadcrumbSeparator,
293
+ BreadcrumbEllipsis,
294
+ Breadcrumbs,
295
+ breadcrumbVariants,
296
+ };
297
+ export type {
298
+ BreadcrumbProps,
299
+ BreadcrumbLinkProps,
300
+ BreadcrumbsItem,
301
+ BreadcrumbsProps,
302
+ };
@@ -0,0 +1,186 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { RiArrowRightLine, RiDownloadLine, RiSearchLine } from "@remixicon/react";
3
+
4
+ import { Button } from "./button";
5
+
6
+ const meta: Meta<typeof Button> = {
7
+ title: "Foundations/Button",
8
+ component: Button,
9
+ parameters: { layout: "centered" },
10
+ argTypes: {
11
+ variant: {
12
+ control: "select",
13
+ options: [
14
+ "default",
15
+ "secondary",
16
+ "outline",
17
+ "ghost",
18
+ "destructive",
19
+ "link",
20
+ "success",
21
+ "warning",
22
+ "info",
23
+ ],
24
+ },
25
+ size: {
26
+ control: "select",
27
+ options: [
28
+ "xs",
29
+ "sm",
30
+ "default",
31
+ "lg",
32
+ "xl",
33
+ "icon-xs",
34
+ "icon-sm",
35
+ "icon",
36
+ "icon-lg",
37
+ "icon-xl",
38
+ ],
39
+ },
40
+ loading: { control: "boolean" },
41
+ disabled: { control: "boolean" },
42
+ fullWidth: { control: "boolean" },
43
+ },
44
+ args: {
45
+ children: "Button",
46
+ variant: "default",
47
+ size: "default",
48
+ },
49
+ };
50
+
51
+ export default meta;
52
+ type Story = StoryObj<typeof Button>;
53
+
54
+ export const Default: Story = {};
55
+
56
+ export const Variants: Story = {
57
+ render: (args) => (
58
+ <div className="flex flex-wrap items-center gap-3">
59
+ <Button {...args} variant="default">
60
+ Default
61
+ </Button>
62
+ <Button {...args} variant="secondary">
63
+ Secondary
64
+ </Button>
65
+ <Button {...args} variant="outline">
66
+ Outline
67
+ </Button>
68
+ <Button {...args} variant="ghost">
69
+ Ghost
70
+ </Button>
71
+ <Button {...args} variant="destructive">
72
+ Destructive
73
+ </Button>
74
+ <Button {...args} variant="link">
75
+ Link
76
+ </Button>
77
+ <Button {...args} variant="success">
78
+ Success
79
+ </Button>
80
+ <Button {...args} variant="warning">
81
+ Warning
82
+ </Button>
83
+ <Button {...args} variant="info">
84
+ Info
85
+ </Button>
86
+ </div>
87
+ ),
88
+ };
89
+
90
+ export const Sizes: Story = {
91
+ render: (args) => (
92
+ <div className="flex flex-wrap items-center gap-3">
93
+ <Button {...args} size="xs">
94
+ xs
95
+ </Button>
96
+ <Button {...args} size="sm">
97
+ sm
98
+ </Button>
99
+ <Button {...args} size="default">
100
+ default
101
+ </Button>
102
+ <Button {...args} size="lg">
103
+ lg
104
+ </Button>
105
+ <Button {...args} size="xl">
106
+ xl
107
+ </Button>
108
+ </div>
109
+ ),
110
+ };
111
+
112
+ export const IconSizes: Story = {
113
+ render: (args) => (
114
+ <div className="flex flex-wrap items-center gap-3">
115
+ <Button {...args} size="icon-xs" aria-label="Search">
116
+ <RiSearchLine />
117
+ </Button>
118
+ <Button {...args} size="icon-sm" aria-label="Search">
119
+ <RiSearchLine />
120
+ </Button>
121
+ <Button {...args} size="icon" aria-label="Search">
122
+ <RiSearchLine />
123
+ </Button>
124
+ <Button {...args} size="icon-lg" aria-label="Search">
125
+ <RiSearchLine />
126
+ </Button>
127
+ <Button {...args} size="icon-xl" aria-label="Search">
128
+ <RiSearchLine />
129
+ </Button>
130
+ </div>
131
+ ),
132
+ };
133
+
134
+ export const WithIcons: Story = {
135
+ args: {
136
+ leftIcon: <RiDownloadLine />,
137
+ rightIcon: <RiArrowRightLine className="rtl-mirror" />,
138
+ children: "Continue",
139
+ },
140
+ };
141
+
142
+ export const Loading: Story = {
143
+ args: { loading: true, children: "Save" },
144
+ };
145
+
146
+ export const LoadingWithText: Story = {
147
+ args: { loading: true, loadingText: "Saving…", children: "Save" },
148
+ };
149
+
150
+ export const Disabled: Story = {
151
+ args: { disabled: true, children: "Disabled" },
152
+ };
153
+
154
+ export const FullWidth: Story = {
155
+ args: { fullWidth: true, children: "Full width" },
156
+ parameters: { layout: "padded" },
157
+ render: (args) => (
158
+ <div className="w-80">
159
+ <Button {...args} />
160
+ </div>
161
+ ),
162
+ };
163
+
164
+ export const AsChildLink: Story = {
165
+ args: { asChild: true, variant: "link" },
166
+ render: (args) => (
167
+ <Button {...args}>
168
+ <a href="https://example.com">Anchor styled as Button</a>
169
+ </Button>
170
+ ),
171
+ };
172
+
173
+ export const States: Story = {
174
+ render: () => (
175
+ <div className="grid grid-cols-4 gap-3">
176
+ <Button>Default</Button>
177
+ <Button data-state="hover" className="hover:bg-primary/90">
178
+ Hover
179
+ </Button>
180
+ <Button disabled>Disabled</Button>
181
+ <Button loading loadingText="Loading">
182
+ Loading
183
+ </Button>
184
+ </div>
185
+ ),
186
+ };
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { RiLoader2Line } from "@remixicon/react";
6
+ import { cva, type VariantProps } from "class-variance-authority";
7
+
8
+ import { cn } from "@/lib/utils";
9
+
10
+ // Per docs/emara-ui-phase-1-components.md §1.
11
+
12
+ const buttonVariants = cva(
13
+ [
14
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap",
15
+ "rounded-md text-sm font-medium select-none cursor-pointer",
16
+ "transition-colors transition-shadow",
17
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
18
+ "disabled:pointer-events-none disabled:opacity-50",
19
+ "aria-busy:cursor-progress",
20
+ "[&_svg]:size-4 [&_svg]:shrink-0",
21
+ ].join(" "),
22
+ {
23
+ variants: {
24
+ variant: {
25
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
26
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
27
+ outline:
28
+ "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
29
+ ghost: "text-foreground hover:bg-accent hover:text-accent-foreground",
30
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
31
+ link: "text-primary underline-offset-4 hover:underline",
32
+ success:
33
+ "bg-success text-primary-foreground hover:bg-success/90 focus-visible:ring-success",
34
+ warning:
35
+ "bg-warning text-primary-foreground hover:bg-warning/90 focus-visible:ring-warning",
36
+ info: "bg-info text-primary-foreground hover:bg-info/90 focus-visible:ring-info",
37
+ },
38
+ size: {
39
+ xs: "h-7 px-2.5 text-xs",
40
+ sm: "h-8 px-3 text-xs",
41
+ default: "h-9 px-4 text-sm",
42
+ lg: "h-10 px-5 text-base",
43
+ xl: "h-12 px-6 text-base",
44
+ "icon-xs": "size-7 p-0",
45
+ "icon-sm": "size-8 p-0",
46
+ icon: "size-9 p-0",
47
+ "icon-lg": "size-10 p-0",
48
+ "icon-xl": "size-12 p-0",
49
+ },
50
+ fullWidth: {
51
+ true: "w-full",
52
+ false: "",
53
+ },
54
+ },
55
+ defaultVariants: {
56
+ variant: "default",
57
+ size: "default",
58
+ fullWidth: false,
59
+ },
60
+ },
61
+ );
62
+
63
+ type ButtonVariantProps = VariantProps<typeof buttonVariants>;
64
+
65
+ type ButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> &
66
+ ButtonVariantProps & {
67
+ asChild?: boolean;
68
+ type?: "button" | "submit" | "reset";
69
+ loading?: boolean;
70
+ loadingText?: string;
71
+ leftIcon?: React.ReactNode;
72
+ rightIcon?: React.ReactNode;
73
+ };
74
+
75
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
76
+ {
77
+ className,
78
+ variant,
79
+ size,
80
+ fullWidth,
81
+ asChild = false,
82
+ type = "button",
83
+ loading = false,
84
+ loadingText,
85
+ leftIcon,
86
+ rightIcon,
87
+ disabled,
88
+ children,
89
+ ...props
90
+ },
91
+ ref,
92
+ ) {
93
+ const Comp = asChild ? Slot : "button";
94
+ const isDisabled = disabled || loading;
95
+
96
+ // When `asChild` is true, the Slot pattern requires a single child element —
97
+ // so spinners/icons are not injected. Consumers compose those themselves.
98
+ const content = asChild ? (
99
+ children
100
+ ) : (
101
+ <>
102
+ {loading ? <RiLoader2Line className="animate-spin" /> : leftIcon}
103
+ {loading && loadingText ? <span>{loadingText}</span> : children}
104
+ {!loading && rightIcon}
105
+ </>
106
+ );
107
+
108
+ const compProps = asChild
109
+ ? { className: cn(buttonVariants({ variant, size, fullWidth }), className), ...props }
110
+ : {
111
+ type,
112
+ disabled: isDisabled,
113
+ "aria-disabled": isDisabled || undefined,
114
+ "aria-busy": loading || undefined,
115
+ className: cn(buttonVariants({ variant, size, fullWidth }), className),
116
+ ...props,
117
+ };
118
+
119
+ return (
120
+ <Comp ref={ref} {...compProps}>
121
+ {content}
122
+ </Comp>
123
+ );
124
+ });
125
+ Button.displayName = "Button";
126
+
127
+ export { Button, buttonVariants };
128
+ export type { ButtonProps };