@cl-light-ds/ui 0.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 (254) hide show
  1. package/README.md +45 -0
  2. package/package.json +59 -0
  3. package/src/components/accordion/Accordion.astro +248 -0
  4. package/src/components/accordion/AccordionContent.astro +26 -0
  5. package/src/components/accordion/AccordionItem.astro +36 -0
  6. package/src/components/accordion/AccordionTrigger.astro +118 -0
  7. package/src/components/accordion/CHANGELOG.md +41 -0
  8. package/src/components/accordion/index.ts +4 -0
  9. package/src/components/alert/Alert.astro +167 -0
  10. package/src/components/alert/AlertDescription.astro +24 -0
  11. package/src/components/alert/AlertTitle.astro +24 -0
  12. package/src/components/alert/CHANGELOG.md +5 -0
  13. package/src/components/alert/index.ts +4 -0
  14. package/src/components/avatar/Avatar.astro +197 -0
  15. package/src/components/avatar/AvatarBadge.astro +44 -0
  16. package/src/components/avatar/AvatarFallback.astro +26 -0
  17. package/src/components/avatar/AvatarGroup.astro +26 -0
  18. package/src/components/avatar/AvatarGroupCount.astro +30 -0
  19. package/src/components/avatar/AvatarImage.astro +46 -0
  20. package/src/components/avatar/CHANGELOG.md +44 -0
  21. package/src/components/avatar/index.ts +6 -0
  22. package/src/components/badge/Badge.astro +167 -0
  23. package/src/components/badge/CHANGELOG.md +49 -0
  24. package/src/components/badge/index.ts +2 -0
  25. package/src/components/breadcrumb/Breadcrumb.astro +76 -0
  26. package/src/components/breadcrumb/BreadcrumbItem.astro +32 -0
  27. package/src/components/breadcrumb/BreadcrumbList.astro +26 -0
  28. package/src/components/breadcrumb/BreadcrumbSeparator.astro +26 -0
  29. package/src/components/breadcrumb/CHANGELOG.md +24 -0
  30. package/src/components/breadcrumb/index.ts +5 -0
  31. package/src/components/button/Button.astro +233 -0
  32. package/src/components/button/CHANGELOG.md +41 -0
  33. package/src/components/button/index.ts +2 -0
  34. package/src/components/button-group/ButtonGroup.astro +130 -0
  35. package/src/components/button-group/ButtonGroupSeparator.astro +30 -0
  36. package/src/components/button-group/ButtonGroupText.astro +21 -0
  37. package/src/components/button-group/CHANGELOG.md +34 -0
  38. package/src/components/button-group/index.ts +3 -0
  39. package/src/components/card/CHANGELOG.md +119 -0
  40. package/src/components/card/Card.astro +182 -0
  41. package/src/components/card/CardAction.astro +26 -0
  42. package/src/components/card/CardContent.astro +28 -0
  43. package/src/components/card/CardDescription.astro +28 -0
  44. package/src/components/card/CardFooter.astro +24 -0
  45. package/src/components/card/CardHeader.astro +30 -0
  46. package/src/components/card/CardMedia.astro +27 -0
  47. package/src/components/card/CardOverlay.astro +32 -0
  48. package/src/components/card/CardTitle.astro +21 -0
  49. package/src/components/card/index.ts +10 -0
  50. package/src/components/carousel/CHANGELOG.md +5 -0
  51. package/src/components/carousel/Carousel.astro +106 -0
  52. package/src/components/carousel/CarouselSlide.astro +22 -0
  53. package/src/components/carousel/index.ts +3 -0
  54. package/src/components/checkbox/CHANGELOG.md +58 -0
  55. package/src/components/checkbox/Checkbox.astro +159 -0
  56. package/src/components/checkbox/index.ts +1 -0
  57. package/src/components/collapsible/CHANGELOG.md +5 -0
  58. package/src/components/collapsible/Collapsible.astro +202 -0
  59. package/src/components/collapsible/CollapsibleContent.astro +36 -0
  60. package/src/components/collapsible/CollapsibleTrigger.astro +34 -0
  61. package/src/components/collapsible/index.ts +4 -0
  62. package/src/components/combobox/CHANGELOG.md +27 -0
  63. package/src/components/combobox/Combobox.astro +365 -0
  64. package/src/components/combobox/ComboboxContent.astro +37 -0
  65. package/src/components/combobox/ComboboxEmpty.astro +18 -0
  66. package/src/components/combobox/ComboboxGroup.astro +19 -0
  67. package/src/components/combobox/ComboboxInput.astro +74 -0
  68. package/src/components/combobox/ComboboxItem.astro +39 -0
  69. package/src/components/combobox/ComboboxLabel.astro +18 -0
  70. package/src/components/combobox/ComboboxList.astro +21 -0
  71. package/src/components/combobox/ComboboxSeparator.astro +17 -0
  72. package/src/components/combobox/ComboboxTrigger.astro +30 -0
  73. package/src/components/combobox/ComboboxValue.astro +22 -0
  74. package/src/components/combobox/index.ts +11 -0
  75. package/src/components/date/CHANGELOG.md +18 -0
  76. package/src/components/date/Date.astro +137 -0
  77. package/src/components/date/index.ts +2 -0
  78. package/src/components/dialog/CHANGELOG.md +39 -0
  79. package/src/components/dialog/Dialog.astro +196 -0
  80. package/src/components/dialog/DialogClose.astro +23 -0
  81. package/src/components/dialog/DialogContent.astro +42 -0
  82. package/src/components/dialog/DialogDescription.astro +18 -0
  83. package/src/components/dialog/DialogFooter.astro +18 -0
  84. package/src/components/dialog/DialogHeader.astro +18 -0
  85. package/src/components/dialog/DialogOverlay.astro +19 -0
  86. package/src/components/dialog/DialogTitle.astro +18 -0
  87. package/src/components/dialog/DialogTrigger.astro +37 -0
  88. package/src/components/dialog/index.ts +9 -0
  89. package/src/components/dropdown-menu/CHANGELOG.md +194 -0
  90. package/src/components/dropdown-menu/DropdownMenu.astro +226 -0
  91. package/src/components/dropdown-menu/DropdownMenuCheckboxItem.astro +114 -0
  92. package/src/components/dropdown-menu/DropdownMenuContent.astro +42 -0
  93. package/src/components/dropdown-menu/DropdownMenuGroup.astro +19 -0
  94. package/src/components/dropdown-menu/DropdownMenuItem.astro +220 -0
  95. package/src/components/dropdown-menu/DropdownMenuItemContent.astro +79 -0
  96. package/src/components/dropdown-menu/DropdownMenuLabel.astro +20 -0
  97. package/src/components/dropdown-menu/DropdownMenuPortal.astro +13 -0
  98. package/src/components/dropdown-menu/DropdownMenuRadioGroup.astro +13 -0
  99. package/src/components/dropdown-menu/DropdownMenuRadioItem.astro +101 -0
  100. package/src/components/dropdown-menu/DropdownMenuSeparator.astro +18 -0
  101. package/src/components/dropdown-menu/DropdownMenuShortcut.astro +18 -0
  102. package/src/components/dropdown-menu/DropdownMenuTrigger.astro +49 -0
  103. package/src/components/dropdown-menu/dropdown-menu-item-variants.ts +24 -0
  104. package/src/components/dropdown-menu/index.ts +12 -0
  105. package/src/components/field/CHANGELOG.md +33 -0
  106. package/src/components/field/Field.astro +664 -0
  107. package/src/components/field/FieldContent.astro +20 -0
  108. package/src/components/field/FieldDescription.astro +22 -0
  109. package/src/components/field/FieldError.astro +51 -0
  110. package/src/components/field/FieldGroup.astro +20 -0
  111. package/src/components/field/FieldLabel.astro +22 -0
  112. package/src/components/field/FieldLegend.astro +23 -0
  113. package/src/components/field/FieldSeparator.astro +30 -0
  114. package/src/components/field/FieldSet.astro +20 -0
  115. package/src/components/field/FieldTitle.astro +20 -0
  116. package/src/components/field/index.ts +10 -0
  117. package/src/components/hover-card/CHANGELOG.md +20 -0
  118. package/src/components/hover-card/HoverCard.astro +235 -0
  119. package/src/components/hover-card/HoverCardContent.astro +54 -0
  120. package/src/components/hover-card/HoverCardTrigger.astro +38 -0
  121. package/src/components/hover-card/index.ts +3 -0
  122. package/src/components/input/CHANGELOG.md +29 -0
  123. package/src/components/input/Input.astro +130 -0
  124. package/src/components/input/index.ts +2 -0
  125. package/src/components/input-group/CHANGELOG.md +49 -0
  126. package/src/components/input-group/InputGroup.astro +135 -0
  127. package/src/components/input-group/InputGroupAddon.astro +49 -0
  128. package/src/components/input-group/InputGroupButton.astro +58 -0
  129. package/src/components/input-group/InputGroupInput.astro +18 -0
  130. package/src/components/input-group/InputGroupText.astro +21 -0
  131. package/src/components/input-group/InputGroupTextarea.astro +17 -0
  132. package/src/components/input-group/controlClasses.ts +2 -0
  133. package/src/components/input-group/index.ts +6 -0
  134. package/src/components/item/CHANGELOG.md +27 -0
  135. package/src/components/item/Item.astro +122 -0
  136. package/src/components/item/ItemActions.astro +21 -0
  137. package/src/components/item/ItemContent.astro +25 -0
  138. package/src/components/item/ItemDescription.astro +25 -0
  139. package/src/components/item/ItemFooter.astro +24 -0
  140. package/src/components/item/ItemGroup.astro +24 -0
  141. package/src/components/item/ItemHeader.astro +24 -0
  142. package/src/components/item/ItemMedia.astro +47 -0
  143. package/src/components/item/ItemSeparator.astro +21 -0
  144. package/src/components/item/ItemTitle.astro +25 -0
  145. package/src/components/item/index.ts +10 -0
  146. package/src/components/kbd/CHANGELOG.md +5 -0
  147. package/src/components/kbd/Kbd.astro +145 -0
  148. package/src/components/kbd/KbdGroup.astro +55 -0
  149. package/src/components/kbd/index.ts +2 -0
  150. package/src/components/label/CHANGELOG.md +28 -0
  151. package/src/components/label/Label.astro +97 -0
  152. package/src/components/label/index.ts +2 -0
  153. package/src/components/link-group/CHANGELOG.md +75 -0
  154. package/src/components/link-group/LinkGroup.astro +67 -0
  155. package/src/components/link-group/LinkGroupItem.astro +32 -0
  156. package/src/components/link-group/LinkGroupList.astro +25 -0
  157. package/src/components/link-group/LinkGroupTitle.astro +22 -0
  158. package/src/components/link-group/index.ts +5 -0
  159. package/src/components/link-group/link-group-list-variants.ts +15 -0
  160. package/src/components/marquee/CHANGELOG.md +5 -0
  161. package/src/components/marquee/Marquee.astro +119 -0
  162. package/src/components/marquee/index.ts +2 -0
  163. package/src/components/native-select/CHANGELOG.md +5 -0
  164. package/src/components/native-select/NativeSelect.astro +125 -0
  165. package/src/components/native-select/NativeSelectOptGroup.astro +14 -0
  166. package/src/components/native-select/NativeSelectOption.astro +9 -0
  167. package/src/components/native-select/index.ts +3 -0
  168. package/src/components/navigation-menu/CHANGELOG.md +63 -0
  169. package/src/components/navigation-menu/NavigationMenu.astro +414 -0
  170. package/src/components/navigation-menu/NavigationMenuContent.astro +37 -0
  171. package/src/components/navigation-menu/NavigationMenuIndicator.astro +24 -0
  172. package/src/components/navigation-menu/NavigationMenuItem.astro +22 -0
  173. package/src/components/navigation-menu/NavigationMenuLink.astro +18 -0
  174. package/src/components/navigation-menu/NavigationMenuList.astro +21 -0
  175. package/src/components/navigation-menu/NavigationMenuPositioner.astro +44 -0
  176. package/src/components/navigation-menu/NavigationMenuTrigger.astro +30 -0
  177. package/src/components/navigation-menu/NavigationMenuViewport.astro +19 -0
  178. package/src/components/navigation-menu/index.ts +10 -0
  179. package/src/components/popover/CHANGELOG.md +5 -0
  180. package/src/components/popover/Popover.astro +210 -0
  181. package/src/components/popover/PopoverClose.astro +18 -0
  182. package/src/components/popover/PopoverContent.astro +49 -0
  183. package/src/components/popover/PopoverDescription.astro +18 -0
  184. package/src/components/popover/PopoverHeader.astro +18 -0
  185. package/src/components/popover/PopoverTitle.astro +18 -0
  186. package/src/components/popover/PopoverTrigger.astro +29 -0
  187. package/src/components/popover/index.ts +7 -0
  188. package/src/components/radio-group/CHANGELOG.md +9 -0
  189. package/src/components/radio-group/RadioGroup.astro +81 -0
  190. package/src/components/radio-group/RadioGroupItem.astro +42 -0
  191. package/src/components/radio-group/index.ts +3 -0
  192. package/src/components/select/CHANGELOG.md +39 -0
  193. package/src/components/select/Select.astro +373 -0
  194. package/src/components/select/SelectContent.astro +91 -0
  195. package/src/components/select/SelectGroup.astro +19 -0
  196. package/src/components/select/SelectItem.astro +48 -0
  197. package/src/components/select/SelectLabel.astro +18 -0
  198. package/src/components/select/SelectSeparator.astro +17 -0
  199. package/src/components/select/SelectTrigger.astro +34 -0
  200. package/src/components/select/SelectValue.astro +22 -0
  201. package/src/components/select/index.ts +8 -0
  202. package/src/components/separator/CHANGELOG.md +5 -0
  203. package/src/components/separator/Separator.astro +60 -0
  204. package/src/components/separator/index.ts +2 -0
  205. package/src/components/skeleton/CHANGELOG.md +5 -0
  206. package/src/components/skeleton/Skeleton.astro +121 -0
  207. package/src/components/skeleton/index.ts +1 -0
  208. package/src/components/slider/CHANGELOG.md +5 -0
  209. package/src/components/slider/Slider.astro +198 -0
  210. package/src/components/slider/index.ts +1 -0
  211. package/src/components/spinner/CHANGELOG.md +5 -0
  212. package/src/components/spinner/Spinner.astro +134 -0
  213. package/src/components/spinner/index.ts +1 -0
  214. package/src/components/sticky-surface/CHANGELOG.md +5 -0
  215. package/src/components/sticky-surface/StickySurface.astro +152 -0
  216. package/src/components/sticky-surface/index.ts +2 -0
  217. package/src/components/switch/CHANGELOG.md +9 -0
  218. package/src/components/switch/Switch.astro +106 -0
  219. package/src/components/switch/index.ts +1 -0
  220. package/src/components/table/CHANGELOG.md +5 -0
  221. package/src/components/table/Table.astro +250 -0
  222. package/src/components/table/TableBody.astro +18 -0
  223. package/src/components/table/TableCaption.astro +17 -0
  224. package/src/components/table/TableCell.astro +21 -0
  225. package/src/components/table/TableFooter.astro +21 -0
  226. package/src/components/table/TableHead.astro +21 -0
  227. package/src/components/table/TableHeader.astro +17 -0
  228. package/src/components/table/TableRow.astro +21 -0
  229. package/src/components/table/index.ts +9 -0
  230. package/src/components/tabs/CHANGELOG.md +5 -0
  231. package/src/components/tabs/Tabs.astro +306 -0
  232. package/src/components/tabs/TabsContent.astro +47 -0
  233. package/src/components/tabs/TabsList.astro +69 -0
  234. package/src/components/tabs/TabsTrigger.astro +40 -0
  235. package/src/components/tabs/index.ts +6 -0
  236. package/src/components/textarea/CHANGELOG.md +5 -0
  237. package/src/components/textarea/Textarea.astro +36 -0
  238. package/src/components/textarea/index.ts +2 -0
  239. package/src/components/toggle/CHANGELOG.md +5 -0
  240. package/src/components/toggle/Toggle.astro +138 -0
  241. package/src/components/toggle/index.ts +1 -0
  242. package/src/components/toggle-group/CHANGELOG.md +5 -0
  243. package/src/components/toggle-group/ToggleGroup.astro +224 -0
  244. package/src/components/toggle-group/ToggleGroupItem.astro +48 -0
  245. package/src/components/toggle-group/index.ts +2 -0
  246. package/src/components/tooltip/CHANGELOG.md +5 -0
  247. package/src/components/tooltip/Tooltip.astro +331 -0
  248. package/src/components/tooltip/TooltipContent.astro +60 -0
  249. package/src/components/tooltip/TooltipTrigger.astro +34 -0
  250. package/src/components/tooltip/index.ts +3 -0
  251. package/src/lib/toggle-group-controller.ts +113 -0
  252. package/src/lib/toggle-shared.ts +26 -0
  253. package/src/lib/utils.ts +6 -0
  254. package/src/styles/globals.css +177 -0
@@ -0,0 +1,196 @@
1
+ ---
2
+ /**
3
+ * @component Dialog
4
+ * @title Dialog
5
+ * @description An accessible modal window for focused content or user actions with customizable open/close behavior.
6
+ * @figmaUrl https://www.figma.com/design/qXkzdqZ3QPQqgckU3AwIAH/ClickLearn-Lightning-Design-Library?node-id=141-6002
7
+ *
8
+ * @preview
9
+ *
10
+ * <Dialog>
11
+ * <DialogTrigger asChild>
12
+ * <Button>Manage Cookies</Button>
13
+ * </DialogTrigger>
14
+ *
15
+ * <DialogContent class="sm:max-w-lg">
16
+ * <DialogHeader>
17
+ * <DialogTitle>Manage Cookies</DialogTitle>
18
+ * <DialogDescription>
19
+ * Using websites and apps involves storing and retrieving information from your device, including cookies and other identifiers.
20
+ * </DialogDescription>
21
+ * </DialogHeader>
22
+ * <div class="mt-3">
23
+ * <div class="flex items-center gap-3">
24
+ * <Checkbox id="cookies-necessary" name="cookies-necessary" disabled checked />
25
+ * <Label for="cookies-necessary">Strictly Necessary Cookies (always active)</Label>
26
+ * </div>
27
+ * <p class="text-sm text-muted-foreground ml-8 mt-1">These cookies are essential for the site to function and cannot be toggled off. They assist with security, user authentication, customer support, etc.</p>
28
+ * </div>
29
+ * <div class="mt-3">
30
+ * <div class="flex items-center gap-3">
31
+ * <Checkbox id="cookies-analytics" name="cookies-analytics" />
32
+ * <Label for="cookies-analytics">Analytics Cookies</Label>
33
+ * </div>
34
+ * <p class="text-sm text-muted-foreground ml-8 mt-1">These cookies help us understand how visitors interact with our site. They allow us to measure traffic and improve site performance.</p>
35
+ * </div>
36
+ * <div class="mt-3">
37
+ * <div class="flex items-center gap-3">
38
+ * <Checkbox id="cookies-marketing-performance" name="cookies-marketing-performance" />
39
+ * <Label for="cookies-marketing-performance">Marketing Performance Cookies</Label>
40
+ * </div>
41
+ * <p class="text-sm text-muted-foreground ml-8 mt-1">These cookies help us measure the effectiveness of our marketing campaigns.</p>
42
+ * </div>
43
+ * <DialogFooter>
44
+ * <Button data-slot="dialog-close" class="mt-4" variant="outline">Close</Button>
45
+ * <Button data-dialog-close class="mt-4">Save changes</Button>
46
+ * </DialogFooter>
47
+ * </DialogContent>
48
+ * </Dialog>
49
+ *
50
+ * @usage
51
+ *
52
+ * ```astro nocollapse
53
+ * ---
54
+ * import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogFooter, DialogClose } from '@bejamas/ui/components/dialog';
55
+ * import { Button } from '@bejamas/ui/components/button';
56
+ * ---
57
+ *
58
+ *
59
+ * <Dialog id="myDialog">
60
+ * <DialogTrigger>Open Dialog</DialogTrigger>
61
+ * <DialogContent>
62
+ * <DialogTitle>Dialog Title</DialogTitle>
63
+ * <DialogDescription>Dialog description.</DialogDescription>
64
+ * Content here
65
+ * <DialogFooter>
66
+ * <DialogClose>Close</DialogClose>
67
+ * </DialogFooter>
68
+ * </DialogContent>
69
+ * </Dialog>
70
+ * ```
71
+ *
72
+ * @api
73
+ *
74
+ * ### Events
75
+ *
76
+ * The dialog emits custom events that you can listen to:
77
+ *
78
+ * | Event | Detail | Description |
79
+ * |-------|--------|-------------|
80
+ * | `dialog:change` | `{ open: boolean }` | Fired when the open state changes |
81
+ *
82
+ * ```astro nocollapse
83
+ * <Dialog id="my-dialog">
84
+ * <DialogTrigger>Open Dialog</DialogTrigger>
85
+ * <DialogContent>
86
+ * <DialogTitle>Title</DialogTitle>
87
+ * <DialogDescription>Description</DialogDescription>
88
+ * </DialogContent>
89
+ * </Dialog>
90
+ * ```
91
+ *
92
+ * ```js nocollapse
93
+ * const dialog = document.getElementById('my-dialog');
94
+ *
95
+ * dialog.addEventListener('dialog:change', (e) => {
96
+ * console.log('Is open:', e.detail.open);
97
+ * });
98
+ * ```
99
+ *
100
+ * ### Programmatic Control
101
+ *
102
+ * You can control the dialog programmatically by dispatching a `dialog:set` event:
103
+ *
104
+ * ```js nocollapse
105
+ * const dialog = document.getElementById('my-dialog');
106
+ *
107
+ * // Open the dialog
108
+ * dialog.dispatchEvent(new CustomEvent('dialog:set', {
109
+ * detail: { open: true }
110
+ * }));
111
+ *
112
+ * // Close the dialog
113
+ * dialog.dispatchEvent(new CustomEvent('dialog:set', {
114
+ * detail: { open: false }
115
+ * }));
116
+ * ```
117
+ *
118
+ * ### Data Attributes
119
+ *
120
+ * The dialog sets these data attributes that you can use for styling or querying state:
121
+ *
122
+ * | Attribute | Element | Description |
123
+ * |-----------|---------|-------------|
124
+ * | `data-state` | dialog | Current state (`open` or `closed`) |
125
+ *
126
+ * @examples
127
+ *
128
+ * ### Nested Dialogs
129
+ *
130
+ * <Dialog>
131
+ * <Button data-slot="dialog-trigger" class="">Open Nested Dialog</Button>
132
+ * <DialogContent class="sm:max-w-lg">
133
+ * <DialogHeader>
134
+ * <DialogTitle>Nested Dialog</DialogTitle>
135
+ * <DialogDescription>
136
+ * Using websites and apps involves storing and retrieving information from your device, including cookies and other identifiers.
137
+ * </DialogDescription>
138
+ * </DialogHeader>
139
+ * <DialogFooter>
140
+ * <Button data-slot="dialog-close" class="mt-4" variant="outline">Close</Button>
141
+ * <Dialog>
142
+ * <Button data-slot="dialog-trigger" class="mt-4">Open Nested Dialog</Button>
143
+ * <DialogContent class="sm:max-w-md">
144
+ * <DialogHeader>
145
+ * <DialogTitle>Nested Dialog</DialogTitle>
146
+ * <DialogDescription>
147
+ * Using websites and apps involves storing and retrieving information from your device, including cookies and other identifiers.
148
+ * </DialogDescription>
149
+ * </DialogHeader>
150
+ * </DialogContent>
151
+ * </Dialog>
152
+ * </DialogFooter>
153
+ * </DialogContent>
154
+ * </Dialog>
155
+ *
156
+ */
157
+ import { cn } from "@cl-light-ds/ui/lib/utils";
158
+ import type { HTMLAttributes } from "astro/types";
159
+
160
+ interface Props extends HTMLAttributes<"div"> {
161
+ defaultOpen?: boolean;
162
+ class?: string;
163
+ closeOnClickOutside?: boolean;
164
+ closeOnEscape?: boolean;
165
+ lockScroll?: boolean;
166
+ }
167
+
168
+ const {
169
+ class: className = "",
170
+ defaultOpen = false,
171
+ closeOnClickOutside = true,
172
+ closeOnEscape = true,
173
+ lockScroll = true,
174
+ ...rest
175
+ } = Astro.props as Props;
176
+ ---
177
+
178
+ <div
179
+ data-slot="dialog"
180
+ data-default-open={defaultOpen}
181
+ data-close-on-click-outside={closeOnClickOutside}
182
+ data-close-on-escape={closeOnEscape}
183
+ data-lock-scroll={lockScroll}
184
+ class={cn("", className)}
185
+ {...rest}
186
+ >
187
+ <slot />
188
+ </div>
189
+
190
+ <script>
191
+ import { createDialog } from "@data-slot/dialog";
192
+
193
+ document
194
+ .querySelectorAll<HTMLElement>('[data-slot="dialog"]')
195
+ .forEach((el) => createDialog(el));
196
+ </script>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+ import Button from "../button/Button.astro";
5
+
6
+ interface Props extends HTMLAttributes<"button"> {
7
+ class?: string;
8
+ }
9
+
10
+ const { class: className = "", ...rest } = Astro.props as Props;
11
+ ---
12
+
13
+ <Button
14
+ {...rest}
15
+ data-slot="dialog-close"
16
+ data-dialog-close
17
+ variant="ghost"
18
+ size="icon-sm"
19
+ class={cn("absolute top-2 right-2", className)}
20
+ >
21
+ <slot />
22
+ {!Astro.slots.has("default") && <span class="sr-only">Close</span>}
23
+ </Button>
@@ -0,0 +1,42 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ import DialogClose from "./DialogClose.astro";
6
+ import DialogOverlay from "./DialogOverlay.astro";
7
+
8
+ interface Props extends HTMLAttributes<"div"> {
9
+ showCloseButton?: boolean;
10
+ }
11
+
12
+ const {
13
+ showCloseButton = true,
14
+ class: className = "",
15
+ ...rest
16
+ } = Astro.props as Props;
17
+ ---
18
+
19
+ <div data-slot="dialog-portal" class="z-50 isolate absolute">
20
+ <DialogOverlay />
21
+ <div
22
+ data-slot="dialog-content"
23
+ hidden
24
+ class={cn(
25
+ "bg-card border-border data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 max-w-[calc(100%-2rem)] rounded-lg border shadow-lg duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
26
+ className,
27
+ )}
28
+ {...rest}
29
+ >
30
+ <div class="grid gap-6 p-6 text-sm">
31
+ <slot />
32
+ </div>
33
+ {
34
+ showCloseButton && (
35
+ <DialogClose>
36
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/></svg>
37
+ <span class="sr-only">Close</span>
38
+ </DialogClose>
39
+ )
40
+ }
41
+ </div>
42
+ </div>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: className = "", ...rest } = Astro.props as Props;
10
+ ---
11
+
12
+ <div
13
+ {...rest}
14
+ data-slot="dialog-description"
15
+ class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
16
+ >
17
+ <slot />
18
+ </div>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: className = "", ...rest } = Astro.props as Props;
10
+ ---
11
+
12
+ <div
13
+ {...rest}
14
+ data-slot="dialog-footer"
15
+ class={cn("flex justify-end gap-2", className)}
16
+ >
17
+ <slot />
18
+ </div>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: className = "", ...rest } = Astro.props as Props;
10
+ ---
11
+
12
+ <div
13
+ data-slot="dialog-header"
14
+ class={cn("gap-2 flex flex-col", className)}
15
+ {...rest}
16
+ >
17
+ <slot />
18
+ </div>
@@ -0,0 +1,19 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: className = "", ...rest } = Astro.props as Props;
10
+ ---
11
+
12
+ <div
13
+ hidden
14
+ data-slot="dialog-overlay"
15
+ class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
16
+ {...rest}
17
+ >
18
+ <slot />
19
+ </div>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"div"> {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: className = "", ...rest } = Astro.props as Props;
10
+ ---
11
+
12
+ <div
13
+ {...rest}
14
+ data-slot="dialog-title"
15
+ class={cn("text-xl leading-6 font-semibold cn-font-heading", className)}
16
+ >
17
+ <slot />
18
+ </div>
@@ -0,0 +1,37 @@
1
+ ---
2
+ import { cn } from "@cl-light-ds/ui/lib/utils";
3
+ import type { HTMLAttributes } from "astro/types";
4
+
5
+ interface Props extends HTMLAttributes<"button"> {
6
+ class?: string;
7
+ /** Radix-like: render wrapper so the child can be the interactive element */
8
+ asChild?: boolean;
9
+ }
10
+
11
+ const {
12
+ class: className = "",
13
+ asChild = false,
14
+ type = "button",
15
+ ...rest
16
+ } = Astro.props as Props;
17
+
18
+ const defaultClasses =
19
+ "cursor-pointer select-none outline-none group-focus-within:pointer-events-none inline-flex items-center gap-1";
20
+ ---
21
+
22
+ {
23
+ asChild ? (
24
+ <div data-slot="dialog-trigger" data-as-child class={cn("", defaultClasses, className)}>
25
+ <slot />
26
+ </div>
27
+ ) : (
28
+ <button
29
+ {...rest}
30
+ type={type}
31
+ data-slot="dialog-trigger"
32
+ class={cn("", defaultClasses, className)}
33
+ >
34
+ <slot />
35
+ </button>
36
+ )
37
+ }
@@ -0,0 +1,9 @@
1
+ export { default as Dialog } from "./Dialog.astro";
2
+ export { default as DialogTitle } from "./DialogTitle.astro";
3
+ export { default as DialogDescription } from "./DialogDescription.astro";
4
+ export { default as DialogContent } from "./DialogContent.astro";
5
+ export { default as DialogFooter } from "./DialogFooter.astro";
6
+ export { default as DialogClose } from "./DialogClose.astro";
7
+ export { default as DialogHeader } from "./DialogHeader.astro";
8
+ export { default as DialogTrigger } from "./DialogTrigger.astro";
9
+ export { default as DialogOverlay } from "./DialogOverlay.astro";
@@ -0,0 +1,194 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `DropdownMenu` component will be documented in this file.
4
+
5
+ ## Date 10th Sep 2026
6
+ ## Figma alignment (node 2329-6521, prop table 7392-10142)
7
+
8
+ ### Variants
9
+
10
+ - Added a `category` variant to `DropdownMenuItem` (`variant: "default" | "destructive" | "category"`, was `"default" | "destructive"`), matching Figma's `.item` component set which documents `Category` as a third item variant. Renders as a non-interactive (`role="presentation"`, `tabindex={-1}`, `pointer-events-none`), muted (`text-muted-foreground font-medium`) row — the visual is intentionally close to the existing `DropdownMenuLabel`, but this variant lives on `DropdownMenuItem` itself so it can sit inline in a list without an extra import, per Figma's own prop table contract. It does not render a separator automatically — pair it with the existing `<DropdownMenuSeparator />` if a divider is wanted, same as any other item.
11
+ - Converted `DropdownMenuItem` from hardcoded `data-[variant=...]:` Tailwind selectors to `class-variance-authority` (`cva`), matching the pattern already used by `Card`/`Button`/`Badge`/`Avatar`.
12
+
13
+ ### Sizing & Spacing
14
+
15
+ - **Radius:** `rounded-sm` (4px) → `rounded-lg` (8px) on `DropdownMenuItem`, `DropdownMenuCheckboxItem`, and `DropdownMenuRadioItem` — Figma's item box is consistently `rounded-[8px]`.
16
+ - **Padding:** `py-1.5` (6px) → `py-1` (4px) on all three item components — Figma's item is `py-[4px]` (`spacing/1`), horizontal `px-2` (`spacing/2`, 8px) was already correct.
17
+ - **Typography:** item label moved from unstyled `text-sm` (14px, regular) → `text-xs font-semibold` (12px/600) on all three item components, matching Figma's `text/sm/sm-semibold` text style for the item label. Supportive text (new, see below) is `text-xs font-normal text-muted-foreground`.
18
+ - **Popover chrome** (`DropdownMenuContent`): `ring-1 ring-foreground/10` → `border border-border` (Figma uses a real 1px border, not a ring); `shadow-md` → Figma's actual 3-layer `shadow-lg` drop-shadow (`0.3px 2px/-2.2 spread #1F003B05, 0.5px 3.2px/-1.4 #1F003B08, 0.7px 4px/-1.8 #1F003B0F`); `p-1` (4px) → `p-2` (8px); added an explicit `gap-1.5` (6px) between items, matching Figma's item-to-item gap (previously no gap was set at all, relying on nothing — items had no separation beyond their own padding).
19
+
20
+ ### New Props (`DropdownMenuItem` only)
21
+
22
+ Following the Button/Badge "boolean prop, auto-detected from a named slot via `Astro.slots.has(...)`" convention:
23
+
24
+ - `checked?: boolean` (default `false`) — presentational checked state, only visible when `showCheckbox` is also set.
25
+ - `showCheckbox?: boolean` (default `false`) — reserves a 20px leading slot for a checkmark; **kept the existing indicator pattern** (no visible box at rest, a checkmark fades in when `checked`), matching `DropdownMenuCheckboxItem`'s existing visual language rather than inventing Figma's filled-square glyph, which has no documented unchecked state in the file. `DropdownMenuCheckboxItem` itself is unchanged — this is a separate, presentation-only affordance for a plain `DropdownMenuItem`.
26
+ - `leadingIcon?: boolean` (default: has a `leading-icon` slot) — renders `<slot name="leading-icon" />` at 20px.
27
+ - `avatar?: boolean` (default: has an `avatar` slot) — renders `<slot name="avatar" />`. Use `<Avatar size="sm">` (24px) — it already matches Figma's item avatar size exactly, no new Avatar sizing needed.
28
+ - `showBadge?: boolean` (default: has a `badge` slot) — renders `<slot name="badge" />`. Use `<Badge size="sm">`.
29
+ - `showSubmenuIcon?: boolean` (default `false`) — renders a static 20px chevron-right. **Cosmetic only** — there is no nested-submenu infrastructure (flyout positioning, keyboard handling) anywhere in `DropdownMenu` yet. See "Flagged for the designer" below.
30
+ - `supportiveText?: boolean` (default: has a `supportive-text` slot) — renders a second, muted line under the item label via `<slot name="supportive-text" />`.
31
+
32
+ `leadingIcon`, `avatar`, and `showCheckbox` are independent booleans (can combine), per Figma's literal prop table — not consolidated into a single `leading` enum.
33
+
34
+ ### Trigger
35
+
36
+ - `DropdownMenuTrigger` now defaults `variant="outline"` (was inheriting `Button`'s own default `"default"`/primary-blue variant) — Figma's default/closed trigger state is a white/bordered button, not primary. Still fully overridable via the existing `variant` prop.
37
+ - `DropdownMenuTrigger` now renders a default trailing chevron (`ChevronDown` from `@lucide/astro`) when the consumer doesn't slot their own `tail-icon` content — Figma's trigger always shows a chevron as core anatomy, not an opt-in add-on. It rotates 180° when the menu is open via a new `group/dropdown-menu` marker added to `DropdownMenu.astro`'s root (`data-state` lands on that root element, confirmed in `@data-slot/dropdown-menu`'s runtime). Pass `tailIcon={false}` to opt out entirely, or your own `<Icon slot="tail-icon" />` to override.
38
+
39
+ ### Tokens (`globals.css`)
40
+
41
+ - No new tokens needed. Every color/radius used above (`background`, `background-hover`, `muted`, `muted-foreground`, `foreground`, `border`, `ring`, `chart-1`, `destructive`, `popover`, `primary`, `primary-foreground`) already existed — this was a binding/usage fix, not a token-addition pass.
42
+
43
+ ### Storybook & Docs
44
+
45
+ - `apps/web/src/pages/storybook/_sections/dropdown-menu.astro`: added the seven new `DropdownMenuItem` prop rows to `itemProps`, updated the `variant` row to include `"category"`, and added preview blocks for leading icon / avatar / badge / supportive text / category / checkbox.
46
+ - `DropdownMenuStory.astro` / `DropdownMenu.stories.ts`: added stories covering the new item variants/props.
47
+
48
+ ### Not changed
49
+
50
+ - `DropdownMenuLabel.astro` — spotted a pre-existing bug while auditing (its class list sets both `text-xs` and `text-sm`, with `text-sm` winning via `tailwind-merge`, so labels render at 14px instead of the intended 12px) but left as-is since it wasn't part of this pass's scope; worth a follow-up fix.
51
+ - `DropdownMenuGroup.astro`, `DropdownMenuSeparator.astro`, `DropdownMenuPortal.astro`, `DropdownMenuRadioGroup.astro`, `DropdownMenuShortcut.astro` — no Figma delta identified.
52
+
53
+ ### Flagged for the designer / engineering (not addressed in this pass)
54
+
55
+ - **Real nested submenus:** Figma's `.item` prop table documents `showSubmenuIcon`, implying a submenu affordance, but no submenu flyout, positioning, or keyboard-navigation behavior exists anywhere in `DropdownMenu`'s runtime (`@data-slot/dropdown-menu`). This pass ships the chevron as a static, cosmetic icon only. Building real nested submenus is a separate, larger feature.
56
+ - **Checkbox unchecked state:** Figma's item file only shows a filled/checked 20px checkbox square in its Left Content — there's no authored unchecked-square state to match. `showCheckbox`/`checked` on `DropdownMenuItem` therefore reuses the existing checkmark-only indicator pattern instead; worth designing a real unchecked square in Figma if a literal checkbox control (vs. a checked-indicator) is wanted here later.
57
+ - **`DropdownMenuLabel` vs. `DropdownMenuItem variant="category"` overlap:** these are now two different ways to render a muted section header (one a dedicated component, one an item variant). Worth a follow-up design decision on whether `DropdownMenuLabel` should be deprecated in favor of `variant="category"`, or the two are meant to serve genuinely different call sites.
58
+
59
+ ## Date 10th Sep 2026 (follow-up)
60
+ ## Fix: menuitemcheckbox/menuitemradio alignment + item padding (node 2329-6521, prop table 7392-10142)
61
+
62
+ The designer updated Figma again after the first pass above: the `DropdownMenuItem` prop table now
63
+ explicitly ties `showCheckbox` to *"menuitemcheckbox"* and adds a new `showRadio` → *"menuitemradio"*,
64
+ and the item component's padding/radio glyph changed. This entry reconciles that with the framework's
65
+ existing three-component structure (`DropdownMenuItem` / `DropdownMenuCheckboxItem` / `DropdownMenuRadioItem`)
66
+ rather than collapsing them into one — see "Architecture" below for why.
67
+
68
+ ### Architecture
69
+
70
+ - Extracted the shared item chrome into two new **internal, non-exported** files (not added to `index.ts`,
71
+ same precedent as `DropdownMenuStory.astro`): `dropdown-menu-item-variants.ts` (the single `cva` box-class
72
+ builder) and `DropdownMenuItemContent.astro` (the shared Left Content / label / Right Content layout).
73
+ `DropdownMenuItem`, `DropdownMenuCheckboxItem`, and `DropdownMenuRadioItem` all now render through these,
74
+ instead of each duplicating the same padding/typography classes.
75
+ - **Did not consolidate into one role-switching component.** Figma's single-component mental model
76
+ (`DropdownMenuItem` + a boolean that "becomes" a checkbox/radio) doesn't map cleanly onto this
77
+ framework: `@data-slot/dropdown-menu`'s runtime detects real interactive item type purely from each
78
+ element's `data-slot` value and manages `role`/`tabindex`/`aria-checked`/checked-state tracking
79
+ automatically for `[data-slot="dropdown-menu-checkbox-item"]` / `[data-slot="dropdown-menu-radio-item"]`.
80
+ Collapsing three ARIA-correct, already-working components into one conditionally-shaped component would
81
+ have been a much larger, riskier change for no framework-side benefit — the shared-chrome extraction above
82
+ gets the same "one visual spec, one place" outcome without touching the runtime contract.
83
+
84
+ ### Sizing & Spacing
85
+
86
+ - **Padding correction:** Figma's item box is now uniform `p-2` (8px on all sides) for `default`/`destructive`
87
+ — was `px-2 py-1` last pass (a padding fix I hadn't fully caught the first time). Row height grows
88
+ 24px → 32px accordingly, on `DropdownMenuItem`, `DropdownMenuCheckboxItem`, and `DropdownMenuRadioItem`.
89
+ - **`category` has its own distinct, tighter padding** (`pt-1 px-1 pb-0 gap-1`, no `p-2`) — this was already
90
+ structurally different in Figma and wasn't captured correctly last pass, where it incorrectly shared the
91
+ interactive variants' row padding.
92
+ - `DropdownMenuCheckboxItem`/`DropdownMenuRadioItem` **dropped their old asymmetric `pr-8 pl-2` padding**
93
+ entirely — no longer needed now that the checked/selected indicator lives in Left Content instead of
94
+ floating absolute-right (see below), so they use the exact same `p-2` box as a plain `DropdownMenuItem`.
95
+
96
+ ### Checkbox / Radio glyph — switched to Figma's literal look
97
+
98
+ - `DropdownMenuCheckboxItem`/`DropdownMenuRadioItem` moved from the old shadcn-style indicator (invisible
99
+ until checked, a bare checkmark fading in at `absolute right-2`) to Figma's literal glyph: an
100
+ always-reserved 20px box (`rounded-sm`, checkbox) or circle (`rounded-full`, radio) in Left Content,
101
+ outlined (`border-input`) when unchecked and `bg-primary border-primary` + a white check icon / centered
102
+ dot when checked. Visual language borrowed directly from the existing `Checkbox.astro`/`RadioGroupItem.astro`
103
+ components (same token usage, scaled to the item's 20px glyph size) rather than inventing new colors.
104
+ - This is driven purely by CSS off the `data-checked` attribute the runtime **already sets** on these exact
105
+ elements (confirmed in `@data-slot/dropdown-menu`'s source) via `group-data-checked/dropdown-menu-item:` —
106
+ no new JavaScript needed, no change to the runtime's checked-state tracking or click/keyboard handling.
107
+ - `DropdownMenuItem`'s own presentational `showCheckbox` also switched from the old checkmark-only pattern to
108
+ the same 20px box glyph (driven by its static `checked` prop, not runtime state), for visual consistency
109
+ with the real components. New presentational `showRadio` (see below) uses the matching circle glyph.
110
+ - **Resolves last pass's "Checkbox unchecked state" flag** — Figma still doesn't author a literal unchecked
111
+ square, but reusing `Checkbox`'s existing unchecked visual (bordered, no fill) is a defensible, low-risk
112
+ default that's already established elsewhere in the design system.
113
+
114
+ ### New Props
115
+
116
+ - `DropdownMenuItem` gains `showRadio?: boolean` (default `false`), mirroring `showCheckbox` — presentational
117
+ only, reuses the shared `checked` prop. Per Figma's prop table: `showRadio` → *"Show radiobutton which is
118
+ menuitemradio"*.
119
+ - `DropdownMenuCheckboxItem` and `DropdownMenuRadioItem` both gain the same extra prop surface
120
+ `DropdownMenuItem` got last pass: `leadingIcon`, `avatar`, `showBadge`, `showSubmenuIcon`, `supportiveText`
121
+ (same `Astro.slots.has(...)`-default convention). Sharing chrome made this nearly free, and Figma's own
122
+ item variants already mix a checkbox with other leading/trailing content in the same row.
123
+ - `value`, `disabled`, `inset`, `defaultChecked`, `role="menuitemcheckbox"`/`"menuitemradio"`,
124
+ `data-slot="dropdown-menu-checkbox-item"`/`"dropdown-menu-radio-item"` are all **unchanged** — this is the
125
+ ARIA/runtime contract and was not touched.
126
+
127
+ ### Fixed (implementation bug, not a Figma delta)
128
+
129
+ - While building the shared chrome, caught a real bug in the first draft: forwarding a
130
+ `<Fragment slot="leading-icon">` etc. from each root component into the shared `DropdownMenuItemContent`
131
+ made `Astro.slots.has("leading-icon")` always return `true` inside that shared partial (it saw the
132
+ pass-through Fragment, not whether the real end-consumer supplied content), so empty leading-icon/avatar/
133
+ badge wrapper `<span>`s rendered on every single item regardless of props. Fixed by resolving
134
+ `Astro.slots.has(...)` defaults in each outer component (`DropdownMenuItem`/`CheckboxItem`/`RadioItem`,
135
+ against their *own* callers) and passing explicit resolved booleans down — the shared partial no longer
136
+ does any slot introspection of its own. Verified via built story output before/after (span count per item
137
+ went from unconditional-always to matching only the items that actually set the prop).
138
+
139
+ ### Storybook & Docs
140
+
141
+ - `apps/web/src/pages/storybook/_sections/dropdown-menu.astro`: added a `showRadio` row to `itemProps`;
142
+ updated `leadingIcon`/`avatar`/`showBadge`/`showSubmenuIcon`/`supportiveText` descriptions to note they now
143
+ apply to all three item components, not just `DropdownMenuItem`; enriched the Single Select/Multi Select
144
+ preview blocks to exercise the new prop parity (`supportiveText` on a radio item, `showBadge` on a checkbox
145
+ item); added `showRadio` to the "Item content" preview.
146
+ - `DropdownMenuStory.astro`/`DropdownMenu.stories.ts`: added a new `CheckboxRadioContent` story
147
+ (`showCheckboxRadio` prop) covering presentational checked/unchecked checkbox and radio glyphs side by side
148
+ with the real, runtime-tracked `DropdownMenuCheckboxItem`/`DropdownMenuRadioItem` (one with `avatar`, one
149
+ with `showBadge`) so both visual systems are directly comparable.
150
+
151
+ ### Flagged for the designer / engineering (not addressed in this pass)
152
+
153
+ - **`showCheckbox`/`showRadio` on plain `DropdownMenuItem` are cosmetic only** — they draw the glyph via a
154
+ static prop and don't manage checked/selected state or emit events. Don't reach for them expecting real
155
+ interactivity; that's what `DropdownMenuCheckboxItem`/`DropdownMenuRadioItem` are for. Worth a naming pass
156
+ if this keeps causing confusion (e.g. renaming the presentational props to make the distinction more
157
+ obvious in autocomplete).
158
+ - Carried over from last pass, still open: real nested submenus (`showSubmenuIcon` is still cosmetic-only),
159
+ and the `DropdownMenuLabel` vs. `variant="category"` overlap.
160
+
161
+ ## Date 10th Sep 2026 (follow-up)
162
+ ## Fix: distinct Hover vs. Focus states (node 2529-2338, item states)
163
+
164
+ While adding a states preview to the Storybook docs, caught that `DropdownMenuItem` (and by extension
165
+ `DropdownMenuCheckboxItem`/`DropdownMenuRadioItem`, which share its `cva`) only ever had **one** interactive
166
+ style — `focus:bg-background-hover` — with no real `:hover` class at all. Figma documents Hover and Focused
167
+ as two visually distinct states (Hover: grey background only; Focused: a blue border + ring, no background
168
+ change), which wasn't implemented.
169
+
170
+ ### Fixed
171
+
172
+ - `dropdown-menu-item-variants.ts`: added a real `hover:bg-background-hover` (per variant, `destructive` gets
173
+ `hover:text-destructive dark:hover:bg-destructive/20`) so mouse hover now works with plain CSS — no JS
174
+ required, matching Figma's Hover state.
175
+ - Replaced `focus:bg-background-hover` with `focus-visible:border-ring focus-visible:ring-2
176
+ focus-visible:ring-chart-1`, matching Figma's Focused effect (`border-ring` + a 2px ring) and the same
177
+ `focus-visible:border-ring focus-visible:ring-*` pattern already used by `Button`/`Card`. Added
178
+ `border border-transparent` to the shared base so the border can swap color on focus without any layout
179
+ shift (same technique `Button`/`Card` already use).
180
+ - Using `:focus-visible` instead of plain `:focus` is deliberate: the dropdown runtime calls a real
181
+ `element.focus()` on pointer hover (so highlight tracks the mouse for accessibility purposes) — with
182
+ `:focus-visible`, browsers' own heuristics correctly suppress the keyboard-style ring for that
183
+ pointer-triggered focus (real `:hover` covers the visual instead) while still showing the ring for genuine
184
+ keyboard navigation (Tab/Arrow keys). Net effect: mouse hover shows grey background only, keyboard
185
+ navigation shows the blue ring — exactly Figma's distinction, with no behavior change to the runtime itself.
186
+
187
+ ### Storybook & Docs
188
+
189
+ - `apps/web/src/pages/storybook/_sections/dropdown-menu.astro`: added a new "DropdownMenuItem states" preview
190
+ (Default/Hover/Focus/Disabled) above "DropdownMenuItem variants" — Hover/Focus rows use a forced `class`
191
+ override for an always-visible reference, with a note that the same states are live (hover/Tab to see them
192
+ for real anywhere in the gallery).
193
+
194
+ ## Unreleased