@autono/open-pages 0.8.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 +5 -5
- 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 -4
- package/skills/create-email/SKILL.md +166 -0
- package/skills/create-theme/SKILL.md +31 -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/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 +9 -50
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as Locale } from "./types-
|
|
1
|
+
import { t as Locale } from "./types-B-9WXL93.js";
|
|
2
2
|
//#region src/config.d.ts
|
|
3
3
|
type OpenPagesBuildConfig = {
|
|
4
4
|
showPageBrowser?: boolean;
|
|
@@ -7,6 +7,7 @@ type OpenPagesBuildConfig = {
|
|
|
7
7
|
type OpenPagesConfig = {
|
|
8
8
|
base?: string;
|
|
9
9
|
pagesDir?: string;
|
|
10
|
+
emailsDir?: string;
|
|
10
11
|
themesDir?: string;
|
|
11
12
|
assetsDir?: string;
|
|
12
13
|
port?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { s as DEV_SUPERVISED_ENV, t as createViteConfig } from "./config-CYLBzMMb.js";
|
|
1
2
|
import { a as printUrls, i as printShortcutsHint, n as formatError, o as shortcutsEnabled, r as printHeader, s as startupDuration, t as createCliLogger } from "./cli/bin.js";
|
|
2
|
-
import { n as DEV_SUPERVISED_ENV, t as createViteConfig } from "./config-D_U_aCQz.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { fork } from "node:child_process";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { a as openPagesPlugin, i as
|
|
1
|
+
import { a as renderEmail, c as extractMeta, d as openPagesPlugin, i as findEmails, l as findPages, o as stripLocTags, t as createViteConfig, u as loadUserConfig } from "./config-CYLBzMMb.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import tailwindcss from "@tailwindcss/vite";
|
|
7
7
|
import react from "@vitejs/plugin-react";
|
|
8
|
-
import { build } from "vite";
|
|
8
|
+
import { build, createServer } from "vite";
|
|
9
9
|
//#region src/cli/export.ts
|
|
10
10
|
function escapeHtml(s) {
|
|
11
11
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
@@ -99,21 +99,72 @@ function extractDescription(src) {
|
|
|
99
99
|
const end = src.indexOf("};", metaStart);
|
|
100
100
|
return src.slice(metaStart, end === -1 ? void 0 : end).match(META_DESCRIPTION_RE)?.[1] ?? null;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Render emails without a browser: a middleware-mode dev server loads each
|
|
104
|
+
* entry through Vite SSR, and `react-email` from the workspace renders it.
|
|
105
|
+
* Each email lands as `<outDir>/<id>/index.html` plus `index.txt`.
|
|
106
|
+
*/
|
|
107
|
+
async function exportEmails(opts) {
|
|
108
|
+
const { userCwd, config, entries, outDir } = opts;
|
|
109
|
+
if (entries.length === 0) return;
|
|
110
|
+
const base = await createViteConfig({
|
|
111
|
+
userCwd,
|
|
112
|
+
config,
|
|
113
|
+
mode: "serve"
|
|
114
|
+
});
|
|
115
|
+
const server = await createServer({
|
|
116
|
+
...base,
|
|
117
|
+
logLevel: "error",
|
|
118
|
+
appType: "custom",
|
|
119
|
+
cacheDir: path.join(userCwd, "node_modules", ".open-pages", "email-render"),
|
|
120
|
+
optimizeDeps: {
|
|
121
|
+
noDiscovery: true,
|
|
122
|
+
include: []
|
|
123
|
+
},
|
|
124
|
+
server: {
|
|
125
|
+
...base.server,
|
|
126
|
+
middlewareMode: true,
|
|
127
|
+
watch: null,
|
|
128
|
+
hmr: false
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const started = performance.now();
|
|
134
|
+
const rendered = await renderEmail(server, userCwd, entry);
|
|
135
|
+
const target = path.join(outDir, entry.id);
|
|
136
|
+
await mkdir(target, { recursive: true });
|
|
137
|
+
await writeFile(path.join(target, "index.html"), stripLocTags(rendered.html), "utf8");
|
|
138
|
+
await writeFile(path.join(target, "index.txt"), rendered.text, "utf8");
|
|
139
|
+
opts.onDone?.(entry, target, Math.round(performance.now() - started));
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
await server.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
102
145
|
async function exportPages(opts = {}) {
|
|
103
146
|
const userCwd = process.cwd();
|
|
104
147
|
const config = await loadUserConfig(userCwd);
|
|
105
148
|
const pagesDir = config.pagesDir ?? "pages";
|
|
149
|
+
const emailsDir = config.emailsDir ?? "emails";
|
|
106
150
|
const outDir = path.resolve(userCwd, opts.outDir ?? "export");
|
|
107
151
|
const entries = await findPages(userCwd, pagesDir);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
152
|
+
const emailEntries = await findEmails(userCwd, emailsDir);
|
|
153
|
+
const pageIds = entries.map((e) => e.id);
|
|
154
|
+
const emailIds = emailEntries.map((e) => e.id);
|
|
155
|
+
if (pageIds.length === 0 && emailIds.length === 0) throw new Error(`No pages found under ${pagesDir}/ and no emails under ${emailsDir}/`);
|
|
156
|
+
const explicit = opts.pages && opts.pages.length > 0 ? opts.pages : null;
|
|
157
|
+
const unknown = (explicit ?? []).filter((id) => !pageIds.includes(id) && !emailIds.includes(id));
|
|
158
|
+
if (unknown.length > 0) {
|
|
159
|
+
const available = [...pageIds, ...emailIds.map((id) => `${id} (email)`)].join(", ");
|
|
160
|
+
throw new Error(`Page not found: ${unknown.join(", ")} (available: ${available})`);
|
|
161
|
+
}
|
|
162
|
+
const requestedPages = explicit ? pageIds.filter((id) => explicit.includes(id)) : pageIds;
|
|
163
|
+
const requestedEmails = explicit ? emailIds.filter((id) => explicit.includes(id)) : emailIds;
|
|
164
|
+
const { readCoreVersion } = await import("./config-CYLBzMMb.js").then((n) => n.r);
|
|
114
165
|
const coreVersion = readCoreVersion();
|
|
115
166
|
await mkdir(outDir, { recursive: true });
|
|
116
|
-
for (const id of
|
|
167
|
+
for (const id of requestedPages) {
|
|
117
168
|
const entry = entries.find((e) => e.id === id);
|
|
118
169
|
if (!entry) continue;
|
|
119
170
|
const started = performance.now();
|
|
@@ -128,8 +179,19 @@ async function exportPages(opts = {}) {
|
|
|
128
179
|
const ms = Math.round(performance.now() - started);
|
|
129
180
|
process.stdout.write(`${chalk.green("ok")} ${path.relative(userCwd, target)}/ ${chalk.dim(`${entry.kind} · ${ms}ms`)}\n`);
|
|
130
181
|
}
|
|
131
|
-
|
|
132
|
-
|
|
182
|
+
await exportEmails({
|
|
183
|
+
userCwd,
|
|
184
|
+
config,
|
|
185
|
+
entries: emailEntries.filter((e) => requestedEmails.includes(e.id)),
|
|
186
|
+
outDir: path.join(outDir, "emails"),
|
|
187
|
+
onDone: (_entry, target, ms) => {
|
|
188
|
+
process.stdout.write(`${chalk.green("ok")} ${path.relative(userCwd, target)}/ ${chalk.dim(`email · ${ms}ms`)}\n`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (requestedPages.length > 0) parts.push(`${requestedPages.length} ${requestedPages.length === 1 ? "page" : "pages"}`);
|
|
193
|
+
if (requestedEmails.length > 0) parts.push(`${requestedEmails.length} ${requestedEmails.length === 1 ? "email" : "emails"}`);
|
|
194
|
+
process.stdout.write(chalk.dim(`${parts.join(", ")} → ${path.relative(userCwd, outDir)}/\n`));
|
|
133
195
|
}
|
|
134
196
|
//#endregion
|
|
135
|
-
export { buildPage, exportPages };
|
|
197
|
+
export { buildPage, exportEmails, exportPages };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Plural, t as Locale } from "./types-
|
|
2
|
-
import { t as OpenPagesConfig } from "./config-
|
|
1
|
+
import { n as Plural, t as Locale } from "./types-B-9WXL93.js";
|
|
2
|
+
import { t as OpenPagesConfig } from "./config-IkwICvOe.js";
|
|
3
3
|
import { ComponentType } from "react";
|
|
4
4
|
//#region src/app/lib/design.d.ts
|
|
5
5
|
type DesignPalette = {
|
|
@@ -50,5 +50,25 @@ type PageModule = {
|
|
|
50
50
|
meta?: PageMeta;
|
|
51
51
|
design?: DesignSystem;
|
|
52
52
|
};
|
|
53
|
+
type EmailMeta = {
|
|
54
|
+
/** Display name in the workspace. Default: the folder name. */
|
|
55
|
+
title?: string;
|
|
56
|
+
/** Subject line the email is meant to be sent with. */
|
|
57
|
+
subject?: string;
|
|
58
|
+
/** What the email is for; shown on the workspace card. */
|
|
59
|
+
description?: string;
|
|
60
|
+
/** ISO 8601 timestamp. Set once at scaffold time; used to sort the email list. */
|
|
61
|
+
createdAt?: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* An email is one react-email component rendered on the server to a single
|
|
65
|
+
* HTML document with inlined styles, plus a plain-text alternative derived
|
|
66
|
+
* from it. It never runs in a browser: no hooks with effects, no state, no
|
|
67
|
+
* browser APIs.
|
|
68
|
+
*/
|
|
69
|
+
type EmailModule = {
|
|
70
|
+
default: ComponentType;
|
|
71
|
+
meta?: EmailMeta;
|
|
72
|
+
};
|
|
53
73
|
//#endregion
|
|
54
|
-
export type { DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, Locale, OpenPagesConfig, PageComponent, PageKind, PageMeta, PageModule, Plural };
|
|
74
|
+
export type { DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, EmailMeta, EmailModule, Locale, OpenPagesConfig, PageComponent, PageKind, PageMeta, PageModule, Plural };
|
package/dist/locale/index.d.ts
CHANGED
package/dist/locale/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const en = {
|
|
|
22
22
|
duplicate: "Duplicate",
|
|
23
23
|
themes: "Themes",
|
|
24
24
|
assets: "Assets",
|
|
25
|
+
emails: "Emails",
|
|
25
26
|
folders: "Folders",
|
|
26
27
|
pages: "Pages",
|
|
27
28
|
menu: "Menu",
|
|
@@ -190,6 +191,7 @@ const en = {
|
|
|
190
191
|
placeholder: "Search pages or run a command…",
|
|
191
192
|
empty: "No results.",
|
|
192
193
|
groupDocs: "Pages",
|
|
194
|
+
groupEmails: "Emails",
|
|
193
195
|
groupFolders: "Folders",
|
|
194
196
|
groupNavigation: "Go to",
|
|
195
197
|
groupAppearance: "Appearance",
|
|
@@ -207,6 +209,28 @@ const en = {
|
|
|
207
209
|
dark: "Dark",
|
|
208
210
|
system: "System"
|
|
209
211
|
},
|
|
212
|
+
emails: {
|
|
213
|
+
title: "Emails",
|
|
214
|
+
noEmailsTitle: "No emails yet",
|
|
215
|
+
noEmailsHintPrefix: "Run ",
|
|
216
|
+
noEmailsHintSuffix: " in your agent to scaffold one under emails/.",
|
|
217
|
+
subjectLabel: "Subject",
|
|
218
|
+
noSubject: "No subject set",
|
|
219
|
+
openEmailAria: "Open email {name}",
|
|
220
|
+
backToEmails: "Back to emails",
|
|
221
|
+
viewHtml: "HTML",
|
|
222
|
+
viewText: "Text",
|
|
223
|
+
viewportDesktop: "Desktop",
|
|
224
|
+
viewportMobile: "Mobile",
|
|
225
|
+
copyHtml: "Copy HTML",
|
|
226
|
+
copiedHtml: "HTML copied",
|
|
227
|
+
copyHtmlFailed: "Could not copy the HTML",
|
|
228
|
+
reload: "Reload email",
|
|
229
|
+
openInTab: "Open the rendered email by itself in a new tab",
|
|
230
|
+
notFound: "Email not found: {id}",
|
|
231
|
+
unresolvedCount: "{count} classes did not compile",
|
|
232
|
+
unresolvedTitle: "Tailwind utilities the email compiler could not inline (check components/email/email-theme.ts):"
|
|
233
|
+
},
|
|
210
234
|
themes: {
|
|
211
235
|
title: "Themes",
|
|
212
236
|
noThemesTitle: "No themes yet",
|
|
@@ -259,6 +283,7 @@ const ja = {
|
|
|
259
283
|
duplicate: "複製",
|
|
260
284
|
themes: "テーマ",
|
|
261
285
|
assets: "アセット",
|
|
286
|
+
emails: "メール",
|
|
262
287
|
folders: "フォルダ",
|
|
263
288
|
pages: "スライド",
|
|
264
289
|
menu: "メニュー",
|
|
@@ -427,6 +452,7 @@ const ja = {
|
|
|
427
452
|
placeholder: "スライドを検索、またはコマンドを実行…",
|
|
428
453
|
empty: "結果がありません。",
|
|
429
454
|
groupDocs: "スライド",
|
|
455
|
+
groupEmails: "メール",
|
|
430
456
|
groupFolders: "フォルダ",
|
|
431
457
|
groupNavigation: "移動",
|
|
432
458
|
groupAppearance: "外観",
|
|
@@ -444,6 +470,28 @@ const ja = {
|
|
|
444
470
|
dark: "ダーク",
|
|
445
471
|
system: "システム"
|
|
446
472
|
},
|
|
473
|
+
emails: {
|
|
474
|
+
title: "メール",
|
|
475
|
+
noEmailsTitle: "メールはまだありません",
|
|
476
|
+
noEmailsHintPrefix: "エージェントで ",
|
|
477
|
+
noEmailsHintSuffix: " を実行すると emails/ に作成されます。",
|
|
478
|
+
subjectLabel: "件名",
|
|
479
|
+
noSubject: "件名が未設定",
|
|
480
|
+
openEmailAria: "メール {name} を開く",
|
|
481
|
+
backToEmails: "メール一覧へ戻る",
|
|
482
|
+
viewHtml: "HTML",
|
|
483
|
+
viewText: "テキスト",
|
|
484
|
+
viewportDesktop: "デスクトップ",
|
|
485
|
+
viewportMobile: "モバイル",
|
|
486
|
+
copyHtml: "HTML をコピー",
|
|
487
|
+
copiedHtml: "HTML をコピーしました",
|
|
488
|
+
copyHtmlFailed: "HTML をコピーできませんでした",
|
|
489
|
+
reload: "メールを再読み込み",
|
|
490
|
+
openInTab: "レンダリングされたメールを新しいタブで開く",
|
|
491
|
+
notFound: "メールが見つかりません: {id}",
|
|
492
|
+
unresolvedCount: "{count} 個のクラスが変換されませんでした",
|
|
493
|
+
unresolvedTitle: "メールコンパイラがインライン化できなかった Tailwind ユーティリティ (components/email/email-theme.ts を確認):"
|
|
494
|
+
},
|
|
447
495
|
themes: {
|
|
448
496
|
title: "テーマ",
|
|
449
497
|
noThemesTitle: "テーマがまだありません",
|
|
@@ -485,6 +533,7 @@ const zhCN = {
|
|
|
485
533
|
duplicate: "复制",
|
|
486
534
|
themes: "主题",
|
|
487
535
|
assets: "素材",
|
|
536
|
+
emails: "邮件",
|
|
488
537
|
folders: "頁面夹",
|
|
489
538
|
pages: "幻灯片",
|
|
490
539
|
menu: "菜单",
|
|
@@ -653,6 +702,7 @@ const zhCN = {
|
|
|
653
702
|
placeholder: "搜索幻灯片或执行命令…",
|
|
654
703
|
empty: "没有匹配的结果。",
|
|
655
704
|
groupDocs: "幻灯片",
|
|
705
|
+
groupEmails: "邮件",
|
|
656
706
|
groupFolders: "頁面夹",
|
|
657
707
|
groupNavigation: "前往",
|
|
658
708
|
groupAppearance: "外观",
|
|
@@ -670,6 +720,28 @@ const zhCN = {
|
|
|
670
720
|
dark: "深色",
|
|
671
721
|
system: "系统"
|
|
672
722
|
},
|
|
723
|
+
emails: {
|
|
724
|
+
title: "邮件",
|
|
725
|
+
noEmailsTitle: "还没有邮件",
|
|
726
|
+
noEmailsHintPrefix: "在你的代理中运行 ",
|
|
727
|
+
noEmailsHintSuffix: " 即可在 emails/ 下创建一封。",
|
|
728
|
+
subjectLabel: "主题",
|
|
729
|
+
noSubject: "未设置主题",
|
|
730
|
+
openEmailAria: "打开邮件 {name}",
|
|
731
|
+
backToEmails: "返回邮件列表",
|
|
732
|
+
viewHtml: "HTML",
|
|
733
|
+
viewText: "纯文本",
|
|
734
|
+
viewportDesktop: "桌面",
|
|
735
|
+
viewportMobile: "移动端",
|
|
736
|
+
copyHtml: "复制 HTML",
|
|
737
|
+
copiedHtml: "已复制 HTML",
|
|
738
|
+
copyHtmlFailed: "无法复制 HTML",
|
|
739
|
+
reload: "重新加载邮件",
|
|
740
|
+
openInTab: "在新标签页中单独打开渲染后的邮件",
|
|
741
|
+
notFound: "找不到邮件: {id}",
|
|
742
|
+
unresolvedCount: "{count} 个类未能编译",
|
|
743
|
+
unresolvedTitle: "邮件编译器无法内联的 Tailwind 工具类 (请检查 components/email/email-theme.ts):"
|
|
744
|
+
},
|
|
673
745
|
themes: {
|
|
674
746
|
title: "主题",
|
|
675
747
|
noThemesTitle: "尚无主题",
|
|
@@ -711,6 +783,7 @@ const zhTW = {
|
|
|
711
783
|
duplicate: "複製",
|
|
712
784
|
themes: "主題",
|
|
713
785
|
assets: "素材",
|
|
786
|
+
emails: "郵件",
|
|
714
787
|
folders: "資料夾",
|
|
715
788
|
pages: "投影片",
|
|
716
789
|
menu: "選單",
|
|
@@ -879,6 +952,7 @@ const zhTW = {
|
|
|
879
952
|
placeholder: "搜尋簡報或執行指令…",
|
|
880
953
|
empty: "沒有符合的結果。",
|
|
881
954
|
groupDocs: "簡報",
|
|
955
|
+
groupEmails: "郵件",
|
|
882
956
|
groupFolders: "資料夾",
|
|
883
957
|
groupNavigation: "前往",
|
|
884
958
|
groupAppearance: "外觀",
|
|
@@ -896,6 +970,28 @@ const zhTW = {
|
|
|
896
970
|
dark: "深色",
|
|
897
971
|
system: "系統"
|
|
898
972
|
},
|
|
973
|
+
emails: {
|
|
974
|
+
title: "郵件",
|
|
975
|
+
noEmailsTitle: "還沒有郵件",
|
|
976
|
+
noEmailsHintPrefix: "在你的代理中執行 ",
|
|
977
|
+
noEmailsHintSuffix: " 即可在 emails/ 下建立一封。",
|
|
978
|
+
subjectLabel: "主旨",
|
|
979
|
+
noSubject: "未設定主旨",
|
|
980
|
+
openEmailAria: "開啟郵件 {name}",
|
|
981
|
+
backToEmails: "返回郵件列表",
|
|
982
|
+
viewHtml: "HTML",
|
|
983
|
+
viewText: "純文字",
|
|
984
|
+
viewportDesktop: "桌面",
|
|
985
|
+
viewportMobile: "行動裝置",
|
|
986
|
+
copyHtml: "複製 HTML",
|
|
987
|
+
copiedHtml: "已複製 HTML",
|
|
988
|
+
copyHtmlFailed: "無法複製 HTML",
|
|
989
|
+
reload: "重新載入郵件",
|
|
990
|
+
openInTab: "在新分頁中單獨開啟渲染後的郵件",
|
|
991
|
+
notFound: "找不到郵件: {id}",
|
|
992
|
+
unresolvedCount: "{count} 個類別未能編譯",
|
|
993
|
+
unresolvedTitle: "郵件編譯器無法內聯的 Tailwind 工具類別 (請檢查 components/email/email-theme.ts):"
|
|
994
|
+
},
|
|
899
995
|
themes: {
|
|
900
996
|
title: "主題",
|
|
901
997
|
noThemesTitle: "尚無主題",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { t as createViteConfig } from "./config-CYLBzMMb.js";
|
|
1
2
|
import { a as printUrls, i as printShortcutsHint, o as shortcutsEnabled, r as printHeader, s as startupDuration, t as createCliLogger } from "./cli/bin.js";
|
|
2
|
-
import { t as createViteConfig } from "./config-D_U_aCQz.js";
|
|
3
3
|
import { mergeConfig, preview as preview$1 } from "vite";
|
|
4
4
|
//#region src/cli/preview.ts
|
|
5
5
|
async function preview(opts = {}) {
|
|
@@ -26,6 +26,7 @@ type Locale = {
|
|
|
26
26
|
duplicate: string;
|
|
27
27
|
themes: string;
|
|
28
28
|
assets: string;
|
|
29
|
+
emails: string;
|
|
29
30
|
folders: string;
|
|
30
31
|
pages: string;
|
|
31
32
|
menu: string;
|
|
@@ -195,6 +196,7 @@ type Locale = {
|
|
|
195
196
|
placeholder: string;
|
|
196
197
|
empty: string;
|
|
197
198
|
groupDocs: string;
|
|
199
|
+
groupEmails: string;
|
|
198
200
|
groupFolders: string;
|
|
199
201
|
groupNavigation: string;
|
|
200
202
|
groupAppearance: string;
|
|
@@ -212,6 +214,28 @@ type Locale = {
|
|
|
212
214
|
dark: string;
|
|
213
215
|
system: string;
|
|
214
216
|
};
|
|
217
|
+
emails: {
|
|
218
|
+
title: string;
|
|
219
|
+
noEmailsTitle: string;
|
|
220
|
+
noEmailsHintPrefix: string;
|
|
221
|
+
noEmailsHintSuffix: string;
|
|
222
|
+
subjectLabel: string;
|
|
223
|
+
noSubject: string;
|
|
224
|
+
openEmailAria: string;
|
|
225
|
+
backToEmails: string;
|
|
226
|
+
viewHtml: string;
|
|
227
|
+
viewText: string;
|
|
228
|
+
viewportDesktop: string;
|
|
229
|
+
viewportMobile: string;
|
|
230
|
+
copyHtml: string;
|
|
231
|
+
copiedHtml: string;
|
|
232
|
+
copyHtmlFailed: string;
|
|
233
|
+
reload: string;
|
|
234
|
+
openInTab: string;
|
|
235
|
+
notFound: string;
|
|
236
|
+
unresolvedCount: string;
|
|
237
|
+
unresolvedTitle: string;
|
|
238
|
+
};
|
|
215
239
|
themes: {
|
|
216
240
|
title: string;
|
|
217
241
|
noThemesTitle: string;
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createViteConfig } from "../config-
|
|
1
|
+
import { t as createViteConfig } from "../config-CYLBzMMb.js";
|
|
2
2
|
export { createViteConfig };
|
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,7 +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?
|
|
59
|
-
- 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.
|
|
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.)
|
|
60
62
|
|
|
61
63
|
7. **Report.**
|
|
62
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.
|