astro-awesomeness 0.5.0 → 0.6.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.
@@ -0,0 +1,2 @@
1
+ import { cn } from "cnfast";
2
+ export { cn as t };
@@ -1,3 +1,4 @@
1
+ import { Button as Button$1 } from "@base-ui/react/button";
1
2
  import { VariantProps } from "class-variance-authority";
2
3
  import * as React from "react";
3
4
  import { Command as Command$1 } from "cmdk";
@@ -8,12 +9,15 @@ declare const buttonVariants: (props?: ({
8
9
  size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
9
10
  variant?: "default" | "destructive" | "ghost" | "link" | "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
+ type ButtonProps = Omit<React.ComponentProps<typeof Button$1>, "className"> & VariantProps<typeof buttonVariants> & {
15
13
  className?: string;
16
- } & React.RefAttributes<HTMLElement>>;
14
+ };
15
+ declare const Button: ({
16
+ className,
17
+ size,
18
+ variant,
19
+ ...props
20
+ }: ButtonProps) => React.JSX.Element;
17
21
  //#endregion
18
22
  //#region src/components/ui/dialog.d.ts
19
23
  declare function Dialog({
@@ -95,12 +99,14 @@ type CommandMenuProps = {
95
99
  groupHeading?: string;
96
100
  items: Array<CommandMenuItem>;
97
101
  placeholder?: string;
102
+ title?: string;
98
103
  };
99
104
  declare const CommandMenu: ({
100
105
  emptyMessage,
101
106
  groupHeading,
102
107
  items,
103
- placeholder
108
+ placeholder,
109
+ title
104
110
  }: CommandMenuProps) => import("react").JSX.Element;
105
111
  //#endregion
106
112
  //#region src/components/theme-toggle.d.ts
@@ -1,7 +1,6 @@
1
- import { t as cn } from "../cn-C_hacsED.js";
1
+ import { t as cn } from "../cn-CKo_fCoY.js";
2
2
  import { Button as Button$1 } from "@base-ui/react/button";
3
3
  import { cva } from "class-variance-authority";
4
- import * as React from "react";
5
4
  import { useEffect, useState, useSyncExternalStore } from "react";
6
5
  import { jsx, jsxs } from "react/jsx-runtime";
7
6
  import { Command as Command$1 } from "cmdk";
@@ -34,7 +33,7 @@ const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap
34
33
  }
35
34
  }
36
35
  });
37
- const Button = React.forwardRef(({ className, size = "default", variant = "default", ...props }, ref) => {
36
+ const Button = ({ className, size = "default", variant = "default", ...props }) => {
38
37
  return /* @__PURE__ */ jsx(Button$1, {
39
38
  className: cn(buttonVariants({
40
39
  className,
@@ -44,11 +43,9 @@ const Button = React.forwardRef(({ className, size = "default", variant = "defau
44
43
  "data-size": size,
45
44
  "data-slot": "button",
46
45
  "data-variant": variant,
47
- ref,
48
46
  ...props
49
47
  });
50
- });
51
- Button.displayName = "Button";
48
+ };
52
49
  //#endregion
53
50
  //#region src/components/ui/dialog.tsx
54
51
  function Dialog({ ...props }) {
@@ -84,7 +81,7 @@ function DialogContent({ children, className, showCloseButton = true, ...props }
84
81
  "data-slot": "dialog-content",
85
82
  ...props,
86
83
  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",
84
+ 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
85
  "data-slot": "dialog-close",
89
86
  type: "button",
90
87
  children: [/* @__PURE__ */ jsx(XIcon, {}), /* @__PURE__ */ jsx("span", {
@@ -191,7 +188,7 @@ function CommandShortcut({ className, ...props }) {
191
188
  }
192
189
  //#endregion
193
190
  //#region src/components/command-menu.tsx
194
- const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", items, placeholder = "Search posts…" }) => {
191
+ const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", items, placeholder = "Search posts…", title = "Search posts" }) => {
195
192
  const [open, setOpen] = useState(false);
196
193
  useEffect(() => {
197
194
  const handleKeyDown = (event) => {
@@ -215,9 +212,12 @@ const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", ite
215
212
  variant: "ghost"
216
213
  }),
217
214
  children: /* @__PURE__ */ jsx(Search, {})
218
- }), /* @__PURE__ */ jsx(DialogContent, {
215
+ }), /* @__PURE__ */ jsxs(DialogContent, {
219
216
  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, {
217
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
218
+ className: "sr-only",
219
+ children: title
220
+ }), /* @__PURE__ */ jsxs(Command, { children: [/* @__PURE__ */ jsx(CommandInput, { placeholder }), /* @__PURE__ */ jsxs(CommandList, { children: [/* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage }), /* @__PURE__ */ jsx(CommandGroup, {
221
221
  heading: groupHeading,
222
222
  children: items.map((item) => /* @__PURE__ */ jsx(CommandItem, {
223
223
  onSelect: () => {
@@ -232,7 +232,7 @@ const CommandMenu = ({ emptyMessage = "No results.", groupHeading = "Posts", ite
232
232
  }) : null]
233
233
  })
234
234
  }, item.url))
235
- })] })] })
235
+ })] })] })]
236
236
  })]
237
237
  });
238
238
  };
@@ -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.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.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, 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 [&_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 { 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,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,3 @@
1
1
  import { t as content_exports } from "./content/index.js";
2
- import { t as lib_exports } from "./lib-Dn4hRxdC.js";
2
+ import { t as lib_exports } from "./lib-qcIaJDAB.js";
3
3
  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,3 @@
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";
1
+ import { t as cn } from "../cn-CKo_fCoY.js";
2
+ import { a as getRelatedPosts, i as slugify, n as readingTime, o as formatDate, r as createPostUrl } from "../lib-qcIaJDAB.js";
3
3
  export { cn, createPostUrl, formatDate, getRelatedPosts, readingTime, slugify };
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-D7D4PA-g.js";
2
- import { t as cn } from "./cn-C_hacsED.js";
2
+ import { t as cn } from "./cn-CKo_fCoY.js";
3
3
  //#region src/lib/format-date.ts
4
4
  const defaultOptions = {
5
5
  day: "numeric",
@@ -7,6 +7,7 @@ const defaultOptions = {
7
7
  timeZone: "UTC",
8
8
  year: "numeric"
9
9
  };
10
+ const defaultFormatter = new Intl.DateTimeFormat("en-US", defaultOptions);
10
11
  const formatterCache = /* @__PURE__ */ new Map();
11
12
  const getFormatter = (locale, options) => {
12
13
  const key = `${locale}:${JSON.stringify(options)}`;
@@ -17,17 +18,20 @@ const getFormatter = (locale, options) => {
17
18
  }
18
19
  return formatter;
19
20
  };
20
- const formatDate = (date, locale = "en-US", options = defaultOptions) => getFormatter(locale, {
21
- ...defaultOptions,
22
- ...options
23
- }).format(date);
21
+ const formatDate = (date, locale = "en-US", options = defaultOptions) => {
22
+ if (locale === "en-US" && options === defaultOptions) return defaultFormatter.format(date);
23
+ return getFormatter(locale, {
24
+ ...defaultOptions,
25
+ ...options
26
+ }).format(date);
27
+ };
24
28
  //#endregion
25
29
  //#region src/lib/get-related-posts.ts
26
30
  const getRelatedPosts = (current, all, n) => {
27
31
  const currentTagSet = new Set(current.data.tags);
28
32
  const scored = [];
29
33
  for (const post of all) {
30
- if (post.slug === current.slug) continue;
34
+ if (post.id === current.id) continue;
31
35
  let score = 0;
32
36
  for (const tag of post.data.tags) if (currentTagSet.has(tag)) score++;
33
37
  scored.push({
@@ -93,4 +97,4 @@ var lib_exports = /* @__PURE__ */ __exportAll({
93
97
  //#endregion
94
98
  export { getRelatedPosts as a, slugify as i, readingTime as n, formatDate as o, createPostUrl as r, lib_exports as t };
95
99
 
96
- //# sourceMappingURL=lib-Dn4hRxdC.js.map
100
+ //# sourceMappingURL=lib-qcIaJDAB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib-qcIaJDAB.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,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-awesomeness",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Astro 6 blog theme with shadcn primitives and a Tailwind v4 preset.",
5
5
  "keywords": [
6
6
  "astro",
@@ -54,10 +54,9 @@
54
54
  "dependencies": {
55
55
  "@base-ui/react": "^1.5.0",
56
56
  "class-variance-authority": "^0.7.1",
57
- "clsx": "^2.1.1",
58
57
  "cmdk": "^1.1.1",
58
+ "cnfast": "^0.0.8",
59
59
  "lucide-react": "^1.17.0",
60
- "tailwind-merge": "^3.6.0",
61
60
  "tw-animate-css": "^1.4.0",
62
61
  "zod": "^4.4.3"
63
62
  },
@@ -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>
@@ -36,25 +36,22 @@ const buttonVariants = cva(
36
36
  },
37
37
  );
38
38
 
39
- type ButtonProps = Omit<React.ComponentPropsWithoutRef<typeof ButtonPrimitive>, "className"> &
39
+ type ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, "className"> &
40
40
  VariantProps<typeof buttonVariants> & {
41
41
  className?: string;
42
42
  };
43
43
 
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";
44
+ // React 19 passes `ref` as a regular prop, so no forwardRef wrapper is needed.
45
+ const Button = ({ className, size = "default", variant = "default", ...props }: ButtonProps) => {
46
+ return (
47
+ <ButtonPrimitive
48
+ className={cn(buttonVariants({ className, size, variant }))}
49
+ data-size={size}
50
+ data-slot="button"
51
+ data-variant={variant}
52
+ {...props}
53
+ />
54
+ );
55
+ };
59
56
 
60
57
  export { Button, buttonVariants };
@@ -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,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"}