astro-awesomeness 0.5.0 → 0.7.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 (38) hide show
  1. package/dist/cn-DmzdyV07.js +3 -0
  2. package/dist/components/index.d.ts +17 -9
  3. package/dist/components/index.js +38 -18
  4. package/dist/components/index.js.map +1 -1
  5. package/dist/content/index.js +3 -2
  6. package/dist/content/index.js.map +1 -1
  7. package/dist/{index-pemcLff6.d.ts → index-syqFQChT.d.ts} +3 -6
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +3 -2
  10. package/dist/lib/index.d.ts +1 -1
  11. package/dist/lib/index.js +4 -3
  12. package/dist/{lib-Dn4hRxdC.js → lib-mtnJ1lv8.js} +19 -9
  13. package/dist/lib-mtnJ1lv8.js.map +1 -0
  14. package/dist/rolldown-runtime-w6R9maHv.js +18 -0
  15. package/dist/tailwind-preset.js +2 -1
  16. package/dist/tailwind-preset.js.map +1 -1
  17. package/package.json +11 -12
  18. package/src/astro/header.astro +1 -1
  19. package/src/astro/post-card.astro +3 -2
  20. package/src/astro/seo.astro +23 -1
  21. package/src/components/command-menu.tsx +4 -1
  22. package/src/components/index.ts +2 -1
  23. package/src/components/theme-toggle.tsx +30 -5
  24. package/src/components/ui/button-variants.ts +35 -0
  25. package/src/components/ui/button.tsx +16 -50
  26. package/src/components/ui/command.tsx +5 -4
  27. package/src/components/ui/dialog.tsx +1 -1
  28. package/src/layouts/base-layout.astro +30 -1
  29. package/src/layouts/list-layout.astro +9 -5
  30. package/src/layouts/post-layout.astro +46 -4
  31. package/src/lib/cn.ts +1 -6
  32. package/src/lib/format-date.ts +10 -1
  33. package/src/lib/get-related-posts.test.ts +15 -9
  34. package/src/lib/get-related-posts.ts +2 -2
  35. package/dist/chunk-D7D4PA-g.js +0 -13
  36. package/dist/cn-C_hacsED.js +0 -8
  37. package/dist/cn-C_hacsED.js.map +0 -1
  38. package/dist/lib-Dn4hRxdC.js.map +0 -1
@@ -0,0 +1,3 @@
1
+ import { cn } from "cnfast";
2
+
3
+ export { cn as t };
@@ -1,19 +1,25 @@
1
- import { VariantProps } from "class-variance-authority";
1
+ import { Button as Button$1 } from "@base-ui/react/button";
2
2
  import * as React from "react";
3
+ import { VariantProps } from "class-variance-authority";
3
4
  import { Command as Command$1 } from "cmdk";
4
5
  import { Dialog as Dialog$1 } from "@base-ui/react/dialog";
5
6
 
6
- //#region src/components/ui/button.d.ts
7
+ //#region src/components/ui/button-variants.d.ts
7
8
  declare const buttonVariants: (props?: ({
8
9
  size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
9
- variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
10
+ variant?: "default" | "link" | "destructive" | "ghost" | "outline" | "secondary" | null | undefined;
10
11
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
11
- declare const Button: React.ForwardRefExoticComponent<Omit<Omit<Omit<import("@base-ui/react").ButtonProps, "ref"> & React.RefAttributes<HTMLElement>, "ref">, "className"> & VariantProps<(props?: ({
12
- size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
13
- variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
14
- } & import("class-variance-authority/types").ClassProp) | undefined) => string> & {
12
+ //#endregion
13
+ //#region src/components/ui/button.d.ts
14
+ type ButtonProps = Omit<React.ComponentProps<typeof Button$1>, "className"> & VariantProps<typeof buttonVariants> & {
15
15
  className?: string;
16
- } & React.RefAttributes<HTMLElement>>;
16
+ };
17
+ declare const Button: ({
18
+ className,
19
+ size,
20
+ variant,
21
+ ...props
22
+ }: ButtonProps) => React.JSX.Element;
17
23
  //#endregion
18
24
  //#region src/components/ui/dialog.d.ts
19
25
  declare function Dialog({
@@ -95,12 +101,14 @@ type CommandMenuProps = {
95
101
  groupHeading?: string;
96
102
  items: Array<CommandMenuItem>;
97
103
  placeholder?: string;
104
+ title?: string;
98
105
  };
99
106
  declare const CommandMenu: ({
100
107
  emptyMessage,
101
108
  groupHeading,
102
109
  items,
103
- placeholder
110
+ placeholder,
111
+ title
104
112
  }: CommandMenuProps) => import("react").JSX.Element;
105
113
  //#endregion
106
114
  //#region src/components/theme-toggle.d.ts
@@ -1,13 +1,13 @@
1
- import { t as cn } from "../cn-C_hacsED.js";
1
+ import { t as cn } from "../cn-DmzdyV07.js";
2
2
  import { Button as Button$1 } from "@base-ui/react/button";
3
- import { cva } from "class-variance-authority";
4
- import * as React from "react";
5
3
  import { useEffect, useState, useSyncExternalStore } from "react";
4
+ import { cva } from "class-variance-authority";
6
5
  import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { Command as Command$1 } from "cmdk";
8
7
  import { Moon, Search, SearchIcon, Sun, XIcon } from "lucide-react";
9
8
  import { Dialog as Dialog$1 } from "@base-ui/react/dialog";
10
- //#region src/components/ui/button.tsx
9
+
10
+ //#region src/components/ui/button-variants.ts
11
11
  const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
12
12
  defaultVariants: {
13
13
  size: "default",
@@ -34,7 +34,10 @@ const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap
34
34
  }
35
35
  }
36
36
  });
37
- const Button = React.forwardRef(({ className, size = "default", variant = "default", ...props }, ref) => {
37
+
38
+ //#endregion
39
+ //#region src/components/ui/button.tsx
40
+ const Button = ({ className, size = "default", variant = "default", ...props }) => {
38
41
  return /* @__PURE__ */ jsx(Button$1, {
39
42
  className: cn(buttonVariants({
40
43
  className,
@@ -44,11 +47,10 @@ const Button = React.forwardRef(({ className, size = "default", variant = "defau
44
47
  "data-size": size,
45
48
  "data-slot": "button",
46
49
  "data-variant": variant,
47
- ref,
48
50
  ...props
49
51
  });
50
- });
51
- Button.displayName = "Button";
52
+ };
53
+
52
54
  //#endregion
53
55
  //#region src/components/ui/dialog.tsx
54
56
  function Dialog({ ...props }) {
@@ -84,7 +86,7 @@ function DialogContent({ children, className, showCloseButton = true, ...props }
84
86
  "data-slot": "dialog-content",
85
87
  ...props,
86
88
  children: [children, showCloseButton && /* @__PURE__ */ jsxs(Dialog$1.Close, {
87
- className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
89
+ className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
88
90
  "data-slot": "dialog-close",
89
91
  type: "button",
90
92
  children: [/* @__PURE__ */ jsx(XIcon, {}), /* @__PURE__ */ jsx("span", {
@@ -127,6 +129,7 @@ function DialogDescription({ className, ...props }) {
127
129
  ...props
128
130
  });
129
131
  }
132
+
130
133
  //#endregion
131
134
  //#region src/components/ui/command.tsx
132
135
  function Command({ className, ...props }) {
@@ -189,9 +192,10 @@ function CommandShortcut({ className, ...props }) {
189
192
  ...props
190
193
  });
191
194
  }
195
+
192
196
  //#endregion
193
197
  //#region src/components/command-menu.tsx
194
- const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", items, placeholder = "Search posts…" }) => {
198
+ const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", items, placeholder = "Search posts…", title = "Search posts" }) => {
195
199
  const [open, setOpen] = useState(false);
196
200
  useEffect(() => {
197
201
  const handleKeyDown = (event) => {
@@ -215,9 +219,12 @@ const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", ite
215
219
  variant: "ghost"
216
220
  }),
217
221
  children: /* @__PURE__ */ jsx(Search, {})
218
- }), /* @__PURE__ */ jsx(DialogContent, {
222
+ }), /* @__PURE__ */ jsxs(DialogContent, {
219
223
  className: "p-0",
220
- children: /* @__PURE__ */ jsxs(Command, { children: [/* @__PURE__ */ jsx(CommandInput, { placeholder }), /* @__PURE__ */ jsxs(CommandList, { children: [/* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage }), /* @__PURE__ */ jsx(CommandGroup, {
224
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
225
+ className: "sr-only",
226
+ children: title
227
+ }), /* @__PURE__ */ jsxs(Command, { children: [/* @__PURE__ */ jsx(CommandInput, { placeholder }), /* @__PURE__ */ jsxs(CommandList, { children: [/* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage }), /* @__PURE__ */ jsx(CommandGroup, {
221
228
  heading: groupHeading,
222
229
  children: items.map((item) => /* @__PURE__ */ jsx(CommandItem, {
223
230
  onSelect: () => {
@@ -232,10 +239,11 @@ const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", ite
232
239
  }) : null]
233
240
  })
234
241
  }, item.url))
235
- })] })] })
242
+ })] })] })]
236
243
  })]
237
244
  });
238
245
  };
246
+
239
247
  //#endregion
240
248
  //#region src/components/theme-toggle.tsx
241
249
  const getStoredTheme = () => {
@@ -244,6 +252,20 @@ const getStoredTheme = () => {
244
252
  if (stored === "light" || stored === "dark") return stored;
245
253
  return null;
246
254
  };
255
+ const storedThemeListeners = /* @__PURE__ */ new Set();
256
+ const subscribeStoredTheme = (callback) => {
257
+ if (typeof window === "undefined") return () => {};
258
+ storedThemeListeners.add(callback);
259
+ window.addEventListener("storage", callback);
260
+ return () => {
261
+ storedThemeListeners.delete(callback);
262
+ window.removeEventListener("storage", callback);
263
+ };
264
+ };
265
+ const setStoredTheme = (next) => {
266
+ window.localStorage.setItem("theme", next);
267
+ for (const listener of storedThemeListeners) listener();
268
+ };
247
269
  const getPrefersDark = () => {
248
270
  if (typeof window === "undefined") return false;
249
271
  return window.matchMedia("(prefers-color-scheme: dark)").matches;
@@ -258,13 +280,11 @@ const subscribePrefersDark = (callback) => {
258
280
  };
259
281
  const ThemeToggle = ({ ariaLabel = "Toggle theme" }) => {
260
282
  const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);
261
- const [override, setOverride] = useState(() => getStoredTheme());
262
- const theme = override ?? (prefersDark ? "dark" : "light");
283
+ const theme = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null) ?? (prefersDark ? "dark" : "light");
263
284
  const handleToggleTheme = () => {
264
285
  const next = theme === "dark" ? "light" : "dark";
265
- window.localStorage.setItem("theme", next);
266
286
  document.documentElement.classList.toggle("dark", next === "dark");
267
- setOverride(next);
287
+ setStoredTheme(next);
268
288
  };
269
289
  return /* @__PURE__ */ jsx(Button, {
270
290
  "aria-label": ariaLabel,
@@ -274,7 +294,7 @@ const ThemeToggle = ({ ariaLabel = "Toggle theme" }) => {
274
294
  children: theme === "dark" ? /* @__PURE__ */ jsx(Sun, {}) : /* @__PURE__ */ jsx(Moon, {})
275
295
  });
276
296
  };
297
+
277
298
  //#endregion
278
299
  export { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandMenu, CommandSeparator, CommandShortcut, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, ThemeToggle, buttonVariants };
279
-
280
300
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["ButtonPrimitive","DialogPrimitive","CommandPrimitive"],"sources":["../../src/components/ui/button.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/command.tsx","../../src/components/command-menu.tsx","../../src/components/theme-toggle.tsx"],"sourcesContent":["import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../lib/cn\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n icon: \"size-9\",\n \"icon-lg\": \"size-10\",\n \"icon-sm\": \"size-8\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n },\n },\n },\n);\n\ntype ButtonProps = Omit<React.ComponentPropsWithoutRef<typeof ButtonPrimitive>, \"className\"> &\n VariantProps<typeof buttonVariants> & {\n className?: string;\n };\n\nconst Button = React.forwardRef<React.ComponentRef<typeof ButtonPrimitive>, ButtonProps>(\n ({ className, size = \"default\", variant = \"default\", ...props }, ref) => {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-size={size}\n data-slot=\"button\"\n data-variant={variant}\n ref={ref}\n {...props}\n />\n );\n },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n","import { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Button } from \"./button\";\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Backdrop>) {\n return (\n <DialogPrimitive.Backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:animate-in data-[open]:fade-in-0\",\n className,\n )}\n data-slot=\"dialog-overlay\"\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n children,\n className,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Popup> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Popup\n className={cn(\n \"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95 sm:max-w-lg\",\n className,\n )}\n data-slot=\"dialog-content\"\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n className=\"absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n data-slot=\"dialog-close\"\n type=\"button\"\n >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Popup>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n data-slot=\"dialog-header\"\n {...props}\n />\n );\n}\n\nfunction DialogFooter({\n children,\n className,\n showCloseButton = false,\n ...props\n}: React.ComponentProps<\"div\"> & {\n showCloseButton?: boolean;\n}) {\n return (\n <div\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n data-slot=\"dialog-footer\"\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close render={<Button variant=\"outline\" />}>Close</DialogPrimitive.Close>\n )}\n </div>\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n className={cn(\"text-lg leading-none font-semibold\", className)}\n data-slot=\"dialog-title\"\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n className={cn(\"text-sm text-muted-foreground\", className)}\n data-slot=\"dialog-description\"\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n // fallow-ignore-next-line unused-export\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n // fallow-ignore-next-line unused-export\n DialogOverlay,\n // fallow-ignore-next-line unused-export\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n","import * as React from \"react\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog\";\n\nfunction Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {\n return (\n <CommandPrimitive\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n className,\n )}\n data-slot=\"command\"\n {...props}\n />\n );\n}\n\nfunction CommandDialog({\n children,\n className,\n description = \"Search for a command to run...\",\n showCloseButton = true,\n title = \"Command Palette\",\n ...props\n}: React.ComponentProps<typeof Dialog> & {\n children?: React.ReactNode;\n className?: string;\n description?: string;\n showCloseButton?: boolean;\n title?: string;\n}) {\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent\n className={cn(\"overflow-hidden p-0\", className)}\n showCloseButton={showCloseButton}\n >\n <Command className=\"**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction CommandInput({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n return (\n <div className=\"flex h-9 items-center gap-2 border-b px-3\" data-slot=\"command-input-wrapper\">\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n className={cn(\n \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n data-slot=\"command-input\"\n {...props}\n />\n </div>\n );\n}\n\nfunction CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {\n return (\n <CommandPrimitive.List\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n data-slot=\"command-list\"\n {...props}\n />\n );\n}\n\nfunction CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n className=\"py-6 text-center text-sm\"\n data-slot=\"command-empty\"\n {...props}\n />\n );\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return (\n <CommandPrimitive.Group\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n className,\n )}\n data-slot=\"command-group\"\n {...props}\n />\n );\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n return (\n <CommandPrimitive.Separator\n className={cn(\"-mx-1 h-px bg-border\", className)}\n data-slot=\"command-separator\"\n {...props}\n />\n );\n}\n\nfunction CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return (\n <CommandPrimitive.Item\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n className,\n )}\n data-slot=\"command-item\"\n {...props}\n />\n );\n}\n\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)}\n data-slot=\"command-shortcut\"\n {...props}\n />\n );\n}\n\nexport {\n Command,\n // fallow-ignore-next-line unused-export\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n};\n","import { Search } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\n\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"./ui/command\";\nimport { Dialog, DialogContent, DialogTrigger } from \"./ui/dialog\";\nimport { Button } from \"./ui/button\";\n\ntype CommandMenuItem = { description?: string; title: string; url: string };\n\ntype CommandMenuProps = {\n emptyMessage?: string;\n groupHeading?: string;\n items: Array<CommandMenuItem>;\n placeholder?: string;\n};\n\nconst CommandMenu = ({\n emptyMessage = \"No results.\",\n groupHeading = \"Posts\",\n items,\n placeholder = \"Search posts…\",\n}: CommandMenuProps) => {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"k\" && (event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n setOpen((value) => !value);\n }\n };\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, []);\n\n return (\n <Dialog onOpenChange={setOpen} open={open}>\n <DialogTrigger render={<Button aria-label=\"Open search\" size=\"icon\" variant=\"ghost\" />}>\n <Search />\n </DialogTrigger>\n <DialogContent className=\"p-0\">\n <Command>\n <CommandInput placeholder={placeholder} />\n <CommandList>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n <CommandGroup heading={groupHeading}>\n {items.map((item) => (\n <CommandItem\n key={item.url}\n onSelect={() => {\n window.location.href = item.url;\n }}\n value={item.title}\n >\n <div className=\"flex flex-col\">\n <span>{item.title}</span>\n {item.description ? (\n <span className=\"text-muted-foreground text-xs\">{item.description}</span>\n ) : null}\n </div>\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n </Command>\n </DialogContent>\n </Dialog>\n );\n};\n\nexport { CommandMenu };\nexport type { CommandMenuItem, CommandMenuProps };\n","import { Moon, Sun } from \"lucide-react\";\nimport { useState, useSyncExternalStore } from \"react\";\n\nimport { Button } from \"./ui/button\";\n\ntype Theme = \"light\" | \"dark\";\n\nconst getStoredTheme = (): Theme | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n const stored = window.localStorage.getItem(\"theme\");\n if (stored === \"light\" || stored === \"dark\") {\n return stored;\n }\n return null;\n};\n\nconst getPrefersDark = (): boolean => {\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n};\n\nconst subscribePrefersDark = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", callback);\n return () => {\n media.removeEventListener(\"change\", callback);\n };\n};\n\ntype Props = {\n ariaLabel?: string;\n};\n\nconst ThemeToggle = ({ ariaLabel = \"Toggle theme\" }: Props) => {\n // useSyncExternalStore avoids hydration mismatch on system preference reads.\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n const [override, setOverride] = useState<Theme | null>(() => getStoredTheme());\n const theme: Theme = override ?? (prefersDark ? \"dark\" : \"light\");\n\n const handleToggleTheme = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n window.localStorage.setItem(\"theme\", next);\n document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n setOverride(next);\n };\n\n return (\n <Button aria-label={ariaLabel} onClick={handleToggleTheme} size=\"icon\" variant=\"ghost\">\n {theme === \"dark\" ? <Sun /> : <Moon />}\n </Button>\n );\n};\n\nexport { ThemeToggle };\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["ButtonPrimitive","DialogPrimitive","CommandPrimitive"],"sources":["../../src/components/ui/button-variants.ts","../../src/components/ui/button.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/command.tsx","../../src/components/command-menu.tsx","../../src/components/theme-toggle.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n icon: \"size-9\",\n \"icon-lg\": \"size-10\",\n \"icon-sm\": \"size-8\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n },\n },\n },\n);\n\nexport { buttonVariants };\n","import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { buttonVariants } from \"./button-variants\";\n\ntype ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, \"className\"> &\n VariantProps<typeof buttonVariants> & {\n className?: string;\n };\n\n// React 19 passes `ref` as a regular prop, so no forwardRef wrapper is needed.\nconst Button = ({ className, size = \"default\", variant = \"default\", ...props }: ButtonProps) => {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-size={size}\n data-slot=\"button\"\n data-variant={variant}\n {...props}\n />\n );\n};\n\nexport { Button };\n","import { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Button } from \"./button\";\n\nfunction Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Backdrop>) {\n return (\n <DialogPrimitive.Backdrop\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[closed]:animate-out data-[closed]:fade-out-0 data-[open]:animate-in data-[open]:fade-in-0\",\n className,\n )}\n data-slot=\"dialog-overlay\"\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n children,\n className,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Popup> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Popup\n className={cn(\n \"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95 sm:max-w-lg\",\n className,\n )}\n data-slot=\"dialog-content\"\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n className=\"absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n data-slot=\"dialog-close\"\n type=\"button\"\n >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Popup>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n data-slot=\"dialog-header\"\n {...props}\n />\n );\n}\n\nfunction DialogFooter({\n children,\n className,\n showCloseButton = false,\n ...props\n}: React.ComponentProps<\"div\"> & {\n showCloseButton?: boolean;\n}) {\n return (\n <div\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n data-slot=\"dialog-footer\"\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close render={<Button variant=\"outline\" />}>Close</DialogPrimitive.Close>\n )}\n </div>\n );\n}\n\nfunction DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n className={cn(\"text-lg leading-none font-semibold\", className)}\n data-slot=\"dialog-title\"\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n className={cn(\"text-sm text-muted-foreground\", className)}\n data-slot=\"dialog-description\"\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n // fallow-ignore-next-line unused-export\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n // fallow-ignore-next-line unused-export\n DialogOverlay,\n // fallow-ignore-next-line unused-export\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n","import * as React from \"react\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { SearchIcon } from \"lucide-react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from \"./dialog\";\n\nfunction Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {\n return (\n <CommandPrimitive\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n className,\n )}\n data-slot=\"command\"\n {...props}\n />\n );\n}\n\nfunction CommandDialog({\n children,\n className,\n description = \"Search for a command to run...\",\n showCloseButton = true,\n title = \"Command Palette\",\n ...props\n}: React.ComponentProps<typeof Dialog> & {\n children?: React.ReactNode;\n className?: string;\n description?: string;\n showCloseButton?: boolean;\n title?: string;\n}) {\n return (\n <Dialog {...props}>\n <DialogContent\n className={cn(\"overflow-hidden p-0\", className)}\n showCloseButton={showCloseButton}\n >\n {/* Inside DialogContent so the sr-only name renders in the portaled popup, not the page. */}\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <Command className=\"**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction CommandInput({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n return (\n <div className=\"flex h-9 items-center gap-2 border-b px-3\" data-slot=\"command-input-wrapper\">\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n className={cn(\n \"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n className,\n )}\n data-slot=\"command-input\"\n {...props}\n />\n </div>\n );\n}\n\nfunction CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {\n return (\n <CommandPrimitive.List\n className={cn(\"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\", className)}\n data-slot=\"command-list\"\n {...props}\n />\n );\n}\n\nfunction CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n className=\"py-6 text-center text-sm\"\n data-slot=\"command-empty\"\n {...props}\n />\n );\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return (\n <CommandPrimitive.Group\n className={cn(\n \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n className,\n )}\n data-slot=\"command-group\"\n {...props}\n />\n );\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n return (\n <CommandPrimitive.Separator\n className={cn(\"-mx-1 h-px bg-border\", className)}\n data-slot=\"command-separator\"\n {...props}\n />\n );\n}\n\nfunction CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return (\n <CommandPrimitive.Item\n className={cn(\n \"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground\",\n className,\n )}\n data-slot=\"command-item\"\n {...props}\n />\n );\n}\n\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground\", className)}\n data-slot=\"command-shortcut\"\n {...props}\n />\n );\n}\n\nexport {\n Command,\n // fallow-ignore-next-line unused-export\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n};\n","import { Search } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\n\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"./ui/command\";\nimport { Dialog, DialogContent, DialogTitle, DialogTrigger } from \"./ui/dialog\";\nimport { Button } from \"./ui/button\";\n\ntype CommandMenuItem = { description?: string; title: string; url: string };\n\ntype CommandMenuProps = {\n emptyMessage?: string;\n groupHeading?: string;\n items: Array<CommandMenuItem>;\n placeholder?: string;\n title?: string;\n};\n\nconst CommandMenu = ({\n emptyMessage = \"No results.\",\n groupHeading = \"Posts\",\n items,\n placeholder = \"Search posts…\",\n title = \"Search posts\",\n}: CommandMenuProps) => {\n const [open, setOpen] = useState(false);\n\n useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"k\" && (event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n setOpen((value) => !value);\n }\n };\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown);\n };\n }, []);\n\n return (\n <Dialog onOpenChange={setOpen} open={open}>\n <DialogTrigger render={<Button aria-label=\"Open search\" size=\"icon\" variant=\"ghost\" />}>\n <Search />\n </DialogTrigger>\n <DialogContent className=\"p-0\">\n <DialogTitle className=\"sr-only\">{title}</DialogTitle>\n <Command>\n <CommandInput placeholder={placeholder} />\n <CommandList>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n <CommandGroup heading={groupHeading}>\n {items.map((item) => (\n <CommandItem\n key={item.url}\n onSelect={() => {\n window.location.href = item.url;\n }}\n value={item.title}\n >\n <div className=\"flex flex-col\">\n <span>{item.title}</span>\n {item.description ? (\n <span className=\"text-muted-foreground text-xs\">{item.description}</span>\n ) : null}\n </div>\n </CommandItem>\n ))}\n </CommandGroup>\n </CommandList>\n </Command>\n </DialogContent>\n </Dialog>\n );\n};\n\nexport { CommandMenu };\nexport type { CommandMenuItem, CommandMenuProps };\n","import { Moon, Sun } from \"lucide-react\";\nimport { useSyncExternalStore } from \"react\";\n\nimport { Button } from \"./ui/button\";\n\ntype Theme = \"light\" | \"dark\";\n\nconst getStoredTheme = (): Theme | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n const stored = window.localStorage.getItem(\"theme\");\n if (stored === \"light\" || stored === \"dark\") {\n return stored;\n }\n return null;\n};\n\nconst storedThemeListeners = new Set<() => void>();\n\nconst subscribeStoredTheme = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n storedThemeListeners.add(callback);\n // \"storage\" only fires in other tabs; same-tab writes notify via setStoredTheme.\n window.addEventListener(\"storage\", callback);\n return () => {\n storedThemeListeners.delete(callback);\n window.removeEventListener(\"storage\", callback);\n };\n};\n\nconst setStoredTheme = (next: Theme) => {\n window.localStorage.setItem(\"theme\", next);\n for (const listener of storedThemeListeners) {\n listener();\n }\n};\n\nconst getPrefersDark = (): boolean => {\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n};\n\nconst subscribePrefersDark = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", callback);\n return () => {\n media.removeEventListener(\"change\", callback);\n };\n};\n\ntype Props = {\n ariaLabel?: string;\n};\n\nconst ThemeToggle = ({ ariaLabel = \"Toggle theme\" }: Props) => {\n // useSyncExternalStore avoids hydration mismatch on both reads: the server\n // snapshots (false / null) match the server HTML, then React re-syncs to the\n // real values right after hydration. Reading localStorage during the initial\n // render instead (e.g. in a useState initializer) makes React 19 log a\n // recoverable hydration error for every visitor with a stored theme.\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n const override = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null);\n const theme: Theme = override ?? (prefersDark ? \"dark\" : \"light\");\n\n const handleToggleTheme = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n setStoredTheme(next);\n };\n\n return (\n <Button aria-label={ariaLabel} onClick={handleToggleTheme} size=\"icon\" variant=\"ghost\">\n {theme === \"dark\" ? <Sun /> : <Moon />}\n </Button>\n );\n};\n\nexport { ThemeToggle };\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
- import { t as __exportAll } from "../chunk-D7D4PA-g.js";
1
+ import { t as __exportAll } from "../rolldown-runtime-w6R9maHv.js";
2
2
  import { z } from "zod";
3
+
3
4
  //#region src/content/index.ts
4
5
  var content_exports = /* @__PURE__ */ __exportAll({
5
6
  authorSchema: () => authorSchema,
@@ -37,7 +38,7 @@ const authorSchema = z.object({
37
38
  url: z.url().optional()
38
39
  });
39
40
  const notDraft = (post) => post.data.status !== "DRAFT" && !post.data.draft;
41
+
40
42
  //#endregion
41
43
  export { authorSchema, notDraft, postSchema, content_exports as t, tagSchema };
42
-
43
44
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/content/index.ts"],"sourcesContent":["import { z } from \"zod\";\n\nconst postSchema = z.object({\n author: z.string().optional(),\n categories: z.array(z.string()).default([]),\n cover: z.string().optional(),\n coverAlt: z.string().optional(),\n description: z.string().min(1),\n draft: z.boolean().default(false),\n heroImageHeight: z.number().int().optional(),\n heroImageUrl: z.url().optional(),\n heroImageWidth: z.number().int().optional(),\n pubDate: z.coerce.date(),\n seo: z\n .object({\n canonical_url: z.string().optional(),\n })\n .optional(),\n series: z.string().optional(),\n slug: z.string().optional(),\n status: z.string().optional(),\n tags: z.array(z.string()).default([]),\n title: z.string().min(1),\n updatedDate: z.coerce.date().optional(),\n});\n\nconst tagSchema = z.object({\n description: z.string().optional(),\n name: z.string().min(1),\n});\n\nconst authorSchema = z.object({\n avatar: z.string().optional(),\n bio: z.string().optional(),\n name: z.string().min(1),\n url: z.url().optional(),\n});\n\ntype Post = z.infer<typeof postSchema>;\ntype Tag = z.infer<typeof tagSchema>;\ntype Author = z.infer<typeof authorSchema>;\n\n// Predicate the 12 blog repos previously inlined ~72 times per repo\n// (`data.status !== \"DRAFT\" && !data.draft`). One source of truth so the\n// loader contract stays in sync with the consumer-side guard.\nconst notDraft = <T extends { data: { draft?: boolean; status?: string } }>(post: T): boolean =>\n post.data.status !== \"DRAFT\" && !post.data.draft;\n\nexport { authorSchema, notDraft, postSchema, tagSchema };\nexport type { Author, Post, Tag };\n"],"mappings":";;;;;;;;;AAEA,MAAM,aAAa,EAAE,OAAO;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC1C,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAC7B,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAChC,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC3C,cAAc,EAAE,IAAI,CAAC,CAAC,SAAS;CAC/B,gBAAgB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC1C,SAAS,EAAE,OAAO,KAAK;CACvB,KAAK,EACF,OAAO,EACN,eAAe,EAAE,OAAO,CAAC,CAAC,SAAS,EACrC,CAAC,CAAC,CACD,SAAS;CACZ,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,aAAa,EAAE,OAAO,KAAK,CAAC,CAAC,SAAS;AACxC,CAAC;AAED,MAAM,YAAY,EAAE,OAAO;CACzB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;CAC5B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;CACzB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,KAAK,EAAE,IAAI,CAAC,CAAC,SAAS;AACxB,CAAC;AASD,MAAM,YAAsE,SAC1E,KAAK,KAAK,WAAW,WAAW,CAAC,KAAK,KAAK"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/content/index.ts"],"sourcesContent":["import { z } from \"zod\";\n\nconst postSchema = z.object({\n author: z.string().optional(),\n categories: z.array(z.string()).default([]),\n cover: z.string().optional(),\n coverAlt: z.string().optional(),\n description: z.string().min(1),\n draft: z.boolean().default(false),\n heroImageHeight: z.number().int().optional(),\n heroImageUrl: z.url().optional(),\n heroImageWidth: z.number().int().optional(),\n pubDate: z.coerce.date(),\n seo: z\n .object({\n canonical_url: z.string().optional(),\n })\n .optional(),\n series: z.string().optional(),\n slug: z.string().optional(),\n status: z.string().optional(),\n tags: z.array(z.string()).default([]),\n title: z.string().min(1),\n updatedDate: z.coerce.date().optional(),\n});\n\nconst tagSchema = z.object({\n description: z.string().optional(),\n name: z.string().min(1),\n});\n\nconst authorSchema = z.object({\n avatar: z.string().optional(),\n bio: z.string().optional(),\n name: z.string().min(1),\n url: z.url().optional(),\n});\n\ntype Post = z.infer<typeof postSchema>;\ntype Tag = z.infer<typeof tagSchema>;\ntype Author = z.infer<typeof authorSchema>;\n\n// Predicate the 12 blog repos previously inlined ~72 times per repo\n// (`data.status !== \"DRAFT\" && !data.draft`). One source of truth so the\n// loader contract stays in sync with the consumer-side guard.\nconst notDraft = <T extends { data: { draft?: boolean; status?: string } }>(post: T): boolean =>\n post.data.status !== \"DRAFT\" && !post.data.draft;\n\nexport { authorSchema, notDraft, postSchema, tagSchema };\nexport type { Author, Post, Tag };\n"],"mappings":";;;;;;;;;;AAEA,MAAM,aAAa,EAAE,OAAO;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC1C,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAC7B,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAChC,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC3C,cAAc,EAAE,IAAI,CAAC,CAAC,SAAS;CAC/B,gBAAgB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC1C,SAAS,EAAE,OAAO,KAAK;CACvB,KAAK,EACF,OAAO,EACN,eAAe,EAAE,OAAO,CAAC,CAAC,SAAS,EACrC,CAAC,CAAC,CACD,SAAS;CACZ,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,aAAa,EAAE,OAAO,KAAK,CAAC,CAAC,SAAS;AACxC,CAAC;AAED,MAAM,YAAY,EAAE,OAAO;CACzB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;CAC5B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;CACzB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,KAAK,EAAE,IAAI,CAAC,CAAC,SAAS;AACxB,CAAC;AASD,MAAM,YAAsE,SAC1E,KAAK,KAAK,WAAW,WAAW,CAAC,KAAK,KAAK"}
@@ -1,8 +1,5 @@
1
- import { ClassValue } from "clsx";
1
+ import { cn } from "cnfast";
2
2
 
3
- //#region src/lib/cn.d.ts
4
- declare const cn: (...inputs: Array<ClassValue>) => string;
5
- //#endregion
6
3
  //#region src/lib/format-date.d.ts
7
4
  declare const formatDate: (date: Date, locale?: string, options?: Intl.DateTimeFormatOptions) => string;
8
5
  //#endregion
@@ -11,7 +8,7 @@ type HasTags = {
11
8
  data: {
12
9
  tags: Array<string>;
13
10
  };
14
- slug: string;
11
+ id: string;
15
12
  };
16
13
  declare const getRelatedPosts: <T extends HasTags>(current: T, all: Array<T>, n: number) => T[];
17
14
  //#endregion
@@ -48,4 +45,4 @@ declare namespace index_d_exports {
48
45
  }
49
46
  //#endregion
50
47
  export { PostParams as a, getRelatedPosts as c, PostLike as i, formatDate as l, slugify as n, PostUrlBuilder as o, readingTime as r, createPostUrl as s, index_d_exports as t, cn as u };
51
- //# sourceMappingURL=index-pemcLff6.d.ts.map
48
+ //# sourceMappingURL=index-syqFQChT.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { Author, Post, Tag, t as index_d_exports } from "./content/index.js";
2
- import { t as index_d_exports$1 } from "./index-pemcLff6.js";
2
+ import { t as index_d_exports$1 } from "./index-syqFQChT.js";
3
3
  export { type Author, type Post, type Tag, index_d_exports as content, index_d_exports$1 as lib };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  import { t as content_exports } from "./content/index.js";
2
- import { t as lib_exports } from "./lib-Dn4hRxdC.js";
3
- export { content_exports as content, lib_exports as lib };
2
+ import { t as lib_exports } from "./lib-mtnJ1lv8.js";
3
+
4
+ export { content_exports as content, lib_exports as lib };
@@ -1,2 +1,2 @@
1
- import { a as PostParams, c as getRelatedPosts, i as PostLike, l as formatDate, n as slugify, o as PostUrlBuilder, r as readingTime, s as createPostUrl, u as cn } from "../index-pemcLff6.js";
1
+ import { a as PostParams, c as getRelatedPosts, i as PostLike, l as formatDate, n as slugify, o as PostUrlBuilder, r as readingTime, s as createPostUrl, u as cn } from "../index-syqFQChT.js";
2
2
  export { type PostLike, type PostParams, type PostUrlBuilder, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, slugify };
package/dist/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
- import { t as cn } from "../cn-C_hacsED.js";
2
- import { a as getRelatedPosts, i as slugify, n as readingTime, o as formatDate, r as createPostUrl } from "../lib-Dn4hRxdC.js";
3
- export { cn, createPostUrl, formatDate, getRelatedPosts, readingTime, slugify };
1
+ import { t as cn } from "../cn-DmzdyV07.js";
2
+ import { a as getRelatedPosts, i as slugify, n as readingTime, o as formatDate, r as createPostUrl } from "../lib-mtnJ1lv8.js";
3
+
4
+ export { cn, createPostUrl, formatDate, getRelatedPosts, readingTime, slugify };
@@ -1,5 +1,6 @@
1
- import { t as __exportAll } from "./chunk-D7D4PA-g.js";
2
- import { t as cn } from "./cn-C_hacsED.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-w6R9maHv.js";
2
+ import { t as cn } from "./cn-DmzdyV07.js";
3
+
3
4
  //#region src/lib/format-date.ts
4
5
  const defaultOptions = {
5
6
  day: "numeric",
@@ -7,6 +8,7 @@ const defaultOptions = {
7
8
  timeZone: "UTC",
8
9
  year: "numeric"
9
10
  };
11
+ const defaultFormatter = new Intl.DateTimeFormat("en-US", defaultOptions);
10
12
  const formatterCache = /* @__PURE__ */ new Map();
11
13
  const getFormatter = (locale, options) => {
12
14
  const key = `${locale}:${JSON.stringify(options)}`;
@@ -17,17 +19,21 @@ const getFormatter = (locale, options) => {
17
19
  }
18
20
  return formatter;
19
21
  };
20
- const formatDate = (date, locale = "en-US", options = defaultOptions) => getFormatter(locale, {
21
- ...defaultOptions,
22
- ...options
23
- }).format(date);
22
+ const formatDate = (date, locale = "en-US", options = defaultOptions) => {
23
+ if (locale === "en-US" && options === defaultOptions) return defaultFormatter.format(date);
24
+ return getFormatter(locale, {
25
+ ...defaultOptions,
26
+ ...options
27
+ }).format(date);
28
+ };
29
+
24
30
  //#endregion
25
31
  //#region src/lib/get-related-posts.ts
26
32
  const getRelatedPosts = (current, all, n) => {
27
33
  const currentTagSet = new Set(current.data.tags);
28
34
  const scored = [];
29
35
  for (const post of all) {
30
- if (post.slug === current.slug) continue;
36
+ if (post.id === current.id) continue;
31
37
  let score = 0;
32
38
  for (const tag of post.data.tags) if (currentTagSet.has(tag)) score++;
33
39
  scored.push({
@@ -37,9 +43,11 @@ const getRelatedPosts = (current, all, n) => {
37
43
  }
38
44
  return scored.toSorted((x, y) => y.score - x.score).slice(0, n).map(({ post }) => post);
39
45
  };
46
+
40
47
  //#endregion
41
48
  //#region src/lib/slugify.ts
42
49
  const slugify = (input) => input.normalize("NFD").replaceAll(/* @__PURE__ */ new RegExp("[̀-ͯ]", "gv"), "").toLowerCase().replaceAll(/* @__PURE__ */ new RegExp("[^a-z0-9\\s\\-]", "gv"), "").trim().replaceAll(/* @__PURE__ */ new RegExp("[\\s\\-]+", "gv"), "-").replaceAll(/* @__PURE__ */ new RegExp("^-+|-+$", "gv"), "");
50
+
43
51
  //#endregion
44
52
  //#region src/lib/post-url.ts
45
53
  const createPostUrl = (categorySlugMap, defaultCategory) => {
@@ -69,6 +77,7 @@ const createPostUrl = (categorySlugMap, defaultCategory) => {
69
77
  postUrl
70
78
  };
71
79
  };
80
+
72
81
  //#endregion
73
82
  //#region src/lib/reading-time.ts
74
83
  const WORDS_PER_MINUTE = 200;
@@ -80,6 +89,7 @@ const readingTime = (input) => {
80
89
  words
81
90
  };
82
91
  };
92
+
83
93
  //#endregion
84
94
  //#region src/lib/index.ts
85
95
  var lib_exports = /* @__PURE__ */ __exportAll({
@@ -90,7 +100,7 @@ var lib_exports = /* @__PURE__ */ __exportAll({
90
100
  readingTime: () => readingTime,
91
101
  slugify: () => slugify
92
102
  });
103
+
93
104
  //#endregion
94
105
  export { getRelatedPosts as a, slugify as i, readingTime as n, formatDate as o, createPostUrl as r, lib_exports as t };
95
-
96
- //# sourceMappingURL=lib-Dn4hRxdC.js.map
106
+ //# sourceMappingURL=lib-mtnJ1lv8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib-mtnJ1lv8.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/reading-time.ts","../src/lib/index.ts"],"sourcesContent":["const defaultOptions: Intl.DateTimeFormatOptions = {\n day: \"numeric\",\n month: \"long\",\n timeZone: \"UTC\",\n year: \"numeric\",\n};\n\n// Hoisted so the default-arg call (the hot path) skips the JSON.stringify cache key.\nconst defaultFormatter = new Intl.DateTimeFormat(\"en-US\", defaultOptions);\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nconst getFormatter = (locale: string, options: Intl.DateTimeFormatOptions) => {\n const key = `${locale}:${JSON.stringify(options)}`;\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n};\n\nconst formatDate = (\n date: Date,\n locale: string = \"en-US\",\n options: Intl.DateTimeFormatOptions = defaultOptions,\n) => {\n const isDefaultCall = locale === \"en-US\" && options === defaultOptions;\n if (isDefaultCall) {\n return defaultFormatter.format(date);\n }\n return getFormatter(locale, { ...defaultOptions, ...options }).format(date);\n};\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; id: string };\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {\n const currentTagSet = new Set(current.data.tags);\n const scored: Array<{ post: T; score: number }> = [];\n\n for (const post of all) {\n if (post.id === current.id) {\n continue;\n }\n let score = 0;\n for (const tag of post.data.tags) {\n if (currentTagSet.has(tag)) {\n score++;\n }\n }\n scored.push({ post, score });\n }\n\n return scored\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n};\n\nexport { getRelatedPosts };\n","const slugify = (input: string) =>\n input\n .normalize(\"NFD\")\n .replaceAll(/[̀-ͯ]/gv, \"\")\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s\\-]/gv, \"\")\n .trim()\n .replaceAll(/[\\s\\-]+/gv, \"-\")\n .replaceAll(/^-+|-+$/gv, \"\");\n\nexport { slugify };\n","import { slugify } from \"./slugify\";\n\ntype PostLike = {\n data: {\n categories?: Array<string>;\n seo?: { canonical_url?: string };\n };\n id: string;\n};\n\ntype PostParams = { category: string; slug: string };\n\ntype PostUrlBuilder = {\n postParams: (post: PostLike) => PostParams;\n postUrl: (post: PostLike) => string;\n};\n\n// Per-blog URL builder. Each blog supplies its own pt-BR -> slug map\n// (`category-slugs.json`) and the catch-all category used when the post has\n// no categories. The canonical_url short-circuit lets WordPress migrations\n// keep their original URL pair while everything else falls back to a slugified\n// derived category + Astro entry id.\nconst createPostUrl = (\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n): PostUrlBuilder => {\n const postParams = (post: PostLike): PostParams => {\n const canonical = post.data.seo?.canonical_url;\n if (canonical) {\n try {\n const segments = new URL(canonical).pathname.split(\"/\").filter(Boolean);\n const category = segments.at(-2);\n const slug = segments.at(-1);\n if (category && slug) {\n return { category, slug };\n }\n } catch {\n // Malformed canonical_url — fall through to category-derived logic.\n }\n }\n const first = post.data.categories?.at(0);\n const category = first ? (categorySlugMap[first] ?? slugify(first)) : defaultCategory;\n return { category, slug: post.id };\n };\n\n const postUrl = (post: PostLike): string => {\n const { category, slug } = postParams(post);\n return `/${category}/${slug}/`;\n };\n\n return { postParams, postUrl };\n};\n\nexport type { PostLike, PostParams, PostUrlBuilder };\nexport { createPostUrl };\n","const WORDS_PER_MINUTE = 200;\n\nconst readingTime = (input: string) => {\n const stripped = input\n .replaceAll(/<[^>]*>/gv, \"\")\n .replaceAll(/[#*_`>\\-]/gv, \" \")\n .replaceAll(/\\s+/gv, \" \")\n .trim();\n const words = stripped.length === 0 ? 0 : stripped.split(\" \").length;\n const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n return { minutes, words };\n};\n\nexport { readingTime };\n","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\nexport type { PostLike, PostParams, PostUrlBuilder } from \"./post-url\";\nexport { createPostUrl } from \"./post-url\";\nexport { readingTime } from \"./reading-time\";\nexport { slugify } from \"./slugify\";\n"],"mappings":";;;;AAAA,MAAM,iBAA6C;CACjD,KAAK;CACL,OAAO;CACP,UAAU;CACV,MAAM;AACR;AAGA,MAAM,mBAAmB,IAAI,KAAK,eAAe,SAAS,cAAc;AAExE,MAAM,iCAAiB,IAAI,IAAiC;AAE5D,MAAM,gBAAgB,QAAgB,YAAwC;CAC5E,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,UAAU,OAAO;CAC/C,IAAI,YAAY,eAAe,IAAI,GAAG;CACtC,IAAI,CAAC,WAAW;EACd,YAAY,IAAI,KAAK,eAAe,QAAQ,OAAO;EACnD,eAAe,IAAI,KAAK,SAAS;CACnC;CACA,OAAO;AACT;AAEA,MAAM,cACJ,MACA,SAAiB,SACjB,UAAsC,mBACnC;CAEH,IADsB,WAAW,WAAW,YAAY,gBAEtD,OAAO,iBAAiB,OAAO,IAAI;CAErC,OAAO,aAAa,QAAQ;EAAE,GAAG;EAAgB,GAAG;CAAQ,CAAC,CAAC,CAAC,OAAO,IAAI;AAC5E;;;;AC9BA,MAAM,mBAAsC,SAAY,KAAe,MAAc;CACnF,MAAM,gBAAgB,IAAI,IAAI,QAAQ,KAAK,IAAI;CAC/C,MAAM,SAA4C,CAAC;CAEnD,KAAK,MAAM,QAAQ,KAAK;EACtB,IAAI,KAAK,OAAO,QAAQ,IACtB;EAEF,IAAI,QAAQ;EACZ,KAAK,MAAM,OAAO,KAAK,KAAK,MAC1B,IAAI,cAAc,IAAI,GAAG,GACvB;EAGJ,OAAO,KAAK;GAAE;GAAM;EAAM,CAAC;CAC7B;CAEA,OAAO,OACJ,UAAU,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CACrC,MAAM,GAAG,CAAC,CAAC,CACX,KAAK,EAAE,WAAW,IAAI;AAC3B;;;;ACvBA,MAAM,WAAW,UACf,MACG,UAAU,KAAK,CAAC,CAChB,2BAAW,wBAAQ,GAAG,EAAE,CAAC,CACzB,YAAY,CAAC,CACb,2BAAW,kCAAgB,GAAG,EAAE,CAAC,CACjC,KAAK,CAAC,CACN,2BAAW,4BAAU,GAAG,GAAG,CAAC,CAC5B,2BAAW,0BAAU,GAAG,EAAE;;;;ACc/B,MAAM,iBACJ,iBACA,oBACmB;CACnB,MAAM,cAAc,SAA+B;EACjD,MAAM,YAAY,KAAK,KAAK,KAAK;EACjC,IAAI,WACF,IAAI;GACF,MAAM,WAAW,IAAI,IAAI,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;GACtE,MAAM,WAAW,SAAS,GAAG,EAAE;GAC/B,MAAM,OAAO,SAAS,GAAG,EAAE;GAC3B,IAAI,YAAY,MACd,OAAO;IAAE;IAAU;GAAK;EAE5B,QAAQ,CAER;EAEF,MAAM,QAAQ,KAAK,KAAK,YAAY,GAAG,CAAC;EAExC,OAAO;GAAE,UADQ,QAAS,gBAAgB,UAAU,QAAQ,KAAK,IAAK;GACnD,MAAM,KAAK;EAAG;CACnC;CAEA,MAAM,WAAW,SAA2B;EAC1C,MAAM,EAAE,UAAU,SAAS,WAAW,IAAI;EAC1C,OAAO,IAAI,SAAS,GAAG,KAAK;CAC9B;CAEA,OAAO;EAAE;EAAY;CAAQ;AAC/B;;;;ACnDA,MAAM,mBAAmB;AAEzB,MAAM,eAAe,UAAkB;CACrC,MAAM,WAAW,MACd,2BAAW,0BAAU,GAAG,EAAE,CAAC,CAC3B,2BAAW,6BAAY,GAAG,GAAG,CAAC,CAC9B,2BAAW,uBAAM,GAAG,GAAG,CAAC,CACxB,KAAK;CACR,MAAM,QAAQ,SAAS,WAAW,IAAI,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC;CAE9D,OAAO;EAAE,SADO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,gBAAgB,CAC/C;EAAG;CAAM;AAC1B"}
@@ -0,0 +1,18 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) {
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ }
11
+ if (!no_symbols) {
12
+ __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ }
14
+ return target;
15
+ };
16
+
17
+ //#endregion
18
+ export { __exportAll as t };
@@ -1,4 +1,5 @@
1
1
  import plugin from "tailwindcss/plugin";
2
+
2
3
  //#region src/tailwind-preset.ts
3
4
  const awesomenessPreset = plugin(({ addUtilities, addVariant }) => {
4
5
  addVariant("dark", "&:is(.dark *)");
@@ -7,7 +8,7 @@ const awesomenessPreset = plugin(({ addUtilities, addVariant }) => {
7
8
  ".icon-inline-start": { marginInlineEnd: "0.375rem" }
8
9
  });
9
10
  });
11
+
10
12
  //#endregion
11
13
  export { awesomenessPreset as default };
12
-
13
14
  //# sourceMappingURL=tailwind-preset.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tailwind-preset.js","names":[],"sources":["../src/tailwind-preset.ts"],"sourcesContent":["import plugin from \"tailwindcss/plugin\";\n\nconst awesomenessPreset = plugin(({ addUtilities, addVariant }) => {\n addVariant(\"dark\", \"&:is(.dark *)\");\n\n addUtilities({\n \".icon-inline-end\": { marginInlineStart: \"0.375rem\" },\n \".icon-inline-start\": { marginInlineEnd: \"0.375rem\" },\n });\n});\n\nexport default awesomenessPreset;\n"],"mappings":";;AAEA,MAAM,oBAAoB,QAAQ,EAAE,cAAc,iBAAiB;CACjE,WAAW,QAAQ,eAAe;CAElC,aAAa;EACX,oBAAoB,EAAE,mBAAmB,WAAW;EACpD,sBAAsB,EAAE,iBAAiB,WAAW;CACtD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"tailwind-preset.js","names":[],"sources":["../src/tailwind-preset.ts"],"sourcesContent":["import plugin from \"tailwindcss/plugin\";\n\nconst awesomenessPreset = plugin(({ addUtilities, addVariant }) => {\n addVariant(\"dark\", \"&:is(.dark *)\");\n\n addUtilities({\n \".icon-inline-end\": { marginInlineStart: \"0.375rem\" },\n \".icon-inline-start\": { marginInlineEnd: \"0.375rem\" },\n });\n});\n\nexport default awesomenessPreset;\n"],"mappings":";;;AAEA,MAAM,oBAAoB,QAAQ,EAAE,cAAc,iBAAiB;CACjE,WAAW,QAAQ,eAAe;CAElC,aAAa;EACX,oBAAoB,EAAE,mBAAmB,WAAW;EACpD,sBAAsB,EAAE,iBAAiB,WAAW;CACtD,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "astro-awesomeness",
3
- "version": "0.5.0",
4
- "description": "Astro 6 blog theme with shadcn primitives and a Tailwind v4 preset.",
3
+ "version": "0.7.0",
4
+ "description": "Astro 6/7 blog theme with shadcn primitives and a Tailwind v4 preset.",
5
5
  "keywords": [
6
6
  "astro",
7
7
  "astro-theme",
@@ -52,29 +52,28 @@
52
52
  "access": "public"
53
53
  },
54
54
  "dependencies": {
55
- "@base-ui/react": "^1.5.0",
55
+ "@base-ui/react": "^1.6.0",
56
56
  "class-variance-authority": "^0.7.1",
57
- "clsx": "^2.1.1",
58
57
  "cmdk": "^1.1.1",
59
- "lucide-react": "^1.17.0",
60
- "tailwind-merge": "^3.6.0",
58
+ "cnfast": "^0.0.8",
59
+ "lucide-react": "^1.23.0",
61
60
  "tw-animate-css": "^1.4.0",
62
61
  "zod": "^4.4.3"
63
62
  },
64
63
  "devDependencies": {
65
- "@tailwindcss/postcss": "^4.3.0",
66
- "@types/node": "^24.13.1",
64
+ "@tailwindcss/postcss": "^4.3.2",
65
+ "@types/node": "^26.1.0",
67
66
  "@types/react": "^19.2.17",
68
67
  "@types/react-dom": "^19.2.3",
69
- "@vitest/coverage-v8": "^4.1.8",
70
- "tailwindcss": "^4.3.0",
68
+ "@vitest/coverage-v8": "^4.1.9",
69
+ "tailwindcss": "^4.3.2",
71
70
  "typescript": "^6.0.3",
72
- "vitest": "^4.1.8",
71
+ "vitest": "^4.1.9",
73
72
  "@repo/config-vitest": "0.0.0",
74
73
  "@repo/typescript-config": "0.0.0"
75
74
  },
76
75
  "peerDependencies": {
77
- "astro": "^6.0.0",
76
+ "astro": "^6.0.0 || ^7.0.0",
78
77
  "react": "^19.0.0",
79
78
  "react-dom": "^19.0.0",
80
79
  "tailwindcss": "^4.0.0"
@@ -24,7 +24,7 @@ const {
24
24
  </a>
25
25
  <nav class="flex items-center gap-2">
26
26
  <slot name="nav" />
27
- {showThemeToggle && <ThemeToggle client:load ariaLabel={themeToggleLabel} />}
27
+ {showThemeToggle && <ThemeToggle client:idle ariaLabel={themeToggleLabel} />}
28
28
  </nav>
29
29
  </div>
30
30
  </header>
@@ -8,11 +8,12 @@ type PostEntry = { id: string; data: Post };
8
8
  type Props = {
9
9
  post: PostEntry;
10
10
  locale?: string;
11
+ postHref?: (post: PostEntry) => string;
11
12
  };
12
13
 
13
- const { post, locale } = Astro.props;
14
+ const { post, locale, postHref } = Astro.props;
14
15
  const { title, description, pubDate, tags } = post.data;
15
- const href = `/blog/${post.id}`;
16
+ const href = postHref ? postHref(post) : `/blog/${post.id}`;
16
17
  ---
17
18
  <article class="post-card border-border group border-b py-6 last:border-0">
18
19
  <a href={href} class="block">
@@ -5,9 +5,23 @@ type Props = {
5
5
  canonical?: string;
6
6
  image?: string;
7
7
  type?: "website" | "article";
8
+ siteName?: string;
9
+ locale?: string;
10
+ publishedTime?: Date;
11
+ modifiedTime?: Date;
8
12
  };
9
13
 
10
- const { title, description, canonical, image, type = "website" } = Astro.props;
14
+ const {
15
+ title,
16
+ description,
17
+ canonical,
18
+ image,
19
+ type = "website",
20
+ siteName,
21
+ locale = "en_US",
22
+ publishedTime,
23
+ modifiedTime,
24
+ } = Astro.props;
11
25
  const url = canonical ?? new URL(Astro.url.pathname, Astro.site ?? Astro.url.origin).toString();
12
26
  ---
13
27
  <title>{title}</title>
@@ -18,6 +32,14 @@ const url = canonical ?? new URL(Astro.url.pathname, Astro.site ?? Astro.url.ori
18
32
  <meta property="og:title" content={title} />
19
33
  <meta property="og:description" content={description} />
20
34
  <meta property="og:url" content={url} />
35
+ {siteName && <meta property="og:site_name" content={siteName} />}
36
+ <meta property="og:locale" content={locale} />
37
+ {type === "article" && publishedTime && (
38
+ <meta property="article:published_time" content={publishedTime.toISOString()} />
39
+ )}
40
+ {type === "article" && modifiedTime && (
41
+ <meta property="article:modified_time" content={modifiedTime.toISOString()} />
42
+ )}
21
43
  {image && <meta property="og:image" content={image} />}
22
44
 
23
45
  <meta name="twitter:card" content={image ? "summary_large_image" : "summary"} />
@@ -9,7 +9,7 @@ import {
9
9
  CommandItem,
10
10
  CommandList,
11
11
  } from "./ui/command";
12
- import { Dialog, DialogContent, DialogTrigger } from "./ui/dialog";
12
+ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog";
13
13
  import { Button } from "./ui/button";
14
14
 
15
15
  type CommandMenuItem = { description?: string; title: string; url: string };
@@ -19,6 +19,7 @@ type CommandMenuProps = {
19
19
  groupHeading?: string;
20
20
  items: Array<CommandMenuItem>;
21
21
  placeholder?: string;
22
+ title?: string;
22
23
  };
23
24
 
24
25
  const CommandMenu = ({
@@ -26,6 +27,7 @@ const CommandMenu = ({
26
27
  groupHeading = "Posts",
27
28
  items,
28
29
  placeholder = "Search posts…",
30
+ title = "Search posts",
29
31
  }: CommandMenuProps) => {
30
32
  const [open, setOpen] = useState(false);
31
33
 
@@ -48,6 +50,7 @@ const CommandMenu = ({
48
50
  <Search />
49
51
  </DialogTrigger>
50
52
  <DialogContent className="p-0">
53
+ <DialogTitle className="sr-only">{title}</DialogTitle>
51
54
  <Command>
52
55
  <CommandInput placeholder={placeholder} />
53
56
  <CommandList>
@@ -1,4 +1,5 @@
1
- export { Button, buttonVariants } from "./ui/button";
1
+ export { Button } from "./ui/button";
2
+ export { buttonVariants } from "./ui/button-variants";
2
3
  export {
3
4
  Command,
4
5
  CommandEmpty,
@@ -1,5 +1,5 @@
1
1
  import { Moon, Sun } from "lucide-react";
2
- import { useState, useSyncExternalStore } from "react";
2
+ import { useSyncExternalStore } from "react";
3
3
 
4
4
  import { Button } from "./ui/button";
5
5
 
@@ -16,6 +16,28 @@ const getStoredTheme = (): Theme | null => {
16
16
  return null;
17
17
  };
18
18
 
19
+ const storedThemeListeners = new Set<() => void>();
20
+
21
+ const subscribeStoredTheme = (callback: () => void) => {
22
+ if (typeof window === "undefined") {
23
+ return () => {};
24
+ }
25
+ storedThemeListeners.add(callback);
26
+ // "storage" only fires in other tabs; same-tab writes notify via setStoredTheme.
27
+ window.addEventListener("storage", callback);
28
+ return () => {
29
+ storedThemeListeners.delete(callback);
30
+ window.removeEventListener("storage", callback);
31
+ };
32
+ };
33
+
34
+ const setStoredTheme = (next: Theme) => {
35
+ window.localStorage.setItem("theme", next);
36
+ for (const listener of storedThemeListeners) {
37
+ listener();
38
+ }
39
+ };
40
+
19
41
  const getPrefersDark = (): boolean => {
20
42
  if (typeof window === "undefined") {
21
43
  return false;
@@ -39,16 +61,19 @@ type Props = {
39
61
  };
40
62
 
41
63
  const ThemeToggle = ({ ariaLabel = "Toggle theme" }: Props) => {
42
- // useSyncExternalStore avoids hydration mismatch on system preference reads.
64
+ // useSyncExternalStore avoids hydration mismatch on both reads: the server
65
+ // snapshots (false / null) match the server HTML, then React re-syncs to the
66
+ // real values right after hydration. Reading localStorage during the initial
67
+ // render instead (e.g. in a useState initializer) makes React 19 log a
68
+ // recoverable hydration error for every visitor with a stored theme.
43
69
  const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);
44
- const [override, setOverride] = useState<Theme | null>(() => getStoredTheme());
70
+ const override = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null);
45
71
  const theme: Theme = override ?? (prefersDark ? "dark" : "light");
46
72
 
47
73
  const handleToggleTheme = () => {
48
74
  const next: Theme = theme === "dark" ? "light" : "dark";
49
- window.localStorage.setItem("theme", next);
50
75
  document.documentElement.classList.toggle("dark", next === "dark");
51
- setOverride(next);
76
+ setStoredTheme(next);
52
77
  };
53
78
 
54
79
  return (
@@ -0,0 +1,35 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ const buttonVariants = cva(
4
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
5
+ {
6
+ defaultVariants: {
7
+ size: "default",
8
+ variant: "default",
9
+ },
10
+ variants: {
11
+ size: {
12
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
13
+ icon: "size-9",
14
+ "icon-lg": "size-10",
15
+ "icon-sm": "size-8",
16
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
17
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
18
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
19
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
20
+ },
21
+ variant: {
22
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
23
+ destructive:
24
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
25
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
26
+ link: "text-primary underline-offset-4 hover:underline",
27
+ outline:
28
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
29
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
30
+ },
31
+ },
32
+ },
33
+ );
34
+
35
+ export { buttonVariants };
@@ -1,60 +1,26 @@
1
1
  import { Button as ButtonPrimitive } from "@base-ui/react/button";
2
- import { cva, type VariantProps } from "class-variance-authority";
2
+ import type { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
 
5
5
  import { cn } from "../../lib/cn";
6
+ import { buttonVariants } from "./button-variants";
6
7
 
7
- const buttonVariants = cva(
8
- "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
- {
10
- defaultVariants: {
11
- size: "default",
12
- variant: "default",
13
- },
14
- variants: {
15
- size: {
16
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
17
- icon: "size-9",
18
- "icon-lg": "size-10",
19
- "icon-sm": "size-8",
20
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
21
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
22
- sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
23
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
24
- },
25
- variant: {
26
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
27
- destructive:
28
- "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
29
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
30
- link: "text-primary underline-offset-4 hover:underline",
31
- outline:
32
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
33
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
34
- },
35
- },
36
- },
37
- );
38
-
39
- type ButtonProps = Omit<React.ComponentPropsWithoutRef<typeof ButtonPrimitive>, "className"> &
8
+ type ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, "className"> &
40
9
  VariantProps<typeof buttonVariants> & {
41
10
  className?: string;
42
11
  };
43
12
 
44
- const Button = React.forwardRef<React.ComponentRef<typeof ButtonPrimitive>, ButtonProps>(
45
- ({ className, size = "default", variant = "default", ...props }, ref) => {
46
- return (
47
- <ButtonPrimitive
48
- className={cn(buttonVariants({ className, size, variant }))}
49
- data-size={size}
50
- data-slot="button"
51
- data-variant={variant}
52
- ref={ref}
53
- {...props}
54
- />
55
- );
56
- },
57
- );
58
- Button.displayName = "Button";
13
+ // React 19 passes `ref` as a regular prop, so no forwardRef wrapper is needed.
14
+ const Button = ({ className, size = "default", variant = "default", ...props }: ButtonProps) => {
15
+ return (
16
+ <ButtonPrimitive
17
+ className={cn(buttonVariants({ className, size, variant }))}
18
+ data-size={size}
19
+ data-slot="button"
20
+ data-variant={variant}
21
+ {...props}
22
+ />
23
+ );
24
+ };
59
25
 
60
- export { Button, buttonVariants };
26
+ export { Button };
@@ -34,14 +34,15 @@ function CommandDialog({
34
34
  }) {
35
35
  return (
36
36
  <Dialog {...props}>
37
- <DialogHeader className="sr-only">
38
- <DialogTitle>{title}</DialogTitle>
39
- <DialogDescription>{description}</DialogDescription>
40
- </DialogHeader>
41
37
  <DialogContent
42
38
  className={cn("overflow-hidden p-0", className)}
43
39
  showCloseButton={showCloseButton}
44
40
  >
41
+ {/* Inside DialogContent so the sr-only name renders in the portaled popup, not the page. */}
42
+ <DialogHeader className="sr-only">
43
+ <DialogTitle>{title}</DialogTitle>
44
+ <DialogDescription>{description}</DialogDescription>
45
+ </DialogHeader>
45
46
  <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
46
47
  {children}
47
48
  </Command>
@@ -59,7 +59,7 @@ function DialogContent({
59
59
  {children}
60
60
  {showCloseButton && (
61
61
  <DialogPrimitive.Close
62
- className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
62
+ className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
63
63
  data-slot="dialog-close"
64
64
  type="button"
65
65
  >
@@ -11,6 +11,11 @@ type Props = {
11
11
  type?: "website" | "article";
12
12
  lang?: string;
13
13
  siteTitle?: string;
14
+ rssHref?: string;
15
+ siteName?: string;
16
+ locale?: string;
17
+ publishedTime?: Date;
18
+ modifiedTime?: Date;
14
19
  };
15
20
 
16
21
  const {
@@ -21,6 +26,11 @@ const {
21
26
  type = "website",
22
27
  lang = "en",
23
28
  siteTitle,
29
+ rssHref,
30
+ siteName,
31
+ locale,
32
+ publishedTime,
33
+ modifiedTime,
24
34
  } = Astro.props;
25
35
  ---
26
36
  <!doctype html>
@@ -30,7 +40,20 @@ const {
30
40
  <meta name="viewport" content="width=device-width, initial-scale=1" />
31
41
  <meta name="color-scheme" content="light dark" />
32
42
  <meta name="generator" content={Astro.generator} />
33
- <Seo title={title} description={description} canonical={canonical} image={image} type={type} />
43
+ <Seo
44
+ title={title}
45
+ description={description}
46
+ canonical={canonical}
47
+ image={image}
48
+ type={type}
49
+ siteName={siteName ?? siteTitle}
50
+ locale={locale}
51
+ publishedTime={publishedTime}
52
+ modifiedTime={modifiedTime}
53
+ />
54
+ {rssHref && (
55
+ <link rel="alternate" type="application/rss+xml" title={siteTitle ?? title} href={rssHref} />
56
+ )}
34
57
  <script is:inline>
35
58
  (() => {
36
59
  const stored = localStorage.getItem("theme");
@@ -50,6 +73,12 @@ const {
50
73
  <slot name="head" />
51
74
  </head>
52
75
  <body class="bg-background text-foreground min-h-dvh flex flex-col">
76
+ <a
77
+ href="#main-content"
78
+ class="bg-background text-foreground border-border focus:ring-ring sr-only z-50 rounded-md border px-3 py-2 focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:ring-2"
79
+ >
80
+ Skip to content
81
+ </a>
53
82
  {siteTitle && <Header siteTitle={siteTitle} />}
54
83
  <div class="flex-1">
55
84
  <slot />
@@ -5,26 +5,30 @@ import BaseLayout from "./base-layout.astro";
5
5
  import PostCard from "../astro/post-card.astro";
6
6
  import Pagination from "../astro/pagination.astro";
7
7
 
8
+ type PostEntry = { id: string; data: Post };
8
9
  type Props = {
9
10
  title: string;
10
11
  description: string;
11
- posts: Array<{ id: string; data: Post }>;
12
+ posts: Array<PostEntry>;
12
13
  page?: { currentPage: number; lastPage: number; url: { prev?: string; next?: string } };
13
14
  urlForPage?: (page: number) => string;
14
15
  locale?: string;
15
16
  siteTitle?: string;
17
+ rssHref?: string;
18
+ postHref?: (post: PostEntry) => string;
16
19
  };
17
20
 
18
- const { title, description, posts, page, urlForPage, locale, siteTitle } = Astro.props;
21
+ const { title, description, posts, page, urlForPage, locale, siteTitle, rssHref, postHref } =
22
+ Astro.props;
19
23
  ---
20
- <BaseLayout title={title} description={description} siteTitle={siteTitle}>
21
- <main class="mx-auto max-w-3xl px-4 py-10">
24
+ <BaseLayout title={title} description={description} siteTitle={siteTitle} rssHref={rssHref}>
25
+ <main id="main-content" tabindex="-1" class="mx-auto max-w-3xl px-4 py-10">
22
26
  <header class="mb-8">
23
27
  <h1 class="text-primary text-3xl font-bold tracking-tight">{title}</h1>
24
28
  <p class="text-muted-foreground mt-2">{description}</p>
25
29
  </header>
26
30
  <section>
27
- {posts.map((post) => <PostCard post={post} locale={locale} />)}
31
+ {posts.map((post) => <PostCard post={post} locale={locale} postHref={postHref} />)}
28
32
  </section>
29
33
  {page && urlForPage && (
30
34
  <Pagination
@@ -6,19 +6,47 @@ import FormattedDate from "../astro/formatted-date.astro";
6
6
  import ReadingTime from "../astro/reading-time.astro";
7
7
  import TagList from "../astro/tag-list.astro";
8
8
  import Prose from "../astro/prose.astro";
9
+ import TableOfContents from "../astro/table-of-contents.astro";
10
+ import PostCard from "../astro/post-card.astro";
9
11
 
12
+ type PostEntry = { id: string; data: Post };
10
13
  type Props = {
11
14
  post: { id: string; data: Post; body?: string };
12
15
  locale?: string;
13
16
  siteTitle?: string;
14
17
  updatedLabel?: string;
18
+ rssHref?: string;
19
+ headings?: Array<{ depth: number; text: string; slug?: string }>;
20
+ relatedPosts?: Array<PostEntry>;
21
+ relatedHeading?: string;
22
+ postHref?: (post: PostEntry) => string;
15
23
  };
16
24
 
17
- const { post, locale, siteTitle, updatedLabel = "Updated" } = Astro.props;
25
+ const {
26
+ post,
27
+ locale,
28
+ siteTitle,
29
+ updatedLabel = "Updated",
30
+ rssHref,
31
+ headings,
32
+ relatedPosts,
33
+ relatedHeading = "Related posts",
34
+ postHref,
35
+ } = Astro.props;
18
36
  const { title, description, pubDate, updatedDate, tags } = post.data;
19
37
  ---
20
- <BaseLayout title={title} description={description} type="article" siteTitle={siteTitle}>
21
- <article class="mx-auto max-w-3xl px-4 py-10">
38
+ <BaseLayout
39
+ title={title}
40
+ description={description}
41
+ type="article"
42
+ siteTitle={siteTitle}
43
+ siteName={siteTitle}
44
+ rssHref={rssHref}
45
+ publishedTime={pubDate}
46
+ modifiedTime={updatedDate}
47
+ >
48
+ <main id="main-content" tabindex="-1">
49
+ <article class="mx-auto max-w-3xl px-4 py-10">
22
50
  <header class="mb-8">
23
51
  <h1 class="text-primary text-3xl font-bold tracking-tight sm:text-4xl">{title}</h1>
24
52
  <div class="text-muted-foreground mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
@@ -35,8 +63,22 @@ const { title, description, pubDate, updatedDate, tags } = post.data;
35
63
  {tags.length > 0 && <TagList tags={tags} class="mt-4" />}
36
64
  </header>
37
65
 
66
+ {headings && headings.length > 0 && <TableOfContents headings={headings} class="mb-8" />}
67
+
38
68
  <Prose>
39
69
  <slot />
40
70
  </Prose>
41
- </article>
71
+
72
+ {relatedPosts && relatedPosts.length > 0 && (
73
+ <section aria-labelledby="related-posts-heading" class="mt-12 border-border border-t pt-8">
74
+ <h2 id="related-posts-heading" class="text-primary text-2xl font-bold tracking-tight">
75
+ {relatedHeading}
76
+ </h2>
77
+ {relatedPosts.map((related) => (
78
+ <PostCard post={related} locale={locale} postHref={postHref} />
79
+ ))}
80
+ </section>
81
+ )}
82
+ </article>
83
+ </main>
42
84
  </BaseLayout>
package/src/lib/cn.ts CHANGED
@@ -1,6 +1 @@
1
- import { type ClassValue, clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- const cn = (...inputs: Array<ClassValue>) => twMerge(clsx(inputs));
5
-
6
- export { cn };
1
+ export { cn } from "cnfast";
@@ -5,6 +5,9 @@ const defaultOptions: Intl.DateTimeFormatOptions = {
5
5
  year: "numeric",
6
6
  };
7
7
 
8
+ // Hoisted so the default-arg call (the hot path) skips the JSON.stringify cache key.
9
+ const defaultFormatter = new Intl.DateTimeFormat("en-US", defaultOptions);
10
+
8
11
  const formatterCache = new Map<string, Intl.DateTimeFormat>();
9
12
 
10
13
  const getFormatter = (locale: string, options: Intl.DateTimeFormatOptions) => {
@@ -21,6 +24,12 @@ const formatDate = (
21
24
  date: Date,
22
25
  locale: string = "en-US",
23
26
  options: Intl.DateTimeFormatOptions = defaultOptions,
24
- ) => getFormatter(locale, { ...defaultOptions, ...options }).format(date);
27
+ ) => {
28
+ const isDefaultCall = locale === "en-US" && options === defaultOptions;
29
+ if (isDefaultCall) {
30
+ return defaultFormatter.format(date);
31
+ }
32
+ return getFormatter(locale, { ...defaultOptions, ...options }).format(date);
33
+ };
25
34
 
26
35
  export { formatDate };
@@ -2,30 +2,36 @@ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { getRelatedPosts } from "./get-related-posts";
4
4
 
5
- type TestPost = { data: { tags: Array<string> }; slug: string };
5
+ type TestPost = { data: { tags: Array<string> }; id: string };
6
6
 
7
- const a: TestPost = { data: { tags: ["x", "y"] }, slug: "a" };
8
- const b: TestPost = { data: { tags: ["x"] }, slug: "b" };
9
- const c: TestPost = { data: { tags: ["z"] }, slug: "c" };
10
- const d: TestPost = { data: { tags: ["x", "y", "z"] }, slug: "d" };
7
+ const a: TestPost = { data: { tags: ["x", "y"] }, id: "a" };
8
+ const b: TestPost = { data: { tags: ["x"] }, id: "b" };
9
+ const c: TestPost = { data: { tags: ["z"] }, id: "c" };
10
+ const d: TestPost = { data: { tags: ["x", "y", "z"] }, id: "d" };
11
11
 
12
12
  describe("getRelatedPosts", () => {
13
13
  it("excludes the current post", () => {
14
14
  const related = getRelatedPosts(a, [a, b, c, d], 3);
15
- expect(related.find((p) => p.slug === "a")).toBeUndefined();
15
+ expect(related.find((p) => p.id === "a")).toBeUndefined();
16
16
  });
17
17
  it("ranks by shared tag count (desc)", () => {
18
18
  const related = getRelatedPosts(a, [a, b, c, d], 3);
19
- expect(related.map((p) => p.slug)).toEqual(["d", "b", "c"]);
19
+ expect(related.map((p) => p.id)).toEqual(["d", "b", "c"]);
20
20
  });
21
21
  it("limits to n", () => {
22
22
  const related = getRelatedPosts(a, [a, b, c, d], 1);
23
23
  expect(related).toHaveLength(1);
24
- expect(related[0].slug).toBe("d");
24
+ expect(related[0].id).toBe("d");
25
25
  });
26
26
  it("falls back to other posts when no overlap exists", () => {
27
- const lonely: TestPost = { data: { tags: ["nothing"] }, slug: "lonely" };
27
+ const lonely: TestPost = { data: { tags: ["nothing"] }, id: "lonely" };
28
28
  const related = getRelatedPosts(lonely, [lonely, b, c], 2);
29
29
  expect(related).toHaveLength(2);
30
30
  });
31
+ it("works with Astro-entry-shaped objects keyed on id (regression)", () => {
32
+ const current = { data: { tags: ["x"] }, id: "current" };
33
+ const other = { data: { tags: ["x"] }, id: "other" };
34
+ const related = getRelatedPosts(current, [current, other], 5);
35
+ expect(related.map((p) => p.id)).toEqual(["other"]);
36
+ });
31
37
  });
@@ -1,11 +1,11 @@
1
- type HasTags = { data: { tags: Array<string> }; slug: string };
1
+ type HasTags = { data: { tags: Array<string> }; id: string };
2
2
 
3
3
  const getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {
4
4
  const currentTagSet = new Set(current.data.tags);
5
5
  const scored: Array<{ post: T; score: number }> = [];
6
6
 
7
7
  for (const post of all) {
8
- if (post.slug === current.slug) {
8
+ if (post.id === current.id) {
9
9
  continue;
10
10
  }
11
11
  let score = 0;
@@ -1,13 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __defProp = Object.defineProperty;
3
- var __exportAll = (all, no_symbols) => {
4
- let target = {};
5
- for (var name in all) __defProp(target, name, {
6
- get: all[name],
7
- enumerable: true
8
- });
9
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
- return target;
11
- };
12
- //#endregion
13
- export { __exportAll as t };
@@ -1,8 +0,0 @@
1
- import { clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
- //#region src/lib/cn.ts
4
- const cn = (...inputs) => twMerge(clsx(inputs));
5
- //#endregion
6
- export { cn as t };
7
-
8
- //# sourceMappingURL=cn-C_hacsED.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cn-C_hacsED.js","names":[],"sources":["../src/lib/cn.ts"],"sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nconst cn = (...inputs: Array<ClassValue>) => twMerge(clsx(inputs));\n\nexport { cn };\n"],"mappings":";;;AAGA,MAAM,MAAM,GAAG,WAA8B,QAAQ,KAAK,MAAM,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"lib-Dn4hRxdC.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/reading-time.ts","../src/lib/index.ts"],"sourcesContent":["const defaultOptions: Intl.DateTimeFormatOptions = {\n day: \"numeric\",\n month: \"long\",\n timeZone: \"UTC\",\n year: \"numeric\",\n};\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nconst getFormatter = (locale: string, options: Intl.DateTimeFormatOptions) => {\n const key = `${locale}:${JSON.stringify(options)}`;\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n};\n\nconst formatDate = (\n date: Date,\n locale: string = \"en-US\",\n options: Intl.DateTimeFormatOptions = defaultOptions,\n) => getFormatter(locale, { ...defaultOptions, ...options }).format(date);\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; slug: string };\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {\n const currentTagSet = new Set(current.data.tags);\n const scored: Array<{ post: T; score: number }> = [];\n\n for (const post of all) {\n if (post.slug === current.slug) {\n continue;\n }\n let score = 0;\n for (const tag of post.data.tags) {\n if (currentTagSet.has(tag)) {\n score++;\n }\n }\n scored.push({ post, score });\n }\n\n return scored\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n};\n\nexport { getRelatedPosts };\n","const slugify = (input: string) =>\n input\n .normalize(\"NFD\")\n .replaceAll(/[̀-ͯ]/gv, \"\")\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s\\-]/gv, \"\")\n .trim()\n .replaceAll(/[\\s\\-]+/gv, \"-\")\n .replaceAll(/^-+|-+$/gv, \"\");\n\nexport { slugify };\n","import { slugify } from \"./slugify\";\n\ntype PostLike = {\n data: {\n categories?: Array<string>;\n seo?: { canonical_url?: string };\n };\n id: string;\n};\n\ntype PostParams = { category: string; slug: string };\n\ntype PostUrlBuilder = {\n postParams: (post: PostLike) => PostParams;\n postUrl: (post: PostLike) => string;\n};\n\n// Per-blog URL builder. Each blog supplies its own pt-BR -> slug map\n// (`category-slugs.json`) and the catch-all category used when the post has\n// no categories. The canonical_url short-circuit lets WordPress migrations\n// keep their original URL pair while everything else falls back to a slugified\n// derived category + Astro entry id.\nconst createPostUrl = (\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n): PostUrlBuilder => {\n const postParams = (post: PostLike): PostParams => {\n const canonical = post.data.seo?.canonical_url;\n if (canonical) {\n try {\n const segments = new URL(canonical).pathname.split(\"/\").filter(Boolean);\n const category = segments.at(-2);\n const slug = segments.at(-1);\n if (category && slug) {\n return { category, slug };\n }\n } catch {\n // Malformed canonical_url — fall through to category-derived logic.\n }\n }\n const first = post.data.categories?.at(0);\n const category = first ? (categorySlugMap[first] ?? slugify(first)) : defaultCategory;\n return { category, slug: post.id };\n };\n\n const postUrl = (post: PostLike): string => {\n const { category, slug } = postParams(post);\n return `/${category}/${slug}/`;\n };\n\n return { postParams, postUrl };\n};\n\nexport type { PostLike, PostParams, PostUrlBuilder };\nexport { createPostUrl };\n","const WORDS_PER_MINUTE = 200;\n\nconst readingTime = (input: string) => {\n const stripped = input\n .replaceAll(/<[^>]*>/gv, \"\")\n .replaceAll(/[#*_`>\\-]/gv, \" \")\n .replaceAll(/\\s+/gv, \" \")\n .trim();\n const words = stripped.length === 0 ? 0 : stripped.split(\" \").length;\n const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n return { minutes, words };\n};\n\nexport { readingTime };\n","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\nexport type { PostLike, PostParams, PostUrlBuilder } from \"./post-url\";\nexport { createPostUrl } from \"./post-url\";\nexport { readingTime } from \"./reading-time\";\nexport { slugify } from \"./slugify\";\n"],"mappings":";;;AAAA,MAAM,iBAA6C;CACjD,KAAK;CACL,OAAO;CACP,UAAU;CACV,MAAM;AACR;AAEA,MAAM,iCAAiB,IAAI,IAAiC;AAE5D,MAAM,gBAAgB,QAAgB,YAAwC;CAC5E,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,UAAU,OAAO;CAC/C,IAAI,YAAY,eAAe,IAAI,GAAG;CACtC,IAAI,CAAC,WAAW;EACd,YAAY,IAAI,KAAK,eAAe,QAAQ,OAAO;EACnD,eAAe,IAAI,KAAK,SAAS;CACnC;CACA,OAAO;AACT;AAEA,MAAM,cACJ,MACA,SAAiB,SACjB,UAAsC,mBACnC,aAAa,QAAQ;CAAE,GAAG;CAAgB,GAAG;AAAQ,CAAC,CAAC,CAAC,OAAO,IAAI;;;ACrBxE,MAAM,mBAAsC,SAAY,KAAe,MAAc;CACnF,MAAM,gBAAgB,IAAI,IAAI,QAAQ,KAAK,IAAI;CAC/C,MAAM,SAA4C,CAAC;CAEnD,KAAK,MAAM,QAAQ,KAAK;EACtB,IAAI,KAAK,SAAS,QAAQ,MACxB;EAEF,IAAI,QAAQ;EACZ,KAAK,MAAM,OAAO,KAAK,KAAK,MAC1B,IAAI,cAAc,IAAI,GAAG,GACvB;EAGJ,OAAO,KAAK;GAAE;GAAM;EAAM,CAAC;CAC7B;CAEA,OAAO,OACJ,UAAU,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CACrC,MAAM,GAAG,CAAC,CAAC,CACX,KAAK,EAAE,WAAW,IAAI;AAC3B;;;ACvBA,MAAM,WAAW,UACf,MACG,UAAU,KAAK,CAAC,CAChB,2BAAW,IAAA,OAAA,SAAA,IAAQ,GAAG,EAAE,CAAC,CACzB,YAAY,CAAC,CACb,2BAAW,IAAA,OAAA,mBAAA,IAAgB,GAAG,EAAE,CAAC,CACjC,KAAK,CAAC,CACN,2BAAW,IAAA,OAAA,aAAA,IAAU,GAAG,GAAG,CAAC,CAC5B,2BAAW,IAAA,OAAA,WAAA,IAAU,GAAG,EAAE;;;ACc/B,MAAM,iBACJ,iBACA,oBACmB;CACnB,MAAM,cAAc,SAA+B;EACjD,MAAM,YAAY,KAAK,KAAK,KAAK;EACjC,IAAI,WACF,IAAI;GACF,MAAM,WAAW,IAAI,IAAI,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,CAAC,OAAO,OAAO;GACtE,MAAM,WAAW,SAAS,GAAG,EAAE;GAC/B,MAAM,OAAO,SAAS,GAAG,EAAE;GAC3B,IAAI,YAAY,MACd,OAAO;IAAE;IAAU;GAAK;EAE5B,QAAQ,CAER;EAEF,MAAM,QAAQ,KAAK,KAAK,YAAY,GAAG,CAAC;EAExC,OAAO;GAAE,UADQ,QAAS,gBAAgB,UAAU,QAAQ,KAAK,IAAK;GACnD,MAAM,KAAK;EAAG;CACnC;CAEA,MAAM,WAAW,SAA2B;EAC1C,MAAM,EAAE,UAAU,SAAS,WAAW,IAAI;EAC1C,OAAO,IAAI,SAAS,GAAG,KAAK;CAC9B;CAEA,OAAO;EAAE;EAAY;CAAQ;AAC/B;;;ACnDA,MAAM,mBAAmB;AAEzB,MAAM,eAAe,UAAkB;CACrC,MAAM,WAAW,MACd,2BAAW,IAAA,OAAA,WAAA,IAAU,GAAG,EAAE,CAAC,CAC3B,2BAAW,IAAA,OAAA,cAAA,IAAY,GAAG,GAAG,CAAC,CAC9B,2BAAW,IAAA,OAAA,QAAA,IAAM,GAAG,GAAG,CAAC,CACxB,KAAK;CACR,MAAM,QAAQ,SAAS,WAAW,IAAI,IAAI,SAAS,MAAM,GAAG,CAAC,CAAC;CAE9D,OAAO;EAAE,SADO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,gBAAgB,CAC/C;EAAG;CAAM;AAC1B"}