@rimelight/ui 0.0.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 (301) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/i18n-data.json +186 -0
  4. package/package.json +49 -0
  5. package/src/components/FormattedDate.astro +22 -0
  6. package/src/components/HeaderLink.astro +24 -0
  7. package/src/components/HttpObservatory.astro +103 -0
  8. package/src/components/LighthouseScores.astro +204 -0
  9. package/src/components/MissingTranslationBanner.astro +39 -0
  10. package/src/components/TableOfContents.astro +106 -0
  11. package/src/components/ThemeToggle.astro +71 -0
  12. package/src/components/accordion/Accordion.astro +257 -0
  13. package/src/components/accordion/AccordionContent.astro +34 -0
  14. package/src/components/accordion/AccordionItem.astro +29 -0
  15. package/src/components/accordion/AccordionTrigger.astro +33 -0
  16. package/src/components/alert/Alert.astro +34 -0
  17. package/src/components/alert/AlertDescription.astro +15 -0
  18. package/src/components/alert/AlertTitle.astro +17 -0
  19. package/src/components/alert-dialog/AlertDialog.astro +275 -0
  20. package/src/components/alert-dialog/AlertDialogAction.astro +46 -0
  21. package/src/components/alert-dialog/AlertDialogCancel.astro +46 -0
  22. package/src/components/alert-dialog/AlertDialogContent.astro +52 -0
  23. package/src/components/alert-dialog/AlertDialogDescription.astro +18 -0
  24. package/src/components/alert-dialog/AlertDialogFooter.astro +16 -0
  25. package/src/components/alert-dialog/AlertDialogHeader.astro +14 -0
  26. package/src/components/alert-dialog/AlertDialogTitle.astro +20 -0
  27. package/src/components/alert-dialog/AlertDialogTrigger.astro +47 -0
  28. package/src/components/aspect-ratio/AspectRatio.astro +32 -0
  29. package/src/components/avatar/Avatar.astro +29 -0
  30. package/src/components/avatar/AvatarFallback.astro +18 -0
  31. package/src/components/avatar/AvatarImage.astro +66 -0
  32. package/src/components/badge/Badge.astro +57 -0
  33. package/src/components/breadcrumb/Breadcrumb.astro +12 -0
  34. package/src/components/breadcrumb/BreadcrumbEllipsis.astro +29 -0
  35. package/src/components/breadcrumb/BreadcrumbItem.astro +15 -0
  36. package/src/components/breadcrumb/BreadcrumbLink.astro +24 -0
  37. package/src/components/breadcrumb/BreadcrumbList.astro +17 -0
  38. package/src/components/breadcrumb/BreadcrumbPage.astro +22 -0
  39. package/src/components/breadcrumb/BreadcrumbSeparator.astro +24 -0
  40. package/src/components/button/Button.astro +58 -0
  41. package/src/components/button-group/ButtonGroup.astro +64 -0
  42. package/src/components/button-group/ButtonGroupSeparator.astro +20 -0
  43. package/src/components/button-group/ButtonGroupText.astro +19 -0
  44. package/src/components/card/Card.astro +36 -0
  45. package/src/components/card/CardAction.astro +16 -0
  46. package/src/components/card/CardContent.astro +16 -0
  47. package/src/components/card/CardDescription.astro +16 -0
  48. package/src/components/card/CardFooter.astro +16 -0
  49. package/src/components/card/CardHeader.astro +19 -0
  50. package/src/components/card/CardTitle.astro +16 -0
  51. package/src/components/carousel/Carousel.astro +55 -0
  52. package/src/components/carousel/CarouselContent.astro +26 -0
  53. package/src/components/carousel/CarouselItem.astro +26 -0
  54. package/src/components/carousel/CarouselNext.astro +39 -0
  55. package/src/components/carousel/CarouselPrevious.astro +41 -0
  56. package/src/components/carousel/carousel-script.ts +188 -0
  57. package/src/components/carousel/index.ts +1 -0
  58. package/src/components/checkbox/Checkbox.astro +133 -0
  59. package/src/components/collapsible/Collapsible.astro +161 -0
  60. package/src/components/collapsible/CollapsibleContent.astro +22 -0
  61. package/src/components/collapsible/CollapsibleTrigger.astro +44 -0
  62. package/src/components/dialog/Dialog.astro +389 -0
  63. package/src/components/dialog/DialogClose.astro +35 -0
  64. package/src/components/dialog/DialogContent.astro +78 -0
  65. package/src/components/dialog/DialogDescription.astro +14 -0
  66. package/src/components/dialog/DialogFooter.astro +14 -0
  67. package/src/components/dialog/DialogHeader.astro +14 -0
  68. package/src/components/dialog/DialogTitle.astro +18 -0
  69. package/src/components/dialog/DialogTrigger.astro +47 -0
  70. package/src/components/dropdown/Dropdown.astro +604 -0
  71. package/src/components/dropdown/DropdownContent.astro +118 -0
  72. package/src/components/dropdown/DropdownItem.astro +48 -0
  73. package/src/components/dropdown/DropdownLabel.astro +29 -0
  74. package/src/components/dropdown/DropdownSeparator.astro +21 -0
  75. package/src/components/dropdown/DropdownShortcut.astro +17 -0
  76. package/src/components/dropdown/DropdownSub.astro +15 -0
  77. package/src/components/dropdown/DropdownSubContent.astro +39 -0
  78. package/src/components/dropdown/DropdownSubTrigger.astro +34 -0
  79. package/src/components/dropdown/DropdownTrigger.astro +52 -0
  80. package/src/components/dropzone/Dropzone.astro +236 -0
  81. package/src/components/dropzone/DropzoneFilesList.astro +26 -0
  82. package/src/components/dropzone/DropzoneLoadingIndicator.astro +10 -0
  83. package/src/components/dropzone/DropzoneUploadIndicator.astro +10 -0
  84. package/src/components/embeds/YoutubeEmbed.astro +94 -0
  85. package/src/components/hero/Hero.astro +166 -0
  86. package/src/components/icons/Flag.astro +62 -0
  87. package/src/components/image/ExtendedImage.astro +46 -0
  88. package/src/components/image/Image.astro +25 -0
  89. package/src/components/input/Input.astro +27 -0
  90. package/src/components/input-group/InputGroup.astro +28 -0
  91. package/src/components/input-group/InputGroupAddon.astro +76 -0
  92. package/src/components/input-group/InputGroupButton.astro +36 -0
  93. package/src/components/input-group/InputGroupInput.astro +18 -0
  94. package/src/components/input-group/InputGroupText.astro +17 -0
  95. package/src/components/input-group/InputGroupTextarea.astro +22 -0
  96. package/src/components/input-otp/InputOtp.astro +352 -0
  97. package/src/components/input-otp/InputOtpGroup.astro +16 -0
  98. package/src/components/input-otp/InputOtpSeparator.astro +25 -0
  99. package/src/components/input-otp/InputOtpSlot.astro +50 -0
  100. package/src/components/input-otp/InputOtpTypes.ts +6 -0
  101. package/src/components/item/Item.astro +52 -0
  102. package/src/components/item/ItemActions.astro +16 -0
  103. package/src/components/item/ItemContent.astro +16 -0
  104. package/src/components/item/ItemDescription.astro +19 -0
  105. package/src/components/item/ItemFooter.astro +16 -0
  106. package/src/components/item/ItemGroup.astro +16 -0
  107. package/src/components/item/ItemHeader.astro +16 -0
  108. package/src/components/item/ItemMedia.astro +40 -0
  109. package/src/components/item/ItemSeparator.astro +23 -0
  110. package/src/components/item/ItemTitle.astro +16 -0
  111. package/src/components/kbd/Kbd.astro +21 -0
  112. package/src/components/kbd/KbdGroup.astro +16 -0
  113. package/src/components/label/Label.astro +24 -0
  114. package/src/components/marquee/Marquee.astro +73 -0
  115. package/src/components/native-select/NativeSelect.astro +68 -0
  116. package/src/components/native-select/NativeSelectOptGroup.astro +15 -0
  117. package/src/components/native-select/NativeSelectOption.astro +15 -0
  118. package/src/components/pagination/Pagination.astro +20 -0
  119. package/src/components/pagination/PaginationContent.astro +16 -0
  120. package/src/components/pagination/PaginationEllipsis.astro +37 -0
  121. package/src/components/pagination/PaginationItem.astro +16 -0
  122. package/src/components/pagination/PaginationLink.astro +27 -0
  123. package/src/components/pagination/PaginationNext.astro +33 -0
  124. package/src/components/pagination/PaginationPrevious.astro +33 -0
  125. package/src/components/popover/Popover.astro +717 -0
  126. package/src/components/popover/PopoverContent.astro +102 -0
  127. package/src/components/popover/PopoverDescription.astro +14 -0
  128. package/src/components/popover/PopoverHeader.astro +14 -0
  129. package/src/components/popover/PopoverTitle.astro +14 -0
  130. package/src/components/popover/PopoverTrigger.astro +51 -0
  131. package/src/components/progress/Progress.astro +154 -0
  132. package/src/components/prose/Prose.astro +619 -0
  133. package/src/components/radio-group/RadioGroup.astro +162 -0
  134. package/src/components/radio-group/RadioGroupItem.astro +133 -0
  135. package/src/components/radio-group/RadioGroupTypes.ts +6 -0
  136. package/src/components/section/Section.astro +167 -0
  137. package/src/components/select/Select.astro +752 -0
  138. package/src/components/select/SelectContent.astro +94 -0
  139. package/src/components/select/SelectGroup.astro +9 -0
  140. package/src/components/select/SelectItem.astro +51 -0
  141. package/src/components/select/SelectLabel.astro +14 -0
  142. package/src/components/select/SelectSearch.astro +49 -0
  143. package/src/components/select/SelectSeparator.astro +12 -0
  144. package/src/components/select/SelectTrigger.astro +56 -0
  145. package/src/components/select/SelectTypes.ts +13 -0
  146. package/src/components/select/SelectValue.astro +19 -0
  147. package/src/components/separator/Separator.astro +36 -0
  148. package/src/components/sheet/Sheet.astro +15 -0
  149. package/src/components/sheet/SheetClose.astro +15 -0
  150. package/src/components/sheet/SheetContent.astro +89 -0
  151. package/src/components/sheet/SheetDescription.astro +16 -0
  152. package/src/components/sheet/SheetFooter.astro +16 -0
  153. package/src/components/sheet/SheetHeader.astro +16 -0
  154. package/src/components/sheet/SheetTitle.astro +16 -0
  155. package/src/components/sheet/SheetTrigger.astro +15 -0
  156. package/src/components/sidebar/Sidebar.astro +231 -0
  157. package/src/components/sidebar/SidebarContent.astro +25 -0
  158. package/src/components/sidebar/SidebarFooter.astro +22 -0
  159. package/src/components/sidebar/SidebarGroup.astro +22 -0
  160. package/src/components/sidebar/SidebarGroupContent.astro +22 -0
  161. package/src/components/sidebar/SidebarGroupLabel.astro +53 -0
  162. package/src/components/sidebar/SidebarHeader.astro +22 -0
  163. package/src/components/sidebar/SidebarInput.astro +24 -0
  164. package/src/components/sidebar/SidebarInset.astro +22 -0
  165. package/src/components/sidebar/SidebarMenu.astro +22 -0
  166. package/src/components/sidebar/SidebarMenuAction.astro +63 -0
  167. package/src/components/sidebar/SidebarMenuBadge.astro +31 -0
  168. package/src/components/sidebar/SidebarMenuButton.astro +124 -0
  169. package/src/components/sidebar/SidebarMenuItem.astro +22 -0
  170. package/src/components/sidebar/SidebarMenuSkeleton.astro +43 -0
  171. package/src/components/sidebar/SidebarMenuSub.astro +25 -0
  172. package/src/components/sidebar/SidebarMenuSubButton.astro +52 -0
  173. package/src/components/sidebar/SidebarMenuSubItem.astro +17 -0
  174. package/src/components/sidebar/SidebarProvider.astro +215 -0
  175. package/src/components/sidebar/SidebarRail.astro +72 -0
  176. package/src/components/sidebar/SidebarSeparator.astro +25 -0
  177. package/src/components/sidebar/SidebarTrigger.astro +69 -0
  178. package/src/components/skeleton/Skeleton.astro +14 -0
  179. package/src/components/slider/Slider.astro +425 -0
  180. package/src/components/spinner/Spinner.astro +23 -0
  181. package/src/components/switch/Switch.astro +192 -0
  182. package/src/components/switch/SwitchTypes.ts +6 -0
  183. package/src/components/table/Table.astro +20 -0
  184. package/src/components/table/TableBody.astro +18 -0
  185. package/src/components/table/TableCaption.astro +18 -0
  186. package/src/components/table/TableCell.astro +18 -0
  187. package/src/components/table/TableFoot.astro +18 -0
  188. package/src/components/table/TableHead.astro +18 -0
  189. package/src/components/table/TableHeader.astro +18 -0
  190. package/src/components/table/TableRow.astro +17 -0
  191. package/src/components/tabs/Tabs.astro +270 -0
  192. package/src/components/tabs/TabsContent.astro +28 -0
  193. package/src/components/tabs/TabsList.astro +23 -0
  194. package/src/components/tabs/TabsTrigger.astro +34 -0
  195. package/src/components/textarea/Textarea.astro +31 -0
  196. package/src/components/toast/ToastDescription.astro +22 -0
  197. package/src/components/toast/ToastItem.astro +56 -0
  198. package/src/components/toast/ToastTemplate.astro +27 -0
  199. package/src/components/toast/ToastTitle.astro +59 -0
  200. package/src/components/toast/Toaster.astro +981 -0
  201. package/src/components/toast/index.ts +1 -0
  202. package/src/components/toast/toast-manager.ts +234 -0
  203. package/src/components/toggle/Toggle.astro +174 -0
  204. package/src/components/toggle/ToggleTypes.ts +14 -0
  205. package/src/components/tooltip/Tooltip.astro +422 -0
  206. package/src/components/tooltip/TooltipContent.astro +83 -0
  207. package/src/components/tooltip/TooltipTrigger.astro +13 -0
  208. package/src/components/video/Video.astro +121 -0
  209. package/src/config/cookies.ts +5 -0
  210. package/src/config/index.ts +3 -0
  211. package/src/config/links.ts +4 -0
  212. package/src/config/site.config.ts +45 -0
  213. package/src/domain/cms/index.ts +1 -0
  214. package/src/domain/cms/types.ts +20 -0
  215. package/src/domain/i18n/constants.ts +21 -0
  216. package/src/domain/i18n/cookie-storage.ts +13 -0
  217. package/src/domain/i18n/country-to-locale-map.ts +67 -0
  218. package/src/domain/i18n/fallbacks.generated.ts +230 -0
  219. package/src/domain/i18n/fetcher.ts +268 -0
  220. package/src/domain/i18n/flags.ts +16 -0
  221. package/src/domain/i18n/format.ts +90 -0
  222. package/src/domain/i18n/index.ts +10 -0
  223. package/src/domain/i18n/middleware/i18n.ts +160 -0
  224. package/src/domain/i18n/resolve-locale.ts +55 -0
  225. package/src/domain/i18n/runtime-constants.ts +14 -0
  226. package/src/domain/i18n/schema.ts +29 -0
  227. package/src/domain/index.ts +4 -0
  228. package/src/domain/seo/constants.ts +20 -0
  229. package/src/domain/seo/index.ts +3 -0
  230. package/src/domain/seo/services/index.ts +3 -0
  231. package/src/domain/seo/services/llms.ts +53 -0
  232. package/src/domain/seo/services/robots.ts +12 -0
  233. package/src/domain/seo/services/sitemap.ts +56 -0
  234. package/src/domain/seo/utils.ts +37 -0
  235. package/src/domain/theme/constants.ts +6 -0
  236. package/src/domain/theme/index.ts +2 -0
  237. package/src/domain/theme/schema.ts +10 -0
  238. package/src/env.d.ts +49 -0
  239. package/src/i18n.generated.d.ts +202 -0
  240. package/src/lib/component-preview.ts +73 -0
  241. package/src/lib/index.ts +6 -0
  242. package/src/lib/remark/remark-modified-time.ts +13 -0
  243. package/src/lib/showcase-preview-elements.ts +408 -0
  244. package/src/lib/showcase-renderer.ts +218 -0
  245. package/src/lib/showcase-utils.ts +189 -0
  246. package/src/lib/turnstile.ts +35 -0
  247. package/src/lib/utils/starwind/positioning.ts +318 -0
  248. package/src/locales/de/blog.json +9 -0
  249. package/src/locales/de/common.json +19 -0
  250. package/src/locales/de/errors.json +6 -0
  251. package/src/locales/de/home.json +20 -0
  252. package/src/locales/de/messages.json +8 -0
  253. package/src/locales/de/nav.json +13 -0
  254. package/src/locales/de/projects.json +9 -0
  255. package/src/locales/de/services.json +43 -0
  256. package/src/locales/en/blog.json +9 -0
  257. package/src/locales/en/chat.json +29 -0
  258. package/src/locales/en/common.json +19 -0
  259. package/src/locales/en/cookie_notice.json +11 -0
  260. package/src/locales/en/editor.json +24 -0
  261. package/src/locales/en/errors.json +6 -0
  262. package/src/locales/en/home.json +20 -0
  263. package/src/locales/en/messages.json +8 -0
  264. package/src/locales/en/nav.json +13 -0
  265. package/src/locales/en/projects.json +9 -0
  266. package/src/locales/en/services.json +43 -0
  267. package/src/locales/es/blog.json +9 -0
  268. package/src/locales/es/common.json +19 -0
  269. package/src/locales/es/errors.json +6 -0
  270. package/src/locales/es/home.json +20 -0
  271. package/src/locales/es/messages.json +8 -0
  272. package/src/locales/es/nav.json +13 -0
  273. package/src/locales/es/projects.json +9 -0
  274. package/src/locales/es/services.json +43 -0
  275. package/src/locales/ja/blog.json +9 -0
  276. package/src/locales/ja/common.json +19 -0
  277. package/src/locales/ja/errors.json +6 -0
  278. package/src/locales/ja/home.json +20 -0
  279. package/src/locales/ja/messages.json +8 -0
  280. package/src/locales/ja/nav.json +13 -0
  281. package/src/locales/ja/projects.json +9 -0
  282. package/src/locales/ja/services.json +43 -0
  283. package/src/locales/pt/blog.json +9 -0
  284. package/src/locales/pt/chat.json +29 -0
  285. package/src/locales/pt/common.json +19 -0
  286. package/src/locales/pt/cookie_notice.json +11 -0
  287. package/src/locales/pt/editor.json +24 -0
  288. package/src/locales/pt/errors.json +6 -0
  289. package/src/locales/pt/home.json +20 -0
  290. package/src/locales/pt/messages.json +8 -0
  291. package/src/locales/pt/nav.json +13 -0
  292. package/src/locales/pt/projects.json +9 -0
  293. package/src/locales/pt/services.json +43 -0
  294. package/src/utils/index.ts +2 -0
  295. package/src/utils/server/cookies.ts +98 -0
  296. package/src/utils/server/index.ts +1 -0
  297. package/src/utils/shared/deep-merge.ts +36 -0
  298. package/src/utils/shared/embed.ts +105 -0
  299. package/src/utils/shared/index.ts +3 -0
  300. package/src/utils/shared/parse-boolean.ts +14 -0
  301. package/worker-configuration.d.ts +14791 -0
@@ -0,0 +1,204 @@
1
+ ---
2
+ /**
3
+ * LighthouseScores.astro
4
+ * A callout component displaying perfect Lighthouse scores
5
+ * Designed to match the authentic Lighthouse report aesthetic
6
+ */
7
+ export interface LighthouseScore {
8
+ label: string;
9
+ value: number;
10
+ }
11
+
12
+ export interface LighthouseScoresProps {
13
+ performance?: number;
14
+ accessibility?: number;
15
+ bestPractices?: number;
16
+ seo?: number;
17
+ }
18
+
19
+ const {
20
+ performance = 100,
21
+ accessibility = 100,
22
+ bestPractices = 100,
23
+ seo = 100,
24
+ } = Astro.props;
25
+
26
+ const scores = [
27
+ { label: 'Performance', value: performance },
28
+ { label: 'Accessibility', value: accessibility },
29
+ { label: 'Best Practices', value: bestPractices },
30
+ { label: 'SEO', value: seo },
31
+ ];
32
+ ---
33
+
34
+ <div class="lighthouse-container">
35
+ <!-- Header -->
36
+ <div class="mb-4xl text-center">
37
+ <h3 class="font-heading text-foreground text-3xl font-bold md:text-4xl">
38
+ Lighthouse Report
39
+ </h3>
40
+ </div>
41
+
42
+ <!-- Scores Grid -->
43
+ <div
44
+ class="lighthouse-scores mx-auto grid max-w-2xl grid-cols-2 gap-4xl sm:grid-cols-4"
45
+ role="list"
46
+ aria-label="Lighthouse scores"
47
+ >
48
+ {
49
+ scores.map((score) => {
50
+ const radius = 54;
51
+ const circumference = 2 * Math.PI * radius;
52
+ const offset = circumference - (score.value / 100) * circumference;
53
+ const color = score.value >= 90 ? "#0cce6b" : score.value >= 50 ? "#ffa400" : "#ff4e42";
54
+
55
+ return (
56
+ <div
57
+ class="lighthouse-score group flex flex-col items-center"
58
+ role="listitem"
59
+ >
60
+ <div class="relative">
61
+ <svg
62
+ class="lighthouse-gauge"
63
+ width="120"
64
+ height="120"
65
+ viewBox="0 0 120 120"
66
+ aria-hidden="true"
67
+ style="width: 96px; height: 96px;"
68
+ >
69
+ <style>
70
+ @media (min-width: 640px) {
71
+ .lighthouse-gauge {
72
+ width: 112px !important;
73
+ height: 112px !important;
74
+ }
75
+ }
76
+ @media (min-width: 768px) {
77
+ .lighthouse-gauge {
78
+ width: 144px !important;
79
+ height: 144px !important;
80
+ }
81
+ }
82
+ </style>
83
+ <circle
84
+ cx="60"
85
+ cy="60"
86
+ r={radius}
87
+ fill="none"
88
+ stroke="#e0e0e0"
89
+ stroke-width="8"
90
+ />
91
+ <circle
92
+ cx="60"
93
+ cy="60"
94
+ r={radius}
95
+ fill="none"
96
+ stroke={color}
97
+ stroke-width="8"
98
+ stroke-linecap="round"
99
+ stroke-dasharray={circumference}
100
+ stroke-dashoffset={offset}
101
+ class="lighthouse-progress"
102
+ transform="rotate(-90 60 60)"
103
+ style={`--target-offset: ${offset}`}
104
+ />
105
+ <text
106
+ x="60"
107
+ y="60"
108
+ text-anchor="middle"
109
+ dominant-baseline="central"
110
+ class="lighthouse-number font-semibold font-sans"
111
+ fill={color}
112
+ style="font-size: 2rem;"
113
+ >
114
+ {score.value}
115
+ </text>
116
+ </svg>
117
+ </div>
118
+
119
+ <span class="text-muted-foreground mt-2xl text-center text-sm font-medium">
120
+ {score.label}
121
+ </span>
122
+ </div>
123
+ );
124
+ })
125
+ }
126
+ </div>
127
+
128
+ <!-- Footer note -->
129
+ <p class="text-muted-foreground/60 mt-4xl text-center text-xs italic">
130
+ *Results represent the starting template. Changes to styling and features may affect final results.
131
+ </p>
132
+ </div>
133
+
134
+ <style>
135
+ /* Entrance animations */
136
+ .lighthouse-score {
137
+ animation: score-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
138
+ }
139
+
140
+ .lighthouse-progress {
141
+ animation: progress-fill 1.2s cubic-bezier(0.16, 1, 0.3, 1) both;
142
+ }
143
+
144
+ /* Staggered delays via nth-child to avoid inline style attributes */
145
+ .lighthouse-score:nth-child(1) { animation-delay: 100ms; }
146
+ .lighthouse-score:nth-child(2) { animation-delay: 180ms; }
147
+ .lighthouse-score:nth-child(3) { animation-delay: 260ms; }
148
+ .lighthouse-score:nth-child(4) { animation-delay: 340ms; }
149
+
150
+ .lighthouse-progress:nth-child(1) { animation-delay: 200ms; }
151
+ .lighthouse-progress:nth-child(2) { animation-delay: 280ms; }
152
+ .lighthouse-progress:nth-child(3) { animation-delay: 360ms; }
153
+ .lighthouse-progress:nth-child(4) { animation-delay: 440ms; }
154
+
155
+ @keyframes score-enter {
156
+ from {
157
+ opacity: 0;
158
+ transform: scale(0.9) translateY(10px);
159
+ }
160
+ to {
161
+ opacity: 1;
162
+ transform: scale(1) translateY(0);
163
+ }
164
+ }
165
+
166
+ @keyframes progress-fill {
167
+ from {
168
+ stroke-dashoffset: 339.292; /* Approximate circumference */
169
+ }
170
+ to {
171
+ stroke-dashoffset: var(--target-offset, 0);
172
+ }
173
+ }
174
+
175
+ @media (prefers-reduced-motion: reduce) {
176
+ .lighthouse-score,
177
+ .lighthouse-progress {
178
+ animation: none;
179
+ }
180
+
181
+ .lighthouse-progress {
182
+ stroke-dashoffset: 0;
183
+ }
184
+ }
185
+ </style>
186
+
187
+ <script>
188
+ // Run animations immediately
189
+ const initLighthouse = () => {
190
+ const scoresContainer = document.querySelector('.lighthouse-scores');
191
+ if (scoresContainer) {
192
+ const elements = scoresContainer.querySelectorAll<HTMLElement>('.lighthouse-score, .lighthouse-progress');
193
+ elements.forEach((el) => {
194
+ el.style.animationPlayState = 'running';
195
+ });
196
+ }
197
+ };
198
+
199
+ // Run on initial load
200
+ initLighthouse();
201
+
202
+ // Run on View Transitions navigate
203
+ document.addEventListener('astro:after-swap', initLighthouse);
204
+ </script>
@@ -0,0 +1,39 @@
1
+ ---
2
+ import { checkMissingTranslation } from '../domain/i18n/resolve-locale'
3
+ import { fetchTranslations } from '../domain/i18n/fetcher'
4
+
5
+ export interface MissingTranslationBannerProps {}
6
+ type Props = MissingTranslationBannerProps;
7
+
8
+ const missingType = checkMissingTranslation(
9
+ Astro.locals.uiLocale,
10
+ Astro.locals.isMissingContent
11
+ )
12
+
13
+ let bannerText: string | null = null
14
+
15
+ if (missingType) {
16
+ const { cfContext, translationLocale } = Astro.locals
17
+ const runtime = { cf: Astro.request.cf, cfContext }
18
+ const { messages } = await fetchTranslations(
19
+ runtime,
20
+ translationLocale,
21
+ ['messages']
22
+ )
23
+
24
+ bannerText =
25
+ missingType === 'content'
26
+ ? messages.errors.ui.MISSING_TRANSLATED_CONTENT ||
27
+ 'Sorry, this article is not yet available in your selected language.'
28
+ : messages.errors.ui.MISSING_TRANSLATED_UI ||
29
+ 'Sorry, this page is not yet fully available in your selected language.'
30
+ }
31
+ ---
32
+
33
+ {
34
+ bannerText && (
35
+ <div class='rounded px-4 py-2 mb-8 max-w-160 mx-auto border border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-600 text-center'>
36
+ {bannerText}
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,106 @@
1
+ ---
2
+ export interface TableOfContentsHeading {
3
+ depth: number; // The heading level (h1 = 1, h2 = 2, etc)
4
+ slug: string; // The ID of the heading for linking
5
+ text: string; // The text content of the heading
6
+ }
7
+
8
+ export interface TableOfContentsProps {
9
+ headings: TableOfContentsHeading[]; // Array of headings from your markdown content
10
+ levels?: 1 | 2 | 3; // How many levels of headers to show in TOC (default: 2)
11
+ }
12
+
13
+ type Props = TableOfContentsProps;
14
+
15
+ const { headings, levels = 2 } = Astro.props as Props;
16
+
17
+ // Filter headings to only show up to the specified level
18
+ const filteredHeadings = headings.filter((heading) => heading.depth <= levels);
19
+ ---
20
+
21
+ <div class="border-base-400 rounded-xs border p-3xs text-sm leading-tight">
22
+ <h4 class="text-xl font-medium">Table of Contents</h4>
23
+ <ul class="mt-2xl flex flex-col gap-3xs">
24
+ {
25
+ filteredHeadings.map((heading) => (
26
+ <li
27
+ class:list={{
28
+ "pl-3xs": heading.depth === 2,
29
+ "pl-6": heading.depth === 3,
30
+ }}
31
+ >
32
+ <a
33
+ href={`#${heading.slug}`}
34
+ class="toc-link transition hover:text-primary-accent"
35
+ >
36
+ {heading.text}
37
+ </a>
38
+ </li>
39
+ ))
40
+ }
41
+ </ul>
42
+ </div>
43
+
44
+ <style>
45
+ .toc-current {
46
+ color: var(--primary);
47
+ font-weight: 500;
48
+ }
49
+ </style>
50
+
51
+ <script>
52
+ // Wrapper for Blog post content
53
+ let wrappingElement: Element | null;
54
+ let observeHeaderTags: IntersectionObserver;
55
+ let allHeaderTags: NodeListOf<Element>;
56
+
57
+ // Function that runs when the Intersection Observer fires
58
+ function setCurrent(e: IntersectionObserverEntry[]) {
59
+ var allSectionLinks = document.querySelectorAll(".toc-link");
60
+ e.map((i) => {
61
+ if (i.isIntersecting === true) {
62
+ allSectionLinks.forEach((link) => link.classList.remove("toc-current"));
63
+ const targetLink = document.querySelector(
64
+ `a[href="#${i.target.id}"].toc-link`,
65
+ );
66
+
67
+ if (targetLink) targetLink.classList.add("toc-current");
68
+ }
69
+ });
70
+ }
71
+
72
+ function initTOC() {
73
+ // update this with whatever class wraps your blog post content
74
+ wrappingElement = document.querySelector(".markdown-content");
75
+ if (wrappingElement !== null) {
76
+ // Get all H1/H2/H3 tags from the post
77
+ allHeaderTags = wrappingElement.querySelectorAll(
78
+ ":scope > h1, :scope > h2, :scope > h3",
79
+ );
80
+ }
81
+
82
+ // Intersection Observer Options
83
+ let options: IntersectionObserverInit = {
84
+ root: null,
85
+ rootMargin: "0px 0px -50% 0px",
86
+ threshold: [1],
87
+ };
88
+
89
+ // Each Intersection Observer runs setCurrent
90
+ observeHeaderTags = new IntersectionObserver(setCurrent, options);
91
+ if (wrappingElement === null) {
92
+ return;
93
+ }
94
+ allHeaderTags.forEach((tag) => {
95
+ // add scroll margin top to account for fixed navbar
96
+ tag.classList.add("scroll-mt-3xl");
97
+ observeHeaderTags.observe(tag);
98
+ });
99
+ }
100
+
101
+ // runs on initial page load
102
+ initTOC();
103
+
104
+ // runs on view transitions navigation
105
+ document.addEventListener("astro:after-swap", initTOC);
106
+ </script>
@@ -0,0 +1,71 @@
1
+ ---
2
+ import IconSun from "~icons/lucide/sun"
3
+ import IconMoon from "~icons/lucide/moon"
4
+ import Toggle from "./toggle/Toggle.astro";
5
+
6
+ export interface ThemeToggleProps {
7
+ class?: string;
8
+ size?: "sm" | "md" | "lg";
9
+ }
10
+ type Props = ThemeToggleProps;
11
+
12
+ const { class: className, size = "md" } = Astro.props as Props;
13
+ ---
14
+
15
+ <Toggle
16
+ id="theme-toggle"
17
+ size={size}
18
+ class:list={[
19
+ "group text-muted-foreground hover:text-foreground rounded-full hover:bg-transparent",
20
+ className,
21
+ ]}
22
+ variant="ghost"
23
+ className={className}
24
+ aria-label="Toggle theme"
25
+ >
26
+ <IconSun class="sun-wrapper" />
27
+ <IconMoon class="moon-wrapper" />
28
+ </Toggle>
29
+
30
+ <style>
31
+ #theme-toggle[data-state="on"] .sun-wrapper {
32
+ display: none;
33
+ }
34
+ #theme-toggle[data-state="off"] .sun-wrapper {
35
+ display: flex;
36
+ }
37
+ #theme-toggle[data-state="on"] .moon-wrapper {
38
+ display: flex;
39
+ }
40
+ #theme-toggle[data-state="off"] .moon-wrapper {
41
+ display: none;
42
+ }
43
+ </style>
44
+
45
+ <script>
46
+ function setupThemeToggle() {
47
+ const button = document.getElementById('theme-toggle');
48
+ if (!button) return;
49
+
50
+ // Use current document class to set initial button state
51
+ const isDark = document.documentElement.classList.contains('dark');
52
+ button.setAttribute('data-state', isDark ? 'on' : 'off');
53
+ button.setAttribute('aria-pressed', isDark.toString());
54
+
55
+ button.addEventListener('click', () => {
56
+ // Toggle the class on the <html> tag
57
+ const newDark = document.documentElement.classList.toggle('dark');
58
+
59
+ // Update button state visually
60
+ button.setAttribute('data-state', newDark ? 'on' : 'off');
61
+ button.setAttribute('aria-pressed', newDark.toString());
62
+
63
+ // Save the user's choice
64
+ localStorage.setItem('theme', newDark ? 'dark' : 'light');
65
+ });
66
+ }
67
+
68
+ // Runs on initial load AND after each navigation (View Transitions)
69
+ document.addEventListener('astro:page-load', setupThemeToggle);
70
+ </script>
71
+
@@ -0,0 +1,257 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { cv } from "css-variants";
4
+
5
+ export interface AccordionProps extends HTMLAttributes<"div"> {
6
+ /**
7
+ * The type of accordion. If "single", only one item can be open at a time.
8
+ */
9
+ type?: "single" | "multiple";
10
+ /**
11
+ * The value of the item that should be open by default
12
+ */
13
+ defaultValue?: string;
14
+ }
15
+
16
+ type Props = AccordionProps;
17
+
18
+ export const accordion = cv({ base: "starwind-accordion" });
19
+
20
+ const { type = "single", defaultValue, class: className, ...rest } = Astro.props;
21
+ ---
22
+
23
+ <div
24
+ class:list={[accordion(), className]}
25
+ data-type={type}
26
+ data-value={defaultValue}
27
+ {...rest}
28
+ data-slot="accordion"
29
+ >
30
+ <slot />
31
+ </div>
32
+
33
+ <script>
34
+ type AccordionType = "single" | "multiple";
35
+ type AccordionState = "open" | "closed";
36
+
37
+ /** Represents a single accordion item with its associated elements */
38
+ interface AccordionItem {
39
+ element: HTMLElement;
40
+ trigger: HTMLElement;
41
+ content: HTMLElement;
42
+ value: string;
43
+ }
44
+
45
+ /**
46
+ * Handles the functionality of an accordion component.
47
+ * Supports single and multiple open items, keyboard navigation,
48
+ * and maintains ARIA accessibility standards.
49
+ */
50
+ class AccordionHandler {
51
+ private accordion: HTMLElement;
52
+ private type: AccordionType;
53
+ private items: AccordionItem[];
54
+ private itemsByValue: Map<string, AccordionItem>;
55
+ private accordionId: string;
56
+ private isInitialized: boolean = false;
57
+
58
+ /**
59
+ * Creates a new AccordionHandler instance
60
+ * @param accordion - The root accordion element
61
+ * @param idx - Unique index for this accordion instance
62
+ */
63
+ constructor(accordion: HTMLElement, idx: number) {
64
+ this.accordion = accordion;
65
+ this.type = (accordion.dataset.type || "single") as AccordionType;
66
+ this.accordionId = `starwind-accordion${idx}`;
67
+
68
+ // Cache all items and create lookup maps
69
+ this.items = this.initializeItems();
70
+ this.itemsByValue = new Map(this.items.map((item) => [item.value, item]));
71
+
72
+ this.setupItems();
73
+ this.setInitialState();
74
+ this.isInitialized = true;
75
+ }
76
+
77
+ /**
78
+ * Initializes accordion items by querying the DOM and setting up data structures
79
+ * @returns Array of AccordionItem objects
80
+ */
81
+ private initializeItems(): AccordionItem[] {
82
+ return Array.from(this.accordion.querySelectorAll<HTMLElement>(".starwind-accordion-item"))
83
+ .map((element, idx) => {
84
+ const trigger = element.querySelector<HTMLElement>(".starwind-accordion-trigger");
85
+ const content = element.querySelector<HTMLElement>(".starwind-accordion-content");
86
+ const value = element.getAttribute("data-value") || String(idx);
87
+
88
+ if (!trigger || !content) return null;
89
+
90
+ return { element, trigger, content, value };
91
+ })
92
+ .filter((item): item is AccordionItem => item !== null);
93
+ }
94
+
95
+ /**
96
+ * Sets up initial state and event listeners for all accordion items
97
+ */
98
+ private setupItems(): void {
99
+ this.items.forEach((item, idx) => {
100
+ this.setupAccessibility(item, idx);
101
+ this.setContentHeight(item.content);
102
+ this.setupEventListeners(item);
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Sets up ARIA attributes and IDs for accessibility
108
+ * @param item - The accordion item to setup
109
+ * @param idx - Index of the item
110
+ */
111
+ private setupAccessibility(item: AccordionItem, idx: number): void {
112
+ const triggerId = `${this.accordionId}-t${idx}`;
113
+ const contentId = `${this.accordionId}-c${idx}`;
114
+
115
+ item.trigger.id = triggerId;
116
+ item.trigger.setAttribute("aria-controls", contentId);
117
+ item.trigger.setAttribute("aria-expanded", "false");
118
+
119
+ item.content.id = contentId;
120
+ item.content.setAttribute("aria-labelledby", triggerId);
121
+ item.content.setAttribute("role", "region");
122
+ }
123
+
124
+ /**
125
+ * Calculates and sets the content height CSS variable for animations
126
+ * @param content - The content element to measure
127
+ */
128
+ private setContentHeight(content: HTMLElement): void {
129
+ const contentInner = content.firstElementChild as HTMLElement;
130
+ if (contentInner) {
131
+ const height = contentInner.getBoundingClientRect().height;
132
+ content.style.setProperty("--starwind-accordion-content-height", `${height}px`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Sets the initial state based on the default value attribute
138
+ */
139
+ private setInitialState(): void {
140
+ const defaultValue = this.accordion.dataset.value;
141
+ if (defaultValue) {
142
+ const item = this.itemsByValue.get(defaultValue);
143
+ if (item) {
144
+ this.setItemState(item, true);
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Sets up click and keyboard event listeners for an accordion item
151
+ * @param item - The accordion item to setup listeners for
152
+ */
153
+ private setupEventListeners(item: AccordionItem): void {
154
+ item.trigger.addEventListener("click", () => this.handleClick(item));
155
+ item.trigger.addEventListener("keydown", (e) => this.handleKeyDown(e, item));
156
+ }
157
+
158
+ /**
159
+ * Handles click events on accordion triggers
160
+ * @param item - The clicked accordion item
161
+ */
162
+ private handleClick(item: AccordionItem): void {
163
+ const isOpen = item.element.getAttribute("data-state") === "open";
164
+ this.toggleItem(item, !isOpen);
165
+ }
166
+
167
+ /**
168
+ * Handles keyboard navigation events
169
+ * @param event - The keyboard event
170
+ * @param item - The current accordion item
171
+ */
172
+ private handleKeyDown(event: KeyboardEvent, item: AccordionItem): void {
173
+ const index = this.items.indexOf(item);
174
+
175
+ const keyActions: Record<string, () => void> = {
176
+ ArrowDown: () => this.focusItem(index + 1),
177
+ ArrowUp: () => this.focusItem(index - 1),
178
+ Home: () => this.focusItem(0),
179
+ End: () => this.focusItem(this.items.length - 1),
180
+ };
181
+
182
+ const action = keyActions[event.key];
183
+ if (action) {
184
+ event.preventDefault();
185
+ action();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Focuses an accordion item by index with wrapping
191
+ * @param index - The target index to focus
192
+ */
193
+ private focusItem(index: number): void {
194
+ if (this.items.length === 0) return;
195
+ const targetIndex = (index + this.items.length) % this.items.length;
196
+ this.items[targetIndex]?.trigger.focus();
197
+ }
198
+
199
+ /**
200
+ * Toggles an accordion item's state
201
+ * @param item - The item to toggle
202
+ * @param shouldOpen - Whether the item should be opened
203
+ */
204
+ private toggleItem(item: AccordionItem, shouldOpen: boolean): void {
205
+ if (this.type === "single" && shouldOpen) {
206
+ // Close other items if in single mode
207
+ this.items.forEach((otherItem) => {
208
+ if (otherItem !== item && otherItem.element.getAttribute("data-state") === "open") {
209
+ this.setItemState(otherItem, false);
210
+ }
211
+ });
212
+ }
213
+
214
+ this.setItemState(item, shouldOpen);
215
+ }
216
+
217
+ /**
218
+ * Sets the state of an accordion item
219
+ * @param item - The item to update
220
+ * @param isOpen - Whether the item should be open
221
+ */
222
+ private setItemState(item: AccordionItem, isOpen: boolean): void {
223
+ const state: AccordionState = isOpen ? "open" : "closed";
224
+
225
+ // Skip animation during initial setup, enable for subsequent toggles
226
+ if (!this.isInitialized) {
227
+ item.content.style.setProperty("animation", "none");
228
+ } else {
229
+ item.content.style.removeProperty("animation");
230
+ }
231
+
232
+ // Set content height variable for animations
233
+ this.setContentHeight(item.content);
234
+
235
+ item.element.setAttribute("data-state", state);
236
+ item.content.setAttribute("data-state", state);
237
+ item.trigger.setAttribute("data-state", state);
238
+ item.trigger.setAttribute("aria-expanded", isOpen.toString());
239
+ }
240
+ }
241
+
242
+ // Store instances in a WeakMap to avoid memory leaks
243
+ const accordionInstances = new WeakMap<HTMLElement, AccordionHandler>();
244
+ let accordionCounter = 0;
245
+
246
+ const setupAccordions = () => {
247
+ document.querySelectorAll<HTMLElement>(".starwind-accordion").forEach((accordion) => {
248
+ if (!accordionInstances.has(accordion)) {
249
+ accordionInstances.set(accordion, new AccordionHandler(accordion, accordionCounter++));
250
+ }
251
+ });
252
+ };
253
+
254
+ setupAccordions();
255
+ document.addEventListener("astro:after-swap", setupAccordions);
256
+ document.addEventListener("starwind:init", setupAccordions);
257
+ </script>