Learning outcome
You will compose the existing Card and Button so that theme changes and
product-copy changes have different homes.
This tutorial assumes you are creating a read-only status panel in the jongminchung repository. The learning goal is to experience ownership boundaries, not to implement a full product workflow.
Prerequisites
- Work from the repository root.
- Read
apps/web/components.jsonandDESIGN_SYSTEM.mdfirst. - Use the existing
CardandButtoninstead of installing a registry component. - Keep the example as a Server Component.
Inspect the current component API
Read the real exports and variants from source.
rg -n "export.*Card|buttonVariants" packages/ui/src/components pnpm exec shadcn info --json -c apps/webThis prevents a remembered API from creating an unnecessary wrapper or a variant that does not exist.
Create a product composition in the app
Let
apps/webown copy and product state while shared primitives retain reusable appearance and accessibility.import { buttonVariants } from "@jongminchung/ui/components/button"; import { Card, CardDescription, CardFooter, CardHeader, CardTitle, } from "@jongminchung/ui/components/card"; import Link from "next/link"; import type { ComponentProps } from "react"; export function DocumentationHealthPanel({ className, ...props }: ComponentProps<typeof Card>) { return ( <Card {...props} className={className}> <CardHeader> <CardTitle>Documentation contracts are current</CardTitle> <CardDescription> Locale pairs and internal links passed the latest build. </CardDescription> </CardHeader> <CardFooter> <Link className={buttonVariants({ variant: "outline", size: "sm" })} href="/en/series" > Browse series </Link> </CardFooter> </Card> ); }The composition owns product copy while
CardHeaderandCardFooterown internal spacing. The primitive owns button focus, size, and colors.Express visual meaning with semantic tokens
Use
text-muted-foregroundfor supporting text and the existing button variant for the action. Do not addtext-gray-500,bg-white dark:bg-*, ortext-white.If a new color seems necessary, first test whether an existing role expresses the meaning. Add a new role only when it recurs across the product, together with light and dark providers, a foreground pair, an adapter, and a contract test.
Let the page decide placement only
At the call site, set external width and grid placement.
<DocumentationHealthPanel className="max-w-xl lg:col-span-2" />If the component accepts
className, keep consumer overrides away from internal colors, padding, typography, and interaction states.Run the nearest verification
pnpm run fmt:check pnpm --filter @jongminchung/web run typecheck pnpm --filter @jongminchung/web run test pnpm --filter @jongminchung/web run buildReview light and dark modes at mobile and wide viewports when visuals change. If you edited a shared primitive, expand verification to the UI package and its other consumers.
Confirm the result
- Product copy and workflow remain in
apps/web. - The shared primitive has no product-named variant.
- No raw palette or dynamic class fragment was added.
- Styling alone did not add
"use client". - Formatting, types, focused tests, and the build pass.
Next lesson
If an existing screen already contains scattered overrides and raw colors, continue with the audit guide.