astro-awesomeness 0.2.0 → 0.4.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.
- package/dist/components/index.js +4 -6
- package/dist/components/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/{lib-DRaCmJ7r.js → lib-BjqsRto0.js} +25 -9
- package/dist/lib-BjqsRto0.js.map +1 -0
- package/dist/styles/globals.css +40 -1
- package/package.json +1 -1
- package/src/astro/header.astro +3 -1
- package/src/astro/post-card.astro +12 -1
- package/src/components/theme-toggle.tsx +4 -7
- package/src/components/ui/command.tsx +0 -2
- package/src/components/ui/dialog.tsx +1 -1
- package/src/layouts/base-layout.astro +9 -0
- package/src/lib/format-date.ts +13 -1
- package/src/lib/get-related-posts.ts +18 -11
- package/src/styles/globals.css +40 -1
- package/dist/lib-DRaCmJ7r.js.map +0 -1
package/dist/components/index.js
CHANGED
|
@@ -80,7 +80,7 @@ function DialogContent({ children, className, showCloseButton = true, ...props }
|
|
|
80
80
|
"data-slot": "dialog-content",
|
|
81
81
|
...props,
|
|
82
82
|
children: [children, showCloseButton && /* @__PURE__ */ jsxs(Dialog$1.Close, {
|
|
83
|
-
className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus: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",
|
|
83
|
+
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
84
|
"data-slot": "dialog-close",
|
|
85
85
|
children: [/* @__PURE__ */ jsx(XIcon, {}), /* @__PURE__ */ jsx("span", {
|
|
86
86
|
className: "sr-only",
|
|
@@ -259,17 +259,15 @@ const ThemeToggle = ({ ariaLabel = "Toggle theme" }) => {
|
|
|
259
259
|
const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);
|
|
260
260
|
const [override, setOverride] = useState(() => getStoredTheme());
|
|
261
261
|
const theme = override ?? (prefersDark ? "dark" : "light");
|
|
262
|
-
|
|
263
|
-
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
264
|
-
}, [theme]);
|
|
265
|
-
const handleClick = () => {
|
|
262
|
+
const handleToggleTheme = () => {
|
|
266
263
|
const next = theme === "dark" ? "light" : "dark";
|
|
267
264
|
window.localStorage.setItem("theme", next);
|
|
265
|
+
document.documentElement.classList.toggle("dark", next === "dark");
|
|
268
266
|
setOverride(next);
|
|
269
267
|
};
|
|
270
268
|
return /* @__PURE__ */ jsx(Button, {
|
|
271
269
|
"aria-label": ariaLabel,
|
|
272
|
-
onClick:
|
|
270
|
+
onClick: handleToggleTheme,
|
|
273
271
|
size: "icon",
|
|
274
272
|
variant: "ghost",
|
|
275
273
|
children: theme === "dark" ? /* @__PURE__ */ jsx(Sun, {}) : /* @__PURE__ */ jsx(Moon, {})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["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 { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { Slot } from \"radix-ui\";\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\nconst Button = ({\n asChild = false,\n className,\n size = \"default\",\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }) => {\n const Comp = asChild ? Slot.Root : \"button\";\n\n return (\n <Comp\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 * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\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.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=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.Content> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=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:ring-2 focus:ring-ring focus:ring-offset-2 focus: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 >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\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 asChild>\n <Button variant=\"outline\">Close</Button>\n </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","\"use client\";\n\nimport * 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 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 asChild>\n <Button aria-label=\"Open search\" size=\"icon\" variant=\"ghost\">\n <Search />\n </Button>\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 { useEffect, 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 warnings while still letting\n // us read the actual system preference on the client.\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 useEffect(() => {\n document.documentElement.classList.toggle(\"dark\", theme === \"dark\");\n }, [theme]);\n\n const handleClick = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n window.localStorage.setItem(\"theme\", next);\n setOverride(next);\n };\n\n return (\n <Button aria-label={ariaLabel} onClick={handleClick} 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":["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 { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { Slot } from \"radix-ui\";\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\nconst Button = ({\n asChild = false,\n className,\n size = \"default\",\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean;\n }) => {\n const Comp = asChild ? Slot.Root : \"button\";\n\n return (\n <Comp\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 * as React from \"react\";\nimport { XIcon } from \"lucide-react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\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.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=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.Content> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=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 >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\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 asChild>\n <Button variant=\"outline\">Close</Button>\n </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 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 asChild>\n <Button aria-label=\"Open search\" size=\"icon\" variant=\"ghost\">\n <Search />\n </Button>\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 warnings while still letting\n // us read the actual system preference on the client.\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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"}
|
package/dist/index.js
CHANGED
package/dist/lib/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { t as cn } from "../cn-B3UhF_Ec.js";
|
|
2
|
-
import { a as formatDate, i as getRelatedPosts, n as slugify, r as readingTime } from "../lib-
|
|
2
|
+
import { a as formatDate, i as getRelatedPosts, n as slugify, r as readingTime } from "../lib-BjqsRto0.js";
|
|
3
3
|
export { cn, formatDate, getRelatedPosts, readingTime, slugify };
|
|
@@ -7,20 +7,36 @@ const defaultOptions = {
|
|
|
7
7
|
timeZone: "UTC",
|
|
8
8
|
year: "numeric"
|
|
9
9
|
};
|
|
10
|
-
const
|
|
10
|
+
const formatterCache = /* @__PURE__ */ new Map();
|
|
11
|
+
const getFormatter = (locale, options) => {
|
|
12
|
+
const key = `${locale}:${JSON.stringify(options)}`;
|
|
13
|
+
let formatter = formatterCache.get(key);
|
|
14
|
+
if (!formatter) {
|
|
15
|
+
formatter = new Intl.DateTimeFormat(locale, options);
|
|
16
|
+
formatterCache.set(key, formatter);
|
|
17
|
+
}
|
|
18
|
+
return formatter;
|
|
19
|
+
};
|
|
20
|
+
const formatDate = (date, locale = "en-US", options = defaultOptions) => getFormatter(locale, {
|
|
11
21
|
...defaultOptions,
|
|
12
22
|
...options
|
|
13
23
|
}).format(date);
|
|
14
24
|
//#endregion
|
|
15
25
|
//#region src/lib/get-related-posts.ts
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
26
|
+
const getRelatedPosts = (current, all, n) => {
|
|
27
|
+
const currentTagSet = new Set(current.data.tags);
|
|
28
|
+
const scored = [];
|
|
29
|
+
for (const post of all) {
|
|
30
|
+
if (post.slug === current.slug) continue;
|
|
31
|
+
let score = 0;
|
|
32
|
+
for (const tag of post.data.tags) if (currentTagSet.has(tag)) score++;
|
|
33
|
+
scored.push({
|
|
34
|
+
post,
|
|
35
|
+
score
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return scored.toSorted((x, y) => y.score - x.score).slice(0, n).map(({ post }) => post);
|
|
19
39
|
};
|
|
20
|
-
const getRelatedPosts = (current, all, n) => all.filter((post) => post.slug !== current.slug).map((post) => ({
|
|
21
|
-
post,
|
|
22
|
-
score: countShared(current.data.tags, post.data.tags)
|
|
23
|
-
})).toSorted((x, y) => y.score - x.score).slice(0, n).map(({ post }) => post);
|
|
24
40
|
//#endregion
|
|
25
41
|
//#region src/lib/reading-time.ts
|
|
26
42
|
const WORDS_PER_MINUTE = 200;
|
|
@@ -47,4 +63,4 @@ var lib_exports = /* @__PURE__ */ __exportAll({
|
|
|
47
63
|
//#endregion
|
|
48
64
|
export { formatDate as a, getRelatedPosts as i, slugify as n, readingTime as r, lib_exports as t };
|
|
49
65
|
|
|
50
|
-
//# sourceMappingURL=lib-
|
|
66
|
+
//# sourceMappingURL=lib-BjqsRto0.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib-BjqsRto0.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/reading-time.ts","../src/lib/slugify.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 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","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","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\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,EAAE,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,EACpC,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,WAAW,IAAI;AAC3B;;;ACvBA,MAAM,mBAAmB;AAEzB,MAAM,eAAe,UAAkB;CACrC,MAAM,WAAW,MACd,2BAAW,IAAA,OAAA,WAAA,IAAU,GAAG,EAAE,EAC1B,2BAAW,IAAA,OAAA,cAAA,IAAY,GAAG,GAAG,EAC7B,2BAAW,IAAA,OAAA,QAAA,IAAM,GAAG,GAAG,EACvB,KAAK;CACR,MAAM,QAAQ,SAAS,WAAW,IAAI,IAAI,SAAS,MAAM,GAAG,EAAE;CAE9D,OAAO;EAAE,SADO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,gBAAgB,CAC/C;EAAG;CAAM;AAC1B;;;ACXA,MAAM,WAAW,UACf,MACG,UAAU,KAAK,EACf,2BAAW,IAAA,OAAA,SAAA,IAAQ,GAAG,EAAE,EACxB,YAAY,EACZ,2BAAW,IAAA,OAAA,mBAAA,IAAgB,GAAG,EAAE,EAChC,KAAK,EACL,2BAAW,IAAA,OAAA,aAAA,IAAU,GAAG,GAAG,EAC3B,2BAAW,IAAA,OAAA,WAAA,IAAU,GAAG,EAAE"}
|
package/dist/styles/globals.css
CHANGED
|
@@ -52,7 +52,6 @@
|
|
|
52
52
|
--color-card-foreground: oklch(0.985 0 0);
|
|
53
53
|
--color-popover: oklch(0.145 0 0);
|
|
54
54
|
--color-popover-foreground: oklch(0.985 0 0);
|
|
55
|
-
--color-primary: oklch(0.985 0 0);
|
|
56
55
|
--color-primary-foreground: oklch(0.205 0 0);
|
|
57
56
|
--color-secondary: oklch(0.269 0 0);
|
|
58
57
|
--color-secondary-foreground: oklch(0.985 0 0);
|
|
@@ -67,12 +66,28 @@
|
|
|
67
66
|
--color-ring: oklch(0.556 0 0);
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
/* Opt-in to cross-document view transitions on same-origin navigations.
|
|
70
|
+
Gated by prefers-reduced-motion: users who request reduced motion get
|
|
71
|
+
instant navigation. Browsers without support degrade to instant nav. */
|
|
72
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
73
|
+
@view-transition {
|
|
74
|
+
navigation: auto;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
@layer base {
|
|
71
79
|
*,
|
|
72
80
|
*::before,
|
|
73
81
|
*::after {
|
|
74
82
|
box-sizing: border-box;
|
|
75
83
|
}
|
|
84
|
+
:root {
|
|
85
|
+
/* Inform the browser which schemes the page supports so native UI
|
|
86
|
+
(scrollbars, form controls, canvas) themes itself before paint and
|
|
87
|
+
avoids a flash of unstyled chrome. The `.dark` class still drives
|
|
88
|
+
our token overrides; this just covers native surfaces. */
|
|
89
|
+
color-scheme: light dark;
|
|
90
|
+
}
|
|
76
91
|
html {
|
|
77
92
|
-webkit-text-size-adjust: 100%;
|
|
78
93
|
}
|
|
@@ -96,6 +111,23 @@
|
|
|
96
111
|
cursor: pointer;
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
/* Balanced wrapping for short heading-like blocks. Limited to top-level
|
|
115
|
+
headings to avoid the perf cost on arbitrary text. */
|
|
116
|
+
h1,
|
|
117
|
+
h2,
|
|
118
|
+
h3,
|
|
119
|
+
h4,
|
|
120
|
+
h5,
|
|
121
|
+
h6 {
|
|
122
|
+
text-wrap: balance;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Render `<time>` metadata with tabular figures so dates and reading-time
|
|
126
|
+
counts align between rows in lists. */
|
|
127
|
+
time {
|
|
128
|
+
font-variant-numeric: tabular-nums;
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
::-webkit-scrollbar {
|
|
100
132
|
width: 6px;
|
|
101
133
|
height: 6px;
|
|
@@ -137,6 +169,13 @@
|
|
|
137
169
|
.prose h3 {
|
|
138
170
|
font-size: 1.375rem;
|
|
139
171
|
}
|
|
172
|
+
.prose p,
|
|
173
|
+
.prose li,
|
|
174
|
+
.prose blockquote {
|
|
175
|
+
/* Prefer "pretty" line breaks (no orphan words) for long-form body text.
|
|
176
|
+
Falls back to the default greedy wrap in browsers without support. */
|
|
177
|
+
text-wrap: pretty;
|
|
178
|
+
}
|
|
140
179
|
.prose p {
|
|
141
180
|
margin-top: 1em;
|
|
142
181
|
margin-bottom: 1em;
|
package/package.json
CHANGED
package/src/astro/header.astro
CHANGED
|
@@ -6,6 +6,7 @@ type Props = {
|
|
|
6
6
|
homeHref?: string;
|
|
7
7
|
class?: string;
|
|
8
8
|
themeToggleLabel?: string;
|
|
9
|
+
showThemeToggle?: boolean;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -13,6 +14,7 @@ const {
|
|
|
13
14
|
homeHref = "/",
|
|
14
15
|
class: className = "",
|
|
15
16
|
themeToggleLabel = "Toggle theme",
|
|
17
|
+
showThemeToggle = true,
|
|
16
18
|
} = Astro.props;
|
|
17
19
|
---
|
|
18
20
|
<header class:list={["border-border bg-background/80 sticky top-0 z-40 border-b backdrop-blur", className]}>
|
|
@@ -22,7 +24,7 @@ const {
|
|
|
22
24
|
</a>
|
|
23
25
|
<nav class="flex items-center gap-2">
|
|
24
26
|
<slot name="nav" />
|
|
25
|
-
<ThemeToggle client:load ariaLabel={themeToggleLabel} />
|
|
27
|
+
{showThemeToggle && <ThemeToggle client:load ariaLabel={themeToggleLabel} />}
|
|
26
28
|
</nav>
|
|
27
29
|
</div>
|
|
28
30
|
</header>
|
|
@@ -14,7 +14,7 @@ const { post, locale } = Astro.props;
|
|
|
14
14
|
const { title, description, pubDate, tags } = post.data;
|
|
15
15
|
const href = `/blog/${post.id}`;
|
|
16
16
|
---
|
|
17
|
-
<article class="border-border group border-b py-6 last:border-0">
|
|
17
|
+
<article class="post-card border-border group border-b py-6 last:border-0">
|
|
18
18
|
<a href={href} class="block">
|
|
19
19
|
<h2 class="text-foreground group-hover:text-primary text-xl font-semibold tracking-tight transition-colors">
|
|
20
20
|
{title}
|
|
@@ -26,3 +26,14 @@ const href = `/blog/${post.id}`;
|
|
|
26
26
|
</a>
|
|
27
27
|
{tags.length > 0 && <TagList tags={tags} class="mt-3" />}
|
|
28
28
|
</article>
|
|
29
|
+
|
|
30
|
+
<style>
|
|
31
|
+
/* Skip layout/paint for off-screen cards on long post lists. The first
|
|
32
|
+
three remain eagerly rendered so above-the-fold paint isn't blocked by
|
|
33
|
+
visibility containment evaluation. `contain-intrinsic-size: auto …`
|
|
34
|
+
lets the browser cache real measurements after first render. */
|
|
35
|
+
.post-card:nth-child(n + 4) {
|
|
36
|
+
content-visibility: auto;
|
|
37
|
+
contain-intrinsic-size: auto 200px;
|
|
38
|
+
}
|
|
39
|
+
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Moon, Sun } from "lucide-react";
|
|
2
|
-
import {
|
|
2
|
+
import { useState, useSyncExternalStore } from "react";
|
|
3
3
|
|
|
4
4
|
import { Button } from "./ui/button";
|
|
5
5
|
|
|
@@ -45,18 +45,15 @@ const ThemeToggle = ({ ariaLabel = "Toggle theme" }: Props) => {
|
|
|
45
45
|
const [override, setOverride] = useState<Theme | null>(() => getStoredTheme());
|
|
46
46
|
const theme: Theme = override ?? (prefersDark ? "dark" : "light");
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
50
|
-
}, [theme]);
|
|
51
|
-
|
|
52
|
-
const handleClick = () => {
|
|
48
|
+
const handleToggleTheme = () => {
|
|
53
49
|
const next: Theme = theme === "dark" ? "light" : "dark";
|
|
54
50
|
window.localStorage.setItem("theme", next);
|
|
51
|
+
document.documentElement.classList.toggle("dark", next === "dark");
|
|
55
52
|
setOverride(next);
|
|
56
53
|
};
|
|
57
54
|
|
|
58
55
|
return (
|
|
59
|
-
<Button aria-label={ariaLabel} onClick={
|
|
56
|
+
<Button aria-label={ariaLabel} onClick={handleToggleTheme} size="icon" variant="ghost">
|
|
60
57
|
{theme === "dark" ? <Sun /> : <Moon />}
|
|
61
58
|
</Button>
|
|
62
59
|
);
|
|
@@ -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:ring-2 focus:ring-ring focus:ring-offset-2 focus: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 data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
63
63
|
data-slot="dialog-close"
|
|
64
64
|
>
|
|
65
65
|
<XIcon />
|
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
<head>
|
|
29
29
|
<meta charset="utf-8" />
|
|
30
30
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
31
|
+
<meta name="color-scheme" content="light dark" />
|
|
31
32
|
<meta name="generator" content={Astro.generator} />
|
|
32
33
|
<Seo title={title} description={description} canonical={canonical} image={image} type={type} />
|
|
33
34
|
<script is:inline>
|
|
@@ -38,6 +39,14 @@ const {
|
|
|
38
39
|
document.documentElement.classList.toggle("dark", theme === "dark");
|
|
39
40
|
})();
|
|
40
41
|
</script>
|
|
42
|
+
<script type="speculationrules" is:inline set:html={JSON.stringify({
|
|
43
|
+
prefetch: [
|
|
44
|
+
{
|
|
45
|
+
where: { and: [{ href_matches: "/*" }, { not: { href_matches: "/*\\?*" } }] },
|
|
46
|
+
eagerness: "moderate",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
})} />
|
|
41
50
|
<slot name="head" />
|
|
42
51
|
</head>
|
|
43
52
|
<body class="bg-background text-foreground min-h-dvh flex flex-col">
|
package/src/lib/format-date.ts
CHANGED
|
@@ -5,10 +5,22 @@ const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
|
5
5
|
year: "numeric",
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
|
9
|
+
|
|
10
|
+
const getFormatter = (locale: string, options: Intl.DateTimeFormatOptions) => {
|
|
11
|
+
const key = `${locale}:${JSON.stringify(options)}`;
|
|
12
|
+
let formatter = formatterCache.get(key);
|
|
13
|
+
if (!formatter) {
|
|
14
|
+
formatter = new Intl.DateTimeFormat(locale, options);
|
|
15
|
+
formatterCache.set(key, formatter);
|
|
16
|
+
}
|
|
17
|
+
return formatter;
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
const formatDate = (
|
|
9
21
|
date: Date,
|
|
10
22
|
locale: string = "en-US",
|
|
11
23
|
options: Intl.DateTimeFormatOptions = defaultOptions,
|
|
12
|
-
) =>
|
|
24
|
+
) => getFormatter(locale, { ...defaultOptions, ...options }).format(date);
|
|
13
25
|
|
|
14
26
|
export { formatDate };
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
type HasTags = { data: { tags: Array<string> }; slug: string };
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
const getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {
|
|
4
|
+
const currentTagSet = new Set(current.data.tags);
|
|
5
|
+
const scored: Array<{ post: T; score: number }> = [];
|
|
6
|
+
|
|
7
|
+
for (const post of all) {
|
|
8
|
+
if (post.slug === current.slug) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
let score = 0;
|
|
12
|
+
for (const tag of post.data.tags) {
|
|
13
|
+
if (currentTagSet.has(tag)) {
|
|
14
|
+
score++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
scored.push({ post, score });
|
|
18
|
+
}
|
|
7
19
|
|
|
8
|
-
|
|
9
|
-
all
|
|
10
|
-
.filter((post) => post.slug !== current.slug)
|
|
11
|
-
.map((post) => ({
|
|
12
|
-
post,
|
|
13
|
-
score: countShared(current.data.tags, post.data.tags),
|
|
14
|
-
}))
|
|
20
|
+
return scored
|
|
15
21
|
.toSorted((x, y) => y.score - x.score)
|
|
16
22
|
.slice(0, n)
|
|
17
23
|
.map(({ post }) => post);
|
|
24
|
+
};
|
|
18
25
|
|
|
19
26
|
export { getRelatedPosts };
|
package/src/styles/globals.css
CHANGED
|
@@ -52,7 +52,6 @@
|
|
|
52
52
|
--color-card-foreground: oklch(0.985 0 0);
|
|
53
53
|
--color-popover: oklch(0.145 0 0);
|
|
54
54
|
--color-popover-foreground: oklch(0.985 0 0);
|
|
55
|
-
--color-primary: oklch(0.985 0 0);
|
|
56
55
|
--color-primary-foreground: oklch(0.205 0 0);
|
|
57
56
|
--color-secondary: oklch(0.269 0 0);
|
|
58
57
|
--color-secondary-foreground: oklch(0.985 0 0);
|
|
@@ -67,12 +66,28 @@
|
|
|
67
66
|
--color-ring: oklch(0.556 0 0);
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
/* Opt-in to cross-document view transitions on same-origin navigations.
|
|
70
|
+
Gated by prefers-reduced-motion: users who request reduced motion get
|
|
71
|
+
instant navigation. Browsers without support degrade to instant nav. */
|
|
72
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
73
|
+
@view-transition {
|
|
74
|
+
navigation: auto;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
@layer base {
|
|
71
79
|
*,
|
|
72
80
|
*::before,
|
|
73
81
|
*::after {
|
|
74
82
|
box-sizing: border-box;
|
|
75
83
|
}
|
|
84
|
+
:root {
|
|
85
|
+
/* Inform the browser which schemes the page supports so native UI
|
|
86
|
+
(scrollbars, form controls, canvas) themes itself before paint and
|
|
87
|
+
avoids a flash of unstyled chrome. The `.dark` class still drives
|
|
88
|
+
our token overrides; this just covers native surfaces. */
|
|
89
|
+
color-scheme: light dark;
|
|
90
|
+
}
|
|
76
91
|
html {
|
|
77
92
|
-webkit-text-size-adjust: 100%;
|
|
78
93
|
}
|
|
@@ -96,6 +111,23 @@
|
|
|
96
111
|
cursor: pointer;
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
/* Balanced wrapping for short heading-like blocks. Limited to top-level
|
|
115
|
+
headings to avoid the perf cost on arbitrary text. */
|
|
116
|
+
h1,
|
|
117
|
+
h2,
|
|
118
|
+
h3,
|
|
119
|
+
h4,
|
|
120
|
+
h5,
|
|
121
|
+
h6 {
|
|
122
|
+
text-wrap: balance;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Render `<time>` metadata with tabular figures so dates and reading-time
|
|
126
|
+
counts align between rows in lists. */
|
|
127
|
+
time {
|
|
128
|
+
font-variant-numeric: tabular-nums;
|
|
129
|
+
}
|
|
130
|
+
|
|
99
131
|
::-webkit-scrollbar {
|
|
100
132
|
width: 6px;
|
|
101
133
|
height: 6px;
|
|
@@ -137,6 +169,13 @@
|
|
|
137
169
|
.prose h3 {
|
|
138
170
|
font-size: 1.375rem;
|
|
139
171
|
}
|
|
172
|
+
.prose p,
|
|
173
|
+
.prose li,
|
|
174
|
+
.prose blockquote {
|
|
175
|
+
/* Prefer "pretty" line breaks (no orphan words) for long-form body text.
|
|
176
|
+
Falls back to the default greedy wrap in browsers without support. */
|
|
177
|
+
text-wrap: pretty;
|
|
178
|
+
}
|
|
140
179
|
.prose p {
|
|
141
180
|
margin-top: 1em;
|
|
142
181
|
margin-bottom: 1em;
|
package/dist/lib-DRaCmJ7r.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lib-DRaCmJ7r.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/reading-time.ts","../src/lib/slugify.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 formatDate = (\n date: Date,\n locale: string = \"en-US\",\n options: Intl.DateTimeFormatOptions = defaultOptions,\n) => new Intl.DateTimeFormat(locale, { ...defaultOptions, ...options }).format(date);\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; slug: string };\n\nconst countShared = (a: Array<string>, b: Array<string>) => {\n const setB = new Set(b);\n return a.filter((tag) => setB.has(tag)).length;\n};\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) =>\n all\n .filter((post) => post.slug !== current.slug)\n .map((post) => ({\n post,\n score: countShared(current.data.tags, post.data.tags),\n }))\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n\nexport { getRelatedPosts };\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","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","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\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,cACJ,MACA,SAAiB,SACjB,UAAsC,mBACnC,IAAI,KAAK,eAAe,QAAQ;CAAE,GAAG;CAAgB,GAAG;AAAQ,CAAC,EAAE,OAAO,IAAI;;;ACTnF,MAAM,eAAe,GAAkB,MAAqB;CAC1D,MAAM,OAAO,IAAI,IAAI,CAAC;CACtB,OAAO,EAAE,QAAQ,QAAQ,KAAK,IAAI,GAAG,CAAC,EAAE;AAC1C;AAEA,MAAM,mBAAsC,SAAY,KAAe,MACrE,IACG,QAAQ,SAAS,KAAK,SAAS,QAAQ,IAAI,EAC3C,KAAK,UAAU;CACd;CACA,OAAO,YAAY,QAAQ,KAAK,MAAM,KAAK,KAAK,IAAI;AACtD,EAAE,EACD,UAAU,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EACpC,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,WAAW,IAAI;;;AChB3B,MAAM,mBAAmB;AAEzB,MAAM,eAAe,UAAkB;CACrC,MAAM,WAAW,MACd,2BAAW,IAAA,OAAA,WAAA,IAAU,GAAG,EAAE,EAC1B,2BAAW,IAAA,OAAA,cAAA,IAAY,GAAG,GAAG,EAC7B,2BAAW,IAAA,OAAA,QAAA,IAAM,GAAG,GAAG,EACvB,KAAK;CACR,MAAM,QAAQ,SAAS,WAAW,IAAI,IAAI,SAAS,MAAM,GAAG,EAAE;CAE9D,OAAO;EAAE,SADO,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,gBAAgB,CAC/C;EAAG;CAAM;AAC1B;;;ACXA,MAAM,WAAW,UACf,MACG,UAAU,KAAK,EACf,2BAAW,IAAA,OAAA,SAAA,IAAQ,GAAG,EAAE,EACxB,YAAY,EACZ,2BAAW,IAAA,OAAA,mBAAA,IAAgB,GAAG,EAAE,EAChC,KAAK,EACL,2BAAW,IAAA,OAAA,aAAA,IAAU,GAAG,GAAG,EAC3B,2BAAW,IAAA,OAAA,WAAA,IAAU,GAAG,EAAE"}
|