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
59 changes: 59 additions & 0 deletions app/components/demo/FeatureCardDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { ArrowUpRight, ChartNoAxesColumnIncreasing, Sparkles } from "lucide-vue-next";
import { FeatureCard } from "~/registry/blocks/feature-card";
</script>

<template>
<div class="mx-auto grid w-full max-w-4xl gap-4 sm:grid-cols-2">
<FeatureCard
title="Launch-ready sections"
description="Compose product pages from focused blocks while keeping copy, data, and route logic in your app."
/>

<FeatureCard title="Title-only highlight" />

<FeatureCard
title="Feature library"
description="Link each card to a deeper product, docs, or marketing page without coupling the component to Nuxt routing."
href="/docs/components"
class="sm:row-span-2"
>
<template #icon>
<Sparkles
class="size-5"
aria-hidden="true"
/>
</template>
</FeatureCard>

<FeatureCard
title="Usage analytics"
description="Open external dashboards safely with a default rel for blank-target links."
href="https://ui.stackhacker.io"
target="_blank"
>
<template #icon>
<ChartNoAxesColumnIncreasing
class="size-5"
aria-hidden="true"
/>
</template>
</FeatureCard>

<FeatureCard
title="Custom rel link"
description="Override rel when your app has a specific security or attribution policy."
href="https://ui.stackhacker.io/r/feature-card.json"
target="_blank"
rel="noreferrer"
class="sm:col-span-2"
>
<template #icon>
<ArrowUpRight
class="size-5"
aria-hidden="true"
/>
</template>
</FeatureCard>
</div>
</template>
65 changes: 65 additions & 0 deletions app/registry/blocks/feature-card/FeatureCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

export interface FeatureCardProps {
/** Feature title supplied by the consuming app */
title: string;
/** Supporting feature copy supplied by the consuming app */
description?: string;
/** Optional destination. When omitted, the card renders as static content. */
href?: string;
/** Anchor target used when href is supplied */
target?: HTMLAnchorElement["target"];
/** Anchor rel used when href is supplied */
rel?: string;
/** Additional CSS classes for the root element */
class?: HTMLAttributes["class"];
}

const props = defineProps<FeatureCardProps>();

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

<template>
<component
:is="href ? 'a' : 'div'"
data-slot="feature-card"
:href="href"
:target="href ? target : undefined"
:rel="href ? linkRel() : undefined"
:class="cn(
'group block rounded-xl border bg-card p-5 text-card-foreground shadow-sm sm:p-6',
href && 'transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
props.class,
)"
>
<div
v-if="$slots.icon"
data-slot="feature-card-icon"
class="mb-5 flex size-10 items-center justify-center rounded-lg bg-primary/10 text-primary transition-colors group-hover:bg-primary/15"
>
<slot name="icon" />
</div>

<h3
data-slot="feature-card-title"
class="text-balance text-base font-semibold tracking-tight"
>
{{ title }}
</h3>

<p
v-if="description"
data-slot="feature-card-description"
class="mt-2 text-pretty text-sm leading-6 text-muted-foreground transition-colors group-hover:text-accent-foreground/80"
>
{{ description }}
</p>
</component>
</template>
1 change: 1 addition & 0 deletions app/registry/blocks/feature-card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as FeatureCard, type FeatureCardProps } from "./FeatureCard.vue";
75 changes: 75 additions & 0 deletions content/docs/2.components/11.feature-card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: FeatureCard
description: Render a reusable feature, content, or navigation card with optional link and icon slot.
category: marketing
---

::component-preview
---
name: FeatureCardDemo
---
::

## Installation

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

## Usage

```vue
<script setup lang="ts">
import { Sparkles } from 'lucide-vue-next'
import { FeatureCard } from '@/components/feature-card'
</script>

<template>
<FeatureCard
title="Launch-ready sections"
description="Compose product pages from focused blocks while keeping copy, data, and route logic in your app."
href="/features"
>
<template #icon>
<Sparkles class="size-5" aria-hidden="true" />
</template>
</FeatureCard>
</template>
```

## App-Owned Feature Data

`FeatureCard` owns a portable card surface for feature, content, or navigation teasers. Your app owns the card list, copy, route generation, analytics events, active states, and any icon system you choose to use.

The component intentionally does not accept Nuxt Icon string names, require `NuxtLink`, or install an icon package. Pass icon markup through the `icon` slot so the consuming app can use Lucide, Nuxt Icon, custom SVGs, or no icon at all.

When `href` is omitted, the component renders static card content. When `href` is supplied, it renders an anchor. For `target="_blank"`, the component adds `rel="noopener noreferrer"` unless you provide a custom `rel` value.

## Examples

### Default

::component-preview
---
name: FeatureCardDemo
---
::

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `string` | — | Feature title supplied by your app. |
| `description` | `string` | — | Optional supporting feature copy supplied by your app. |
| `href` | `string` | — | Optional destination. When omitted, the card renders as static content. |
| `target` | `HTMLAnchorElement['target']` | — | Anchor target used when `href` is supplied. |
| `rel` | `string` | — | Anchor rel used when `href` is supplied. Defaults to `noopener noreferrer` for blank-target links. |
| `class` | `string` | — | Additional CSS classes for the root element. |

### Slots

| Slot | Description |
|------|-------------|
| `icon` | Optional icon markup rendered above the title. |
4 changes: 4 additions & 0 deletions scripts/registry-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ const expectedItems: Record<string, ExpectedRegistryItem> = {
dependencies: [],
registryDependencies: ["button"],
},
"feature-card": {
dependencies: [],
registryDependencies: [],
},
};

if (registryIndex) {
Expand Down
Loading