astro-awesomeness 1.2.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,13 @@
1
1
  import { Button as Button$1 } from "@base-ui/react/button";
2
- import * as React from "react";
3
2
  import { VariantProps } from "class-variance-authority";
4
- //#region src/components/ui/button-variants.d.ts
3
+ //#region src/components/button.d.ts
5
4
  declare const buttonVariants: (props?: ({
6
5
  size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
7
6
  variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
8
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
+ declare const Button: ({ className, size, variant, ...props }: Button$1.Props & VariantProps<typeof buttonVariants>) => import("react").JSX.Element;
9
9
  //#endregion
10
- //#region src/components/ui/button.d.ts
11
- type ButtonProps = Omit<React.ComponentProps<typeof Button$1>, "className"> & VariantProps<typeof buttonVariants> & {
12
- className?: string;
13
- };
14
- declare const Button: ({ className, size, variant, ...props }: ButtonProps) => React.JSX.Element;
15
- //#endregion
16
- //#region src/components/theme-toggle.d.ts
10
+ //#region src/compositions/theme-toggle.d.ts
17
11
  type Props = {
18
12
  ariaLabel?: string;
19
13
  };
@@ -1,40 +1,37 @@
1
- import { t as cn } from "../cn-B2A3VjBM.js";
1
+ import { cn } from "cn";
2
2
  import { Button as Button$1 } from "@base-ui/react/button";
3
- import { useSyncExternalStore } from "react";
4
3
  import { cva } from "class-variance-authority";
5
4
  import { jsx } from "react/jsx-runtime";
6
5
  import { Moon, Sun } from "lucide-react";
6
+ import { useSyncExternalStore } from "react";
7
7
 
8
- //#region src/components/ui/button-variants.ts
9
- const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
8
+ //#region src/components/button.tsx
9
+ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
10
10
  defaultVariants: {
11
11
  size: "default",
12
12
  variant: "default"
13
13
  },
14
14
  variants: {
15
15
  size: {
16
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
17
- icon: "size-11 sm:size-9",
18
- "icon-lg": "size-10",
19
- "icon-sm": "size-8",
20
- "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
21
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
22
- sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
23
- xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3"
16
+ default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
17
+ icon: "size-8",
18
+ "icon-lg": "size-9",
19
+ "icon-sm": "size-7 rounded-control in-data-[slot=button-group]:rounded-lg",
20
+ "icon-xs": "size-6 rounded-control-sm in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
21
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
22
+ sm: "h-7 gap-1 rounded-control px-2.5 text-(length:--text-control) in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
23
+ xs: "h-6 gap-1 rounded-control-sm px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3"
24
24
  },
25
25
  variant: {
26
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
27
- destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
28
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
26
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
27
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
28
+ ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
29
29
  link: "text-primary underline-offset-4 hover:underline",
30
- outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
31
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80"
30
+ outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
31
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-hover aria-expanded:bg-secondary aria-expanded:text-secondary-foreground"
32
32
  }
33
33
  }
34
34
  });
35
-
36
- //#endregion
37
- //#region src/components/ui/button.tsx
38
35
  const Button = ({ className, size = "default", variant = "default", ...props }) => {
39
36
  return /* @__PURE__ */ jsx(Button$1, {
40
37
  className: cn(buttonVariants({
@@ -42,15 +39,13 @@ const Button = ({ className, size = "default", variant = "default", ...props })
42
39
  size,
43
40
  variant
44
41
  })),
45
- "data-size": size,
46
42
  "data-slot": "button",
47
- "data-variant": variant,
48
43
  ...props
49
44
  });
50
45
  };
51
46
 
52
47
  //#endregion
53
- //#region src/components/theme-toggle.tsx
48
+ //#region src/compositions/theme-toggle.tsx
54
49
  const getStoredTheme = () => {
55
50
  if (typeof window === "undefined") return null;
56
51
  const stored = window.localStorage.getItem("theme");
@@ -83,7 +78,11 @@ const subscribePrefersDark = (callback) => {
83
78
  media.removeEventListener("change", callback);
84
79
  };
85
80
  };
81
+ const subscribeHydration = () => () => {};
82
+ const getHydratedSnapshot = () => true;
83
+ const getServerHydratedSnapshot = () => false;
86
84
  const ThemeToggle = ({ ariaLabel = "Toggle theme" }) => {
85
+ const isHydrated = useSyncExternalStore(subscribeHydration, getHydratedSnapshot, getServerHydratedSnapshot);
87
86
  const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);
88
87
  const theme = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null) ?? (prefersDark ? "dark" : "light");
89
88
  const handleToggleTheme = () => {
@@ -93,6 +92,7 @@ const ThemeToggle = ({ ariaLabel = "Toggle theme" }) => {
93
92
  };
94
93
  return /* @__PURE__ */ jsx(Button, {
95
94
  "aria-label": ariaLabel,
95
+ disabled: !isHydrated,
96
96
  onClick: handleToggleTheme,
97
97
  size: "icon",
98
98
  variant: "ghost",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["ButtonPrimitive"],"sources":["../../src/components/ui/button-variants.ts","../../src/components/ui/button.tsx","../../src/components/theme-toggle.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n icon: \"size-11 sm:size-9\",\n \"icon-lg\": \"size-10\",\n \"icon-sm\": \"size-8\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n },\n },\n },\n);\n\nexport { buttonVariants };\n","import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { buttonVariants } from \"./button-variants\";\n\ntype ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, \"className\"> &\n VariantProps<typeof buttonVariants> & {\n className?: string;\n };\n\nconst Button = ({ className, size = \"default\", variant = \"default\", ...props }: ButtonProps) => {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-size={size}\n data-slot=\"button\"\n data-variant={variant}\n {...props}\n />\n );\n};\n\nexport { Button };\n","import { Moon, Sun } from \"lucide-react\";\nimport { useSyncExternalStore } from \"react\";\n\nimport { Button } from \"./ui/button\";\n\ntype Theme = \"light\" | \"dark\";\n\nconst getStoredTheme = (): Theme | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n const stored = window.localStorage.getItem(\"theme\");\n if (stored === \"light\" || stored === \"dark\") {\n return stored;\n }\n return null;\n};\n\nconst storedThemeListeners = new Set<() => void>();\n\nconst subscribeStoredTheme = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n storedThemeListeners.add(callback);\n window.addEventListener(\"storage\", callback);\n return () => {\n storedThemeListeners.delete(callback);\n window.removeEventListener(\"storage\", callback);\n };\n};\n\nconst setStoredTheme = (next: Theme) => {\n window.localStorage.setItem(\"theme\", next);\n for (const listener of storedThemeListeners) {\n listener();\n }\n};\n\nconst getPrefersDark = (): boolean => {\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n};\n\nconst subscribePrefersDark = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", callback);\n return () => {\n media.removeEventListener(\"change\", callback);\n };\n};\n\ntype Props = {\n ariaLabel?: string;\n};\n\nconst ThemeToggle = ({ ariaLabel = \"Toggle theme\" }: Props) => {\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n const override = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null);\n const theme: Theme = override ?? (prefersDark ? \"dark\" : \"light\");\n\n const handleToggleTheme = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n setStoredTheme(next);\n };\n\n return (\n <Button aria-label={ariaLabel} onClick={handleToggleTheme} size=\"icon\" variant=\"ghost\">\n {theme === \"dark\" ? <Sun /> : <Moon />}\n </Button>\n );\n};\n\nexport { ThemeToggle };\n"],"mappings":";;;;;;;;AAEA,MAAM,iBAAiB,IACrB,+bACA;CACE,iBAAiB;EACf,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,MAAM;GACJ,SAAS;GACT,MAAM;GACN,WAAW;GACX,WAAW;GACX,WAAW;GACX,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,SAAS;GACP,SAAS;GACT,aACE;GACF,OAAO;GACP,MAAM;GACN,SACE;GACF,WAAW;EACb;CACF;AACF,CACF;;;;ACpBA,MAAM,UAAU,EAAE,WAAW,OAAO,WAAW,UAAU,WAAW,GAAG,YAAyB;CAC9F,OACE,oBAACA,UAAD;EACE,WAAW,GAAG,eAAe;GAAE;GAAW;GAAM;EAAQ,CAAC,CAAC;EAC1D,aAAW;EACX,aAAU;EACV,gBAAc;EACd,GAAI;CACL;AAEL;;;;ACfA,MAAM,uBAAqC;CACzC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,MAAM,SAAS,OAAO,aAAa,QAAQ,OAAO;CAClD,IAAI,WAAW,WAAW,WAAW,QACnC,OAAO;CAET,OAAO;AACT;AAEA,MAAM,uCAAuB,IAAI,IAAgB;AAEjD,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,qBAAqB,IAAI,QAAQ;CACjC,OAAO,iBAAiB,WAAW,QAAQ;CAC3C,aAAa;EACX,qBAAqB,OAAO,QAAQ;EACpC,OAAO,oBAAoB,WAAW,QAAQ;CAChD;AACF;AAEA,MAAM,kBAAkB,SAAgB;CACtC,OAAO,aAAa,QAAQ,SAAS,IAAI;CACzC,KAAK,MAAM,YAAY,sBACrB,SAAS;AAEb;AAEA,MAAM,uBAAgC;CACpC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,OAAO,OAAO,WAAW,8BAA8B,CAAC,CAAC;AAC3D;AAEA,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,MAAM,QAAQ,OAAO,WAAW,8BAA8B;CAC9D,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa;EACX,MAAM,oBAAoB,UAAU,QAAQ;CAC9C;AACF;AAMA,MAAM,eAAe,EAAE,YAAY,qBAA4B;CAC7D,MAAM,cAAc,qBAAqB,sBAAsB,sBAAsB,KAAK;CAE1F,MAAM,QADW,qBAAqB,sBAAsB,sBAAsB,IACtD,MAAM,cAAc,SAAS;CAEzD,MAAM,0BAA0B;EAC9B,MAAM,OAAc,UAAU,SAAS,UAAU;EACjD,SAAS,gBAAgB,UAAU,OAAO,QAAQ,SAAS,MAAM;EACjE,eAAe,IAAI;CACrB;CAEA,OACE,oBAAC,QAAD;EAAQ,cAAY;EAAW,SAAS;EAAmB,MAAK;EAAO,SAAQ;YAC5E,UAAU,SAAS,oBAAC,KAAD,CAAM,KAAI,oBAAC,MAAD,CAAO;CAC/B;AAEZ"}
1
+ {"version":3,"file":"index.js","names":["ButtonPrimitive"],"sources":["../../src/components/button.tsx","../../src/compositions/theme-toggle.tsx"],"sourcesContent":["import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"cn\";\n\nconst buttonVariants = cva(\n \"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 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:\n \"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n icon: \"size-8\",\n \"icon-lg\": \"size-9\",\n \"icon-sm\": \"size-7 rounded-control in-data-[slot=button-group]:rounded-lg\",\n \"icon-xs\":\n \"size-6 rounded-control-sm in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3\",\n lg: \"h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2\",\n sm: \"h-7 gap-1 rounded-control px-2.5 text-(length:--text-control) in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5\",\n xs: \"h-6 gap-1 rounded-control-sm px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/80\",\n destructive:\n \"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40\",\n ghost:\n \"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n outline:\n \"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary-hover aria-expanded:bg-secondary aria-expanded:text-secondary-foreground\",\n },\n },\n },\n);\n\nconst Button = ({\n className,\n size = \"default\",\n variant = \"default\",\n ...props\n}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) => {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-slot=\"button\"\n {...props}\n />\n );\n};\n\nexport { Button, buttonVariants };\n","import { Moon, Sun } from \"lucide-react\";\nimport { useSyncExternalStore } from \"react\";\n\nimport { Button } from \"../components/button\";\n\ntype Theme = \"light\" | \"dark\";\n\nconst getStoredTheme = (): Theme | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n const stored = window.localStorage.getItem(\"theme\");\n if (stored === \"light\" || stored === \"dark\") {\n return stored;\n }\n return null;\n};\n\nconst storedThemeListeners = new Set<() => void>();\n\nconst subscribeStoredTheme = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n storedThemeListeners.add(callback);\n window.addEventListener(\"storage\", callback);\n return () => {\n storedThemeListeners.delete(callback);\n window.removeEventListener(\"storage\", callback);\n };\n};\n\nconst setStoredTheme = (next: Theme) => {\n window.localStorage.setItem(\"theme\", next);\n for (const listener of storedThemeListeners) {\n listener();\n }\n};\n\nconst getPrefersDark = (): boolean => {\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n};\n\nconst subscribePrefersDark = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", callback);\n return () => {\n media.removeEventListener(\"change\", callback);\n };\n};\n\ntype Props = {\n ariaLabel?: string;\n};\n\nconst subscribeHydration = () => () => {};\nconst getHydratedSnapshot = () => true;\nconst getServerHydratedSnapshot = () => false;\n\nconst ThemeToggle = ({ ariaLabel = \"Toggle theme\" }: Props) => {\n const isHydrated = useSyncExternalStore(\n subscribeHydration,\n getHydratedSnapshot,\n getServerHydratedSnapshot,\n );\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n const override = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null);\n const theme: Theme = override ?? (prefersDark ? \"dark\" : \"light\");\n\n const handleToggleTheme = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n setStoredTheme(next);\n };\n\n return (\n <Button\n aria-label={ariaLabel}\n disabled={!isHydrated}\n onClick={handleToggleTheme}\n size=\"icon\"\n variant=\"ghost\"\n >\n {theme === \"dark\" ? <Sun /> : <Moon />}\n </Button>\n );\n};\n\nexport { ThemeToggle };\n"],"mappings":";;;;;;;;AAIA,MAAM,iBAAiB,IACrB,8lBACA;CACE,iBAAiB;EACf,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,MAAM;GACJ,SACE;GACF,MAAM;GACN,WAAW;GACX,WAAW;GACX,WACE;GACF,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,SAAS;GACP,SAAS;GACT,aACE;GACF,OACE;GACF,MAAM;GACN,SACE;GACF,WACE;EACJ;CACF;AACF,CACF;AAEA,MAAM,UAAU,EACd,WACA,OAAO,WACP,UAAU,WACV,GAAG,YAC8D;CACjE,OACE,oBAACA,UAAD;EACE,WAAW,GAAG,eAAe;GAAE;GAAW;GAAM;EAAQ,CAAC,CAAC;EAC1D,aAAU;EACV,GAAI;CACL;AAEL;;;;AC9CA,MAAM,uBAAqC;CACzC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,MAAM,SAAS,OAAO,aAAa,QAAQ,OAAO;CAClD,IAAI,WAAW,WAAW,WAAW,QACnC,OAAO;CAET,OAAO;AACT;AAEA,MAAM,uCAAuB,IAAI,IAAgB;AAEjD,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,qBAAqB,IAAI,QAAQ;CACjC,OAAO,iBAAiB,WAAW,QAAQ;CAC3C,aAAa;EACX,qBAAqB,OAAO,QAAQ;EACpC,OAAO,oBAAoB,WAAW,QAAQ;CAChD;AACF;AAEA,MAAM,kBAAkB,SAAgB;CACtC,OAAO,aAAa,QAAQ,SAAS,IAAI;CACzC,KAAK,MAAM,YAAY,sBACrB,SAAS;AAEb;AAEA,MAAM,uBAAgC;CACpC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,OAAO,OAAO,WAAW,8BAA8B,CAAC,CAAC;AAC3D;AAEA,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,MAAM,QAAQ,OAAO,WAAW,8BAA8B;CAC9D,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa;EACX,MAAM,oBAAoB,UAAU,QAAQ;CAC9C;AACF;AAMA,MAAM,iCAAiC,CAAC;AACxC,MAAM,4BAA4B;AAClC,MAAM,kCAAkC;AAExC,MAAM,eAAe,EAAE,YAAY,qBAA4B;CAC7D,MAAM,aAAa,qBACjB,oBACA,qBACA,yBACF;CACA,MAAM,cAAc,qBAAqB,sBAAsB,sBAAsB,KAAK;CAE1F,MAAM,QADW,qBAAqB,sBAAsB,sBAAsB,IACtD,MAAM,cAAc,SAAS;CAEzD,MAAM,0BAA0B;EAC9B,MAAM,OAAc,UAAU,SAAS,UAAU;EACjD,SAAS,gBAAgB,UAAU,OAAO,QAAQ,SAAS,MAAM;EACjE,eAAe,IAAI;CACrB;CAEA,OACE,oBAAC,QAAD;EACE,cAAY;EACZ,UAAU,CAAC;EACX,SAAS;EACT,MAAK;EACL,SAAQ;YAEP,UAAU,SAAS,oBAAC,KAAD,CAAM,KAAI,oBAAC,MAAD,CAAO;CAC/B;AAEZ"}
@@ -0,0 +1,84 @@
1
+ import { cn as cn$1 } from "cn";
2
+ //#region src/lib/post-url.d.ts
3
+ type PostLike = {
4
+ data: {
5
+ categories?: Array<string>;
6
+ seo?: {
7
+ canonical_url?: string;
8
+ };
9
+ };
10
+ id: string;
11
+ };
12
+ type PostParams = {
13
+ category: string;
14
+ slug: string;
15
+ };
16
+ type PostUrlBuilder = {
17
+ postParams: (post: PostLike) => PostParams;
18
+ postUrl: (post: PostLike) => string;
19
+ };
20
+ declare const createPostUrl: (categorySlugMap: Record<string, string>, defaultCategory: string) => PostUrlBuilder;
21
+ //#endregion
22
+ //#region src/lib/category-index.d.ts
23
+ type CategoryPostLike = PostLike & {
24
+ data: {
25
+ categories: Array<string>;
26
+ pubDate: Date;
27
+ };
28
+ };
29
+ type CategoryBucket<P> = {
30
+ name: string;
31
+ posts: Array<P>;
32
+ slug: string;
33
+ };
34
+ type PostCategory = {
35
+ name: string;
36
+ slug: string;
37
+ };
38
+ type GroupOptions = {
39
+ includeSecondaryCategories?: boolean;
40
+ };
41
+ declare const createCategoryIndex: <P extends CategoryPostLike>(categorySlugMap: Record<string, string>, defaultCategory: string, posts: Array<P>) => {
42
+ categoryName: (slug: string) => string;
43
+ groupByCategory: ({ includeSecondaryCategories }?: GroupOptions) => Array<CategoryBucket<P>>;
44
+ postCategory: (post: PostLike) => PostCategory;
45
+ };
46
+ //#endregion
47
+ //#region src/lib/category-label.d.ts
48
+ declare const categoryLabel: (name: string) => string;
49
+ //#endregion
50
+ //#region src/lib/format-date.d.ts
51
+ declare const formatDate: (date: Date, locale?: string, options?: Intl.DateTimeFormatOptions) => string;
52
+ //#endregion
53
+ //#region src/lib/get-related-posts.d.ts
54
+ type HasTags = {
55
+ data: {
56
+ tags: Array<string>;
57
+ };
58
+ id: string;
59
+ };
60
+ declare const getRelatedPosts: <T extends HasTags>(current: T, all: Array<T>, n: number) => T[];
61
+ //#endregion
62
+ //#region src/lib/reading-time.d.ts
63
+ declare const readingTime: (input: string) => {
64
+ minutes: number;
65
+ words: number;
66
+ };
67
+ //#endregion
68
+ //#region src/lib/require-env.d.ts
69
+ declare const requireEnv: (name: string) => string;
70
+ //#endregion
71
+ //#region src/lib/slugify.d.ts
72
+ declare const slugify: (input: string) => string;
73
+ //#endregion
74
+ //#region src/lib/tag-url.d.ts
75
+ declare const DEFAULT_TAG_BASE = "/tag";
76
+ declare const tagUrl: (tag: string, base?: string) => string;
77
+ declare const tagLabel: (tag: string) => string;
78
+ declare const uniqueTags: (tags: Array<string>) => Array<string>;
79
+ declare namespace index_d_exports {
80
+ export { CategoryBucket, CategoryPostLike, DEFAULT_TAG_BASE, PostCategory, PostLike, PostParams, PostUrlBuilder, categoryLabel, cn$1 as cn, createCategoryIndex, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagLabel, tagUrl, uniqueTags };
81
+ }
82
+ //#endregion
83
+ export { PostParams as _, uniqueTags as a, cn$1 as b, readingTime as c, categoryLabel as d, CategoryBucket as f, PostLike as g, createCategoryIndex as h, tagUrl as i, getRelatedPosts as l, PostCategory as m, DEFAULT_TAG_BASE as n, slugify as o, CategoryPostLike as p, tagLabel as r, requireEnv as s, index_d_exports as t, formatDate as u, PostUrlBuilder as v, createPostUrl as y };
84
+ //# sourceMappingURL=index-B8l1pSeN.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-iwkIePdz.js";
2
+ import { t as index_d_exports$1 } from "./index-B8l1pSeN.js";
3
3
  export { type Author, type Post, type Tag, index_d_exports as content, index_d_exports$1 as lib };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { t as content_exports } from "./content/index.js";
2
- import { t as lib_exports } from "./lib-D7HdNeEs.js";
2
+ import { t as lib_exports } from "./lib-6eq4E-OX.js";
3
3
 
4
4
  export { content_exports as content, lib_exports as lib };
@@ -1,2 +1,2 @@
1
- import { a as requireEnv, c as PostParams, d as getRelatedPosts, f as formatDate, i as slugify, l as PostUrlBuilder, m as cn, n as DEFAULT_TAG_BASE, o as readingTime, p as categoryLabel, r as tagUrl, s as PostLike, u as createPostUrl } from "../index-iwkIePdz.js";
2
- export { DEFAULT_TAG_BASE, type PostLike, type PostParams, type PostUrlBuilder, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
1
+ import { _ as PostParams, a as uniqueTags, b as cn, c as readingTime, d as categoryLabel, f as CategoryBucket, g as PostLike, h as createCategoryIndex, i as tagUrl, l as getRelatedPosts, m as PostCategory, n as DEFAULT_TAG_BASE, o as slugify, p as CategoryPostLike, r as tagLabel, s as requireEnv, u as formatDate, v as PostUrlBuilder, y as createPostUrl } from "../index-B8l1pSeN.js";
2
+ export { type CategoryBucket, type CategoryPostLike, DEFAULT_TAG_BASE, type PostCategory, type PostLike, type PostParams, type PostUrlBuilder, categoryLabel, cn, createCategoryIndex, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagLabel, tagUrl, uniqueTags };
package/dist/lib/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { t as cn } from "../cn-B2A3VjBM.js";
2
- import { a as readingTime, c as getRelatedPosts, i as requireEnv, l as formatDate, n as DEFAULT_TAG_BASE, o as createPostUrl, r as tagUrl, s as slugify, u as categoryLabel } from "../lib-D7HdNeEs.js";
1
+ import { a as uniqueTags, c as getRelatedPosts, d as createPostUrl, f as slugify, i as tagUrl, l as formatDate, m as cn, n as DEFAULT_TAG_BASE, o as requireEnv, p as categoryLabel, r as tagLabel, s as readingTime, u as createCategoryIndex } from "../lib-6eq4E-OX.js";
3
2
 
4
- export { DEFAULT_TAG_BASE, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
3
+ export { DEFAULT_TAG_BASE, categoryLabel, cn, createCategoryIndex, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagLabel, tagUrl, uniqueTags };
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-w6R9maHv.js";
2
- import { t as cn } from "./cn-B2A3VjBM.js";
2
+ import { byPubDateDesc } from "./content/index.js";
3
+ import { cn as cn$1 } from "cn";
3
4
 
4
5
  //#region src/lib/category-label.ts
5
6
  const NAMED_ENTITIES = {
@@ -32,6 +33,103 @@ const decodeEntity = (entity, decimal, hex, named) => {
32
33
  };
33
34
  const categoryLabel = (name) => name.replaceAll(ENTITY_PATTERN, decodeEntity);
34
35
 
36
+ //#endregion
37
+ //#region src/lib/slugify.ts
38
+ const slugify = (input) => input.normalize("NFD").replaceAll(new RegExp("[̀-ͯ]", "gv"), "").toLowerCase().replaceAll(new RegExp("[^a-z0-9\\s\\-]", "gv"), "").trim().replaceAll(new RegExp("[\\s\\-]+", "gv"), "-").replaceAll(new RegExp("^-+|-+$", "gv"), "");
39
+
40
+ //#endregion
41
+ //#region src/lib/post-url.ts
42
+ const createPostUrl = (categorySlugMap, defaultCategory) => {
43
+ const postParams = (post) => {
44
+ const canonical = post.data.seo?.canonical_url;
45
+ if (canonical !== void 0 && canonical !== "") try {
46
+ const segments = new URL(canonical).pathname.split("/").filter(Boolean);
47
+ const category = segments.at(-2);
48
+ const slug = segments.at(-1);
49
+ if (category !== void 0 && slug !== void 0) return {
50
+ category,
51
+ slug
52
+ };
53
+ } catch {}
54
+ const first = post.data.categories?.at(0);
55
+ return {
56
+ category: first !== void 0 && first !== "" ? categorySlugMap[first] ?? slugify(first) : defaultCategory,
57
+ slug: post.id
58
+ };
59
+ };
60
+ const postUrl = (post) => {
61
+ const { category, slug } = postParams(post);
62
+ return `/${category}/${slug}/`;
63
+ };
64
+ return {
65
+ postParams,
66
+ postUrl
67
+ };
68
+ };
69
+
70
+ //#endregion
71
+ //#region src/lib/category-index.ts
72
+ const humanize = (slug) => {
73
+ const words = slug.replaceAll("-", " ");
74
+ return words.charAt(0).toUpperCase() + words.slice(1);
75
+ };
76
+ const createCategoryIndex = (categorySlugMap, defaultCategory, posts) => {
77
+ const { postParams } = createPostUrl(categorySlugMap, defaultCategory);
78
+ const categorySlug = (name) => categorySlugMap[name] ?? slugify(name);
79
+ const namesBySlug = /* @__PURE__ */ new Map();
80
+ const register = (slug, name) => {
81
+ namesBySlug.set(slug, (namesBySlug.get(slug) ?? /* @__PURE__ */ new Set()).add(name));
82
+ };
83
+ for (const post of posts) {
84
+ const { category } = postParams(post);
85
+ const names = post.data.categories;
86
+ for (const name of names) register(categorySlug(name), name);
87
+ const first = names.at(0);
88
+ if (first !== void 0 && !names.some((name) => categorySlug(name) === category)) register(category, first);
89
+ }
90
+ const categoryName = (slug) => {
91
+ const names = [...namesBySlug.get(slug) ?? []];
92
+ const native = names.find((name) => slugify(categoryLabel(name)) === slug);
93
+ const mapped = names.find((name) => categorySlug(name) === slug);
94
+ const name = native ?? mapped ?? names.at(0);
95
+ return name === void 0 ? humanize(slug) : categoryLabel(name);
96
+ };
97
+ const postCategory = (post) => {
98
+ const { category } = postParams(post);
99
+ return {
100
+ name: categoryName(category),
101
+ slug: category
102
+ };
103
+ };
104
+ const groupByCategory = ({ includeSecondaryCategories = false } = {}) => {
105
+ const postsBySlug = /* @__PURE__ */ new Map();
106
+ const add = (slug, post) => {
107
+ const bucket = postsBySlug.get(slug);
108
+ if (bucket) bucket.push(post);
109
+ else postsBySlug.set(slug, [post]);
110
+ };
111
+ for (const post of posts) {
112
+ const { category } = postParams(post);
113
+ add(category, post);
114
+ if (!includeSecondaryCategories) continue;
115
+ for (const name of post.data.categories) {
116
+ const slug = categorySlug(name);
117
+ if (slug !== category) add(slug, post);
118
+ }
119
+ }
120
+ return [...postsBySlug].map(([slug, bucketPosts]) => ({
121
+ name: categoryName(slug),
122
+ posts: bucketPosts.toSorted(byPubDateDesc),
123
+ slug
124
+ }));
125
+ };
126
+ return {
127
+ categoryName,
128
+ groupByCategory,
129
+ postCategory
130
+ };
131
+ };
132
+
35
133
  //#endregion
36
134
  //#region src/lib/format-date.ts
37
135
  const defaultOptions = {
@@ -75,40 +173,6 @@ const getRelatedPosts = (current, all, n) => {
75
173
  return scored.toSorted((x, y) => y.score - x.score).slice(0, n).map(({ post }) => post);
76
174
  };
77
175
 
78
- //#endregion
79
- //#region src/lib/slugify.ts
80
- const slugify = (input) => input.normalize("NFD").replaceAll(new RegExp("[̀-ͯ]", "gv"), "").toLowerCase().replaceAll(new RegExp("[^a-z0-9\\s\\-]", "gv"), "").trim().replaceAll(new RegExp("[\\s\\-]+", "gv"), "-").replaceAll(new RegExp("^-+|-+$", "gv"), "");
81
-
82
- //#endregion
83
- //#region src/lib/post-url.ts
84
- const createPostUrl = (categorySlugMap, defaultCategory) => {
85
- const postParams = (post) => {
86
- const canonical = post.data.seo?.canonical_url;
87
- if (canonical !== void 0 && canonical !== "") try {
88
- const segments = new URL(canonical).pathname.split("/").filter(Boolean);
89
- const category = segments.at(-2);
90
- const slug = segments.at(-1);
91
- if (category !== void 0 && slug !== void 0) return {
92
- category,
93
- slug
94
- };
95
- } catch {}
96
- const first = post.data.categories?.at(0);
97
- return {
98
- category: first !== void 0 && first !== "" ? categorySlugMap[first] ?? slugify(first) : defaultCategory,
99
- slug: post.id
100
- };
101
- };
102
- const postUrl = (post) => {
103
- const { category, slug } = postParams(post);
104
- return `/${category}/${slug}/`;
105
- };
106
- return {
107
- postParams,
108
- postUrl
109
- };
110
- };
111
-
112
176
  //#endregion
113
177
  //#region src/lib/reading-time.ts
114
178
  const WORDS_PER_MINUTE = 200;
@@ -133,22 +197,35 @@ const requireEnv = (name) => {
133
197
  //#region src/lib/tag-url.ts
134
198
  const DEFAULT_TAG_BASE = "/tag";
135
199
  const tagUrl = (tag, base = DEFAULT_TAG_BASE) => `${base.replace(new RegExp("\\/+$", "v"), "")}/${slugify(tag)}/`;
200
+ const tagLabel = (tag) => tag.replace(new RegExp("^#", "v"), "");
201
+ const uniqueTags = (tags) => {
202
+ const seen = /* @__PURE__ */ new Set();
203
+ return tags.filter((tag) => {
204
+ const slug = slugify(tag);
205
+ if (seen.has(slug)) return false;
206
+ seen.add(slug);
207
+ return true;
208
+ });
209
+ };
136
210
 
137
211
  //#endregion
138
212
  //#region src/lib/index.ts
139
213
  var lib_exports = /* @__PURE__ */ __exportAll({
140
214
  DEFAULT_TAG_BASE: () => DEFAULT_TAG_BASE,
141
215
  categoryLabel: () => categoryLabel,
142
- cn: () => cn,
216
+ cn: () => cn$1,
217
+ createCategoryIndex: () => createCategoryIndex,
143
218
  createPostUrl: () => createPostUrl,
144
219
  formatDate: () => formatDate,
145
220
  getRelatedPosts: () => getRelatedPosts,
146
221
  readingTime: () => readingTime,
147
222
  requireEnv: () => requireEnv,
148
223
  slugify: () => slugify,
149
- tagUrl: () => tagUrl
224
+ tagLabel: () => tagLabel,
225
+ tagUrl: () => tagUrl,
226
+ uniqueTags: () => uniqueTags
150
227
  });
151
228
 
152
229
  //#endregion
153
- export { readingTime as a, getRelatedPosts as c, requireEnv as i, formatDate as l, DEFAULT_TAG_BASE as n, createPostUrl as o, tagUrl as r, slugify as s, lib_exports as t, categoryLabel as u };
154
- //# sourceMappingURL=lib-D7HdNeEs.js.map
230
+ export { uniqueTags as a, getRelatedPosts as c, createPostUrl as d, slugify as f, tagUrl as i, formatDate as l, cn$1 as m, DEFAULT_TAG_BASE as n, requireEnv as o, categoryLabel as p, tagLabel as r, readingTime as s, lib_exports as t, createCategoryIndex as u };
231
+ //# sourceMappingURL=lib-6eq4E-OX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib-6eq4E-OX.js","names":[],"sources":["../src/lib/category-label.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/category-index.ts","../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/reading-time.ts","../src/lib/require-env.ts","../src/lib/tag-url.ts","../src/lib/index.ts"],"sourcesContent":["const NAMED_ENTITIES = {\n amp: \"&\",\n apos: \"'\",\n gt: \">\",\n hellip: \"\\u2026\",\n ldquo: \"\\u201c\",\n lsquo: \"\\u2018\",\n lt: \"<\",\n nbsp: \"\\u00a0\",\n ndash: \"\\u2013\",\n quot: '\"',\n rdquo: \"\\u201d\",\n rsquo: \"\\u2019\",\n} as const;\n\nconst MAX_CODE_POINT = 1_114_111;\nconst EM_DASH = 8212;\nconst HEX_RADIX = 16;\nconst ENTITY_PATTERN = /&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));/giv;\n\nconst isNamedEntity = (key: string): key is keyof typeof NAMED_ENTITIES =>\n Object.hasOwn(NAMED_ENTITIES, key);\n\nconst fromCodePoint = (codePoint: number, entity: string): string =>\n Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH\n ? String.fromCodePoint(codePoint)\n : entity;\n\nconst decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {\n if (decimal !== undefined) {\n return fromCodePoint(Number(decimal), entity);\n }\n if (hex !== undefined) {\n return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);\n }\n if (named === undefined) {\n return entity;\n }\n const key = named.toLowerCase();\n if (!isNamedEntity(key)) {\n return entity;\n }\n return NAMED_ENTITIES[key];\n};\n\nconst categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);\n\nexport { categoryLabel };\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\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 !== undefined && 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 !== undefined && slug !== undefined) {\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 =\n first !== undefined && first !== \"\"\n ? (categorySlugMap[first] ?? slugify(first))\n : 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","import { byPubDateDesc } from \"../content/index\";\nimport { categoryLabel } from \"./category-label\";\nimport type { PostLike } from \"./post-url\";\nimport { createPostUrl } from \"./post-url\";\nimport { slugify } from \"./slugify\";\n\ntype CategoryPostLike = PostLike & { data: { categories: Array<string>; pubDate: Date } };\n\ntype CategoryBucket<P> = { name: string; posts: Array<P>; slug: string };\n\ntype PostCategory = { name: string; slug: string };\n\ntype GroupOptions = {\n includeSecondaryCategories?: boolean;\n};\n\nconst humanize = (slug: string): string => {\n const words = slug.replaceAll(\"-\", \" \");\n return words.charAt(0).toUpperCase() + words.slice(1);\n};\n\nconst createCategoryIndex = <P extends CategoryPostLike>(\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n posts: Array<P>,\n) => {\n const { postParams } = createPostUrl(categorySlugMap, defaultCategory);\n const categorySlug = (name: string) => categorySlugMap[name] ?? slugify(name);\n\n const namesBySlug = new Map<string, Set<string>>();\n const register = (slug: string, name: string) => {\n namesBySlug.set(slug, (namesBySlug.get(slug) ?? new Set()).add(name));\n };\n for (const post of posts) {\n const { category } = postParams(post);\n const names = post.data.categories;\n for (const name of names) {\n register(categorySlug(name), name);\n }\n const first = names.at(0);\n if (first !== undefined && !names.some((name) => categorySlug(name) === category)) {\n register(category, first);\n }\n }\n\n const categoryName = (slug: string): string => {\n const names = [...(namesBySlug.get(slug) ?? [])];\n const native = names.find((name) => slugify(categoryLabel(name)) === slug);\n const mapped = names.find((name) => categorySlug(name) === slug);\n const name = native ?? mapped ?? names.at(0);\n return name === undefined ? humanize(slug) : categoryLabel(name);\n };\n\n const postCategory = (post: PostLike): PostCategory => {\n const { category } = postParams(post);\n return { name: categoryName(category), slug: category };\n };\n\n const groupByCategory = ({ includeSecondaryCategories = false }: GroupOptions = {}): Array<\n CategoryBucket<P>\n > => {\n const postsBySlug = new Map<string, Array<P>>();\n const add = (slug: string, post: P) => {\n const bucket = postsBySlug.get(slug);\n if (bucket) {\n bucket.push(post);\n } else {\n postsBySlug.set(slug, [post]);\n }\n };\n\n for (const post of posts) {\n const { category } = postParams(post);\n add(category, post);\n if (!includeSecondaryCategories) {\n continue;\n }\n for (const name of post.data.categories) {\n const slug = categorySlug(name);\n if (slug !== category) {\n add(slug, post);\n }\n }\n }\n\n return [...postsBySlug].map(([slug, bucketPosts]) => ({\n name: categoryName(slug),\n posts: bucketPosts.toSorted(byPubDateDesc),\n slug,\n }));\n };\n\n return { categoryName, groupByCategory, postCategory };\n};\n\nexport type { CategoryBucket, CategoryPostLike, PostCategory };\nexport { createCategoryIndex };\n","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 = (key: string, locale: string, options: Intl.DateTimeFormatOptions) => {\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 = (date: Date, locale: string = \"en-US\", options?: Intl.DateTimeFormatOptions) => {\n if (!options) {\n return getFormatter(locale, locale, defaultOptions).format(date);\n }\n const merged = { ...defaultOptions, ...options };\n return getFormatter(`${locale}:${JSON.stringify(merged)}`, locale, merged).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 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 requireEnv = (name: string): string => {\n const value = process.env[name];\n if (value === undefined || value === \"\") {\n throw new Error(`Missing required env var: ${name}`);\n }\n return value;\n};\n\nexport { requireEnv };\n","import { slugify } from \"./slugify\";\n\nconst DEFAULT_TAG_BASE = \"/tag\";\n\nconst tagUrl = (tag: string, base: string = DEFAULT_TAG_BASE): string =>\n `${base.replace(/\\/+$/v, \"\")}/${slugify(tag)}/`;\n\nconst tagLabel = (tag: string): string => tag.replace(/^#/v, \"\");\n\nconst uniqueTags = (tags: Array<string>): Array<string> => {\n const seen = new Set<string>();\n return tags.filter((tag) => {\n const slug = slugify(tag);\n if (seen.has(slug)) {\n return false;\n }\n seen.add(slug);\n return true;\n });\n};\n\nexport { DEFAULT_TAG_BASE, tagLabel, tagUrl, uniqueTags };\n","export { cn } from \"./cn\";\nexport type { CategoryBucket, CategoryPostLike, PostCategory } from \"./category-index\";\nexport { createCategoryIndex } from \"./category-index\";\nexport { categoryLabel } from \"./category-label\";\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 { requireEnv } from \"./require-env\";\nexport { slugify } from \"./slugify\";\nexport { DEFAULT_TAG_BASE, tagLabel, tagUrl, uniqueTags } from \"./tag-url\";\n"],"mappings":"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"}
@@ -2,6 +2,7 @@
2
2
  It contributes tokens, base styles, prose, and the dark variant. */
3
3
 
4
4
  @import "tw-animate-css";
5
+ @import "shadcn/tailwind.css";
5
6
 
6
7
  /* Scan our own .astro/.tsx files so consumer Tailwind generates utilities
7
8
  used by the theme. Paths resolve from dist/styles/globals.css after build:
@@ -9,10 +10,17 @@
9
10
  @source "../../src/astro/**/*.astro";
10
11
  @source "../../src/layouts/**/*.astro";
11
12
  @source "../../src/components/**/*.{ts,tsx}";
13
+ @source "../../src/compositions/**/*.{ts,tsx}";
12
14
 
13
15
  @custom-variant dark (&:is(.dark *));
14
16
 
15
17
  @theme {
18
+ --tracking-eyebrow: 0.16em;
19
+ --tracking-list-title: -0.045em;
20
+ --leading-list-title: 0.95;
21
+ --measure-list-title: 14ch;
22
+ --measure-list-intro: 38ch;
23
+
16
24
  --color-background: oklch(1 0 0);
17
25
  --color-foreground: oklch(0.145 0 0);
18
26
  --color-card: oklch(1 0 0);
@@ -24,7 +32,7 @@
24
32
  --color-secondary: oklch(0.97 0 0);
25
33
  --color-secondary-foreground: oklch(0.205 0 0);
26
34
  --color-muted: oklch(0.97 0 0);
27
- --color-muted-foreground: oklch(0.556 0 0);
35
+ --color-muted-foreground: oklch(0.54 0 0);
28
36
  --color-accent: oklch(0.97 0 0);
29
37
  --color-accent-foreground: oklch(0.205 0 0);
30
38
  --color-destructive: oklch(0.577 0.245 27.325);
@@ -264,3 +272,58 @@
264
272
  text-align: left;
265
273
  }
266
274
  }
275
+
276
+ @utility post-grid {
277
+ display: grid;
278
+ gap: 1px;
279
+ overflow: hidden;
280
+ border: 1px solid var(--color-border);
281
+ background: var(--color-border);
282
+ }
283
+
284
+ .post-grid .post-card {
285
+ border: 0;
286
+ background: var(--color-background);
287
+ padding: 1.75rem;
288
+ }
289
+
290
+ .post-grid .post-card:first-child h2 {
291
+ max-width: 18ch;
292
+ font-family: var(--font-serif);
293
+ font-size: clamp(1.75rem, 4vw, 2.75rem);
294
+ line-height: 1.05;
295
+ }
296
+
297
+ @media (min-width: 48rem) {
298
+ .post-grid {
299
+ grid-template-columns: repeat(2, minmax(0, 1fr));
300
+ }
301
+
302
+ .post-grid .post-card:first-child {
303
+ grid-column: 1 / -1;
304
+ min-height: 18rem;
305
+ display: flex;
306
+ flex-direction: column;
307
+ justify-content: end;
308
+ padding: 3rem;
309
+ }
310
+ }
311
+
312
+ /* Keep first 3 eager so above-the-fold paint isn't gated on containment checks. */
313
+ @utility post-card {
314
+ &:nth-child(n + 4) {
315
+ content-visibility: auto;
316
+ contain-intrinsic-size: auto 200px;
317
+ }
318
+ }
319
+
320
+ @utility grid-cols-list-heading {
321
+ grid-template-columns: 1.2fr 0.8fr;
322
+ }
323
+
324
+ @theme inline {
325
+ --radius-control-sm: min(var(--radius-md), 10px);
326
+ --radius-control: min(var(--radius-md), 12px);
327
+ --text-control: 0.8rem;
328
+ --color-secondary-hover: color-mix(in oklch, var(--color-secondary), var(--color-foreground) 5%);
329
+ }