Skip to content
Lantern

Getting started

Getting started

How to consume Lantern in a product repo, how to run this site, and what to do when a component is finished.

Consuming the system

Three steps, and the first two are what stop the tokens drifting between repos again.

01

Extend the preset — never copy the theme

tailwind.config.ts
// tailwind.config.ts
import btsPreset from "bts-storybook/tailwind-preset";

export default {
  presets: [btsPreset],
  content: ["./src/**/*.{ts,tsx}"],
};
02

Import the stylesheet once

main.tsx
// main.tsx — once, at the application root
import "bts-storybook/styles.css";
03

Import components from the package root

checkout-footer.tsx
import { Button } from "bts-storybook";

export function CheckoutFooter() {
  return (
    <div className="flex gap-3">
      <Button variant="tertiary">Back</Button>
      <Button>Continue to payment</Button>
    </div>
  );
}

You need both the preset and the stylesheet

The preset supplies the Tailwind theme; the stylesheet supplies the custom properties the theme points at, plus the hand-written classes (.btn-white, .nav-item, .stat-card) that several components depend on. The preset alone produces utilities that resolve to nothing.

Running Lantern

Storybook is where components are built. This site is where they are explained.

terminal
# from the repo root — the component library in Storybook
npm run storybook

# from lantern/ — this documentation site
npm install
npm run dev

Lantern runs on port 3100 so it can sit alongside Storybook on 6006 without a clash. The two are not alternatives — Storybook drives a component through its states in isolation; Lantern says what it is for, when to reach for it, and what the rules around it are.

How this site is wired

Worth understanding before you change it, because two of the decisions are load-bearing.

It compiles the library's source directly

lantern/ is a sibling of the design system, not a consumer of a published tarball. Next compiles ../src straight from disk, so the site cannot drift from the code it documents — if a component breaks, this site fails to build. That is why @/* is aliased to the design system rather than to the site's own code: the library imports @/lib/utils internally, and remapping it would mean patching components on the way in.

Every primitive comes through one seam

Pages import from ~/ds and nothing else. That file re-exports what has been promoted and falls through to stand-ins for what has not — so finishing a component is a two-line change in one file rather than a search across the site.

lantern/src/ds/index.ts
// lantern/src/ds/index.ts

// PROMOTED — shipping from the design system.
export { Button, buttonVariants, cn } from "@ds";
export type { ButtonProps } from "@ds";

// PENDING — stand-ins, awaiting promotion.
export * from "./pending";

Adding a finished component

The five steps. Nothing else on the site needs editing — the index, the sidebar and the component page all read from the same registry.

five steps
// 1. Re-export it in lantern/src/ds/index.ts
export { Card, CardHeader, CardTitle, CardContent } from "@ds";

// 2. Delete the matching stand-in from lantern/src/ds/pending.tsx
//    (leave it in and TypeScript fails on the duplicate export —
//     which is the point: you cannot forget this step)

// 3. Add examples in lantern/src/registry/card.tsx
export function CardBasic() {
  return (
    <Card>
      <CardHeader><CardTitle>Revenue</CardTitle></CardHeader>
      <CardContent>...</CardContent>
    </Card>
  );
}

// 4. Register them in lantern/src/registry/index.tsx
{ id: "card/basic", name: "Basic", component: card.CardBasic }

// 5. Point the catalogue entry at them
//    lantern/src/content/components.ts
{ slug: "card", status: "available", demoId: "card", ... }

Step 2 is not optional

Leaving the stand-in in place next to the real export is a duplicate export, and TypeScript fails the build on it. That is deliberate — there is no way to promote a component and quietly leave the site rendering the placeholder.

What makes a good example

Static and deterministic. No Math.random, no Date.now-relative labels, no live intervals, no network calls. Lift internal state to a prop so each state is a fixed, reviewable thing rather than a click-path.

Cover both breakpoints

If the component carries a max-md: / md: pair, add an example with defaultViewport: 'mobile'. The preview is a real iframe, so it renders the actual mobile tree rather than a narrow div pretending to be one.