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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Config: `registry.config.ts` and `components.json` `registries` field.

## Content

- `content.config.ts` defines the docs collection with `category` field: `element | content | chat | overview`
- `content.config.ts` defines the docs collection with `category` field: `element | content | marketing | chat | overview`
- `@nuxt/content` v3 with D1 database (`content.database.type: "d1"`)
- Markdown highlight disabled; `nuxt-shiki` module handles syntax highlighting instead

Expand Down
65 changes: 65 additions & 0 deletions app/components/demo/PricingPlansDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { PricingPlans, type PricingPlan } from "~/registry/blocks/pricing-plans";

const billingPeriod = ref<"month" | "year">("month");

const plans: PricingPlan[] = [
{
title: "Basic",
description: "For solo builders validating a product idea.",
price: { month: "$9", year: "$90" },
cta: { label: "Start basic", href: "#" },
features: ["1 workspace", "Core analytics", "Community support"],
},
{
title: "Standard",
description: "For small teams shipping customer-facing apps.",
price: { month: "$19", year: "$190" },
cta: { label: "Start standard", href: "#" },
features: ["5 workspaces", "Priority analytics", "Email support", "Team roles"],
highlight: true,
},
{
title: "Premium",
description: "For teams that need more scale and control.",
price: { month: "$49", year: "$490" },
cta: { label: "Contact sales", href: "#" },
features: ["Unlimited workspaces", "Advanced reporting", "Priority support", "SAML SSO"],
},
];
</script>

<template>
<div class="w-full space-y-6">
<div class="flex justify-center">
<div class="inline-flex rounded-lg border bg-muted p-1">
<button
type="button"
:class="[
'rounded-md px-3 py-1.5 text-sm transition-colors',
billingPeriod === 'month' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
]"
@click="billingPeriod = 'month'"
>
Monthly
</button>
<button
type="button"
:class="[
'rounded-md px-3 py-1.5 text-sm transition-colors',
billingPeriod === 'year' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
]"
@click="billingPeriod = 'year'"
>
Yearly
</button>
</div>
</div>

<PricingPlans
:plans="plans"
:billing-period="billingPeriod"
class="py-0"
/>
</div>
</template>
26 changes: 26 additions & 0 deletions app/components/ui/badge/Badge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui";
import type { HTMLAttributes } from "vue";
import type { BadgeVariants } from ".";
import { reactiveOmit } from "@vueuse/core";
import { Primitive } from "reka-ui";
import { cn } from "@/lib/utils";
import { badgeVariants } from ".";

const props = defineProps<PrimitiveProps & {
variant?: BadgeVariants["variant"];
class?: HTMLAttributes["class"];
}>();

const delegatedProps = reactiveOmit(props, "class");
</script>

<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
</template>
26 changes: 26 additions & 0 deletions app/components/ui/badge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";

export { default as Badge } from "./Badge.vue";

export const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
);
export type BadgeVariants = VariantProps<typeof badgeVariants>;
17 changes: 17 additions & 0 deletions app/components/ui/card/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<div
data-slot="card"
:class="cn('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', props.class)"
>
<slot />
</div>
</template>
17 changes: 17 additions & 0 deletions app/components/ui/card/CardAction.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<div
data-slot="card-action"
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
>
<slot />
</div>
</template>
17 changes: 17 additions & 0 deletions app/components/ui/card/CardContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<div
data-slot="card-content"
:class="cn('px-6', props.class)"
>
<slot />
</div>
</template>
17 changes: 17 additions & 0 deletions app/components/ui/card/CardDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<p
data-slot="card-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>
17 changes: 17 additions & 0 deletions app/components/ui/card/CardFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<div
data-slot="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
>
<slot />
</div>
</template>
17 changes: 17 additions & 0 deletions app/components/ui/card/CardHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<div
data-slot="card-header"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
>
<slot />
</div>
</template>
17 changes: 17 additions & 0 deletions app/components/ui/card/CardTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";

const props = defineProps<{
class?: HTMLAttributes["class"];
}>();
</script>

<template>
<h3
data-slot="card-title"
:class="cn('leading-none font-semibold', props.class)"
>
<slot />
</h3>
</template>
7 changes: 7 additions & 0 deletions app/components/ui/card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { default as Card } from "./Card.vue";
export { default as CardAction } from "./CardAction.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardFooter } from "./CardFooter.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";
1 change: 1 addition & 0 deletions app/composables/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const sectionCategories: Record<string, { id: string; title: string }[]>
"components": [
{ id: "overview", title: "Overview" },
{ id: "content", title: "Content" },
{ id: "marketing", title: "Marketing" },
{ id: "element", title: "Element" },
{ id: "chat", title: "AI Chat" },
],
Expand Down
106 changes: 106 additions & 0 deletions app/registry/blocks/pricing-plans/PricingPlans.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { Check } from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";

export interface PricingPlanPrice {
month: string;
year: string;
}

export interface PricingPlanCta {
label: string;
href: string;
}

export interface PricingPlan {
title: string;
description: string;
price: PricingPlanPrice;
cta: PricingPlanCta;
features: string[];
highlight?: boolean;
}

export interface PricingPlansProps {
/** Pricing plans supplied by the consuming app */
plans?: PricingPlan[];
/** Selected billing period to render */
billingPeriod?: "month" | "year";
/** Label for highlighted plans. Use an empty string to hide the badge. */
highlightLabel?: string;
/** Additional CSS classes for the section */
class?: HTMLAttributes["class"];
}

const props = withDefaults(defineProps<PricingPlansProps>(), {
plans: () => [],
billingPeriod: "month",
highlightLabel: "Popular",
});
</script>

<template>
<section
data-slot="pricing-plans"
:class="cn('py-12 md:py-16', props.class)"
>
<div class="mx-auto grid w-full max-w-7xl gap-4 px-4 sm:px-6 lg:grid-cols-3 lg:px-8">
<Card
v-for="plan in plans"
:key="plan.title"
class="relative flex rounded-2xl p-0"
:class="plan.highlight ? 'border-primary/50 ring-1 ring-primary/30' : 'bg-card/70'"
>
<CardContent class="flex w-full flex-col p-6">
<Badge
v-if="plan.highlight && highlightLabel"
class="absolute right-4 top-4"
>
{{ highlightLabel }}
</Badge>

<h2 class="text-xl font-semibold">
{{ plan.title }}
</h2>
<p class="mt-2 text-sm/6 text-muted-foreground">
{{ plan.description }}
</p>

<div class="mt-6 flex items-end gap-1">
<span class="text-4xl font-semibold tracking-tight">
{{ plan.price[billingPeriod] }}
</span>
<span class="pb-1 text-sm text-muted-foreground">/{{ billingPeriod }}</span>
</div>

<Button
as="a"
:href="plan.cta.href"
class="mt-6"
:variant="plan.highlight ? 'default' : 'outline'"
>
{{ plan.cta.label }}
</Button>

<ul class="mt-6 space-y-3 text-sm/6">
<li
v-for="feature in plan.features"
:key="feature"
class="flex gap-3"
>
<Check
class="mt-0.5 size-4 shrink-0 text-primary"
aria-hidden="true"
/>
<span>{{ feature }}</span>
</li>
</ul>
</CardContent>
</Card>
</div>
</section>
</template>
1 change: 1 addition & 0 deletions app/registry/blocks/pricing-plans/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as PricingPlans, type PricingPlan, type PricingPlanCta, type PricingPlanPrice, type PricingPlansProps } from "./PricingPlans.vue";
2 changes: 1 addition & 1 deletion content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineContentConfig({
},
schema: z.object({
category: z
.enum(["element", "content", "chat", "overview"])
.enum(["element", "content", "marketing", "chat", "overview"])
.optional(),
}),
}),
Expand Down
Loading
Loading