@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.
Files changed (44) hide show
  1. package/dist/{build-CCoxDxL-.js → build-Cpod-K9R.js} +9 -4
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-D_U_aCQz.js → config-CYLBzMMb.js} +1077 -18
  4. package/dist/{config-p-TWNXbv.d.ts → config-IkwICvOe.d.ts} +2 -1
  5. package/dist/{dev-CTrB263u.js → dev-B7x-G-wk.js} +1 -1
  6. package/dist/{export-enKL9R7x.js → export-BxMksWEc.js} +74 -12
  7. package/dist/index.d.ts +23 -3
  8. package/dist/locale/index.d.ts +1 -1
  9. package/dist/locale/index.js +96 -0
  10. package/dist/{preview-Dmzyd1cf.js → preview-CMv1lGOC.js} +1 -1
  11. package/dist/{types-DDdUFUIS.d.ts → types-B-9WXL93.d.ts} +24 -0
  12. package/dist/vite/index.d.ts +1 -1
  13. package/dist/vite/index.js +1 -1
  14. package/package.json +1 -1
  15. package/skills/apply-comments/SKILL.md +6 -4
  16. package/skills/create-email/SKILL.md +166 -0
  17. package/skills/create-theme/SKILL.md +31 -7
  18. package/skills/create-theme/references/email-theme-from-css.mjs +185 -0
  19. package/skills/create-theme/references/email-theme.md +48 -0
  20. package/skills/current-page/SKILL.md +3 -3
  21. package/src/app/app.tsx +4 -0
  22. package/src/app/components/command/home-command-menu.tsx +19 -2
  23. package/src/app/components/selection-note.tsx +71 -0
  24. package/src/app/components/sidebar/folder-item.tsx +9 -2
  25. package/src/app/components/sidebar/sidebar.tsx +10 -0
  26. package/src/app/frame/email-inspect.ts +17 -0
  27. package/src/app/lib/emails.ts +17 -0
  28. package/src/app/lib/frame.ts +15 -4
  29. package/src/app/lib/page-thumb.tsx +11 -3
  30. package/src/app/lib/sdk.ts +22 -0
  31. package/src/app/routes/email.tsx +424 -0
  32. package/src/app/routes/emails.tsx +94 -0
  33. package/src/app/routes/home-shell.tsx +21 -2
  34. package/src/app/routes/page.tsx +9 -50
  35. package/src/app/virtual.d.ts +10 -0
  36. package/src/locale/en.ts +25 -0
  37. package/src/locale/ja.ts +25 -0
  38. package/src/locale/types.ts +24 -0
  39. package/src/locale/zh-cn.ts +25 -0
  40. package/src/locale/zh-tw.ts +25 -0
  41. package/workspace/components.json +3 -1
  42. package/workspace/styles/globals.css +8 -2
  43. package/dist/open-pages-plugin-C8cFhMB9.js +0 -662
  44. package/dist/version-DZrXLoAD.js +0 -42
@@ -1,4 +1,4 @@
1
- import { t as Locale } from "./types-DDdUFUIS.js";
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 loadUserConfig, n as findPages, t as extractMeta } from "./open-pages-plugin-C8cFhMB9.js";
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, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
@@ -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 idsOnDisk = entries.map((e) => e.id);
109
- if (idsOnDisk.length === 0) throw new Error(`No pages found under ${pagesDir}/`);
110
- const requested = opts.pages && opts.pages.length > 0 ? opts.pages : idsOnDisk;
111
- const unknown = requested.filter((id) => !idsOnDisk.includes(id));
112
- if (unknown.length > 0) throw new Error(`Page not found: ${unknown.join(", ")} (available: ${idsOnDisk.join(", ")})`);
113
- const { readCoreVersion } = await import("./version-DZrXLoAD.js").then((n) => n.i);
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 requested) {
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
- const n = requested.length;
132
- process.stdout.write(chalk.dim(`${n} ${n === 1 ? "page" : "pages"} → ${path.relative(userCwd, outDir)}/\n`));
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-DDdUFUIS.js";
2
- import { t as OpenPagesConfig } from "./config-p-TWNXbv.js";
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 };
@@ -1,4 +1,4 @@
1
- import { n as Plural, t as Locale } from "../types-DDdUFUIS.js";
1
+ import { n as Plural, t as Locale } from "../types-B-9WXL93.js";
2
2
  //#region src/locale/en.d.ts
3
3
  export declare const en: Locale;
4
4
  //#endregion
@@ -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;
@@ -1,4 +1,4 @@
1
- import { t as OpenPagesConfig } from "../config-p-TWNXbv.js";
1
+ import { t as OpenPagesConfig } from "../config-IkwICvOe.js";
2
2
  import { InlineConfig } from "vite";
3
3
  //#region src/vite/config.d.ts
4
4
  type CreateViteConfigOptions = {
@@ -1,2 +1,2 @@
1
- import { t as createViteConfig } from "../config-D_U_aCQz.js";
1
+ import { t as createViteConfig } from "../config-CYLBzMMb.js";
2
2
  export { createViteConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autono/open-pages",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Runtime and CLI for open-pages — live preview of React and HTML pages, click-to-comment inspector, and static export.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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` (or its `components/*.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 page's source — usually `pages/<pageId>/index.tsx`, occasionally a file under `pages/<pageId>/components/`.
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 `pages/*/components/*.tsx`. Process each page one at a time.
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.