@autono/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/{build-CCoxDxL-.js → build-Cpod-K9R.js} +9 -4
- package/dist/cli/bin.js +7 -7
- package/dist/{config-D_U_aCQz.js → config-CYLBzMMb.js} +1077 -18
- package/dist/{config-p-TWNXbv.d.ts → config-IkwICvOe.d.ts} +2 -1
- package/dist/{dev-CTrB263u.js → dev-B7x-G-wk.js} +1 -1
- package/dist/{export-enKL9R7x.js → export-BxMksWEc.js} +74 -12
- package/dist/index.d.ts +23 -3
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +96 -0
- package/dist/{preview-Dmzyd1cf.js → preview-CMv1lGOC.js} +1 -1
- package/dist/{types-DDdUFUIS.d.ts → types-B-9WXL93.d.ts} +24 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/apply-comments/SKILL.md +6 -3
- package/skills/create-email/SKILL.md +166 -0
- package/skills/create-page/SKILL.md +5 -1
- package/skills/create-theme/SKILL.md +33 -7
- package/skills/create-theme/references/email-theme-from-css.mjs +185 -0
- package/skills/create-theme/references/email-theme.md +48 -0
- package/skills/current-page/SKILL.md +3 -3
- package/skills/page-authoring/SKILL.md +3 -0
- package/skills/web-design-guidelines/SKILL.md +41 -0
- package/src/app/app.tsx +4 -0
- package/src/app/components/command/home-command-menu.tsx +19 -2
- package/src/app/components/selection-note.tsx +71 -0
- package/src/app/components/sidebar/folder-item.tsx +9 -2
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/frame/email-inspect.ts +17 -0
- package/src/app/lib/emails.ts +17 -0
- package/src/app/lib/frame.ts +15 -4
- package/src/app/lib/page-thumb.tsx +11 -3
- package/src/app/lib/sdk.ts +22 -0
- package/src/app/routes/email.tsx +424 -0
- package/src/app/routes/emails.tsx +94 -0
- package/src/app/routes/home-shell.tsx +21 -2
- package/src/app/routes/page.tsx +21 -57
- package/src/app/virtual.d.ts +10 -0
- package/src/locale/en.ts +25 -0
- package/src/locale/ja.ts +25 -0
- package/src/locale/types.ts +24 -0
- package/src/locale/zh-cn.ts +25 -0
- package/src/locale/zh-tw.ts +25 -0
- package/workspace/components.json +3 -1
- package/workspace/styles/globals.css +8 -2
- package/dist/open-pages-plugin-C8cFhMB9.js +0 -662
- package/dist/version-DZrXLoAD.js +0 -42
|
@@ -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/src/app/app.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { Toaster } from './components/ui/sonner';
|
|
|
4
4
|
import { TooltipProvider } from './components/ui/tooltip';
|
|
5
5
|
import { useLocale } from './lib/use-locale';
|
|
6
6
|
import { AssetsPage } from './routes/assets';
|
|
7
|
+
import { EmailView } from './routes/email';
|
|
8
|
+
import { EmailsGalleryPage } from './routes/emails';
|
|
7
9
|
import { Home } from './routes/home';
|
|
8
10
|
import { HomeShell } from './routes/home-shell';
|
|
9
11
|
import { PageView } from './routes/page';
|
|
@@ -22,11 +24,13 @@ export function App() {
|
|
|
22
24
|
<Route path="/themes" element={<ThemesGalleryPage />} />
|
|
23
25
|
<Route path="/themes/:themeId" element={<ThemeDetailPage />} />
|
|
24
26
|
<Route path="/assets" element={<AssetsPage />} />
|
|
27
|
+
<Route path="/emails" element={<EmailsGalleryPage />} />
|
|
25
28
|
</Route>
|
|
26
29
|
) : (
|
|
27
30
|
<Route path="/" element={<NotFound />} />
|
|
28
31
|
)}
|
|
29
32
|
<Route path="/p/:pageId" element={<PageView />} />
|
|
33
|
+
<Route path="/e/:emailId" element={<EmailView />} />
|
|
30
34
|
<Route path="*" element={<NotFound />} />
|
|
31
35
|
</Routes>
|
|
32
36
|
</TooltipProvider>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { Presentation } from 'lucide-react';
|
|
1
|
+
import { Mail, Presentation } from 'lucide-react';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { useNavigate } from 'react-router-dom';
|
|
4
4
|
import { useLocale } from '~/lib/use-locale';
|
|
5
5
|
import { usePageTitles } from '~/lib/use-page-titles';
|
|
6
|
+
import { emailIds, emailMeta } from '../../lib/emails';
|
|
6
7
|
import { pageIds } from '../../lib/pages';
|
|
7
8
|
import type { Folder } from '../../lib/sdk';
|
|
8
9
|
import { FolderIconChip, SystemViewIcon } from '../sidebar/folder-item';
|
|
9
|
-
import { ALL_DOCS_ID, ASSETS_ID, DRAFT_ID, THEMES_ID } from '../sidebar/sidebar';
|
|
10
|
+
import { ALL_DOCS_ID, ASSETS_ID, DRAFT_ID, EMAILS_ID, THEMES_ID } from '../sidebar/sidebar';
|
|
10
11
|
import { type CommandGroupSpec, CommandMenu, type CommandSpec } from './command-menu';
|
|
11
12
|
|
|
12
13
|
export function HomeCommandMenu({
|
|
@@ -35,6 +36,14 @@ export function HomeCommandMenu({
|
|
|
35
36
|
run: () => navigate(`/p/${id}`),
|
|
36
37
|
}));
|
|
37
38
|
|
|
39
|
+
const emails: CommandSpec[] = emailIds.map((id) => ({
|
|
40
|
+
id: `email-${id}`,
|
|
41
|
+
label: emailMeta[id]?.title ?? id,
|
|
42
|
+
icon: <Mail />,
|
|
43
|
+
keywords: [id, 'email', emailMeta[id]?.subject ?? ''],
|
|
44
|
+
run: () => navigate(`/e/${id}`),
|
|
45
|
+
}));
|
|
46
|
+
|
|
38
47
|
const folderItems: CommandSpec[] = [
|
|
39
48
|
{
|
|
40
49
|
id: `view-${ALL_DOCS_ID}`,
|
|
@@ -67,6 +76,13 @@ export function HomeCommandMenu({
|
|
|
67
76
|
keywords: ['themes', 'design'],
|
|
68
77
|
run: () => onSelectView(THEMES_ID),
|
|
69
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: `view-${EMAILS_ID}`,
|
|
81
|
+
label: t.home.emails,
|
|
82
|
+
icon: <SystemViewIcon kind="emails" />,
|
|
83
|
+
keywords: ['emails', 'mail', 'templates'],
|
|
84
|
+
run: () => onSelectView(EMAILS_ID),
|
|
85
|
+
},
|
|
70
86
|
];
|
|
71
87
|
if (import.meta.env.DEV) {
|
|
72
88
|
navigation.push({
|
|
@@ -80,6 +96,7 @@ export function HomeCommandMenu({
|
|
|
80
96
|
|
|
81
97
|
return [
|
|
82
98
|
{ id: 'pages', heading: t.commandMenu.groupDocs, items: pages },
|
|
99
|
+
{ id: 'emails', heading: t.commandMenu.groupEmails, items: emails },
|
|
83
100
|
{ id: 'folders', heading: t.commandMenu.groupFolders, items: folderItems },
|
|
84
101
|
{ id: 'navigation', heading: t.commandMenu.groupNavigation, items: navigation },
|
|
85
102
|
];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Loader2, MessageSquarePlus, X } from 'lucide-react';
|
|
2
|
+
import { Button } from '~/components/ui/button';
|
|
3
|
+
|
|
4
|
+
export type Selection = { loc: string; tag: string; text: string };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The floating note box that follows an inspector pick: shows the picked
|
|
8
|
+
* element and lets the user leave a comment for their agent.
|
|
9
|
+
*/
|
|
10
|
+
export function SelectionNote({
|
|
11
|
+
selection,
|
|
12
|
+
note,
|
|
13
|
+
saving,
|
|
14
|
+
onNoteChange,
|
|
15
|
+
onSubmit,
|
|
16
|
+
onClear,
|
|
17
|
+
}: {
|
|
18
|
+
selection: Selection;
|
|
19
|
+
note: string;
|
|
20
|
+
saving: boolean;
|
|
21
|
+
onNoteChange: (note: string) => void;
|
|
22
|
+
onSubmit: () => void;
|
|
23
|
+
onClear: () => void;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="fixed bottom-6 right-6 z-20 w-[340px] rounded-lg border bg-background p-3 shadow-xl">
|
|
27
|
+
<div className="flex items-center gap-2">
|
|
28
|
+
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[11px]">
|
|
29
|
+
{selection.tag}
|
|
30
|
+
</span>
|
|
31
|
+
<span className="font-mono text-[11px] text-muted-foreground">
|
|
32
|
+
line {selection.loc.replace(':', ', col ')}
|
|
33
|
+
</span>
|
|
34
|
+
<Button
|
|
35
|
+
variant="ghost"
|
|
36
|
+
size="icon"
|
|
37
|
+
className="ml-auto size-6"
|
|
38
|
+
aria-label="Clear selection"
|
|
39
|
+
onClick={onClear}
|
|
40
|
+
>
|
|
41
|
+
<X className="size-3.5" />
|
|
42
|
+
</Button>
|
|
43
|
+
</div>
|
|
44
|
+
{selection.text && (
|
|
45
|
+
<p className="mt-1.5 line-clamp-2 text-xs text-muted-foreground">“{selection.text}”</p>
|
|
46
|
+
)}
|
|
47
|
+
<textarea
|
|
48
|
+
value={note}
|
|
49
|
+
onChange={(e) => onNoteChange(e.target.value)}
|
|
50
|
+
placeholder="Leave a note for your agent — e.g. “make this bold”"
|
|
51
|
+
rows={2}
|
|
52
|
+
className="mt-2 w-full resize-none rounded-md border bg-transparent px-2 py-1.5 text-sm outline-none focus:ring-1 focus:ring-ring"
|
|
53
|
+
/>
|
|
54
|
+
<div className="mt-2 flex justify-end gap-2">
|
|
55
|
+
<Button
|
|
56
|
+
size="sm"
|
|
57
|
+
onClick={onSubmit}
|
|
58
|
+
disabled={!note.trim() || saving}
|
|
59
|
+
aria-label="Save comment"
|
|
60
|
+
>
|
|
61
|
+
{saving ? (
|
|
62
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
63
|
+
) : (
|
|
64
|
+
<MessageSquarePlus className="size-3.5" />
|
|
65
|
+
)}
|
|
66
|
+
Comment
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
FolderOpen,
|
|
3
3
|
LayoutGrid,
|
|
4
4
|
type LucideIcon,
|
|
5
|
+
Mail,
|
|
5
6
|
MoreHorizontal,
|
|
6
7
|
Palette,
|
|
7
8
|
Pencil,
|
|
@@ -42,13 +43,14 @@ function useDocDragActive() {
|
|
|
42
43
|
return active;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
export type SystemViewKind = 'all' | 'draft' | 'themes' | 'assets';
|
|
46
|
+
export type SystemViewKind = 'all' | 'draft' | 'themes' | 'assets' | 'emails';
|
|
46
47
|
|
|
47
48
|
const SYSTEM_VIEW_ICONS: Record<SystemViewKind, LucideIcon> = {
|
|
48
49
|
all: LayoutGrid,
|
|
49
50
|
draft: PenLine,
|
|
50
51
|
themes: Palette,
|
|
51
52
|
assets: FolderOpen,
|
|
53
|
+
emails: Mail,
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
export function SystemViewIcon({ kind, className }: { kind: SystemViewKind; className?: string }) {
|
|
@@ -103,6 +105,9 @@ type Row =
|
|
|
103
105
|
}
|
|
104
106
|
| {
|
|
105
107
|
kind: 'assets';
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
kind: 'emails';
|
|
106
111
|
};
|
|
107
112
|
|
|
108
113
|
export function FolderItem({
|
|
@@ -168,7 +173,9 @@ export function FolderItem({
|
|
|
168
173
|
? t.home.themes
|
|
169
174
|
: row.kind === 'assets'
|
|
170
175
|
? t.home.assets
|
|
171
|
-
: row.
|
|
176
|
+
: row.kind === 'emails'
|
|
177
|
+
? t.home.emails
|
|
178
|
+
: row.folder.name;
|
|
172
179
|
|
|
173
180
|
const commitRename = () => {
|
|
174
181
|
if (row.kind !== 'folder') return;
|