@dogsbay/ui 0.2.0-beta.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 (322) hide show
  1. package/package.json +38 -0
  2. package/src/accordion/Accordion.astro +38 -0
  3. package/src/accordion/AccordionContent.astro +25 -0
  4. package/src/accordion/AccordionItem.astro +26 -0
  5. package/src/accordion/AccordionTrigger.astro +38 -0
  6. package/src/accordion/accordion.ts +189 -0
  7. package/src/accordion/index.ts +4 -0
  8. package/src/alert/Alert.astro +69 -0
  9. package/src/alert/AlertDescription.astro +17 -0
  10. package/src/alert/AlertTitle.astro +21 -0
  11. package/src/alert/CollapsibleAlert.astro +103 -0
  12. package/src/alert/icon.ts +32 -0
  13. package/src/alert/index.ts +4 -0
  14. package/src/alert/variants.ts +29 -0
  15. package/src/alert-dialog/AlertDialog.astro +16 -0
  16. package/src/alert-dialog/AlertDialogAction.astro +23 -0
  17. package/src/alert-dialog/AlertDialogCancel.astro +23 -0
  18. package/src/alert-dialog/AlertDialogContent.astro +37 -0
  19. package/src/alert-dialog/AlertDialogDescription.astro +18 -0
  20. package/src/alert-dialog/AlertDialogFooter.astro +18 -0
  21. package/src/alert-dialog/AlertDialogHeader.astro +18 -0
  22. package/src/alert-dialog/AlertDialogTitle.astro +18 -0
  23. package/src/alert-dialog/AlertDialogTrigger.astro +24 -0
  24. package/src/alert-dialog/alert-dialog.ts +154 -0
  25. package/src/alert-dialog/index.ts +9 -0
  26. package/src/api-doc/ApiDoc.astro +52 -0
  27. package/src/api-doc/index.ts +1 -0
  28. package/src/api-layout/ApiCodePanel.astro +21 -0
  29. package/src/api-layout/ApiLayout.astro +29 -0
  30. package/src/api-layout/ExampleBlock.astro +63 -0
  31. package/src/api-params/ApiParams.astro +96 -0
  32. package/src/api-params/index.ts +1 -0
  33. package/src/api-source/ApiSource.astro +20 -0
  34. package/src/api-source/api-source.css +32 -0
  35. package/src/api-source/index.ts +1 -0
  36. package/src/api-symbol/ApiSymbol.astro +48 -0
  37. package/src/api-symbol/api-symbol.css +89 -0
  38. package/src/api-symbol/index.ts +1 -0
  39. package/src/api-url-bar/ApiUrlBar.astro +63 -0
  40. package/src/avatar/Avatar.astro +57 -0
  41. package/src/avatar/index.ts +1 -0
  42. package/src/badge/Badge.astro +37 -0
  43. package/src/badge/index.ts +1 -0
  44. package/src/breadcrumb/Breadcrumb.astro +32 -0
  45. package/src/breadcrumb/BreadcrumbEllipsis.astro +32 -0
  46. package/src/breadcrumb/BreadcrumbItem.astro +41 -0
  47. package/src/breadcrumb/BreadcrumbSeparator.astro +33 -0
  48. package/src/breadcrumb/index.ts +4 -0
  49. package/src/button/Button.astro +70 -0
  50. package/src/button/index.ts +1 -0
  51. package/src/card/Card.astro +17 -0
  52. package/src/card/CardContent.astro +17 -0
  53. package/src/card/CardDescription.astro +17 -0
  54. package/src/card/CardFooter.astro +17 -0
  55. package/src/card/CardHeader.astro +17 -0
  56. package/src/card/CardTitle.astro +17 -0
  57. package/src/card/index.ts +6 -0
  58. package/src/checkbox/Checkbox.astro +70 -0
  59. package/src/checkbox/index.ts +1 -0
  60. package/src/checkbox-group/CheckboxGroup.astro +38 -0
  61. package/src/checkbox-group/checkbox-group.ts +53 -0
  62. package/src/checkbox-group/index.ts +1 -0
  63. package/src/code-block/CodeBlock.astro +160 -0
  64. package/src/code-rich/CodeRich.astro +267 -0
  65. package/src/code-rich/code-rich.css +68 -0
  66. package/src/code-rich/index.ts +1 -0
  67. package/src/code-samples/CodeSamples.astro +59 -0
  68. package/src/collapsible/Collapsible.astro +35 -0
  69. package/src/collapsible/CollapsibleContent.astro +24 -0
  70. package/src/collapsible/CollapsibleTrigger.astro +23 -0
  71. package/src/collapsible/collapsible.ts +42 -0
  72. package/src/collapsible/index.ts +3 -0
  73. package/src/combobox/Combobox.astro +53 -0
  74. package/src/combobox/ComboboxEmpty.astro +18 -0
  75. package/src/combobox/ComboboxItem.astro +28 -0
  76. package/src/combobox/combobox.ts +179 -0
  77. package/src/combobox/index.ts +3 -0
  78. package/src/command/Command.astro +22 -0
  79. package/src/command/CommandEmpty.astro +18 -0
  80. package/src/command/CommandGroup.astro +24 -0
  81. package/src/command/CommandInput.astro +49 -0
  82. package/src/command/CommandItem.astro +26 -0
  83. package/src/command/CommandList.astro +19 -0
  84. package/src/command/CommandSeparator.astro +16 -0
  85. package/src/command/CommandShortcut.astro +18 -0
  86. package/src/command/command.ts +212 -0
  87. package/src/command/index.ts +8 -0
  88. package/src/content-renderer/ContentRenderer.astro +410 -0
  89. package/src/content-renderer/InlineRenderer.astro +149 -0
  90. package/src/context-menu/ContextMenu.astro +22 -0
  91. package/src/context-menu/ContextMenuContent.astro +24 -0
  92. package/src/context-menu/ContextMenuItem.astro +49 -0
  93. package/src/context-menu/ContextMenuSeparator.astro +14 -0
  94. package/src/context-menu/ContextMenuShortcut.astro +14 -0
  95. package/src/context-menu/ContextMenuTrigger.astro +22 -0
  96. package/src/context-menu/context-menu.ts +238 -0
  97. package/src/context-menu/index.ts +6 -0
  98. package/src/copy-button/CopyButton.astro +88 -0
  99. package/src/copy-button/index.ts +1 -0
  100. package/src/diagram/Diagram.astro +15 -0
  101. package/src/diagram/index.ts +1 -0
  102. package/src/dialog/Dialog.astro +22 -0
  103. package/src/dialog/DialogClose.astro +32 -0
  104. package/src/dialog/DialogContent.astro +37 -0
  105. package/src/dialog/DialogDescription.astro +22 -0
  106. package/src/dialog/DialogFooter.astro +18 -0
  107. package/src/dialog/DialogHeader.astro +18 -0
  108. package/src/dialog/DialogTitle.astro +18 -0
  109. package/src/dialog/DialogTrigger.astro +33 -0
  110. package/src/dialog/dialog.ts +291 -0
  111. package/src/dialog/index.ts +8 -0
  112. package/src/docs-footer/DocsFooter.astro +50 -0
  113. package/src/docs-header/DocsHeader.astro +32 -0
  114. package/src/docs-layout/DocsLayout.astro +187 -0
  115. package/src/docs-layout/index.ts +1 -0
  116. package/src/drawer/Drawer.astro +12 -0
  117. package/src/drawer/DrawerClose.astro +12 -0
  118. package/src/drawer/DrawerContent.astro +46 -0
  119. package/src/drawer/DrawerDescription.astro +10 -0
  120. package/src/drawer/DrawerFooter.astro +10 -0
  121. package/src/drawer/DrawerHandle.astro +16 -0
  122. package/src/drawer/DrawerHeader.astro +10 -0
  123. package/src/drawer/DrawerTitle.astro +10 -0
  124. package/src/drawer/DrawerTrigger.astro +25 -0
  125. package/src/drawer/drawer.ts +262 -0
  126. package/src/drawer/index.ts +9 -0
  127. package/src/dropdown-menu/DropdownMenu.astro +22 -0
  128. package/src/dropdown-menu/DropdownMenuContent.astro +33 -0
  129. package/src/dropdown-menu/DropdownMenuGroup.astro +17 -0
  130. package/src/dropdown-menu/DropdownMenuItem.astro +48 -0
  131. package/src/dropdown-menu/DropdownMenuLabel.astro +28 -0
  132. package/src/dropdown-menu/DropdownMenuSeparator.astro +21 -0
  133. package/src/dropdown-menu/DropdownMenuShortcut.astro +18 -0
  134. package/src/dropdown-menu/DropdownMenuTrigger.astro +34 -0
  135. package/src/dropdown-menu/dropdown-menu.ts +348 -0
  136. package/src/dropdown-menu/index.ts +8 -0
  137. package/src/endpoint-card/EndpointCard.astro +60 -0
  138. package/src/endpoint-card/EndpointCardSection.astro +22 -0
  139. package/src/endpoint-card/EndpointSection.astro +35 -0
  140. package/src/field/Field.astro +31 -0
  141. package/src/field/FieldDescription.astro +22 -0
  142. package/src/field/FieldLabel.astro +24 -0
  143. package/src/field/field.ts +61 -0
  144. package/src/field/index.ts +3 -0
  145. package/src/fieldset/Fieldset.astro +24 -0
  146. package/src/fieldset/FieldsetLegend.astro +18 -0
  147. package/src/fieldset/index.ts +2 -0
  148. package/src/footnote/Footnote.astro +37 -0
  149. package/src/footnote/footnote.ts +79 -0
  150. package/src/footnote/index.ts +1 -0
  151. package/src/form/Form.astro +27 -0
  152. package/src/form/form.ts +160 -0
  153. package/src/form/index.ts +1 -0
  154. package/src/grid/Grid.astro +117 -0
  155. package/src/grid/GridItem.astro +95 -0
  156. package/src/grid/index.ts +2 -0
  157. package/src/image/Image.astro +115 -0
  158. package/src/image/index.ts +1 -0
  159. package/src/input/Input.astro +34 -0
  160. package/src/input/index.ts +1 -0
  161. package/src/kbd/Kbd.astro +17 -0
  162. package/src/kbd/index.ts +1 -0
  163. package/src/label/Label.astro +17 -0
  164. package/src/label/index.ts +1 -0
  165. package/src/link-card/LinkCard.astro +47 -0
  166. package/src/link-card/index.ts +1 -0
  167. package/src/map-view/MapView.astro +88 -0
  168. package/src/map-view/index.ts +1 -0
  169. package/src/map-view/map-view.ts +194 -0
  170. package/src/markdown-example/MarkdownExample.astro +138 -0
  171. package/src/markdown-example/index.ts +1 -0
  172. package/src/math/MathKatex.astro +35 -0
  173. package/src/math/index.ts +1 -0
  174. package/src/menubar/Menubar.astro +25 -0
  175. package/src/menubar/MenubarContent.astro +24 -0
  176. package/src/menubar/MenubarItem.astro +26 -0
  177. package/src/menubar/MenubarMenu.astro +12 -0
  178. package/src/menubar/MenubarSeparator.astro +11 -0
  179. package/src/menubar/MenubarShortcut.astro +11 -0
  180. package/src/menubar/MenubarTrigger.astro +27 -0
  181. package/src/menubar/index.ts +7 -0
  182. package/src/menubar/menubar.ts +258 -0
  183. package/src/meter/Meter.astro +75 -0
  184. package/src/meter/index.ts +1 -0
  185. package/src/method-badge/MethodBadge.astro +32 -0
  186. package/src/navigation-menu/NavigationMenu.astro +24 -0
  187. package/src/navigation-menu/NavigationMenuContent.astro +24 -0
  188. package/src/navigation-menu/NavigationMenuItem.astro +22 -0
  189. package/src/navigation-menu/NavigationMenuLink.astro +26 -0
  190. package/src/navigation-menu/NavigationMenuList.astro +22 -0
  191. package/src/navigation-menu/NavigationMenuTrigger.astro +40 -0
  192. package/src/navigation-menu/index.ts +6 -0
  193. package/src/navigation-menu/navigation-menu.ts +192 -0
  194. package/src/number-field/NumberField.astro +84 -0
  195. package/src/number-field/index.ts +1 -0
  196. package/src/number-field/number-field.ts +68 -0
  197. package/src/pagination/Pagination.astro +59 -0
  198. package/src/pagination/index.ts +1 -0
  199. package/src/parameter-table/ParameterTable.astro +75 -0
  200. package/src/popover/Popover.astro +21 -0
  201. package/src/popover/PopoverContent.astro +31 -0
  202. package/src/popover/PopoverTrigger.astro +31 -0
  203. package/src/popover/index.ts +3 -0
  204. package/src/popover/popover.ts +144 -0
  205. package/src/preview-card/PreviewCard.astro +24 -0
  206. package/src/preview-card/PreviewCardContent.astro +24 -0
  207. package/src/preview-card/PreviewCardTrigger.astro +23 -0
  208. package/src/preview-card/index.ts +3 -0
  209. package/src/preview-card/preview-card.ts +91 -0
  210. package/src/progress/Progress.astro +61 -0
  211. package/src/progress/index.ts +1 -0
  212. package/src/property-item/PropertyItem.astro +183 -0
  213. package/src/property-list/PropertyList.astro +68 -0
  214. package/src/radio-group/RadioGroup.astro +41 -0
  215. package/src/radio-group/RadioGroupItem.astro +36 -0
  216. package/src/radio-group/index.ts +2 -0
  217. package/src/radio-group/radio-group.ts +126 -0
  218. package/src/response-tabs/ResponseTabs.astro +60 -0
  219. package/src/schema-viewer/SchemaViewer.astro +42 -0
  220. package/src/scroll-area/ScrollArea.astro +44 -0
  221. package/src/scroll-area/index.ts +1 -0
  222. package/src/scroll-area/scroll-area.ts +176 -0
  223. package/src/select/Select.astro +34 -0
  224. package/src/select/SelectContent.astro +24 -0
  225. package/src/select/SelectGroup.astro +23 -0
  226. package/src/select/SelectItem.astro +28 -0
  227. package/src/select/SelectLabel.astro +18 -0
  228. package/src/select/SelectSeparator.astro +22 -0
  229. package/src/select/SelectTrigger.astro +28 -0
  230. package/src/select/SelectValue.astro +18 -0
  231. package/src/select/index.ts +8 -0
  232. package/src/select/select.ts +381 -0
  233. package/src/separator/Separator.astro +35 -0
  234. package/src/separator/index.ts +1 -0
  235. package/src/sheet/Sheet.astro +12 -0
  236. package/src/sheet/SheetClose.astro +35 -0
  237. package/src/sheet/SheetContent.astro +52 -0
  238. package/src/sheet/SheetDescription.astro +18 -0
  239. package/src/sheet/SheetFooter.astro +18 -0
  240. package/src/sheet/SheetHeader.astro +18 -0
  241. package/src/sheet/SheetTitle.astro +18 -0
  242. package/src/sheet/SheetTrigger.astro +32 -0
  243. package/src/sheet/index.ts +8 -0
  244. package/src/sheet/sheet.ts +173 -0
  245. package/src/sidebar/Sidebar.astro +113 -0
  246. package/src/sidebar/SidebarContent.astro +18 -0
  247. package/src/sidebar/SidebarFooter.astro +18 -0
  248. package/src/sidebar/SidebarGroup.astro +18 -0
  249. package/src/sidebar/SidebarGroupContent.astro +18 -0
  250. package/src/sidebar/SidebarGroupLabel.astro +18 -0
  251. package/src/sidebar/SidebarHeader.astro +18 -0
  252. package/src/sidebar/SidebarInset.astro +18 -0
  253. package/src/sidebar/SidebarMenu.astro +18 -0
  254. package/src/sidebar/SidebarMenuBadge.astro +18 -0
  255. package/src/sidebar/SidebarMenuButton.astro +71 -0
  256. package/src/sidebar/SidebarMenuItem.astro +18 -0
  257. package/src/sidebar/SidebarMenuSub.astro +18 -0
  258. package/src/sidebar/SidebarMenuSubButton.astro +45 -0
  259. package/src/sidebar/SidebarMenuSubItem.astro +12 -0
  260. package/src/sidebar/SidebarNavTree.astro +106 -0
  261. package/src/sidebar/SidebarProvider.astro +23 -0
  262. package/src/sidebar/SidebarRail.astro +25 -0
  263. package/src/sidebar/SidebarSeparator.astro +21 -0
  264. package/src/sidebar/SidebarTrigger.astro +40 -0
  265. package/src/sidebar/index.ts +19 -0
  266. package/src/sidebar/sidebar.ts +265 -0
  267. package/src/skeleton/Skeleton.astro +16 -0
  268. package/src/skeleton/index.ts +1 -0
  269. package/src/skip-link/SkipLink.astro +24 -0
  270. package/src/slider/Slider.astro +61 -0
  271. package/src/slider/index.ts +1 -0
  272. package/src/slider/slider.ts +111 -0
  273. package/src/spinner/Spinner.astro +54 -0
  274. package/src/spinner/index.ts +1 -0
  275. package/src/steps/Step.astro +16 -0
  276. package/src/steps/Steps.astro +122 -0
  277. package/src/steps/SubSteps.astro +73 -0
  278. package/src/switch/Switch.astro +52 -0
  279. package/src/switch/index.ts +1 -0
  280. package/src/switch/switch.ts +109 -0
  281. package/src/table/Table.astro +24 -0
  282. package/src/table/TableBody.astro +18 -0
  283. package/src/table/TableCaption.astro +18 -0
  284. package/src/table/TableCell.astro +18 -0
  285. package/src/table/TableFooter.astro +18 -0
  286. package/src/table/TableHead.astro +18 -0
  287. package/src/table/TableHeader.astro +18 -0
  288. package/src/table/TableRow.astro +18 -0
  289. package/src/table/index.ts +8 -0
  290. package/src/tabs/Tabs.astro +37 -0
  291. package/src/tabs/TabsContent.astro +32 -0
  292. package/src/tabs/TabsList.astro +23 -0
  293. package/src/tabs/TabsTrigger.astro +29 -0
  294. package/src/tabs/index.ts +4 -0
  295. package/src/tabs/tabs.ts +220 -0
  296. package/src/textarea/Textarea.astro +25 -0
  297. package/src/textarea/index.ts +1 -0
  298. package/src/theme-toggle/ThemeToggle.astro +67 -0
  299. package/src/theme-toggle/index.ts +1 -0
  300. package/src/toast/Toaster.astro +39 -0
  301. package/src/toast/index.ts +3 -0
  302. package/src/toast/toast.ts +104 -0
  303. package/src/toc/TableOfContents.astro +225 -0
  304. package/src/toggle/Toggle.astro +56 -0
  305. package/src/toggle/index.ts +1 -0
  306. package/src/toggle/toggle.ts +38 -0
  307. package/src/toggle-group/ToggleGroup.astro +39 -0
  308. package/src/toggle-group/ToggleGroupItem.astro +56 -0
  309. package/src/toggle-group/index.ts +2 -0
  310. package/src/toggle-group/toggle-group.ts +121 -0
  311. package/src/toolbar/Toolbar.astro +26 -0
  312. package/src/toolbar/ToolbarButton.astro +29 -0
  313. package/src/toolbar/ToolbarSeparator.astro +14 -0
  314. package/src/toolbar/index.ts +3 -0
  315. package/src/toolbar/toolbar.ts +65 -0
  316. package/src/tooltip/Tooltip.astro +21 -0
  317. package/src/tooltip/TooltipContent.astro +25 -0
  318. package/src/tooltip/TooltipTrigger.astro +31 -0
  319. package/src/tooltip/index.ts +3 -0
  320. package/src/tooltip/tooltip.ts +156 -0
  321. package/src/youtube/YouTube.astro +119 -0
  322. package/src/youtube/index.ts +1 -0
@@ -0,0 +1,22 @@
1
+ ---
2
+ import { tv } from "tailwind-variants";
3
+
4
+ const fieldDescription = tv({
5
+ base: "text-sm text-muted-foreground",
6
+ });
7
+
8
+ interface Props {
9
+ class?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ const { class: className, ...rest } = Astro.props;
14
+ ---
15
+
16
+ <p
17
+ class={fieldDescription({ class: className })}
18
+ data-part="description"
19
+ {...rest}
20
+ >
21
+ <slot />
22
+ </p>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import { tv } from "tailwind-variants";
3
+
4
+ const fieldLabel = tv({
5
+ base: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
6
+ });
7
+
8
+ interface Props {
9
+ for?: string;
10
+ class?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ const { for: htmlFor, class: className, ...rest } = Astro.props;
15
+ ---
16
+
17
+ <label
18
+ class={fieldLabel({ class: className })}
19
+ for={htmlFor}
20
+ data-part="label"
21
+ {...rest}
22
+ >
23
+ <slot />
24
+ </label>
@@ -0,0 +1,61 @@
1
+ let idCounter = 0;
2
+ function uniqueId(prefix: string) {
3
+ return `${prefix}-${++idCounter}`;
4
+ }
5
+
6
+ function init(root: HTMLElement) {
7
+ if (root.hasAttribute("data-initialized")) return;
8
+ root.setAttribute("data-initialized", "");
9
+
10
+ const label = root.querySelector<HTMLElement>("[data-part='label']");
11
+ const description = root.querySelector<HTMLElement>("[data-part='description']");
12
+ const error = root.querySelector<HTMLElement>("[data-part='error']");
13
+
14
+ // Find the input/textarea/select inside the field
15
+ const input = root.querySelector<HTMLElement>(
16
+ "input, textarea, select, [role='radiogroup'], [role='spinbutton'], [role='slider']",
17
+ );
18
+ if (!input) return;
19
+
20
+ const fieldId = uniqueId("base-field");
21
+
22
+ // Ensure input has an ID for label association
23
+ if (!input.id) input.id = `${fieldId}-input`;
24
+
25
+ // Link label
26
+ if (label) {
27
+ label.setAttribute("for", input.id);
28
+ }
29
+
30
+ // Link description via aria-describedby
31
+ const describedBy: string[] = [];
32
+ if (description) {
33
+ const descId = description.id || `${fieldId}-desc`;
34
+ description.id = descId;
35
+ describedBy.push(descId);
36
+ }
37
+ if (error) {
38
+ const errId = error.id || `${fieldId}-error`;
39
+ error.id = errId;
40
+ describedBy.push(errId);
41
+ }
42
+ if (describedBy.length > 0) {
43
+ input.setAttribute("aria-describedby", describedBy.join(" "));
44
+ }
45
+
46
+ // Set aria-invalid if error is present
47
+ if (root.hasAttribute("data-invalid")) {
48
+ input.setAttribute("aria-invalid", "true");
49
+ }
50
+ }
51
+
52
+ function initAll() {
53
+ document.querySelectorAll<HTMLElement>("[data-component='field']").forEach(init);
54
+ }
55
+
56
+ if (document.readyState === "loading") {
57
+ document.addEventListener("DOMContentLoaded", initAll);
58
+ } else {
59
+ initAll();
60
+ }
61
+ document.addEventListener("astro:page-load", initAll);
@@ -0,0 +1,3 @@
1
+ export { default as Field } from "./Field.astro";
2
+ export { default as FieldLabel } from "./FieldLabel.astro";
3
+ export { default as FieldDescription } from "./FieldDescription.astro";
@@ -0,0 +1,24 @@
1
+ ---
2
+ import { tv } from "tailwind-variants";
3
+
4
+ const fieldset = tv({
5
+ base: "space-y-4",
6
+ });
7
+
8
+ interface Props {
9
+ disabled?: boolean;
10
+ class?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ const { disabled = false, class: className, ...rest } = Astro.props;
15
+ ---
16
+
17
+ <fieldset
18
+ class={fieldset({ class: className })}
19
+ data-component="fieldset"
20
+ disabled={disabled || undefined}
21
+ {...rest}
22
+ >
23
+ <slot />
24
+ </fieldset>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import { tv } from "tailwind-variants";
3
+
4
+ const fieldsetLegend = tv({
5
+ base: "text-sm font-medium leading-none",
6
+ });
7
+
8
+ interface Props {
9
+ class?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ const { class: className, ...rest } = Astro.props;
14
+ ---
15
+
16
+ <legend class={fieldsetLegend({ class: className })} {...rest}>
17
+ <slot />
18
+ </legend>
@@ -0,0 +1,2 @@
1
+ export { default as Fieldset } from "./Fieldset.astro";
2
+ export { default as FieldsetLegend } from "./FieldsetLegend.astro";
@@ -0,0 +1,37 @@
1
+ ---
2
+ /**
3
+ * Footnote component — click a superscript reference to show content in a popover.
4
+ *
5
+ * Usage:
6
+ * <Footnote label="1" content="<p>Footnote content</p>" />
7
+ *
8
+ * Or with a slot:
9
+ * <Footnote label="1">
10
+ * <p>Footnote content</p>
11
+ * </Footnote>
12
+ */
13
+ import Popover from "../popover/Popover.astro";
14
+ import PopoverTrigger from "../popover/PopoverTrigger.astro";
15
+ import PopoverContent from "../popover/PopoverContent.astro";
16
+
17
+ interface Props {
18
+ /** Footnote label (e.g. "1", "note") */
19
+ label: string;
20
+ /** Pre-rendered HTML content (alternative to slot) */
21
+ content?: string;
22
+ class?: string;
23
+ }
24
+
25
+ const { label, content, class: className } = Astro.props;
26
+ ---
27
+
28
+ <Popover class:list={["inline", className]}>
29
+ <PopoverTrigger asChild>
30
+ <sup class="cursor-pointer text-primary hover:underline" aria-label={`Footnote ${label}`}>
31
+ [{label}]
32
+ </sup>
33
+ </PopoverTrigger>
34
+ <PopoverContent side="top" align="center" class="max-w-sm text-sm">
35
+ {content ? <Fragment set:html={content} /> : <slot />}
36
+ </PopoverContent>
37
+ </Popover>
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Footnote runtime — hover shows tooltip, click navigates to definition.
3
+ */
4
+ import { setupFloating } from "@dogsbay/primitives/floating";
5
+
6
+ const initialized = new WeakSet<HTMLElement>();
7
+
8
+ function createPopover(content: string): HTMLElement {
9
+ const el = document.createElement("div");
10
+ el.className =
11
+ "fixed z-50 max-w-sm rounded-lg border bg-popover p-3 text-sm text-popover-foreground shadow-md";
12
+ el.innerHTML = content;
13
+ el.setAttribute("role", "tooltip");
14
+ el.hidden = true;
15
+ document.body.appendChild(el);
16
+ return el;
17
+ }
18
+
19
+ function initFootnote(trigger: HTMLElement) {
20
+ if (initialized.has(trigger)) return;
21
+ initialized.add(trigger);
22
+
23
+ const content = trigger.getAttribute("data-content") || "";
24
+ let popover: HTMLElement | null = null;
25
+ let cleanup: (() => void) | null = null;
26
+ let hideTimeout: ReturnType<typeof setTimeout> | null = null;
27
+
28
+ function show() {
29
+ if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
30
+ if (!popover) {
31
+ popover = createPopover(content);
32
+ // Keep popover open while hovering it
33
+ popover.addEventListener("mouseenter", () => {
34
+ if (hideTimeout) { clearTimeout(hideTimeout); hideTimeout = null; }
35
+ });
36
+ popover.addEventListener("mouseleave", () => {
37
+ hideTimeout = setTimeout(hide, 150);
38
+ });
39
+ }
40
+ popover.hidden = false;
41
+ cleanup = setupFloating(trigger, popover, {
42
+ placement: "top",
43
+ offset: 8,
44
+ });
45
+ }
46
+
47
+ function hide() {
48
+ if (popover) popover.hidden = true;
49
+ if (cleanup) { cleanup(); cleanup = null; }
50
+ hideTimeout = null;
51
+ }
52
+
53
+ // Hover to show tooltip
54
+ trigger.addEventListener("mouseenter", show);
55
+ trigger.addEventListener("mouseleave", () => {
56
+ hideTimeout = setTimeout(hide, 150);
57
+ });
58
+
59
+ // Focus for keyboard accessibility
60
+ trigger.addEventListener("focus", show);
61
+ trigger.addEventListener("blur", () => {
62
+ hideTimeout = setTimeout(hide, 150);
63
+ });
64
+
65
+ // Click navigates to footnote definition (default <a> behavior)
66
+ // No preventDefault — let the browser follow the href="#fn-{label}"
67
+ }
68
+
69
+ function initAll() {
70
+ document.querySelectorAll<HTMLElement>("[data-footnote]").forEach(initFootnote);
71
+ }
72
+
73
+ if (document.readyState === "loading") {
74
+ document.addEventListener("DOMContentLoaded", initAll);
75
+ } else {
76
+ initAll();
77
+ }
78
+
79
+ document.addEventListener("astro:page-load", initAll);
@@ -0,0 +1 @@
1
+ export { default as Footnote } from "./Footnote.astro";
@@ -0,0 +1,27 @@
1
+ ---
2
+ import { tv } from "tailwind-variants";
3
+
4
+ const form = tv({
5
+ base: "space-y-6",
6
+ });
7
+
8
+ interface Props {
9
+ action?: string;
10
+ method?: "GET" | "POST";
11
+ class?: string;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ const { action, method = "POST", class: className, ...rest } = Astro.props;
16
+ ---
17
+
18
+ <form
19
+ class={form({ class: className })}
20
+ data-component="form"
21
+ action={action}
22
+ method={method}
23
+ novalidate
24
+ {...rest}
25
+ >
26
+ <slot />
27
+ </form>
@@ -0,0 +1,160 @@
1
+ interface ValidationRule {
2
+ required?: boolean;
3
+ minLength?: number;
4
+ maxLength?: number;
5
+ pattern?: string;
6
+ type?: "email" | "url";
7
+ message?: string;
8
+ }
9
+
10
+ function init(root: HTMLFormElement) {
11
+ if (root.hasAttribute("data-initialized")) return;
12
+ root.setAttribute("data-initialized", "");
13
+
14
+ root.addEventListener("submit", (e) => {
15
+ const errors = validate(root);
16
+ if (errors.size > 0) {
17
+ e.preventDefault();
18
+ showErrors(root, errors);
19
+ // Focus first invalid field
20
+ const firstInvalid = root.querySelector<HTMLElement>("[aria-invalid='true']");
21
+ firstInvalid?.focus();
22
+ } else {
23
+ clearErrors(root);
24
+ }
25
+ });
26
+
27
+ // Clear field error on input
28
+ root.addEventListener("input", (e) => {
29
+ const input = e.target as HTMLInputElement;
30
+ const field = input.closest<HTMLElement>("[data-component='field']");
31
+ if (field) clearFieldError(field);
32
+ });
33
+
34
+ // Validate on blur (tab away)
35
+ root.addEventListener("focusout", (e) => {
36
+ const input = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
37
+ if (!input.name) return;
38
+ // Only validate if the field has been interacted with (has a value or is required)
39
+ if (!input.value && !input.required) return;
40
+ const errors = validateField(input);
41
+ if (errors) {
42
+ showFieldError(root, input, errors);
43
+ }
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Validate a single field. Returns error message or null.
49
+ */
50
+ function validateField(input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): string | null {
51
+ const value = input.value.trim();
52
+
53
+ if (input.required && !value) {
54
+ return `${labelFor(input) || input.name} is required.`;
55
+ }
56
+
57
+ if (!value) return null;
58
+
59
+ if (input.type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
60
+ return "Please enter a valid email address.";
61
+ }
62
+
63
+ if (input.type === "url" && !/^https?:\/\/.+/.test(value)) {
64
+ return "Please enter a valid URL.";
65
+ }
66
+
67
+ if (input.minLength > 0 && value.length < input.minLength) {
68
+ return `Must be at least ${input.minLength} characters.`;
69
+ }
70
+
71
+ if (input.maxLength > 0 && value.length > input.maxLength) {
72
+ return `Must be at most ${input.maxLength} characters.`;
73
+ }
74
+
75
+ if (input.pattern) {
76
+ const re = new RegExp(`^${input.pattern}$`);
77
+ if (!re.test(value)) {
78
+ return input.title || "Please match the required format.";
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ function validate(form: HTMLFormElement): Map<string, string> {
86
+ const errors = new Map<string, string>();
87
+ form.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(
88
+ "input, textarea, select",
89
+ ).forEach((input) => {
90
+ if (!input.name) return;
91
+ const error = validateField(input);
92
+ if (error) errors.set(input.name, error);
93
+ });
94
+ return errors;
95
+ }
96
+
97
+ function labelFor(input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement): string | null {
98
+ const field = input.closest<HTMLElement>("[data-component='field']");
99
+ const label = field?.querySelector<HTMLElement>("[data-part='label']");
100
+ return label?.textContent?.trim() || null;
101
+ }
102
+
103
+ function showFieldError(form: HTMLFormElement, input: HTMLElement, message: string) {
104
+ const field = input.closest<HTMLElement>("[data-component='field']");
105
+ if (!field) return;
106
+
107
+ input.setAttribute("aria-invalid", "true");
108
+ field.setAttribute("data-invalid", "");
109
+
110
+ let errorEl = field.querySelector<HTMLElement>("[data-part='error']");
111
+ if (!errorEl) {
112
+ errorEl = document.createElement("p");
113
+ errorEl.className = "text-sm font-medium text-destructive";
114
+ errorEl.setAttribute("data-part", "error");
115
+ errorEl.setAttribute("role", "alert");
116
+ field.appendChild(errorEl);
117
+ }
118
+ errorEl.textContent = message;
119
+
120
+ if (!errorEl.id) errorEl.id = `${input.id || (input as HTMLInputElement).name}-error`;
121
+ const existing = input.getAttribute("aria-describedby") || "";
122
+ if (!existing.includes(errorEl.id)) {
123
+ input.setAttribute("aria-describedby", `${existing} ${errorEl.id}`.trim());
124
+ }
125
+ }
126
+
127
+ function showErrors(form: HTMLFormElement, errors: Map<string, string>) {
128
+ clearErrors(form);
129
+ errors.forEach((message, name) => {
130
+ const input = form.querySelector<HTMLElement>(`[name='${name}']`);
131
+ if (input) showFieldError(form, input, message);
132
+ });
133
+ }
134
+
135
+ function clearErrors(form: HTMLFormElement) {
136
+ form.querySelectorAll<HTMLElement>("[data-component='field']").forEach(clearFieldError);
137
+ }
138
+
139
+ function clearFieldError(field: HTMLElement) {
140
+ field.removeAttribute("data-invalid");
141
+ const input = field.querySelector<HTMLElement>("input, textarea, select");
142
+ if (input) input.removeAttribute("aria-invalid");
143
+
144
+ // Remove dynamically-added error messages (keep server-rendered ones)
145
+ const errorEl = field.querySelector<HTMLElement>("[data-part='error']");
146
+ if (errorEl && !errorEl.hasAttribute("data-server")) {
147
+ errorEl.remove();
148
+ }
149
+ }
150
+
151
+ function initAll() {
152
+ document.querySelectorAll<HTMLFormElement>("[data-component='form']").forEach(init);
153
+ }
154
+
155
+ if (document.readyState === "loading") {
156
+ document.addEventListener("DOMContentLoaded", initAll);
157
+ } else {
158
+ initAll();
159
+ }
160
+ document.addEventListener("astro:page-load", initAll);
@@ -0,0 +1 @@
1
+ export { default as Form } from "./Form.astro";
@@ -0,0 +1,117 @@
1
+ ---
2
+ interface Props {
3
+ /** Number of columns. Can be responsive: [mobile, sm, md, lg] */
4
+ cols?: number | number[];
5
+ /** Gap between items in Tailwind spacing units (1 = 0.25rem). Can be responsive array. */
6
+ gap?: number | number[];
7
+ /** Column gap override. Same format as gap. */
8
+ colGap?: number | number[];
9
+ /** Row gap override. Same format as gap. */
10
+ rowGap?: number | number[];
11
+ /** Vertical alignment of items. Can be responsive. */
12
+ align?: AlignValue | AlignValue[];
13
+ class?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ type AlignValue = "start" | "center" | "end" | "stretch";
18
+
19
+ const {
20
+ cols = 1,
21
+ gap,
22
+ colGap,
23
+ rowGap,
24
+ align,
25
+ class: className,
26
+ ...rest
27
+ } = Astro.props;
28
+
29
+ // Breakpoints: sm=640px, md=768px, lg=1024px
30
+ const breakpoints = [0, 640, 768, 1024];
31
+
32
+ function toArray<T>(value: T | T[] | undefined): T[] {
33
+ if (value === undefined) return [];
34
+ return Array.isArray(value) ? value : [value];
35
+ }
36
+
37
+ function toRem(spacingUnit: number): string {
38
+ return `${spacingUnit * 0.25}rem`;
39
+ }
40
+
41
+ const colsArr = toArray(cols);
42
+ const gapArr = toArray(gap);
43
+ const colGapArr = toArray(colGap);
44
+ const rowGapArr = toArray(rowGap);
45
+ const alignArr = toArray(align);
46
+
47
+ function buildResponsiveCSS(id: string): string {
48
+ let css = "";
49
+
50
+ const baseCols = colsArr[0] || 1;
51
+ const baseGap = gapArr[0];
52
+ const baseColGap = colGapArr[0] ?? baseGap;
53
+ const baseRowGap = rowGapArr[0] ?? baseGap;
54
+ const baseAlign = alignArr[0];
55
+
56
+ css += `[data-grid-id="${id}"] {
57
+ display: grid;
58
+ grid-template-columns: repeat(${baseCols}, minmax(0, 1fr));`;
59
+
60
+ if (baseColGap !== undefined)
61
+ css += `\n column-gap: ${toRem(baseColGap)};`;
62
+ if (baseRowGap !== undefined) css += `\n row-gap: ${toRem(baseRowGap)};`;
63
+ if (baseAlign) css += `\n align-items: ${baseAlign};`;
64
+ css += "\n }\n";
65
+
66
+ const maxLen = Math.max(
67
+ colsArr.length,
68
+ gapArr.length,
69
+ colGapArr.length,
70
+ rowGapArr.length,
71
+ alignArr.length,
72
+ );
73
+
74
+ for (let i = 1; i < maxLen; i++) {
75
+ const bp = breakpoints[i] || breakpoints[breakpoints.length - 1];
76
+ const bpCols = colsArr[i];
77
+ const bpGap = gapArr[i];
78
+ const bpColGap = colGapArr[i] ?? bpGap;
79
+ const bpRowGap = rowGapArr[i] ?? bpGap;
80
+ const bpAlign = alignArr[i];
81
+
82
+ const hasChanges =
83
+ bpCols !== undefined ||
84
+ bpColGap !== undefined ||
85
+ bpRowGap !== undefined ||
86
+ bpAlign !== undefined;
87
+
88
+ if (hasChanges) {
89
+ css += ` @media (min-width: ${bp}px) {\n`;
90
+ css += ` [data-grid-id="${id}"] {`;
91
+ if (bpCols !== undefined)
92
+ css += `\n grid-template-columns: repeat(${bpCols}, minmax(0, 1fr));`;
93
+ if (bpColGap !== undefined)
94
+ css += `\n column-gap: ${toRem(bpColGap)};`;
95
+ if (bpRowGap !== undefined) css += `\n row-gap: ${toRem(bpRowGap)};`;
96
+ if (bpAlign) css += `\n align-items: ${bpAlign};`;
97
+ css += "\n }\n }\n";
98
+ }
99
+ }
100
+
101
+ return css;
102
+ }
103
+
104
+ const gridId = `g${Math.random().toString(36).slice(2, 8)}`;
105
+ const responsiveCSS = buildResponsiveCSS(gridId);
106
+ ---
107
+
108
+ <div
109
+ class:list={["base-grid", className]}
110
+ data-grid-id={gridId}
111
+ data-cols={Array.isArray(cols) ? cols.join(",") : cols}
112
+ {...rest}
113
+ >
114
+ <slot />
115
+ </div>
116
+
117
+ <style set:html={responsiveCSS}></style>
@@ -0,0 +1,95 @@
1
+ ---
2
+ interface Props {
3
+ /** Number of columns this item spans. Can be responsive: [mobile, sm, md, lg] */
4
+ span?: number | number[];
5
+ /** Number of rows this item spans. */
6
+ rowSpan?: number;
7
+ /** Number of columns to skip (offset). Can be responsive. */
8
+ skip?: number | number[];
9
+ /** CSS order for reordering items. Can be responsive. */
10
+ order?: number | number[];
11
+ class?: string;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ const { span, rowSpan, skip, order, class: className, ...rest } = Astro.props;
16
+
17
+ // Breakpoints match Grid.astro
18
+ const breakpoints = [0, 640, 768, 1024];
19
+
20
+ function toArray(value: number | number[] | undefined): number[] {
21
+ if (value === undefined) return [];
22
+ return Array.isArray(value) ? value : [value];
23
+ }
24
+
25
+ const spanArr = toArray(span);
26
+ const skipArr = toArray(skip);
27
+ const orderArr = toArray(order);
28
+
29
+ const needsResponsiveCSS =
30
+ spanArr.length > 1 || skipArr.length > 1 || orderArr.length > 1;
31
+
32
+ // For non-responsive, use inline styles (simpler)
33
+ const inlineStyle: Record<string, string> = {};
34
+ if (spanArr.length === 1)
35
+ inlineStyle["grid-column"] = `span ${spanArr[0]} / span ${spanArr[0]}`;
36
+ if (rowSpan !== undefined && rowSpan > 1)
37
+ inlineStyle["grid-row"] = `span ${rowSpan} / span ${rowSpan}`;
38
+ if (skipArr.length === 1)
39
+ inlineStyle["grid-column-start"] = String(skipArr[0] + 1);
40
+ if (orderArr.length === 1) inlineStyle["order"] = String(orderArr[0]);
41
+
42
+ // For responsive, generate scoped CSS
43
+ let responsiveCSS = "";
44
+ let itemId = "";
45
+
46
+ if (needsResponsiveCSS) {
47
+ itemId = `gi${Math.random().toString(36).slice(2, 8)}`;
48
+
49
+ const baseSpan = spanArr[0];
50
+ const baseSkip = skipArr[0];
51
+ const baseOrder = orderArr[0];
52
+
53
+ responsiveCSS += `[data-grid-item-id="${itemId}"] {`;
54
+ if (baseSpan)
55
+ responsiveCSS += `\n grid-column: span ${baseSpan} / span ${baseSpan};`;
56
+ if (baseSkip) responsiveCSS += `\n grid-column-start: ${baseSkip + 1};`;
57
+ if (baseOrder !== undefined) responsiveCSS += `\n order: ${baseOrder};`;
58
+ responsiveCSS += "\n }\n";
59
+
60
+ const maxLen = Math.max(spanArr.length, skipArr.length, orderArr.length);
61
+ for (let i = 1; i < maxLen; i++) {
62
+ const bp = breakpoints[i] || breakpoints[breakpoints.length - 1];
63
+ const bpSpan = spanArr[i];
64
+ const bpSkip = skipArr[i];
65
+ const bpOrder = orderArr[i];
66
+
67
+ if (bpSpan !== undefined || bpSkip !== undefined || bpOrder !== undefined) {
68
+ responsiveCSS += ` @media (min-width: ${bp}px) {\n`;
69
+ responsiveCSS += ` [data-grid-item-id="${itemId}"] {`;
70
+ if (bpSpan !== undefined)
71
+ responsiveCSS += `\n grid-column: span ${bpSpan} / span ${bpSpan};`;
72
+ if (bpSkip !== undefined)
73
+ responsiveCSS += `\n grid-column-start: ${bpSkip + 1};`;
74
+ if (bpOrder !== undefined) responsiveCSS += `\n order: ${bpOrder};`;
75
+ responsiveCSS += "\n }\n }\n";
76
+ }
77
+ }
78
+ }
79
+
80
+ const styleStr =
81
+ Object.entries(inlineStyle)
82
+ .map(([k, v]) => `${k}: ${v}`)
83
+ .join("; ") || undefined;
84
+ ---
85
+
86
+ <div
87
+ class:list={["base-grid-item", className]}
88
+ style={!needsResponsiveCSS ? styleStr : undefined}
89
+ data-grid-item-id={needsResponsiveCSS ? itemId : undefined}
90
+ {...rest}
91
+ >
92
+ <slot />
93
+ </div>
94
+
95
+ {needsResponsiveCSS && <style set:html={responsiveCSS} />}
@@ -0,0 +1,2 @@
1
+ export { default as Grid } from "./Grid.astro";
2
+ export { default as GridItem } from "./GridItem.astro";