@liustack/pptwise 0.22.0 → 0.24.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/README.md +72 -41
- package/README.zh-CN.md +72 -41
- package/dist/{chunk-M35M4QUC.js → chunk-2CA3JRFR.js} +460 -499
- package/dist/chunk-2CA3JRFR.js.map +1 -0
- package/dist/{chunk-WL5KWYKS.js → chunk-G7WQ7KSP.js} +34800 -30625
- package/dist/chunk-G7WQ7KSP.js.map +1 -0
- package/dist/{chunk-VUOLBHD7.js → chunk-RQUKZYSH.js} +1 -1
- package/dist/{chunk-VUOLBHD7.js.map → chunk-RQUKZYSH.js.map} +1 -1
- package/dist/{chunk-3ZUKISTY.js → chunk-VEZGVYJS.js} +2 -2
- package/dist/cli.js +911 -462
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5320 -1534
- package/dist/index.js +15 -13
- package/dist/node.d.ts +1 -1
- package/dist/node.js +2 -2
- package/dist/{pixel-audit-H5K6JK3X.js → pixel-audit-M5Q5WCST.js} +4 -4
- package/dist/pixel-audit-M5Q5WCST.js.map +1 -0
- package/dist/{registry-C0GJH7ZT.d.ts → registry-D8bkC8ff.d.ts} +1 -1
- package/dsh/preview-tool.js +20 -17
- package/package.json +2 -1
- package/skills/pptwise/SKILL.md +133 -70
- package/skills/pptwise/SKILL.zh-CN.md +136 -73
- package/skills/pptwise/references/branding.md +34 -8
- package/skills/pptwise/references/branding.zh-CN.md +36 -8
- package/skills/pptwise/references/components.md +63 -33
- package/skills/pptwise/references/components.zh-CN.md +63 -33
- package/skills/pptwise/references/density.md +23 -9
- package/skills/pptwise/references/density.zh-CN.md +23 -9
- package/skills/pptwise/references/images.md +26 -13
- package/skills/pptwise/references/images.zh-CN.md +26 -13
- package/skills/pptwise/references/layouts.md +30 -26
- package/skills/pptwise/references/layouts.zh-CN.md +30 -26
- package/skills/pptwise/references/spec.md +143 -69
- package/skills/pptwise/references/spec.zh-CN.md +143 -69
- package/skills/pptwise/references/validate.md +33 -45
- package/skills/pptwise/references/validate.zh-CN.md +33 -45
- package/skills/pptwise/scripts/run.ps1 +1 -1
- package/skills/pptwise/scripts/run.sh +1 -1
- package/dist/chunk-M35M4QUC.js.map +0 -1
- package/dist/chunk-WL5KWYKS.js.map +0 -1
- package/dist/pixel-audit-H5K6JK3X.js.map +0 -1
- /package/dist/{chunk-3ZUKISTY.js.map → chunk-VEZGVYJS.js.map} +0 -0
|
@@ -1,220 +1,124 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
BEAT_VALUES,
|
|
4
|
-
BrandConfigSchema,
|
|
2
|
+
BUILTIN_THEME_FILES,
|
|
5
3
|
BrandSchema,
|
|
6
4
|
CANONICAL_THEME_IDS,
|
|
7
5
|
CAPACITY,
|
|
8
6
|
COMPONENT_TYPES,
|
|
9
7
|
DeckBrandingSchema,
|
|
10
|
-
|
|
8
|
+
HexTokenSchema,
|
|
9
|
+
KIND_VALUES,
|
|
11
10
|
MetaSchema,
|
|
12
11
|
NarrativeProfileInputSchema,
|
|
13
12
|
PptwiseError,
|
|
14
13
|
PptxIRSchema,
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
THEME_ID_CONSTRAINT,
|
|
15
|
+
THEME_ID_PATTERN,
|
|
17
16
|
THEME_LABELS,
|
|
18
|
-
|
|
17
|
+
THEME_OCCASIONS,
|
|
18
|
+
THEME_REQUIRED_MESSAGE,
|
|
19
|
+
ThemeFileSchema,
|
|
20
|
+
assertContrastFloor,
|
|
19
21
|
contrastRatio,
|
|
20
22
|
getInstalledThemeIds,
|
|
23
|
+
getLayout,
|
|
21
24
|
getThemeDefinition,
|
|
25
|
+
installThemeFile,
|
|
22
26
|
mixHex,
|
|
23
27
|
normalizeDeckRootAliases,
|
|
24
28
|
normalizeNarrativeShape,
|
|
29
|
+
offeredContentKinds,
|
|
25
30
|
parseTransform,
|
|
26
|
-
registerTheme,
|
|
27
31
|
renderSlideSvg,
|
|
28
|
-
|
|
32
|
+
resolveLayoutId,
|
|
29
33
|
resolveNarrative,
|
|
30
|
-
resolveStyle
|
|
31
|
-
|
|
34
|
+
resolveStyle,
|
|
35
|
+
themeIssueMessage
|
|
36
|
+
} from "./chunk-G7WQ7KSP.js";
|
|
32
37
|
import {
|
|
33
38
|
getPlatform
|
|
34
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-RQUKZYSH.js";
|
|
35
40
|
|
|
36
41
|
// src/version.ts
|
|
37
|
-
var VERSION = "0.
|
|
42
|
+
var VERSION = "0.24.0";
|
|
38
43
|
|
|
39
|
-
// src/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// chrome key still parses. migrateIrV3ToV4 rewrites it to branding.
|
|
56
|
-
chrome: DeckBrandingSchema.optional(),
|
|
57
|
-
seed: z.number().int().optional(),
|
|
58
|
-
slides: z.array(SlideSchema)
|
|
59
|
-
}).strict();
|
|
60
|
-
|
|
61
|
-
// src/ir/migrate.ts
|
|
62
|
-
function migrateChromeToBranding(raw) {
|
|
63
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
64
|
-
const obj = raw;
|
|
65
|
-
const hasChrome = Object.hasOwn(obj, "chrome");
|
|
66
|
-
const hasBranding = Object.hasOwn(obj, "branding");
|
|
67
|
-
if (hasChrome && hasBranding) {
|
|
68
|
-
throw new PptwiseError('cannot migrate: both "chrome" and "branding" are present');
|
|
69
|
-
}
|
|
70
|
-
if (!hasChrome) return raw;
|
|
71
|
-
const next = { ...obj, branding: obj.chrome };
|
|
72
|
-
delete next.chrome;
|
|
73
|
-
return next;
|
|
74
|
-
}
|
|
75
|
-
function migrateBloomToClassroom(raw) {
|
|
76
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
77
|
-
const obj = raw;
|
|
78
|
-
const theme = obj.theme;
|
|
79
|
-
if (theme === "bloom") {
|
|
80
|
-
return { ...obj, theme: "classroom" };
|
|
81
|
-
}
|
|
82
|
-
if (typeof theme === "object" && theme !== null && !Array.isArray(theme)) {
|
|
83
|
-
const themeObj = theme;
|
|
84
|
-
if (themeObj.id === "bloom") {
|
|
85
|
-
return { ...obj, theme: { ...themeObj, id: "classroom" } };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return raw;
|
|
89
|
-
}
|
|
90
|
-
function isPlainRecord(value) {
|
|
91
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
92
|
-
}
|
|
93
|
-
function isLogoWallComponent(value) {
|
|
94
|
-
return isPlainRecord(value) && value.type === "logo_wall";
|
|
95
|
-
}
|
|
96
|
-
function arrayHasLogoWall(components) {
|
|
97
|
-
return Array.isArray(components) && components.some(isLogoWallComponent);
|
|
98
|
-
}
|
|
99
|
-
function rewriteLogoWallItem(item) {
|
|
100
|
-
if (!isPlainRecord(item)) return item;
|
|
101
|
-
const next = { asset_id: item.asset_id };
|
|
102
|
-
if (Object.hasOwn(item, "label") && item.label !== void 0) next.caption = item.label;
|
|
103
|
-
return next;
|
|
104
|
-
}
|
|
105
|
-
function rewriteLogoWallComponent(component) {
|
|
106
|
-
const items = component.items;
|
|
107
|
-
if (!Array.isArray(items)) {
|
|
108
|
-
return { type: "image_grid", items };
|
|
44
|
+
// src/themes/presets.ts
|
|
45
|
+
var THEME_PRESETS = CANONICAL_THEME_IDS.map((id) => ({
|
|
46
|
+
id,
|
|
47
|
+
label: BUILTIN_THEME_FILES[id].label,
|
|
48
|
+
occasions: THEME_OCCASIONS[id].occasions,
|
|
49
|
+
identity: THEME_OCCASIONS[id].identity
|
|
50
|
+
}));
|
|
51
|
+
function isThemePresetId(id) {
|
|
52
|
+
return CANONICAL_THEME_IDS.includes(id);
|
|
53
|
+
}
|
|
54
|
+
function getThemePreset(id) {
|
|
55
|
+
const summary = THEME_PRESETS.find((preset) => preset.id === id);
|
|
56
|
+
if (!summary) {
|
|
57
|
+
throw new PptwiseError(
|
|
58
|
+
`unknown theme preset "${id}". Installed presets: ${CANONICAL_THEME_IDS.join(", ")}`
|
|
59
|
+
);
|
|
109
60
|
}
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
function rewriteComponentsArray(components) {
|
|
113
|
-
return components.map((component) => isLogoWallComponent(component) ? rewriteLogoWallComponent(component) : component);
|
|
61
|
+
return summary;
|
|
114
62
|
}
|
|
115
|
-
function
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
63
|
+
function withDecor(entry, motif) {
|
|
64
|
+
if (entry.decor !== void 0 || motif === void 0) return structuredClone(entry);
|
|
65
|
+
if (getLayout(entry.face)?.suppressMotif === true) return structuredClone(entry);
|
|
66
|
+
return {
|
|
67
|
+
...structuredClone(entry),
|
|
68
|
+
decor: motif.params ? { kind: "motif", id: motif.id, params: { ...motif.params } } : { kind: "motif", id: motif.id }
|
|
121
69
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const slides = raw.slides.map((slide) => {
|
|
128
|
-
if (!isPlainRecord(slide) || !arrayHasLogoWall(slide.components)) return slide;
|
|
129
|
-
slidesChanged = true;
|
|
130
|
-
return { ...slide, components: rewriteComponentsArray(slide.components) };
|
|
131
|
-
});
|
|
132
|
-
if (slidesChanged) take().slides = slides;
|
|
70
|
+
}
|
|
71
|
+
function copyMenu(menu, motif) {
|
|
72
|
+
const content = {};
|
|
73
|
+
for (const [kind, entry] of Object.entries(menu.content)) {
|
|
74
|
+
content[kind] = withDecor(entry, motif);
|
|
133
75
|
}
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
function migrateBannerHeadingToTwoColumn(raw) {
|
|
141
|
-
if (!isPlainRecord(raw)) return raw;
|
|
142
|
-
let next;
|
|
143
|
-
const take = () => {
|
|
144
|
-
if (!next) next = { ...raw };
|
|
145
|
-
return next;
|
|
76
|
+
return {
|
|
77
|
+
cover: withDecor(menu.cover, motif),
|
|
78
|
+
chapter: withDecor(menu.chapter, motif),
|
|
79
|
+
content,
|
|
80
|
+
ending: withDecor(menu.ending, motif)
|
|
146
81
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (Array.isArray(raw.slides)) {
|
|
154
|
-
let slidesChanged = false;
|
|
155
|
-
const slides = raw.slides.map((slide) => {
|
|
156
|
-
if (!isPlainRecord(slide)) return slide;
|
|
157
|
-
const rewritten = rewriteBannerHeadingField(slide, "layout");
|
|
158
|
-
if (!rewritten) return slide;
|
|
159
|
-
slidesChanged = true;
|
|
160
|
-
return rewritten;
|
|
161
|
-
});
|
|
162
|
-
if (slidesChanged) take().slides = slides;
|
|
163
|
-
}
|
|
164
|
-
if (Array.isArray(raw.pages)) {
|
|
165
|
-
let pagesChanged = false;
|
|
166
|
-
const pages = raw.pages.map((page) => {
|
|
167
|
-
if (!isPlainRecord(page)) return page;
|
|
168
|
-
const layoutHit = rewriteBannerHeadingField(page, "layout");
|
|
169
|
-
const focusHit = rewriteBannerHeadingField(layoutHit ?? page, "focus");
|
|
170
|
-
const rewritten = focusHit ?? layoutHit;
|
|
171
|
-
if (!rewritten) return page;
|
|
172
|
-
pagesChanged = true;
|
|
173
|
-
return rewritten;
|
|
174
|
-
});
|
|
175
|
-
if (pagesChanged) take().pages = pages;
|
|
176
|
-
}
|
|
177
|
-
return next ?? raw;
|
|
178
|
-
}
|
|
179
|
-
var STRATEGY_VALUE_MIGRATION = { narrative: "storytelling" };
|
|
180
|
-
var PACING_VALUE_MIGRATION = { text: "dense", presentation: "spacious" };
|
|
181
|
-
function migrateNarrativeInput(scenario) {
|
|
182
|
-
if (scenario === void 0 || typeof scenario === "string") return scenario;
|
|
183
|
-
const narrative = {};
|
|
184
|
-
for (const [key, value] of Object.entries(scenario)) {
|
|
185
|
-
if (key === "mode") {
|
|
186
|
-
narrative.strategy = typeof value === "string" ? STRATEGY_VALUE_MIGRATION[value] ?? value : value;
|
|
187
|
-
} else if (key === "delivery") {
|
|
188
|
-
narrative.pacing = typeof value === "string" ? PACING_VALUE_MIGRATION[value] ?? value : value;
|
|
189
|
-
} else if (key === "audience") {
|
|
190
|
-
narrative.audience = value;
|
|
191
|
-
} else {
|
|
192
|
-
narrative[key] = value;
|
|
193
|
-
}
|
|
82
|
+
}
|
|
83
|
+
function copyThemePreset(presetId, targetId) {
|
|
84
|
+
getThemePreset(presetId);
|
|
85
|
+
if (!THEME_ID_PATTERN.test(targetId)) {
|
|
86
|
+
throw new PptwiseError(`invalid theme id "${targetId}". ${THEME_ID_CONSTRAINT}`);
|
|
194
87
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
...
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
...v3.brand !== void 0 ? { brand: v3.brand } : {},
|
|
207
|
-
...v3.chrome !== void 0 ? { chrome: v3.chrome } : {},
|
|
208
|
-
...v3.seed !== void 0 ? { seed: v3.seed } : {},
|
|
209
|
-
slides: v3.slides
|
|
88
|
+
const preset = BUILTIN_THEME_FILES[presetId];
|
|
89
|
+
const record = THEME_OCCASIONS[preset.id];
|
|
90
|
+
return {
|
|
91
|
+
version: 2,
|
|
92
|
+
id: targetId,
|
|
93
|
+
label: preset.label,
|
|
94
|
+
style: { ...structuredClone(preset.style), id: targetId },
|
|
95
|
+
...preset.brand ? { brand: structuredClone(preset.brand) } : {},
|
|
96
|
+
occasions: [...record.occasions],
|
|
97
|
+
identity: record.identity,
|
|
98
|
+
menu: copyMenu(preset.menu, preset.motif)
|
|
210
99
|
};
|
|
211
|
-
return migrateBannerHeadingToTwoColumn(
|
|
212
|
-
migrateLogoWallToImageGrid(migrateBloomToClassroom(migrateChromeToBranding(v4)))
|
|
213
|
-
);
|
|
214
100
|
}
|
|
215
101
|
|
|
216
|
-
// src/themes/brand-extract.ts
|
|
102
|
+
// src/themes/extract/brand-extract.ts
|
|
217
103
|
import JSZip from "jszip";
|
|
104
|
+
var EXTRACTED_THEME_MENU = {
|
|
105
|
+
cover: { face: "poster-center" },
|
|
106
|
+
chapter: { face: "masthead-chapter" },
|
|
107
|
+
content: {
|
|
108
|
+
points: { face: "two-column" },
|
|
109
|
+
list: { face: "bento-panel" },
|
|
110
|
+
comparison: { face: "two-column" },
|
|
111
|
+
process: { face: "rail-numbered" },
|
|
112
|
+
data: { face: "bento-panel" },
|
|
113
|
+
photo: { face: "asymmetric-triptych" },
|
|
114
|
+
statement: { face: "statement" },
|
|
115
|
+
quote: { face: "pull-quote" },
|
|
116
|
+
fact: { face: "stat-hero" },
|
|
117
|
+
evidence: { face: "one-evidence" },
|
|
118
|
+
hierarchy: { face: "two-column" }
|
|
119
|
+
},
|
|
120
|
+
ending: { face: "poster-ending" }
|
|
121
|
+
};
|
|
218
122
|
var ACCENT_SLOTS = ["accent1", "accent2", "accent3", "accent4", "accent5", "accent6"];
|
|
219
123
|
var THEME_PART_RE = /theme\d*\.xml$/;
|
|
220
124
|
var THEME_VARIANTS_SEGMENT = "themeVariants";
|
|
@@ -252,15 +156,22 @@ function resolveBgText(dk1, lt1) {
|
|
|
252
156
|
}
|
|
253
157
|
var MUTED_STEPS = 20;
|
|
254
158
|
var MUTED_TARGET_RATIO = 4.5;
|
|
255
|
-
function
|
|
159
|
+
function findMutedCandidate(text, bg, surface) {
|
|
256
160
|
for (let step = MUTED_STEPS; step >= 0; step--) {
|
|
257
161
|
const t = step / MUTED_STEPS;
|
|
258
162
|
const candidate = mixHex(text, bg, t);
|
|
259
163
|
if (contrastRatio(candidate, bg) >= MUTED_TARGET_RATIO && contrastRatio(candidate, surface) >= MUTED_TARGET_RATIO) {
|
|
260
|
-
return candidate;
|
|
164
|
+
return candidate.toUpperCase();
|
|
261
165
|
}
|
|
262
166
|
}
|
|
263
|
-
return
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
function deriveMuted(text, bg, surface) {
|
|
170
|
+
const candidate = findMutedCandidate(text, bg, surface);
|
|
171
|
+
if (candidate !== void 0) return candidate;
|
|
172
|
+
throw new PptwiseError(
|
|
173
|
+
`cannot derive colors.muted with a contrast ratio of ${MUTED_TARGET_RATIO.toFixed(1)}:1 against both colors.bg (${bg}) and colors.surface (${surface}) from colors.text (${text}). Choose compatible background and text anchors`
|
|
174
|
+
);
|
|
264
175
|
}
|
|
265
176
|
var SERIF_HINTS = [
|
|
266
177
|
"times",
|
|
@@ -333,124 +244,259 @@ async function extractBrandTheme(bytes, opts = {}) {
|
|
|
333
244
|
ending: { kind: "color", value: bg }
|
|
334
245
|
}
|
|
335
246
|
};
|
|
336
|
-
return { id, label, style, brand: {},
|
|
247
|
+
return { version: 2, id, label, style, brand: {}, menu: EXTRACTED_THEME_MENU };
|
|
337
248
|
}
|
|
338
249
|
|
|
339
250
|
// src/themes/brand-theme-file.ts
|
|
340
|
-
|
|
341
|
-
var HexToken = z2.string().regex(/^#[0-9A-Fa-f]{3,8}$/, "expected a hex color like #RRGGBB");
|
|
342
|
-
var BackgroundSpecFileSchema = z2.discriminatedUnion("kind", [
|
|
343
|
-
z2.object({ kind: z2.literal("color"), value: HexToken }).strict(),
|
|
344
|
-
z2.object({
|
|
345
|
-
kind: z2.literal("gradient"),
|
|
346
|
-
from: HexToken,
|
|
347
|
-
to: HexToken,
|
|
348
|
-
direction: z2.enum(["tb", "lr", "diagonal"]).optional()
|
|
349
|
-
}).strict(),
|
|
350
|
-
z2.object({
|
|
351
|
-
kind: z2.literal("asset"),
|
|
352
|
-
asset_id: z2.string(),
|
|
353
|
-
overlay: z2.object({ color: HexToken, opacity: z2.number().min(0).max(1) }).strict().optional(),
|
|
354
|
-
fit: z2.enum(["cover", "contain"]).optional()
|
|
355
|
-
}).strict()
|
|
356
|
-
]);
|
|
357
|
-
var StyleTokensFileSchema = z2.object({
|
|
358
|
-
id: z2.string().min(1),
|
|
359
|
-
allowCustomBackground: z2.boolean().optional(),
|
|
360
|
-
colors: z2.object({
|
|
361
|
-
bg: HexToken,
|
|
362
|
-
surface: HexToken,
|
|
363
|
-
panel: HexToken.optional(),
|
|
364
|
-
primary: HexToken,
|
|
365
|
-
accent: HexToken,
|
|
366
|
-
text: HexToken,
|
|
367
|
-
muted: HexToken,
|
|
368
|
-
border: HexToken.optional(),
|
|
369
|
-
chartPalette: z2.array(HexToken).min(1),
|
|
370
|
-
accentPool: z2.array(HexToken).min(1).optional(),
|
|
371
|
-
cardStroke: HexToken.optional()
|
|
372
|
-
}).strict(),
|
|
373
|
-
fonts: z2.object({
|
|
374
|
-
heading: z2.array(z2.string()).min(1),
|
|
375
|
-
body: z2.array(z2.string()).min(1),
|
|
376
|
-
mono: z2.array(z2.string()).min(1).optional()
|
|
377
|
-
}).strict(),
|
|
378
|
-
shape: z2.object({
|
|
379
|
-
radius: z2.number().min(0).max(32).optional(),
|
|
380
|
-
gapScale: z2.number().min(0.8).max(1.3).optional(),
|
|
381
|
-
typeScale: z2.number().min(0.5).max(2).optional()
|
|
382
|
-
}).strict().optional(),
|
|
383
|
-
defaultBackgrounds: z2.object({
|
|
384
|
-
cover: BackgroundSpecFileSchema,
|
|
385
|
-
chapter: BackgroundSpecFileSchema,
|
|
386
|
-
content: BackgroundSpecFileSchema,
|
|
387
|
-
ending: BackgroundSpecFileSchema
|
|
388
|
-
}).strict()
|
|
389
|
-
}).strict();
|
|
390
|
-
var BrandThemeFileSchema = z2.object({
|
|
391
|
-
id: z2.string().min(1),
|
|
392
|
-
label: z2.string().optional(),
|
|
393
|
-
style: StyleTokensFileSchema,
|
|
394
|
-
brand: BrandConfigSchema.optional(),
|
|
395
|
-
tags: z2.array(z2.string()).optional()
|
|
396
|
-
}).strict();
|
|
251
|
+
var BrandThemeFileSchema = ThemeFileSchema;
|
|
397
252
|
function parseBrandThemeFile(raw, source) {
|
|
398
|
-
const
|
|
399
|
-
if (!
|
|
400
|
-
const detail =
|
|
401
|
-
throw new PptwiseError(
|
|
402
|
-
${
|
|
253
|
+
const result = ThemeFileSchema.safeParse(raw);
|
|
254
|
+
if (!result.success) {
|
|
255
|
+
const detail = result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
256
|
+
throw new PptwiseError(
|
|
257
|
+
`invalid theme file ${source}: current theme format is version 2 and every file is self-contained.
|
|
258
|
+
${detail}`
|
|
259
|
+
);
|
|
403
260
|
}
|
|
404
|
-
return
|
|
261
|
+
return result.data;
|
|
405
262
|
}
|
|
406
263
|
function registerBrandThemeFile(file) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
264
|
+
installThemeFile(file);
|
|
265
|
+
return file.id;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/cli/theme-fork.ts
|
|
269
|
+
function clamp(value, min = 0, max = 1) {
|
|
270
|
+
return Math.min(max, Math.max(min, value));
|
|
271
|
+
}
|
|
272
|
+
function hexToRgb(hex) {
|
|
273
|
+
const canonical = HexTokenSchema.parse(hex);
|
|
274
|
+
const n = Number.parseInt(canonical.slice(1), 16);
|
|
275
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
276
|
+
}
|
|
277
|
+
function hexToHsl(hex) {
|
|
278
|
+
const [r, g, b] = hexToRgb(hex);
|
|
279
|
+
const max = Math.max(r, g, b);
|
|
280
|
+
const min = Math.min(r, g, b);
|
|
281
|
+
const delta = max - min;
|
|
282
|
+
const l = (max + min) / 2;
|
|
283
|
+
if (delta === 0) return { h: 0, s: 0, l };
|
|
284
|
+
const s = delta / (1 - Math.abs(2 * l - 1));
|
|
285
|
+
let h = 0;
|
|
286
|
+
if (max === r) h = 60 * ((g - b) / delta % 6);
|
|
287
|
+
else if (max === g) h = 60 * ((b - r) / delta + 2);
|
|
288
|
+
else h = 60 * ((r - g) / delta + 4);
|
|
289
|
+
return { h: (h + 360) % 360, s, l };
|
|
290
|
+
}
|
|
291
|
+
function hslToHex({ h, s, l }) {
|
|
292
|
+
const hue = (h % 360 + 360) % 360;
|
|
293
|
+
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
294
|
+
const segment = hue / 60;
|
|
295
|
+
const x = chroma * (1 - Math.abs(segment % 2 - 1));
|
|
296
|
+
const [r1, g1, b1] = segment < 1 ? [chroma, x, 0] : segment < 2 ? [x, chroma, 0] : segment < 3 ? [0, chroma, x] : segment < 4 ? [0, x, chroma] : segment < 5 ? [x, 0, chroma] : [chroma, 0, x];
|
|
297
|
+
const m = l - chroma / 2;
|
|
298
|
+
const channel = (value) => Math.round((value + m) * 255).toString(16).padStart(2, "0");
|
|
299
|
+
return `#${channel(r1)}${channel(g1)}${channel(b1)}`.toUpperCase();
|
|
300
|
+
}
|
|
301
|
+
function hueDelta(from, to) {
|
|
302
|
+
return (to - from + 540) % 360 - 180;
|
|
303
|
+
}
|
|
304
|
+
function rebaseRelativeColor(color, sourcePlane, targetPlane, sourceText, targetText) {
|
|
305
|
+
const value = hexToHsl(color);
|
|
306
|
+
const source = hexToHsl(sourcePlane);
|
|
307
|
+
const target = hexToHsl(targetPlane);
|
|
308
|
+
const oldText = hexToHsl(sourceText);
|
|
309
|
+
const newText = hexToHsl(targetText);
|
|
310
|
+
const sourceRange = oldText.l - source.l;
|
|
311
|
+
const progress = Math.abs(sourceRange) < 1e-6 ? 0 : (value.l - source.l) / sourceRange;
|
|
312
|
+
const lightness = clamp(target.l + progress * (newText.l - target.l));
|
|
313
|
+
const offset = source.s > 0.02 && value.s > 0.02 ? hueDelta(source.h, value.h) : 0;
|
|
314
|
+
const hue = target.s > 0.02 ? target.h + offset : value.h;
|
|
315
|
+
const saturation = clamp(target.s + value.s - source.s);
|
|
316
|
+
return hslToHex({ h: hue, s: saturation, l: lightness });
|
|
317
|
+
}
|
|
318
|
+
var ORDINARY_BACKGROUND_ANCHORS = ["bg", "surface", "primary", "accent", "text"];
|
|
319
|
+
var CHAPTER_BACKGROUND_ANCHORS = ["primary", "bg", "surface", "accent", "text"];
|
|
320
|
+
function rgbDistance(a, b) {
|
|
321
|
+
const [ar, ag, ab] = hexToRgb(a);
|
|
322
|
+
const [br, bg, bb] = hexToRgb(b);
|
|
323
|
+
return Math.hypot(ar - br, ag - bg, ab - bb);
|
|
324
|
+
}
|
|
325
|
+
function resolveBackgroundAnchor(color, slideRole, source) {
|
|
326
|
+
const roles = slideRole === "chapter" ? CHAPTER_BACKGROUND_ANCHORS : ORDINARY_BACKGROUND_ANCHORS;
|
|
327
|
+
let best = roles[0];
|
|
328
|
+
let bestDistance = rgbDistance(color, source[best]);
|
|
329
|
+
for (const role of roles.slice(1)) {
|
|
330
|
+
const distance = rgbDistance(color, source[role]);
|
|
331
|
+
if (distance < bestDistance) {
|
|
332
|
+
best = role;
|
|
333
|
+
bestDistance = distance;
|
|
334
|
+
}
|
|
411
335
|
}
|
|
412
|
-
|
|
413
|
-
|
|
336
|
+
return best;
|
|
337
|
+
}
|
|
338
|
+
function deriveBackgroundColor(color, slideRole, source, target) {
|
|
339
|
+
const role = resolveBackgroundAnchor(color, slideRole, source);
|
|
340
|
+
return rebaseRelativeColor(color, source[role], target[role], source.text, target.text);
|
|
341
|
+
}
|
|
342
|
+
function deriveBackground(spec, slideRole, source, target) {
|
|
343
|
+
if (spec.kind === "color") {
|
|
344
|
+
return { ...spec, value: deriveBackgroundColor(spec.value, slideRole, source, target) };
|
|
414
345
|
}
|
|
415
|
-
|
|
346
|
+
if (spec.kind === "gradient") {
|
|
347
|
+
return {
|
|
348
|
+
...spec,
|
|
349
|
+
from: deriveBackgroundColor(spec.from, slideRole, source, target),
|
|
350
|
+
to: deriveBackgroundColor(spec.to, slideRole, source, target)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (spec.overlay !== void 0) {
|
|
354
|
+
return {
|
|
355
|
+
...spec,
|
|
356
|
+
overlay: {
|
|
357
|
+
...spec.overlay,
|
|
358
|
+
color: deriveBackgroundColor(spec.overlay.color, slideRole, source, target)
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return spec;
|
|
363
|
+
}
|
|
364
|
+
function rotatePaletteColor(color, sourceAnchor, targetAnchor) {
|
|
365
|
+
const value = hexToHsl(color);
|
|
366
|
+
const source = hexToHsl(sourceAnchor);
|
|
367
|
+
const target = hexToHsl(targetAnchor);
|
|
368
|
+
if (value.s <= 0.02 || source.s <= 0.02 || target.s <= 0.02) {
|
|
369
|
+
return hslToHex(value);
|
|
370
|
+
}
|
|
371
|
+
return hslToHex({ ...value, h: value.h + hueDelta(source.h, target.h) });
|
|
372
|
+
}
|
|
373
|
+
function derivePaletteSequence(sequence, sourceAnchor, targetAnchor) {
|
|
374
|
+
return [
|
|
375
|
+
HexTokenSchema.parse(targetAnchor),
|
|
376
|
+
...sequence.slice(1).map((color) => rotatePaletteColor(color, sourceAnchor, targetAnchor))
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
var STATUS_CONTRAST_RATIO = 4.5;
|
|
380
|
+
var STATUS_LIGHTNESS_STEPS = 1e3;
|
|
381
|
+
function calibrateStatusColor(role, color, bg, surface) {
|
|
382
|
+
const source = hexToHsl(color);
|
|
383
|
+
const original = hslToHex(source);
|
|
384
|
+
const clearsFloor = (candidate) => contrastRatio(candidate, bg) >= STATUS_CONTRAST_RATIO && contrastRatio(candidate, surface) >= STATUS_CONTRAST_RATIO;
|
|
385
|
+
if (clearsFloor(original)) return original;
|
|
386
|
+
let best;
|
|
387
|
+
for (let step = 0; step <= STATUS_LIGHTNESS_STEPS; step++) {
|
|
388
|
+
const lightness = step / STATUS_LIGHTNESS_STEPS;
|
|
389
|
+
const candidate = hslToHex({ ...source, l: lightness });
|
|
390
|
+
if (!clearsFloor(candidate)) continue;
|
|
391
|
+
const distance = Math.abs(lightness - source.l);
|
|
392
|
+
if (best === void 0 || distance < best.distance) best = { color: candidate, distance };
|
|
393
|
+
}
|
|
394
|
+
if (best !== void 0) return best.color;
|
|
395
|
+
throw new PptwiseError(
|
|
396
|
+
`cannot calibrate colors.${role} to a ${STATUS_CONTRAST_RATIO.toFixed(1)}:1 contrast ratio against both colors.bg (${bg}) and colors.surface (${surface}) while preserving its hue`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
function buildForkedTheme(source, anchors, identity) {
|
|
400
|
+
const bg = HexTokenSchema.parse(anchors.bg ?? source.style.colors.bg);
|
|
401
|
+
const primary = HexTokenSchema.parse(anchors.primary);
|
|
402
|
+
const accent = HexTokenSchema.parse(anchors.accent ?? source.style.colors.accent);
|
|
403
|
+
const text = HexTokenSchema.parse(anchors.text ?? source.style.colors.text);
|
|
404
|
+
const surface = HexTokenSchema.parse(anchors.surface ?? source.style.colors.surface);
|
|
405
|
+
const muted = deriveMuted(text, bg, surface);
|
|
406
|
+
const sourceColors = source.style.colors;
|
|
407
|
+
const sourceAnchors = {
|
|
408
|
+
bg: sourceColors.bg,
|
|
409
|
+
surface: sourceColors.surface,
|
|
410
|
+
text: sourceColors.text,
|
|
411
|
+
primary: sourceColors.primary,
|
|
412
|
+
accent: sourceColors.accent
|
|
413
|
+
};
|
|
414
|
+
const targetAnchors = { bg, surface, text, primary, accent };
|
|
415
|
+
const chartPalette = anchors.chartPalette ?? sourceColors.chartPalette;
|
|
416
|
+
const chartAnchor = anchors.chartPalette === void 0 ? sourceColors.primary : chartPalette[0];
|
|
417
|
+
const colors = {
|
|
418
|
+
...sourceColors,
|
|
419
|
+
bg,
|
|
420
|
+
primary,
|
|
421
|
+
accent,
|
|
422
|
+
text,
|
|
423
|
+
surface,
|
|
424
|
+
muted,
|
|
425
|
+
chartPalette: derivePaletteSequence(chartPalette, chartAnchor, primary)
|
|
426
|
+
};
|
|
427
|
+
if (sourceColors.accentPool !== void 0) {
|
|
428
|
+
colors.accentPool = derivePaletteSequence(sourceColors.accentPool, sourceColors.accent, accent);
|
|
429
|
+
}
|
|
430
|
+
if (sourceColors.panel !== void 0) {
|
|
431
|
+
colors.panel = rebaseRelativeColor(sourceColors.panel, sourceColors.bg, bg, sourceColors.text, text);
|
|
432
|
+
}
|
|
433
|
+
if (sourceColors.border !== void 0) {
|
|
434
|
+
colors.border = rebaseRelativeColor(sourceColors.border, sourceColors.surface, surface, sourceColors.text, text);
|
|
435
|
+
}
|
|
436
|
+
if (sourceColors.cardStroke !== void 0) {
|
|
437
|
+
colors.cardStroke = rebaseRelativeColor(sourceColors.cardStroke, sourceColors.surface, surface, sourceColors.text, text);
|
|
438
|
+
}
|
|
439
|
+
if (sourceColors.danger !== void 0) {
|
|
440
|
+
colors.danger = calibrateStatusColor("danger", sourceColors.danger, bg, surface);
|
|
441
|
+
}
|
|
442
|
+
if (sourceColors.warning !== void 0) {
|
|
443
|
+
colors.warning = calibrateStatusColor("warning", sourceColors.warning, bg, surface);
|
|
444
|
+
}
|
|
445
|
+
if (sourceColors.success !== void 0) {
|
|
446
|
+
colors.success = calibrateStatusColor("success", sourceColors.success, bg, surface);
|
|
447
|
+
}
|
|
448
|
+
const defaultBackgrounds = {
|
|
449
|
+
cover: deriveBackground(source.style.defaultBackgrounds.cover, "cover", sourceAnchors, targetAnchors),
|
|
450
|
+
chapter: deriveBackground(source.style.defaultBackgrounds.chapter, "chapter", sourceAnchors, targetAnchors),
|
|
451
|
+
content: deriveBackground(source.style.defaultBackgrounds.content, "content", sourceAnchors, targetAnchors),
|
|
452
|
+
ending: deriveBackground(source.style.defaultBackgrounds.ending, "ending", sourceAnchors, targetAnchors)
|
|
453
|
+
};
|
|
454
|
+
const file = {
|
|
455
|
+
version: 2,
|
|
456
|
+
id: identity.id,
|
|
457
|
+
label: identity.label ?? source.label,
|
|
458
|
+
style: {
|
|
459
|
+
...source.style,
|
|
460
|
+
id: identity.id,
|
|
461
|
+
colors,
|
|
462
|
+
fonts: identity.fonts ?? source.style.fonts,
|
|
463
|
+
defaultBackgrounds
|
|
464
|
+
},
|
|
465
|
+
brand: source.brand,
|
|
466
|
+
occasions: source.occasions,
|
|
467
|
+
identity: source.identity,
|
|
468
|
+
menu: structuredClone(source.menu)
|
|
469
|
+
};
|
|
470
|
+
return ThemeFileSchema.parse(file);
|
|
471
|
+
}
|
|
472
|
+
function forkTheme(source, anchors, identity) {
|
|
473
|
+
const file = buildForkedTheme(source, anchors, identity);
|
|
474
|
+
assertContrastFloor(file.id, file.style);
|
|
475
|
+
return file;
|
|
416
476
|
}
|
|
417
477
|
|
|
418
478
|
// src/spec/index.ts
|
|
419
|
-
import { z
|
|
420
|
-
var
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
type
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
* (which layouts a given beat favors) are two independent consumers
|
|
441
|
-
* of the same declared value, not two views of one mechanism. */
|
|
442
|
-
beat: z3.enum(BEAT_VALUES).optional(),
|
|
443
|
-
/** Optional authoring hint pointing fill/select at a preferred
|
|
444
|
-
* component type or layout id — see {@link checkFocusVocabulary}. */
|
|
445
|
-
focus: z3.string().optional(),
|
|
446
|
-
/** Free-text content anchor read by the fill step and never validated or
|
|
447
|
-
* interpreted here. Assemble also surfaces it as `subheading` on
|
|
448
|
-
* boundary pages, which have no page-content field for that line. On a
|
|
449
|
-
* filled content page it remains a fill-only authoring hint. */
|
|
450
|
-
summary: z3.string().optional()
|
|
451
|
-
}).strict();
|
|
452
|
-
var DeckSpecSchema = z3.object({
|
|
453
|
-
version: z3.literal("1").default("1"),
|
|
479
|
+
import { z } from "zod";
|
|
480
|
+
var CommonPageSpecFields = {
|
|
481
|
+
id: z.string(),
|
|
482
|
+
heading: z.string(),
|
|
483
|
+
/** Optional authoring hint pointing fill at a preferred content kind or component type, not a face. */
|
|
484
|
+
focus: z.string().optional(),
|
|
485
|
+
/** Free-text content anchor read by the fill step. */
|
|
486
|
+
summary: z.string().optional()
|
|
487
|
+
};
|
|
488
|
+
var PageSpecSchema = z.discriminatedUnion("type", [
|
|
489
|
+
z.object({ type: z.literal("cover"), ...CommonPageSpecFields }).strict(),
|
|
490
|
+
z.object({ type: z.literal("chapter"), ...CommonPageSpecFields }).strict(),
|
|
491
|
+
z.object({
|
|
492
|
+
type: z.literal("content"),
|
|
493
|
+
kind: z.enum(KIND_VALUES),
|
|
494
|
+
...CommonPageSpecFields
|
|
495
|
+
}).strict(),
|
|
496
|
+
z.object({ type: z.literal("ending"), ...CommonPageSpecFields }).strict()
|
|
497
|
+
]);
|
|
498
|
+
var DeckSpecSchema = z.object({
|
|
499
|
+
version: z.literal("1").default("1"),
|
|
454
500
|
// Same open-schema/closed-semantic split as PptxIRSchema's `narrative`
|
|
455
501
|
// field — see `NarrativeProfileInputSchema`'s doc comment in `ir/index.ts`
|
|
456
502
|
// for the full rationale (reused verbatim here, not redefined, so the
|
|
@@ -459,17 +505,18 @@ var DeckSpecSchema = z3.object({
|
|
|
459
505
|
// *value* was already in the new strategy/pacing vocabulary as of task 1
|
|
460
506
|
// (vocabulary-v4 rename) — `resolveNarrative` below is what actually
|
|
461
507
|
// enforces that.
|
|
462
|
-
narrative:
|
|
463
|
-
theme:
|
|
464
|
-
|
|
465
|
-
|
|
508
|
+
narrative: z.union([z.string(), NarrativeProfileInputSchema]).optional(),
|
|
509
|
+
theme: z.string({
|
|
510
|
+
error: (iss) => iss.input === void 0 ? THEME_REQUIRED_MESSAGE : void 0
|
|
511
|
+
}).regex(THEME_ID_PATTERN, THEME_ID_CONSTRAINT),
|
|
512
|
+
filename: z.string().optional(),
|
|
466
513
|
meta: MetaSchema.default({}),
|
|
467
514
|
/** Deck logo placement — reused verbatim from the IR's own `brand` field
|
|
468
515
|
* (`BrandSchema`, `../ir`) so the deck spec and IR can't drift apart on
|
|
469
516
|
* shape, same pattern as `meta` just above. Unlike `meta`, no
|
|
470
517
|
* `.default({})`: IR's own `brand` field is a bare `.optional()` with no
|
|
471
518
|
* default either (`undefined` means "no brand", not "an empty brand
|
|
472
|
-
* object") — consumed by `Branding` (`src/
|
|
519
|
+
* object") — consumed by `Branding` (`src/render/branding.tsx`) for
|
|
473
520
|
* the deck's logo image and corner position. */
|
|
474
521
|
brand: BrandSchema.optional(),
|
|
475
522
|
/**
|
|
@@ -479,14 +526,14 @@ var DeckSpecSchema = z3.object({
|
|
|
479
526
|
* does not write `"cover-only"` into the IR. The renderer treats that
|
|
480
527
|
* as `"cover-only"`. Omitted by default. Write `"full"` only when every
|
|
481
528
|
* content page needs the brand footer. `"full"` also paints confidentiality
|
|
482
|
-
* and date on cover and ending meta rows.
|
|
483
|
-
*
|
|
529
|
+
* and date on cover and ending meta rows. A silent menu entry can suppress
|
|
530
|
+
* branding for its own page.
|
|
484
531
|
*/
|
|
485
532
|
branding: DeckBrandingSchema.optional(),
|
|
486
|
-
pages:
|
|
533
|
+
pages: z.array(PageSpecSchema)
|
|
487
534
|
}).strict();
|
|
488
535
|
function specJsonSchema() {
|
|
489
|
-
return
|
|
536
|
+
return z.toJSONSchema(DeckSpecSchema);
|
|
490
537
|
}
|
|
491
538
|
function formatSpecIssues(errors) {
|
|
492
539
|
return errors.map((e) => e.pageId ? `page "${e.pageId}" \u2014 ${e.path}: ${e.message}` : `${e.path}: ${e.message}`).join("\n");
|
|
@@ -496,7 +543,7 @@ function formatInvalidSpecError(errors) {
|
|
|
496
543
|
${formatSpecIssues(errors)}`;
|
|
497
544
|
}
|
|
498
545
|
function resolveSpecThemeId(spec) {
|
|
499
|
-
return spec.theme
|
|
546
|
+
return spec.theme;
|
|
500
547
|
}
|
|
501
548
|
function checkPagesNonEmpty(spec) {
|
|
502
549
|
if (spec.pages.length > 0) return [];
|
|
@@ -592,12 +639,11 @@ function checkTheme(spec) {
|
|
|
592
639
|
const themeId = resolveSpecThemeId(spec);
|
|
593
640
|
const installed = getInstalledThemeIds();
|
|
594
641
|
if (installed.includes(themeId)) return [];
|
|
595
|
-
const message = themeId === "bloom" ? 'theme id "bloom" was removed \u2014
|
|
642
|
+
const message = themeId === "bloom" ? 'theme id "bloom" was removed \u2014 current format uses an installed theme id (see `pptwise themes`)' : `unknown theme "${themeId}" \u2014 available: ${installed.join(", ")} (see \`pptwise themes\`)`;
|
|
596
643
|
return [{ path: "theme", message }];
|
|
597
644
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const tendencies = STRATEGY_DEFINITIONS[strategy].tendencies;
|
|
645
|
+
function checkFocusVocabulary(spec) {
|
|
646
|
+
const kinds = KIND_VALUES;
|
|
601
647
|
const errors = [];
|
|
602
648
|
spec.pages.forEach((page, i) => {
|
|
603
649
|
if (page.focus === void 0) return;
|
|
@@ -605,7 +651,7 @@ function checkFocusVocabulary(spec, strategy) {
|
|
|
605
651
|
errors.push({
|
|
606
652
|
path: `pages.${i}.focus`,
|
|
607
653
|
pageId: page.id,
|
|
608
|
-
message: 'component type "logo_wall"
|
|
654
|
+
message: 'component type "logo_wall" is not in the current vocabulary \u2014 use "image_grid"'
|
|
609
655
|
});
|
|
610
656
|
return;
|
|
611
657
|
}
|
|
@@ -613,99 +659,65 @@ function checkFocusVocabulary(spec, strategy) {
|
|
|
613
659
|
errors.push({
|
|
614
660
|
path: `pages.${i}.focus`,
|
|
615
661
|
pageId: page.id,
|
|
616
|
-
message: '
|
|
662
|
+
message: '"banner-heading" is a face id, not authoring vocabulary \u2014 focus accepts a content kind or a component type'
|
|
617
663
|
});
|
|
618
664
|
return;
|
|
619
665
|
}
|
|
620
|
-
if (
|
|
666
|
+
if (kinds.includes(page.focus) || COMPONENT_TYPES.includes(page.focus)) {
|
|
621
667
|
return;
|
|
622
668
|
}
|
|
623
669
|
errors.push({
|
|
624
670
|
path: `pages.${i}.focus`,
|
|
625
671
|
pageId: page.id,
|
|
626
|
-
message: `unknown focus "${page.focus}"
|
|
672
|
+
message: `unknown focus "${page.focus}" \u2014 expected a content kind (${KIND_VALUES.join(", ")}) or a component type (${COMPONENT_TYPES.join(", ")})`
|
|
627
673
|
});
|
|
628
674
|
});
|
|
629
675
|
return errors;
|
|
630
676
|
}
|
|
631
|
-
function
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
677
|
+
function checkThemeMenuKinds(spec) {
|
|
678
|
+
const menu = getThemeDefinition(resolveSpecThemeId(spec)).menu;
|
|
679
|
+
if (menu === void 0) {
|
|
680
|
+
return [
|
|
681
|
+
{
|
|
682
|
+
path: "theme",
|
|
683
|
+
message: `theme "${resolveSpecThemeId(spec)}" has no menu`
|
|
684
|
+
}
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
const offered = offeredContentKinds(menu);
|
|
688
|
+
return spec.pages.flatMap((page, index) => {
|
|
689
|
+
if (page.type !== "content" || resolveLayoutId("content", page.kind, menu) !== null) return [];
|
|
690
|
+
return [
|
|
691
|
+
{
|
|
692
|
+
path: `pages.${index}.kind`,
|
|
693
|
+
pageId: page.id,
|
|
694
|
+
message: `kind "${page.kind}" is not offered by theme "${resolveSpecThemeId(
|
|
695
|
+
spec
|
|
696
|
+
)}". Available content kinds: ${offered.join(", ")}`
|
|
697
|
+
}
|
|
698
|
+
];
|
|
637
699
|
});
|
|
638
|
-
return result;
|
|
639
700
|
}
|
|
640
|
-
function
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
701
|
+
function checkKindDistribution(spec) {
|
|
702
|
+
const contentPages = spec.pages.map((page, index) => ({ page, index })).filter(
|
|
703
|
+
(entry) => entry.page.type === "content"
|
|
704
|
+
);
|
|
705
|
+
const warnings = [];
|
|
706
|
+
let start = 0;
|
|
707
|
+
while (start < contentPages.length) {
|
|
708
|
+
let end = start + 1;
|
|
709
|
+
while (end < contentPages.length && contentPages[end].page.kind === contentPages[start].page.kind) end++;
|
|
710
|
+
if (end - start >= 3) {
|
|
711
|
+
const run = contentPages.slice(start, end);
|
|
712
|
+
warnings.push({
|
|
651
713
|
path: "pages",
|
|
652
|
-
pageId:
|
|
653
|
-
message: `${
|
|
714
|
+
pageId: run[0].page.id,
|
|
715
|
+
message: `${run.length} consecutive content pages use kind "${run[0].page.kind}" (${run.map((entry) => entry.page.id).join(", ")}). Vary the explanation pattern where the argument allows it`
|
|
654
716
|
});
|
|
655
717
|
}
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
return errors;
|
|
659
|
-
}
|
|
660
|
-
function checkAnchorOpenPolicy(spec, strategy) {
|
|
661
|
-
const firstContentIndex = spec.pages.findIndex((page) => page.type === "content");
|
|
662
|
-
if (firstContentIndex === -1) return [];
|
|
663
|
-
const firstContent = spec.pages[firstContentIndex];
|
|
664
|
-
if (firstContent.beat === void 0 || firstContent.beat === "anchor") return [];
|
|
665
|
-
return [
|
|
666
|
-
{
|
|
667
|
-
path: `pages.${firstContentIndex}.beat`,
|
|
668
|
-
pageId: firstContent.id,
|
|
669
|
-
message: `first content page declares beat "${firstContent.beat}" \u2014 strategy "${strategy}" requires the deck to open its first content page on "anchor" beat when a beat is declared`
|
|
670
|
-
}
|
|
671
|
-
];
|
|
672
|
-
}
|
|
673
|
-
function checkAnchorSparsePolicy(spec, strategy) {
|
|
674
|
-
const declared = declaredBeatContentPages(spec);
|
|
675
|
-
if (declared.length === 0) return [];
|
|
676
|
-
const anchorPages = declared.filter((page) => page.beat === "anchor");
|
|
677
|
-
if (anchorPages.length / declared.length <= 0.5) return [];
|
|
678
|
-
const pct = Math.round(anchorPages.length / declared.length * 100);
|
|
679
|
-
return [
|
|
680
|
-
{
|
|
681
|
-
path: "pages",
|
|
682
|
-
// First offending anchor page, same "representative pageId" shape
|
|
683
|
-
// checkAlternatePolicy's own issue carries (members[0]!.id there) —
|
|
684
|
-
// this gate's violation is deck-wide (a ratio, not one page), but a
|
|
685
|
-
// representative id still gives a CLI/agent caller something to jump
|
|
686
|
-
// to rather than only a bare "pages" path.
|
|
687
|
-
pageId: anchorPages[0].id,
|
|
688
|
-
message: `${anchorPages.length} of ${declared.length} content pages with a declared beat are "anchor" (${pct}%: ${anchorPages.map((page) => page.id).join(", ")}) \u2014 strategy "${strategy}" requires "anchor" to stay a minority of declared beats, vary some to "dense" or "breathing"`
|
|
689
|
-
}
|
|
690
|
-
];
|
|
691
|
-
}
|
|
692
|
-
function checkBeatRotation(spec, strategy) {
|
|
693
|
-
const policy = STRATEGY_DEFINITIONS[strategy].beatPolicy;
|
|
694
|
-
switch (policy) {
|
|
695
|
-
case "uniform-dense":
|
|
696
|
-
case "repetition-ok":
|
|
697
|
-
return [];
|
|
698
|
-
case "alternate":
|
|
699
|
-
return checkAlternatePolicy(spec, strategy);
|
|
700
|
-
case "anchor-open":
|
|
701
|
-
return checkAnchorOpenPolicy(spec, strategy);
|
|
702
|
-
case "anchor-sparse":
|
|
703
|
-
return checkAnchorSparsePolicy(spec, strategy);
|
|
704
|
-
default: {
|
|
705
|
-
const exhaustive = policy;
|
|
706
|
-
throw new Error(`unhandled beat policy: ${String(exhaustive)}`);
|
|
707
|
-
}
|
|
718
|
+
start = end;
|
|
708
719
|
}
|
|
720
|
+
return warnings;
|
|
709
721
|
}
|
|
710
722
|
var SPEC_PAGE_COUNT_RANGE = {
|
|
711
723
|
dense: { min: 8, max: 30 },
|
|
@@ -743,7 +755,11 @@ function validateSpec(input) {
|
|
|
743
755
|
const errors = r.error.issues.map((issue) => {
|
|
744
756
|
const path = issue.path.join(".");
|
|
745
757
|
const m = /^pages\.(\d+)/.exec(path);
|
|
746
|
-
return {
|
|
758
|
+
return {
|
|
759
|
+
path,
|
|
760
|
+
message: themeIssueMessage(path, issue.message, issue.input, issue.code),
|
|
761
|
+
pageId: m ? pageIdFromRawInput(normalizedInput, Number(m[1])) : void 0
|
|
762
|
+
};
|
|
747
763
|
});
|
|
748
764
|
return withNormalized({ ok: false, errors });
|
|
749
765
|
}
|
|
@@ -758,6 +774,8 @@ function validateSpec(input) {
|
|
|
758
774
|
if (headingErrors.length > 0) return withNormalized({ ok: false, errors: headingErrors });
|
|
759
775
|
const themeErrors = checkTheme(spec);
|
|
760
776
|
if (themeErrors.length > 0) return withNormalized({ ok: false, errors: themeErrors });
|
|
777
|
+
const menuErrors = checkThemeMenuKinds(spec);
|
|
778
|
+
if (menuErrors.length > 0) return withNormalized({ ok: false, errors: menuErrors });
|
|
761
779
|
let resolvedAxes;
|
|
762
780
|
try {
|
|
763
781
|
resolvedAxes = resolveNarrative(spec.narrative);
|
|
@@ -765,30 +783,48 @@ function validateSpec(input) {
|
|
|
765
783
|
if (!(err instanceof PptwiseError)) throw err;
|
|
766
784
|
return withNormalized({ ok: false, errors: [{ path: "narrative", message: err.message }] });
|
|
767
785
|
}
|
|
768
|
-
const
|
|
769
|
-
if (beatErrors.length > 0) return withNormalized({ ok: false, errors: beatErrors });
|
|
770
|
-
const focusErrors = checkFocusVocabulary(spec, resolvedAxes.strategy);
|
|
786
|
+
const focusErrors = checkFocusVocabulary(spec);
|
|
771
787
|
if (focusErrors.length > 0) return withNormalized({ ok: false, errors: focusErrors });
|
|
772
788
|
const pageCountErrors = checkPageCount(spec, resolvedAxes.pacing);
|
|
773
789
|
if (pageCountErrors.length > 0) return withNormalized({ ok: false, errors: pageCountErrors });
|
|
774
|
-
|
|
790
|
+
const warnings = checkKindDistribution(spec);
|
|
791
|
+
return withNormalized({
|
|
792
|
+
ok: true,
|
|
793
|
+
spec,
|
|
794
|
+
errors: [],
|
|
795
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
796
|
+
});
|
|
775
797
|
}
|
|
776
798
|
|
|
777
799
|
// src/spec/assemble.ts
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
800
|
+
var LOCKED_KEYS = ["type", "kind", "heading"];
|
|
801
|
+
function buildSlide(page, content) {
|
|
802
|
+
const locked = {
|
|
803
|
+
id: page.id,
|
|
804
|
+
type: page.type,
|
|
805
|
+
heading: page.heading,
|
|
806
|
+
...page.type === "content" ? { kind: page.kind } : {}
|
|
807
|
+
};
|
|
808
|
+
if (content === void 0) {
|
|
809
|
+
return {
|
|
810
|
+
...locked,
|
|
811
|
+
placeholder: true,
|
|
812
|
+
...page.summary !== void 0 ? { subheading: page.summary } : {}
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
...locked,
|
|
817
|
+
...page.type !== "content" && page.summary !== void 0 ? { subheading: page.summary } : {},
|
|
818
|
+
...content.components !== void 0 ? { components: content.components } : {},
|
|
819
|
+
...content.background !== void 0 ? { background: content.background } : {},
|
|
820
|
+
...content.image_side !== void 0 ? { image_side: content.image_side } : {},
|
|
821
|
+
...content.footnote !== void 0 ? { footnote: content.footnote } : {},
|
|
822
|
+
...content.notes !== void 0 ? { notes: content.notes } : {}
|
|
823
|
+
};
|
|
785
824
|
}
|
|
786
|
-
var LOCKED_KEYS = ["type", "heading"];
|
|
787
825
|
function assembleDeck(spec, pages) {
|
|
788
826
|
const validated = validateSpec(spec);
|
|
789
|
-
if (!validated.ok)
|
|
790
|
-
throw new PptwiseError(formatInvalidSpecError(validated.errors));
|
|
791
|
-
}
|
|
827
|
+
if (!validated.ok) throw new PptwiseError(formatInvalidSpecError(validated.errors));
|
|
792
828
|
const deckSpec = validated.spec;
|
|
793
829
|
for (const page of deckSpec.pages) {
|
|
794
830
|
const raw = pages[page.id];
|
|
@@ -798,7 +834,7 @@ function assembleDeck(spec, pages) {
|
|
|
798
834
|
}
|
|
799
835
|
for (const key of LOCKED_KEYS) {
|
|
800
836
|
if (Object.hasOwn(raw, key)) {
|
|
801
|
-
throw new PptwiseError(`page "${page.id}": "${key}" is locked by the spec
|
|
837
|
+
throw new PptwiseError(`page "${page.id}": "${key}" is locked by the spec. Remove it from the page file`);
|
|
802
838
|
}
|
|
803
839
|
}
|
|
804
840
|
}
|
|
@@ -806,22 +842,18 @@ function assembleDeck(spec, pages) {
|
|
|
806
842
|
const orphanIds = Object.keys(pages).filter((id) => !specIds.has(id));
|
|
807
843
|
if (orphanIds.length > 0) {
|
|
808
844
|
throw new PptwiseError(
|
|
809
|
-
`orphan page id${orphanIds.length === 1 ? "" : "s"} ${orphanIds.map((id) => `"${id}"`).join(", ")}
|
|
845
|
+
`orphan page id${orphanIds.length === 1 ? "" : "s"} ${orphanIds.map((id) => `"${id}"`).join(", ")}. Delete the page file or add the page to the spec`
|
|
810
846
|
);
|
|
811
847
|
}
|
|
812
|
-
const slides = deckSpec.pages.map((page) => buildSlide(page, pages[page.id]));
|
|
813
|
-
const generatedSeed = deckSpec.seed === void 0 ? generateSeed(deckSpec.filename, deckSpec.pages.map((page) => page.id)) : void 0;
|
|
814
|
-
const seed = deckSpec.seed ?? generatedSeed;
|
|
815
848
|
const rawIr = {
|
|
816
|
-
version: "
|
|
849
|
+
version: "5",
|
|
817
850
|
...deckSpec.narrative !== void 0 ? { narrative: deckSpec.narrative } : {},
|
|
818
|
-
|
|
851
|
+
theme: { id: deckSpec.theme },
|
|
819
852
|
...deckSpec.filename !== void 0 ? { filename: deckSpec.filename } : {},
|
|
820
853
|
...deckSpec.brand !== void 0 ? { brand: deckSpec.brand } : {},
|
|
821
854
|
...deckSpec.branding !== void 0 ? { branding: deckSpec.branding } : {},
|
|
822
855
|
meta: deckSpec.meta,
|
|
823
|
-
|
|
824
|
-
slides
|
|
856
|
+
slides: deckSpec.pages.map((page) => buildSlide(page, pages[page.id]))
|
|
825
857
|
};
|
|
826
858
|
const parsed = PptxIRSchema.safeParse(rawIr);
|
|
827
859
|
if (!parsed.success) {
|
|
@@ -829,49 +861,7 @@ function assembleDeck(spec, pages) {
|
|
|
829
861
|
throw new PptwiseError(`assembled deck did not produce valid IR:
|
|
830
862
|
${detail}`);
|
|
831
863
|
}
|
|
832
|
-
|
|
833
|
-
return {
|
|
834
|
-
ir,
|
|
835
|
-
...generatedSeed !== void 0 ? { generatedSeed } : {},
|
|
836
|
-
...materializedCount > 0 ? { materializedLayoutCount: materializedCount } : {}
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
function materializeEffectiveLayouts(ir) {
|
|
840
|
-
let materializedCount = 0;
|
|
841
|
-
const slides = ir.slides.map((slide, index) => {
|
|
842
|
-
if (slide.layout !== void 0) return slide;
|
|
843
|
-
const effectiveLayoutId = resolveEffectiveLayoutId(ir, slide, index);
|
|
844
|
-
if (effectiveLayoutId === null) return slide;
|
|
845
|
-
materializedCount++;
|
|
846
|
-
return { ...slide, layout: effectiveLayoutId };
|
|
847
|
-
});
|
|
848
|
-
return materializedCount === 0 ? { ir, materializedCount } : { ir: { ...ir, slides }, materializedCount };
|
|
849
|
-
}
|
|
850
|
-
function buildSlide(page, raw) {
|
|
851
|
-
if (raw === void 0) {
|
|
852
|
-
return {
|
|
853
|
-
id: page.id,
|
|
854
|
-
type: page.type,
|
|
855
|
-
heading: page.heading,
|
|
856
|
-
placeholder: true,
|
|
857
|
-
...page.beat !== void 0 ? { beat: page.beat } : {},
|
|
858
|
-
...page.summary !== void 0 ? { subheading: page.summary } : {}
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
return {
|
|
862
|
-
id: page.id,
|
|
863
|
-
type: page.type,
|
|
864
|
-
heading: page.heading,
|
|
865
|
-
...page.beat !== void 0 ? { beat: page.beat } : {},
|
|
866
|
-
...page.type !== "content" && page.summary !== void 0 ? { subheading: page.summary } : {},
|
|
867
|
-
...raw.components !== void 0 ? { components: raw.components } : {},
|
|
868
|
-
...raw.layout !== void 0 ? { layout: raw.layout } : {},
|
|
869
|
-
...raw.arrangement !== void 0 ? { arrangement: raw.arrangement } : {},
|
|
870
|
-
...raw.background !== void 0 ? { background: raw.background } : {},
|
|
871
|
-
...raw.image_side !== void 0 ? { image_side: raw.image_side } : {},
|
|
872
|
-
...raw.footnote !== void 0 ? { footnote: raw.footnote } : {},
|
|
873
|
-
...raw.notes !== void 0 ? { notes: raw.notes } : {}
|
|
874
|
-
};
|
|
864
|
+
return { ir: parsed.data };
|
|
875
865
|
}
|
|
876
866
|
var UNTITLED_HEADING = "Untitled";
|
|
877
867
|
function disassembleDeck(ir) {
|
|
@@ -883,33 +873,29 @@ function disassembleDeck(ir) {
|
|
|
883
873
|
id,
|
|
884
874
|
type: slide.type,
|
|
885
875
|
heading,
|
|
886
|
-
...slide.
|
|
876
|
+
...slide.type === "content" ? { kind: slide.kind } : {},
|
|
887
877
|
...(slide.placeholder === true || slide.type !== "content") && slide.subheading !== void 0 ? { summary: slide.subheading } : {}
|
|
888
878
|
};
|
|
889
879
|
if (slide.placeholder !== true) pages[id] = extractPageContent(slide);
|
|
890
880
|
return pageSpec;
|
|
891
881
|
});
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
pages: pageSpecs
|
|
882
|
+
return {
|
|
883
|
+
spec: {
|
|
884
|
+
version: "1",
|
|
885
|
+
...ir.narrative !== void 0 ? { narrative: ir.narrative } : {},
|
|
886
|
+
theme: ir.theme.id,
|
|
887
|
+
filename: ir.filename,
|
|
888
|
+
...ir.brand !== void 0 ? { brand: ir.brand } : {},
|
|
889
|
+
...ir.branding !== void 0 ? { branding: ir.branding } : {},
|
|
890
|
+
meta: ir.meta,
|
|
891
|
+
pages: pageSpecs
|
|
892
|
+
},
|
|
893
|
+
pages
|
|
905
894
|
};
|
|
906
|
-
return { spec, pages };
|
|
907
895
|
}
|
|
908
896
|
function extractPageContent(slide) {
|
|
909
897
|
const content = {};
|
|
910
898
|
if (slide.components.length > 0) content.components = slide.components;
|
|
911
|
-
if (slide.layout !== void 0) content.layout = slide.layout;
|
|
912
|
-
if (slide.arrangement !== void 0) content.arrangement = slide.arrangement;
|
|
913
899
|
if (slide.background !== void 0) content.background = slide.background;
|
|
914
900
|
if (slide.image_side !== void 0) content.image_side = slide.image_side;
|
|
915
901
|
if (slide.footnote !== void 0) content.footnote = slide.footnote;
|
|
@@ -917,30 +903,7 @@ function extractPageContent(slide) {
|
|
|
917
903
|
return content;
|
|
918
904
|
}
|
|
919
905
|
|
|
920
|
-
// src/
|
|
921
|
-
function migrateDeckPlanToSpec(raw) {
|
|
922
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return raw;
|
|
923
|
-
const { scenario, pages, ...rest } = raw;
|
|
924
|
-
const result = { ...rest };
|
|
925
|
-
if (scenario !== void 0) result.narrative = scenario;
|
|
926
|
-
if (Array.isArray(pages)) {
|
|
927
|
-
result.pages = pages.map(migratePageRhythmToBeat);
|
|
928
|
-
} else if (pages !== void 0) {
|
|
929
|
-
result.pages = pages;
|
|
930
|
-
}
|
|
931
|
-
return migrateBannerHeadingToTwoColumn(
|
|
932
|
-
migrateLogoWallToImageGrid(migrateBloomToClassroom(migrateChromeToBranding(result)))
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
function migratePageRhythmToBeat(page) {
|
|
936
|
-
if (typeof page !== "object" || page === null || Array.isArray(page)) return page;
|
|
937
|
-
const { rhythm, ...rest } = page;
|
|
938
|
-
const next = { ...rest };
|
|
939
|
-
if (rhythm !== void 0) next.beat = rhythm;
|
|
940
|
-
return next;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// src/svg/asset-brief.ts
|
|
906
|
+
// src/render/asset-brief.ts
|
|
944
907
|
var DUMMY_PNG_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
|
|
945
908
|
function extractImageFrames(markup) {
|
|
946
909
|
const Parser = getPlatform().domParser ?? globalThis.DOMParser;
|
|
@@ -1039,7 +1002,7 @@ function buildPrompt(mood, palette, frame, fit) {
|
|
|
1039
1002
|
}
|
|
1040
1003
|
function buildAssetBrief(ir) {
|
|
1041
1004
|
const themeDef = getThemeDefinition(ir.theme.id);
|
|
1042
|
-
const tokens = resolveStyle(ir.theme.id
|
|
1005
|
+
const tokens = resolveStyle(ir.theme.id);
|
|
1043
1006
|
const palette = buildPalette(tokens.colors);
|
|
1044
1007
|
const mood = buildMood(ir.theme.id, themeDef);
|
|
1045
1008
|
const occurrences = [];
|
|
@@ -1140,17 +1103,16 @@ function buildAssetBrief(ir) {
|
|
|
1140
1103
|
|
|
1141
1104
|
export {
|
|
1142
1105
|
VERSION,
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
migrateBannerHeadingToTwoColumn,
|
|
1148
|
-
migrateIrV3ToV4,
|
|
1106
|
+
THEME_PRESETS,
|
|
1107
|
+
isThemePresetId,
|
|
1108
|
+
getThemePreset,
|
|
1109
|
+
copyThemePreset,
|
|
1149
1110
|
slugify,
|
|
1150
1111
|
extractBrandTheme,
|
|
1151
1112
|
BrandThemeFileSchema,
|
|
1152
1113
|
parseBrandThemeFile,
|
|
1153
1114
|
registerBrandThemeFile,
|
|
1115
|
+
forkTheme,
|
|
1154
1116
|
PageSpecSchema,
|
|
1155
1117
|
DeckSpecSchema,
|
|
1156
1118
|
specJsonSchema,
|
|
@@ -1161,7 +1123,6 @@ export {
|
|
|
1161
1123
|
validateSpec,
|
|
1162
1124
|
assembleDeck,
|
|
1163
1125
|
disassembleDeck,
|
|
1164
|
-
migrateDeckPlanToSpec,
|
|
1165
1126
|
buildAssetBrief
|
|
1166
1127
|
};
|
|
1167
|
-
//# sourceMappingURL=chunk-
|
|
1128
|
+
//# sourceMappingURL=chunk-2CA3JRFR.js.map
|