@bearmenu/ui 0.5.2 → 0.6.1

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 (227) hide show
  1. package/dist/{chunk-ERMIA7NP.js → chunk-7PPCH42H.js} +3 -3
  2. package/dist/{chunk-HLQ2IRE7.js → chunk-BAZFVSSG.js} +3 -3
  3. package/dist/chunk-D5QHEHXJ.js +26 -0
  4. package/dist/{chunk-UISA4CIG.js → chunk-N2KNBPAQ.js} +2 -2
  5. package/dist/{chunk-7H54PTVH.js → chunk-PSR7N36A.js} +38 -10
  6. package/dist/chunk-RIDXFY6M.js +38 -0
  7. package/dist/chunk-UUJVXN2O.js +82 -0
  8. package/dist/chunk-WKMLZDUT.js +122 -0
  9. package/dist/chunk-XK2ZDQVE.js +122 -0
  10. package/dist/{chunk-4KOMHDZD.js → chunk-Y5D5LP4A.js} +18 -3
  11. package/dist/chunk-YJ6OBKZJ.js +32 -0
  12. package/dist/{chunk-ESO2EM3L.js → chunk-ZBBUKXM5.js} +2 -2
  13. package/dist/chunk-ZFNOCP6W.js +106 -0
  14. package/dist/components/accordion.js +11 -5
  15. package/dist/components/alert-dialog.js +5 -3
  16. package/dist/components/alert.d.ts +4 -1
  17. package/dist/components/alert.js +25 -9
  18. package/dist/components/avatar.d.ts +17 -2
  19. package/dist/components/avatar.js +91 -12
  20. package/dist/components/badge.d.ts +1 -1
  21. package/dist/components/badge.js +1 -1
  22. package/dist/components/breadcrumb.js +5 -2
  23. package/dist/components/button-group.d.ts +14 -0
  24. package/dist/components/button-group.js +39 -0
  25. package/dist/components/button.d.ts +3 -1
  26. package/dist/components/button.js +1 -1
  27. package/dist/components/calendar.js +3 -3
  28. package/dist/components/card.d.ts +13 -5
  29. package/dist/components/card.js +2 -2
  30. package/dist/components/carousel.d.ts +20 -3
  31. package/dist/components/carousel.js +72 -12
  32. package/dist/components/checkbox.js +1 -1
  33. package/dist/components/circular-progress.d.ts +16 -0
  34. package/dist/components/circular-progress.js +105 -0
  35. package/dist/components/combobox.d.ts +27 -0
  36. package/dist/components/combobox.js +102 -0
  37. package/dist/components/command.d.ts +16 -3
  38. package/dist/components/command.js +2 -2
  39. package/dist/components/currency-input.d.ts +1 -0
  40. package/dist/components/currency-input.js +73 -52
  41. package/dist/components/date-input.d.ts +10 -0
  42. package/dist/components/date-input.js +40 -0
  43. package/dist/components/description-list.d.ts +15 -0
  44. package/dist/components/description-list.js +67 -0
  45. package/dist/components/dialog.d.ts +13 -2
  46. package/dist/components/dialog.js +1 -1
  47. package/dist/components/drawer.d.ts +5 -2
  48. package/dist/components/drawer.js +47 -9
  49. package/dist/components/dropdown-menu.d.ts +7 -2
  50. package/dist/components/dropdown-menu.js +22 -6
  51. package/dist/components/empty-state.js +2 -2
  52. package/dist/components/field.d.ts +22 -0
  53. package/dist/components/field.js +130 -0
  54. package/dist/components/form.d.ts +2 -1
  55. package/dist/components/form.js +11 -1
  56. package/dist/components/haptic-button.js +3 -3
  57. package/dist/components/input-group.d.ts +18 -0
  58. package/dist/components/input-group.js +92 -0
  59. package/dist/components/input.d.ts +8 -2
  60. package/dist/components/input.js +1 -1
  61. package/dist/components/item.d.ts +23 -0
  62. package/dist/components/item.js +120 -0
  63. package/dist/components/kbd.d.ts +13 -0
  64. package/dist/components/kbd.js +49 -0
  65. package/dist/components/lazy-markdown.d.ts +3 -1
  66. package/dist/components/lazy-markdown.js +7 -7
  67. package/dist/components/link-button.js +1 -1
  68. package/dist/components/multi-select-combobox.js +92 -83
  69. package/dist/components/multi-select-menu.d.ts +0 -4
  70. package/dist/components/multi-select-menu.js +28 -17
  71. package/dist/components/native-select.d.ts +12 -0
  72. package/dist/components/native-select.js +49 -0
  73. package/dist/components/pagination.d.ts +5 -2
  74. package/dist/components/pagination.js +66 -7
  75. package/dist/components/place-about-tab.js +4 -4
  76. package/dist/components/place-card.js +1 -1
  77. package/dist/components/place-details-mobile.js +4 -4
  78. package/dist/components/popover.d.ts +7 -1
  79. package/dist/components/popover.js +1 -1
  80. package/dist/components/progress.d.ts +5 -1
  81. package/dist/components/progress.js +17 -3
  82. package/dist/components/radio-group.js +12 -3
  83. package/dist/components/rating.d.ts +2 -0
  84. package/dist/components/rating.js +88 -70
  85. package/dist/components/scroll-fade.d.ts +15 -0
  86. package/dist/components/scroll-fade.js +116 -0
  87. package/dist/components/search-bar.d.ts +1 -2
  88. package/dist/components/search-bar.js +37 -60
  89. package/dist/components/searchable-select.js +76 -72
  90. package/dist/components/select.d.ts +5 -1
  91. package/dist/components/select.js +30 -14
  92. package/dist/components/selection-bar.d.ts +11 -0
  93. package/dist/components/selection-bar.js +65 -0
  94. package/dist/components/separator.js +3 -2
  95. package/dist/components/sheet.d.ts +1 -1
  96. package/dist/components/sheet.js +1 -1
  97. package/dist/components/skeleton-card.js +1 -1
  98. package/dist/components/skeleton.js +1 -1
  99. package/dist/components/slider.js +21 -4
  100. package/dist/components/sliding-panels.js +9 -6
  101. package/dist/components/spinner.d.ts +17 -0
  102. package/dist/components/spinner.js +50 -0
  103. package/dist/components/stepper.d.ts +18 -0
  104. package/dist/components/stepper.js +199 -0
  105. package/dist/components/switch.js +4 -3
  106. package/dist/components/table.d.ts +23 -5
  107. package/dist/components/table.js +121 -35
  108. package/dist/components/tabs.d.ts +3 -1
  109. package/dist/components/tabs.js +37 -9
  110. package/dist/components/tag-group.d.ts +9 -0
  111. package/dist/components/tag-group.js +26 -0
  112. package/dist/components/tag.d.ts +17 -0
  113. package/dist/components/tag.js +61 -0
  114. package/dist/components/text.d.ts +1 -1
  115. package/dist/components/text.js +1 -1
  116. package/dist/components/textarea.js +2 -1
  117. package/dist/components/toast.js +1 -1
  118. package/dist/components/toaster.js +1 -1
  119. package/dist/components/toggle-group.js +1 -1
  120. package/dist/components/toggle.js +1 -1
  121. package/dist/components/tooltip.d.ts +3 -2
  122. package/dist/components/tooltip.js +16 -2
  123. package/package.json +45 -26
  124. package/src/components/accordion.tsx +11 -6
  125. package/src/components/alert-dialog.tsx +4 -2
  126. package/src/components/alert.tsx +20 -8
  127. package/src/components/avatar.tsx +106 -13
  128. package/src/components/badge.tsx +12 -11
  129. package/src/components/breadcrumb.tsx +5 -2
  130. package/src/components/button-group.stories.tsx +52 -0
  131. package/src/components/button-group.tsx +40 -0
  132. package/src/components/button.test.tsx +174 -0
  133. package/src/components/button.tsx +42 -19
  134. package/src/components/calendar.tsx +2 -2
  135. package/src/components/card.tsx +55 -13
  136. package/src/components/carousel.tsx +85 -13
  137. package/src/components/checkbox.test.tsx +58 -0
  138. package/src/components/checkbox.tsx +8 -3
  139. package/src/components/circular-progress.stories.tsx +39 -0
  140. package/src/components/circular-progress.tsx +102 -0
  141. package/src/components/collapsible.stories.tsx +7 -3
  142. package/src/components/combobox.stories.tsx +82 -0
  143. package/src/components/combobox.test.tsx +118 -0
  144. package/src/components/combobox.tsx +136 -0
  145. package/src/components/command.test.tsx +91 -0
  146. package/src/components/command.tsx +38 -15
  147. package/src/components/currency-input.test.tsx +63 -0
  148. package/src/components/currency-input.tsx +84 -66
  149. package/src/components/date-input.stories.tsx +31 -0
  150. package/src/components/date-input.tsx +43 -0
  151. package/src/components/description-list.stories.tsx +62 -0
  152. package/src/components/description-list.tsx +68 -0
  153. package/src/components/dialog.test.tsx +84 -0
  154. package/src/components/dialog.tsx +64 -24
  155. package/src/components/drawer.tsx +72 -23
  156. package/src/components/dropdown-menu.test.tsx +180 -0
  157. package/src/components/dropdown-menu.tsx +31 -6
  158. package/src/components/field.stories.tsx +36 -0
  159. package/src/components/field.test.tsx +157 -0
  160. package/src/components/field.tsx +146 -0
  161. package/src/components/form.stories.tsx +3 -2
  162. package/src/components/form.test.tsx +95 -0
  163. package/src/components/form.tsx +9 -0
  164. package/src/components/haptic-button.tsx +1 -1
  165. package/src/components/input-group.stories.tsx +61 -0
  166. package/src/components/input-group.tsx +99 -0
  167. package/src/components/input.test.tsx +103 -0
  168. package/src/components/input.tsx +24 -7
  169. package/src/components/item.stories.tsx +52 -0
  170. package/src/components/item.tsx +115 -0
  171. package/src/components/kbd.stories.tsx +33 -0
  172. package/src/components/kbd.tsx +46 -0
  173. package/src/components/lazy-markdown.tsx +10 -7
  174. package/src/components/multi-select-combobox.test.tsx +101 -0
  175. package/src/components/multi-select-combobox.tsx +85 -82
  176. package/src/components/multi-select-menu.tsx +49 -41
  177. package/src/components/native-select.stories.tsx +52 -0
  178. package/src/components/native-select.tsx +45 -0
  179. package/src/components/pagination.tsx +64 -5
  180. package/src/components/popover.tsx +66 -3
  181. package/src/components/progress.tsx +27 -4
  182. package/src/components/radio-group.test.tsx +64 -0
  183. package/src/components/radio-group.tsx +11 -3
  184. package/src/components/rating.test.tsx +112 -0
  185. package/src/components/rating.tsx +97 -82
  186. package/src/components/scroll-fade.stories.tsx +98 -0
  187. package/src/components/scroll-fade.tsx +127 -0
  188. package/src/components/search-bar.stories.tsx +1 -32
  189. package/src/components/search-bar.tsx +22 -59
  190. package/src/components/searchable-select.tsx +69 -73
  191. package/src/components/select.tsx +34 -19
  192. package/src/components/selection-bar.stories.tsx +37 -0
  193. package/src/components/selection-bar.tsx +65 -0
  194. package/src/components/separator.tsx +5 -2
  195. package/src/components/sheet.tsx +1 -1
  196. package/src/components/skeleton.tsx +2 -2
  197. package/src/components/slider.test.tsx +45 -0
  198. package/src/components/slider.tsx +31 -12
  199. package/src/components/sliding-panels.tsx +8 -5
  200. package/src/components/spinner.stories.tsx +35 -0
  201. package/src/components/spinner.tsx +50 -0
  202. package/src/components/stepper.stories.tsx +71 -0
  203. package/src/components/stepper.tsx +220 -0
  204. package/src/components/switch.test.tsx +52 -0
  205. package/src/components/switch.tsx +4 -3
  206. package/src/components/table.tsx +140 -35
  207. package/src/components/tabs.test.tsx +107 -0
  208. package/src/components/tabs.tsx +70 -23
  209. package/src/components/tag-group.stories.tsx +39 -0
  210. package/src/components/tag-group.tsx +28 -0
  211. package/src/components/tag.stories.tsx +50 -0
  212. package/src/components/tag.tsx +63 -0
  213. package/src/components/text.test.tsx +1 -1
  214. package/src/components/text.tsx +19 -3
  215. package/src/components/textarea.test.tsx +48 -0
  216. package/src/components/textarea.tsx +2 -1
  217. package/src/components/toast.tsx +4 -3
  218. package/src/components/toggle.tsx +2 -2
  219. package/src/components/tooltip.tsx +12 -2
  220. package/src/globals.css +126 -90
  221. package/dist/chunk-2BMJRXQX.js +0 -80
  222. package/dist/chunk-4AVS6CV4.js +0 -89
  223. package/dist/chunk-4UO73SKC.js +0 -26
  224. package/dist/chunk-CW24PLDW.js +0 -24
  225. package/dist/chunk-IQASCH2V.js +0 -25
  226. package/dist/chunk-NKHH23YK.js +0 -31
  227. package/dist/chunk-QJ54Z7TC.js +0 -83
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "./tabs";
5
+
6
+ function Triplet({ defaultValue = "a" }: { defaultValue?: string }) {
7
+ return (
8
+ <Tabs defaultValue={defaultValue}>
9
+ <TabsList>
10
+ <TabsTrigger value="a">A</TabsTrigger>
11
+ <TabsTrigger value="b">B</TabsTrigger>
12
+ <TabsTrigger value="c">C</TabsTrigger>
13
+ </TabsList>
14
+ <TabsContent value="a">Panel A</TabsContent>
15
+ <TabsContent value="b">Panel B</TabsContent>
16
+ <TabsContent value="c">Panel C</TabsContent>
17
+ </Tabs>
18
+ );
19
+ }
20
+
21
+ describe("Tabs", () => {
22
+ it("renders tablist with the right number of tabs", () => {
23
+ render(<Triplet />);
24
+ expect(screen.getByRole("tablist")).toBeInTheDocument();
25
+ expect(screen.getAllByRole("tab")).toHaveLength(3);
26
+ });
27
+
28
+ it("first tab is active by default and only its panel is visible", () => {
29
+ render(<Triplet />);
30
+ const tabs = screen.getAllByRole("tab");
31
+ expect(tabs[0].getAttribute("data-state")).toBe("active");
32
+ expect(tabs[1].getAttribute("data-state")).toBe("inactive");
33
+ expect(screen.getByText("Panel A")).toBeInTheDocument();
34
+ expect(screen.queryByText("Panel B")).toBeNull();
35
+ });
36
+
37
+ it("switches panel when a different tab is clicked", async () => {
38
+ const user = userEvent.setup();
39
+ render(<Triplet />);
40
+ await user.click(screen.getByRole("tab", { name: "B" }));
41
+ expect(screen.getByText("Panel B")).toBeInTheDocument();
42
+ expect(screen.queryByText("Panel A")).toBeNull();
43
+ });
44
+
45
+ it("wires aria-controls and aria-labelledby between tab and panel", () => {
46
+ render(<Triplet />);
47
+ const tabA = screen.getByRole("tab", { name: "A" });
48
+ const panelA = screen.getByText("Panel A");
49
+ expect(tabA.getAttribute("aria-controls")).toBe(panelA.id);
50
+ expect(panelA.getAttribute("aria-labelledby")).toBe(tabA.id);
51
+ });
52
+
53
+ it("supports ArrowRight to focus next tab", async () => {
54
+ const user = userEvent.setup();
55
+ render(<Triplet />);
56
+ const tabs = screen.getAllByRole("tab");
57
+ tabs[0].focus();
58
+ await user.keyboard("{ArrowRight}");
59
+ expect(tabs[1]).toHaveFocus();
60
+ });
61
+
62
+ it("supports ArrowLeft to focus previous tab", async () => {
63
+ const user = userEvent.setup();
64
+ render(<Triplet defaultValue="b" />);
65
+ const tabs = screen.getAllByRole("tab");
66
+ tabs[1].focus();
67
+ await user.keyboard("{ArrowLeft}");
68
+ expect(tabs[0]).toHaveFocus();
69
+ });
70
+
71
+ it("fires onValueChange when tab changes", async () => {
72
+ const user = userEvent.setup();
73
+ const onValueChange = vi.fn();
74
+ render(
75
+ <Tabs defaultValue="a" onValueChange={onValueChange}>
76
+ <TabsList>
77
+ <TabsTrigger value="a">A</TabsTrigger>
78
+ <TabsTrigger value="b">B</TabsTrigger>
79
+ </TabsList>
80
+ <TabsContent value="a">A</TabsContent>
81
+ <TabsContent value="b">B</TabsContent>
82
+ </Tabs>
83
+ );
84
+ await user.click(screen.getByRole("tab", { name: "B" }));
85
+ expect(onValueChange).toHaveBeenCalledWith("b");
86
+ });
87
+
88
+ it("supports disabled triggers", async () => {
89
+ const user = userEvent.setup();
90
+ render(
91
+ <Tabs defaultValue="a">
92
+ <TabsList>
93
+ <TabsTrigger value="a">A</TabsTrigger>
94
+ <TabsTrigger value="b" disabled>
95
+ B
96
+ </TabsTrigger>
97
+ </TabsList>
98
+ <TabsContent value="a">Panel A</TabsContent>
99
+ <TabsContent value="b">Panel B</TabsContent>
100
+ </Tabs>
101
+ );
102
+ const tabB = screen.getByRole("tab", { name: "B" });
103
+ expect(tabB.getAttribute("data-disabled")).not.toBeNull();
104
+ await user.click(tabB);
105
+ expect(screen.getByText("Panel A")).toBeInTheDocument();
106
+ });
107
+ });
@@ -5,47 +5,93 @@ import * as TabsPrimitive from "@radix-ui/react-tabs";
5
5
  import { cn } from "../lib/utils";
6
6
 
7
7
  type TabsVariant = "pill" | "underline";
8
+ type TabsOrientation = "horizontal" | "vertical";
8
9
 
9
- const TabsVariantContext = React.createContext<TabsVariant>("pill");
10
+ type TabsContextValue = {
11
+ variant: TabsVariant;
12
+ orientation: TabsOrientation;
13
+ };
10
14
 
11
- const Tabs = TabsPrimitive.Root;
15
+ const TabsContext = React.createContext<TabsContextValue>({
16
+ variant: "pill",
17
+ orientation: "horizontal",
18
+ });
19
+
20
+ const Tabs = React.forwardRef<
21
+ React.ElementRef<typeof TabsPrimitive.Root>,
22
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
23
+ >(({ className, orientation = "horizontal", ...props }, ref) => (
24
+ <TabsPrimitive.Root
25
+ ref={ref}
26
+ data-slot="tabs"
27
+ orientation={orientation}
28
+ className={cn(
29
+ "flex",
30
+ orientation === "vertical" ? "flex-row gap-4" : "flex-col gap-2",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ ));
36
+ Tabs.displayName = TabsPrimitive.Root.displayName;
12
37
 
13
38
  const TabsList = React.forwardRef<
14
39
  React.ElementRef<typeof TabsPrimitive.List>,
15
40
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
16
41
  variant?: TabsVariant;
42
+ /** Make horizontal lists scrollable when overflowing. Default true for underline, false for pill. */
43
+ scrollable?: boolean;
17
44
  }
18
- >(({ className, variant = "pill", children, ...props }, ref) => (
19
- <TabsVariantContext.Provider value={variant}>
20
- <TabsPrimitive.List
21
- ref={ref}
22
- className={cn(
23
- variant === "pill"
24
- ? "inline-flex h-12 items-center justify-center gap-2 rounded-3xl bg-sidebar p-1 text-muted-foreground border border-border/50 shadow-lg"
25
- : "inline-flex h-auto items-center gap-0 rounded-none bg-transparent border-b border-border p-0 shadow-none text-muted-foreground",
26
- className
27
- )}
28
- {...props}
29
- >
30
- {children}
31
- </TabsPrimitive.List>
32
- </TabsVariantContext.Provider>
33
- ));
45
+ >(({ className, variant = "pill", scrollable, children, ...props }, ref) => {
46
+ const orientation: TabsOrientation = props["aria-orientation"] === "vertical" ? "vertical" : "horizontal";
47
+ const allowScroll = scrollable ?? orientation === "horizontal";
48
+
49
+ return (
50
+ <TabsContext.Provider value={{ variant, orientation }}>
51
+ <TabsPrimitive.List
52
+ ref={ref}
53
+ data-slot="tabs-list"
54
+ className={cn(
55
+ variant === "pill"
56
+ ? orientation === "vertical"
57
+ ? "inline-flex flex-col items-stretch gap-1 rounded-2xl bg-muted p-1 text-muted-foreground"
58
+ : "inline-flex h-11 max-w-full items-center justify-start gap-1 rounded-2xl bg-muted p-1 text-muted-foreground"
59
+ : orientation === "vertical"
60
+ ? "inline-flex flex-col items-stretch gap-0 border-r border-border bg-transparent text-muted-foreground"
61
+ : "inline-flex h-auto max-w-full items-center gap-0 border-b border-border bg-transparent text-muted-foreground",
62
+ allowScroll &&
63
+ "overflow-x-auto overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&>*]:shrink-0",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ {children}
69
+ </TabsPrimitive.List>
70
+ </TabsContext.Provider>
71
+ );
72
+ });
34
73
  TabsList.displayName = TabsPrimitive.List.displayName;
35
74
 
36
75
  const TabsTrigger = React.forwardRef<
37
76
  React.ElementRef<typeof TabsPrimitive.Trigger>,
38
77
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
39
78
  >(({ className, ...props }, ref) => {
40
- const variant = React.useContext(TabsVariantContext);
79
+ const { variant, orientation } = React.useContext(TabsContext);
41
80
 
42
81
  return (
43
82
  <TabsPrimitive.Trigger
44
83
  ref={ref}
84
+ data-slot="tabs-trigger"
45
85
  className={cn(
46
- variant === "pill"
47
- ? "inline-flex items-center justify-center whitespace-nowrap rounded-2xl px-4 py-2 text-sm font-medium ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-primary/90 data-[state=active]:text-primary-foreground data-[state=active]:shadow-lg data-[state=active]:backdrop-blur-md data-[state=active]:border data-[state=active]:border-border/50 hover:bg-accent/50 hover:text-accent-foreground [@media(pointer:coarse)]:active:scale-95"
48
- : "relative inline-flex items-center justify-center whitespace-nowrap rounded-none px-4 h-12 text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:text-foreground data-[state=active]:bg-transparent data-[state=active]:shadow-none text-muted-foreground hover:text-foreground/80 cursor-pointer",
86
+ "inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
87
+ variant === "pill" &&
88
+ "rounded-xl px-4 py-1.5 text-muted-foreground hover:text-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
89
+ variant === "underline" &&
90
+ orientation === "horizontal" &&
91
+ "relative h-11 rounded-none border-b-2 border-transparent px-4 text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground",
92
+ variant === "underline" &&
93
+ orientation === "vertical" &&
94
+ "relative rounded-none border-r-2 border-transparent px-4 py-2 -mr-px text-muted-foreground hover:text-foreground data-[state=active]:border-primary data-[state=active]:text-foreground justify-start",
49
95
  className
50
96
  )}
51
97
  {...props}
@@ -60,8 +106,9 @@ const TabsContent = React.forwardRef<
60
106
  >(({ className, ...props }, ref) => (
61
107
  <TabsPrimitive.Content
62
108
  ref={ref}
109
+ data-slot="tabs-content"
63
110
  className={cn(
64
- "mt-4 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
111
+ "flex-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md",
65
112
  className
66
113
  )}
67
114
  {...props}
@@ -0,0 +1,39 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { TagGroup } from "./tag-group";
3
+ import { Tag } from "./tag";
4
+
5
+ const meta = {
6
+ title: "Components/TagGroup",
7
+ component: TagGroup,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof TagGroup>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ render: () => (
16
+ <TagGroup label="Tags">
17
+ <Tag>Design</Tag>
18
+ <Tag>Engineering</Tag>
19
+ <Tag>Product</Tag>
20
+ <Tag>Research</Tag>
21
+ </TagGroup>
22
+ ),
23
+ };
24
+
25
+ export const VisibleLabel: Story = {
26
+ render: () => (
27
+ <TagGroup label="Filters" srLabel={false}>
28
+ <Tag variant="primary" onRemove={() => {}}>
29
+ Active
30
+ </Tag>
31
+ <Tag variant="primary" onRemove={() => {}}>
32
+ Last 7 days
33
+ </Tag>
34
+ <Tag variant="primary" onRemove={() => {}}>
35
+ Owner: me
36
+ </Tag>
37
+ </TagGroup>
38
+ ),
39
+ };
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ import { cn } from "../lib/utils";
3
+
4
+ const TagGroup = React.forwardRef<
5
+ HTMLDivElement,
6
+ React.HTMLAttributes<HTMLDivElement> & {
7
+ label?: string;
8
+ /** Visually hide the label, keep accessible. */
9
+ srLabel?: boolean;
10
+ }
11
+ >(({ className, label, srLabel = true, children, ...props }, ref) => (
12
+ <div
13
+ ref={ref}
14
+ role="group"
15
+ aria-label={label}
16
+ data-slot="tag-group"
17
+ className={cn("flex flex-wrap items-center gap-1.5", className)}
18
+ {...props}
19
+ >
20
+ {label && !srLabel && (
21
+ <span className="mr-1 text-xs font-medium text-muted-foreground">{label}</span>
22
+ )}
23
+ {children}
24
+ </div>
25
+ ));
26
+ TagGroup.displayName = "TagGroup";
27
+
28
+ export { TagGroup };
@@ -0,0 +1,50 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Tag } from "./tag";
3
+
4
+ const meta = {
5
+ title: "Components/Tag",
6
+ component: Tag,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof Tag>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Variants: Story = {
14
+ render: () => (
15
+ <div className="flex flex-wrap items-center gap-2">
16
+ <Tag>Default</Tag>
17
+ <Tag variant="primary">Primary</Tag>
18
+ <Tag variant="success">Success</Tag>
19
+ <Tag variant="warning">Warning</Tag>
20
+ <Tag variant="destructive">Destructive</Tag>
21
+ <Tag variant="outline">Outline</Tag>
22
+ </div>
23
+ ),
24
+ };
25
+
26
+ export const Sizes: Story = {
27
+ render: () => (
28
+ <div className="flex flex-wrap items-center gap-2">
29
+ <Tag size="sm">Small</Tag>
30
+ <Tag size="default">Default</Tag>
31
+ <Tag size="lg">Large</Tag>
32
+ </div>
33
+ ),
34
+ };
35
+
36
+ export const Removable: Story = {
37
+ render: () => (
38
+ <div className="flex flex-wrap items-center gap-2">
39
+ <Tag variant="primary" onRemove={() => {}}>
40
+ React
41
+ </Tag>
42
+ <Tag variant="primary" onRemove={() => {}}>
43
+ TypeScript
44
+ </Tag>
45
+ <Tag variant="primary" onRemove={() => {}}>
46
+ Tailwind
47
+ </Tag>
48
+ </div>
49
+ ),
50
+ };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { X } from "lucide-react";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import { cn } from "../lib/utils";
7
+
8
+ const tagVariants = cva(
9
+ "inline-flex items-center gap-1 rounded-full border text-xs font-medium transition-colors",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "border-border bg-muted text-foreground",
14
+ primary: "border-primary/30 bg-primary/10 text-primary",
15
+ success: "border-success/30 bg-success/10 text-success",
16
+ warning: "border-warning/30 bg-warning/10 text-warning",
17
+ destructive: "border-destructive/30 bg-destructive/10 text-destructive",
18
+ outline: "border-border bg-transparent text-foreground",
19
+ },
20
+ size: {
21
+ sm: "h-5 px-2 text-[10px]",
22
+ default: "h-6 px-2.5",
23
+ lg: "h-7 px-3 text-sm",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: "default",
28
+ size: "default",
29
+ },
30
+ }
31
+ );
32
+
33
+ type TagProps = React.HTMLAttributes<HTMLSpanElement> &
34
+ VariantProps<typeof tagVariants> & {
35
+ onRemove?: () => void;
36
+ removeLabel?: string;
37
+ };
38
+
39
+ const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
40
+ ({ className, variant, size, onRemove, removeLabel = "Remove", children, ...props }, ref) => (
41
+ <span
42
+ ref={ref}
43
+ data-slot="tag"
44
+ className={cn(tagVariants({ variant, size }), className)}
45
+ {...props}
46
+ >
47
+ {children}
48
+ {onRemove && (
49
+ <button
50
+ type="button"
51
+ aria-label={removeLabel}
52
+ onClick={onRemove}
53
+ className="-mr-0.5 inline-flex size-3.5 items-center justify-center rounded-full transition-colors hover:bg-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
54
+ >
55
+ <X className="size-3" />
56
+ </button>
57
+ )}
58
+ </span>
59
+ )
60
+ );
61
+ Tag.displayName = "Tag";
62
+
63
+ export { Tag, tagVariants };
@@ -26,7 +26,7 @@ describe("textVariants", () => {
26
26
  expect(textVariants({ variant: "h3" })).toContain("text-xl");
27
27
  expect(textVariants({ variant: "h4" })).toContain("text-lg");
28
28
  expect(textVariants({ variant: "body-lg" })).toContain("text-base");
29
- expect(textVariants({ variant: "body" })).toContain("text-sm");
29
+ expect(textVariants({ variant: "body" })).toContain("text-base");
30
30
  expect(textVariants({ variant: "body-sm" })).toContain("text-sm");
31
31
  expect(textVariants({ variant: "caption" })).toContain("text-xs");
32
32
  expect(textVariants({ variant: "label" })).toContain("font-medium");
@@ -11,9 +11,10 @@ const textVariants = cva("", {
11
11
  h3: "text-xl font-semibold",
12
12
  h4: "text-lg font-semibold leading-none tracking-tight",
13
13
  "body-lg": "text-base leading-relaxed",
14
- body: "text-sm leading-normal",
14
+ body: "text-base leading-normal",
15
15
  "body-sm": "text-sm leading-relaxed",
16
- caption: "text-xs leading-normal",
16
+ small: "text-sm text-muted-foreground",
17
+ caption: "text-xs leading-normal text-muted-foreground",
17
18
  label: "text-sm font-medium",
18
19
  overline: "text-xs font-semibold uppercase tracking-wider",
19
20
  },
@@ -89,6 +90,21 @@ type TextOwnProps<T extends TextTag = "p"> = {
89
90
  type TextProps<T extends TextTag = "p"> = TextOwnProps<T> &
90
91
  Omit<React.ComponentPropsWithoutRef<T>, keyof TextOwnProps<T> | "color">;
91
92
 
93
+ const defaultTagForVariant: Record<NonNullable<VariantProps<typeof textVariants>["variant"]>, TextTag> = {
94
+ display: "h1",
95
+ h1: "h1",
96
+ h2: "h2",
97
+ h3: "h3",
98
+ h4: "h4",
99
+ "body-lg": "p",
100
+ body: "p",
101
+ "body-sm": "p",
102
+ small: "p",
103
+ caption: "span",
104
+ label: "span",
105
+ overline: "span",
106
+ };
107
+
92
108
  function Text<T extends TextTag = "p">({
93
109
  as,
94
110
  variant,
@@ -100,7 +116,7 @@ function Text<T extends TextTag = "p">({
100
116
  className,
101
117
  ...props
102
118
  }: TextProps<T>) {
103
- const Comp = (as ?? "p") as React.ElementType;
119
+ const Comp = (as ?? (variant ? defaultTagForVariant[variant] : "p")) as React.ElementType;
104
120
  return (
105
121
  <Comp
106
122
  className={cn(textVariants({ variant, color, weight, leading, tracking, wrap }), className)}
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { Textarea } from "./textarea";
5
+
6
+ describe("Textarea", () => {
7
+ it("renders a textarea", () => {
8
+ render(<Textarea aria-label="bio" />);
9
+ expect(screen.getByRole("textbox")).toBeDefined();
10
+ });
11
+
12
+ it("uses text-base (16px) so iOS does not zoom on focus", () => {
13
+ render(<Textarea aria-label="bio" />);
14
+ expect(screen.getByRole("textbox").className).toContain("text-base");
15
+ });
16
+
17
+ it("fires onChange when typing", async () => {
18
+ const user = userEvent.setup();
19
+ const onChange = vi.fn();
20
+ render(<Textarea aria-label="bio" onChange={onChange} />);
21
+ await user.type(screen.getByRole("textbox"), "hi");
22
+ expect(onChange).toHaveBeenCalled();
23
+ });
24
+
25
+ it("respects disabled", async () => {
26
+ const user = userEvent.setup();
27
+ const onChange = vi.fn();
28
+ render(<Textarea aria-label="bio" disabled onChange={onChange} />);
29
+ await user.type(screen.getByRole("textbox"), "hi");
30
+ expect(onChange).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it("reflects aria-invalid", () => {
34
+ render(<Textarea aria-label="bio" aria-invalid />);
35
+ expect(screen.getByRole("textbox")).toHaveAttribute("aria-invalid", "true");
36
+ });
37
+
38
+ it("merges className", () => {
39
+ render(<Textarea aria-label="bio" className="custom" />);
40
+ expect(screen.getByRole("textbox").className).toContain("custom");
41
+ });
42
+
43
+ it("forwards ref to the textarea element", () => {
44
+ const ref = { current: null as HTMLTextAreaElement | null };
45
+ render(<Textarea aria-label="bio" ref={ref} />);
46
+ expect(ref.current?.tagName).toBe("TEXTAREA");
47
+ });
48
+ });
@@ -7,8 +7,9 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
7
7
  ({ className, ...props }, ref) => {
8
8
  return (
9
9
  <textarea
10
+ data-slot="textarea"
10
11
  className={cn(
11
- "flex min-h-[80px] w-full rounded-xl border border-border bg-input/50 backdrop-blur-md px-4 py-3 text-base transition-all duration-300 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:bg-input focus-visible:border-accent-foreground/30 focus-visible:shadow-lg focus-visible:shadow-accent/10 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
12
+ "flex min-h-20 w-full rounded-xl border border-border bg-input/50 px-4 py-3 text-base transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:bg-input disabled:cursor-not-allowed disabled:opacity-50 resize-y aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/40",
12
13
  className
13
14
  )}
14
15
  ref={ref}
@@ -24,12 +24,13 @@ const ToastViewport = React.forwardRef<
24
24
  ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
25
25
 
26
26
  const toastVariants = cva(
27
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl glass p-6 pr-8 shadow-2xl transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full border border-border/50",
27
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-xl p-6 pr-8 shadow-2xl transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
28
28
  {
29
29
  variants: {
30
30
  variant: {
31
- default: "border bg-background text-foreground",
32
- destructive: "destructive border-destructive/50 bg-destructive text-destructive-foreground",
31
+ default: "glass text-foreground border border-border/50",
32
+ destructive:
33
+ "destructive border border-destructive/60 bg-destructive text-destructive-foreground",
33
34
  },
34
35
  },
35
36
  defaultVariants: {
@@ -7,13 +7,13 @@ import { cva, type VariantProps } from "class-variance-authority";
7
7
  import { cn } from "../lib/utils";
8
8
 
9
9
  const toggleVariants = cva(
10
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
10
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-accent/50 hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 transition-colors aria-invalid:ring-2 aria-invalid:ring-destructive/40 whitespace-nowrap",
11
11
  {
12
12
  variants: {
13
13
  variant: {
14
14
  default: "bg-transparent",
15
15
  outline:
16
- "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
16
+ "border border-border bg-transparent hover:bg-accent/50 hover:text-accent-foreground data-[state=on]:border-primary",
17
17
  },
18
18
  size: {
19
19
  default: "h-9 px-2 min-w-9",
@@ -4,9 +4,19 @@ import * as React from "react";
4
4
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5
5
  import { cn } from "../lib/utils";
6
6
 
7
- const TooltipProvider = TooltipPrimitive.Provider;
7
+ const TooltipProvider = ({
8
+ delayDuration = 200,
9
+ ...props
10
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) => (
11
+ <TooltipPrimitive.Provider delayDuration={delayDuration} {...props} />
12
+ );
8
13
 
9
- const Tooltip = TooltipPrimitive.Root;
14
+ const Tooltip = ({
15
+ delayDuration = 200,
16
+ ...props
17
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) => (
18
+ <TooltipPrimitive.Root delayDuration={delayDuration} {...props} />
19
+ );
10
20
 
11
21
  const TooltipTrigger = TooltipPrimitive.Trigger;
12
22