diff --git a/apps/web/content/docs/introduction.mdx b/apps/web/content/docs/introduction.mdx index 746f739e..846d4eef 100644 --- a/apps/web/content/docs/introduction.mdx +++ b/apps/web/content/docs/introduction.mdx @@ -9,4 +9,58 @@ PayKit is an embedded Stripe billing framework for TypeScript apps. Define plans PayKit aims to be a complete billing framework. It provides a wide range of features out of the box and allows you to extend it with plugins. - + + +**Products in Code** + +Define plans and features as typed primitives. + + + +**Webhooks Handled** + +Verified, deduplicated, synced to your database automatically. + + + +**Usage Billing** + +Metered features with check() and report(). + + + +**Built For Stripe** + +Stripe subscriptions, webhooks, portal, and product sync built in. + + + +**Plugin Ecosystem** + +Dashboard, analytics, or build your own plugin. + + + +**Local Billing State** + +Billing state in your Postgres, joinable with your tables. + + + +**CLI** + +Init, push, and status. Scaffold, migrate, validate. + + + +**Client SDK** + +Browser-side billing calls with full type inference. + + + +**Type-safe** + +Plan IDs, feature IDs, events — all inferred from your schema. + + diff --git a/apps/web/source.config.ts b/apps/web/source.config.ts index 3a6ea149..f8e105d8 100644 --- a/apps/web/source.config.ts +++ b/apps/web/source.config.ts @@ -7,7 +7,16 @@ export const docs = defineDocs({ dir: "./content/docs", docs: { postprocess: { - includeProcessedMarkdown: true, + includeProcessedMarkdown: { + stringify(node, _parent, state, info) { + if ( + node.type === "mdxJsxFlowElement" && + (node.name === "Features" || node.name === "FeatureCard") + ) { + return state.containerFlow(node, info); + } + }, + }, }, }, }); diff --git a/apps/web/src/components/docs/docs-mdx-components.tsx b/apps/web/src/components/docs/docs-mdx-components.tsx index 64831709..2074b3a6 100644 --- a/apps/web/src/components/docs/docs-mdx-components.tsx +++ b/apps/web/src/components/docs/docs-mdx-components.tsx @@ -9,7 +9,7 @@ import { RiLinkM, } from "react-icons/ri"; -import { Features } from "@/components/docs/features"; +import { FeatureCard, Features } from "@/components/docs/features"; import { MdxTab, MdxTabs, MdxTabsList, MdxTabsPanel, MdxTabsTab } from "@/components/docs/mdx-tabs"; import { Anchor, @@ -308,5 +308,6 @@ export const docsMdxComponents = { TabsList: MdxTabsList, TabsPanel: MdxTabsPanel, TabsTab: MdxTabsTab, + FeatureCard, Features, } satisfies MDXComponents; diff --git a/apps/web/src/components/docs/features.tsx b/apps/web/src/components/docs/features.tsx index a90944d0..bce1cf9c 100644 --- a/apps/web/src/components/docs/features.tsx +++ b/apps/web/src/components/docs/features.tsx @@ -1,6 +1,5 @@ -"use client"; - import type { ReactNode } from "react"; +import { Children, isValidElement } from "react"; import { RiBox3Line, RiComputerLine, @@ -13,75 +12,61 @@ import { RiWebhookLine, } from "react-icons/ri"; +import { extractText } from "@/components/docs/react-node-text"; import { FrameCorners } from "@/components/ui/frame-corners"; -const features: { icon: ReactNode; title: string; description: string }[] = [ - { - icon: , - title: "Products in Code", - description: "Define plans and features as typed primitives.", - }, - { - icon: , - title: "Webhooks Handled", - description: "Verified, deduplicated, synced to your database automatically.", - }, - { - icon: , - title: "Usage Billing", - description: "Metered features with check() and report().", - }, - { - icon: , - title: "Built For Stripe", - description: "Stripe subscriptions, webhooks, portal, and product sync built in.", - }, - { - icon: , - title: "Plugin Ecosystem", - description: "Dashboard, analytics, or build your own plugin.", - }, - { - icon: , - title: "Local Billing State", - description: "Billing state in your Postgres, joinable with your tables.", - }, - { - icon: , - title: "CLI", - description: "Init, push, and status. Scaffold, migrate, validate.", - }, - { - icon: , - title: "Client SDK", - description: "Browser-side billing calls with full type inference.", - }, - { - icon: , - title: "Type-safe", - description: "Plan IDs, feature IDs, events — all inferred from your schema.", - }, -]; +const featureIcons: Record = { + "Products in Code": RiBox3Line, + "Webhooks Handled": RiWebhookLine, + "Usage Billing": RiSpeedUpLine, + "Built For Stripe": RiPlug2Line, + "Plugin Ecosystem": RiPuzzle2Line, + "Local Billing State": RiDatabase2Line, + CLI: RiTerminalBoxLine, + "Client SDK": RiComputerLine, + "Type-safe": RiShieldCheckLine, +}; + +export function Features({ children }: { children: ReactNode }) { + return ( +
{children}
+ ); +} + +/** Renders a feature's Markdown title and description in the card layout. */ +export function FeatureCard({ children }: { children: ReactNode }) { + const blocks = Children.toArray(children); + const [titleNode, descriptionNode] = blocks; + + if ( + blocks.length !== 2 || + !isValidElement<{ children?: ReactNode }>(titleNode) || + !isValidElement<{ children?: ReactNode }>(descriptionNode) + ) { + throw new Error("FeatureCard needs a Markdown title and description paragraph."); + } + + const title = extractText(titleNode.props.children).trim().replace(/\s+/g, " "); + const Icon = featureIcons[title]; + + if (!Icon || !extractText(descriptionNode.props.children).trim()) { + throw new Error(`FeatureCard has an unknown title or empty description: ${title}`); + } -export function Features() { return ( -
- {features.map((feature) => ( -
- -
- - {feature.icon} - -
-

{feature.title}

-

- {feature.description} -

-
-
+
+ +
+ + + +
+

{title}

+

+ {descriptionNode.props.children} +

- ))} +
); }