@sliiu/present-themes 0.0.0-stage → 0.1.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/COPYRIGHT +5 -0
- package/LICENSE +661 -0
- package/THIRD-PARTY-NOTICES.md +221 -0
- package/dist/cjs/builtin-ids.js +19 -0
- package/dist/cjs/decorations.js +303 -0
- package/dist/cjs/fonts/inject.js +160 -0
- package/dist/cjs/gallery-order.js +19 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/qa/color.js +98 -0
- package/dist/cjs/template-masters.js +882 -0
- package/dist/cjs/theme-commands.js +290 -0
- package/dist/cjs/themes.js +742 -0
- package/dist/esm/builtin-ids.js +16 -0
- package/dist/esm/decorations.js +300 -0
- package/dist/esm/fonts/inject.js +154 -0
- package/dist/esm/gallery-order.js +16 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/qa/color.js +88 -0
- package/dist/esm/template-masters.js +874 -0
- package/dist/esm/theme-commands.js +285 -0
- package/dist/esm/themes.js +736 -0
- package/dist/types/builtin-ids.d.ts +3 -0
- package/dist/types/decorations.d.ts +18 -0
- package/dist/types/fonts/inject.d.ts +27 -0
- package/dist/types/gallery-order.d.ts +3 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/qa/color.d.ts +27 -0
- package/dist/types/template-masters.d.ts +18 -0
- package/dist/types/theme-commands.d.ts +41 -0
- package/dist/types/themes.d.ts +276 -0
- package/package.json +27 -4
- package/README.md +0 -3
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// 内置主题:色板 + 字体 + 装饰 + 默认背景。
|
|
2
|
+
//
|
|
3
|
+
// 设计要点:
|
|
4
|
+
// 1. 主题是 compile 阶段使用的"上下文",layout 内部通过 ThemeContext 取色和字号。
|
|
5
|
+
// 2. 目前 13 套,覆盖主流场景(商务/学术/创意/极简/暗黑/科技/自然/暖色/杂志/终端/
|
|
6
|
+
// 粉彩/高奢/国风);扩展只需新增条目,并同步 schema 的 BUILTIN_THEME_IDS +
|
|
7
|
+
// 本文件的 THEME_OUTLINE_HINTS(有编译/运行期断言守护)。
|
|
8
|
+
// 3. 色板字段名直接复用 OOXML 主题色语义(accent1/accent2/.../bg/text),
|
|
9
|
+
// 未来如果接入 master theme 序列化可以零成本对接。
|
|
10
|
+
// 4. fontFamily 使用 web-safe + CJK 兼容栈,避免 PPT 渲染缺字。
|
|
11
|
+
import { BUILTIN_THEME_IDS } from "./builtin-ids.js";
|
|
12
|
+
import { THEME_FONT_POOL } from "./fonts/inject.js";
|
|
13
|
+
// 字体 fallback 链统一原则:拉丁字 → 简体中文 → 系统兜底。
|
|
14
|
+
const FONT_MONO = '"JetBrains Mono", "Menlo", "Consolas", "Noto Sans Mono CJK SC", monospace';
|
|
15
|
+
// CJK 兜底链:所有内置主题的拉丁字体都补一段中文回退,避免缺字。
|
|
16
|
+
const CJK_TAIL = '"PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"';
|
|
17
|
+
// AI 生成主题在字体 preset 缺失时的中性兜底栈。
|
|
18
|
+
const FONT_SANS = `"Inter", -apple-system, BlinkMacSystemFont, ${CJK_TAIL}, sans-serif`;
|
|
19
|
+
const FONT_DISPLAY = `"Space Grotesk", "Inter", ${CJK_TAIL}, sans-serif`;
|
|
20
|
+
/**
|
|
21
|
+
* 从 THEME_FONT_POOL 取某字体的 CSS stack(含 CJK 兜底)。找不到回退到中性 sans。
|
|
22
|
+
* 内置主题改用 pool 字体(每条都带 Google Fonts cssUrl),保证"字体真的随主题加载",
|
|
23
|
+
* 而不是落到系统默认字形 → 这是之前"字体不随主题"的根因之一。
|
|
24
|
+
*/
|
|
25
|
+
function poolStack(family) {
|
|
26
|
+
const p = THEME_FONT_POOL.find((x) => x.family === family);
|
|
27
|
+
if (!p)
|
|
28
|
+
return `-apple-system, BlinkMacSystemFont, ${CJK_TAIL}, sans-serif`;
|
|
29
|
+
// pool.stack 已含 fallback;display/serif 仍补 CJK 兜底(pool 的 latin 字体未必含中文回退)。
|
|
30
|
+
return p.language === "cjk" ? p.stack : `${p.stack.replace(/, (sans-serif|serif|monospace)$/, "")}, ${CJK_TAIL}, ${p.category === "serif" ? "serif" : "sans-serif"}`;
|
|
31
|
+
}
|
|
32
|
+
/** 组装某组字体 family 的 Google Fonts cssLinks(去重、跳过 pool 里没有的)。 */
|
|
33
|
+
function poolLinks(...families) {
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const f of families) {
|
|
37
|
+
if (seen.has(f))
|
|
38
|
+
continue;
|
|
39
|
+
seen.add(f);
|
|
40
|
+
const p = THEME_FONT_POOL.find((x) => x.family === f);
|
|
41
|
+
if (p)
|
|
42
|
+
out.push({ family: p.family, cssUrl: p.cssUrl });
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
const DEFAULT_METRICS = {
|
|
47
|
+
marginX: 0.06,
|
|
48
|
+
marginY: 0.08,
|
|
49
|
+
titleBodyGap: 24,
|
|
50
|
+
bulletLineHeight: 1.5,
|
|
51
|
+
};
|
|
52
|
+
const DEFAULT_FONT_SIZES = {
|
|
53
|
+
// 与 AI compile 封面路径的 coverTitle×1.25 对齐;旧默认 56/24 在 1280×720 上偏小。
|
|
54
|
+
coverTitle: 72,
|
|
55
|
+
coverSubtitle: 30,
|
|
56
|
+
sectionTitle: 44,
|
|
57
|
+
pageTitle: 32,
|
|
58
|
+
body: 18,
|
|
59
|
+
small: 14,
|
|
60
|
+
quote: 26,
|
|
61
|
+
};
|
|
62
|
+
export const THEMES = {
|
|
63
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
64
|
+
// Atelier 系列(v2 全量换血):13 套内置主题按新艺术方向重新设计。
|
|
65
|
+
// 定位:**手动指定 / 主题画廊 / AI 生成失败的兜底** —— 默认生成路径交给
|
|
66
|
+
// LLM 现场设计(generateTheme / generateLayoutStyle),不参考这些 preset。
|
|
67
|
+
// id 保持不变(i18n / 动画 bundle 推断 / 模板接线依赖 id 语义分类)。
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
// ── business · Arctic Signal ── 冰白精密 + 信号红,工程报告的克制权威
|
|
70
|
+
business: {
|
|
71
|
+
id: "business",
|
|
72
|
+
name: "Arctic Signal",
|
|
73
|
+
description: "冰白精密 · 信号红锚点 · Manrope 冷静排印",
|
|
74
|
+
palette: {
|
|
75
|
+
bg: "#f2f5f8",
|
|
76
|
+
bgAlt: "#e4eaf1",
|
|
77
|
+
text: "#0c1420",
|
|
78
|
+
textMuted: "#4d5f74",
|
|
79
|
+
accent1: "#d91f30",
|
|
80
|
+
accent2: "#33506d",
|
|
81
|
+
accent3: "#7e93a9",
|
|
82
|
+
accent4: "#0e7490",
|
|
83
|
+
divider: "#d3dce6",
|
|
84
|
+
},
|
|
85
|
+
fonts: { title: poolStack("Manrope"), body: poolStack("Inter"), mono: FONT_MONO },
|
|
86
|
+
fontSizes: DEFAULT_FONT_SIZES,
|
|
87
|
+
metrics: DEFAULT_METRICS,
|
|
88
|
+
decoration: "authority",
|
|
89
|
+
titleWordArt: { color: "#0c1420", weight: 800, letterSpacing: -0.5 },
|
|
90
|
+
fontCssLinks: poolLinks("Manrope", "Inter"),
|
|
91
|
+
},
|
|
92
|
+
// ── academic · Sandstone ── 暖砂岩纸面 + 焦赭/深青,出版级书卷气
|
|
93
|
+
academic: {
|
|
94
|
+
id: "academic",
|
|
95
|
+
name: "Sandstone",
|
|
96
|
+
description: "暖砂岩 · 焦赭深青 · Fraunces 光学衬线",
|
|
97
|
+
palette: {
|
|
98
|
+
bg: "#f6efe4",
|
|
99
|
+
bgAlt: "#ecdfc9",
|
|
100
|
+
text: "#241a10",
|
|
101
|
+
textMuted: "#6f5b41",
|
|
102
|
+
accent1: "#a2451c",
|
|
103
|
+
accent2: "#155e63",
|
|
104
|
+
accent3: "#8c6d1f",
|
|
105
|
+
accent4: "#1e3a8a",
|
|
106
|
+
divider: "#dccdb4",
|
|
107
|
+
},
|
|
108
|
+
fonts: { title: poolStack("Fraunces"), body: poolStack("Newsreader"), mono: FONT_MONO },
|
|
109
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 77, body: 19 },
|
|
110
|
+
metrics: DEFAULT_METRICS,
|
|
111
|
+
decoration: "manuscript",
|
|
112
|
+
titleWordArt: { color: "#241a10", weight: 900, letterSpacing: -0.5 },
|
|
113
|
+
fontCssLinks: poolLinks("Fraunces", "Newsreader"),
|
|
114
|
+
},
|
|
115
|
+
// ── creative · Violet Hour ── 暮色靛底,热粉→紫的电影感消费品牌
|
|
116
|
+
creative: {
|
|
117
|
+
id: "creative",
|
|
118
|
+
name: "Violet Hour",
|
|
119
|
+
description: "暮色靛紫 · 热粉电光 · Bricolage 圆润 display",
|
|
120
|
+
palette: {
|
|
121
|
+
bg: "#100829",
|
|
122
|
+
bgAlt: "#1e1145",
|
|
123
|
+
text: "#f6f2ff",
|
|
124
|
+
textMuted: "#c4b1f0",
|
|
125
|
+
accent1: "#ff2e92",
|
|
126
|
+
accent2: "#8b5cf6",
|
|
127
|
+
accent3: "#22d3ee",
|
|
128
|
+
accent4: "#fbbf24",
|
|
129
|
+
divider: "#3b2a6b",
|
|
130
|
+
},
|
|
131
|
+
fonts: { title: poolStack("Bricolage Grotesque"), body: poolStack("Plus Jakarta Sans"), mono: FONT_MONO },
|
|
132
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 77 },
|
|
133
|
+
metrics: DEFAULT_METRICS,
|
|
134
|
+
layout: "centered",
|
|
135
|
+
decoration: "neon-burst",
|
|
136
|
+
titleWordArt: {
|
|
137
|
+
color: "#f6f2ff",
|
|
138
|
+
weight: 800,
|
|
139
|
+
letterSpacing: -0.5,
|
|
140
|
+
shadow: { color: "#ff2e92", alpha: 0.5, blur: 24, dx: 0, dy: 0 },
|
|
141
|
+
},
|
|
142
|
+
backgroundPaint: {
|
|
143
|
+
kind: "linear",
|
|
144
|
+
angle: 145,
|
|
145
|
+
stops: [
|
|
146
|
+
{ offset: 0, color: "#241055" },
|
|
147
|
+
{ offset: 0.55, color: "#100829" },
|
|
148
|
+
{ offset: 1, color: "#0a0620" },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
fontCssLinks: poolLinks("Bricolage Grotesque", "Plus Jakarta Sans"),
|
|
152
|
+
},
|
|
153
|
+
// ── minimal · Gallery ── 画廊暖白 + 单一克莱因蓝锚点,美术馆式留白
|
|
154
|
+
minimal: {
|
|
155
|
+
id: "minimal",
|
|
156
|
+
name: "Gallery",
|
|
157
|
+
description: "画廊暖白 · 克莱因蓝单锚 · Inter 极简排印",
|
|
158
|
+
palette: {
|
|
159
|
+
bg: "#fcfcfb",
|
|
160
|
+
bgAlt: "#f2f1ee",
|
|
161
|
+
text: "#111110",
|
|
162
|
+
textMuted: "#6e6c66",
|
|
163
|
+
accent1: "#002fa7",
|
|
164
|
+
accent2: "#111110",
|
|
165
|
+
accent3: "#a6a49c",
|
|
166
|
+
accent4: "#e0301e",
|
|
167
|
+
divider: "#e7e5e0",
|
|
168
|
+
},
|
|
169
|
+
fonts: { title: poolStack("Inter"), body: poolStack("Inter"), mono: FONT_MONO },
|
|
170
|
+
fontSizes: DEFAULT_FONT_SIZES,
|
|
171
|
+
metrics: { ...DEFAULT_METRICS, marginX: 0.1, marginY: 0.12 },
|
|
172
|
+
decoration: "ink-thread",
|
|
173
|
+
titleWordArt: { color: "#111110", weight: 600, letterSpacing: -0.5 },
|
|
174
|
+
fontCssLinks: poolLinks("Inter"),
|
|
175
|
+
},
|
|
176
|
+
// ── dark · Midnight Gold ── 真午夜蓝黑 + 香槟金,颁奖礼级发布会
|
|
177
|
+
dark: {
|
|
178
|
+
id: "dark",
|
|
179
|
+
name: "Midnight Gold",
|
|
180
|
+
description: "午夜深蓝 · 香槟金辉 · Sora 发光标题",
|
|
181
|
+
palette: {
|
|
182
|
+
bg: "#0a0c14",
|
|
183
|
+
bgAlt: "#141827",
|
|
184
|
+
text: "#f5f1e8",
|
|
185
|
+
textMuted: "#9aa0b5",
|
|
186
|
+
accent1: "#e0b458",
|
|
187
|
+
accent2: "#8fae8b",
|
|
188
|
+
accent3: "#6b7280",
|
|
189
|
+
accent4: "#38bdf8",
|
|
190
|
+
divider: "#242a3c",
|
|
191
|
+
},
|
|
192
|
+
fonts: { title: poolStack("Sora"), body: poolStack("Inter"), mono: FONT_MONO },
|
|
193
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 77 },
|
|
194
|
+
metrics: DEFAULT_METRICS,
|
|
195
|
+
contentPanel: true,
|
|
196
|
+
decoration: "aurora",
|
|
197
|
+
titleWordArt: {
|
|
198
|
+
color: "#f5f1e8",
|
|
199
|
+
weight: 800,
|
|
200
|
+
letterSpacing: -0.5,
|
|
201
|
+
shadow: { color: "#e0b458", alpha: 0.45, blur: 26, dx: 0, dy: 0 },
|
|
202
|
+
},
|
|
203
|
+
backgroundPaint: {
|
|
204
|
+
kind: "radial",
|
|
205
|
+
path: "circle",
|
|
206
|
+
fillToRect: { l: 0, t: 0, r: 1, b: 1 },
|
|
207
|
+
stops: [
|
|
208
|
+
{ offset: 0, color: "#161c33" },
|
|
209
|
+
{ offset: 0.55, color: "#0a0c14" },
|
|
210
|
+
{ offset: 1, color: "#05060b" },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
fontCssLinks: poolLinks("Sora", "Inter"),
|
|
214
|
+
},
|
|
215
|
+
// ── tech · Graphite Acid ── 石墨炭 + 酸性青柠,开发者/数据的高压感
|
|
216
|
+
tech: {
|
|
217
|
+
id: "tech",
|
|
218
|
+
name: "Graphite Acid",
|
|
219
|
+
description: "石墨炭底 · 酸性青柠 · Space Grotesk 工程排印",
|
|
220
|
+
palette: {
|
|
221
|
+
bg: "#141518",
|
|
222
|
+
bgAlt: "#1e2024",
|
|
223
|
+
text: "#eef1ec",
|
|
224
|
+
textMuted: "#a2aca0",
|
|
225
|
+
accent1: "#c3f53c",
|
|
226
|
+
accent2: "#22d3ee",
|
|
227
|
+
accent3: "#8b8f96",
|
|
228
|
+
accent4: "#f472b6",
|
|
229
|
+
divider: "#2a2d33",
|
|
230
|
+
},
|
|
231
|
+
fonts: { title: poolStack("Space Grotesk"), body: poolStack("Inter"), mono: poolStack("Space Mono") },
|
|
232
|
+
fontSizes: DEFAULT_FONT_SIZES,
|
|
233
|
+
metrics: DEFAULT_METRICS,
|
|
234
|
+
contentPanel: true,
|
|
235
|
+
decoration: "tech-bracket",
|
|
236
|
+
titleWordArt: {
|
|
237
|
+
color: "#eef1ec",
|
|
238
|
+
weight: 700,
|
|
239
|
+
letterSpacing: 0,
|
|
240
|
+
shadow: { color: "#c3f53c", alpha: 0.4, blur: 18, dx: 0, dy: 0 },
|
|
241
|
+
},
|
|
242
|
+
backgroundPaint: {
|
|
243
|
+
kind: "linear",
|
|
244
|
+
angle: 150,
|
|
245
|
+
stops: [
|
|
246
|
+
{ offset: 0, color: "#1b1d21" },
|
|
247
|
+
{ offset: 0.5, color: "#141518" },
|
|
248
|
+
{ offset: 1, color: "#0d0e10" },
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
fontCssLinks: poolLinks("Space Grotesk", "Inter", "Space Mono"),
|
|
252
|
+
},
|
|
253
|
+
// ── nature · Moss & Clay ── 奶油纸 + 苔藓深绿/陶土,有机高级感
|
|
254
|
+
nature: {
|
|
255
|
+
id: "nature",
|
|
256
|
+
name: "Moss & Clay",
|
|
257
|
+
description: "奶油纸面 · 苔藓陶土 · Fraunces 有机衬线",
|
|
258
|
+
palette: {
|
|
259
|
+
bg: "#f7f5ec",
|
|
260
|
+
bgAlt: "#ebe8d8",
|
|
261
|
+
text: "#1c2317",
|
|
262
|
+
textMuted: "#5c6a52",
|
|
263
|
+
accent1: "#2f5d33",
|
|
264
|
+
accent2: "#c05f33",
|
|
265
|
+
accent3: "#7ba05b",
|
|
266
|
+
accent4: "#a08118",
|
|
267
|
+
divider: "#dcd9c4",
|
|
268
|
+
},
|
|
269
|
+
fonts: { title: poolStack("Fraunces"), body: poolStack("DM Sans"), mono: FONT_MONO },
|
|
270
|
+
fontSizes: DEFAULT_FONT_SIZES,
|
|
271
|
+
metrics: DEFAULT_METRICS,
|
|
272
|
+
decoration: "organic-blobs",
|
|
273
|
+
titleWordArt: { color: "#1c2317", weight: 800, letterSpacing: -0.5 },
|
|
274
|
+
fontCssLinks: poolLinks("Fraunces", "DM Sans"),
|
|
275
|
+
},
|
|
276
|
+
// ── warm · Ember ── 陶烧暖调,余烬橙 + 酒红,人味与能量
|
|
277
|
+
warm: {
|
|
278
|
+
id: "warm",
|
|
279
|
+
name: "Ember",
|
|
280
|
+
description: "陶烧暖调 · 余烬橙酒红 · Bricolage 有力标题",
|
|
281
|
+
palette: {
|
|
282
|
+
bg: "#faf3ec",
|
|
283
|
+
bgAlt: "#f3e4d5",
|
|
284
|
+
text: "#2a1810",
|
|
285
|
+
textMuted: "#8a6650",
|
|
286
|
+
accent1: "#c93c10",
|
|
287
|
+
accent2: "#9c2b3b",
|
|
288
|
+
accent3: "#e9a13b",
|
|
289
|
+
accent4: "#4f6d7a",
|
|
290
|
+
divider: "#e6d5c3",
|
|
291
|
+
},
|
|
292
|
+
fonts: { title: poolStack("Bricolage Grotesque"), body: poolStack("DM Sans"), mono: FONT_MONO },
|
|
293
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 77 },
|
|
294
|
+
metrics: DEFAULT_METRICS,
|
|
295
|
+
decoration: "sunrise-arc",
|
|
296
|
+
titleWordArt: {
|
|
297
|
+
color: "#2a1810",
|
|
298
|
+
weight: 800,
|
|
299
|
+
letterSpacing: -0.5,
|
|
300
|
+
shadow: { color: "#c93c10", alpha: 0.25, blur: 0, dx: 3, dy: 3 },
|
|
301
|
+
},
|
|
302
|
+
fontCssLinks: poolLinks("Bricolage Grotesque", "DM Sans"),
|
|
303
|
+
},
|
|
304
|
+
// ── editorial · Ink & Ember ── 近黑墨底暗编辑部 + 余烬橙,野兽派杂志
|
|
305
|
+
editorial: {
|
|
306
|
+
id: "editorial",
|
|
307
|
+
name: "Ink & Ember",
|
|
308
|
+
description: "近黑墨底 · 余烬橙骨白 · Playfair 暗报头",
|
|
309
|
+
palette: {
|
|
310
|
+
bg: "#111013",
|
|
311
|
+
bgAlt: "#1c1a1e",
|
|
312
|
+
text: "#f4efe9",
|
|
313
|
+
textMuted: "#a39a8e",
|
|
314
|
+
accent1: "#ff5a1f",
|
|
315
|
+
accent2: "#d9c8b4",
|
|
316
|
+
accent3: "#6e675e",
|
|
317
|
+
accent4: "#3d8b8b",
|
|
318
|
+
divider: "#2b2830",
|
|
319
|
+
},
|
|
320
|
+
fonts: { title: poolStack("Playfair Display"), body: poolStack("Inter"), mono: FONT_MONO },
|
|
321
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 82, body: 19 },
|
|
322
|
+
metrics: DEFAULT_METRICS,
|
|
323
|
+
layout: "editorial",
|
|
324
|
+
decoration: "masthead",
|
|
325
|
+
titleWordArt: { color: "#f4efe9", weight: 900, letterSpacing: -1 },
|
|
326
|
+
fontCssLinks: poolLinks("Playfair Display", "Inter"),
|
|
327
|
+
},
|
|
328
|
+
// ── terminal · Phosphor ── 深 CRT 绿黑 + 磷光绿/琥珀,复古终端
|
|
329
|
+
terminal: {
|
|
330
|
+
id: "terminal",
|
|
331
|
+
name: "Phosphor",
|
|
332
|
+
description: "CRT 深绿 · 磷光琥珀 · 等宽扫描线",
|
|
333
|
+
palette: {
|
|
334
|
+
bg: "#0c1210",
|
|
335
|
+
bgAlt: "#14201b",
|
|
336
|
+
text: "#dcf5e4",
|
|
337
|
+
textMuted: "#7fa88d",
|
|
338
|
+
accent1: "#2ee88a",
|
|
339
|
+
accent2: "#ffb454",
|
|
340
|
+
accent3: "#5eead4",
|
|
341
|
+
accent4: "#f471b5",
|
|
342
|
+
divider: "#1f3329",
|
|
343
|
+
},
|
|
344
|
+
fonts: { title: poolStack("Space Mono"), body: poolStack("JetBrains Mono"), mono: poolStack("JetBrains Mono") },
|
|
345
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 67 },
|
|
346
|
+
metrics: DEFAULT_METRICS,
|
|
347
|
+
layout: "terminal",
|
|
348
|
+
coverPrompt: false,
|
|
349
|
+
coverMotif: "terminal-bar",
|
|
350
|
+
decoration: "terminal",
|
|
351
|
+
contentPanel: true,
|
|
352
|
+
titleWordArt: {
|
|
353
|
+
color: "#dcf5e4",
|
|
354
|
+
weight: 700,
|
|
355
|
+
letterSpacing: 0,
|
|
356
|
+
shadow: { color: "#2ee88a", alpha: 0.45, blur: 18, dx: 0, dy: 0 },
|
|
357
|
+
},
|
|
358
|
+
backgroundPaint: {
|
|
359
|
+
kind: "linear",
|
|
360
|
+
angle: 160,
|
|
361
|
+
stops: [
|
|
362
|
+
{ offset: 0, color: "#101a15" },
|
|
363
|
+
{ offset: 0.6, color: "#0c1210" },
|
|
364
|
+
{ offset: 1, color: "#070b08" },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
fontCssLinks: poolLinks("Space Mono", "JetBrains Mono"),
|
|
368
|
+
},
|
|
369
|
+
// ── pastel · Porcelain ── 冷瓷雾彩:长春花蓝 + 瓷粉/薄荷,脱离马卡龙腻感
|
|
370
|
+
pastel: {
|
|
371
|
+
id: "pastel",
|
|
372
|
+
name: "Porcelain",
|
|
373
|
+
description: "冷瓷雾彩 · 长春花蓝 · DM Sans 轻盈亲和",
|
|
374
|
+
palette: {
|
|
375
|
+
bg: "#f6f6f9",
|
|
376
|
+
bgAlt: "#eceef4",
|
|
377
|
+
text: "#20242e",
|
|
378
|
+
textMuted: "#69707f",
|
|
379
|
+
accent1: "#4a58bd",
|
|
380
|
+
accent2: "#e8879c",
|
|
381
|
+
accent3: "#79c3b7",
|
|
382
|
+
accent4: "#e0b56a",
|
|
383
|
+
divider: "#dee1ea",
|
|
384
|
+
},
|
|
385
|
+
fonts: { title: poolStack("DM Sans"), body: poolStack("DM Sans"), mono: FONT_MONO },
|
|
386
|
+
fontSizes: DEFAULT_FONT_SIZES,
|
|
387
|
+
metrics: DEFAULT_METRICS,
|
|
388
|
+
layout: "centered",
|
|
389
|
+
decoration: "confetti",
|
|
390
|
+
titleWordArt: { color: "#20242e", weight: 800, letterSpacing: -0.5 },
|
|
391
|
+
fontCssLinks: poolLinks("DM Sans"),
|
|
392
|
+
},
|
|
393
|
+
// ── luxe · Bordeaux ── 酒红丝绒 + 香槟金,高奢品牌的温度版黑金
|
|
394
|
+
luxe: {
|
|
395
|
+
id: "luxe",
|
|
396
|
+
name: "Bordeaux",
|
|
397
|
+
description: "酒红丝绒 · 香槟金饰 · Playfair 高奢衬线",
|
|
398
|
+
palette: {
|
|
399
|
+
bg: "#1c0d12",
|
|
400
|
+
bgAlt: "#2a151c",
|
|
401
|
+
text: "#f4e9df",
|
|
402
|
+
textMuted: "#b39a8c",
|
|
403
|
+
accent1: "#d9b47f",
|
|
404
|
+
accent2: "#8c2f39",
|
|
405
|
+
accent3: "#5d6e64",
|
|
406
|
+
accent4: "#c98a4b",
|
|
407
|
+
divider: "#3c2229",
|
|
408
|
+
},
|
|
409
|
+
fonts: { title: poolStack("Playfair Display"), body: poolStack("Manrope"), mono: FONT_MONO },
|
|
410
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 77 },
|
|
411
|
+
metrics: DEFAULT_METRICS,
|
|
412
|
+
layout: "editorial",
|
|
413
|
+
decoration: "gilded",
|
|
414
|
+
titleWordArt: {
|
|
415
|
+
color: "#f4e9df",
|
|
416
|
+
weight: 700,
|
|
417
|
+
letterSpacing: 1,
|
|
418
|
+
shadow: { color: "#d9b47f", alpha: 0.35, blur: 16, dx: 0, dy: 0 },
|
|
419
|
+
},
|
|
420
|
+
backgroundPaint: {
|
|
421
|
+
kind: "radial",
|
|
422
|
+
path: "circle",
|
|
423
|
+
fillToRect: { l: 0, t: 0, r: 1, b: 1 },
|
|
424
|
+
stops: [
|
|
425
|
+
{ offset: 0, color: "#2c1219" },
|
|
426
|
+
{ offset: 0.6, color: "#1c0d12" },
|
|
427
|
+
{ offset: 1, color: "#0f0609" },
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
fontCssLinks: poolLinks("Playfair Display", "Manrope"),
|
|
431
|
+
},
|
|
432
|
+
// ── ink · Porcelain Indigo ── 瓷白 + 帝国靛/鎏金/朱砂,青花文气
|
|
433
|
+
ink: {
|
|
434
|
+
id: "ink",
|
|
435
|
+
name: "Porcelain Indigo",
|
|
436
|
+
description: "瓷白青花 · 帝国靛朱砂 · 宋体雅正",
|
|
437
|
+
palette: {
|
|
438
|
+
bg: "#f7f6f2",
|
|
439
|
+
bgAlt: "#eceae2",
|
|
440
|
+
text: "#1a2233",
|
|
441
|
+
textMuted: "#5c6678",
|
|
442
|
+
accent1: "#26357c",
|
|
443
|
+
accent2: "#b8892d",
|
|
444
|
+
accent3: "#7c8aa8",
|
|
445
|
+
accent4: "#9f3a38",
|
|
446
|
+
divider: "#dfdcd2",
|
|
447
|
+
},
|
|
448
|
+
fonts: { title: poolStack("Noto Serif SC"), body: poolStack("Noto Sans SC"), mono: FONT_MONO },
|
|
449
|
+
fontSizes: { ...DEFAULT_FONT_SIZES, coverTitle: 77 },
|
|
450
|
+
metrics: { ...DEFAULT_METRICS, marginX: 0.08 },
|
|
451
|
+
layout: "editorial",
|
|
452
|
+
decoration: "ink-wash",
|
|
453
|
+
titleWordArt: { color: "#1a2233", weight: 700, letterSpacing: 2 },
|
|
454
|
+
fontCssLinks: poolLinks("Noto Serif SC", "Noto Sans SC"),
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
* 把一份主题用到的 Google Fonts 注入当前 document(idempotent)。
|
|
459
|
+
* 内置模板在 playground 启动时调用,保证"用内置模板新建"时标题/正文字体真的加载,
|
|
460
|
+
* 而不是落到系统默认字形(这是之前"字体不随主题"的根因之一)。
|
|
461
|
+
* 非浏览器环境(SSR / 子进程)自动跳过。
|
|
462
|
+
*/
|
|
463
|
+
export function injectThemeFonts(theme) {
|
|
464
|
+
for (const link of theme.fontCssLinks ?? [])
|
|
465
|
+
injectFontCss(link.cssUrl, link.family);
|
|
466
|
+
}
|
|
467
|
+
export const THEME_IDS = Object.keys(THEMES);
|
|
468
|
+
/**
|
|
469
|
+
* 每套主题的「适用场景」一句话(英文,喂给 outline 的 themeId 选择器)。
|
|
470
|
+
* 这是 outline prompt 里主题映射的**单一事实源** —— 新增主题时在这里补一行即可,
|
|
471
|
+
* prompt 会自动带上。
|
|
472
|
+
*
|
|
473
|
+
* 类型 `Record<BuiltinThemeId, string>` 强制覆盖 schema 里全部 builtin id:漏一个就编译报错。
|
|
474
|
+
* 加新主题的完整流程:THEMES 加条目 → BUILTIN_THEME_IDS 加 id → 这里加一行 hint。
|
|
475
|
+
*/
|
|
476
|
+
export const THEME_OUTLINE_HINTS = {
|
|
477
|
+
business: "corporate, finance, consulting, engineering reports (arctic ice + signal red, precise)",
|
|
478
|
+
academic: "research, education, whitepapers, lectures (warm sandstone paper, optical serif)",
|
|
479
|
+
creative: "marketing, brand launch, consumer, nightlife (violet-hour dusk, hot pink electric)",
|
|
480
|
+
minimal: "design-forward product, portfolio, studio (gallery white, single Klein-blue anchor)",
|
|
481
|
+
dark: "keynote, product launch, awards, cinematic reveal (true midnight + champagne gold)",
|
|
482
|
+
tech: "engineering, SaaS, developer tools, data/AI (graphite charcoal + acid lime)",
|
|
483
|
+
nature: "sustainability, health, wellness, organic premium (cream + moss green + clay)",
|
|
484
|
+
warm: "energetic talks, lifestyle, community, culture (kiln-fired ember orange + wine)",
|
|
485
|
+
editorial: "magazine feature, storytelling, brand editorial (near-black ink + ember orange, brutalist serif)",
|
|
486
|
+
terminal: "developer, hacker, cybersecurity, retro-tech (CRT phosphor green + amber, monospace)",
|
|
487
|
+
pastel: "education, friendly consumer product, community (cool porcelain mist, periwinkle)",
|
|
488
|
+
luxe: "luxury, fashion, premium brand, hospitality (bordeaux velvet + champagne gold)",
|
|
489
|
+
ink: "Chinese cultural, heritage, literary, art (porcelain white + imperial indigo, qinghua)",
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* 每套内置主题的「AI 生成种子」—— 一句 **英文 styleHint**,喂给 `generateTheme` 让 AI 现场设计
|
|
493
|
+
* 一套贴合该风格系统、比静态 preset 更精致的主题。
|
|
494
|
+
*
|
|
495
|
+
* 设计要点(对应 `.cursor/skills/ppt-style-systems/SKILL.md` +
|
|
496
|
+
* `packages/ai/src/skills/builtin/ppt-style-systems.md`):
|
|
497
|
+
* - 每条种子把该风格系统的 **精确色锚 + 字体配对 + 版式纪律 + 装饰气质** 写死,
|
|
498
|
+
* 防止模型「自由发挥」乱配色(skill 明确要求 "pick one family, do not invent random hex")。
|
|
499
|
+
* - 色锚 / 字体沿用对应 preset 的 palette / fonts,让 AI 主题落在**用户认得出的 vibe** 附近,
|
|
500
|
+
* 但由模型重新做渐变 / 层次 / 对比,产出更耐看的成品。
|
|
501
|
+
* - 这是外观步骤「按风格系统 AI 生成」的**单一事实源**:改风格语言只改这里。
|
|
502
|
+
*
|
|
503
|
+
* 类型 `Record<BuiltinThemeId, string>` 强制覆盖全部 builtin id(漏一个编译报错),与
|
|
504
|
+
* `THEME_OUTLINE_HINTS` 同源守护。
|
|
505
|
+
*/
|
|
506
|
+
export const THEME_STYLE_SEEDS = {
|
|
507
|
+
business: "precision-engineering corporate deck, arctic discipline; ice-white ground #f2f5f8 with steel ink #0c1420, ONE signal-red anchor #d91f30 used sparingly, Manrope tight geometric titles + Inter body, hairline rules, zero gradients, drafting-table calm",
|
|
508
|
+
academic: "scholarly publishing warmth; sandstone paper #f6efe4 with espresso ink #241a10, burnt-sienna #a2451c + deep-teal #155e63 accents, Fraunces optical serif titles + Newsreader body, manuscript top/bottom rules, generous bookish leading",
|
|
509
|
+
creative: "violet-hour consumer brand energy; dusk indigo #100829 deepening to black, hot-pink #ff2e92 hero with violet #8b5cf6 support, Bricolage Grotesque display titles with a pink rim glow, cinematic gradient, confident night-launch mood",
|
|
510
|
+
minimal: "gallery-white studio minimalism; warm near-white #fcfcfb with pure ink #111110, a SINGLE Klein blue #002fa7 anchor, Inter at extreme scale contrast, one hairline, museum-mat whitespace, no decoration beyond a thread line",
|
|
511
|
+
dark: "midnight awards keynote; true midnight #0a0c14 with a soft radial lift, champagne-gold hero #e0b458 and muted sage #8fae8b support, Sora display titles with a warm gold glow, luxurious cinematic restraint",
|
|
512
|
+
tech: "graphite terminal-grade engineering; charcoal #141518 ground, acid-lime hero #c3f53c with electric cyan #22d3ee support, Space Grotesk titles + Space Mono details, L-bracket framing, flat panels, zero decorative photography",
|
|
513
|
+
nature: "organic premium botanical; cream paper #f7f5ec with deep moss hero #2f5d33 and clay #c05f33 counterpoint, Fraunces humanist serif titles + DM Sans body, soft organic blob accents, natural and unhurried",
|
|
514
|
+
warm: "kiln-fired community warmth; clay-cream #faf3ec ground, ember hero #c93c10 with wine #9c2b3b depth and ochre #e9a13b light, Bricolage Grotesque bold titles + DM Sans, sunrise-arc glow, human and generous",
|
|
515
|
+
editorial: "brutalist dark editorial masthead; near-black ink #111013 with warm bone text #f4efe9, ember-orange hero #ff5a1f, Playfair Display 900 mastheads + Inter body, thick overline rules, asymmetric wide-left columns, print-noir confidence",
|
|
516
|
+
terminal: "phosphor CRT hacker sheet; deep green-black #0c1210, phosphor hero #2ee88a with amber #ffb454 warnings, Space Mono command-prompt titles + JetBrains Mono body, scanlines and corner brackets, disciplined two-color glow",
|
|
517
|
+
pastel: "cool porcelain mist friendliness; pale blue-grey #f6f6f9 ground, periwinkle hero #4a58bd with porcelain-pink #e8879c and mint #79c3b7 support, rounded DM Sans everywhere, confetti restraint, soft but never saccharine",
|
|
518
|
+
luxe: "bordeaux velvet luxury; deep wine-black #1c0d12 with a candlelit radial core, champagne hero #d9b47f and burgundy #8c2f39 depth, Playfair Display serif titles + Manrope body, thin gilded frame, hushed five-star elegance",
|
|
519
|
+
ink: "qinghua porcelain literati; porcelain white #f7f6f2 with indigo ink #1a2233, imperial-indigo hero #26357c, gilt #b8892d and cinnabar #9f3a38 seals, Noto Serif SC upright titles + Noto Sans SC body, eastern negative space and vertical rhythm",
|
|
520
|
+
};
|
|
521
|
+
// 运行期守护:每个 builtin id 必须在 THEMES 里真的存在(getTheme 才不会静默回退到 business)。
|
|
522
|
+
for (const id of BUILTIN_THEME_IDS) {
|
|
523
|
+
if (!THEMES[id])
|
|
524
|
+
throw new Error(`[themes] BUILTIN_THEME_IDS lists "${id}" but THEMES has no such theme`);
|
|
525
|
+
}
|
|
526
|
+
export function getTheme(id) {
|
|
527
|
+
// outline / spec 里的 "ai" 不是内置 preset,compile 应走 themeOverride。
|
|
528
|
+
if (id && id !== "ai" && THEMES[id])
|
|
529
|
+
return THEMES[id];
|
|
530
|
+
return THEMES.business;
|
|
531
|
+
}
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// AI 生成主题:GeneratedThemeSpec schema + spec → ThemeDef 转换
|
|
534
|
+
//
|
|
535
|
+
// "美观"导向:让 AI 输出一份完整设计参数,而不是从 5 套固定主题里挑。
|
|
536
|
+
// 用户输入主题 / 受众 / 场景 + 可选风格描述 → AI 产出一份调色板 + 字体组合 +
|
|
537
|
+
// 背景渐变 + 装饰风格的 spec → 即时编译为 ThemeDef 用于 compile。
|
|
538
|
+
//
|
|
539
|
+
// 设计要点:
|
|
540
|
+
// - 颜色必须严格 #rrggbb(zod 校验);模型偶尔输出 rgb()/hsl() / 缩写会被拒。
|
|
541
|
+
// - 字体只能从 THEME_FONT_POOL 里挑(保证浏览器可加载 + 商用授权);
|
|
542
|
+
// 模型若给出未知 family,回退到默认栈。
|
|
543
|
+
// - palette 中 bg 与 text 必须有足够对比度(这层放在 prompt 引导,不在 schema
|
|
544
|
+
// 强校验,避免模型一次没达标就整个失败)。
|
|
545
|
+
// ============================================================================
|
|
546
|
+
import { z } from "zod";
|
|
547
|
+
import { findFontPreset, injectFontCss } from "./fonts/inject.js";
|
|
548
|
+
import { contrastRatio, ensureContrast, parseHex, readableTextColor, toHex } from "./qa/color.js";
|
|
549
|
+
/** AI 输出的"主题 spec"。compile 之前用 `realizeGeneratedTheme` 把它具象化为 ThemeDef。 */
|
|
550
|
+
export const GeneratedThemeSpecSchema = z.object({
|
|
551
|
+
name: z.string().min(1).describe("Theme name (creative, descriptive; e.g. 'Aurora Bloom', '墨韵 Mist')."),
|
|
552
|
+
description: z.string().describe("One-sentence vibe summary, language follows user."),
|
|
553
|
+
mode: z.enum(["light", "dark"]).describe("Overall lightness; affects text contrast strategy."),
|
|
554
|
+
palette: z.object({
|
|
555
|
+
bg: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
556
|
+
bgAlt: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
557
|
+
text: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
558
|
+
textMuted: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
559
|
+
accent1: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
560
|
+
accent2: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
561
|
+
accent3: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
562
|
+
accent4: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
563
|
+
divider: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
564
|
+
}),
|
|
565
|
+
background: z
|
|
566
|
+
.union([
|
|
567
|
+
z.object({ kind: z.literal("solid") }),
|
|
568
|
+
z.object({
|
|
569
|
+
kind: z.literal("linear"),
|
|
570
|
+
angle: z.number().min(0).max(360).describe("Degrees, 0=left→right, 90=top→bottom."),
|
|
571
|
+
stops: z
|
|
572
|
+
.array(z.object({
|
|
573
|
+
offset: z.number().min(0).max(1),
|
|
574
|
+
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
575
|
+
alpha: z.number().min(0).max(1).optional(),
|
|
576
|
+
}))
|
|
577
|
+
.min(2)
|
|
578
|
+
.max(5),
|
|
579
|
+
}),
|
|
580
|
+
z.object({
|
|
581
|
+
kind: z.literal("radial"),
|
|
582
|
+
stops: z
|
|
583
|
+
.array(z.object({
|
|
584
|
+
offset: z.number().min(0).max(1),
|
|
585
|
+
color: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
586
|
+
alpha: z.number().min(0).max(1).optional(),
|
|
587
|
+
}))
|
|
588
|
+
.min(2)
|
|
589
|
+
.max(5),
|
|
590
|
+
}),
|
|
591
|
+
])
|
|
592
|
+
.describe("Slide background: solid uses palette.bg; linear/radial define your own."),
|
|
593
|
+
fonts: z.object({
|
|
594
|
+
title: z.string().describe("Must be one of the family names from FONT_POOL."),
|
|
595
|
+
body: z.string().describe("Must be one of the family names from FONT_POOL."),
|
|
596
|
+
mono: z.string().optional().describe("Optional; falls back to JetBrains Mono."),
|
|
597
|
+
}),
|
|
598
|
+
// legacy 装饰(corner-gradient-shape / geometric-lines / bottom-accent-bar / dotted-grid)
|
|
599
|
+
// 已从 LLM 可选枚举移除:渲染层(decorations.ts)保留兼容,但新生成的主题只能用命名装饰。
|
|
600
|
+
decoration: z
|
|
601
|
+
.enum(["none", "aurora", "tech-bracket", "neon-burst", "authority", "manuscript", "ink-thread", "organic-blobs", "sunrise-arc", "masthead", "terminal", "confetti", "gilded", "ink-wash"])
|
|
602
|
+
.describe("Decorative layer drawn beneath content: aurora (dark horizontal light veils), tech-bracket (engineering), neon-burst (creative night, edge neon tubes), authority (corporate), manuscript (academic), ink-thread (minimal), organic-blobs (nature, layered ridges), sunrise-arc (warm energy), masthead (magazine/editorial), terminal (geeky/dev), confetti (playful/education), gilded (luxury/brand), ink-wash (Chinese ink/literary), none (pure minimal)."),
|
|
603
|
+
layout: z
|
|
604
|
+
.enum(["classic", "centered", "editorial", "terminal"])
|
|
605
|
+
.optional()
|
|
606
|
+
.describe("Layout skeleton family — decides the GEOMETRY of generated slides, not just colors. classic (left-aligned kicker + left title + divider, the safe default), centered (centered cover & titles, symmetric whitespace — good for creative/playful), editorial (cover title sinks low with a thick overline rule + asymmetric wide-left two-column with a right media frame — good for magazine/editorial/luxury/literary), terminal (command-prompt prefix on titles + code-block backdrop — good for geeky/dev). Pick the family that matches the vibe; omit for classic."),
|
|
607
|
+
});
|
|
608
|
+
// AA 阈值:正文 4.5,次要文字(muted)3.0(≈ WCAG AA large / graphical)。
|
|
609
|
+
const MIN_TEXT_CONTRAST = 4.5;
|
|
610
|
+
const MIN_MUTED_CONTRAST = 3.0;
|
|
611
|
+
/** 文字铺在整页上的"有效底色":solid → palette.bg;gradient → 各 stop 的平均色。 */
|
|
612
|
+
function effectiveBgRgb(palette, background) {
|
|
613
|
+
if (background.kind === "solid")
|
|
614
|
+
return parseHex(palette.bg);
|
|
615
|
+
const stops = background.stops.map((s) => parseHex(s.color)).filter((c) => !!c);
|
|
616
|
+
if (stops.length === 0)
|
|
617
|
+
return parseHex(palette.bg);
|
|
618
|
+
const sum = stops.reduce((a, c) => ({ r: a.r + c.r, g: a.g + c.g, b: a.b + c.b }), { r: 0, g: 0, b: 0 });
|
|
619
|
+
return { r: sum.r / stops.length, g: sum.g / stops.length, b: sum.b / stops.length };
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* AI palette 源头对比度兜底(roadmap #5):模型偶尔给「暗底 + 偏暗文字」或「浅底 + 太浅的 muted」,
|
|
623
|
+
* 会让整篇 deck 从一开始就不可读。per-slide QA(qa/validate.ts)只能救 slide 上硬编码的文字色,
|
|
624
|
+
* 救不了主题 palette 本身 —— 所以在 realize 这一步就做**保守**修正:
|
|
625
|
+
* - text vs 有效底色 < 4.5 → 翻到规范兜底文字色(亮底 #0f172a / 暗底 #f8fafc)
|
|
626
|
+
* - textMuted vs 有效底色 < 3.0 → 朝 text 方向小步靠拢到 3.0(保留色调,只补对比)
|
|
627
|
+
* 只在"明显不达标"时动手,不主动美化达标的 palette。
|
|
628
|
+
*/
|
|
629
|
+
function sanitizePaletteContrast(palette, background) {
|
|
630
|
+
const bgRgb = effectiveBgRgb(palette, background);
|
|
631
|
+
if (!bgRgb)
|
|
632
|
+
return { palette, changed: [] };
|
|
633
|
+
const changed = [];
|
|
634
|
+
const next = { ...palette };
|
|
635
|
+
const textRgb = parseHex(palette.text);
|
|
636
|
+
if (textRgb && contrastRatio(textRgb, bgRgb) < MIN_TEXT_CONTRAST) {
|
|
637
|
+
next.text = readableTextColor(bgRgb);
|
|
638
|
+
changed.push("text");
|
|
639
|
+
}
|
|
640
|
+
const mutedRgb = parseHex(palette.textMuted);
|
|
641
|
+
const targetRgb = parseHex(next.text) ?? bgRgb;
|
|
642
|
+
if (mutedRgb && contrastRatio(mutedRgb, bgRgb) < MIN_MUTED_CONTRAST) {
|
|
643
|
+
next.textMuted = toHex(ensureContrast(mutedRgb, bgRgb, targetRgb, MIN_MUTED_CONTRAST));
|
|
644
|
+
changed.push("textMuted");
|
|
645
|
+
}
|
|
646
|
+
return { palette: next, changed };
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* 把 AI 输出的 spec 具象化为 ThemeDef:
|
|
650
|
+
* - 字体:family → 查 THEME_FONT_POOL 拿 stack & cssUrl;找不到回退到默认栈。
|
|
651
|
+
* - 背景:solid 时 backgroundPaint 留 undefined(compile 走 palette.bg);
|
|
652
|
+
* gradient 时构造 LinearGradientPaint / RadialGradientPaint。
|
|
653
|
+
* - palette:源头对比度兜底(sanitizePaletteContrast),保证 text/textMuted 相对底色达 WCAG。
|
|
654
|
+
* - 装饰:原样保留,compile 时翻译为 Shape。
|
|
655
|
+
* - 浏览器内自动注入 Google Fonts CSS(subprocess / server-side 无 document 时跳过)。
|
|
656
|
+
*/
|
|
657
|
+
export function realizeGeneratedTheme(spec, opts = {}) {
|
|
658
|
+
const titlePreset = findFontPreset(spec.fonts.title);
|
|
659
|
+
const bodyPreset = findFontPreset(spec.fonts.body);
|
|
660
|
+
const monoPreset = spec.fonts.mono ? findFontPreset(spec.fonts.mono) : null;
|
|
661
|
+
// 按需注入 google fonts link:让 SVG 文字渲染时能拿到真正字形。
|
|
662
|
+
if (opts.autoInject !== false) {
|
|
663
|
+
if (titlePreset)
|
|
664
|
+
injectFontCss(titlePreset.cssUrl, titlePreset.family);
|
|
665
|
+
if (bodyPreset)
|
|
666
|
+
injectFontCss(bodyPreset.cssUrl, bodyPreset.family);
|
|
667
|
+
if (monoPreset)
|
|
668
|
+
injectFontCss(monoPreset.cssUrl, monoPreset.family);
|
|
669
|
+
}
|
|
670
|
+
let backgroundPaint;
|
|
671
|
+
if (spec.background.kind === "linear") {
|
|
672
|
+
backgroundPaint = {
|
|
673
|
+
kind: "linear",
|
|
674
|
+
angle: spec.background.angle,
|
|
675
|
+
stops: spec.background.stops.map((s) => ({
|
|
676
|
+
offset: s.offset,
|
|
677
|
+
color: s.color,
|
|
678
|
+
...(s.alpha !== undefined ? { alpha: s.alpha } : {}),
|
|
679
|
+
})),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
else if (spec.background.kind === "radial") {
|
|
683
|
+
backgroundPaint = {
|
|
684
|
+
kind: "radial",
|
|
685
|
+
path: "circle",
|
|
686
|
+
fillToRect: { l: 0, t: 0, r: 1, b: 1 },
|
|
687
|
+
stops: spec.background.stops.map((s) => ({
|
|
688
|
+
offset: s.offset,
|
|
689
|
+
color: s.color,
|
|
690
|
+
...(s.alpha !== undefined ? { alpha: s.alpha } : {}),
|
|
691
|
+
})),
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const links = [];
|
|
695
|
+
if (titlePreset)
|
|
696
|
+
links.push({ family: titlePreset.family, cssUrl: titlePreset.cssUrl });
|
|
697
|
+
if (bodyPreset && bodyPreset.family !== titlePreset?.family)
|
|
698
|
+
links.push({ family: bodyPreset.family, cssUrl: bodyPreset.cssUrl });
|
|
699
|
+
if (monoPreset && monoPreset.family !== titlePreset?.family && monoPreset.family !== bodyPreset?.family)
|
|
700
|
+
links.push({ family: monoPreset.family, cssUrl: monoPreset.cssUrl });
|
|
701
|
+
// palette 源头对比度兜底:修正后再用于 titleWordArt / theme.palette,保证标题与正文都读得清。
|
|
702
|
+
const { palette: safePalette, changed } = sanitizePaletteContrast(spec.palette, spec.background);
|
|
703
|
+
if (changed.length && opts.debug) {
|
|
704
|
+
console.warn(`[theme] "${spec.name}" palette contrast auto-fixed: ${changed.join(", ")}`);
|
|
705
|
+
}
|
|
706
|
+
// AI 主题默认艺术字:粗体 + 微字距;暗色模式叠一层 accent1 发光阴影,使标题在深底上更跳。
|
|
707
|
+
const titleWordArt = spec.mode === "dark"
|
|
708
|
+
? {
|
|
709
|
+
color: safePalette.text,
|
|
710
|
+
weight: 800,
|
|
711
|
+
letterSpacing: -0.5,
|
|
712
|
+
shadow: { color: safePalette.accent1, alpha: 0.45, blur: 24, dx: 0, dy: 0 },
|
|
713
|
+
}
|
|
714
|
+
: { color: safePalette.text, weight: 800, letterSpacing: -0.5 };
|
|
715
|
+
const theme = {
|
|
716
|
+
id: `ai_${Date.now().toString(36)}`,
|
|
717
|
+
name: spec.name,
|
|
718
|
+
description: spec.description,
|
|
719
|
+
palette: { ...safePalette },
|
|
720
|
+
fonts: {
|
|
721
|
+
title: titlePreset?.stack ?? FONT_DISPLAY,
|
|
722
|
+
body: bodyPreset?.stack ?? FONT_SANS,
|
|
723
|
+
mono: monoPreset?.stack ?? FONT_MONO,
|
|
724
|
+
},
|
|
725
|
+
fontSizes: DEFAULT_FONT_SIZES,
|
|
726
|
+
metrics: DEFAULT_METRICS,
|
|
727
|
+
...(spec.layout ? { layout: spec.layout } : {}),
|
|
728
|
+
titleWordArt,
|
|
729
|
+
...(backgroundPaint ? { backgroundPaint } : {}),
|
|
730
|
+
decoration: spec.decoration,
|
|
731
|
+
fontCssLinks: links,
|
|
732
|
+
};
|
|
733
|
+
return theme;
|
|
734
|
+
}
|
|
735
|
+
/** 给 prompt / UI 用的字体 family 白名单(与 THEME_FONT_POOL 同步)。 */
|
|
736
|
+
export const THEME_FONT_FAMILIES = THEME_FONT_POOL.map((p) => p.family);
|