@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,203 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { RiCodeLine, RiGlobalLine, RiPaletteLine, RiSearchLine } from "@remixicon/react";
3
+ import { useState } from "react";
4
+
5
+ import {
6
+ Select,
7
+ SelectContent,
8
+ SelectGroup,
9
+ SelectItem,
10
+ SelectLabel,
11
+ SelectSeparator,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "./select";
15
+
16
+ const meta: Meta<typeof Select> = {
17
+ title: "Forms/Select",
18
+ component: Select,
19
+ parameters: { layout: "centered" },
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj;
24
+
25
+ export const Default: Story = {
26
+ render: () => (
27
+ <Select>
28
+ <SelectTrigger className="w-56">
29
+ <SelectValue placeholder="Pick a framework" />
30
+ </SelectTrigger>
31
+ <SelectContent>
32
+ <SelectItem value="next">Next.js</SelectItem>
33
+ <SelectItem value="remix">Remix</SelectItem>
34
+ <SelectItem value="astro">Astro</SelectItem>
35
+ <SelectItem value="vite">Vite</SelectItem>
36
+ </SelectContent>
37
+ </Select>
38
+ ),
39
+ };
40
+
41
+ export const Sizes: Story = {
42
+ render: () => (
43
+ <div className="space-y-2">
44
+ {(["xs", "sm", "md", "lg", "xl"] as const).map((s) => (
45
+ <Select key={s}>
46
+ <SelectTrigger size={s} className="w-56">
47
+ <SelectValue placeholder={`size=${s}`} />
48
+ </SelectTrigger>
49
+ <SelectContent>
50
+ <SelectItem value="a">Apple</SelectItem>
51
+ <SelectItem value="b">Banana</SelectItem>
52
+ <SelectItem value="c">Cherry</SelectItem>
53
+ </SelectContent>
54
+ </Select>
55
+ ))}
56
+ </div>
57
+ ),
58
+ };
59
+
60
+ export const Grouped: Story = {
61
+ render: () => (
62
+ <Select>
63
+ <SelectTrigger className="w-64">
64
+ <SelectValue placeholder="Pick a fruit" />
65
+ </SelectTrigger>
66
+ <SelectContent>
67
+ <SelectGroup>
68
+ <SelectLabel>Tropical</SelectLabel>
69
+ <SelectItem value="mango">Mango</SelectItem>
70
+ <SelectItem value="pineapple">Pineapple</SelectItem>
71
+ <SelectItem value="papaya">Papaya</SelectItem>
72
+ </SelectGroup>
73
+ <SelectSeparator />
74
+ <SelectGroup>
75
+ <SelectLabel>Berries</SelectLabel>
76
+ <SelectItem value="strawberry">Strawberry</SelectItem>
77
+ <SelectItem value="blueberry">Blueberry</SelectItem>
78
+ </SelectGroup>
79
+ </SelectContent>
80
+ </Select>
81
+ ),
82
+ };
83
+
84
+ export const WithIcons: Story = {
85
+ render: () => (
86
+ <Select>
87
+ <SelectTrigger className="w-64" startAdornment={<RiPaletteLine />}>
88
+ <SelectValue placeholder="Pick a theme" />
89
+ </SelectTrigger>
90
+ <SelectContent>
91
+ <SelectItem value="light" icon={<RiGlobalLine />}>
92
+ Light
93
+ </SelectItem>
94
+ <SelectItem value="dark" icon={<RiCodeLine />}>
95
+ Dark
96
+ </SelectItem>
97
+ <SelectItem value="system" icon={<RiSearchLine />}>
98
+ System
99
+ </SelectItem>
100
+ </SelectContent>
101
+ </Select>
102
+ ),
103
+ };
104
+
105
+ export const WithDescriptions: Story = {
106
+ render: () => (
107
+ <Select>
108
+ <SelectTrigger className="w-72">
109
+ <SelectValue placeholder="Pick a plan" />
110
+ </SelectTrigger>
111
+ <SelectContent>
112
+ <SelectItem value="starter" description="Up to 3 projects. Free.">
113
+ Starter
114
+ </SelectItem>
115
+ <SelectItem value="pro" description="Unlimited projects. $12/mo.">
116
+ Pro
117
+ </SelectItem>
118
+ <SelectItem value="team" description="Seats and roles. $48/mo.">
119
+ Team
120
+ </SelectItem>
121
+ </SelectContent>
122
+ </Select>
123
+ ),
124
+ };
125
+
126
+ export const Invalid: Story = {
127
+ render: () => (
128
+ <Select>
129
+ <SelectTrigger className="w-56" invalid>
130
+ <SelectValue placeholder="Required field" />
131
+ </SelectTrigger>
132
+ <SelectContent>
133
+ <SelectItem value="a">A</SelectItem>
134
+ <SelectItem value="b">B</SelectItem>
135
+ </SelectContent>
136
+ </Select>
137
+ ),
138
+ };
139
+
140
+ export const Loading: Story = {
141
+ render: () => (
142
+ <Select>
143
+ <SelectTrigger className="w-56" loading>
144
+ <SelectValue placeholder="Loading…" />
145
+ </SelectTrigger>
146
+ <SelectContent>
147
+ <SelectItem value="a">A</SelectItem>
148
+ </SelectContent>
149
+ </Select>
150
+ ),
151
+ };
152
+
153
+ export const Clearable: Story = {
154
+ render: () => {
155
+ const Wrapper = () => {
156
+ const [value, setValue] = useState("medium");
157
+ return (
158
+ <Select value={value} onValueChange={setValue}>
159
+ <SelectTrigger className="w-56" clearable value={value} onClear={() => setValue("")}>
160
+ <SelectValue placeholder="Pick a size" />
161
+ </SelectTrigger>
162
+ <SelectContent>
163
+ <SelectItem value="small">Small</SelectItem>
164
+ <SelectItem value="medium">Medium</SelectItem>
165
+ <SelectItem value="large">Large</SelectItem>
166
+ </SelectContent>
167
+ </Select>
168
+ );
169
+ };
170
+ return <Wrapper />;
171
+ },
172
+ };
173
+
174
+ export const Disabled: Story = {
175
+ render: () => (
176
+ <Select disabled defaultValue="a">
177
+ <SelectTrigger className="w-56">
178
+ <SelectValue />
179
+ </SelectTrigger>
180
+ <SelectContent>
181
+ <SelectItem value="a">A</SelectItem>
182
+ <SelectItem value="b">B</SelectItem>
183
+ </SelectContent>
184
+ </Select>
185
+ ),
186
+ };
187
+
188
+ export const DisabledItem: Story = {
189
+ render: () => (
190
+ <Select>
191
+ <SelectTrigger className="w-56">
192
+ <SelectValue placeholder="Pick one" />
193
+ </SelectTrigger>
194
+ <SelectContent>
195
+ <SelectItem value="free">Free</SelectItem>
196
+ <SelectItem value="pro">Pro</SelectItem>
197
+ <SelectItem value="enterprise" disabled>
198
+ Enterprise (contact sales)
199
+ </SelectItem>
200
+ </SelectContent>
201
+ </Select>
202
+ ),
203
+ };
@@ -0,0 +1,318 @@
1
+ "use client";
2
+
3
+ import { forwardRef } from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import {
6
+ RiArrowDownSLine,
7
+ RiArrowUpSLine,
8
+ RiCheckLine,
9
+ RiCloseLine,
10
+ RiLoader2Line,
11
+ } from "@remixicon/react";
12
+ import { cva, type VariantProps } from "class-variance-authority";
13
+
14
+ import { cn } from "@/lib/utils";
15
+
16
+ // Per docs/emara-ui-phase-2-components.md §4.
17
+
18
+ const Select = SelectPrimitive.Root;
19
+ const SelectGroup = SelectPrimitive.Group;
20
+ const SelectValue = SelectPrimitive.Value;
21
+
22
+ // ----------------------------------------------------------------------------
23
+ // SelectTrigger
24
+ // ----------------------------------------------------------------------------
25
+
26
+ const selectTriggerVariants = cva(
27
+ [
28
+ "flex items-center justify-between w-full gap-2",
29
+ "rounded-md border border-input bg-background text-foreground",
30
+ "transition-colors",
31
+ "hover:border-foreground/40 disabled:hover:border-input",
32
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
33
+ "disabled:cursor-not-allowed disabled:opacity-50",
34
+ "data-[placeholder]:text-muted-foreground",
35
+ "[&>span]:line-clamp-1 [&>span]:text-start",
36
+ ].join(" "),
37
+ {
38
+ variants: {
39
+ size: {
40
+ xs: "h-7 ps-2.5 pe-2 text-xs",
41
+ sm: "h-8 ps-3 pe-2 text-xs",
42
+ md: "h-9 ps-3 pe-2 text-sm",
43
+ lg: "h-10 ps-3.5 pe-2.5 text-base",
44
+ xl: "h-12 ps-4 pe-3 text-base",
45
+ },
46
+ invalid: {
47
+ true: "border-destructive focus-visible:ring-destructive",
48
+ false: "",
49
+ },
50
+ },
51
+ defaultVariants: { size: "md", invalid: false },
52
+ },
53
+ );
54
+
55
+ type SelectTriggerVariants = VariantProps<typeof selectTriggerVariants>;
56
+
57
+ type SelectTriggerProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> &
58
+ SelectTriggerVariants & {
59
+ clearable?: boolean;
60
+ loading?: boolean;
61
+ startAdornment?: React.ReactNode;
62
+ onClear?: () => void;
63
+ /** Current value — needed for `clearable` to decide whether to show X. */
64
+ value?: string;
65
+ };
66
+
67
+ const SelectTrigger = forwardRef<
68
+ React.ElementRef<typeof SelectPrimitive.Trigger>,
69
+ SelectTriggerProps
70
+ >(function SelectTrigger(
71
+ {
72
+ className,
73
+ size,
74
+ invalid,
75
+ clearable = false,
76
+ loading = false,
77
+ startAdornment,
78
+ onClear,
79
+ value,
80
+ children,
81
+ ...props
82
+ },
83
+ ref,
84
+ ) {
85
+ const showClear = clearable && !props.disabled && !loading && Boolean(value);
86
+
87
+ return (
88
+ <SelectPrimitive.Trigger
89
+ ref={ref}
90
+ className={cn(selectTriggerVariants({ size, invalid }), className)}
91
+ {...props}
92
+ >
93
+ {startAdornment ? (
94
+ <span
95
+ aria-hidden="true"
96
+ className="text-muted-foreground me-1 inline-flex shrink-0 items-center [&_svg]:size-4 [&_svg]:shrink-0"
97
+ >
98
+ {startAdornment}
99
+ </span>
100
+ ) : null}
101
+ {children}
102
+ <span className="text-muted-foreground ms-auto inline-flex shrink-0 items-center gap-1 [&_svg]:size-4 [&_svg]:shrink-0">
103
+ {showClear ? (
104
+ <button
105
+ type="button"
106
+ aria-label="Clear"
107
+ onClick={(e) => {
108
+ e.preventDefault();
109
+ e.stopPropagation();
110
+ onClear?.();
111
+ }}
112
+ className={cn(
113
+ "inline-flex items-center justify-center rounded p-0.5",
114
+ "hover:text-foreground",
115
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
116
+ )}
117
+ >
118
+ <RiCloseLine />
119
+ </button>
120
+ ) : null}
121
+ {loading ? (
122
+ <RiLoader2Line className="size-4 animate-spin" />
123
+ ) : (
124
+ <SelectPrimitive.Icon asChild>
125
+ <RiArrowDownSLine />
126
+ </SelectPrimitive.Icon>
127
+ )}
128
+ </span>
129
+ </SelectPrimitive.Trigger>
130
+ );
131
+ });
132
+ SelectTrigger.displayName = "SelectTrigger";
133
+
134
+ // ----------------------------------------------------------------------------
135
+ // SelectContent
136
+ // ----------------------------------------------------------------------------
137
+
138
+ const SelectContent = forwardRef<
139
+ React.ElementRef<typeof SelectPrimitive.Content>,
140
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
141
+ >(function SelectContent(
142
+ { className, children, position = "popper", sideOffset = 4, ...props },
143
+ ref,
144
+ ) {
145
+ return (
146
+ <SelectPrimitive.Portal>
147
+ <SelectPrimitive.Content
148
+ ref={ref}
149
+ position={position}
150
+ sideOffset={sideOffset}
151
+ className={cn(
152
+ "z-popover relative max-h-(--radix-select-content-available-height) min-w-32 overflow-hidden",
153
+ "border-border bg-popover text-popover-foreground rounded-md border shadow-md",
154
+ "data-[state=open]:animate-[scale-in_var(--duration-fast)_var(--ease-out)]",
155
+ "data-[state=closed]:animate-[scale-out_var(--duration-fast)_var(--ease-in)]",
156
+ position === "popper" && "w-(--radix-select-trigger-width)",
157
+ className,
158
+ )}
159
+ {...props}
160
+ >
161
+ <SelectScrollUpButton />
162
+ <SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
163
+ <SelectScrollDownButton />
164
+ </SelectPrimitive.Content>
165
+ </SelectPrimitive.Portal>
166
+ );
167
+ });
168
+ SelectContent.displayName = "SelectContent";
169
+
170
+ // ----------------------------------------------------------------------------
171
+ // SelectScrollUpButton / SelectScrollDownButton
172
+ // ----------------------------------------------------------------------------
173
+
174
+ const SelectScrollUpButton = forwardRef<
175
+ React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
176
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
177
+ >(function SelectScrollUpButton({ className, ...props }, ref) {
178
+ return (
179
+ <SelectPrimitive.ScrollUpButton
180
+ ref={ref}
181
+ className={cn(
182
+ "text-muted-foreground flex cursor-default items-center justify-center py-1",
183
+ "hover:bg-accent hover:text-accent-foreground",
184
+ className,
185
+ )}
186
+ {...props}
187
+ >
188
+ <RiArrowUpSLine className="size-4" />
189
+ </SelectPrimitive.ScrollUpButton>
190
+ );
191
+ });
192
+ SelectScrollUpButton.displayName = "SelectScrollUpButton";
193
+
194
+ const SelectScrollDownButton = forwardRef<
195
+ React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
196
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
197
+ >(function SelectScrollDownButton({ className, ...props }, ref) {
198
+ return (
199
+ <SelectPrimitive.ScrollDownButton
200
+ ref={ref}
201
+ className={cn(
202
+ "text-muted-foreground flex cursor-default items-center justify-center py-1",
203
+ "hover:bg-accent hover:text-accent-foreground",
204
+ className,
205
+ )}
206
+ {...props}
207
+ >
208
+ <RiArrowDownSLine className="size-4" />
209
+ </SelectPrimitive.ScrollDownButton>
210
+ );
211
+ });
212
+ SelectScrollDownButton.displayName = "SelectScrollDownButton";
213
+
214
+ // ----------------------------------------------------------------------------
215
+ // SelectLabel
216
+ // ----------------------------------------------------------------------------
217
+
218
+ const SelectLabel = forwardRef<
219
+ React.ElementRef<typeof SelectPrimitive.Label>,
220
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
221
+ >(function SelectLabel({ className, ...props }, ref) {
222
+ return (
223
+ <SelectPrimitive.Label
224
+ ref={ref}
225
+ className={cn(
226
+ "text-muted-foreground px-2 py-1.5 text-xs font-semibold tracking-wide uppercase",
227
+ className,
228
+ )}
229
+ {...props}
230
+ />
231
+ );
232
+ });
233
+ SelectLabel.displayName = "SelectLabel";
234
+
235
+ // ----------------------------------------------------------------------------
236
+ // SelectItem (with Emara additions: description, icon)
237
+ // ----------------------------------------------------------------------------
238
+
239
+ type SelectItemProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
240
+ description?: React.ReactNode;
241
+ icon?: React.ReactNode;
242
+ };
243
+
244
+ const SelectItem = forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, SelectItemProps>(
245
+ function SelectItem({ className, children, description, icon, ...props }, ref) {
246
+ return (
247
+ <SelectPrimitive.Item
248
+ ref={ref}
249
+ className={cn(
250
+ "relative flex w-full cursor-pointer items-start gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-none select-none",
251
+ "transition-colors",
252
+ "focus:bg-accent focus:text-accent-foreground",
253
+ "data-[state=checked]:font-medium",
254
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
255
+ className,
256
+ )}
257
+ {...props}
258
+ >
259
+ <span className="absolute start-2 inline-flex h-4 w-4 items-center justify-center text-current">
260
+ <SelectPrimitive.ItemIndicator>
261
+ <RiCheckLine className="size-4" />
262
+ </SelectPrimitive.ItemIndicator>
263
+ </span>
264
+
265
+ <span className="flex flex-1 flex-col gap-0.5 leading-none">
266
+ <span className="inline-flex items-center gap-2">
267
+ {icon ? (
268
+ <span
269
+ aria-hidden="true"
270
+ className="text-muted-foreground inline-flex shrink-0 [&_svg]:size-4 [&_svg]:shrink-0"
271
+ >
272
+ {icon}
273
+ </span>
274
+ ) : null}
275
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
276
+ </span>
277
+ {description ? (
278
+ <span className="text-muted-foreground text-xs">{description}</span>
279
+ ) : null}
280
+ </span>
281
+ </SelectPrimitive.Item>
282
+ );
283
+ },
284
+ );
285
+ SelectItem.displayName = "SelectItem";
286
+
287
+ // ----------------------------------------------------------------------------
288
+ // SelectSeparator
289
+ // ----------------------------------------------------------------------------
290
+
291
+ const SelectSeparator = forwardRef<
292
+ React.ElementRef<typeof SelectPrimitive.Separator>,
293
+ React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
294
+ >(function SelectSeparator({ className, ...props }, ref) {
295
+ return (
296
+ <SelectPrimitive.Separator
297
+ ref={ref}
298
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
299
+ {...props}
300
+ />
301
+ );
302
+ });
303
+ SelectSeparator.displayName = "SelectSeparator";
304
+
305
+ export {
306
+ Select,
307
+ SelectGroup,
308
+ SelectValue,
309
+ SelectTrigger,
310
+ SelectContent,
311
+ SelectScrollUpButton,
312
+ SelectScrollDownButton,
313
+ SelectLabel,
314
+ SelectItem,
315
+ SelectSeparator,
316
+ selectTriggerVariants,
317
+ };
318
+ export type { SelectTriggerProps, SelectItemProps };
@@ -0,0 +1,186 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import {
4
+ RiBarChart2Line,
5
+ RiDashboardLine,
6
+ RiFolderLine,
7
+ RiSettings4Line,
8
+ RiTeamLine,
9
+ RiUser3Line,
10
+ RiWalletLine,
11
+ } from "@remixicon/react";
12
+
13
+ import { Avatar, AvatarFallback } from "./avatar";
14
+ import { Badge } from "./badge";
15
+ import {
16
+ Sidebar,
17
+ SidebarBody,
18
+ SidebarBrand,
19
+ SidebarCollapseToggle,
20
+ SidebarFooter,
21
+ SidebarGroup,
22
+ SidebarGroupLabel,
23
+ SidebarItem,
24
+ SidebarSubItem,
25
+ } from "./sidebar";
26
+ import { TooltipProvider } from "./tooltip";
27
+
28
+ const meta: Meta<typeof Sidebar> = {
29
+ title: "Layout/Sidebar",
30
+ component: Sidebar,
31
+ parameters: { layout: "fullscreen" },
32
+ argTypes: {
33
+ variant: { control: "select", options: ["filled", "outline", "floating"] },
34
+ collapsed: { control: "boolean" },
35
+ },
36
+ };
37
+
38
+ export default meta;
39
+ type Story = StoryObj<typeof Sidebar>;
40
+
41
+ function Brand() {
42
+ return (
43
+ <SidebarBrand>
44
+ <span className="bg-primary text-primary-foreground inline-flex size-6 items-center justify-center rounded text-xs font-bold">
45
+ E
46
+ </span>
47
+ <span>Emara</span>
48
+ </SidebarBrand>
49
+ );
50
+ }
51
+
52
+ function FooterUser() {
53
+ return (
54
+ <SidebarFooter>
55
+ <div className="flex items-center gap-2 px-2 py-1.5">
56
+ <Avatar size="sm">
57
+ <AvatarFallback>AZ</AvatarFallback>
58
+ </Avatar>
59
+ <div className="flex-1 truncate text-sm">
60
+ <div className="truncate font-medium">Abdelkader</div>
61
+ <div className="text-muted-foreground truncate text-xs">abdel@emara.io</div>
62
+ </div>
63
+ </div>
64
+ </SidebarFooter>
65
+ );
66
+ }
67
+
68
+ function NavContent() {
69
+ return (
70
+ <>
71
+ <SidebarGroup>
72
+ <SidebarGroupLabel>Workspace</SidebarGroupLabel>
73
+ <SidebarItem icon={<RiDashboardLine />} label="Dashboard" tooltip="Dashboard" active />
74
+ <SidebarItem
75
+ icon={<RiTeamLine />}
76
+ label="Team"
77
+ tooltip="Team"
78
+ badge={
79
+ <Badge size="xs" variant="info">
80
+ 12
81
+ </Badge>
82
+ }
83
+ />
84
+ <SidebarItem
85
+ icon={<RiFolderLine />}
86
+ label="Projects"
87
+ tooltip="Projects"
88
+ expandable
89
+ defaultExpanded
90
+ >
91
+ <SidebarSubItem label="Apollo" />
92
+ <SidebarSubItem label="Mercury" active />
93
+ <SidebarSubItem label="Gemini" />
94
+ </SidebarItem>
95
+ </SidebarGroup>
96
+
97
+ <SidebarGroup>
98
+ <SidebarGroupLabel>Insights</SidebarGroupLabel>
99
+ <SidebarItem icon={<RiBarChart2Line />} label="Analytics" tooltip="Analytics" />
100
+ <SidebarItem icon={<RiWalletLine />} label="Billing" tooltip="Billing" kbd={["⌘", "B"]} />
101
+ </SidebarGroup>
102
+
103
+ <SidebarGroup>
104
+ <SidebarGroupLabel>Settings</SidebarGroupLabel>
105
+ <SidebarItem icon={<RiUser3Line />} label="Profile" tooltip="Profile" />
106
+ <SidebarItem icon={<RiSettings4Line />} label="Preferences" tooltip="Preferences" />
107
+ </SidebarGroup>
108
+ </>
109
+ );
110
+ }
111
+
112
+ export const Filled: Story = {
113
+ args: { variant: "filled" },
114
+ render: (args) => (
115
+ <TooltipProvider delayDuration={200}>
116
+ <div className="flex h-[32rem]">
117
+ <Sidebar {...args}>
118
+ <Brand />
119
+ <SidebarBody>
120
+ <NavContent />
121
+ </SidebarBody>
122
+ <FooterUser />
123
+ </Sidebar>
124
+ <main className="text-muted-foreground flex-1 p-6">Main content area</main>
125
+ </div>
126
+ </TooltipProvider>
127
+ ),
128
+ };
129
+
130
+ export const Outline: Story = {
131
+ ...Filled,
132
+ args: { variant: "outline" },
133
+ };
134
+
135
+ export const Floating: Story = {
136
+ ...Filled,
137
+ args: { variant: "floating" },
138
+ };
139
+
140
+ export const Collapsed: Story = {
141
+ args: { collapsed: true },
142
+ render: (args) => (
143
+ <TooltipProvider delayDuration={200}>
144
+ <div className="flex h-[32rem]">
145
+ <Sidebar {...args}>
146
+ <Brand />
147
+ <SidebarBody>
148
+ <NavContent />
149
+ </SidebarBody>
150
+ <FooterUser />
151
+ </Sidebar>
152
+ <main className="text-muted-foreground flex-1 p-6">
153
+ Main content area (collapsed sidebar — hover items to see tooltips, click Projects for its
154
+ popover)
155
+ </main>
156
+ </div>
157
+ </TooltipProvider>
158
+ ),
159
+ };
160
+
161
+ export const Interactive: Story = {
162
+ render: () => {
163
+ function Demo() {
164
+ const [collapsed, setCollapsed] = useState(false);
165
+ return (
166
+ <TooltipProvider delayDuration={200}>
167
+ <div className="flex h-[32rem]">
168
+ <Sidebar collapsed={collapsed}>
169
+ <Brand />
170
+ <SidebarBody>
171
+ <NavContent />
172
+ </SidebarBody>
173
+ <SidebarFooter>
174
+ <SidebarCollapseToggle onClick={() => setCollapsed((v) => !v)} />
175
+ </SidebarFooter>
176
+ </Sidebar>
177
+ <main className="text-muted-foreground flex-1 p-6">
178
+ Click the collapse button at the bottom of the sidebar.
179
+ </main>
180
+ </div>
181
+ </TooltipProvider>
182
+ );
183
+ }
184
+ return <Demo />;
185
+ },
186
+ };