fulldev-ui 0.11.0 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Fulldev UI
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#190](https://github.com/fulldotdev/ui/pull/190) [`a7b9580`](https://github.com/fulldotdev/ui/commit/a7b9580c416a72abb4d09b721d2cba917c806f1b) Thanks [@silveltman](https://github.com/silveltman)! - Fix repeated banner initialization, render informational contact icons without inactive buttons, and improve narrow documentation headers and prose link contrast.
8
+
3
9
  ## 0.11.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fulldev-ui",
3
3
  "type": "module",
4
- "version": "0.11.0",
4
+ "version": "0.11.1",
5
5
  "description": "Open-source Astro UI components and blocks",
6
6
  "homepage": "https://ui.full.dev",
7
7
  "bugs": "https://github.com/fulldotdev/ui/issues",
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "src/components/blocks/banner-2.astro",
13
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Banner, BannerContainer } from \"@/components/ui/banner\"\nimport { Button } from \"@/components/ui/button\"\n\ntype Props = HTMLAttributes<\"aside\"> & {\n title: string\n description: string\n buttons?: {\n label: string\n href: string\n variant?: \"default\" | \"outline\" | \"secondary\" | \"ghost\"\n }[]\n}\n\nconst { class: className, title, description, buttons, ...props } = Astro.props\n---\n\n<Banner\n variant=\"floating\"\n storageKey=\"banner-2\"\n class={cn(\"\", className)}\n {...props}\n>\n <BannerContainer class=\"gap-4 py-1\" showClose={false}>\n <div\n class=\"flex flex-1 flex-col gap-0.5 @5xl:flex-row @5xl:items-center @5xl:gap-x-4\"\n >\n <p class=\"text-sm font-semibold\">{title}</p>\n <p class=\"text-muted-foreground text-sm\">{description}</p>\n </div>\n {\n buttons?.length && (\n <div class=\"flex shrink-0 items-center gap-2\">\n {buttons.map(({ href, label, ...button }, index) => (\n <Button\n as=\"a\"\n href={href}\n size=\"sm\"\n variant={button.variant ?? (index === 0 ? \"default\" : \"outline\")}\n {...button}\n >\n {label}\n </Button>\n ))}\n </div>\n )\n }\n </BannerContainer>\n</Banner>\n",
13
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Banner, BannerContainer } from \"@/components/ui/banner\"\nimport { Button } from \"@/components/ui/button\"\n\ntype Props = HTMLAttributes<\"aside\"> & {\n title: string\n description: string\n buttons?: {\n label: string\n href: string\n variant?: \"default\" | \"outline\" | \"secondary\" | \"ghost\"\n }[]\n}\n\nconst { class: className, title, description, buttons, ...props } = Astro.props\n---\n\n<Banner\n variant=\"floating\"\n storageKey=\"banner-2\"\n class={cn(\"@container\", className)}\n {...props}\n>\n <BannerContainer\n class=\"flex-col items-start gap-4 py-1 @2xl:flex-row @2xl:items-center\"\n showClose={false}\n >\n <div\n class=\"flex min-w-0 flex-1 flex-col gap-0.5 @5xl:flex-row @5xl:items-center @5xl:gap-x-4\"\n >\n <p class=\"text-sm font-semibold\">{title}</p>\n <p class=\"text-muted-foreground text-sm\">{description}</p>\n </div>\n {\n buttons?.length && (\n <div class=\"flex flex-wrap items-center gap-2\">\n {buttons.map(({ href, label, ...button }, index) => (\n <Button\n as=\"a\"\n href={href}\n size=\"sm\"\n variant={button.variant ?? (index === 0 ? \"default\" : \"outline\")}\n {...button}\n >\n {label}\n </Button>\n ))}\n </div>\n )\n }\n </BannerContainer>\n</Banner>\n",
14
14
  "type": "registry:block"
15
15
  }
16
16
  ],
@@ -13,7 +13,7 @@
13
13
  "files": [
14
14
  {
15
15
  "path": "src/components/ui/banner/banner.astro",
16
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"aside\"> &\n VariantProps<typeof variants> & {\n storageKey?: string\n }\n\nconst variants = cva(\"relative flex items-center py-2\", {\n variants: {\n variant: {\n default: \"bg-foreground text-background w-full\",\n floating:\n \"bg-background mx-auto my-2 w-[calc(100%-2rem)] max-w-[calc(var(--container,1280px)-2rem)] overflow-hidden rounded-lg border shadow-sm\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nconst { class: className, variant, storageKey, ...props } = Astro.props\n---\n\n<aside\n class={cn(variants({ variant }), className)}\n data-slot=\"banner\"\n data-banner-storage-key={storageKey}\n data-variant={variant}\n {...props}\n>\n <slot />\n</aside>\n\n<script is:inline>\n const initializedKey = \"fulldevBannerInitialized\"\n\n if (!document.documentElement.dataset[initializedKey]) {\n document.documentElement.dataset[initializedKey] = \"true\"\n\n const getStorageToken = (key) => `banner:${key}`\n\n const syncBanners = () => {\n document.querySelectorAll(\"[data-slot='banner']\").forEach((banner) => {\n if (!(banner instanceof HTMLElement)) return\n\n const storageKey = banner.dataset.bannerStorageKey\n\n if (\n storageKey &&\n sessionStorage.getItem(getStorageToken(storageKey)) === \"hidden\"\n ) {\n banner.hidden = true\n }\n })\n }\n\n syncBanners()\n\n document.addEventListener(\"click\", (event) => {\n const target = event.target\n\n if (!(target instanceof Element)) return\n\n const closeButton = target.closest(\"[data-slot='banner-close']\")\n\n if (!(closeButton instanceof HTMLElement)) return\n\n const banner = closeButton.closest(\"[data-slot='banner']\")\n\n if (!(banner instanceof HTMLElement)) return\n\n banner.hidden = true\n\n const storageKey = banner.dataset.bannerStorageKey\n\n if (storageKey) {\n sessionStorage.setItem(getStorageToken(storageKey), \"hidden\")\n }\n })\n }\n</script>\n",
16
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Props = HTMLAttributes<\"aside\"> &\n VariantProps<typeof variants> & {\n storageKey?: string\n }\n\nconst variants = cva(\"relative flex items-center py-2\", {\n variants: {\n variant: {\n default: \"bg-foreground text-background w-full\",\n floating:\n \"bg-background mx-auto my-2 w-[calc(100%-2rem)] max-w-[calc(var(--container,1280px)-2rem)] overflow-hidden rounded-lg border shadow-sm\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n})\n\nconst { class: className, variant, storageKey, ...props } = Astro.props\n---\n\n<aside\n class={cn(variants({ variant }), className)}\n data-slot=\"banner\"\n data-banner-storage-key={storageKey}\n data-variant={variant}\n {...props}\n>\n <slot />\n</aside>\n\n<script>\n const getStorageToken = (key: string) => `banner:${key}`\n\n const syncBanners = () => {\n document.querySelectorAll(\"[data-slot='banner']\").forEach((banner) => {\n if (!(banner instanceof HTMLElement)) return\n const storageKey = banner.dataset.bannerStorageKey\n if (\n storageKey &&\n sessionStorage.getItem(getStorageToken(storageKey)) === \"hidden\"\n ) {\n banner.hidden = true\n }\n })\n }\n\n syncBanners()\n document.addEventListener(\"astro:page-load\", syncBanners)\n\n document.addEventListener(\"click\", (event) => {\n if (!(event.target instanceof Element)) return\n const closeButton = event.target.closest(\"[data-slot='banner-close']\")\n const banner = closeButton?.closest(\"[data-slot='banner']\")\n if (!(banner instanceof HTMLElement)) return\n banner.hidden = true\n const storageKey = banner.dataset.bannerStorageKey\n if (storageKey)\n sessionStorage.setItem(getStorageToken(storageKey), \"hidden\")\n })\n</script>\n",
17
17
  "type": "registry:ui"
18
18
  },
19
19
  {
@@ -18,7 +18,7 @@
18
18
  "files": [
19
19
  {
20
20
  "path": "src/components/blocks/contact-1.astro",
21
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Field,\n FieldDescription,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n} from \"@/components/ui/field\"\nimport { Form } from \"@/components/ui/form\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Section,\n SectionContainer,\n SectionDescription,\n SectionTitle,\n} from \"@/components/ui/section\"\nimport { Textarea } from \"@/components/ui/textarea\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title: string\n description: string\n contactItems: {\n icon: string\n title: string\n description: string\n href?: string\n }[]\n form: {\n id?: string\n action?: string\n method?: \"get\" | \"post\"\n legend: string\n nameLabel: string\n namePlaceholder: string\n emailLabel: string\n emailPlaceholder: string\n messageLabel: string\n messagePlaceholder: string\n messageDescription: string\n actionLabel: string\n }\n}\n\nconst {\n class: className,\n title,\n description,\n contactItems,\n form,\n ...props\n} = Astro.props\nconst formId = form.id ?? `contact-${crypto.randomUUID()}`\n---\n\n<Section class={cn(\"\", className)} {...props}>\n <SectionContainer class=\"items-center gap-6 text-center\">\n <div class=\"flex max-w-2xl flex-col items-center gap-4\">\n <SectionTitle>\n {title}\n </SectionTitle>\n <SectionDescription class=\"max-w-2xl\">\n {description}\n </SectionDescription>\n </div>\n </SectionContainer>\n <SectionContainer>\n <div class=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {\n contactItems.map((item) => (\n <div class=\"flex flex-col gap-3 rounded-xl border p-5\">\n {item.href ? (\n <Button\n as=\"a\"\n href={item.href}\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n ) : (\n <Button\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n )}\n <h3 class=\"text-foreground text-sm font-medium\">{item.title}</h3>\n <p class=\"text-muted-foreground text-sm leading-relaxed\">\n {item.description}\n </p>\n </div>\n ))\n }\n </div>\n </SectionContainer>\n <SectionContainer>\n <div class=\"mx-auto w-full max-w-2xl rounded-xl border p-6 sm:p-8\">\n <Form id={formId} action={form.action} method={form.method ?? \"post\"}>\n <FieldSet>\n <FieldLegend class=\"sr-only\">{form.legend}</FieldLegend>\n <FieldGroup>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <Field>\n <FieldLabel for={`${formId}-name`}>{form.nameLabel}</FieldLabel>\n <Input\n id={`${formId}-name`}\n name=\"name\"\n autocomplete=\"name\"\n required\n placeholder={form.namePlaceholder}\n />\n </Field>\n <Field>\n <FieldLabel for={`${formId}-email`}\n >{form.emailLabel}</FieldLabel\n >\n <Input\n id={`${formId}-email`}\n name=\"email\"\n autocomplete=\"email\"\n required\n type=\"email\"\n placeholder={form.emailPlaceholder}\n />\n </Field>\n </div>\n <Field>\n <FieldLabel for={`${formId}-message`}\n >{form.messageLabel}</FieldLabel\n >\n <Textarea\n id={`${formId}-message`}\n name=\"message\"\n required\n rows={5}\n placeholder={form.messagePlaceholder}\n />\n <FieldDescription>{form.messageDescription}</FieldDescription>\n </Field>\n <Button type=\"submit\" size=\"lg\" class=\"w-full\"\n >{form.actionLabel}</Button\n >\n </FieldGroup>\n </FieldSet>\n </Form>\n </div>\n </SectionContainer>\n</Section>\n",
21
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Field,\n FieldDescription,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n} from \"@/components/ui/field\"\nimport { Form } from \"@/components/ui/form\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Section,\n SectionContainer,\n SectionDescription,\n SectionTitle,\n} from \"@/components/ui/section\"\nimport { Textarea } from \"@/components/ui/textarea\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title: string\n description: string\n contactItems: {\n icon: string\n title: string\n description: string\n href?: string\n }[]\n form: {\n id?: string\n action?: string\n method?: \"get\" | \"post\"\n legend: string\n nameLabel: string\n namePlaceholder: string\n emailLabel: string\n emailPlaceholder: string\n messageLabel: string\n messagePlaceholder: string\n messageDescription: string\n actionLabel: string\n }\n}\n\nconst {\n class: className,\n title,\n description,\n contactItems,\n form,\n ...props\n} = Astro.props\nconst formId = form.id ?? `contact-${crypto.randomUUID()}`\n---\n\n<Section class={cn(\"\", className)} {...props}>\n <SectionContainer class=\"items-center gap-6 text-center\">\n <div class=\"flex max-w-2xl flex-col items-center gap-4\">\n <SectionTitle>\n {title}\n </SectionTitle>\n <SectionDescription class=\"max-w-2xl\">\n {description}\n </SectionDescription>\n </div>\n </SectionContainer>\n <SectionContainer>\n <div class=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {\n contactItems.map((item) => (\n <div class=\"flex flex-col gap-3 rounded-xl border p-5\">\n {item.href ? (\n <Button\n as=\"a\"\n href={item.href}\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n ) : (\n <Button\n as=\"span\"\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary\"\n aria-hidden=\"true\"\n >\n <Icon name={item.icon} />\n </Button>\n )}\n <h3 class=\"text-foreground text-sm font-medium\">{item.title}</h3>\n <p class=\"text-muted-foreground text-sm leading-relaxed\">\n {item.description}\n </p>\n </div>\n ))\n }\n </div>\n </SectionContainer>\n <SectionContainer>\n <div class=\"mx-auto w-full max-w-2xl rounded-xl border p-6 sm:p-8\">\n <Form id={formId} action={form.action} method={form.method ?? \"post\"}>\n <FieldSet>\n <FieldLegend class=\"sr-only\">{form.legend}</FieldLegend>\n <FieldGroup>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <Field>\n <FieldLabel for={`${formId}-name`}>{form.nameLabel}</FieldLabel>\n <Input\n id={`${formId}-name`}\n name=\"name\"\n autocomplete=\"name\"\n required\n placeholder={form.namePlaceholder}\n />\n </Field>\n <Field>\n <FieldLabel for={`${formId}-email`}\n >{form.emailLabel}</FieldLabel\n >\n <Input\n id={`${formId}-email`}\n name=\"email\"\n autocomplete=\"email\"\n required\n type=\"email\"\n placeholder={form.emailPlaceholder}\n />\n </Field>\n </div>\n <Field>\n <FieldLabel for={`${formId}-message`}\n >{form.messageLabel}</FieldLabel\n >\n <Textarea\n id={`${formId}-message`}\n name=\"message\"\n required\n rows={5}\n placeholder={form.messagePlaceholder}\n />\n <FieldDescription>{form.messageDescription}</FieldDescription>\n </Field>\n <Button type=\"submit\" size=\"lg\" class=\"w-full\"\n >{form.actionLabel}</Button\n >\n </FieldGroup>\n </FieldSet>\n </Form>\n </div>\n </SectionContainer>\n</Section>\n",
22
22
  "type": "registry:block"
23
23
  }
24
24
  ],
@@ -18,7 +18,7 @@
18
18
  "files": [
19
19
  {
20
20
  "path": "src/components/blocks/contact-2.astro",
21
- "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Field,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n} from \"@/components/ui/field\"\nimport { Form } from \"@/components/ui/form\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Section,\n SectionContainer,\n SectionTitle,\n} from \"@/components/ui/section\"\nimport { Textarea } from \"@/components/ui/textarea\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title: string\n description: string\n contactItems: {\n icon: string\n title: string\n description: string\n href?: string\n }[]\n form: {\n id?: string\n action?: string\n method?: \"get\" | \"post\"\n title: string\n legend: string\n nameLabel: string\n namePlaceholder: string\n emailLabel: string\n emailPlaceholder: string\n messageLabel: string\n messagePlaceholder: string\n actionLabel: string\n }\n}\n\nconst {\n class: className,\n title,\n description,\n contactItems,\n form,\n ...props\n} = Astro.props\nconst formId = form.id ?? `contact-${crypto.randomUUID()}`\n---\n\n<Section class={cn(\"\", className)} {...props}>\n <SectionContainer\n class=\"grid gap-10 lg:grid-cols-[minmax(0,1fr)_22rem] lg:items-start\"\n >\n <div class=\"flex flex-col gap-6\">\n <div class=\"flex flex-col gap-4\">\n <SectionTitle>\n {title}\n </SectionTitle>\n <p class=\"text-muted-foreground max-w-xl text-base leading-relaxed\">\n {description}\n </p>\n </div>\n <div class=\"rounded-xl border p-6\">\n <div class=\"mb-6\">\n <h3 class=\"text-lg font-medium\">{form.title}</h3>\n </div>\n <Form id={formId} action={form.action} method={form.method ?? \"post\"}>\n <FieldSet>\n <FieldLegend class=\"sr-only\">{form.legend}</FieldLegend>\n <FieldGroup>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <Field>\n <FieldLabel for={`${formId}-name`}\n >{form.nameLabel}</FieldLabel\n >\n <Input\n id={`${formId}-name`}\n name=\"name\"\n autocomplete=\"name\"\n required\n placeholder={form.namePlaceholder}\n />\n </Field>\n <Field>\n <FieldLabel for={`${formId}-email`}\n >{form.emailLabel}</FieldLabel\n >\n <Input\n id={`${formId}-email`}\n name=\"email\"\n autocomplete=\"email\"\n required\n type=\"email\"\n placeholder={form.emailPlaceholder}\n />\n </Field>\n </div>\n <Field>\n <FieldLabel for={`${formId}-message`}\n >{form.messageLabel}</FieldLabel\n >\n <Textarea\n id={`${formId}-message`}\n name=\"message\"\n required\n rows={5}\n placeholder={form.messagePlaceholder}\n />\n </Field>\n <Button type=\"submit\" size=\"lg\" class=\"w-full\"\n >{form.actionLabel}</Button\n >\n </FieldGroup>\n </FieldSet>\n </Form>\n </div>\n </div>\n\n <div class=\"flex flex-col gap-3\">\n {\n contactItems.map((item) => (\n <div class=\"flex gap-4 rounded-xl border p-4\">\n {item.href ? (\n <Button\n as=\"a\"\n href={item.href}\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n ) : (\n <Button\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n )}\n <div class=\"flex flex-col gap-1\">\n <p class=\"text-foreground text-sm font-medium\">{item.title}</p>\n <p class=\"text-muted-foreground text-sm leading-relaxed\">\n {item.description}\n </p>\n </div>\n </div>\n ))\n }\n </div>\n </SectionContainer>\n</Section>\n",
21
+ "content": "---\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Field,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n} from \"@/components/ui/field\"\nimport { Form } from \"@/components/ui/form\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Section,\n SectionContainer,\n SectionTitle,\n} from \"@/components/ui/section\"\nimport { Textarea } from \"@/components/ui/textarea\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title: string\n description: string\n contactItems: {\n icon: string\n title: string\n description: string\n href?: string\n }[]\n form: {\n id?: string\n action?: string\n method?: \"get\" | \"post\"\n title: string\n legend: string\n nameLabel: string\n namePlaceholder: string\n emailLabel: string\n emailPlaceholder: string\n messageLabel: string\n messagePlaceholder: string\n actionLabel: string\n }\n}\n\nconst {\n class: className,\n title,\n description,\n contactItems,\n form,\n ...props\n} = Astro.props\nconst formId = form.id ?? `contact-${crypto.randomUUID()}`\n---\n\n<Section class={cn(\"\", className)} {...props}>\n <SectionContainer\n class=\"grid gap-10 lg:grid-cols-[minmax(0,1fr)_22rem] lg:items-start\"\n >\n <div class=\"flex flex-col gap-6\">\n <div class=\"flex flex-col gap-4\">\n <SectionTitle>\n {title}\n </SectionTitle>\n <p class=\"text-muted-foreground max-w-xl text-base leading-relaxed\">\n {description}\n </p>\n </div>\n <div class=\"rounded-xl border p-6\">\n <div class=\"mb-6\">\n <h3 class=\"text-lg font-medium\">{form.title}</h3>\n </div>\n <Form id={formId} action={form.action} method={form.method ?? \"post\"}>\n <FieldSet>\n <FieldLegend class=\"sr-only\">{form.legend}</FieldLegend>\n <FieldGroup>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <Field>\n <FieldLabel for={`${formId}-name`}\n >{form.nameLabel}</FieldLabel\n >\n <Input\n id={`${formId}-name`}\n name=\"name\"\n autocomplete=\"name\"\n required\n placeholder={form.namePlaceholder}\n />\n </Field>\n <Field>\n <FieldLabel for={`${formId}-email`}\n >{form.emailLabel}</FieldLabel\n >\n <Input\n id={`${formId}-email`}\n name=\"email\"\n autocomplete=\"email\"\n required\n type=\"email\"\n placeholder={form.emailPlaceholder}\n />\n </Field>\n </div>\n <Field>\n <FieldLabel for={`${formId}-message`}\n >{form.messageLabel}</FieldLabel\n >\n <Textarea\n id={`${formId}-message`}\n name=\"message\"\n required\n rows={5}\n placeholder={form.messagePlaceholder}\n />\n </Field>\n <Button type=\"submit\" size=\"lg\" class=\"w-full\"\n >{form.actionLabel}</Button\n >\n </FieldGroup>\n </FieldSet>\n </Form>\n </div>\n </div>\n\n <div class=\"flex flex-col gap-3\">\n {\n contactItems.map((item) => (\n <div class=\"flex gap-4 rounded-xl border p-4\">\n {item.href ? (\n <Button\n as=\"a\"\n href={item.href}\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n ) : (\n <Button\n as=\"span\"\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-hidden=\"true\"\n >\n <Icon name={item.icon} />\n </Button>\n )}\n <div class=\"flex flex-col gap-1\">\n <p class=\"text-foreground text-sm font-medium\">{item.title}</p>\n <p class=\"text-muted-foreground text-sm leading-relaxed\">\n {item.description}\n </p>\n </div>\n </div>\n ))\n }\n </div>\n </SectionContainer>\n</Section>\n",
22
22
  "type": "registry:block"
23
23
  }
24
24
  ],
@@ -18,7 +18,7 @@
18
18
  "files": [
19
19
  {
20
20
  "path": "src/components/blocks/contact-3.astro",
21
- "content": "---\nimport type { ImageMetadata } from \"astro\"\nimport { Image } from \"astro:assets\"\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Field,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n} from \"@/components/ui/field\"\nimport { Form } from \"@/components/ui/form\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Section,\n SectionContainer,\n SectionTitle,\n} from \"@/components/ui/section\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport PlaceholderImage from \"@/assets/placeholder.webp\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title: string\n description: string\n contactItems: {\n icon: string\n title: string\n description: string\n href?: string\n }[]\n form: {\n id?: string\n action?: string\n method?: \"get\" | \"post\"\n legend: string\n nameLabel: string\n namePlaceholder: string\n emailLabel: string\n emailPlaceholder: string\n messageLabel: string\n messagePlaceholder: string\n actionLabel: string\n }\n image?: {\n src: ImageMetadata\n alt: string\n }\n}\n\nconst {\n class: className,\n title,\n description,\n contactItems,\n form,\n image = { src: PlaceholderImage, alt: \"Contact image\" },\n ...props\n} = Astro.props\nconst formId = form.id ?? `contact-${crypto.randomUUID()}`\n---\n\n<Section class={cn(\"\", className)} {...props}>\n <SectionContainer class=\"grid gap-12 lg:grid-cols-2 lg:items-start\">\n <div class=\"flex flex-col gap-8\">\n <div class=\"flex flex-col gap-4\">\n <SectionTitle>\n {title}\n </SectionTitle>\n <p class=\"text-muted-foreground max-w-xl text-base leading-relaxed\">\n {description}\n </p>\n </div>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n {\n contactItems.map((item) => (\n <div class=\"flex gap-4\">\n {item.href ? (\n <Button\n as=\"a\"\n href={item.href}\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n ) : (\n <Button\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n )}\n <div class=\"flex flex-col gap-1\">\n <p class=\"text-foreground text-sm font-medium\">{item.title}</p>\n <p class=\"text-muted-foreground text-sm leading-relaxed\">\n {item.description}\n </p>\n </div>\n </div>\n ))\n }\n </div>\n <div class=\"rounded-xl border p-6\">\n <Form id={formId} action={form.action} method={form.method ?? \"post\"}>\n <FieldSet>\n <FieldLegend class=\"sr-only\">{form.legend}</FieldLegend>\n <FieldGroup>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <Field>\n <FieldLabel for={`${formId}-name`}\n >{form.nameLabel}</FieldLabel\n >\n <Input\n id={`${formId}-name`}\n name=\"name\"\n autocomplete=\"name\"\n required\n placeholder={form.namePlaceholder}\n />\n </Field>\n <Field>\n <FieldLabel for={`${formId}-email`}\n >{form.emailLabel}</FieldLabel\n >\n <Input\n id={`${formId}-email`}\n name=\"email\"\n autocomplete=\"email\"\n required\n type=\"email\"\n placeholder={form.emailPlaceholder}\n />\n </Field>\n </div>\n <Field>\n <FieldLabel for={`${formId}-message`}\n >{form.messageLabel}</FieldLabel\n >\n <Textarea\n id={`${formId}-message`}\n name=\"message\"\n required\n rows={5}\n placeholder={form.messagePlaceholder}\n />\n </Field>\n <Button type=\"submit\" size=\"lg\" class=\"w-full\"\n >{form.actionLabel}</Button\n >\n </FieldGroup>\n </FieldSet>\n </Form>\n </div>\n </div>\n <Image\n src={image.src}\n alt={image.alt}\n class=\"aspect-[3/4] w-full rounded-xl border object-cover lg:sticky lg:top-24\"\n widths={[640, 960, 1280]}\n sizes=\"(min-width: 1024px) 50vw, 100vw\"\n />\n </SectionContainer>\n</Section>\n",
21
+ "content": "---\nimport type { ImageMetadata } from \"astro\"\nimport { Image } from \"astro:assets\"\nimport type { HTMLAttributes } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Field,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n} from \"@/components/ui/field\"\nimport { Form } from \"@/components/ui/form\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Input } from \"@/components/ui/input\"\nimport {\n Section,\n SectionContainer,\n SectionTitle,\n} from \"@/components/ui/section\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport PlaceholderImage from \"@/assets/placeholder.webp\"\n\ntype Props = HTMLAttributes<\"section\"> & {\n title: string\n description: string\n contactItems: {\n icon: string\n title: string\n description: string\n href?: string\n }[]\n form: {\n id?: string\n action?: string\n method?: \"get\" | \"post\"\n legend: string\n nameLabel: string\n namePlaceholder: string\n emailLabel: string\n emailPlaceholder: string\n messageLabel: string\n messagePlaceholder: string\n actionLabel: string\n }\n image?: {\n src: ImageMetadata\n alt: string\n }\n}\n\nconst {\n class: className,\n title,\n description,\n contactItems,\n form,\n image = { src: PlaceholderImage, alt: \"Contact image\" },\n ...props\n} = Astro.props\nconst formId = form.id ?? `contact-${crypto.randomUUID()}`\n---\n\n<Section class={cn(\"\", className)} {...props}>\n <SectionContainer class=\"grid gap-12 lg:grid-cols-2 lg:items-start\">\n <div class=\"flex flex-col gap-8\">\n <div class=\"flex flex-col gap-4\">\n <SectionTitle>\n {title}\n </SectionTitle>\n <p class=\"text-muted-foreground max-w-xl text-base leading-relaxed\">\n {description}\n </p>\n </div>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n {\n contactItems.map((item) => (\n <div class=\"flex gap-4\">\n {item.href ? (\n <Button\n as=\"a\"\n href={item.href}\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-label={item.title}\n >\n <Icon name={item.icon} />\n </Button>\n ) : (\n <Button\n as=\"span\"\n variant=\"secondary\"\n size=\"icon\"\n class=\"text-primary shrink-0\"\n aria-hidden=\"true\"\n >\n <Icon name={item.icon} />\n </Button>\n )}\n <div class=\"flex flex-col gap-1\">\n <p class=\"text-foreground text-sm font-medium\">{item.title}</p>\n <p class=\"text-muted-foreground text-sm leading-relaxed\">\n {item.description}\n </p>\n </div>\n </div>\n ))\n }\n </div>\n <div class=\"rounded-xl border p-6\">\n <Form id={formId} action={form.action} method={form.method ?? \"post\"}>\n <FieldSet>\n <FieldLegend class=\"sr-only\">{form.legend}</FieldLegend>\n <FieldGroup>\n <div class=\"grid gap-4 md:grid-cols-2\">\n <Field>\n <FieldLabel for={`${formId}-name`}\n >{form.nameLabel}</FieldLabel\n >\n <Input\n id={`${formId}-name`}\n name=\"name\"\n autocomplete=\"name\"\n required\n placeholder={form.namePlaceholder}\n />\n </Field>\n <Field>\n <FieldLabel for={`${formId}-email`}\n >{form.emailLabel}</FieldLabel\n >\n <Input\n id={`${formId}-email`}\n name=\"email\"\n autocomplete=\"email\"\n required\n type=\"email\"\n placeholder={form.emailPlaceholder}\n />\n </Field>\n </div>\n <Field>\n <FieldLabel for={`${formId}-message`}\n >{form.messageLabel}</FieldLabel\n >\n <Textarea\n id={`${formId}-message`}\n name=\"message\"\n required\n rows={5}\n placeholder={form.messagePlaceholder}\n />\n </Field>\n <Button type=\"submit\" size=\"lg\" class=\"w-full\"\n >{form.actionLabel}</Button\n >\n </FieldGroup>\n </FieldSet>\n </Form>\n </div>\n </div>\n <Image\n src={image.src}\n alt={image.alt}\n class=\"aspect-[3/4] w-full rounded-xl border object-cover lg:sticky lg:top-24\"\n widths={[640, 960, 1280]}\n sizes=\"(min-width: 1024px) 50vw, 100vw\"\n />\n </SectionContainer>\n</Section>\n",
22
22
  "type": "registry:block"
23
23
  },
24
24
  {
@@ -23,7 +23,7 @@
23
23
  "files": [
24
24
  {
25
25
  "path": "src/components/blocks/sidebar-1.astro",
26
- "content": "---\nimport ChevronDownIcon from \"@lucide/astro/icons/chevron-down\"\nimport SearchIcon from \"@lucide/astro/icons/search\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Breadcrumb,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandShortcut,\n} from \"@/components/ui/command\"\nimport { Dialog, DialogTrigger } from \"@/components/ui/dialog\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Header, HeaderContainer, HeaderGroup } from \"@/components/ui/header\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { LogoImage, LogoText } from \"@/components/ui/logo\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n Sidebar,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInset,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarProvider,\n SidebarTrigger,\n} from \"@/components/ui/sidebar\"\nimport { ThemeToggle } from \"@/components/ui/theme-toggle\"\n\ntype Props = {\n class?: string\n logo: {\n label: string\n href: string\n src?: any\n srcLight?: any\n srcDark?: any\n alt?: string\n }\n search: {\n label: string\n empty: string\n items: {\n label: string\n href: string\n title: string\n path: string\n description: string\n group: string\n }[]\n }\n breadcrumb: {\n items: {\n label: string\n href: string\n }[]\n menu: {\n label: string\n href: string\n }[]\n }\n navigation: {\n label: string\n href: string\n links?: {\n label: string\n href: string\n }[]\n }[]\n githubRepo: string\n}\n\nconst {\n class: className,\n logo,\n search,\n breadcrumb,\n navigation,\n githubRepo,\n}: Props = Astro.props\n\nconst normalizePath = (path: string) => {\n if (path === \"/\") return path\n\n return path.replace(/\\/$/, \"\")\n}\nconst currentPath = normalizePath(Astro.url.pathname)\nconst searchGroups = search.items.reduce<\n { label: string; items: typeof search.items }[]\n>((groups, item) => {\n const group = groups.find((entry) => entry.label === item.group)\n\n if (group) {\n group.items.push(item)\n } else {\n groups.push({ label: item.group, items: [item] })\n }\n\n return groups\n}, [])\n\nlet githubStars = \"Stars\"\n\ntry {\n const response = await fetch(`https://api.github.com/repos/${githubRepo}`)\n const data = await response.json()\n\n if (response.ok && typeof data.stargazers_count === \"number\") {\n githubStars = new Intl.NumberFormat().format(data.stargazers_count)\n }\n} catch {\n // Keep the fallback label when GitHub is unavailable.\n}\n---\n\n<SidebarProvider>\n <Sidebar class={cn(\"\", className)} collapsible=\"offcanvas\" variant=\"inset\">\n <SidebarHeader class=\"flex-col\">\n <SidebarMenuButton size=\"lg\" variant=\"ghost\" href={logo.href}>\n <LogoImage\n src={logo.src}\n srcLight={logo.srcLight}\n srcDark={logo.srcDark}\n alt={logo.alt}\n />\n <LogoText class=\"font-semibold\">{logo.label}</LogoText>\n </SidebarMenuButton>\n <Dialog class=\"w-full\" data-search-dialog>\n <DialogTrigger\n variant=\"outline\"\n size=\"sm\"\n class=\"w-full justify-start shadow-none\"\n aria-keyshortcuts=\"Meta+K Control+K\"\n >\n <SearchIcon data-icon=\"inline-start\" />\n <span class=\"truncate\">{search.label}</span>\n <Kbd\n class=\"bg-background/80 ml-auto h-4 min-w-4 translate-y-0 px-1 text-[10px]\"\n data-search-shortcut\n >\n ⌘ + K\n </Kbd>\n </DialogTrigger>\n\n <CommandDialog title={search.label} description={search.label}>\n <Command\n class=\"[&_[data-slot=command-item][data-selected]]:bg-accent [&_[data-slot=command-item][data-selected]]:text-accent-foreground\"\n >\n <CommandInput placeholder={search.label} />\n <CommandList>\n <CommandEmpty>{search.empty}</CommandEmpty>\n\n {\n searchGroups.map((group) => (\n <CommandGroup heading={group.label}>\n {group.items.map((item) => (\n <CommandItem\n href={item.href}\n value={`${item.group} ${item.label}`}\n label={item.label}\n keywords={[\n item.group,\n item.title,\n item.path,\n item.description,\n ]}\n >\n {item.label}\n <CommandShortcut>{item.href}</CommandShortcut>\n </CommandItem>\n ))}\n </CommandGroup>\n ))\n }\n </CommandList>\n </Command>\n </CommandDialog>\n </Dialog>\n </SidebarHeader>\n <SidebarContent\n class=\"[mask-image:linear-gradient(to_bottom,transparent,black_1rem,black_calc(100%_-_1rem),transparent)] [-webkit-mask-image:linear-gradient(to_bottom,transparent,black_1rem,black_calc(100%_-_1rem),transparent)] group-data-[collapsible=icon]:[mask-image:none] group-data-[collapsible=icon]:[-webkit-mask-image:none]\"\n >\n {\n navigation.map((group) => (\n <SidebarGroup>\n <SidebarGroupLabel>{group.label}</SidebarGroupLabel>\n <SidebarMenu>\n {(group.links ?? [group]).map((item) => (\n <SidebarMenuItem>\n <SidebarMenuButton\n href={item.href}\n isActive={normalizePath(item.href) === currentPath}\n >\n <span>{item.label}</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroup>\n ))\n }\n </SidebarContent>\n </Sidebar>\n <SidebarInset>\n <Header class=\"h-16\">\n <HeaderContainer class=\"max-w-none justify-between\">\n <HeaderGroup>\n <SidebarTrigger />\n <Separator orientation=\"vertical\" class=\"my-auto mr-2 h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n {\n breadcrumb.items.map((item, index) => {\n const isLast = index === breadcrumb.items.length - 1\n const isHome = item.href === \"/\"\n const hasMenu =\n !isHome &&\n breadcrumb.menu.some(\n (menuItem) =>\n normalizePath(menuItem.href) ===\n normalizePath(item.href)\n )\n\n return (\n <>\n {index > 0 && (\n <BreadcrumbSeparator>/</BreadcrumbSeparator>\n )}\n <BreadcrumbItem>\n {hasMenu ? (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-current={isLast ? \"page\" : undefined}\n class={cn(\n \"hover:text-foreground inline-flex items-center gap-1 transition-colors\",\n isLast && \"text-foreground\"\n )}\n >\n {item.label}\n <ChevronDownIcon\n class=\"size-3.5 opacity-70\"\n aria-hidden=\"true\"\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuGroup>\n {breadcrumb.menu.map((menuItem) => (\n <a\n href={menuItem.href}\n aria-current={\n normalizePath(menuItem.href) ===\n currentPath\n ? \"page\"\n : undefined\n }\n class=\"hover:bg-accent hover:text-accent-foreground aria-[current=page]:text-foreground flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none\"\n >\n {menuItem.label}\n </a>\n ))}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n ) : isLast ? (\n <BreadcrumbPage>{item.label}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink href={item.href}>\n {item.label}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </>\n )\n })\n }\n {\n breadcrumb.menu.length > 0 && currentPath === \"/\" && (\n <>\n <BreadcrumbSeparator>/</BreadcrumbSeparator>\n <BreadcrumbItem>\n <DropdownMenu>\n <DropdownMenuTrigger class=\"hover:text-foreground inline-flex items-center leading-none transition-colors\">\n <BreadcrumbEllipsis />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuGroup>\n {breadcrumb.menu.map((item) => (\n <a\n href={item.href}\n class=\"hover:bg-accent hover:text-accent-foreground flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none\"\n >\n {item.label}\n </a>\n ))}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n </BreadcrumbItem>\n </>\n )\n }\n </BreadcrumbList>\n </Breadcrumb>\n </HeaderGroup>\n <HeaderGroup>\n <Button\n as=\"a\"\n href={`https://github.com/${githubRepo}`}\n size=\"sm\"\n variant=\"ghost\"\n target=\"_blank\"\n rel=\"noreferrer\"\n aria-label={`View ${githubRepo} on GitHub`}\n >\n <Icon name=\"github\" />\n <span>{githubStars}</span>\n </Button>\n <ThemeToggle />\n </HeaderGroup>\n </HeaderContainer>\n </Header>\n <slot />\n </SidebarInset>\n</SidebarProvider>\n\n<script>\n const isMac = /Mac|iPhone|iPad|iPod/i.test(\n navigator.platform || navigator.userAgent\n )\n\n document.querySelectorAll(\"[data-search-shortcut]\").forEach((shortcut) => {\n shortcut.textContent = isMac ? \"⌘ + K\" : \"Ctrl + K\"\n })\n\n document.addEventListener(\"keydown\", (event) => {\n if (event.key.toLowerCase() !== \"k\") return\n if (event.shiftKey || event.altKey) return\n\n const dialog = document.querySelector(\"[data-search-dialog]\")\n const isSearchShortcut = isMac\n ? event.metaKey && !event.ctrlKey\n : event.ctrlKey && !event.metaKey\n\n if (!dialog || dialog.hasAttribute(\"data-open\") || !isSearchShortcut) return\n\n event.preventDefault()\n dialog.dispatchEvent(\n new CustomEvent(\"dialog:set\", { detail: { open: true } })\n )\n })\n</script>\n",
26
+ "content": "---\nimport ChevronDownIcon from \"@lucide/astro/icons/chevron-down\"\nimport SearchIcon from \"@lucide/astro/icons/search\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Breadcrumb,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from \"@/components/ui/breadcrumb\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n Command,\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandShortcut,\n} from \"@/components/ui/command\"\nimport { Dialog, DialogTrigger } from \"@/components/ui/dialog\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\"\nimport { Header, HeaderContainer, HeaderGroup } from \"@/components/ui/header\"\nimport { Icon } from \"@/components/ui/icon\"\nimport { Kbd } from \"@/components/ui/kbd\"\nimport { LogoImage, LogoText } from \"@/components/ui/logo\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n Sidebar,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInset,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarProvider,\n SidebarTrigger,\n} from \"@/components/ui/sidebar\"\nimport { ThemeToggle } from \"@/components/ui/theme-toggle\"\n\ntype Props = {\n class?: string\n logo: {\n label: string\n href: string\n src?: any\n srcLight?: any\n srcDark?: any\n alt?: string\n }\n search: {\n label: string\n empty: string\n items: {\n label: string\n href: string\n title: string\n path: string\n description: string\n group: string\n }[]\n }\n breadcrumb: {\n items: {\n label: string\n href: string\n }[]\n menu: {\n label: string\n href: string\n }[]\n }\n navigation: {\n label: string\n href: string\n links?: {\n label: string\n href: string\n }[]\n }[]\n githubRepo: string\n}\n\nconst {\n class: className,\n logo,\n search,\n breadcrumb,\n navigation,\n githubRepo,\n}: Props = Astro.props\n\nconst normalizePath = (path: string) => {\n if (path === \"/\") return path\n\n return path.replace(/\\/$/, \"\")\n}\nconst currentPath = normalizePath(Astro.url.pathname)\nconst searchGroups = search.items.reduce<\n { label: string; items: typeof search.items }[]\n>((groups, item) => {\n const group = groups.find((entry) => entry.label === item.group)\n\n if (group) {\n group.items.push(item)\n } else {\n groups.push({ label: item.group, items: [item] })\n }\n\n return groups\n}, [])\n\nlet githubStars = \"Stars\"\n\ntry {\n const response = await fetch(`https://api.github.com/repos/${githubRepo}`)\n const data = await response.json()\n\n if (response.ok && typeof data.stargazers_count === \"number\") {\n githubStars = new Intl.NumberFormat().format(data.stargazers_count)\n }\n} catch {\n // Keep the fallback label when GitHub is unavailable.\n}\n---\n\n<SidebarProvider>\n <Sidebar class={cn(\"\", className)} collapsible=\"offcanvas\" variant=\"inset\">\n <SidebarHeader class=\"flex-col\">\n <SidebarMenuButton size=\"lg\" variant=\"ghost\" href={logo.href}>\n <LogoImage\n src={logo.src}\n srcLight={logo.srcLight}\n srcDark={logo.srcDark}\n alt={logo.alt}\n />\n <LogoText class=\"font-semibold\">{logo.label}</LogoText>\n </SidebarMenuButton>\n <Dialog class=\"w-full\" data-search-dialog>\n <DialogTrigger\n variant=\"outline\"\n size=\"sm\"\n class=\"w-full justify-start shadow-none\"\n aria-keyshortcuts=\"Meta+K Control+K\"\n >\n <SearchIcon data-icon=\"inline-start\" />\n <span class=\"truncate\">{search.label}</span>\n <Kbd\n class=\"bg-background/80 ml-auto h-4 min-w-4 translate-y-0 px-1 text-[10px]\"\n data-search-shortcut\n >\n ⌘ + K\n </Kbd>\n </DialogTrigger>\n\n <CommandDialog title={search.label} description={search.label}>\n <Command\n class=\"[&_[data-slot=command-item][data-selected]]:bg-accent [&_[data-slot=command-item][data-selected]]:text-accent-foreground\"\n >\n <CommandInput placeholder={search.label} />\n <CommandList>\n <CommandEmpty>{search.empty}</CommandEmpty>\n\n {\n searchGroups.map((group) => (\n <CommandGroup heading={group.label}>\n {group.items.map((item) => (\n <CommandItem\n href={item.href}\n value={`${item.group} ${item.label}`}\n label={item.label}\n keywords={[\n item.group,\n item.title,\n item.path,\n item.description,\n ]}\n >\n {item.label}\n <CommandShortcut>{item.href}</CommandShortcut>\n </CommandItem>\n ))}\n </CommandGroup>\n ))\n }\n </CommandList>\n </Command>\n </CommandDialog>\n </Dialog>\n </SidebarHeader>\n <SidebarContent\n class=\"[mask-image:linear-gradient(to_bottom,transparent,black_1rem,black_calc(100%_-_1rem),transparent)] [-webkit-mask-image:linear-gradient(to_bottom,transparent,black_1rem,black_calc(100%_-_1rem),transparent)] group-data-[collapsible=icon]:[mask-image:none] group-data-[collapsible=icon]:[-webkit-mask-image:none]\"\n >\n {\n navigation.map((group) => (\n <SidebarGroup>\n <SidebarGroupLabel>{group.label}</SidebarGroupLabel>\n <SidebarMenu>\n {(group.links ?? [group]).map((item) => (\n <SidebarMenuItem>\n <SidebarMenuButton\n href={item.href}\n isActive={normalizePath(item.href) === currentPath}\n >\n <span>{item.label}</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n ))}\n </SidebarMenu>\n </SidebarGroup>\n ))\n }\n </SidebarContent>\n </Sidebar>\n <SidebarInset>\n <Header class=\"h-16\">\n <HeaderContainer class=\"max-w-none justify-between\">\n <HeaderGroup>\n <SidebarTrigger />\n <Separator orientation=\"vertical\" class=\"my-auto mr-2 h-4\" />\n <Breadcrumb>\n <BreadcrumbList>\n {\n breadcrumb.items.map((item, index) => {\n const isLast = index === breadcrumb.items.length - 1\n const isHome = item.href === \"/\"\n const hasMenu =\n !isHome &&\n breadcrumb.menu.some(\n (menuItem) =>\n normalizePath(menuItem.href) ===\n normalizePath(item.href)\n )\n\n return (\n <>\n {index > 0 && (\n <BreadcrumbSeparator>/</BreadcrumbSeparator>\n )}\n <BreadcrumbItem>\n {hasMenu ? (\n <DropdownMenu>\n <DropdownMenuTrigger\n aria-current={isLast ? \"page\" : undefined}\n class={cn(\n \"hover:text-foreground inline-flex items-center gap-1 transition-colors\",\n isLast && \"text-foreground\"\n )}\n >\n {item.label}\n <ChevronDownIcon\n class=\"size-3.5 opacity-70\"\n aria-hidden=\"true\"\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuGroup>\n {breadcrumb.menu.map((menuItem) => (\n <a\n href={menuItem.href}\n aria-current={\n normalizePath(menuItem.href) ===\n currentPath\n ? \"page\"\n : undefined\n }\n class=\"hover:bg-accent hover:text-accent-foreground aria-[current=page]:text-foreground flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none\"\n >\n {menuItem.label}\n </a>\n ))}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n ) : isLast ? (\n <BreadcrumbPage>{item.label}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink href={item.href}>\n {item.label}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n </>\n )\n })\n }\n {\n breadcrumb.menu.length > 0 && currentPath === \"/\" && (\n <>\n <BreadcrumbSeparator>/</BreadcrumbSeparator>\n <BreadcrumbItem>\n <DropdownMenu>\n <DropdownMenuTrigger class=\"hover:text-foreground inline-flex items-center leading-none transition-colors\">\n <BreadcrumbEllipsis />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n <DropdownMenuGroup>\n {breadcrumb.menu.map((item) => (\n <a\n href={item.href}\n class=\"hover:bg-accent hover:text-accent-foreground flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none\"\n >\n {item.label}\n </a>\n ))}\n </DropdownMenuGroup>\n </DropdownMenuContent>\n </DropdownMenu>\n </BreadcrumbItem>\n </>\n )\n }\n </BreadcrumbList>\n </Breadcrumb>\n </HeaderGroup>\n <HeaderGroup class=\"shrink-0\">\n <Button\n as=\"a\"\n href={`https://github.com/${githubRepo}`}\n size=\"sm\"\n variant=\"ghost\"\n target=\"_blank\"\n rel=\"noreferrer\"\n aria-label={`View ${githubRepo} on GitHub`}\n >\n <Icon name=\"github\" />\n <span class=\"hidden sm:inline\">{githubStars}</span>\n </Button>\n <ThemeToggle />\n </HeaderGroup>\n </HeaderContainer>\n </Header>\n <slot />\n </SidebarInset>\n</SidebarProvider>\n\n<script>\n const isMac = /Mac|iPhone|iPad|iPod/i.test(\n navigator.platform || navigator.userAgent\n )\n\n document.querySelectorAll(\"[data-search-shortcut]\").forEach((shortcut) => {\n shortcut.textContent = isMac ? \"⌘ + K\" : \"Ctrl + K\"\n })\n\n document.addEventListener(\"keydown\", (event) => {\n if (event.key.toLowerCase() !== \"k\") return\n if (event.shiftKey || event.altKey) return\n\n const dialog = document.querySelector(\"[data-search-dialog]\")\n const isSearchShortcut = isMac\n ? event.metaKey && !event.ctrlKey\n : event.ctrlKey && !event.metaKey\n\n if (!dialog || dialog.hasAttribute(\"data-open\") || !isSearchShortcut) return\n\n event.preventDefault()\n dialog.dispatchEvent(\n new CustomEvent(\"dialog:set\", { detail: { open: true } })\n )\n })\n</script>\n",
27
27
  "type": "registry:block"
28
28
  }
29
29
  ],
@@ -6,7 +6,7 @@
6
6
  "files": [
7
7
  {
8
8
  "path": "src/components/ui/typography/typography.astro",
9
- "content": "---\nimport type { HTMLTag, Polymorphic } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Size = \"sm\" | \"default\" | \"lg\"\ntype Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag; size?: Size }>\n\nconst {\n as: Tag = \"div\",\n class: className,\n size = \"default\",\n ...props\n} = Astro.props\n---\n\n<Tag\n data-slot=\"typography\"\n data-size={size}\n class={cn(\"text-foreground text-base\", className)}\n {...props}\n>\n <slot />\n</Tag>\n\n<style is:global>\n @reference \"@/styles/global.css\";\n\n [data-slot=\"typography\"]\n > :is(\n [data-slot=\"typography-lead\"],\n [data-slot=\"typography-table-container\"],\n div,\n section,\n article,\n aside,\n nav,\n figure,\n details,\n pre,\n small,\n dl,\n img,\n picture,\n video,\n iframe\n ):not(:first-child) {\n @apply mt-6;\n }\n\n [data-slot=\"typography\"] > h1 {\n @apply scroll-m-20 text-4xl font-semibold tracking-tight text-balance;\n }\n\n [data-slot=\"typography\"] > h2 {\n @apply scroll-m-20 text-3xl font-semibold tracking-tight;\n }\n\n [data-slot=\"typography\"] > h2:first-child {\n @apply mt-0;\n }\n\n [data-slot=\"typography\"] > h2:not(:first-child) {\n @apply mt-10;\n }\n\n [data-slot=\"typography\"] > h3 {\n @apply scroll-m-20 text-2xl font-semibold tracking-tight;\n }\n\n [data-slot=\"typography\"] > h3:not(:first-child) {\n @apply mt-8;\n }\n\n [data-slot=\"typography\"] > h4 {\n @apply scroll-m-20 text-xl font-semibold tracking-tight;\n }\n\n [data-slot=\"typography\"] > h4:not(:first-child) {\n @apply mt-8;\n }\n\n [data-slot=\"typography\"]\n > :is(h1, h2, h3, h4)\n + :is(\n [data-slot=\"typography-lead\"],\n [data-slot=\"typography-table-container\"],\n div,\n section,\n article,\n aside,\n nav,\n figure,\n details,\n p,\n blockquote,\n pre,\n ul,\n ol,\n table,\n small,\n dl,\n img,\n picture,\n video,\n iframe\n ) {\n @apply mt-4;\n }\n\n [data-slot=\"typography\"] > p {\n @apply text-base leading-7;\n }\n\n [data-slot=\"typography\"] > p:not(:first-child) {\n @apply mt-6;\n }\n\n [data-slot=\"typography\"]\n > :is(h1, h2, h3, h4)\n + :is(p, [data-slot=\"typography-lead\"]):not(:first-child) {\n @apply mt-4;\n }\n\n [data-slot=\"typography\"] > a:not([data-slot]),\n [data-slot=\"typography\"]\n > :is(p, blockquote, ul, ol, dl, table)\n a:not([data-slot]) {\n @apply text-primary font-medium underline underline-offset-4;\n }\n\n [data-slot=\"typography\"] > blockquote {\n @apply mt-6 border-l-2 pl-6 text-base italic;\n }\n\n [data-slot=\"typography\"] > hr {\n @apply border-border my-8;\n }\n\n [data-slot=\"typography\"] > pre {\n @apply border-border bg-muted/60 overflow-x-auto rounded-xl border p-4 text-sm;\n }\n\n [data-slot=\"typography\"] > .docs-code-block {\n @apply border-border bg-muted/60 relative overflow-hidden rounded-xl border;\n }\n\n [data-slot=\"typography\"] > .docs-code-block > pre,\n [data-slot=\"typography\"] > .docs-code-block > pre.astro-code {\n @apply m-0 rounded-none border-0 !bg-transparent p-4 pr-14 text-sm leading-[var(--text-sm--line-height)];\n font-family: var(--font-mono);\n }\n\n [data-slot=\"typography\"] > .docs-code-block > pre.astro-code code {\n @apply text-sm leading-[var(--text-sm--line-height)] !text-[var(--shiki-light,var(--foreground))];\n }\n\n .dark [data-slot=\"typography\"] > .docs-code-block > pre.astro-code code {\n @apply !text-[var(--shiki-dark,var(--foreground))];\n }\n\n .dark\n [data-slot=\"typography\"]\n > .docs-code-block\n > pre.astro-code\n span[style*=\"--shiki-dark\"] {\n @apply ![font-weight:var(--shiki-dark-font-weight,inherit)] !text-[var(--shiki-dark)] ![font-style:var(--shiki-dark-font-style,inherit)] ![text-decoration:var(--shiki-dark-text-decoration,inherit)];\n }\n\n [data-slot=\"typography\"] > .docs-code-block > .docs-code-copy {\n @apply border-border bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground absolute top-3 right-3 z-10 inline-flex size-8 items-center justify-center rounded-md border shadow-sm transition-colors;\n }\n\n [data-slot=\"typography\"] > .docs-code-block svg {\n @apply size-4;\n }\n\n [data-slot=\"typography\"] > ul,\n [data-slot=\"typography\"] > ol {\n @apply my-6 ml-6 text-base;\n }\n\n [data-slot=\"typography\"] > ul {\n @apply list-disc;\n }\n\n [data-slot=\"typography\"] > ol {\n @apply list-decimal;\n }\n\n [data-slot=\"typography\"] > ul > li,\n [data-slot=\"typography\"] > ol > li {\n @apply mt-2;\n }\n\n [data-slot=\"typography\"] > [data-slot=\"typography-lead\"] {\n @apply text-muted-foreground;\n }\n\n [data-slot=\"typography\"][data-size=\"sm\"] > [data-slot=\"typography-lead\"] {\n @apply text-base;\n }\n\n [data-slot=\"typography\"] > [data-slot=\"typography-lead\"],\n [data-slot=\"typography\"][data-size=\"default\"]\n > [data-slot=\"typography-lead\"] {\n @apply text-lg;\n }\n\n [data-slot=\"typography\"][data-size=\"lg\"] > [data-slot=\"typography-lead\"] {\n @apply text-xl;\n }\n\n [data-slot=\"typography\"] > small {\n @apply block leading-none font-medium;\n }\n\n [data-slot=\"typography\"] > small {\n @apply text-sm;\n }\n\n [data-slot=\"typography\"] > table {\n @apply border-border my-6 block w-full overflow-x-auto rounded-lg border;\n border-collapse: separate;\n border-spacing: 0;\n }\n\n [data-slot=\"typography\"] > table th,\n [data-slot=\"typography\"] > table td {\n @apply border-border border-b px-4 py-2 text-left align-top whitespace-nowrap;\n }\n\n [data-slot=\"typography\"] > table th {\n @apply text-foreground bg-muted/40 font-medium;\n }\n\n [data-slot=\"typography\"] > table tr:last-child td {\n @apply border-b-0;\n }\n\n [data-slot=\"typography\"] > table thead,\n [data-slot=\"typography\"] > table tbody,\n [data-slot=\"typography\"] > table tr {\n @apply w-full;\n }\n\n [data-slot=\"typography\"] > table tr {\n @apply grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)];\n }\n\n [data-slot=\"typography\"] > table tr {\n @apply even:bg-muted/40 m-0 border-t p-0 first:border-t-0;\n }\n\n [data-slot=\"typography\"] > table th {\n @apply border-r px-4 py-2 text-left font-bold last:border-r-0;\n }\n\n [data-slot=\"typography\"] > table th[align=\"center\"] {\n @apply text-center;\n }\n\n [data-slot=\"typography\"] > table th[align=\"right\"] {\n @apply text-right;\n }\n\n [data-slot=\"typography\"] > table td {\n @apply border-r px-4 py-2 text-left last:border-r-0;\n }\n\n [data-slot=\"typography\"] > table td[align=\"center\"] {\n @apply text-center;\n }\n\n [data-slot=\"typography\"] > table td[align=\"right\"] {\n @apply text-right;\n }\n\n [data-slot=\"typography\"] > table tbody tr:last-child td {\n @apply border-b-0;\n }\n\n [data-slot=\"typography\"] :not(pre) > code {\n @apply bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold;\n }\n\n [data-slot=\"typography\"] > table :not(pre) > code {\n @apply rounded-md text-sm leading-5 font-normal;\n }\n\n [data-slot=\"typography\"] > pre.astro-code {\n @apply border-border !bg-muted/60 rounded-xl border p-4 text-sm leading-[var(--text-sm--line-height)];\n }\n\n [data-slot=\"typography\"] > pre.astro-code code {\n @apply !text-inherit;\n }\n\n .dark [data-slot=\"typography\"] > pre.astro-code {\n @apply !text-[var(--shiki-dark,var(--foreground))];\n }\n\n .dark [data-slot=\"typography\"] > pre.astro-code span[style*=\"--shiki-dark\"] {\n @apply ![font-weight:var(--shiki-dark-font-weight,inherit)] !text-[var(--shiki-dark)] ![font-style:var(--shiki-dark-font-style,inherit)] ![text-decoration:var(--shiki-dark-text-decoration,inherit)];\n }\n</style>\n",
9
+ "content": "---\nimport type { HTMLTag, Polymorphic } from \"astro/types\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype Size = \"sm\" | \"default\" | \"lg\"\ntype Props<Tag extends HTMLTag> = Polymorphic<{ as: Tag; size?: Size }>\n\nconst {\n as: Tag = \"div\",\n class: className,\n size = \"default\",\n ...props\n} = Astro.props\n---\n\n<Tag\n data-slot=\"typography\"\n data-size={size}\n class={cn(\"text-foreground text-base\", className)}\n {...props}\n>\n <slot />\n</Tag>\n\n<style is:global>\n @reference \"@/styles/global.css\";\n\n [data-slot=\"typography\"]\n > :is(\n [data-slot=\"typography-lead\"],\n [data-slot=\"typography-table-container\"],\n div,\n section,\n article,\n aside,\n nav,\n figure,\n details,\n pre,\n small,\n dl,\n img,\n picture,\n video,\n iframe\n ):not(:first-child) {\n @apply mt-6;\n }\n\n [data-slot=\"typography\"] > h1 {\n @apply scroll-m-20 text-4xl font-semibold tracking-tight text-balance;\n }\n\n [data-slot=\"typography\"] > h2 {\n @apply scroll-m-20 text-3xl font-semibold tracking-tight;\n }\n\n [data-slot=\"typography\"] > h2:first-child {\n @apply mt-0;\n }\n\n [data-slot=\"typography\"] > h2:not(:first-child) {\n @apply mt-10;\n }\n\n [data-slot=\"typography\"] > h3 {\n @apply scroll-m-20 text-2xl font-semibold tracking-tight;\n }\n\n [data-slot=\"typography\"] > h3:not(:first-child) {\n @apply mt-8;\n }\n\n [data-slot=\"typography\"] > h4 {\n @apply scroll-m-20 text-xl font-semibold tracking-tight;\n }\n\n [data-slot=\"typography\"] > h4:not(:first-child) {\n @apply mt-8;\n }\n\n [data-slot=\"typography\"]\n > :is(h1, h2, h3, h4)\n + :is(\n [data-slot=\"typography-lead\"],\n [data-slot=\"typography-table-container\"],\n div,\n section,\n article,\n aside,\n nav,\n figure,\n details,\n p,\n blockquote,\n pre,\n ul,\n ol,\n table,\n small,\n dl,\n img,\n picture,\n video,\n iframe\n ) {\n @apply mt-4;\n }\n\n [data-slot=\"typography\"] > p {\n @apply text-base leading-7;\n }\n\n [data-slot=\"typography\"] > p:not(:first-child) {\n @apply mt-6;\n }\n\n [data-slot=\"typography\"]\n > :is(h1, h2, h3, h4)\n + :is(p, [data-slot=\"typography-lead\"]):not(:first-child) {\n @apply mt-4;\n }\n\n [data-slot=\"typography\"] > a:not([data-slot]),\n [data-slot=\"typography\"]\n > :is(p, blockquote, ul, ol, dl, table)\n a:not([data-slot]) {\n @apply text-foreground font-medium underline underline-offset-4;\n }\n\n [data-slot=\"typography\"] > blockquote {\n @apply mt-6 border-l-2 pl-6 text-base italic;\n }\n\n [data-slot=\"typography\"] > hr {\n @apply border-border my-8;\n }\n\n [data-slot=\"typography\"] > pre {\n @apply border-border bg-muted/60 overflow-x-auto rounded-xl border p-4 text-sm;\n }\n\n [data-slot=\"typography\"] > .docs-code-block {\n @apply border-border bg-muted/60 relative overflow-hidden rounded-xl border;\n }\n\n [data-slot=\"typography\"] > .docs-code-block > pre,\n [data-slot=\"typography\"] > .docs-code-block > pre.astro-code {\n @apply m-0 rounded-none border-0 !bg-transparent p-4 pr-14 text-sm leading-[var(--text-sm--line-height)];\n font-family: var(--font-mono);\n }\n\n [data-slot=\"typography\"] > .docs-code-block > pre.astro-code code {\n @apply text-sm leading-[var(--text-sm--line-height)] !text-[var(--shiki-light,var(--foreground))];\n }\n\n .dark [data-slot=\"typography\"] > .docs-code-block > pre.astro-code code {\n @apply !text-[var(--shiki-dark,var(--foreground))];\n }\n\n .dark\n [data-slot=\"typography\"]\n > .docs-code-block\n > pre.astro-code\n span[style*=\"--shiki-dark\"] {\n @apply ![font-weight:var(--shiki-dark-font-weight,inherit)] !text-[var(--shiki-dark)] ![font-style:var(--shiki-dark-font-style,inherit)] ![text-decoration:var(--shiki-dark-text-decoration,inherit)];\n }\n\n [data-slot=\"typography\"] > .docs-code-block > .docs-code-copy {\n @apply border-border bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground absolute top-3 right-3 z-10 inline-flex size-8 items-center justify-center rounded-md border shadow-sm transition-colors;\n }\n\n [data-slot=\"typography\"] > .docs-code-block svg {\n @apply size-4;\n }\n\n [data-slot=\"typography\"] > ul,\n [data-slot=\"typography\"] > ol {\n @apply my-6 ml-6 text-base;\n }\n\n [data-slot=\"typography\"] > ul {\n @apply list-disc;\n }\n\n [data-slot=\"typography\"] > ol {\n @apply list-decimal;\n }\n\n [data-slot=\"typography\"] > ul > li,\n [data-slot=\"typography\"] > ol > li {\n @apply mt-2;\n }\n\n [data-slot=\"typography\"] > [data-slot=\"typography-lead\"] {\n @apply text-muted-foreground;\n }\n\n [data-slot=\"typography\"][data-size=\"sm\"] > [data-slot=\"typography-lead\"] {\n @apply text-base;\n }\n\n [data-slot=\"typography\"] > [data-slot=\"typography-lead\"],\n [data-slot=\"typography\"][data-size=\"default\"]\n > [data-slot=\"typography-lead\"] {\n @apply text-lg;\n }\n\n [data-slot=\"typography\"][data-size=\"lg\"] > [data-slot=\"typography-lead\"] {\n @apply text-xl;\n }\n\n [data-slot=\"typography\"] > small {\n @apply block leading-none font-medium;\n }\n\n [data-slot=\"typography\"] > small {\n @apply text-sm;\n }\n\n [data-slot=\"typography\"] > table {\n @apply border-border my-6 block w-full overflow-x-auto rounded-lg border;\n border-collapse: separate;\n border-spacing: 0;\n }\n\n [data-slot=\"typography\"] > table th,\n [data-slot=\"typography\"] > table td {\n @apply border-border border-b px-4 py-2 text-left align-top whitespace-nowrap;\n }\n\n [data-slot=\"typography\"] > table th {\n @apply text-foreground bg-muted/40 font-medium;\n }\n\n [data-slot=\"typography\"] > table tr:last-child td {\n @apply border-b-0;\n }\n\n [data-slot=\"typography\"] > table thead,\n [data-slot=\"typography\"] > table tbody,\n [data-slot=\"typography\"] > table tr {\n @apply w-full;\n }\n\n [data-slot=\"typography\"] > table tr {\n @apply grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)];\n }\n\n [data-slot=\"typography\"] > table tr {\n @apply even:bg-muted/40 m-0 border-t p-0 first:border-t-0;\n }\n\n [data-slot=\"typography\"] > table th {\n @apply border-r px-4 py-2 text-left font-bold last:border-r-0;\n }\n\n [data-slot=\"typography\"] > table th[align=\"center\"] {\n @apply text-center;\n }\n\n [data-slot=\"typography\"] > table th[align=\"right\"] {\n @apply text-right;\n }\n\n [data-slot=\"typography\"] > table td {\n @apply border-r px-4 py-2 text-left last:border-r-0;\n }\n\n [data-slot=\"typography\"] > table td[align=\"center\"] {\n @apply text-center;\n }\n\n [data-slot=\"typography\"] > table td[align=\"right\"] {\n @apply text-right;\n }\n\n [data-slot=\"typography\"] > table tbody tr:last-child td {\n @apply border-b-0;\n }\n\n [data-slot=\"typography\"] :not(pre) > code {\n @apply bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold;\n }\n\n [data-slot=\"typography\"] > table :not(pre) > code {\n @apply rounded-md text-sm leading-5 font-normal;\n }\n\n [data-slot=\"typography\"] > pre.astro-code {\n @apply border-border !bg-muted/60 rounded-xl border p-4 text-sm leading-[var(--text-sm--line-height)];\n }\n\n [data-slot=\"typography\"] > pre.astro-code code {\n @apply !text-inherit;\n }\n\n .dark [data-slot=\"typography\"] > pre.astro-code {\n @apply !text-[var(--shiki-dark,var(--foreground))];\n }\n\n .dark [data-slot=\"typography\"] > pre.astro-code span[style*=\"--shiki-dark\"] {\n @apply ![font-weight:var(--shiki-dark-font-weight,inherit)] !text-[var(--shiki-dark)] ![font-style:var(--shiki-dark-font-style,inherit)] ![text-decoration:var(--shiki-dark-text-decoration,inherit)];\n }\n</style>\n",
10
10
  "type": "registry:ui"
11
11
  },
12
12
  {
@@ -21,19 +21,22 @@ const { class: className, title, description, buttons, ...props } = Astro.props
21
21
  <Banner
22
22
  variant="floating"
23
23
  storageKey="banner-2"
24
- class={cn("", className)}
24
+ class={cn("@container", className)}
25
25
  {...props}
26
26
  >
27
- <BannerContainer class="gap-4 py-1" showClose={false}>
27
+ <BannerContainer
28
+ class="flex-col items-start gap-4 py-1 @2xl:flex-row @2xl:items-center"
29
+ showClose={false}
30
+ >
28
31
  <div
29
- class="flex flex-1 flex-col gap-0.5 @5xl:flex-row @5xl:items-center @5xl:gap-x-4"
32
+ class="flex min-w-0 flex-1 flex-col gap-0.5 @5xl:flex-row @5xl:items-center @5xl:gap-x-4"
30
33
  >
31
34
  <p class="text-sm font-semibold">{title}</p>
32
35
  <p class="text-muted-foreground text-sm">{description}</p>
33
36
  </div>
34
37
  {
35
38
  buttons?.length && (
36
- <div class="flex shrink-0 items-center gap-2">
39
+ <div class="flex flex-wrap items-center gap-2">
37
40
  {buttons.map(({ href, label, ...button }, index) => (
38
41
  <Button
39
42
  as="a"
@@ -87,10 +87,11 @@ const formId = form.id ?? `contact-${crypto.randomUUID()}`
87
87
  </Button>
88
88
  ) : (
89
89
  <Button
90
+ as="span"
90
91
  variant="secondary"
91
92
  size="icon"
92
93
  class="text-primary"
93
- aria-label={item.title}
94
+ aria-hidden="true"
94
95
  >
95
96
  <Icon name={item.icon} />
96
97
  </Button>
@@ -142,10 +142,11 @@ const formId = form.id ?? `contact-${crypto.randomUUID()}`
142
142
  </Button>
143
143
  ) : (
144
144
  <Button
145
+ as="span"
145
146
  variant="secondary"
146
147
  size="icon"
147
148
  class="text-primary shrink-0"
148
- aria-label={item.title}
149
+ aria-hidden="true"
149
150
  >
150
151
  <Icon name={item.icon} />
151
152
  </Button>
@@ -91,10 +91,11 @@ const formId = form.id ?? `contact-${crypto.randomUUID()}`
91
91
  </Button>
92
92
  ) : (
93
93
  <Button
94
+ as="span"
94
95
  variant="secondary"
95
96
  size="icon"
96
97
  class="text-primary shrink-0"
97
- aria-label={item.title}
98
+ aria-hidden="true"
98
99
  >
99
100
  <Icon name={item.icon} />
100
101
  </Button>
@@ -325,7 +325,7 @@ try {
325
325
  </BreadcrumbList>
326
326
  </Breadcrumb>
327
327
  </HeaderGroup>
328
- <HeaderGroup>
328
+ <HeaderGroup class="shrink-0">
329
329
  <Button
330
330
  as="a"
331
331
  href={`https://github.com/${githubRepo}`}
@@ -336,7 +336,7 @@ try {
336
336
  aria-label={`View ${githubRepo} on GitHub`}
337
337
  >
338
338
  <Icon name="github" />
339
- <span>{githubStars}</span>
339
+ <span class="hidden sm:inline">{githubStars}</span>
340
340
  </Button>
341
341
  <ThemeToggle />
342
342
  </HeaderGroup>
@@ -35,51 +35,33 @@ const { class: className, variant, storageKey, ...props } = Astro.props
35
35
  <slot />
36
36
  </aside>
37
37
 
38
- <script is:inline>
39
- const initializedKey = "fulldevBannerInitialized"
40
-
41
- if (!document.documentElement.dataset[initializedKey]) {
42
- document.documentElement.dataset[initializedKey] = "true"
43
-
44
- const getStorageToken = (key) => `banner:${key}`
45
-
46
- const syncBanners = () => {
47
- document.querySelectorAll("[data-slot='banner']").forEach((banner) => {
48
- if (!(banner instanceof HTMLElement)) return
49
-
50
- const storageKey = banner.dataset.bannerStorageKey
51
-
52
- if (
53
- storageKey &&
54
- sessionStorage.getItem(getStorageToken(storageKey)) === "hidden"
55
- ) {
56
- banner.hidden = true
57
- }
58
- })
59
- }
60
-
61
- syncBanners()
62
-
63
- document.addEventListener("click", (event) => {
64
- const target = event.target
65
-
66
- if (!(target instanceof Element)) return
67
-
68
- const closeButton = target.closest("[data-slot='banner-close']")
69
-
70
- if (!(closeButton instanceof HTMLElement)) return
71
-
72
- const banner = closeButton.closest("[data-slot='banner']")
38
+ <script>
39
+ const getStorageToken = (key: string) => `banner:${key}`
73
40
 
41
+ const syncBanners = () => {
42
+ document.querySelectorAll("[data-slot='banner']").forEach((banner) => {
74
43
  if (!(banner instanceof HTMLElement)) return
75
-
76
- banner.hidden = true
77
-
78
44
  const storageKey = banner.dataset.bannerStorageKey
79
-
80
- if (storageKey) {
81
- sessionStorage.setItem(getStorageToken(storageKey), "hidden")
45
+ if (
46
+ storageKey &&
47
+ sessionStorage.getItem(getStorageToken(storageKey)) === "hidden"
48
+ ) {
49
+ banner.hidden = true
82
50
  }
83
51
  })
84
52
  }
53
+
54
+ syncBanners()
55
+ document.addEventListener("astro:page-load", syncBanners)
56
+
57
+ document.addEventListener("click", (event) => {
58
+ if (!(event.target instanceof Element)) return
59
+ const closeButton = event.target.closest("[data-slot='banner-close']")
60
+ const banner = closeButton?.closest("[data-slot='banner']")
61
+ if (!(banner instanceof HTMLElement)) return
62
+ banner.hidden = true
63
+ const storageKey = banner.dataset.bannerStorageKey
64
+ if (storageKey)
65
+ sessionStorage.setItem(getStorageToken(storageKey), "hidden")
66
+ })
85
67
  </script>
@@ -126,7 +126,7 @@ const {
126
126
  [data-slot="typography"]
127
127
  > :is(p, blockquote, ul, ol, dl, table)
128
128
  a:not([data-slot]) {
129
- @apply text-primary font-medium underline underline-offset-4;
129
+ @apply text-foreground font-medium underline underline-offset-4;
130
130
  }
131
131
 
132
132
  [data-slot="typography"] > blockquote {
@@ -68,7 +68,7 @@
68
68
  --popover: oklch(0.269 0 0);
69
69
  --popover-foreground: oklch(0.985 0 0);
70
70
  --primary: oklch(0.686 0.2105 41.139);
71
- --primary-foreground: oklch(1 0 0);
71
+ --primary-foreground: oklch(0.145 0 0);
72
72
  --secondary: oklch(0.269 0 0);
73
73
  --secondary-foreground: oklch(0.985 0 0);
74
74
  --muted: oklch(0.269 0 0);
@@ -88,7 +88,7 @@
88
88
  --sidebar: oklch(0.205 0 0);
89
89
  --sidebar-foreground: oklch(0.985 0 0);
90
90
  --sidebar-primary: oklch(0.686 0.2105 41.139);
91
- --sidebar-primary-foreground: oklch(1 0 0);
91
+ --sidebar-primary-foreground: oklch(0.145 0 0);
92
92
  --sidebar-accent: oklch(0.269 0 0);
93
93
  --sidebar-accent-foreground: oklch(0.985 0 0);
94
94
  --sidebar-border: oklch(0.275 0 0);