@autono/open-pages 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/{build-TP72kiw7.js → build-Cq6uszaI.js} +3 -3
- package/dist/cli/bin.js +4 -4
- package/dist/{config-L0IbktJ0.js → config-DniCVIl6.js} +34 -7
- package/dist/{dev-I2IyEh5W.js → dev-DyY9SvwF.js} +2 -2
- package/dist/export-DqLDEQkL.js +4 -0
- package/dist/{export-ic28osQP.js → export-pYjzc4Cl.js} +10 -3
- package/dist/{open-pages-plugin-D_DN_zjh.js → open-pages-plugin-CACQ7TVo.js} +40 -6
- package/dist/{preview-BrDuvgAN.js → preview-CLb35jHx.js} +2 -2
- package/dist/vite/index.js +2 -2
- package/package.json +1 -1
- package/skills/apply-comments/SKILL.md +4 -4
- package/skills/create-page/SKILL.md +26 -8
- package/skills/create-theme/SKILL.md +137 -150
- package/skills/current-page/SKILL.md +1 -1
- package/skills/page-authoring/SKILL.md +97 -42
- package/skills/page-authoring/references/interactivity.md +70 -22
- package/skills/page-authoring/references/layout-and-responsive.md +38 -17
- package/skills/page-authoring/references/typography-and-color.md +50 -32
- package/skills/shadcn/SKILL.md +277 -0
- package/skills/shadcn/assets/shadcn-small.png +0 -0
- package/skills/shadcn/assets/shadcn.png +0 -0
- package/skills/shadcn/cli.md +290 -0
- package/skills/shadcn/customization.md +209 -0
- package/skills/shadcn/mcp.md +105 -0
- package/skills/shadcn/registry.md +277 -0
- package/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/skills/shadcn/rules/chat.md +224 -0
- package/skills/shadcn/rules/composition.md +213 -0
- package/skills/shadcn/rules/forms.md +192 -0
- package/skills/shadcn/rules/icons.md +101 -0
- package/skills/shadcn/rules/styling.md +185 -0
- package/src/app/components/asset-view.tsx +11 -11
- package/src/app/components/command/command-menu.tsx +6 -6
- package/src/app/components/command/command.tsx +2 -2
- package/src/app/components/command/home-command-menu.tsx +2 -2
- package/src/app/components/icon-tooltip.tsx +1 -1
- package/src/app/components/language-toggle.tsx +7 -7
- package/src/app/components/sidebar/folder-item.tsx +5 -5
- package/src/app/components/sidebar/icon-picker.tsx +3 -3
- package/src/app/components/sidebar/sidebar-footer.tsx +4 -4
- package/src/app/components/sidebar/sidebar.tsx +6 -6
- package/src/app/components/theme-toggle.tsx +6 -6
- package/src/app/components/themes/theme-detail.tsx +4 -4
- package/src/app/components/themes/themes-gallery.tsx +1 -1
- package/src/app/components/ui/badge.tsx +1 -1
- package/src/app/components/ui/button.tsx +1 -1
- package/src/app/components/ui/card.tsx +1 -1
- package/src/app/components/ui/context-menu.tsx +1 -1
- package/src/app/components/ui/dialog.tsx +2 -2
- package/src/app/components/ui/dropdown-menu.tsx +1 -1
- package/src/app/components/ui/input.tsx +1 -1
- package/src/app/components/ui/label.tsx +1 -1
- package/src/app/components/ui/popover.tsx +1 -1
- package/src/app/components/ui/progress.tsx +1 -1
- package/src/app/components/ui/scroll-area.tsx +1 -1
- package/src/app/components/ui/select.tsx +1 -1
- package/src/app/components/ui/separator.tsx +1 -1
- package/src/app/components/ui/slider.tsx +1 -1
- package/src/app/components/ui/tabs.tsx +1 -1
- package/src/app/components/ui/textarea.tsx +1 -1
- package/src/app/components/ui/toggle-group.tsx +2 -2
- package/src/app/components/ui/toggle.tsx +1 -1
- package/src/app/components/ui/tooltip.tsx +1 -1
- package/src/app/frame/main.tsx +3 -1
- package/src/app/lib/themes.ts +1 -0
- package/src/app/lib/use-restart-server.ts +1 -1
- package/src/app/routes/home-shell.tsx +7 -7
- package/src/app/routes/home.tsx +5 -5
- package/src/app/routes/page.tsx +2 -2
- package/src/app/routes/themes.tsx +1 -1
- package/src/app/virtual.d.ts +3 -0
- package/dist/export-B-kBRWB1.js +0 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: page-authoring
|
|
3
|
-
description: Technical reference for writing or editing open-pages pages — file contract,
|
|
3
|
+
description: Technical reference for writing or editing open-pages pages — file contract, composing from the preinstalled shadcn/ui set under `ui/`, semantic theme tokens, layout and responsive breakpoints, web type scale, interactivity with hooks and forms, assets and fonts, plain `index.html` pages, and the self-review checklist. Consult this whenever you are about to write or modify any file under `pages/<id>/`, including from inside the `create-page` or `apply-comments` workflows, or for any ad-hoc page edit. Triggers on phrases like "edit the page", "fix the layout", "change the palette", "add a section", "make it responsive", "add a form", "use a dialog", "investigate the page framework", "how do pages work here".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Authoring open-pages pages
|
|
@@ -10,19 +10,37 @@ This skill is the **technical reference** for everything that happens inside `pa
|
|
|
10
10
|
- `create-page` owns "build a new page" — it asks the user scoping questions, then delegates the *how* to this skill.
|
|
11
11
|
- `apply-comments` owns "process inspector markers" — it finds markers and applies edits, but the edits themselves follow the rules here.
|
|
12
12
|
- `current-page` resolves deictic references ("this page", "this element") to a concrete `pageId` + selection. Consult it **first** when the user references the current page without naming it, then come back here for how to edit it.
|
|
13
|
+
- `shadcn` (the vendored official skill) covers the shadcn CLI, registries, presets, and component-level docs. Consult it for "how does `<Combobox>` work" or "add a block from a registry"; this skill covers how pages in *this* workspace use those components.
|
|
13
14
|
- Any ad-hoc page edit (manual tweak, one-off fix) should also consult this skill before touching the file.
|
|
14
15
|
|
|
15
16
|
A page here is a **real web page**: one React component rendered into a real browser document. The workspace previews it live in an iframe, the inspector maps clicks back to source lines, and `open-pages export` turns it into a static folder you can deploy anywhere. Your job is a good web page; the runtime's job is preview, comments, and export.
|
|
16
17
|
|
|
18
|
+
## What the workspace already gives you
|
|
19
|
+
|
|
20
|
+
Every workspace is a shadcn/ui project out of the box. Nothing needs installing:
|
|
21
|
+
|
|
22
|
+
| Path | What it is |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `ui/*.tsx` | The full shadcn/ui set (61 components: accordion, alert, alert-dialog, aspect-ratio, attachment, avatar, badge, breadcrumb, bubble, button, button-group, calendar, card, carousel, chart, checkbox, collapsible, combobox, command, context-menu, dialog, direction, drawer, dropdown-menu, empty, field, form, hover-card, input, input-group, input-otp, item, kbd, label, marker, menubar, message, message-scroller, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, sidebar, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip). Import as `@/ui/<name>`. |
|
|
25
|
+
| `lib/utils.ts` | `cn()` for merging class names. Import as `@/lib/utils`. |
|
|
26
|
+
| `hooks/use-mobile.ts` | `useIsMobile()` breakpoint hook. Import as `@/hooks/use-mobile`. |
|
|
27
|
+
| `styles/globals.css` | Tailwind v4 entry: `@source` for `pages/ ui/ lib/ hooks/ themes/`, the `dark` variant, `:root`/`.dark` OKLCH tokens, `@theme inline`, base layer. Loaded into every page automatically. |
|
|
28
|
+
| `components.json` | shadcn config (style `new-york`, base `radix`, aliases `@/ui`, `@/lib/utils`, `@/hooks`, `@/components`). Activates the `shadcn` skill and CLI lookups. |
|
|
29
|
+
| `themes/<id>.css` | Optional token overrides a page opts into with `meta.theme` (see Themes). |
|
|
30
|
+
|
|
31
|
+
Installed deps you may import directly: `lucide-react` (icons), `radix-ui`, `class-variance-authority`, `clsx`, `tailwind-merge`, `cmdk`, `sonner`, `vaul`, `recharts`, `react-day-picker`, `date-fns`, `react-hook-form`, `zod`, `@hookform/resolvers`, `input-otp`, `embla-carousel-react`, `react-resizable-panels`, `next-themes`. Nothing else.
|
|
32
|
+
|
|
33
|
+
`npx shadcn@latest add <item>` is only for **blocks** (`dashboard-01`, `login-03`, …) or third-party registries; every core component is already under `ui/`. For lookups use `npx shadcn@latest docs <component> --json`, `view`, `search` — details in the `shadcn` skill.
|
|
34
|
+
|
|
17
35
|
## Topic references
|
|
18
36
|
|
|
19
37
|
Details live under `references/` in this skill. **Read the relevant file before using the feature**:
|
|
20
38
|
|
|
21
39
|
| Topic | Read before | File |
|
|
22
40
|
| --- | --- | --- |
|
|
23
|
-
| Layout + responsive | any multi-column layout, nav,
|
|
24
|
-
| Typography + color | picking a type scale
|
|
25
|
-
| Interactivity | state, forms,
|
|
41
|
+
| Layout + responsive | any multi-column layout, nav, app shell, sidebar, grid; anything that must work on mobile | `references/layout-and-responsive.md` |
|
|
42
|
+
| Typography + color | picking a type scale, the token system, dark sections, contrast | `references/typography-and-color.md` |
|
|
43
|
+
| Interactivity | state, forms, dialogs, toasts, tabs, anything with an event handler | `references/interactivity.md` |
|
|
26
44
|
| Assets + fonts | images, icons, custom fonts, Google Fonts | `references/assets-and-fonts.md` |
|
|
27
45
|
| Plain HTML pages | a page authored as `index.html` instead of React | `references/html-pages.md` |
|
|
28
46
|
|
|
@@ -30,10 +48,11 @@ Details live under `references/` in this skill. **Read the relevant file before
|
|
|
30
48
|
|
|
31
49
|
- Put the page under `pages/<kebab-case-id>/`.
|
|
32
50
|
- Entry is `pages/<id>/index.tsx` (or `pages/<id>/index.html` for a plain HTML page — see `references/html-pages.md`).
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
51
|
+
- A page may contain only `index.tsx`, `components/*.tsx`, `styles.css`, and `assets/`. Shared assets live in the root `assets/` folder and import via `@assets/...`. No `README.md`, no config files.
|
|
52
|
+
- `ui/`, `lib/`, `hooks/`, `styles/globals.css`, and `components.json` are **workspace-level and shared by every page**. Do not edit a file under `ui/` to suit one page — wrap or extend it in `pages/<id>/components/` instead (`<Button className={cn('rounded-full', …)}>`, or a `HeroButton` that renders `<Button>`). Do not edit `styles/globals.css` from a page task; token changes belong to a theme (`create-theme`).
|
|
53
|
+
- Do **not** touch `package.json`, `open-pages.config.ts`, or other pages. Do not add dependencies beyond the installed list above.
|
|
54
|
+
- **Use `@/ui/*` first.** Hand-roll an element only when no shadcn component covers it (a hero, a marketing feature grid, a footer). Buttons, inputs, cards, dialogs, tabs, tables, menus, tooltips, forms: always the `ui/` component.
|
|
55
|
+
- **Style with semantic tokens**, not raw palette classes: `bg-background text-foreground`, `bg-card`, `bg-primary text-primary-foreground`, `text-muted-foreground`, `border-border`, `bg-muted`, `bg-accent`, `text-destructive`, `ring-ring`, `rounded-lg`. Themes restyle every token at once; a page written in `bg-slate-900` ignores them. Raw palette utilities are for deliberate one-off brand moments only.
|
|
37
56
|
|
|
38
57
|
## File contract
|
|
39
58
|
|
|
@@ -41,6 +60,9 @@ Details live under `references/` in this skill. **Read the relevant file before
|
|
|
41
60
|
// pages/<id>/index.tsx
|
|
42
61
|
import type { PageMeta } from '@autono/open-pages';
|
|
43
62
|
import { useState } from 'react';
|
|
63
|
+
import { Button } from '@/ui/button';
|
|
64
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/ui/card';
|
|
65
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/tabs';
|
|
44
66
|
|
|
45
67
|
export const meta: PageMeta = {
|
|
46
68
|
title: 'Meridian — Launch',
|
|
@@ -49,100 +71,133 @@ export const meta: PageMeta = {
|
|
|
49
71
|
};
|
|
50
72
|
|
|
51
73
|
export default function Launch() {
|
|
52
|
-
const [
|
|
74
|
+
const [yearly, setYearly] = useState(false);
|
|
53
75
|
return (
|
|
54
|
-
<main className="min-h-screen bg-
|
|
55
|
-
|
|
76
|
+
<main className="min-h-screen bg-background text-foreground antialiased">
|
|
77
|
+
<section className="mx-auto max-w-5xl px-6 py-20">
|
|
78
|
+
<h1 className="text-5xl font-bold tracking-tight">Your analytics, turned into decisions</h1>
|
|
79
|
+
<div className="mt-8 flex gap-3">
|
|
80
|
+
<Button size="lg">Start free</Button>
|
|
81
|
+
<Button size="lg" variant="outline">See how it works</Button>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
<section className="mx-auto max-w-5xl px-6 pb-20">
|
|
85
|
+
<Tabs defaultValue={yearly ? 'yearly' : 'monthly'} onValueChange={(v) => setYearly(v === 'yearly')}>
|
|
86
|
+
<TabsList>
|
|
87
|
+
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
|
88
|
+
<TabsTrigger value="yearly">Yearly</TabsTrigger>
|
|
89
|
+
</TabsList>
|
|
90
|
+
<TabsContent value="monthly">
|
|
91
|
+
<Card>
|
|
92
|
+
<CardHeader><CardTitle>Growth</CardTitle></CardHeader>
|
|
93
|
+
<CardContent className="text-muted-foreground">$49 / month</CardContent>
|
|
94
|
+
</Card>
|
|
95
|
+
</TabsContent>
|
|
96
|
+
</Tabs>
|
|
97
|
+
</section>
|
|
56
98
|
</main>
|
|
57
99
|
);
|
|
58
100
|
}
|
|
59
101
|
```
|
|
60
102
|
|
|
61
|
-
- `export default` is **one zero-prop React component** — the whole page.
|
|
103
|
+
- `export default` is **one zero-prop React component** — the whole page. Its root is `min-h-screen bg-background text-foreground` (add `dark` to the root's `className` for a dark page — the variant is `&:is(.dark *)`, so every token flips underneath it).
|
|
62
104
|
- `meta.title` (optional) becomes the browser tab title and the workspace card label. Default is the folder name.
|
|
63
105
|
- `meta.description` (optional) becomes `<meta name="description">` in the exported HTML.
|
|
64
|
-
- `meta.theme` (optional)
|
|
106
|
+
- `meta.theme` (optional) opts the page into `themes/<id>.css` — the runtime injects that stylesheet into the preview frame and the export. The id must match a `themes/<id>.md` basename. Omit for the default neutral tokens.
|
|
65
107
|
- `meta.createdAt` is an **ISO 8601 string literal** set once when the page is scaffolded — **immediately before writing the file, run `node -e "console.log(new Date().toISOString())"` via Bash and paste the exact output**. Must stay a plain string literal (the framework reads it via regex, never by evaluating the module).
|
|
66
108
|
- Hooks, state, event handlers, `fetch`, `window`, client-side routing: all allowed. This is a browser. See `references/interactivity.md` for the constraints that still apply (no `window` at module top level, StrictMode double-invokes effects).
|
|
67
109
|
|
|
68
|
-
## Styling:
|
|
110
|
+
## Styling: tokens first, then shadcn, then utilities
|
|
69
111
|
|
|
70
|
-
|
|
112
|
+
1. **Tokens.** Surfaces and text use the semantic classes from `styles/globals.css`. Pairs go together: `bg-primary text-primary-foreground`, `bg-card text-card-foreground`, `bg-muted text-muted-foreground`. Borders are `border-border`; focus rings `ring-ring`; radius `rounded-md`/`rounded-lg`/`rounded-xl` (all scale from `--radius`).
|
|
113
|
+
2. **shadcn components** carry their own token styling and variants (`<Button variant="outline" size="sm">`, `<Badge variant="secondary">`). Reach for a variant before adding classes; add classes via `className` (merged with `cn`) for layout — width, margin, grid placement — not to repaint the component.
|
|
114
|
+
3. **Tailwind utilities** on `className` for everything structural: containers, grids, spacing, type sizes, responsive variants. Arbitrary values are fine (`max-w-[72ch]`). Mobile-first: unprefixed utilities are the phone layout, `sm:`/`md:`/`lg:` layer on top.
|
|
71
115
|
|
|
72
116
|
```tsx
|
|
73
117
|
<section className="mx-auto max-w-5xl px-6 py-20">
|
|
74
|
-
<
|
|
75
|
-
<
|
|
118
|
+
<Badge variant="secondary">Pricing</Badge>
|
|
119
|
+
<h2 className="mt-4 text-3xl font-bold tracking-tight">Simple plans</h2>
|
|
120
|
+
<p className="mt-3 max-w-xl text-lg text-muted-foreground">Grow at your own pace.</p>
|
|
121
|
+
<div className="mt-8 grid gap-6 sm:grid-cols-3">
|
|
122
|
+
{plans.map((p) => (
|
|
123
|
+
<Card key={p.name} className={cn(p.featured && 'border-primary')}>…</Card>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
76
126
|
</section>
|
|
77
127
|
```
|
|
78
128
|
|
|
79
|
-
- `className`, never a `tw` prop.
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
- Preflight (Tailwind's reset) is applied inside the page, so headings and buttons start unstyled — set sizes and weights explicitly.
|
|
129
|
+
- `className`, never a `tw` prop. Build class strings literally; `cn()` for conditionals — never `` `text-${size}` ``.
|
|
130
|
+
- Icons: `lucide-react` (`<ArrowRight className="size-4" />`), sized with `size-*`, `aria-hidden` when decorative.
|
|
131
|
+
- `style={{ … }}` only for values Tailwind cannot express (computed positions, CSS variables from data).
|
|
132
|
+
- A page may import its own stylesheet (`import './styles.css'`) for keyframes, complex selectors, or a font `@import`. Keep it small; never redefine the tokens there — that is a theme's job.
|
|
133
|
+
- Preflight plus the shadcn base layer are applied inside the page: headings start unstyled (set sizes and weights explicitly); `body` already carries `bg-background text-foreground`.
|
|
85
134
|
|
|
86
|
-
## Web type scale
|
|
135
|
+
## Web type scale
|
|
87
136
|
|
|
88
137
|
| Element | Size |
|
|
89
138
|
| --- | --- |
|
|
90
139
|
| Hero heading | 48–72px (`text-5xl`–`text-7xl`), tight leading + tracking |
|
|
91
140
|
| Section heading | 24–36px (`text-2xl`–`text-4xl`) |
|
|
92
141
|
| Body | 16–18px (`text-base`–`text-lg`), `leading-relaxed` |
|
|
93
|
-
| Secondary / captions | 13–14px (`text-sm`)
|
|
142
|
+
| Secondary / captions | 13–14px (`text-sm text-muted-foreground`) |
|
|
94
143
|
| Labels, eyebrows | 11–12px (`text-xs`), `uppercase tracking-widest` |
|
|
95
144
|
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
- Details in `references/typography-and-color.md`.
|
|
145
|
+
- Contrast: body text must pass 4.5:1. The tokens pass by construction; if you reach for `text-foreground/40`, you are below the floor.
|
|
146
|
+
- Details and the token → OKLCH map in `references/typography-and-color.md`.
|
|
99
147
|
|
|
100
148
|
## Data rows vs designed repeats
|
|
101
149
|
|
|
102
150
|
- **Repeated data belongs in a `.map` over a data array** — pricing tiers, feature lists, testimonials pulled from a typed const at the top of the file; keep the row JSX in the map body. A comment on any row means "this row template".
|
|
103
151
|
- **Designed repeats that differ (hero vs. secondary CTA, three distinct feature panels with custom art) are explicit instances** of a small helper component defined in the same file or under `components/`. The inspector targets source JSX; explicit instances give each block its own address, so "make the middle one green" is one edit, not three.
|
|
152
|
+
- The inspector tags components as well as host elements: shadcn components spread their props onto their root, so a click on a rendered `<Button>` resolves to the `<Button>` line in *your* page, not to `ui/button.tsx`.
|
|
104
153
|
|
|
105
154
|
## Editing an existing page
|
|
106
155
|
|
|
107
156
|
Locate the section first instead of reading the whole file:
|
|
108
157
|
|
|
109
158
|
```bash
|
|
110
|
-
grep -n '<section\|<h[12]\|<header\|<footer' pages/<id>/index.tsx
|
|
159
|
+
grep -n '<section\|<h[12]\|<header\|<footer\|<Card\b\|<Dialog\b' pages/<id>/index.tsx
|
|
111
160
|
```
|
|
112
161
|
|
|
113
|
-
Landmarks and
|
|
162
|
+
Landmarks, headings, and top-level shadcn wrappers anchor sections; read the target range with `offset` + `limit`. Read the whole file when auditing tokens or restructuring.
|
|
114
163
|
|
|
115
164
|
## Themes
|
|
116
165
|
|
|
117
|
-
|
|
166
|
+
A theme is `themes/<id>.md` (direction and component notes) + `themes/<id>.css` (token overrides) + `themes/<id>.demo.tsx` (a demo page). If the page is meant to follow one, **read `themes/<id>.md` first — its guidance overrides the defaults in this skill** — and set `meta.theme: '<id>'`; the runtime injects the CSS. Do not paste token values into the page; the whole point is that the page stays token-based and the theme repaints it. Themes are produced by the `create-theme` skill.
|
|
118
167
|
|
|
119
168
|
## Runtime behavior you get for free
|
|
120
169
|
|
|
121
170
|
- Home lists every folder under `pages/`; cards show a live, scaled-down preview of the real page.
|
|
122
171
|
- The page view at `http://localhost:5173/p/<id>` renders the page in an iframe with **Desktop / Tablet (820px) / Mobile (390px)** viewport toggles, a reload button, and **Open** (the page by itself in a new tab).
|
|
123
|
-
- **Inspect mode** (toolbar button or `i`): the user clicks any element, sees its tag and source line, and can leave a comment that lands in your source as an `@page-comment` marker (see `apply-comments`). The current selection is always in `node_modules/.open-pages/current.json` (see `current-page`).
|
|
172
|
+
- **Inspect mode** (toolbar button or `i`): the user clicks any element or component, sees its tag and source line, and can leave a comment that lands in your source as an `@page-comment` marker (see `apply-comments`). The current selection is always in `node_modules/.open-pages/current.json` (see `current-page`).
|
|
124
173
|
- Hot reload: save any file under `pages/<id>/` and the preview updates in place.
|
|
125
|
-
- `open-pages export <id>` builds `export/<id>/` — `index.html` plus hashed assets with relative URLs. Drop the folder on Netlify, Vercel, Cloudflare Pages, GitHub Pages, or S3.
|
|
174
|
+
- `open-pages export <id>` builds `export/<id>/` — `index.html` plus hashed assets with relative URLs, bundling only the `ui/` components the page imports. Drop the folder on Netlify, Vercel, Cloudflare Pages, GitHub Pages, or S3.
|
|
126
175
|
|
|
127
176
|
## Self-review before finishing
|
|
128
177
|
|
|
129
|
-
- [ ] `pages/<id>/index.tsx` default-exports **one** component; `meta` has `title` + fresh `createdAt` literal.
|
|
130
|
-
- [ ] Root element
|
|
178
|
+
- [ ] `pages/<id>/index.tsx` default-exports **one** component; `meta` has `title` + fresh `createdAt` literal (+ `theme` if built from one).
|
|
179
|
+
- [ ] Root element is `min-h-screen bg-background text-foreground`; the page has a `<main>` landmark.
|
|
180
|
+
- [ ] Every button, input, card, dialog, tab, table, menu, tooltip, or form is the `@/ui/*` component, not a hand-rolled lookalike.
|
|
181
|
+
- [ ] Colors are semantic tokens; raw palette classes appear only where a one-off brand moment was intended.
|
|
182
|
+
- [ ] Nothing under `ui/`, `lib/`, `hooks/`, or `styles/` was edited.
|
|
131
183
|
- [ ] Preview it at `http://localhost:5173/p/<id>` (or ask the user to). No error banner, no console errors.
|
|
132
184
|
- [ ] Check the **Mobile** viewport: no horizontal overflow, nav collapses or wraps, grids stack, text sizes step down.
|
|
133
|
-
- [ ] Every `className` is Tailwind v4 the scanner will see (literal strings,
|
|
134
|
-
- [ ] Interactive elements are real `<
|
|
135
|
-
- [ ] One coherent
|
|
185
|
+
- [ ] Every `className` is Tailwind v4 the scanner will see (literal strings, `cn()` for conditionals).
|
|
186
|
+
- [ ] Interactive elements are real `<Button>`/`<a>`/`<Input>` with visible focus styles and labels; images have `alt`.
|
|
187
|
+
- [ ] One coherent type scale across the page; contrast holds on dark sections.
|
|
136
188
|
- [ ] Designed repeats are explicit component instances; data lists are a `.map` over a typed const.
|
|
137
189
|
- [ ] No `window`/`document` access at module top level; effects clean up.
|
|
138
190
|
- [ ] Nothing outside `pages/<id>/` was edited.
|
|
139
191
|
|
|
140
192
|
## Anti-patterns
|
|
141
193
|
|
|
194
|
+
- ❌ A hand-rolled `<button className="rounded-md bg-slate-900 px-4 …">` when `<Button>` exists. Same for inputs, cards, dialogs, dropdowns, tabs, tables.
|
|
195
|
+
- ❌ Repainting a component with raw colors (`<Button className="bg-blue-600">`). Use a variant, or change the theme.
|
|
196
|
+
- ❌ Editing `ui/button.tsx` because one page wants rounder buttons. Wrap it under `pages/<id>/components/`.
|
|
142
197
|
- ❌ `tw` props, `pageOptions`, fixed-size "page" `<div>`s, or any document/slide thinking. This is a scrolling web page.
|
|
143
198
|
- ❌ Desktop-only layouts: absolute pixel positioning, fixed widths on the root, `grid-cols-3` with no `sm:` fallback.
|
|
144
|
-
- ❌ Building class names at runtime (`` `text-${size}` ``) — Tailwind only generates utilities it can read literally.
|
|
145
|
-
- ❌ `<div onClick>` where a `<
|
|
199
|
+
- ❌ Building class names at runtime (`` `text-${size}` ``) — Tailwind only generates utilities it can read literally.
|
|
200
|
+
- ❌ `<div onClick>` where a `<Button>` belongs; `<a>` without `href`; icon buttons without `aria-label`.
|
|
146
201
|
- ❌ Tiny typography (11px body). Web body is 16px+.
|
|
147
|
-
- ❌ Global CSS resets
|
|
148
|
-
- ❌ Installing packages, editing `package.json`/config/other pages, adding a `README.md` to the page folder.
|
|
202
|
+
- ❌ Global CSS resets, `body {}` rules, or `:root { --primary: … }` in the page's `styles.css`. Tokens belong to themes.
|
|
203
|
+
- ❌ Installing packages, `npx shadcn add` for a component already in `ui/`, editing `package.json`/config/other pages, adding a `README.md` to the page folder.
|
|
@@ -2,49 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
Pages are real React 18 components running in the browser, wrapped in
|
|
4
4
|
`StrictMode`. Everything React can do is available; the constraints below
|
|
5
|
-
keep pages preview-safe, export-safe, and inspectable.
|
|
5
|
+
keep pages preview-safe, export-safe, and inspectable. Interactive UI comes
|
|
6
|
+
from `ui/` first — `Tabs`, `Accordion`, `Dialog`, `Sheet`, `Drawer`,
|
|
7
|
+
`DropdownMenu`, `Popover`, `Tooltip`, `Switch`, `Select`, `Combobox`,
|
|
8
|
+
`Command`, `Slider`, `Toggle`, `Collapsible` all ship with keyboard handling
|
|
9
|
+
and ARIA wired. Hand-rolled state is for what they do not cover.
|
|
6
10
|
|
|
7
11
|
## State
|
|
8
12
|
|
|
9
13
|
```tsx
|
|
10
|
-
const [
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
>
|
|
18
|
-
Yearly
|
|
19
|
-
</button>
|
|
14
|
+
const [billing, setBilling] = useState<'monthly' | 'yearly'>('monthly');
|
|
15
|
+
|
|
16
|
+
<Tabs value={billing} onValueChange={(v) => setBilling(v as typeof billing)}>
|
|
17
|
+
<TabsList>
|
|
18
|
+
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
|
19
|
+
<TabsTrigger value="yearly">Yearly</TabsTrigger>
|
|
20
|
+
</TabsList>
|
|
21
|
+
</Tabs>
|
|
20
22
|
```
|
|
21
23
|
|
|
22
24
|
- Keep state local and small: tabs, toggles, accordions, filters, form values.
|
|
23
25
|
- Derive, do not duplicate: compute filtered lists with `useMemo` (or inline)
|
|
24
26
|
from the source array and the filter state.
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
+
- Controlled vs. uncontrolled: shadcn components accept `defaultValue` for
|
|
28
|
+
fire-and-forget UI and `value` + `onValueChange` (or `open` +
|
|
29
|
+
`onOpenChange`) when the page needs the value.
|
|
30
|
+
- When you must hand-roll a toggle, it is a `<Button variant="outline"
|
|
31
|
+
aria-pressed={on}>`; a disclosure carries `aria-expanded` and
|
|
32
|
+
`aria-controls` — or just use `<Collapsible>`.
|
|
27
33
|
|
|
28
34
|
## Forms
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
Simple forms: `<form onSubmit>` with `@/ui/field`, `@/ui/input`, `@/ui/label`,
|
|
37
|
+
`@/ui/select`, `@/ui/checkbox`, `@/ui/textarea`, `@/ui/button`. Validated
|
|
38
|
+
forms: `react-hook-form` + `zod` through `@/ui/form`:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
42
|
+
import { useForm } from 'react-hook-form';
|
|
43
|
+
import { z } from 'zod';
|
|
44
|
+
import { Button } from '@/ui/button';
|
|
45
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/ui/form';
|
|
46
|
+
import { Input } from '@/ui/input';
|
|
47
|
+
|
|
48
|
+
const schema = z.object({ email: z.string().email('Enter a valid work email') });
|
|
49
|
+
|
|
50
|
+
const form = useForm<z.infer<typeof schema>>({ resolver: zodResolver(schema), defaultValues: { email: '' } });
|
|
51
|
+
|
|
52
|
+
<Form {...form}>
|
|
53
|
+
<form onSubmit={form.handleSubmit((values) => setSubmitted(values))} className="flex flex-col gap-4">
|
|
54
|
+
<FormField control={form.control} name="email" render={({ field }) => (
|
|
55
|
+
<FormItem>
|
|
56
|
+
<FormLabel>Work email</FormLabel>
|
|
57
|
+
<FormControl><Input type="email" autoComplete="email" {...field} /></FormControl>
|
|
58
|
+
<FormMessage />
|
|
59
|
+
</FormItem>
|
|
60
|
+
)} />
|
|
61
|
+
<Button type="submit">Join the waitlist</Button>
|
|
62
|
+
</form>
|
|
63
|
+
</Form>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- Every input has a label (`<Label htmlFor>` or `<FormLabel>`), a `name`, and
|
|
67
|
+
a sensible `type`/`inputMode`/`autoComplete`.
|
|
33
68
|
- No backend exists in the workspace or the export. Either mirror submitted
|
|
34
69
|
values into local state ("Thanks, we'll be in touch"), or point the form at
|
|
35
70
|
a URL the user supplies (`action="https://formspree.io/…" method="post"`).
|
|
36
71
|
Never invent an endpoint.
|
|
37
72
|
|
|
73
|
+
## Dialogs, sheets, toasts
|
|
74
|
+
|
|
75
|
+
- Modal confirmation: `<Dialog>` / `<AlertDialog>` with a `<DialogTrigger asChild><Button>…`.
|
|
76
|
+
Side panels: `<Sheet>`; mobile bottom panels: `<Drawer>`.
|
|
77
|
+
- Toasts: render `<Toaster />` from `@/ui/sonner` once at the root of the
|
|
78
|
+
page, then `toast('Saved')` / `toast.success(…)` from `sonner` in handlers.
|
|
79
|
+
- Menus: `<DropdownMenu>` for actions, `<Select>` for a value, `<Combobox>`
|
|
80
|
+
/ `<Command>` for searchable lists.
|
|
81
|
+
|
|
38
82
|
## Network and browser APIs
|
|
39
83
|
|
|
40
84
|
- `fetch`, `localStorage`, `IntersectionObserver`, `matchMedia` are fine —
|
|
41
|
-
inside effects or handlers, with loading
|
|
85
|
+
inside effects or handlers, with loading (`<Skeleton>`, `<Spinner>`) and
|
|
86
|
+
error (`<Alert variant="destructive">`) states.
|
|
42
87
|
- Assume the exported page may run from `file://` or a static host with no
|
|
43
88
|
API: gate network features behind a URL the user supplied, and render
|
|
44
|
-
something meaningful without it.
|
|
89
|
+
something meaningful without it (`<Empty>`).
|
|
45
90
|
- Access `window`/`document` only inside `useEffect` or event handlers.
|
|
46
91
|
Module-top-level `window.innerWidth` breaks the module import and the
|
|
47
|
-
export build.
|
|
92
|
+
export build. For breakpoints use `useIsMobile()` from `@/hooks/use-mobile`.
|
|
48
93
|
|
|
49
94
|
## Effects and StrictMode
|
|
50
95
|
|
|
@@ -57,21 +102,24 @@ const [tab, setTab] = useState<'monthly' | 'yearly'>('monthly');
|
|
|
57
102
|
|
|
58
103
|
- Content must be visible with JavaScript disabled or before hydration: no
|
|
59
104
|
empty shells that only fill from an effect.
|
|
60
|
-
- Hover-only affordances need a keyboard/touch equivalent
|
|
105
|
+
- Hover-only affordances need a keyboard/touch equivalent (`<Tooltip>` and
|
|
106
|
+
`<HoverCard>` already handle focus).
|
|
61
107
|
- Animations: `motion-safe:transition-*`; respect `prefers-reduced-motion`.
|
|
62
108
|
|
|
63
109
|
## Routing inside a page
|
|
64
110
|
|
|
65
111
|
- Simple multi-view pages: a `view` state and conditional rendering, with
|
|
66
|
-
`<a href="#section">` anchors for in-page navigation
|
|
112
|
+
`<a href="#section">` anchors for in-page navigation and `<NavigationMenu>`
|
|
113
|
+
or `<Breadcrumb>` for chrome.
|
|
67
114
|
- Hash routing is acceptable when the user asks for "pages" inside one export
|
|
68
115
|
(`window.location.hash`, read in an effect). Path-based routing needs a
|
|
69
116
|
host with rewrites — mention this to the user before choosing it.
|
|
70
117
|
|
|
71
118
|
## Anti-patterns
|
|
72
119
|
|
|
120
|
+
- ❌ A `useState` + two `<div>`s reimplementing `<Tabs>`, `<Accordion>`, or `<Dialog>`.
|
|
73
121
|
- ❌ `useEffect` to compute something derivable from props/state.
|
|
74
122
|
- ❌ Fetching from an endpoint nobody set up.
|
|
75
123
|
- ❌ `document.querySelector` to mutate the DOM React owns.
|
|
76
124
|
- ❌ Global listeners without cleanup; timers that outlive the component.
|
|
77
|
-
- ❌ Interactive `<div>`s. Use `<
|
|
125
|
+
- ❌ Interactive `<div>`s. Use `<Button>`, `<a>`, `<Input>`, `<Select>`.
|
|
@@ -4,7 +4,9 @@ The viewer previews every page at three widths — **Desktop** (the full
|
|
|
4
4
|
viewport), **Tablet** (820px), and **Mobile** (390px). A page is done when all
|
|
5
5
|
three look intentional. Tailwind is mobile-first: unprefixed utilities are the
|
|
6
6
|
phone layout; `sm:` (640px), `md:` (768px), `lg:` (1024px), `xl:` (1280px)
|
|
7
|
-
layer larger layouts on top.
|
|
7
|
+
layer larger layouts on top. `useIsMobile()` from `@/hooks/use-mobile` is
|
|
8
|
+
available when JS must know (swap a `<Sheet>` for a `<Drawer>`, collapse a
|
|
9
|
+
table into cards).
|
|
8
10
|
|
|
9
11
|
## Containers
|
|
10
12
|
|
|
@@ -17,14 +19,19 @@ layer larger layouts on top.
|
|
|
17
19
|
- Reading columns: `max-w-2xl` / `max-w-[65ch]` for prose. Marketing grids:
|
|
18
20
|
`max-w-5xl`–`max-w-7xl`.
|
|
19
21
|
- Vertical rhythm: `py-16` between sections on mobile, `sm:py-24` on desktop.
|
|
20
|
-
Sections separate by space or a hairline (`border-t border-
|
|
21
|
-
both.
|
|
22
|
+
Sections separate by space or a hairline (`border-t border-border`, or a
|
|
23
|
+
`<Separator />`), not both.
|
|
22
24
|
|
|
23
25
|
## Grids and stacks
|
|
24
26
|
|
|
25
27
|
```tsx
|
|
26
28
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
27
|
-
{features.map((f) =>
|
|
29
|
+
{features.map((f) => (
|
|
30
|
+
<Card key={f.title}>
|
|
31
|
+
<CardHeader><CardTitle>{f.title}</CardTitle></CardHeader>
|
|
32
|
+
<CardContent className="text-muted-foreground">{f.body}</CardContent>
|
|
33
|
+
</Card>
|
|
34
|
+
))}
|
|
28
35
|
</div>
|
|
29
36
|
```
|
|
30
37
|
|
|
@@ -32,35 +39,48 @@ layer larger layouts on top.
|
|
|
32
39
|
- `flex flex-col gap-4 sm:flex-row sm:items-center` for header rows that must
|
|
33
40
|
stack on phones.
|
|
34
41
|
- Use `gap-*`, not margins on children, for spacing inside grids and flex rows.
|
|
42
|
+
- Tabular data is `<Table>` from `@/ui/table` inside an `overflow-x-auto`
|
|
43
|
+
wrapper; on mobile either let it scroll or render a card list.
|
|
35
44
|
|
|
36
45
|
## Navigation
|
|
37
46
|
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
- Marketing header: wordmark + `<NavigationMenu>` (or plain links) with
|
|
48
|
+
`hidden sm:flex`, one `<Button>` CTA. On mobile, a `<Sheet side="left">`
|
|
49
|
+
opened by an icon `<Button variant="ghost" size="icon" aria-label="Menu">`
|
|
50
|
+
holding the stacked links. Never let a nav overflow horizontally.
|
|
51
|
+
- Sticky headers: `sticky top-0 z-10 bg-background/90 backdrop-blur border-b border-border`.
|
|
52
|
+
- Sub-navigation and hierarchy: `<Breadcrumb>`, `<Tabs>`, `<Pagination>`.
|
|
43
53
|
|
|
44
54
|
## Hero
|
|
45
55
|
|
|
46
56
|
- Heading `text-4xl sm:text-6xl lg:text-7xl`, `leading-[1.05] tracking-tight`.
|
|
47
57
|
- Constrain the heading (`max-w-3xl`) and the lede (`max-w-xl`) independently.
|
|
48
|
-
- CTA row: `flex flex-col gap-3 sm:flex-row`
|
|
58
|
+
- CTA row: `flex flex-col gap-3 sm:flex-row` with `<Button size="lg">` and
|
|
59
|
+
`<Button size="lg" variant="outline">` so buttons stack on mobile.
|
|
49
60
|
|
|
50
|
-
##
|
|
61
|
+
## App shells (dashboards, tools, docs)
|
|
51
62
|
|
|
52
63
|
- Root: `min-h-screen` (not `h-screen`) so content can scroll.
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
- Sidebar layouts use `@/ui/sidebar`: wrap the page in `<SidebarProvider>`,
|
|
65
|
+
render `<Sidebar>` + `<SidebarInset>`; it collapses to a sheet on mobile by
|
|
66
|
+
itself and gives you `<SidebarTrigger />` for the topbar. Do not hand-roll
|
|
67
|
+
a `hidden lg:block w-64` aside when this exists.
|
|
68
|
+
- Split panes (editor + preview, list + detail): `@/ui/resizable`
|
|
69
|
+
(`ResizablePanelGroup` / `ResizablePanel` / `ResizableHandle`); stack them
|
|
70
|
+
(`direction="vertical"`) or render one pane at a time on mobile via
|
|
71
|
+
`useIsMobile()`.
|
|
72
|
+
- Content area: `min-w-0 flex-1` so tables and code blocks can shrink. Wide
|
|
73
|
+
content scrolls inside its own `overflow-x-auto` wrapper (or
|
|
74
|
+
`<ScrollArea>`); the page body must never scroll horizontally.
|
|
75
|
+
- Loading and empty states: `<Skeleton>` while data arrives, `<Empty>` when
|
|
76
|
+
there is none.
|
|
57
77
|
|
|
58
78
|
## Checklist
|
|
59
79
|
|
|
60
|
-
- [ ] Mobile: no horizontal scrollbar, every grid stacks, headings shrink.
|
|
80
|
+
- [ ] Mobile: no horizontal scrollbar, every grid stacks, headings shrink, nav collapses into a sheet or wraps.
|
|
61
81
|
- [ ] Tablet: two-column layouts where three would cramp.
|
|
62
82
|
- [ ] Desktop: content is capped by a `max-w-*`, not stretched edge to edge.
|
|
63
|
-
- [ ] Touch targets ≥ 40px tall on mobile (`py-2.5` on
|
|
83
|
+
- [ ] Touch targets ≥ 40px tall on mobile (`<Button size="lg">` or `py-2.5` on links).
|
|
64
84
|
|
|
65
85
|
## Anti-patterns
|
|
66
86
|
|
|
@@ -69,3 +89,4 @@ layer larger layouts on top.
|
|
|
69
89
|
- ❌ `h-screen` on scrolling pages; `overflow-hidden` on `body`/root.
|
|
70
90
|
- ❌ Grids with no mobile fallback (`grid-cols-4` alone).
|
|
71
91
|
- ❌ Text sized only for desktop (`text-7xl` with no smaller base).
|
|
92
|
+
- ❌ A hand-built sidebar or drawer when `@/ui/sidebar`, `<Sheet>`, or `<Drawer>` fits.
|
|
@@ -7,54 +7,72 @@
|
|
|
7
7
|
| Hero heading | `text-5xl sm:text-7xl font-bold leading-[1.02] tracking-tight` | one per page |
|
|
8
8
|
| Page / section heading | `text-3xl sm:text-4xl font-bold tracking-tight` | |
|
|
9
9
|
| Subsection | `text-xl font-semibold` | |
|
|
10
|
-
| Lede | `text-lg sm:text-xl text
|
|
10
|
+
| Lede | `text-lg sm:text-xl text-muted-foreground` | under the hero heading |
|
|
11
11
|
| Body | `text-base leading-relaxed` (16px) | never smaller for running copy |
|
|
12
|
-
| Secondary | `text-sm text
|
|
13
|
-
| Eyebrow / label | `text-xs font-semibold uppercase tracking-[0.2em]` | above headings |
|
|
14
|
-
| Code | `font-mono text-[0.9em]
|
|
12
|
+
| Secondary | `text-sm text-muted-foreground` | captions, metadata, table cells |
|
|
13
|
+
| Eyebrow / label | `text-xs font-semibold uppercase tracking-[0.2em]` or `<Badge variant="secondary">` | above headings |
|
|
14
|
+
| Code | `font-mono text-[0.9em] bg-muted rounded px-1` | |
|
|
15
15
|
|
|
16
16
|
- Line-height: tight (`leading-[1.05]`–`leading-tight`) for display sizes,
|
|
17
17
|
`leading-relaxed` (1.625) for body.
|
|
18
18
|
- Measure: cap prose at `max-w-[65ch]` / `max-w-2xl`.
|
|
19
19
|
- Numbers in tables and prices: `tabular-nums`.
|
|
20
|
-
- Fonts: the
|
|
21
|
-
brand font only when the user names one
|
|
20
|
+
- Fonts: `--font-sans` from the tokens is the default (system stack unless a
|
|
21
|
+
theme sets it). Load a display or brand font only when the user names one
|
|
22
|
+
or a theme requires it — see `assets-and-fonts.md`.
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## The token system
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
`styles/globals.css` defines the shadcn tokens in OKLCH for `:root` (light)
|
|
27
|
+
and `.dark`, and exposes them as utilities through `@theme inline`. Pages
|
|
28
|
+
and `ui/` components read the utilities; themes rewrite the values.
|
|
26
29
|
|
|
27
|
-
|
|
|
30
|
+
| Token pair | Utilities | Use for |
|
|
28
31
|
| --- | --- | --- |
|
|
29
|
-
| background
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
| `--background` / `--foreground` | `bg-background text-foreground` | page root |
|
|
33
|
+
| `--card` / `--card-foreground` | `bg-card text-card-foreground` | panels, `<Card>` |
|
|
34
|
+
| `--popover` / `--popover-foreground` | `bg-popover` | menus, popovers (used by `ui/`) |
|
|
35
|
+
| `--primary` / `--primary-foreground` | `bg-primary text-primary-foreground` | the one accent: primary CTA, active state |
|
|
36
|
+
| `--secondary` / `--secondary-foreground` | `bg-secondary` | secondary buttons, badges |
|
|
37
|
+
| `--muted` / `--muted-foreground` | `bg-muted text-muted-foreground` | subdued surfaces, secondary copy |
|
|
38
|
+
| `--accent` / `--accent-foreground` | `bg-accent` | hover surfaces, selected rows |
|
|
39
|
+
| `--destructive` | `text-destructive`, `<Button variant="destructive">` | delete, errors |
|
|
40
|
+
| `--border`, `--input`, `--ring` | `border-border`, `border-input`, `ring-ring` | dividers, field borders, focus |
|
|
41
|
+
| `--chart-1`…`--chart-5` | `text-chart-1`, `fill-chart-1` | `<Chart>` series |
|
|
42
|
+
| `--sidebar*` | `bg-sidebar` … | `<Sidebar>` shell |
|
|
43
|
+
| `--radius` | `rounded-sm`…`rounded-2xl` | all radii scale from one value |
|
|
44
|
+
|
|
45
|
+
- Always use a pair together: a `bg-primary` without `text-primary-foreground`
|
|
46
|
+
breaks the moment a theme changes the primary hue.
|
|
47
|
+
- Opacity modifiers work on tokens: `bg-primary/10` for a tinted band,
|
|
48
|
+
`border-border/60` for a softer rule.
|
|
49
|
+
- Dark pages: put `dark` on the root element's `className`. Every token flips;
|
|
50
|
+
no manual `dark:` prefixes needed unless a single element must differ.
|
|
51
|
+
- Dark *sections* on a light page: wrap the section in `<div className="dark bg-background text-foreground">`.
|
|
52
|
+
- Raw palette classes (`bg-emerald-400`, `text-slate-600`) are for deliberate
|
|
53
|
+
brand moments a theme should not repaint — a neon launch button, a
|
|
54
|
+
gradient hero. Keep them rare and keep them out of `ui/` components.
|
|
42
55
|
|
|
43
56
|
## Contrast
|
|
44
57
|
|
|
45
|
-
- Body text ≥ 4.5:1 against its background; large headings ≥ 3:1.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
`
|
|
50
|
-
-
|
|
51
|
-
|
|
58
|
+
- Body text ≥ 4.5:1 against its background; large headings ≥ 3:1. The token
|
|
59
|
+
pairs pass by construction; `text-muted-foreground` is the floor for
|
|
60
|
+
secondary copy.
|
|
61
|
+
- Do not dim below the floor with opacity: `text-foreground/40` and
|
|
62
|
+
`text-white/30` fail. Decorative metadata at most `text-muted-foreground/80`.
|
|
63
|
+
- Accent text on light backgrounds must be the darker shade a theme sets for
|
|
64
|
+
`--primary`; on dark, the lighter one. Never `bg-primary` text on
|
|
65
|
+
`bg-background` without checking.
|
|
66
|
+
- Links inside prose: `underline underline-offset-4`, not color alone.
|
|
52
67
|
|
|
53
68
|
## Anti-patterns
|
|
54
69
|
|
|
55
70
|
- ❌ Body text under 16px, or `text-xs` for anything the user must read.
|
|
56
71
|
- ❌ Three font families. Two is the ceiling (display + body).
|
|
57
|
-
- ❌
|
|
58
|
-
|
|
72
|
+
- ❌ Hardcoding a palette per page (`bg-white text-slate-900` everywhere)
|
|
73
|
+
when `bg-background text-foreground` exists — the page silently opts out of
|
|
74
|
+
every theme.
|
|
75
|
+
- ❌ `dark:bg-…` sprinkled on every element instead of one `dark` on the root.
|
|
59
76
|
- ❌ Gradient text, drop shadows, and glows on every heading. One flourish, once.
|
|
60
|
-
- ❌ Palette drift: a new grey shade every section.
|
|
77
|
+
- ❌ Palette drift: a new grey shade every section. There are `muted`,
|
|
78
|
+
`card`, and `background`; that is the whole grey scale.
|