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.
- package/dist/cn-CKo_fCoY.js +2 -0
- package/dist/components/index.d.ts +12 -6
- package/dist/components/index.js +11 -11
- package/dist/components/index.js.map +1 -1
- package/dist/{index-pemcLff6.d.ts → index-syqFQChT.d.ts} +3 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/{lib-Dn4hRxdC.js → lib-qcIaJDAB.js} +11 -7
- package/dist/lib-qcIaJDAB.js.map +1 -0
- package/package.json +2 -3
- package/src/astro/post-card.astro +3 -2
- package/src/astro/seo.astro +23 -1
- package/src/components/command-menu.tsx +4 -1
- package/src/components/ui/button.tsx +13 -16
- package/src/components/ui/command.tsx +5 -4
- package/src/components/ui/dialog.tsx +1 -1
- package/src/layouts/base-layout.astro +30 -1
- package/src/layouts/list-layout.astro +9 -5
- package/src/layouts/post-layout.astro +46 -4
- package/src/lib/cn.ts +1 -6
- package/src/lib/format-date.ts +10 -1
- package/src/lib/get-related-posts.test.ts +15 -9
- package/src/lib/get-related-posts.ts +2 -2
- package/dist/cn-C_hacsED.js +0 -8
- package/dist/cn-C_hacsED.js.map +0 -1
- package/dist/lib-Dn4hRxdC.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
-
}
|
|
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
|
package/dist/components/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { t as cn } from "../cn-
|
|
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 =
|
|
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
|
|
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__ */
|
|
215
|
+
}), /* @__PURE__ */ jsxs(DialogContent, {
|
|
219
216
|
className: "p-0",
|
|
220
|
-
children:
|
|
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 {
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
package/dist/lib/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
2
|
-
import { a as getRelatedPosts, i as slugify, n as readingTime, o as formatDate, r as createPostUrl } from "../lib-
|
|
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-
|
|
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) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
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-
|
|
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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-awesomeness",
|
|
3
|
-
"version": "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">
|
package/src/astro/seo.astro
CHANGED
|
@@ -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 {
|
|
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.
|
|
39
|
+
type ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, "className"> &
|
|
40
40
|
VariantProps<typeof buttonVariants> & {
|
|
41
41
|
className?: string;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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<
|
|
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 } =
|
|
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 {
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
package/src/lib/format-date.ts
CHANGED
|
@@ -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
|
-
) =>
|
|
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> };
|
|
5
|
+
type TestPost = { data: { tags: Array<string> }; id: string };
|
|
6
6
|
|
|
7
|
-
const a: TestPost = { data: { tags: ["x", "y"] },
|
|
8
|
-
const b: TestPost = { data: { tags: ["x"] },
|
|
9
|
-
const c: TestPost = { data: { tags: ["z"] },
|
|
10
|
-
const d: TestPost = { data: { tags: ["x", "y", "z"] },
|
|
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.
|
|
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.
|
|
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].
|
|
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"] },
|
|
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> };
|
|
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.
|
|
8
|
+
if (post.id === current.id) {
|
|
9
9
|
continue;
|
|
10
10
|
}
|
|
11
11
|
let score = 0;
|
package/dist/cn-C_hacsED.js
DELETED
package/dist/cn-C_hacsED.js.map
DELETED
|
@@ -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"}
|
package/dist/lib-Dn4hRxdC.js.map
DELETED
|
@@ -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"}
|