Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/components/demo/HomeCtaDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script setup lang="ts">
import { HomeCta, type HomeCtaLink } from "~/registry/blocks/home-cta";

const links: HomeCtaLink[] = [
{
label: "Start building",
href: "#",
},
{
label: "View components",
href: "https://ui.stackhacker.io/docs/components",
target: "_blank",
variant: "outline",
},
];
</script>

<template>
<HomeCta
title="Launch your product surface faster"
description="Drop in a focused CTA section, then connect your own routes, analytics, campaigns, and conversion logic."
:links="links"
class="rounded-xl border py-16 md:py-20"
/>
</template>
74 changes: 74 additions & 0 deletions app/registry/blocks/home-cta/HomeCta.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";

export interface HomeCtaLink {
label: string;
href: string;
target?: HTMLAnchorElement["target"];
rel?: string;
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
}

export interface HomeCtaProps {
/** Main CTA headline supplied by the consuming app */
title: string;
/** Supporting CTA copy supplied by the consuming app */
description: string;
/** CTA links supplied by the consuming app */
links?: HomeCtaLink[];
/** Additional CSS classes for the section */
class?: HTMLAttributes["class"];
}

const props = withDefaults(defineProps<HomeCtaProps>(), {
links: () => [],
});

function linkRel(link: HomeCtaLink) {
if (link.rel) return link.rel;
if (link.target === "_blank") return "noopener noreferrer";
return undefined;
}
</script>

<template>
<section
data-slot="home-cta"
:class="cn('relative overflow-hidden py-20 text-center md:py-28', props.class)"
>
<div
class="absolute inset-0 -z-10 overflow-hidden"
aria-hidden="true"
>
<div class="absolute left-1/2 top-0 h-48 w-[70%] -translate-x-1/2 rounded-full bg-primary/20 blur-3xl" />
<div class="absolute inset-0 bg-[radial-gradient(circle_at_center,currentColor_1px,transparent_1px)] text-primary/20 [background-size:24px_24px] [mask-image:linear-gradient(to_bottom,transparent,black,transparent)]" />
</div>

<div class="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8">
<h2 class="mx-auto max-w-2xl text-pretty text-3xl font-semibold tracking-tight md:text-5xl">
{{ title }}
</h2>
<p class="mx-auto mt-4 max-w-xl text-muted-foreground">
{{ description }}
</p>
<div
v-if="links.length"
class="mt-8 flex flex-wrap justify-center gap-2.5"
>
<Button
v-for="link in links"
:key="`${link.label}-${link.href}`"
as="a"
:href="link.href"
:target="link.target"
:rel="linkRel(link)"
:variant="link.variant || 'default'"
>
{{ link.label }}
</Button>
</div>
</div>
</section>
</template>
1 change: 1 addition & 0 deletions app/registry/blocks/home-cta/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as HomeCta, type HomeCtaLink, type HomeCtaProps } from "./HomeCta.vue";
84 changes: 84 additions & 0 deletions content/docs/2.components/9.home-cta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: HomeCta
description: Render a centered marketing CTA section with an integrated starfield background.
category: marketing
---

::component-preview
---
name: HomeCtaDemo
---
::

## Installation

```bash
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/home-cta.json"
```

## Usage

```vue
<script setup lang="ts">
import { HomeCta, type HomeCtaLink } from '@/components/home-cta'

const links: HomeCtaLink[] = [
{
label: 'Start building',
href: '/signup'
},
{
label: 'Read the docs',
href: '/docs',
variant: 'outline'
}
]
</script>

<template>
<HomeCta
title="Build the next product moment"
description="Use the block for the section layout and CTA rendering. Keep copy, routes, analytics, and campaigns in your app."
:links="links"
/>
</template>
```

## App-Owned Marketing

`HomeCta` owns the centered section layout, starfield background, and CTA button rendering. Your app owns the message, routes, analytics, conversion logic, auth state, campaign state, and any experiment logic around the section.

Pass links as plain data. Use `target: '_blank'` for external links; the component adds `rel="noopener noreferrer"` by default unless you provide a custom `rel` value.

## Examples

### Default

::component-preview
---
name: HomeCtaDemo
---
::

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `string` | — | Main CTA headline supplied by your app. |
| `description` | `string` | — | Supporting CTA copy supplied by your app. |
| `links` | `HomeCtaLink[]` | `[]` | CTA links supplied by your app. |
| `class` | `string` | — | Additional CSS classes for the section. |

### Types

```ts
interface HomeCtaLink {
label: string
href: string
target?: HTMLAnchorElement['target']
rel?: string
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
}
```
4 changes: 4 additions & 0 deletions scripts/registry-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ const expectedItems: Record<string, ExpectedRegistryItem> = {
dependencies: ["@lucide/vue"],
registryDependencies: ["button", "collapsible"],
},
"home-cta": {
dependencies: [],
registryDependencies: ["button"],
},
};

if (registryIndex) {
Expand Down
Loading