@sliiu/present-themes 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.THEME_FONT_POOL = void 0;
4
+ exports.isFontInjected = isFontInjected;
5
+ exports.injectFontCss = injectFontCss;
6
+ exports.findFontPreset = findFontPreset;
7
+ const core_1 = require("@sliiu/present-core");
8
+ // 浏览器内字体注入工具:把 Google Fonts CSS 链接以 idempotent 方式塞进 <head>,
9
+ // 让 SVG / Canvas / DOM 渲染时都能正确解析到指定 fontFamily。
10
+ //
11
+ // 用于:
12
+ // - stock-fonts MCP:用户主动选字体(fonts_apply)
13
+ // - theme generator:AI 生成主题时一并把题字 / 正文 family 注入
14
+ //
15
+ // 设计:
16
+ // - 缓存已注入的 family,防止 N 次 apply 同一字体重复添加 <link>。
17
+ // - preconnect 只插入一次(fonts.googleapis.com + fonts.gstatic.com)。
18
+ // - 不依赖任何外部资源;离线 / CDN 不可达时 link 失败不会阻塞渲染,会回退到
19
+ // font 栈中的下一个备选。
20
+ const INJECTED_FAMILIES = new Set();
21
+ function isFontInjected(family) {
22
+ return INJECTED_FAMILIES.has(family);
23
+ }
24
+ /**
25
+ * 注入一份 Google Fonts CSS(或任意公开 CSS)到当前 document。
26
+ *
27
+ * @param cssUrl Google Fonts CSS v2 url(如 'https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap')
28
+ * 也可以是任何指向 @font-face 规则的 CSS url。
29
+ * @param family CSS font-family 名(用于 dedup 缓存 + DOM 标记)。
30
+ */
31
+ function injectFontCss(cssUrl, family) {
32
+ if (typeof document === "undefined")
33
+ return;
34
+ const existing = document.querySelector(`link[data-mcp-font="${family}"]`);
35
+ if (existing) {
36
+ if (existing.getAttribute("href") !== cssUrl)
37
+ existing.href = cssUrl;
38
+ INJECTED_FAMILIES.add(family);
39
+ return;
40
+ }
41
+ if (INJECTED_FAMILIES.has(family))
42
+ return;
43
+ if (!document.querySelector('link[data-mcp-font-preconnect]')) {
44
+ const pre1 = document.createElement("link");
45
+ pre1.rel = "preconnect";
46
+ pre1.href = "https://fonts.googleapis.com";
47
+ pre1.dataset.mcpFontPreconnect = "1";
48
+ document.head.appendChild(pre1);
49
+ const pre2 = document.createElement("link");
50
+ pre2.rel = "preconnect";
51
+ pre2.href = "https://fonts.gstatic.com";
52
+ pre2.crossOrigin = "anonymous";
53
+ pre2.dataset.mcpFontPreconnect = "1";
54
+ document.head.appendChild(pre2);
55
+ }
56
+ const link = document.createElement("link");
57
+ link.rel = "stylesheet";
58
+ link.href = cssUrl;
59
+ link.dataset.mcpFont = family;
60
+ document.head.appendChild(link);
61
+ INJECTED_FAMILIES.add(family);
62
+ }
63
+ // 字体栈 fallback:CJK 兼容 + 系统兜底。
64
+ const CJK_FALLBACK = ', "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC"';
65
+ exports.THEME_FONT_POOL = [
66
+ // ---- 拉丁 sans-serif ----
67
+ { family: "Inter", category: "sans-serif", language: "latin",
68
+ cssUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap",
69
+ stack: `"Inter", -apple-system, BlinkMacSystemFont${CJK_FALLBACK}, sans-serif`,
70
+ tags: ["modern", "ui", "neutral", "tech", "clean"] },
71
+ { family: "Manrope", category: "sans-serif", language: "latin",
72
+ cssUrl: "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap",
73
+ stack: `"Manrope", -apple-system${CJK_FALLBACK}, sans-serif`,
74
+ tags: ["modern", "elegant", "startup"] },
75
+ { family: "Space Grotesk", category: "sans-serif", language: "latin",
76
+ cssUrl: "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap",
77
+ stack: `"Space Grotesk", -apple-system${CJK_FALLBACK}, sans-serif`,
78
+ tags: ["modern", "tech", "geometric", "futuristic"] },
79
+ { family: "DM Sans", category: "sans-serif", language: "latin",
80
+ cssUrl: "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap",
81
+ stack: `"DM Sans"${CJK_FALLBACK}, sans-serif`,
82
+ tags: ["modern", "warm", "friendly"] },
83
+ { family: "Plus Jakarta Sans", category: "sans-serif", language: "latin",
84
+ cssUrl: "https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap",
85
+ stack: `"Plus Jakarta Sans"${CJK_FALLBACK}, sans-serif`,
86
+ tags: ["modern", "balanced", "saas"] },
87
+ // ---- 拉丁 display / headline ----
88
+ { family: "Sora", category: "display", language: "latin",
89
+ cssUrl: "https://fonts.googleapis.com/css2?family=Sora:wght@400;600;800&display=swap",
90
+ stack: `"Sora"${CJK_FALLBACK}, sans-serif`,
91
+ tags: ["headline", "tech", "modern", "bold"] },
92
+ { family: "Bricolage Grotesque", category: "display", language: "latin",
93
+ cssUrl: "https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&display=swap",
94
+ stack: `"Bricolage Grotesque"${CJK_FALLBACK}, sans-serif`,
95
+ tags: ["editorial", "expressive", "headline"] },
96
+ { family: "Unbounded", category: "display", language: "latin",
97
+ cssUrl: "https://fonts.googleapis.com/css2?family=Unbounded:wght@400;600;800&display=swap",
98
+ stack: `"Unbounded"${CJK_FALLBACK}, sans-serif`,
99
+ tags: ["futuristic", "rounded", "luxury", "headline"] },
100
+ // ---- 拉丁 serif ----
101
+ { family: "Playfair Display", category: "serif", language: "latin",
102
+ cssUrl: "https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,800;0,900;1,400&display=swap",
103
+ stack: `"Playfair Display"${CJK_FALLBACK}, serif`,
104
+ tags: ["elegant", "editorial", "luxury", "headline"] },
105
+ { family: "Fraunces", category: "serif", language: "latin",
106
+ cssUrl: "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,700;9..144,900&display=swap",
107
+ stack: `"Fraunces"${CJK_FALLBACK}, serif`,
108
+ tags: ["editorial", "warm", "expressive"] },
109
+ { family: "Newsreader", category: "serif", language: "latin",
110
+ cssUrl: "https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,600&display=swap",
111
+ stack: `"Newsreader"${CJK_FALLBACK}, serif`,
112
+ tags: ["reading", "magazine", "warm"] },
113
+ // ---- 等宽 ----
114
+ { family: "JetBrains Mono", category: "monospace", language: "latin",
115
+ cssUrl: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap",
116
+ stack: `"JetBrains Mono", "Menlo", "Consolas", monospace`,
117
+ tags: ["code", "monospace", "developer"] },
118
+ { family: "IBM Plex Mono", category: "monospace", language: "latin",
119
+ cssUrl: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap",
120
+ stack: `"IBM Plex Mono", "Menlo", "Consolas", monospace`,
121
+ tags: ["code", "monospace", "editorial", "meta"] },
122
+ { family: "Space Mono", category: "monospace", language: "latin",
123
+ cssUrl: "https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap",
124
+ stack: `"Space Mono", "Menlo", monospace`,
125
+ tags: ["code", "retro", "tech"] },
126
+ // ---- 中文 sans ----
127
+ { family: "Noto Sans SC", category: "sans-serif", language: "cjk",
128
+ cssUrl: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap",
129
+ stack: `"Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif`,
130
+ tags: ["中文", "黑体", "简体", "正式", "现代"] },
131
+ // ---- 中文 serif ----
132
+ { family: "Noto Serif SC", category: "serif", language: "cjk",
133
+ cssUrl: "https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700;800;900&display=swap",
134
+ stack: `"Noto Serif SC", "Songti SC", "SimSun", serif`,
135
+ tags: ["中文", "宋体", "简体", "正式", "经典"] },
136
+ // ---- 中文显示 / 标题 ----
137
+ { family: "ZCOOL XiaoWei", category: "display", language: "cjk",
138
+ cssUrl: "https://fonts.googleapis.com/css2?family=ZCOOL+XiaoWei&display=swap",
139
+ stack: `"ZCOOL XiaoWei", "Songti SC", serif`,
140
+ tags: ["中文", "宋体", "手写", "标题", "文艺"] },
141
+ { family: "ZCOOL KuaiLe", category: "display", language: "cjk",
142
+ cssUrl: "https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&display=swap",
143
+ stack: `"ZCOOL KuaiLe"${CJK_FALLBACK}, sans-serif`,
144
+ tags: ["中文", "可爱", "标题", "活泼"] },
145
+ { family: "Ma Shan Zheng", category: "handwriting", language: "cjk",
146
+ cssUrl: "https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap",
147
+ stack: `"Ma Shan Zheng", "Songti SC", serif`,
148
+ tags: ["中文", "书法", "毛笔", "手写", "传统"] },
149
+ ];
150
+ /** 按 family 找 preset;找不到返回 null,调用方应回退到默认栈。 */
151
+ function findFontPreset(family) {
152
+ const exact = exports.THEME_FONT_POOL.find((p) => p.family === family || p.stack === family);
153
+ if (exact)
154
+ return exact;
155
+ const primary = (0, core_1.canonicalTypeface)(family) ?? (0, core_1.firstNamedTypeface)(family);
156
+ if (!primary)
157
+ return null;
158
+ const lower = primary.toLowerCase();
159
+ return exports.THEME_FONT_POOL.find((p) => p.family.toLowerCase() === lower) ?? null;
160
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.THEME_GALLERY_ORDER = void 0;
4
+ /** Design / Console 主题画廊展示顺序(与 template-store 内置模板顺序一致)。 */
5
+ exports.THEME_GALLERY_ORDER = [
6
+ "dark",
7
+ "tech",
8
+ "terminal",
9
+ "creative",
10
+ "luxe",
11
+ "business",
12
+ "editorial",
13
+ "warm",
14
+ "pastel",
15
+ "academic",
16
+ "ink",
17
+ "nature",
18
+ "minimal",
19
+ ];
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApplyThemeCmd = exports.ApplyCustomThemeCmd = exports.themeSpecFromThemeDef = exports.stripSlideThemeDecorations = exports.resolveLayoutTypeForSlide = exports.findLayoutRefByType = exports.buildMastersFromThemeDef = exports.buildCoreThemeFromSpec = exports.injectFontCss = exports.findFontPreset = exports.THEME_FONT_POOL = exports.buildDecorationShapesShared = exports.realizeGeneratedTheme = exports.injectThemeFonts = exports.getTheme = exports.GeneratedThemeSpecSchema = exports.THEME_FONT_FAMILIES = exports.THEME_STYLE_SEEDS = exports.THEME_OUTLINE_HINTS = exports.THEME_IDS = exports.THEMES = exports.THEME_GALLERY_ORDER = exports.BUILTIN_THEME_IDS = void 0;
4
+ var builtin_ids_1 = require("./builtin-ids");
5
+ Object.defineProperty(exports, "BUILTIN_THEME_IDS", { enumerable: true, get: function () { return builtin_ids_1.BUILTIN_THEME_IDS; } });
6
+ var gallery_order_1 = require("./gallery-order");
7
+ Object.defineProperty(exports, "THEME_GALLERY_ORDER", { enumerable: true, get: function () { return gallery_order_1.THEME_GALLERY_ORDER; } });
8
+ var themes_1 = require("./themes");
9
+ Object.defineProperty(exports, "THEMES", { enumerable: true, get: function () { return themes_1.THEMES; } });
10
+ Object.defineProperty(exports, "THEME_IDS", { enumerable: true, get: function () { return themes_1.THEME_IDS; } });
11
+ Object.defineProperty(exports, "THEME_OUTLINE_HINTS", { enumerable: true, get: function () { return themes_1.THEME_OUTLINE_HINTS; } });
12
+ Object.defineProperty(exports, "THEME_STYLE_SEEDS", { enumerable: true, get: function () { return themes_1.THEME_STYLE_SEEDS; } });
13
+ Object.defineProperty(exports, "THEME_FONT_FAMILIES", { enumerable: true, get: function () { return themes_1.THEME_FONT_FAMILIES; } });
14
+ Object.defineProperty(exports, "GeneratedThemeSpecSchema", { enumerable: true, get: function () { return themes_1.GeneratedThemeSpecSchema; } });
15
+ Object.defineProperty(exports, "getTheme", { enumerable: true, get: function () { return themes_1.getTheme; } });
16
+ Object.defineProperty(exports, "injectThemeFonts", { enumerable: true, get: function () { return themes_1.injectThemeFonts; } });
17
+ Object.defineProperty(exports, "realizeGeneratedTheme", { enumerable: true, get: function () { return themes_1.realizeGeneratedTheme; } });
18
+ var decorations_1 = require("./decorations");
19
+ Object.defineProperty(exports, "buildDecorationShapesShared", { enumerable: true, get: function () { return decorations_1.buildDecorationShapesShared; } });
20
+ var inject_1 = require("./fonts/inject");
21
+ Object.defineProperty(exports, "THEME_FONT_POOL", { enumerable: true, get: function () { return inject_1.THEME_FONT_POOL; } });
22
+ Object.defineProperty(exports, "findFontPreset", { enumerable: true, get: function () { return inject_1.findFontPreset; } });
23
+ Object.defineProperty(exports, "injectFontCss", { enumerable: true, get: function () { return inject_1.injectFontCss; } });
24
+ var template_masters_1 = require("./template-masters");
25
+ Object.defineProperty(exports, "buildCoreThemeFromSpec", { enumerable: true, get: function () { return template_masters_1.buildCoreThemeFromSpec; } });
26
+ Object.defineProperty(exports, "buildMastersFromThemeDef", { enumerable: true, get: function () { return template_masters_1.buildMastersFromThemeDef; } });
27
+ Object.defineProperty(exports, "findLayoutRefByType", { enumerable: true, get: function () { return template_masters_1.findLayoutRefByType; } });
28
+ Object.defineProperty(exports, "resolveLayoutTypeForSlide", { enumerable: true, get: function () { return template_masters_1.resolveLayoutTypeForSlide; } });
29
+ Object.defineProperty(exports, "stripSlideThemeDecorations", { enumerable: true, get: function () { return template_masters_1.stripSlideThemeDecorations; } });
30
+ Object.defineProperty(exports, "themeSpecFromThemeDef", { enumerable: true, get: function () { return template_masters_1.themeSpecFromThemeDef; } });
31
+ var theme_commands_1 = require("./theme-commands");
32
+ Object.defineProperty(exports, "ApplyCustomThemeCmd", { enumerable: true, get: function () { return theme_commands_1.ApplyCustomThemeCmd; } });
33
+ Object.defineProperty(exports, "ApplyThemeCmd", { enumerable: true, get: function () { return theme_commands_1.ApplyThemeCmd; } });
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ // 共享颜色 / 对比度工具(WCAG 2.x relative luminance + contrast ratio)。
3
+ //
4
+ // 抽出来给两处复用,避免颜色数学各写一份漂移:
5
+ // - qa/validate.ts:生成后 per-slide 文字对比度修复
6
+ // - themes.ts realizeGeneratedTheme:AI 主题 palette 源头对比度兜底(#5)
7
+ //
8
+ // 兜底文字色与 ppt-visual-design.md / ppt-design-rules.mdc §7 保持一致。
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.WCAG_BODY_TEXT_CONTRAST = exports.WCAG_LARGE_TEXT_CONTRAST = exports.SAFE_TEXT_LIGHT = exports.SAFE_TEXT_DARK = void 0;
11
+ exports.parseHex = parseHex;
12
+ exports.toHex = toHex;
13
+ exports.relativeLuminance = relativeLuminance;
14
+ exports.contrastRatio = contrastRatio;
15
+ exports.readableTextColor = readableTextColor;
16
+ exports.mixRgb = mixRgb;
17
+ exports.ensureContrast = ensureContrast;
18
+ /** 解析 #rgb / #rrggbb(大小写均可)。无法解析返回 null。 */
19
+ function parseHex(input) {
20
+ if (!input)
21
+ return null;
22
+ let s = input.trim();
23
+ if (s.startsWith("#"))
24
+ s = s.slice(1);
25
+ if (s.length === 3) {
26
+ s = s
27
+ .split("")
28
+ .map((c) => c + c)
29
+ .join("");
30
+ }
31
+ if (s.length !== 6 || /[^0-9a-fA-F]/.test(s))
32
+ return null;
33
+ return {
34
+ r: parseInt(s.slice(0, 2), 16),
35
+ g: parseInt(s.slice(2, 4), 16),
36
+ b: parseInt(s.slice(4, 6), 16),
37
+ };
38
+ }
39
+ /** Rgb → #rrggbb(小写)。 */
40
+ function toHex(rgb) {
41
+ const h = (n) => Math.max(0, Math.min(255, Math.round(n)))
42
+ .toString(16)
43
+ .padStart(2, "0");
44
+ return `#${h(rgb.r)}${h(rgb.g)}${h(rgb.b)}`;
45
+ }
46
+ function srgbChannel(c) {
47
+ const cs = c / 255;
48
+ return cs <= 0.03928 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);
49
+ }
50
+ /** WCAG 相对亮度 0..1。 */
51
+ function relativeLuminance(rgb) {
52
+ return 0.2126 * srgbChannel(rgb.r) + 0.7152 * srgbChannel(rgb.g) + 0.0722 * srgbChannel(rgb.b);
53
+ }
54
+ /** WCAG 对比度 1..21。 */
55
+ function contrastRatio(a, b) {
56
+ const la = relativeLuminance(a);
57
+ const lb = relativeLuminance(b);
58
+ const hi = Math.max(la, lb);
59
+ const lo = Math.min(la, lb);
60
+ return (hi + 0.05) / (lo + 0.05);
61
+ }
62
+ // 规范里的两个兜底文字色(与 ppt-visual-design.md / ppt-design-rules.mdc 一致)。
63
+ exports.SAFE_TEXT_DARK = "#0f172a"; // 用于亮底
64
+ exports.SAFE_TEXT_LIGHT = "#f8fafc"; // 用于暗底
65
+ // WCAG AA 对比度阈值(单一事实源):
66
+ // - 大字(标题 / 大数字)≥3.0;正文 ≥4.5。
67
+ // validate.ts 的默认阈值、art-director 的 recolor 门槛、art-director rubric 文案都引用这两个常量,
68
+ // 保证"生成时兜底"和"验收时打分"用同一把尺。
69
+ exports.WCAG_LARGE_TEXT_CONTRAST = 3.0;
70
+ exports.WCAG_BODY_TEXT_CONTRAST = 4.5;
71
+ /** 根据底色亮度选一个高对比度的兜底文字色。 */
72
+ function readableTextColor(bg) {
73
+ return relativeLuminance(bg) > 0.5 ? exports.SAFE_TEXT_DARK : exports.SAFE_TEXT_LIGHT;
74
+ }
75
+ /** 线性插值两色,t=0 → a,t=1 → b。 */
76
+ function mixRgb(a, b, t) {
77
+ const k = Math.max(0, Math.min(1, t));
78
+ return {
79
+ r: a.r + (b.r - a.r) * k,
80
+ g: a.g + (b.g - a.g) * k,
81
+ b: a.b + (b.b - a.b) * k,
82
+ };
83
+ }
84
+ /**
85
+ * 把 `color` 朝 `target` 方向逐步靠拢,直到相对 `bg` 的对比度 ≥ `min`。
86
+ * 用于把「对底色太浅」的次要文字色拉到达标,同时尽量保留原色调(小步逼近)。
87
+ * 若走到 target 仍不达标(几乎不可能,target 应是高对比色),返回 target。
88
+ */
89
+ function ensureContrast(color, bg, target, min) {
90
+ if (contrastRatio(color, bg) >= min)
91
+ return color;
92
+ for (let t = 0.15; t < 1; t += 0.15) {
93
+ const c = mixRgb(color, target, t);
94
+ if (contrastRatio(c, bg) >= min)
95
+ return c;
96
+ }
97
+ return target;
98
+ }