From 292a913486c7ae2b169e1f57b2590d524d1ccfd3 Mon Sep 17 00:00:00 2001 From: Hirotaka Mizutani <52546+hirotaka@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:53:03 +0900 Subject: [PATCH] feat: add pricing plans registry block --- AGENTS.md | 2 +- app/components/demo/PricingPlansDemo.vue | 65 +++++++++++ app/components/ui/badge/Badge.vue | 26 +++++ app/components/ui/badge/index.ts | 26 +++++ app/components/ui/card/Card.vue | 17 +++ app/components/ui/card/CardAction.vue | 17 +++ app/components/ui/card/CardContent.vue | 17 +++ app/components/ui/card/CardDescription.vue | 17 +++ app/components/ui/card/CardFooter.vue | 17 +++ app/components/ui/card/CardHeader.vue | 17 +++ app/components/ui/card/CardTitle.vue | 17 +++ app/components/ui/card/index.ts | 7 ++ app/composables/useNavigation.ts | 1 + .../blocks/pricing-plans/PricingPlans.vue | 106 ++++++++++++++++++ app/registry/blocks/pricing-plans/index.ts | 1 + content.config.ts | 2 +- content/docs/2.components/7.pricing-plans.md | 99 ++++++++++++++++ scripts/registry-verify.ts | 4 + 18 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 app/components/demo/PricingPlansDemo.vue create mode 100644 app/components/ui/badge/Badge.vue create mode 100644 app/components/ui/badge/index.ts create mode 100644 app/components/ui/card/Card.vue create mode 100644 app/components/ui/card/CardAction.vue create mode 100644 app/components/ui/card/CardContent.vue create mode 100644 app/components/ui/card/CardDescription.vue create mode 100644 app/components/ui/card/CardFooter.vue create mode 100644 app/components/ui/card/CardHeader.vue create mode 100644 app/components/ui/card/CardTitle.vue create mode 100644 app/components/ui/card/index.ts create mode 100644 app/registry/blocks/pricing-plans/PricingPlans.vue create mode 100644 app/registry/blocks/pricing-plans/index.ts create mode 100644 content/docs/2.components/7.pricing-plans.md diff --git a/AGENTS.md b/AGENTS.md index 7fd1d04..d8482c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/app/components/demo/PricingPlansDemo.vue b/app/components/demo/PricingPlansDemo.vue new file mode 100644 index 0000000..bc948e5 --- /dev/null +++ b/app/components/demo/PricingPlansDemo.vue @@ -0,0 +1,65 @@ + + + diff --git a/app/components/ui/badge/Badge.vue b/app/components/ui/badge/Badge.vue new file mode 100644 index 0000000..ee60757 --- /dev/null +++ b/app/components/ui/badge/Badge.vue @@ -0,0 +1,26 @@ + + + diff --git a/app/components/ui/badge/index.ts b/app/components/ui/badge/index.ts new file mode 100644 index 0000000..6015c4d --- /dev/null +++ b/app/components/ui/badge/index.ts @@ -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; diff --git a/app/components/ui/card/Card.vue b/app/components/ui/card/Card.vue new file mode 100644 index 0000000..230f9bc --- /dev/null +++ b/app/components/ui/card/Card.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardAction.vue b/app/components/ui/card/CardAction.vue new file mode 100644 index 0000000..d7ae294 --- /dev/null +++ b/app/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardContent.vue b/app/components/ui/card/CardContent.vue new file mode 100644 index 0000000..e787f25 --- /dev/null +++ b/app/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardDescription.vue b/app/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..35566ed --- /dev/null +++ b/app/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardFooter.vue b/app/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..0a2d744 --- /dev/null +++ b/app/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardHeader.vue b/app/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..40adf9e --- /dev/null +++ b/app/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/CardTitle.vue b/app/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..6b6e1db --- /dev/null +++ b/app/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/app/components/ui/card/index.ts b/app/components/ui/card/index.ts new file mode 100644 index 0000000..409685b --- /dev/null +++ b/app/components/ui/card/index.ts @@ -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"; diff --git a/app/composables/useNavigation.ts b/app/composables/useNavigation.ts index cf5ccf6..d030f1e 100644 --- a/app/composables/useNavigation.ts +++ b/app/composables/useNavigation.ts @@ -7,6 +7,7 @@ export const sectionCategories: Record "components": [ { id: "overview", title: "Overview" }, { id: "content", title: "Content" }, + { id: "marketing", title: "Marketing" }, { id: "element", title: "Element" }, { id: "chat", title: "AI Chat" }, ], diff --git a/app/registry/blocks/pricing-plans/PricingPlans.vue b/app/registry/blocks/pricing-plans/PricingPlans.vue new file mode 100644 index 0000000..3b79d11 --- /dev/null +++ b/app/registry/blocks/pricing-plans/PricingPlans.vue @@ -0,0 +1,106 @@ + + + diff --git a/app/registry/blocks/pricing-plans/index.ts b/app/registry/blocks/pricing-plans/index.ts new file mode 100644 index 0000000..3355cac --- /dev/null +++ b/app/registry/blocks/pricing-plans/index.ts @@ -0,0 +1 @@ +export { default as PricingPlans, type PricingPlan, type PricingPlanCta, type PricingPlanPrice, type PricingPlansProps } from "./PricingPlans.vue"; diff --git a/content.config.ts b/content.config.ts index 31ea1e4..6b69213 100644 --- a/content.config.ts +++ b/content.config.ts @@ -9,7 +9,7 @@ export default defineContentConfig({ }, schema: z.object({ category: z - .enum(["element", "content", "chat", "overview"]) + .enum(["element", "content", "marketing", "chat", "overview"]) .optional(), }), }), diff --git a/content/docs/2.components/7.pricing-plans.md b/content/docs/2.components/7.pricing-plans.md new file mode 100644 index 0000000..19f32b0 --- /dev/null +++ b/content/docs/2.components/7.pricing-plans.md @@ -0,0 +1,99 @@ +--- +title: PricingPlans +description: Render a responsive pricing plan section from app-owned data. +category: marketing +--- + +::component-preview +--- +name: PricingPlansDemo +--- +:: + +## Installation + +```bash +npx shadcn-vue@latest add "https://ui.stackhacker.io/r/pricing-plans.json" +``` + +## Usage + +```vue + + + +``` + +## App-Owned Billing + +`PricingPlans` renders pricing cards from the data you pass in. Your app owns billing periods, checkout links, auth, subscription state, and any pricing data source. + +Use the `billingPeriod` prop to decide which price label to show. Keep monthly/yearly toggles, billing-provider integration, and account logic outside the component. + +## Examples + +### Default + +::component-preview +--- +name: PricingPlansDemo +--- +:: + +## API Reference + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `plans` | `PricingPlan[]` | `[]` | Pricing plans supplied by your app. | +| `billingPeriod` | `'month' \| 'year'` | `'month'` | Selected price period to render. | +| `highlightLabel` | `string` | `'Popular'` | Label shown on highlighted plans. Use an empty string to hide it. | +| `class` | `string` | — | Additional CSS classes for the section. | + +### Types + +```ts +interface PricingPlan { + title: string + description: string + price: PricingPlanPrice + cta: PricingPlanCta + features: string[] + highlight?: boolean +} + +interface PricingPlanPrice { + month: string + year: string +} + +interface PricingPlanCta { + label: string + href: string +} +``` diff --git a/scripts/registry-verify.ts b/scripts/registry-verify.ts index 43da953..fae4e56 100644 --- a/scripts/registry-verify.ts +++ b/scripts/registry-verify.ts @@ -95,6 +95,10 @@ const expectedItems: Record = { dependencies: ["@lucide/vue"], registryDependencies: [], }, + "pricing-plans": { + dependencies: ["@lucide/vue"], + registryDependencies: ["badge", "button", "card"], + }, }; if (registryIndex) {