@autono/create-open-pages 0.7.0 → 0.9.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/dist/cli.js +2 -2
- package/package.json +1 -1
- package/template/.agents/skills/apply-comments/SKILL.md +6 -3
- package/template/.agents/skills/create-email/SKILL.md +166 -0
- package/template/.agents/skills/create-page/SKILL.md +5 -1
- package/template/.agents/skills/create-theme/SKILL.md +33 -7
- package/template/.agents/skills/create-theme/references/email-theme-from-css.mjs +185 -0
- package/template/.agents/skills/create-theme/references/email-theme.md +48 -0
- package/template/.agents/skills/current-page/SKILL.md +3 -3
- package/template/.agents/skills/page-authoring/SKILL.md +3 -0
- package/template/.agents/skills/web-design-guidelines/SKILL.md +41 -0
- package/template/AGENTS.md +8 -5
- package/template/README.md +7 -1
- package/template/components.json +3 -1
- package/template/emails/welcome/index.tsx +61 -0
- package/template/package.json +1 -0
- package/template/styles/globals.css +8 -2
- package/template/tsconfig.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -100,7 +100,7 @@ async function isDirNonEmpty(target) {
|
|
|
100
100
|
return (await readdir(target)).some((e) => !e.startsWith("."));
|
|
101
101
|
}
|
|
102
102
|
function coreVersionRange() {
|
|
103
|
-
return `^0.
|
|
103
|
+
return `^0.9.0`;
|
|
104
104
|
}
|
|
105
105
|
async function linkOrCopy(relSrc, dst) {
|
|
106
106
|
await rm(dst, {
|
|
@@ -341,7 +341,7 @@ async function runInit(dirArg, flags) {
|
|
|
341
341
|
if (!installed) next.push(`${pm} install`);
|
|
342
342
|
next.push(pm === "npm" ? "npm run dev" : `${pm} dev`);
|
|
343
343
|
p.note(next.map((line) => chalk.cyan(line)).join("\n"), "Next steps");
|
|
344
|
-
p.outro(`All set! ${chalk.dim("
|
|
344
|
+
p.outro(`All set! ${chalk.dim("Docs: https://docs.openpages.sh")}`);
|
|
345
345
|
}
|
|
346
346
|
async function run(argv) {
|
|
347
347
|
const version = await readVersion();
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: apply-comments
|
|
3
|
-
description: Apply pending @page-comment markers written by the open-pages inspector tool. Use when the user asks to "apply comments", "process page comments", "apply the inspector comments", or references markers left inside `pages/<id>/index.tsx`
|
|
3
|
+
description: Apply pending @page-comment markers written by the open-pages inspector tool. Use when the user asks to "apply comments", "process page comments", "apply the inspector comments", or references markers left inside `pages/<id>/index.tsx`, `emails/<id>/index.tsx`, or their `components/*.tsx`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Apply page comments
|
|
7
7
|
|
|
8
|
-
The open-pages viewer has an inspector that lets the user click any element on the live page and attach a textual comment (e.g. *"make this red"*, *"change to 'Open Pages Rocks'"*). Each comment is persisted as an in-source JSX marker inside the
|
|
8
|
+
The open-pages viewer has an inspector that lets the user click any element on the live page (or rendered email) and attach a textual comment (e.g. *"make this red"*, *"change to 'Open Pages Rocks'"*). Each comment is persisted as an in-source JSX marker inside the source — usually `pages/<pageId>/index.tsx`, occasionally a file under `pages/<pageId>/components/`; for emails, `emails/<id>/index.tsx` and `emails/<id>/components/`.
|
|
9
9
|
|
|
10
10
|
Your job: read those markers, perform the described edits, and delete the markers.
|
|
11
11
|
|
|
12
12
|
> **Before making any page edit**, consult the **`page-authoring`** skill (and the **`shadcn`** skill for component props and variants) — it is the technical reference for how a page is structured (file contract, `className` styling, layout and responsive rules, type scale, interactivity). A comment like *"make this bigger"* or *"change the accent colour"* should be applied in a way that stays consistent with those rules and still works on mobile.
|
|
13
|
+
>
|
|
14
|
+
> **For a marker under `emails/`**, consult the **`create-email`** skill instead: emails are react-email components with email-safe Tailwind only (no flex/grid, no `ui/` components, no hooks), and the edit must keep both the HTML and the derived plain text sensible.
|
|
13
15
|
|
|
14
16
|
## Marker format
|
|
15
17
|
|
|
@@ -29,7 +31,7 @@ Your job: read those markers, perform the described edits, and delete the marker
|
|
|
29
31
|
|
|
30
32
|
1. **Identify the target page(s).**
|
|
31
33
|
- If the user names one (`launch`, `pricing`, etc.), work on that single page's source files.
|
|
32
|
-
- If they say "all" or don't specify, scan every `pages/*/index.tsx` and `
|
|
34
|
+
- If they say "all" or don't specify, scan every `pages/*/index.tsx`, `pages/*/components/*.tsx`, `emails/*/index.tsx`, and `emails/*/components/*.tsx`. Process each page or email one at a time.
|
|
33
35
|
|
|
34
36
|
2. **Read the file and find all markers.**
|
|
35
37
|
- Run the regex above against the whole file.
|
|
@@ -56,6 +58,7 @@ Your job: read those markers, perform the described edits, and delete the marker
|
|
|
56
58
|
- After all edits, re-read the file and confirm the only remaining markers are ones you reported as skipped.
|
|
57
59
|
- Confirm the edited JSX is well-formed (balanced tags, no dangling attributes) and that changed `className` strings are literal Tailwind utilities. If the project's `package.json` has typecheck/lint scripts, run them with the project's package manager; scaffolded projects ship neither TypeScript nor a linter — there, rely on the running dev server (or the `build` script) to surface compile errors. Fix any errors you introduced.
|
|
58
60
|
- For layout changes, mentally check the Mobile viewport (390px): did the edit introduce a fixed width or a grid with no stacking fallback?
|
|
61
|
+
- When a comment changed interactive elements, forms, motion, or layout, run the `web-design-guidelines` skill on the page and fix any regression it reports before you report. (Not for emails: check the email's HTML and Text views in the viewer instead.)
|
|
59
62
|
|
|
60
63
|
7. **Report.**
|
|
61
64
|
- Summarise: `N applied, M skipped` plus a one-line description of each change (including the page id).
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-email
|
|
3
|
+
description: Use this skill when the user wants to create, build, draft, or edit an email template in this open-pages repo — a welcome email, receipt, password reset, newsletter, notification, invite, or any HTML email. Triggers on phrases like "make an email for X", "email template", "transactional email", "newsletter", "onboarding email", or when the user asks to add or change content under `emails/`. Do NOT use for web pages (that is `create-page`) or for the framework itself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create an email in open-pages
|
|
7
|
+
|
|
8
|
+
An email is one folder under `emails/<id>/` with an `index.tsx` that default-exports a [react-email](https://react.email) component. The workspace renders it on the server to a single HTML document with inlined styles plus a plain-text alternative, previews both live, and exports them with `open-pages export`. Nothing in an email runs in a browser: no hooks with effects, no state, no event handlers, no browser APIs.
|
|
9
|
+
|
|
10
|
+
This skill owns both the **workflow** and the **technical reference** for emails. The `page-authoring` skill does not apply here: emails are not web pages, and the shadcn `ui/` set cannot be used in them.
|
|
11
|
+
|
|
12
|
+
You only write files under `emails/<id>/`, plus whatever `npx shadcn@latest add @emailcn/...` installs under `components/email/`. Never modify `package.json`, `open-pages.config.ts`, `components.json`, or other emails.
|
|
13
|
+
|
|
14
|
+
## Step 1 — Clarify requirements (MUST ask before writing code)
|
|
15
|
+
|
|
16
|
+
Lock in the decisions below with `AskUserQuestion` before writing. Skip a question only when the user's message already answers it unambiguously, and restate your assumption when you skip.
|
|
17
|
+
|
|
18
|
+
1. **Email type** — offer the closest fits: transactional (welcome / onboarding, receipt, password reset, magic link, OTP, invite, notification) or marketing (newsletter, announcement, promotion). Mark the best fit "(Recommended)". Transactional emails are short, one action, no images required; marketing emails carry sections and imagery.
|
|
19
|
+
|
|
20
|
+
2. **Starting point** — offer: an emailcn block (Recommended when one matches: `block-onboarding-*`, `block-receipt-*`, `block-auth-*`, `block-invite-*`, `block-newsletter-*`, `block-notification-*`), emailcn components composed by you (headers, heroes, CTAs, footers, stats, pricing tables), or react-email primitives from scratch. The block route is fastest and already email-client-safe.
|
|
21
|
+
|
|
22
|
+
3. **Look** — list every `components/email/theme-<id>.ts` that pairs with a `themes/<id>.md` first (Recommended when one exists: the email will match the workspace's pages), then the emailcn themes that fit the brand: `theme-default` (neutral), `theme-linear`, `theme-vercel`, `theme-stripe`, `theme-notion`, `theme-slack`, `theme-github`, `theme-raycast`, `theme-apple`, `theme-airbnb`, `theme-dropbox`, `theme-nike`, `theme-twitch`, `theme-stack-overflow`. Step 3b covers applying a workspace theme.
|
|
23
|
+
|
|
24
|
+
4. **Content** — the subject line, the preheader (the preview text mail clients show next to the subject), the sender name or product name, the one thing the reader should do (CTA label + URL), and any real copy, prices, or names. Real emails live on real content; ask rather than invent.
|
|
25
|
+
|
|
26
|
+
Ask follow-ups only if still unclear: logo URL, brand color, footer address and unsubscribe URL (marketing emails legally need both).
|
|
27
|
+
|
|
28
|
+
## Step 2 — Pick an email id
|
|
29
|
+
|
|
30
|
+
Kebab-case, short, descriptive: `welcome`, `receipt`, `password-reset`, `weekly-digest`, `invite-teammate`. Check `emails/` to avoid collisions. Page ids and email ids are separate namespaces, so `welcome` can be both a page and an email.
|
|
31
|
+
|
|
32
|
+
## Step 3 — Install what the email needs
|
|
33
|
+
|
|
34
|
+
The workspace ships `react-email` (components, `Tailwind`, `render`) and a `components.json` with the `@emailcn` registry registered. Everything else is installed per item with the shadcn CLI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# a full email as a block (installs its theme, fonts, sections)
|
|
38
|
+
npx shadcn@latest add @emailcn/react-email/block-onboarding-default
|
|
39
|
+
|
|
40
|
+
# individual sections and a theme
|
|
41
|
+
npx shadcn@latest add @emailcn/react-email/theme-linear @emailcn/react-email/split-hero @emailcn/react-email/navigation-footer
|
|
42
|
+
|
|
43
|
+
# see everything available
|
|
44
|
+
curl -s https://emailcn.run/r/registry.json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).items.filter(i=>i.name.startsWith("react-email/")).map(i=>i.name).join("\n")))'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Files land under `components/email/` (`email-theme.ts`, `theme-<id>.ts`, `email-assets.ts`, one file per section or block). They are the email equivalent of `ui/`: shared by every email, read but not edited per email. Wrap or extend a section inside `emails/<id>/components/` when one email needs a different look. Always use the `react-email/` variants of registry items; the `mjml-react/` and `jsx-email/` variants need packages this workspace does not install.
|
|
48
|
+
|
|
49
|
+
### Step 3b — Using a workspace theme
|
|
50
|
+
|
|
51
|
+
Every workspace theme authored by `create-theme` ships `components/email/theme-<id>.ts`, the same palette as an `EmailTheme` object (mail clients cannot read `themes/<id>.css`). To put an email on that theme:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { createEmailTailwindConfig } from '@/components/email/email-theme';
|
|
55
|
+
import { autonoTheme } from '@/components/email/theme-autono';
|
|
56
|
+
|
|
57
|
+
<Tailwind config={createEmailTailwindConfig(autonoTheme)}>…</Tailwind>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
emailcn **sections** (`split-hero`, `call-to-action`, `navigation-footer`, `button`, …) take a `theme` prop and default to `defaultTheme`, so pass the workspace theme to each one you compose. emailcn **blocks** (`block-*`) pin `defaultTheme` (or their named theme) inside the file; to put a block on a workspace theme, copy it into `emails/<id>/components/` and swap the theme import there rather than editing the shared copy.
|
|
61
|
+
|
|
62
|
+
If `themes/<id>.css` exists but `components/email/theme-<id>.ts` does not, generate it the way `create-theme` does instead of converting colors by hand:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
node .agents/skills/create-theme/references/email-theme-from-css.mjs themes/<id>.css <id> > components/email/theme-<id>.ts
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
(`components/email/email-theme.ts` must exist first: `npx shadcn@latest add @emailcn/react-email/theme-default` installs it.)
|
|
69
|
+
|
|
70
|
+
## Step 4 — Write `emails/<id>/index.tsx`
|
|
71
|
+
|
|
72
|
+
### File contract
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import type { EmailMeta } from '@autono/open-pages';
|
|
76
|
+
import { Body, Button, Container, Head, Heading, Html, Preview, Section, Tailwind, Text } from 'react-email';
|
|
77
|
+
import { createEmailTailwindConfig } from '@/components/email/email-theme';
|
|
78
|
+
import { defaultTheme } from '@/components/email/theme-default';
|
|
79
|
+
|
|
80
|
+
export const meta: EmailMeta = {
|
|
81
|
+
title: 'Welcome',
|
|
82
|
+
subject: 'Welcome to Acme',
|
|
83
|
+
description: 'Sent right after signup.',
|
|
84
|
+
createdAt: '2026-09-27T20:00:00.000Z',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default function Welcome() {
|
|
88
|
+
return (
|
|
89
|
+
<Html lang="en">
|
|
90
|
+
<Head />
|
|
91
|
+
<Preview>Your workspace is ready. Here is how to get started.</Preview>
|
|
92
|
+
<Tailwind config={createEmailTailwindConfig(defaultTheme)}>
|
|
93
|
+
<Body className="bg-bg font-sans">
|
|
94
|
+
<Container className="mx-auto max-w-email p-8">
|
|
95
|
+
<Heading className="m-0 font-24 text-fg">Welcome to Acme</Heading>
|
|
96
|
+
<Text className="font-16 text-fg-2">Your workspace is ready.</Text>
|
|
97
|
+
<Section className="my-6">
|
|
98
|
+
<Button href="https://acme.example/app" className="rounded bg-brand px-5 py-3 font-14 font-medium text-brand-fg">
|
|
99
|
+
Open Acme
|
|
100
|
+
</Button>
|
|
101
|
+
</Section>
|
|
102
|
+
</Container>
|
|
103
|
+
</Body>
|
|
104
|
+
</Tailwind>
|
|
105
|
+
</Html>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **`meta`** — `title` is the workspace card name; `subject` is the subject line the email ships with; `description` is what it is for; `createdAt` is an ISO literal set once (the framework reads these with a regex, so keep them plain string literals).
|
|
111
|
+
- **The default export** takes no props. When composing an emailcn block that takes props (`_firstName`, `ctaHref`, …), pass sample values in the email; the sending system will substitute real ones later. Keep sample data plausible, not `Lorem ipsum`.
|
|
112
|
+
- **Structure** is always `Html > Head + Preview + Tailwind > Body > Container`. `Preview` is the preheader; keep it under 90 characters and make it continue the subject line, not repeat it.
|
|
113
|
+
- **Helper components** go under `emails/<id>/components/`. Split sections out when `index.tsx` passes ~120 lines.
|
|
114
|
+
- **Images** must be absolute `https://` URLs with `width`, `height`, and `alt`. Email clients do not load relative paths or local files. emailcn's `emailAsset()` helper points at hosted sample imagery; replace it with the user's hosted assets before shipping.
|
|
115
|
+
|
|
116
|
+
### What react-email gives you
|
|
117
|
+
|
|
118
|
+
`Html`, `Head`, `Preview`, `Body`, `Container` (centered column, set `max-w-container` or `max-w-[600px]`), `Section` (a table row band), `Row` + `Column` (side-by-side cells), `Heading`, `Text`, `Link`, `Button` (a padded anchor that survives Outlook), `Img`, `Hr`, `Font` (web font with fallback), `Markdown`, `CodeBlock` / `CodeInline`. Every one of them renders to nested tables and inline styles; you never write `<table>` yourself.
|
|
119
|
+
|
|
120
|
+
`Tailwind` compiles the utilities in `className` to inline styles at render time. Only utilities it can read literally are compiled (no runtime string building), and only properties email clients support survive: spacing, colors, typography, borders, widths, alignment. `flex`, `grid`, `gap`, `position`, `transform`, `transition`, `hover:` and `dark:` variants, and CSS variables are not email-safe; use `Row` / `Column` for layout and `Section` padding for spacing. A utility the compiler cannot resolve is left behind as a `class` attribute and does nothing in mail clients; the viewer shows an amber "N classes did not compile" chip (hover it for the list) and the dev server logs the same.
|
|
121
|
+
|
|
122
|
+
The emailcn theme config (`components/email/email-theme.ts`) adds semantic names on top of Tailwind: colors `bg`, `bg-2`, `bg-3`, `fg`, `fg-2`, `fg-3`, `brand`, `brand-fg`, `brand-hover`, `stroke`, `danger`, `success`, `warning` (so `bg-bg`, `text-fg-2`, `bg-brand text-brand-fg`, `border-stroke`), `max-w-email` for the 600px column, `rounded` / `rounded-lg` from the theme radii, `font-11` … `font-28` type steps, and a `mobile:` variant. Read that file for the current list before inventing names. Prefer these over raw hex so swapping the theme file restyles the email.
|
|
123
|
+
|
|
124
|
+
**Registry quirk, handled for you.** emailcn's react-email blocks and sections use `bg-background`, `text-foreground`, `text-foreground-muted`, `bg-primary`, `text-primary-fg`, `border-border`, and `max-w-container`, which the `email-theme.ts` the same registry installs does not define. open-pages patches that module at load time so those names resolve to the matching theme fields (`colorBackground`, `colorText`, `colorPrimary`, `containerWidth`, …) in dev, export, and build. Both vocabularies work in your own emails; do not edit `components/email/email-theme.ts` to add them by hand.
|
|
125
|
+
|
|
126
|
+
### Email constraints that differ from pages
|
|
127
|
+
|
|
128
|
+
- **One column, 600px.** Multi-column layouts stack on phones only when built with `Row` / `Column`; keep them to two columns and put the important one first.
|
|
129
|
+
- **Fonts**: a web font via `<Font>` with a system fallback, or a plain stack (`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`). Gmail ignores web fonts; the fallback must look right on its own.
|
|
130
|
+
- **Type**: 16px body (14px minimum), 24–32px headings, line-height 1.5. Dark text on light backgrounds; many clients force-invert dark designs unpredictably.
|
|
131
|
+
- **Buttons** are `<Button href>` with padding classes, never `<button>`; there is no JavaScript.
|
|
132
|
+
- **Footer** of a marketing email: physical address and an unsubscribe link. Transactional emails: a one-line reason ("You are receiving this because you created an account.").
|
|
133
|
+
- **Plain text** is derived automatically from the HTML. Make sure the HTML reads top to bottom as prose: the CTA text plus its URL, no meaning carried only by images.
|
|
134
|
+
- **No `window`, `document`, `useState`, `useEffect`, `fetch`, event handlers** anywhere. The module is evaluated in Node.
|
|
135
|
+
|
|
136
|
+
## Step 5 — Preview and check
|
|
137
|
+
|
|
138
|
+
The dev server renders the email at `http://localhost:5173/e/<id>` with **HTML** and **Text** views, a **Mobile** (375px) toggle, **Copy HTML**, and **Open** (the raw document by itself). The frame re-renders on every save of the email or of anything it imports. An error banner in the frame means the module threw on the server; the dev server output has the stack.
|
|
139
|
+
|
|
140
|
+
Check both views. In Text, every link should read as `label URL`, and nothing important should be missing. If the header shows an amber "classes did not compile" chip, resolve every name it lists before handing off; a shipped email must have zero.
|
|
141
|
+
|
|
142
|
+
## Step 6 — Self-review
|
|
143
|
+
|
|
144
|
+
- [ ] `emails/<id>/index.tsx` default-exports one zero-prop component; `meta` has `title`, `subject`, and a fresh `createdAt` literal.
|
|
145
|
+
- [ ] `Preview` is set, under 90 characters, and does not repeat the subject.
|
|
146
|
+
- [ ] Only `react-email` components and files under `components/email/` are imported. Nothing from `@/ui`, `@/lib`, `@/hooks`, `lucide-react`, or any browser-only package.
|
|
147
|
+
- [ ] No hooks, state, effects, handlers, or browser globals.
|
|
148
|
+
- [ ] Every `className` is a literal Tailwind utility the email compiler supports; layout uses `Section` / `Row` / `Column`, not flex or grid. The viewer reports zero uncompiled classes.
|
|
149
|
+
- [ ] Every `Img` has an absolute `https://` `src`, `width`, `height`, and `alt`.
|
|
150
|
+
- [ ] Every CTA is a `<Button href>` or `<Link href>` with an absolute URL, and the primary CTA appears once.
|
|
151
|
+
- [ ] Body text is 14px or larger; the layout holds at 375px in the Mobile toggle.
|
|
152
|
+
- [ ] Marketing emails carry a physical address and an unsubscribe link; transactional emails carry a one-line reason for receipt.
|
|
153
|
+
- [ ] The Text view reads as complete prose with every link's URL present.
|
|
154
|
+
- [ ] Nothing under `components/email/` was edited for this one email; nothing outside `emails/<id>/` changed except emailcn installs.
|
|
155
|
+
|
|
156
|
+
## Step 7 — Hand off to the user
|
|
157
|
+
|
|
158
|
+
Tell the user:
|
|
159
|
+
|
|
160
|
+
- The email id, its file path, and the `subject` it carries.
|
|
161
|
+
- The preview URL — `http://localhost:5173/e/<id>` — with HTML / Text views, a Mobile toggle, Copy HTML, and Inspect (press `i`, click any element, leave a note, then ask you to run `apply-comments`).
|
|
162
|
+
- That `open-pages export <id>` writes `export/emails/<id>/index.html` and `index.txt`, ready to paste into Resend, SendGrid, Postmark, Mailchimp, Customer.io, or any sender that takes raw HTML; `Copy HTML` in the viewer gives the same document.
|
|
163
|
+
- That the email's look comes from `components/email/theme-*.ts`; swapping the theme import restyles it without touching its structure.
|
|
164
|
+
- If dev isn't running: run the project's `dev` script from the project root with its package manager (`npm run dev`, `pnpm dev`, … — match the lockfile).
|
|
165
|
+
|
|
166
|
+
Don't run the dev server yourself unless asked.
|
|
@@ -94,7 +94,11 @@ Read the **`page-authoring`** skill before writing — file contract, `@/ui/*` c
|
|
|
94
94
|
|
|
95
95
|
## Step 7 — Self-review
|
|
96
96
|
|
|
97
|
-
Run the checklist in `page-authoring` ("Self-review before finishing"): every button/input/card/dialog/tab is a `@/ui` component, colors are tokens, nothing under `ui/` changed. Check all three viewports.
|
|
97
|
+
Run the checklist in `page-authoring` ("Self-review before finishing"): every button/input/card/dialog/tab is a `@/ui` component, colors are tokens, nothing under `ui/` changed. Check all three viewports. Pay particular attention to the CTA and motion items: one label per intent, no wrapped button text at desktop, every animation answers "what does this communicate?".
|
|
98
|
+
|
|
99
|
+
## Step 7b — Guidelines review
|
|
100
|
+
|
|
101
|
+
Run the `web-design-guidelines` skill on `pages/<id>/` (it fetches Vercel's current Web Interface Guidelines and reports `file:line` findings). Fix what it finds in the page files, re-run until clean or until only findings you can justify remain, and mention any you left in the hand-off.
|
|
98
102
|
|
|
99
103
|
## Step 8 — Hand off to the user
|
|
100
104
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: create-theme
|
|
3
|
-
description: Use this skill when the user wants to create, draft, author, or extract a theme in this open-pages repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <page>", "match our brand", "build a design system from these screenshots", "apply this shadcn preset". Produces a
|
|
3
|
+
description: Use this skill when the user wants to create, draft, author, or extract a theme in this open-pages repo. Triggers on phrases like "create a theme", "make a theme called X", "extract a theme from <page>", "match our brand", "build a design system from these screenshots", "apply this shadcn preset", "email theme for X". Produces a bundle under `themes/` — `<id>.md` (direction, fonts, component notes, token table), `<id>.css` (the shadcn token overrides), and `<id>.demo.tsx` (a demo page composed from `ui/` components that the workspace's Themes panel previews live) — plus `components/email/theme-<id>.ts`, the same palette as an emailcn EmailTheme so emails match. Do NOT use for editing real pages — only for authoring the theme bundle.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Create a theme
|
|
7
7
|
|
|
8
8
|
A theme is a **token set**. Every workspace ships the full shadcn/ui set under `ui/`, and every component reads its colors, radius, and fonts from the CSS variables in `styles/globals.css` (`--background`, `--primary`, `--radius`, …). A theme overrides those variables; every component and every token-styled page restyles at once. Nothing is copied into pages.
|
|
9
9
|
|
|
10
|
-
The bundle is three files sharing one stem under `themes/`:
|
|
10
|
+
The bundle is three files sharing one stem under `themes/`, plus one under `components/email/`:
|
|
11
11
|
|
|
12
12
|
1. `themes/<id>.md` — agent-facing direction: aesthetic, fonts, how to use the components in this theme, and a table of token → value. This is what `create-page` reads when an author picks the theme.
|
|
13
13
|
2. `themes/<id>.css` — **only** `:root { … }` and `.dark { … }` blocks overriding the shadcn tokens (plus an optional Google Fonts `@import url(…)` at the very top). No `@theme` block, no Tailwind import, no selectors beyond those two.
|
|
14
14
|
3. `themes/<id>.demo.tsx` — a runnable page module (same shape as `pages/<id>/index.tsx`, **one default-exported component**) composed from `@/ui/*` components so the tokens are shown on real parts. The Themes panel renders it with `<id>.css` injected automatically; the demo does not set `meta.theme`.
|
|
15
|
+
4. `components/email/theme-<id>.ts` — the same palette as an emailcn `EmailTheme` object (hex colors, px sizes, font stacks), generated from the CSS by a script (Step 6b). Mail clients cannot read CSS variables or `oklch()`, so this is how emails under `emails/` pick the theme up; the `create-email` skill consumes it.
|
|
15
16
|
|
|
16
|
-
A page opts in with `meta.theme: '<id>'`; the runtime injects `themes/<id>.css` into that page's preview frame and into its export.
|
|
17
|
+
A page opts in with `meta.theme: '<id>'`; the runtime injects `themes/<id>.css` into that page's preview frame and into its export. An email opts in by passing the exported theme object to `createEmailTailwindConfig` (or to a section's `theme` prop).
|
|
17
18
|
|
|
18
|
-
You only write
|
|
19
|
+
You only write these four files. Never modify pages, emails, `ui/`, `styles/globals.css`, or configuration. The token names and how pages consume them live in the **`page-authoring`** skill (`references/typography-and-color.md`) — read it first. Component props and variants: the **`shadcn`** skill.
|
|
19
20
|
|
|
20
21
|
## Step 1 — Identify the input source
|
|
21
22
|
|
|
@@ -25,6 +26,7 @@ A theme can be derived from any combination of:
|
|
|
25
26
|
- **Image references / brand guidelines** — paths or URLs to screenshots, mood boards, logo files, a brand PDF. You will translate them into OKLCH tokens.
|
|
26
27
|
- **Free-text description** — prose describing the desired palette, weight, feel.
|
|
27
28
|
- **An existing page** — `pages/<id>/index.tsx` whose look should become reusable.
|
|
29
|
+
- **A `DESIGN.md`** — a design-system document in the Google-spec shape (YAML frontmatter, then Overview, Colors, Typography, Layout, Elevation, Shapes, Components, Do's and Don'ts). [designmd.supply](https://designmd.supply) generates one from any public domain, so "match our brand" is: run the site through it, drop the file at `themes/<id>.design.md` (or paste the path), and ask for a theme.
|
|
28
30
|
|
|
29
31
|
If the user's original message already specifies the inputs unambiguously, skip the question and proceed. Otherwise call `AskUserQuestion` (multi-select) so they can pick one or more sources, and ask follow-ups (paths, preset code, page id, prose) only as needed.
|
|
30
32
|
|
|
@@ -34,6 +36,7 @@ If the user's original message already specifies the inputs unambiguously, skip
|
|
|
34
36
|
- **Images**: read each path with the `Read` tool (it accepts images). Note dominant colors (write them as hex, then convert to OKLCH), type family feel, corner radius, surface treatment (flat vs. cards vs. borders), light or dark default, and chrome (nav style, footer).
|
|
35
37
|
- **Text**: extract explicit values (hex codes, font names, "rounded", "sharp", "dense") and resolve vague language into concrete decisions before writing.
|
|
36
38
|
- **Existing page**: read `pages/<id>/index.tsx` (and `components/`) and pull any raw palette classes or hex values into token roles (the page's `bg-[#0b0b10]` root → `--background`; its CTA fill → `--primary`; its card border → `--border`), plus fonts and radius.
|
|
39
|
+
- **`DESIGN.md`**: read it and map sections onto tokens — **Colors** gives `--background`/`--foreground`, `--primary`, `--secondary`, `--accent`, `--muted`, `--destructive` (primary = the brand's main action color, not its logo color, when they differ); **Typography** gives `--font-sans` / `--font-heading` and the type-scale feel; **Shapes** gives `--radius`; **Elevation** decides flat vs. card-with-shadow treatment in the component notes; **Layout** informs container widths and density in the demo; **Components** and **Do's and Don'ts** become the "Components in this theme" guidance. Colors may arrive as hex, `rgb()`, `hsl()`, `oklch()`, or CSS named colors — parse each by its own syntax, then convert to OKLCH. A single light palette is normal; derive the `.dark` block from it (invert lightness, keep hue and chroma) and say so in the `.md`.
|
|
37
40
|
|
|
38
41
|
Every color ends up as `oklch(L C H)` — the same format as `styles/globals.css`. When inputs disagree (images use blue but the description says green), ask the user which to honor.
|
|
39
42
|
|
|
@@ -148,6 +151,11 @@ Notes on how to use the `ui/` set so pages feel like this theme — which varian
|
|
|
148
151
|
- Feedback: `<Alert>`, `sonner` toasts.
|
|
149
152
|
- Avoid: gradients | shadows | more than one accent | … (whatever the theme forbids).
|
|
150
153
|
|
|
154
|
+
## Email
|
|
155
|
+
|
|
156
|
+
- `components/email/theme-<id>.ts` exports `<id>Theme`, generated from this CSS. Emails pass it to `createEmailTailwindConfig` or to a section's `theme` prop.
|
|
157
|
+
- Any email-specific notes: which emailcn sections suit the brand, whether headings keep the serif via `<Font>`, what the footer must carry.
|
|
158
|
+
|
|
151
159
|
## Aesthetic
|
|
152
160
|
|
|
153
161
|
One paragraph. What it feels like, the references it draws on, what to avoid. Commit to a single direction.
|
|
@@ -177,6 +185,21 @@ Contract:
|
|
|
177
185
|
- Root element: `min-h-screen bg-background text-foreground` (with `dark` if the theme is dark by default). Everything token-styled — the demo is the proof that pages need no raw colors.
|
|
178
186
|
- Realistic content, not lorem ipsum. Must look right at Mobile (390px) as well as Desktop. Self-contained: no page-local assets.
|
|
179
187
|
|
|
188
|
+
## Step 6b — Generate `components/email/theme-<id>.ts`
|
|
189
|
+
|
|
190
|
+
Emails are rendered by react-email with emailcn's `createEmailTailwindConfig(theme)`, which takes a plain `EmailTheme` object, not CSS variables. Derive it from the finished CSS with the script that ships with this skill:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# once per workspace, if components/email/email-theme.ts does not exist yet
|
|
194
|
+
npx shadcn@latest add @emailcn/react-email/theme-default
|
|
195
|
+
|
|
196
|
+
node .agents/skills/create-theme/references/email-theme-from-css.mjs themes/<id>.css <id> > components/email/theme-<id>.ts
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The script reads the `:root` block (emails are light documents; the `.dark` block is not used), converts every color to hex, and maps roles by `references/email-theme.md`: `--background` → `colorBackground`, `--foreground` → `colorText`, `--muted` / `--muted-foreground` → `colorBackgroundMuted` / `colorTextMuted`, `--secondary` → `colorBackgroundSubtle`, `--primary` / `--primary-foreground` → `colorPrimary` / `colorPrimaryForeground` and the primary button, `--border` → `colorBorder` and the secondary button's border, `--destructive` → `colorDanger`, `--radius` → `borderRadius` in px, `--font-sans` / `--font-mono` → `fontFamily` / `fontFamilyMono` with `var()` entries dropped. Success, warning, the type scale, and spacing keep emailcn's defaults because the CSS has no opinion on them.
|
|
200
|
+
|
|
201
|
+
Open the generated file and review it: a web font in `fontFamily` needs a web-safe fallback after it (the script keeps whatever fallbacks the CSS listed), and `colorPrimaryHover` / `colorTextSubtle` are shifted from their base tokens, so glance that they still read well. Do not restyle it by hand beyond that; re-run the script after changing the CSS so the two never drift.
|
|
202
|
+
|
|
180
203
|
## Step 7 — Self-review
|
|
181
204
|
|
|
182
205
|
- [ ] `themes/<id>.css` overrides every shadcn token in both `:root` and `.dark`, in OKLCH, with only those two selectors (plus an optional font `@import`).
|
|
@@ -185,15 +208,17 @@ Contract:
|
|
|
185
208
|
- [ ] Typography names only fonts the CSS loads (or the system stack).
|
|
186
209
|
- [ ] "Components in this theme" gives variant guidance for buttons, cards, nav, data, feedback.
|
|
187
210
|
- [ ] Demo `.tsx` default-exports one component, imports only `@/ui/*`, uses semantic tokens only, shows both modes, nothing overflows on mobile.
|
|
188
|
-
- [ ]
|
|
211
|
+
- [ ] `components/email/theme-<id>.ts` generated from the CSS, exports `<id>Theme` typed as `EmailTheme`, hex colors only, and its font stacks end in a web-safe family.
|
|
212
|
+
- [ ] All four files written with the same stem. No page or email changes, no `ui/` changes, no `styles/globals.css` changes.
|
|
189
213
|
|
|
190
214
|
## Step 8 — Hand off
|
|
191
215
|
|
|
192
216
|
Tell the user:
|
|
193
217
|
|
|
194
|
-
- The theme id and the
|
|
218
|
+
- The theme id and the four file paths.
|
|
195
219
|
- That the Themes panel in the workspace (`http://localhost:5173/themes`) previews the demo live, and `/create-page` will list the theme as a picker option on its next run.
|
|
196
220
|
- That any existing page can adopt it by setting `meta.theme: '<id>'` — no other change.
|
|
221
|
+
- That emails adopt it by importing `<id>Theme` from `@/components/email/theme-<id>` and passing it to `createEmailTailwindConfig` (or to an emailcn section's `theme` prop); `/create-email` offers it whenever the file exists.
|
|
197
222
|
- A one-line summary of the look (accent, mode, radius, fonts).
|
|
198
223
|
|
|
199
224
|
Do not run the dev server. Do not modify real pages — the demo `.tsx` is the demonstration.
|
|
@@ -203,7 +228,8 @@ Do not run the dev server. Do not modify real pages — the demo `.tsx` is the d
|
|
|
203
228
|
- ❌ Pasting token values into pages, or theme markdown full of `bg-[#hex]` snippets. Tokens live in the CSS; pages stay semantic.
|
|
204
229
|
- ❌ `@theme inline`, `@import "tailwindcss"`, `@source`, or selectors other than `:root`/`.dark` in `themes/<id>.css`.
|
|
205
230
|
- ❌ Editing `styles/globals.css` or running `npx shadcn apply` against the workspace — that silently rethemes every page.
|
|
206
|
-
- ❌ Producing
|
|
231
|
+
- ❌ Producing a subset of the files. A theme is the **bundle** — `.md`, `.css`, `.demo.tsx`, and `components/email/theme-<id>.ts`, every time.
|
|
232
|
+
- ❌ Hand-typing hex values into the email theme. Generate it from the CSS with the script so the page and email palettes cannot disagree.
|
|
207
233
|
- ❌ A demo with raw colors or hand-rolled buttons — it must prove the tokens carry the look through `ui/`.
|
|
208
234
|
- ❌ Naming font families the CSS never loads.
|
|
209
235
|
- ❌ Inventing values when the user supplied a preset, images, or an existing page. Extract, don't fabricate.
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Derives an emailcn EmailTheme module from a workspace theme's CSS.
|
|
3
|
+
//
|
|
4
|
+
// node <this file> themes/<id>.css <id> > components/email/theme-<id>.ts
|
|
5
|
+
//
|
|
6
|
+
// Reads the :root block (emails are light-mode documents), converts each
|
|
7
|
+
// OKLCH token to hex, and maps the shadcn roles onto EmailTheme fields.
|
|
8
|
+
// Fields the CSS cannot decide (success, warning, type scale, spacing) take
|
|
9
|
+
// emailcn's defaults so the file stays a drop-in for createEmailTailwindConfig.
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
|
|
13
|
+
const [, , cssPath, rawId] = process.argv;
|
|
14
|
+
if (!cssPath || !rawId) {
|
|
15
|
+
process.stderr.write('usage: email-theme-from-css.mjs themes/<id>.css <id>\n');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const css = readFileSync(cssPath, 'utf8');
|
|
20
|
+
const rootBlock = css.match(/:root\s*\{([\s\S]*?)\}/)?.[1] ?? '';
|
|
21
|
+
const tokens = {};
|
|
22
|
+
for (const m of rootBlock.matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/g)) tokens[m[1]] = m[2].trim();
|
|
23
|
+
|
|
24
|
+
const missing = ['background', 'foreground', 'primary', 'primary-foreground', 'border'].filter(
|
|
25
|
+
(t) => !tokens[t],
|
|
26
|
+
);
|
|
27
|
+
if (missing.length) {
|
|
28
|
+
process.stderr.write(`${cssPath} :root is missing ${missing.map((t) => `--${t}`).join(', ')}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clamp01(x) {
|
|
33
|
+
return Math.min(1, Math.max(0, x));
|
|
34
|
+
}
|
|
35
|
+
function linToSrgb(c) {
|
|
36
|
+
const v = clamp01(c);
|
|
37
|
+
return v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
|
|
38
|
+
}
|
|
39
|
+
function oklchToRgb(L, C, H) {
|
|
40
|
+
const h = (H * Math.PI) / 180;
|
|
41
|
+
const a = C * Math.cos(h);
|
|
42
|
+
const b = C * Math.sin(h);
|
|
43
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
44
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
45
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
46
|
+
const l = l_ ** 3;
|
|
47
|
+
const m = m_ ** 3;
|
|
48
|
+
const s = s_ ** 3;
|
|
49
|
+
return [
|
|
50
|
+
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
51
|
+
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
52
|
+
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
53
|
+
].map(linToSrgb);
|
|
54
|
+
}
|
|
55
|
+
function toHex([r, g, b]) {
|
|
56
|
+
return `#${[r, g, b]
|
|
57
|
+
.map((v) =>
|
|
58
|
+
Math.round(v * 255)
|
|
59
|
+
.toString(16)
|
|
60
|
+
.padStart(2, '0'),
|
|
61
|
+
)
|
|
62
|
+
.join('')}`;
|
|
63
|
+
}
|
|
64
|
+
function parseOklch(value) {
|
|
65
|
+
const m = value.match(/oklch\(\s*([\d.]+)%?\s+([\d.]+)\s+([\d.]+)/);
|
|
66
|
+
if (!m) return null;
|
|
67
|
+
let L = Number(m[1]);
|
|
68
|
+
if (value.includes('%')) L /= 100;
|
|
69
|
+
return { L, C: Number(m[2]), H: Number(m[3]) };
|
|
70
|
+
}
|
|
71
|
+
function hex(value) {
|
|
72
|
+
if (!value) return null;
|
|
73
|
+
if (/^#[0-9a-f]{6}$/i.test(value)) return value.toLowerCase();
|
|
74
|
+
const m = value.match(/rgb\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
75
|
+
if (m) return toHex([m[1], m[2], m[3]].map((n) => Number(n) / 255));
|
|
76
|
+
const o = parseOklch(value);
|
|
77
|
+
if (!o) return null;
|
|
78
|
+
return toHex(oklchToRgb(o.L, o.C, o.H));
|
|
79
|
+
}
|
|
80
|
+
function shifted(value, dL) {
|
|
81
|
+
const o = parseOklch(value);
|
|
82
|
+
if (!o) return hex(value);
|
|
83
|
+
return toHex(oklchToRgb(clamp01(o.L + dL), o.C, o.H));
|
|
84
|
+
}
|
|
85
|
+
function px(value, fallback) {
|
|
86
|
+
if (!value) return fallback;
|
|
87
|
+
const m = value.match(/([\d.]+)\s*(rem|px)/);
|
|
88
|
+
if (!m) return fallback;
|
|
89
|
+
const n = Number(m[1]);
|
|
90
|
+
return `${Math.round(m[2] === 'rem' ? n * 16 : n)}px`;
|
|
91
|
+
}
|
|
92
|
+
function fontStack(value, fallback) {
|
|
93
|
+
if (!value) return fallback;
|
|
94
|
+
return value
|
|
95
|
+
.split(',')
|
|
96
|
+
.map((f) => f.trim())
|
|
97
|
+
.filter((f) => !f.startsWith('var('))
|
|
98
|
+
.map((f) => f.replace(/^'(.*)'$/, '"$1"'))
|
|
99
|
+
.join(', ');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const color = (name, fallback) => hex(tokens[name]) ?? fallback;
|
|
103
|
+
const background = color('background', '#ffffff');
|
|
104
|
+
const foreground = color('foreground', '#111827');
|
|
105
|
+
const primary = color('primary', '#111827');
|
|
106
|
+
const primaryForeground = color('primary-foreground', '#ffffff');
|
|
107
|
+
const border = color('border', '#e5e7eb');
|
|
108
|
+
const muted = color('muted', '#f9fafb');
|
|
109
|
+
const mutedForeground = color('muted-foreground', '#6b7280');
|
|
110
|
+
const secondary = color('secondary', '#f3f4f6');
|
|
111
|
+
const radius = px(tokens.radius, '6px');
|
|
112
|
+
const radiusPx = Number.parseInt(radius, 10);
|
|
113
|
+
|
|
114
|
+
const theme = {
|
|
115
|
+
borderRadius: radius,
|
|
116
|
+
borderRadiusLg: `${radiusPx * 2}px`,
|
|
117
|
+
button: {
|
|
118
|
+
primary: {
|
|
119
|
+
backgroundColor: primary,
|
|
120
|
+
borderRadius: radius,
|
|
121
|
+
color: primaryForeground,
|
|
122
|
+
fontSize: '14px',
|
|
123
|
+
fontWeight: '500',
|
|
124
|
+
paddingX: '24px',
|
|
125
|
+
paddingY: '12px',
|
|
126
|
+
},
|
|
127
|
+
secondary: {
|
|
128
|
+
backgroundColor: 'transparent',
|
|
129
|
+
border: `1px solid ${border}`,
|
|
130
|
+
borderRadius: radius,
|
|
131
|
+
color: foreground,
|
|
132
|
+
fontSize: '14px',
|
|
133
|
+
fontWeight: '500',
|
|
134
|
+
paddingX: '24px',
|
|
135
|
+
paddingY: '12px',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
colorBackground: background,
|
|
139
|
+
colorBackgroundMuted: muted,
|
|
140
|
+
colorBackgroundSubtle: secondary,
|
|
141
|
+
colorBorder: border,
|
|
142
|
+
colorBorderSubtle: tokens.input ? color('input', border) : muted,
|
|
143
|
+
colorDanger: color('destructive', '#ef4444'),
|
|
144
|
+
colorPrimary: primary,
|
|
145
|
+
colorPrimaryForeground: primaryForeground,
|
|
146
|
+
colorPrimaryHover: tokens.primary ? shifted(tokens.primary, -0.08) : '#374151',
|
|
147
|
+
colorSuccess: '#10b981',
|
|
148
|
+
colorText: foreground,
|
|
149
|
+
colorTextMuted: mutedForeground,
|
|
150
|
+
colorTextSubtle: tokens['muted-foreground']
|
|
151
|
+
? shifted(tokens['muted-foreground'], 0.14)
|
|
152
|
+
: '#9ca3af',
|
|
153
|
+
colorWarning: '#f59e0b',
|
|
154
|
+
containerWidth: '600px',
|
|
155
|
+
fontFamily: fontStack(
|
|
156
|
+
tokens['font-sans'],
|
|
157
|
+
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
158
|
+
),
|
|
159
|
+
fontFamilyMono: fontStack(tokens['font-mono'], '"Menlo", "Monaco", "Courier New", monospace'),
|
|
160
|
+
fontSizeBase: '14px',
|
|
161
|
+
fontSizeHeading: '28px',
|
|
162
|
+
fontSizeLg: '16px',
|
|
163
|
+
fontSizeSm: '12px',
|
|
164
|
+
fontSizeXl: '20px',
|
|
165
|
+
fontWeightBold: '600',
|
|
166
|
+
fontWeightMedium: '500',
|
|
167
|
+
fontWeightNormal: '400',
|
|
168
|
+
lineHeightBase: '1.6',
|
|
169
|
+
spacingBase: '24px',
|
|
170
|
+
spacingLg: '32px',
|
|
171
|
+
spacingXl: '48px',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const id = rawId.replace(/\.css$/, '');
|
|
175
|
+
const exportName = `${id.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())}Theme`;
|
|
176
|
+
// Unquoted keys, single-quoted strings unless the string itself holds one.
|
|
177
|
+
const body = JSON.stringify(theme, null, 2)
|
|
178
|
+
.replace(/"([a-zA-Z]+)":/g, '$1:')
|
|
179
|
+
.replace(/"((?:[^"\\]|\\.)*)"/g, (_, str) =>
|
|
180
|
+
str.includes("'") ? `"${str}"` : `'${str.replace(/\\"/g, '"')}'`,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
`import type { EmailTheme } from '@/components/email/email-theme';\n\n// Derived from themes/${id}.css by the create-theme skill. Re-run the script\n// after changing the CSS rather than editing colors here by hand.\nexport const ${exportName}: EmailTheme = ${body};\n`,
|
|
185
|
+
);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Email theme mapping
|
|
2
|
+
|
|
3
|
+
`components/email/theme-<id>.ts` is the workspace theme expressed as an emailcn `EmailTheme`: hex colors, pixel sizes, plain font stacks. `email-theme-from-css.mjs` in this folder generates it from `themes/<id>.css`; this file records what it does so the output can be reviewed.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
node .agents/skills/create-theme/references/email-theme-from-css.mjs themes/<id>.css <id> > components/email/theme-<id>.ts
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Only the `:root` block is read. Emails are light-mode documents; mail clients that force dark mode recolor on their own and cannot be steered by a `.dark` block.
|
|
10
|
+
|
|
11
|
+
## Token → field
|
|
12
|
+
|
|
13
|
+
| CSS token (`:root`) | EmailTheme field | Notes |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `--background` | `colorBackground` | body and container fill |
|
|
16
|
+
| `--foreground` | `colorText` | body copy, and the secondary button's text |
|
|
17
|
+
| `--muted` | `colorBackgroundMuted` | bands, table stripes |
|
|
18
|
+
| `--secondary` | `colorBackgroundSubtle` | quieter fills |
|
|
19
|
+
| `--muted-foreground` | `colorTextMuted` | supporting copy |
|
|
20
|
+
| `--muted-foreground` lightened (+0.14 L) | `colorTextSubtle` | captions, legal lines |
|
|
21
|
+
| `--primary` | `colorPrimary`, `button.primary.backgroundColor` | the accent and the CTA fill |
|
|
22
|
+
| `--primary` darkened (−0.08 L) | `colorPrimaryHover` | link hover where clients honor it |
|
|
23
|
+
| `--primary-foreground` | `colorPrimaryForeground`, `button.primary.color` | text on the accent |
|
|
24
|
+
| `--border` | `colorBorder`, `button.secondary.border` | hairlines, outline buttons |
|
|
25
|
+
| `--input` (else `--muted`) | `colorBorderSubtle` | softer rules |
|
|
26
|
+
| `--destructive` | `colorDanger` | errors |
|
|
27
|
+
| `--radius` (rem × 16) | `borderRadius`, both buttons' `borderRadius`; ×2 → `borderRadiusLg` | |
|
|
28
|
+
| `--font-sans` | `fontFamily` | `var()` entries dropped; keep a web-safe family at the end |
|
|
29
|
+
| `--font-mono` | `fontFamilyMono` | same |
|
|
30
|
+
|
|
31
|
+
Fixed at emailcn's defaults because the CSS has no equivalent: `colorSuccess` `#10b981`, `colorWarning` `#f59e0b`, `containerWidth` `600px`, the `fontSize*`, `fontWeight*`, `lineHeightBase`, and `spacing*` fields, and the button padding.
|
|
32
|
+
|
|
33
|
+
## Review after generating
|
|
34
|
+
|
|
35
|
+
- Web fonts: Gmail ignores them. The stack must end in `Arial, sans-serif` or a similar system family; the script keeps whatever the CSS listed, so add one if the CSS relied on `system-ui` alone.
|
|
36
|
+
- Contrast: `colorTextMuted` on `colorBackground` and `colorPrimaryForeground` on `colorPrimary` are the pairs to check; they inherit the CSS values, so they pass when the CSS did.
|
|
37
|
+
- Serif headings: `EmailTheme` has one `fontFamily`. If the page theme uses a serif `--font-heading`, emails set it per heading with `<Font>` plus a `font-serif` class or an explicit `style`, and the theme's `.md` should say so under "Email".
|
|
38
|
+
|
|
39
|
+
## Using the theme in an email
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { createEmailTailwindConfig } from '@/components/email/email-theme';
|
|
43
|
+
import { midnightSaasTheme } from '@/components/email/theme-midnight-saas';
|
|
44
|
+
|
|
45
|
+
<Tailwind config={createEmailTailwindConfig(midnightSaasTheme)}>…</Tailwind>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
emailcn sections take a `theme` prop; blocks pin their theme inside the file and need a copy under `emails/<id>/components/` to swap it. The `create-email` skill covers both.
|
|
@@ -43,10 +43,10 @@ Path is relative to the project root (the user's `cwd`, the directory that conta
|
|
|
43
43
|
}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
- `pageId` — folder name under `pages
|
|
46
|
+
- `pageId` — folder name under `pages/` (or under `emails/` when `view` is `"emails"`). Use as-is for any `/__pages/<id>/...` API or as the URL segment (`/p/<id>`, `/e/<id>` for an email).
|
|
47
47
|
- `pageTitle` — the page's `meta.title` (or `<title>` for an HTML page), falling back to the id.
|
|
48
|
-
- `pagePath` —
|
|
49
|
-
- `view` — `"pages"` when the user is viewing
|
|
48
|
+
- `pagePath` — entry path **relative to the project root**: `pages/<id>/index.tsx`, `pages/<id>/index.html` for a plain HTML page, or `emails/<id>/index.tsx` for an email. Prefix it with the project root before handing it to `Read` / `Edit`. Note the selection may point into a file under `pages/<id>/components/` (or `emails/<id>/components/`) if the entry is split — match the line against the file whose JSX contains that tag and text.
|
|
49
|
+
- `view` — `"pages"` when the user is viewing a page, `"assets"` when they are browsing that page's files in the asset manager, `"emails"` when they are viewing an email template. An email means the `create-email` skill's rules apply (react-email components, no `ui/`), not `page-authoring`.
|
|
50
50
|
- `selection` — `null` if nothing is selected. Otherwise, the JSX element the user picked in the inspector:
|
|
51
51
|
- `line` (1-indexed) and `column` (0-indexed) point to the JSX opening tag in the page source. This is the canonical handle — match against the source line.
|
|
52
52
|
- `tagName` is the rendered HTML tag, lowercased (`"h1"`, `"div"`, `"button"`). The source line it points at may be a shadcn component rather than that tag — a `"button"` selection usually lands on a `<Button>` line in the page, because `ui/` components spread the inspector tag onto their root. Edit the page line; never follow it into `ui/*.tsx`.
|
|
@@ -187,7 +187,10 @@ A theme is `themes/<id>.md` (direction and component notes) + `themes/<id>.css`
|
|
|
187
187
|
- [ ] One coherent type scale across the page; contrast holds on dark sections.
|
|
188
188
|
- [ ] Designed repeats are explicit component instances; data lists are a `.map` over a typed const.
|
|
189
189
|
- [ ] No `window`/`document` access at module top level; effects clean up.
|
|
190
|
+
- [ ] One label per CTA intent across the page ("Get started" in the nav, hero, and footer — not "Get started" / "Sign up free" / "Try it"), and no primary button label wraps at desktop width.
|
|
191
|
+
- [ ] Every animation is motivated: it shows hierarchy, sequence, feedback, or a state change. If you cannot say which in one sentence, remove it. `motion-safe:` / `motion-reduce:` variants respect the user's preference.
|
|
190
192
|
- [ ] Nothing outside `pages/<id>/` was edited.
|
|
193
|
+
- [ ] Ran the `web-design-guidelines` skill on the page and resolved or justified its findings.
|
|
191
194
|
|
|
192
195
|
## Anti-patterns
|
|
193
196
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-design-guidelines
|
|
3
|
+
description: Review page code against Vercel's Web Interface Guidelines — accessibility, focus and keyboard handling, forms, motion, layout, typography, and performance. Use when asked to "review my page", "check accessibility", "audit the design", "review UX", or as the review pass at the end of `create-page` and `apply-comments`.
|
|
4
|
+
metadata:
|
|
5
|
+
author: vercel
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
argument-hint: <page-id or file-or-pattern>
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Web Interface Guidelines
|
|
11
|
+
|
|
12
|
+
Review page files for compliance with Vercel's [Web Interface Guidelines](https://github.com/vercel-labs/web-interface-guidelines) (MIT). Vendored from the [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) `web-design-guidelines` skill and scoped to an open-pages workspace.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
1. Fetch the latest guidelines from the source URL below.
|
|
17
|
+
2. Read the files to review.
|
|
18
|
+
3. Check them against every rule in the fetched guidelines.
|
|
19
|
+
4. Report findings in the terse `file:line` format the guidelines specify.
|
|
20
|
+
|
|
21
|
+
## Guidelines source
|
|
22
|
+
|
|
23
|
+
Fetch fresh guidelines before each review:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and the output format. If the fetch fails (offline, blocked), say so and fall back to the self-review checklist in the `page-authoring` skill instead of guessing at rules.
|
|
30
|
+
|
|
31
|
+
## What to review
|
|
32
|
+
|
|
33
|
+
- Given a page id, review the page's entry (`pages/<id>/index.tsx` or `pages/<id>/index.html`), everything under `pages/<id>/components/`, and any `styles.css`, `style.css`, or `main.js` beside the entry.
|
|
34
|
+
- Given a file or glob, review those files.
|
|
35
|
+
- Given nothing, resolve the current page with the `current-page` skill; if that yields nothing, ask which page to review.
|
|
36
|
+
|
|
37
|
+
Never review or report on files under `ui/`, `lib/`, `hooks/`, or `styles/`. Those are the shared shadcn set and are not edited for one page; a finding there is a `create-theme` or upstream matter, not a page fix.
|
|
38
|
+
|
|
39
|
+
## Applying findings
|
|
40
|
+
|
|
41
|
+
Report first. When the user asks you to fix, or the review runs inside `create-page` or `apply-comments`, apply the fixes to the page files only, following the `page-authoring` skill: keep `@/ui/*` components, keep semantic tokens, keep the type scale. Do not add dependencies to satisfy a rule.
|
package/template/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# open-pages — Agent Guide
|
|
2
2
|
|
|
3
|
-
You are authoring **web pages** in this repo. Every page is a React component rendered as a real web page — composed from the shadcn/ui components under `ui/`, styled with Tailwind via `className`, with hooks, state, and browser APIs available. A page folder can hold an `index.html` instead when plain HTML is the better fit.
|
|
3
|
+
You are authoring **web pages** and **email templates** in this repo. Every page is a React component rendered as a real web page — composed from the shadcn/ui components under `ui/`, styled with Tailwind via `className`, with hooks, state, and browser APIs available. A page folder can hold an `index.html` instead when plain HTML is the better fit. An email is a [react-email](https://react.email) component under `emails/<id>/`, rendered on the server to inlined HTML plus plain text; it never runs in a browser.
|
|
4
4
|
|
|
5
5
|
## Hard rules
|
|
6
6
|
|
|
@@ -8,16 +8,19 @@ You are authoring **web pages** in this repo. Every page is a React component re
|
|
|
8
8
|
- The entry is `pages/<id>/index.tsx` (or `pages/<id>/index.html`).
|
|
9
9
|
- A page is its entry plus optional `components/`, `styles.css`, and `assets/` (images, fonts) inside its folder. Shared assets live in the root `assets/` folder (import via `@assets/...`).
|
|
10
10
|
- Use the shadcn components first: `import { Button } from '@/ui/button'`, `cn` from `@/lib/utils`. Use the semantic token classes (`bg-background`, `text-muted-foreground`, `bg-primary`) so themes apply.
|
|
11
|
-
-
|
|
12
|
-
- Do **not**
|
|
13
|
-
- Do not
|
|
11
|
+
- Put an email under `emails/<kebab-case-id>/index.tsx`. Emails import from `react-email` and `@/components/email/*` only — never `@/ui`, hooks, or browser APIs.
|
|
12
|
+
- Do **not** edit files under `ui/`, `lib/`, `hooks/`, or `components/email/` for one page or email. They are shared; wrap or extend a component inside the page or email instead.
|
|
13
|
+
- Do **not** touch `package.json`, `open-pages.config.ts`, `components.json`, `styles/globals.css`, or other pages and emails.
|
|
14
|
+
- Do not add dependencies beyond what is installed. `npx shadcn@latest add` is fine for blocks and registry items, including `@emailcn/react-email/*` email sections, themes, and blocks.
|
|
14
15
|
|
|
15
16
|
## Which skill to use
|
|
16
17
|
|
|
17
18
|
- **Drafting a new page** — use the `create-page` skill. It walks through scoping questions, structure, and hand-off.
|
|
18
|
-
- **
|
|
19
|
+
- **Drafting or editing an email template** — use the `create-email` skill. It owns both the workflow and the technical reference for `emails/<id>/` (react-email components, the emailcn registry, email-safe styling, plain-text output).
|
|
20
|
+
- **Applying inspector comments** (`@page-comment` markers in a page or email) — use the `apply-comments` skill.
|
|
19
21
|
- **Creating or extracting a theme** — use the `create-theme` skill. A theme is `themes/<id>.md` plus `themes/<id>.css` (shadcn token overrides) and a `<id>.demo.tsx` preview; `create-page` reads it before authoring and a page opts in with `meta.theme`.
|
|
20
22
|
- **shadcn CLI, registries, presets, component docs** — the bundled `shadcn` skill (the official one) covers `npx shadcn@latest search / view / docs / add / apply`.
|
|
23
|
+
- **Reviewing a page for accessibility and interaction quality** — the bundled `web-design-guidelines` skill (Vercel's, vendored) checks a page against the current Web Interface Guidelines. `create-page` runs it before hand-off; run it on its own when asked to "review my page" or "check accessibility".
|
|
21
24
|
- **Resolving "this page" / "this element"** — when the user references the current page or selection without naming it, consult the `current-page` skill. It reads the dev server's `node_modules/.open-pages/current.json` to find which page and inspector-picked element they mean.
|
|
22
25
|
- **Any other page edit** — read the `page-authoring` skill before writing. It is the technical reference for everything inside `pages/<id>/`: file contract, styling with Tailwind, layout and responsiveness, interactivity, assets and fonts, self-review checklist. `create-page` and `apply-comments` both defer to it for the *how*.
|
|
23
26
|
|
package/template/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Web pages as React components. Each page lives under `pages/<id>/index.tsx` and
|
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
pages/ one folder per page
|
|
7
|
+
emails/ one folder per email template (react-email)
|
|
8
|
+
components/email/ emailcn sections, themes, blocks (npx shadcn add @emailcn/...)
|
|
7
9
|
ui/ all shadcn/ui components (import from @/ui/*)
|
|
8
10
|
lib/utils.ts cn()
|
|
9
11
|
hooks/ use-mobile
|
|
@@ -28,7 +30,7 @@ Then open `http://localhost:5173`, edit `pages/getting-started/index.tsx`, or cr
|
|
|
28
30
|
| --- | --- |
|
|
29
31
|
| `npm run dev` | Start the dev server with live preview and hot reload. |
|
|
30
32
|
| `npm run build` | Build the whole workspace viewer as a static site. |
|
|
31
|
-
| `npm run export` | Build pages into `export/<id
|
|
33
|
+
| `npm run export` | Build pages into `export/<id>/` and emails into `export/emails/<id>/`. |
|
|
32
34
|
| `npm run preview` | Preview the built workspace locally. |
|
|
33
35
|
| `npm run sync:skills` | Sync the bundled agent skills into the workspace. |
|
|
34
36
|
| `npx open-pages sync:ui` | Update `ui/`, `lib/`, and `hooks/` to the installed runtime's set; files you edited are kept (`--force` to overwrite). |
|
|
@@ -61,6 +63,10 @@ A page is a real web page: every shadcn component is importable from `@/ui/<name
|
|
|
61
63
|
|
|
62
64
|
A folder holding an `index.html` (with sibling CSS/JS) instead of `index.tsx` works too. It is served as-is and exported the same way.
|
|
63
65
|
|
|
66
|
+
## Authoring an email
|
|
67
|
+
|
|
68
|
+
`emails/<id>/index.tsx` default-exports a [react-email](https://react.email) component with a `meta` export (`title`, `subject`, `createdAt`). The workspace renders it on the server, previews the HTML and the plain-text version at `http://localhost:5173/e/<id>`, and `npm run export` writes both files under `export/emails/<id>/`. Ask your agent to `/create-email`; it installs sections and themes from the emailcn registry with `npx shadcn@latest add @emailcn/react-email/<item>`.
|
|
69
|
+
|
|
64
70
|
See [`AGENTS.md`](./AGENTS.md) for the rules your agent follows.
|
|
65
71
|
|
|
66
72
|
## The viewer
|
package/template/components.json
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { EmailMeta } from '@autono/open-pages';
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Button,
|
|
5
|
+
Container,
|
|
6
|
+
Head,
|
|
7
|
+
Heading,
|
|
8
|
+
Hr,
|
|
9
|
+
Html,
|
|
10
|
+
Preview,
|
|
11
|
+
Section,
|
|
12
|
+
Tailwind,
|
|
13
|
+
Text,
|
|
14
|
+
} from 'react-email';
|
|
15
|
+
|
|
16
|
+
export const meta: EmailMeta = {
|
|
17
|
+
title: 'Welcome',
|
|
18
|
+
subject: 'Welcome to open-pages',
|
|
19
|
+
description: 'The starter email. Ask your agent to /create-email for a real one.',
|
|
20
|
+
createdAt: '2026-09-27T00:00:00.000Z',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default function Welcome() {
|
|
24
|
+
return (
|
|
25
|
+
<Html lang="en">
|
|
26
|
+
<Head />
|
|
27
|
+
<Preview>Emails live next to your pages now. Here is how they work.</Preview>
|
|
28
|
+
<Tailwind>
|
|
29
|
+
<Body className="bg-[#f4f4f2] font-sans">
|
|
30
|
+
<Container className="mx-auto my-8 max-w-[600px] rounded-lg bg-white px-8 py-10">
|
|
31
|
+
<Heading className="m-0 text-[24px] font-semibold leading-tight text-[#111111]">
|
|
32
|
+
Emails, the same way as pages
|
|
33
|
+
</Heading>
|
|
34
|
+
<Text className="text-[16px] leading-6 text-[#444444]">
|
|
35
|
+
This file is <code>emails/welcome/index.tsx</code>: one react-email component. The
|
|
36
|
+
workspace renders it on the server, shows the HTML and the plain-text version, and
|
|
37
|
+
exports both with <code>open-pages export</code>.
|
|
38
|
+
</Text>
|
|
39
|
+
<Text className="text-[16px] leading-6 text-[#444444]">
|
|
40
|
+
Ask your agent to run <code>/create-email</code>. It installs sections, themes, and
|
|
41
|
+
whole emails from the emailcn registry with the shadcn CLI, then writes the email
|
|
42
|
+
here.
|
|
43
|
+
</Text>
|
|
44
|
+
<Section className="my-6">
|
|
45
|
+
<Button
|
|
46
|
+
href="https://docs.openpages.sh/authoring/emails"
|
|
47
|
+
className="rounded-md bg-[#111111] px-5 py-3 text-[14px] font-medium text-white"
|
|
48
|
+
>
|
|
49
|
+
Read the email docs
|
|
50
|
+
</Button>
|
|
51
|
+
</Section>
|
|
52
|
+
<Hr className="my-6 border-[#e5e5e5]" />
|
|
53
|
+
<Text className="m-0 text-[13px] leading-5 text-[#888888]">
|
|
54
|
+
You are seeing this because it ships with every new open-pages workspace.
|
|
55
|
+
</Text>
|
|
56
|
+
</Container>
|
|
57
|
+
</Body>
|
|
58
|
+
</Tailwind>
|
|
59
|
+
</Html>
|
|
60
|
+
);
|
|
61
|
+
}
|
package/template/package.json
CHANGED
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
@custom-variant dark (&:is(.dark *));
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/* Fonts are emitted as :root variables (not inlined) so a theme's own
|
|
14
|
+
:root block can swap them; inlined, Geist would be baked into every
|
|
15
|
+
font-* utility and themes could never change the typeface. */
|
|
16
|
+
@theme {
|
|
15
17
|
--font-sans: 'Geist Variable', sans-serif;
|
|
18
|
+
--font-heading: var(--font-sans);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@theme inline {
|
|
16
22
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
17
23
|
--color-sidebar-border: var(--sidebar-border);
|
|
18
24
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
package/template/tsconfig.json
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
"strict": true,
|
|
13
13
|
"skipLibCheck": true,
|
|
14
14
|
"types": ["@autono/open-pages/env"],
|
|
15
|
-
"baseUrl": ".",
|
|
16
15
|
"paths": {
|
|
17
16
|
"@/*": ["./*"]
|
|
18
17
|
}
|
|
19
18
|
},
|
|
20
19
|
"include": [
|
|
21
20
|
"pages/**/*",
|
|
21
|
+
"emails/**/*",
|
|
22
|
+
"components/**/*",
|
|
22
23
|
"ui/**/*",
|
|
23
24
|
"lib/**/*",
|
|
24
25
|
"hooks/**/*",
|