@litfamily/litclaude 1.0.19 → 1.0.21
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/CHANGELOG.md +12 -0
- package/README.md +14 -8
- package/README_ko-KR.md +13 -8
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/hooks.md +3 -2
- package/package.json +1 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-hook.js +4 -4
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +91 -31
- package/plugins/litclaude/skills/frontend-ui-ux/ATTRIBUTION.md +3 -6
- package/plugins/litclaude/skills/lit-docx/SKILL.md +50 -19
- package/plugins/litclaude/skills/lit-docx/references/components.md +207 -0
- package/plugins/litclaude/skills/lit-docx/references/direction-step.md +277 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/01-report-ko.md +166 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/02-brief-ko.md +105 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/03-manual-ko.md +164 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/04-proposal-ko.md +138 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/05-memo-en.md +55 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/06-journal-en.md +130 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/07-report-en.md +123 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/08-proposal-en.md +123 -0
- package/plugins/litclaude/skills/lit-docx/references/examples/09-memo-ko.md +45 -0
- package/plugins/litclaude/skills/lit-docx/references/page-composition.md +221 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/brief.md +143 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/journal.md +142 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/manual.md +148 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/memo.md +137 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/proposal.md +140 -0
- package/plugins/litclaude/skills/lit-docx/references/tonalities/report.md +156 -0
- package/plugins/litclaude/skills/lit-docx/scripts/convert_md_to_docx.py +274 -73
- package/plugins/litclaude/skills/lit-docx/scripts/docx_design.py +2449 -0
- package/plugins/litclaude/skills/lit-docx/scripts/docx_layout.py +959 -0
- package/plugins/litclaude/skills/lit-docx/scripts/qa_docx.py +66 -9
- package/plugins/litclaude/skills/lit-docx/scripts/slop_lint.py +18 -6
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/brief.yaml +28 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/journal.yaml +25 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/manual.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/memo.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/proposal.yaml +26 -0
- package/plugins/litclaude/skills/lit-docx/templates/tonalities/report.yaml +26 -0
- package/plugins/litclaude/skills/lit-pptx/SKILL.md +105 -48
- package/plugins/litclaude/skills/lit-pptx/references/agents/analyst.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/agents/thesis.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/anti-slop-checklist.md +59 -30
- package/plugins/litclaude/skills/lit-pptx/references/authoring-guide.md +119 -42
- package/plugins/litclaude/skills/lit-pptx/references/consensus-protocol.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/data-display-cookbook.md +13 -6
- package/plugins/litclaude/skills/lit-pptx/references/density-and-fill.md +164 -0
- package/plugins/litclaude/skills/lit-pptx/references/design-system-azure.md +7 -4
- package/plugins/litclaude/skills/lit-pptx/references/direction-step.md +186 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/01-pitch-signal-ko.md +210 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/02-pitch-studio-ko.md +147 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/03-business-review-ledger-ko.md +199 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/04-business-review-night-en.md +203 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +179 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/06-lecture-chalk-ko.md +199 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/07-data-review-night-en.md +194 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/08-image-led-atlas-ko.md +175 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/09-briefing-gazette-ko.md +204 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/10-status-update-ledger-ko.md +179 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/11-portfolio-studio-en.md +164 -0
- package/plugins/litclaude/skills/lit-pptx/references/examples/12-research-tutorial-chalk-en.md +174 -0
- package/plugins/litclaude/skills/lit-pptx/references/interview-protocol.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/layout-families.md +198 -0
- package/plugins/litclaude/skills/lit-pptx/references/stages/autopilot.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/stages/interview.md +2 -2
- package/plugins/litclaude/skills/lit-pptx/references/stages/plan.md +1 -1
- package/plugins/litclaude/skills/lit-pptx/references/title-treatments.md +109 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/atlas.md +139 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/chalk.md +148 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/gazette.md +149 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/ledger.md +149 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/night.md +147 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/paper.md +151 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/signal.md +150 -0
- package/plugins/litclaude/skills/lit-pptx/references/tonalities/studio.md +132 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/compile-deck.js +64 -16
- package/plugins/litclaude/skills/lit-pptx/scripts/craft_extras.py +664 -3
- package/plugins/litclaude/skills/lit-pptx/scripts/deck_craft.py +48 -6
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/grid-resolver.js +521 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/layout-resolver.js +362 -11
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/markdown-loader.js +6 -4
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-adapter-pptx.js +211 -65
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/render-pack.js +3559 -0
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/slide-ast.js +10 -6
- package/plugins/litclaude/skills/lit-pptx/scripts/lib/template-registry.js +286 -1
- package/plugins/litclaude/skills/lit-pptx/scripts/qa_deck.py +39 -10
- package/plugins/litclaude/skills/lit-pptx/scripts/validate_pptx.py +22 -7
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/layout-mapping.yaml +1 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-A2Z/template.yaml +4 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/layout-mapping.yaml +1 -1
- package/plugins/litclaude/skills/lit-pptx/templates/enrolled/AZURE-PRO/template.yaml +4 -1
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +92 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +97 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +93 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +94 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/night/pack.yaml +93 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/paper/pack.yaml +98 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/signal/pack.yaml +94 -0
- package/plugins/litclaude/skills/lit-pptx/templates/tonalities/studio/pack.yaml +92 -0
|
@@ -139,12 +139,16 @@ function buildBlocks(layout, content) {
|
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Check for standalone image
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
// Check for standalone images: a run of image lines keeps every image, each with its caption.
|
|
143
|
+
const imageLines = remaining.split("\n").filter((l) => l.trim());
|
|
144
|
+
if (imageLines.length && imageLines.every((l) => l.trim().startsWith("![")) && imageLines.some((l) => parseImage(l))) {
|
|
145
|
+
for (const line of imageLines) {
|
|
146
|
+
const img = parseImage(line);
|
|
147
|
+
if (!img) continue;
|
|
148
|
+
blocks.push({ type: "image", src: img.src, caption: img.alt || "" });
|
|
149
|
+
if (img.alt) {
|
|
150
|
+
blocks.push({ type: "figure-caption", caption: img.alt });
|
|
151
|
+
}
|
|
148
152
|
}
|
|
149
153
|
continue;
|
|
150
154
|
}
|
|
@@ -388,7 +388,292 @@ function loadTemplate(name) {
|
|
|
388
388
|
template,
|
|
389
389
|
mapping,
|
|
390
390
|
capabilities,
|
|
391
|
+
pack: legacyPack(template.name || name, template),
|
|
391
392
|
};
|
|
392
393
|
}
|
|
393
394
|
|
|
394
|
-
|
|
395
|
+
// ── Tonality packs ──────────────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
const grid = require("./grid-resolver");
|
|
398
|
+
|
|
399
|
+
const TONALITY_DIR = path.resolve(__dirname, "../../templates/tonalities");
|
|
400
|
+
|
|
401
|
+
/** Pack lookup, first match wins, in the same order as templates. */
|
|
402
|
+
function tonalityDirs() {
|
|
403
|
+
const extra = (process.env.LIT_PPTX_TONALITY_DIRS || "")
|
|
404
|
+
.split(path.delimiter)
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
.map((dir) => path.resolve(dir));
|
|
407
|
+
return [...extra, path.resolve(process.cwd(), ".lit-pptx", "tonalities"), TONALITY_DIR];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function listTonalities() {
|
|
411
|
+
const names = new Set();
|
|
412
|
+
for (const dir of tonalityDirs()) {
|
|
413
|
+
if (!fs.existsSync(dir)) continue;
|
|
414
|
+
for (const name of fs.readdirSync(dir)) {
|
|
415
|
+
if (fs.existsSync(path.join(dir, name, "pack.yaml"))) names.add(name);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return [...names].sort();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const PALETTE_ROLES = ["ground", "surface", "ink", "ink-muted", "line", "accent", "accent-deep", "accent-tint", "field", "on-field", "positive", "negative"];
|
|
422
|
+
const FACE_KEYS = ["display", "title", "body", "label", "numeral"];
|
|
423
|
+
const FACES = {
|
|
424
|
+
"Pretendard Regular": { font: "Pretendard", bold: false },
|
|
425
|
+
"Pretendard Bold": { font: "Pretendard", bold: true },
|
|
426
|
+
"A2Z Light": { font: "에이투지체 3 Light", bold: false },
|
|
427
|
+
"A2Z Regular": { font: "에이투지체 4 Regular", bold: false },
|
|
428
|
+
"A2Z Medium": { font: "에이투지체 5 Medium", bold: false },
|
|
429
|
+
"A2Z Bold": { font: "에이투지체 7 Bold", bold: false },
|
|
430
|
+
"A2Z Black": { font: "에이투지체 9 Black", bold: false },
|
|
431
|
+
};
|
|
432
|
+
const DECORATIONS = ["accent-rule", "hairline-rule", "header-band", "rail-fill", "colour-field", "caption-band", "column-hairlines", "box-outline", "mark-underline", "numeral"];
|
|
433
|
+
const SLIDE_ROLES = ["content", "data", "data-takeaway", "sequence", "image", "statement", "reference", "definition"];
|
|
434
|
+
const FILL_POLICIES = ["distribute", "step-up", "anchor-visual", "change-family"];
|
|
435
|
+
// How a pack carries its display slides: the device on a type-led cover, around a statement,
|
|
436
|
+
// behind a big number and under a closing's next step (`display.index` adds the part index to
|
|
437
|
+
// section slides).
|
|
438
|
+
const DISPLAY_DEVICES = {
|
|
439
|
+
cover: ["plain", "drench", "plate", "rail", "band", "figures", "rules", "numeral"],
|
|
440
|
+
statement: ["open", "drench", "rules", "plate", "offset"],
|
|
441
|
+
number: ["field", "tint", "outline"],
|
|
442
|
+
closing: ["band", "box", "rules"],
|
|
443
|
+
};
|
|
444
|
+
// Contrast pairs every pack must meet (text pairs 4.5:1, large marks 3:1).
|
|
445
|
+
const CONTRAST_PAIRS = [["ink", "ground", 4.5], ["ink", "surface", 4.5], ["ink-muted", "ground", 4.5], ["ink-muted", "surface", 4.5],
|
|
446
|
+
["accent", "ground", 3.0], ["accent-deep", "accent-tint", 4.5], ["ink", "accent-tint", 4.5], ["on-field", "field", 4.5],
|
|
447
|
+
["positive", "ground", 4.5], ["negative", "ground", 4.5]];
|
|
448
|
+
|
|
449
|
+
function luminance(hexColour) {
|
|
450
|
+
const v = String(hexColour).replace("#", "");
|
|
451
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(v.slice(i, i + 2), 16) / 255)
|
|
452
|
+
.map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
453
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function contrast(a, b) {
|
|
457
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
458
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Check a parsed pack against the pack schema; throws one message naming every problem. */
|
|
462
|
+
function validatePack(pack, file) {
|
|
463
|
+
const problems = [];
|
|
464
|
+
const need = (cond, msg) => { if (!cond) problems.push(msg); };
|
|
465
|
+
const isList = (v) => Array.isArray(v);
|
|
466
|
+
const hex = (v) => typeof v === "string" && /^#[0-9A-Fa-f]{6}$/u.test(v);
|
|
467
|
+
need(typeof pack.id === "string" && /^[a-z][a-z0-9-]{2,23}$/u.test(pack.id), "id must be 3-24 lowercase letters, digits or hyphens");
|
|
468
|
+
need(typeof pack.name === "string" && pack.name.length >= 1 && pack.name.length <= 24, "name must be 1-24 characters");
|
|
469
|
+
need(typeof pack.version === "string" && /^\d+\.\d+\.\d+$/u.test(pack.version), "version must be semver");
|
|
470
|
+
need(typeof pack.intent === "string" && pack.intent.length > 0, "intent is missing");
|
|
471
|
+
need(isList(pack.canvas) && pack.canvas.length > 0 && pack.canvas.every((c) => grid.CANVAS[c]), "canvas must list 16:9 and/or 4:3");
|
|
472
|
+
// `faces` is the deck's default and is Pretendard only; `faces-a2z` applies when the source asks.
|
|
473
|
+
for (const key of ["faces", "faces-a2z"]) {
|
|
474
|
+
if (key === "faces-a2z" && pack[key] == null) continue;
|
|
475
|
+
const faces = pack[key] || {};
|
|
476
|
+
for (const role of FACE_KEYS) {
|
|
477
|
+
need(FACES[faces[role]] && (key === "faces-a2z" || /^Pretendard /u.test(faces[role])), `${key}.${role} must name a bundled face${key === "faces-a2z" ? "" : " (Pretendard Regular or Bold)"}`);
|
|
478
|
+
}
|
|
479
|
+
const distinct = new Set(FACE_KEYS.map((r) => faces[r]));
|
|
480
|
+
need(distinct.size <= 3, `${key} uses ${distinct.size} faces; at most 3`);
|
|
481
|
+
need(new Set([...distinct].map((f) => String(f).split(" ")[0])).size <= 2, `${key} mixes more than two families`);
|
|
482
|
+
}
|
|
483
|
+
const palette = pack.palette || {};
|
|
484
|
+
for (const role of PALETTE_ROLES) need(hex(palette[role]), `palette.${role} is missing or not #RRGGBB`);
|
|
485
|
+
need(isList(palette.series) && palette.series.length >= 2 && palette.series.length <= 4 && palette.series.every(hex),
|
|
486
|
+
"palette.series must list 2-4 #RRGGBB colours");
|
|
487
|
+
if (PALETTE_ROLES.every((r) => hex(palette[r]))) {
|
|
488
|
+
for (const [a, b, min] of CONTRAST_PAIRS) {
|
|
489
|
+
const got = contrast(palette[a], palette[b]);
|
|
490
|
+
need(got >= min, `${a} on ${b} is ${got.toFixed(2)}:1, below ${min}:1`);
|
|
491
|
+
}
|
|
492
|
+
need(Math.abs(luminance(palette.line) - luminance(palette.ground)) >= 0.08, "line is not visible on ground (luminance difference below 0.08)");
|
|
493
|
+
if (isList(palette.series) && palette.series.every(hex)) {
|
|
494
|
+
need(palette.series[0].toUpperCase() === palette.accent.toUpperCase(), "palette.series must start with the accent");
|
|
495
|
+
for (const s of palette.series) need(contrast(s, palette.ground) >= 3, `series colour ${s} is below 3:1 on ground`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
need(["presented", "reading"].includes(pack.ramp), "ramp must be presented or reading");
|
|
499
|
+
need(typeof pack.hero === "boolean", "hero must be true or false");
|
|
500
|
+
need(Number.isInteger(pack.density) && pack.density >= 1 && pack.density <= 10, "density must be an integer 1-10");
|
|
501
|
+
need(Number.isInteger(pack.variance) && pack.variance >= 4 && pack.variance <= 10, "variance must be an integer 4-10");
|
|
502
|
+
need(pack.radius === 0 || pack.radius === 6, "radius must be 0 or 6");
|
|
503
|
+
need(["border", "fill", "fill+border"].includes(pack.edge), "edge must be border, fill or fill+border");
|
|
504
|
+
const treatments = isList(pack.treatments) ? pack.treatments : [];
|
|
505
|
+
need(treatments.length >= 3 && treatments.length <= 5, "treatments must list 3-5 title treatments");
|
|
506
|
+
for (const t of treatments) need(grid.TREATMENTS.includes(t), `unknown treatment "${t}" (known: ${grid.TREATMENTS.join(", ")})`);
|
|
507
|
+
const roles = pack["role-defaults"] || {};
|
|
508
|
+
for (const role of SLIDE_ROLES) need(treatments.includes(roles[role]), `role-defaults.${role} must be one of the pack's treatments`);
|
|
509
|
+
// A family's own title in this pack, ahead of its role default: the structure the pack draws it with.
|
|
510
|
+
const structure = pack.structure || {};
|
|
511
|
+
need(typeof structure === "object" && !Array.isArray(structure), "structure must be a map of family to treatment");
|
|
512
|
+
for (const [f, t] of Object.entries(structure)) {
|
|
513
|
+
need(Boolean(grid.FAMILIES[f]), `structure names an unknown family "${f}"`);
|
|
514
|
+
need(treatments.includes(t) && grid.FAMILIES[f] && grid.FAMILIES[f].allow.includes(t), `structure.${f} must be a treatment of the pack that the family allows`);
|
|
515
|
+
}
|
|
516
|
+
for (const d of pack.decoration || []) need(DECORATIONS.includes(d), `unknown decoration "${d}"`);
|
|
517
|
+
need(pack.rail == null || ["surface", "field"].includes(pack.rail), "rail must be surface or field");
|
|
518
|
+
const display = pack.display || {};
|
|
519
|
+
need(typeof display === "object" && !Array.isArray(display), "display must be a map");
|
|
520
|
+
for (const [key, devices] of Object.entries(DISPLAY_DEVICES)) {
|
|
521
|
+
need(display[key] == null || devices.includes(display[key]), `display.${key} must be one of ${devices.join(", ")}`);
|
|
522
|
+
}
|
|
523
|
+
need(display.index == null || typeof display.index === "boolean", "display.index must be true or false");
|
|
524
|
+
for (const key of Object.keys(display)) need(key === "index" || DISPLAY_DEVICES[key], `unknown display key "${key}"`);
|
|
525
|
+
const families = pack["layout-families"] || [];
|
|
526
|
+
need(families.length >= 6, "layout-families must list at least 6 families");
|
|
527
|
+
for (const f of families) {
|
|
528
|
+
need(Boolean(grid.FAMILIES[f]), `unknown layout family "${f}"`);
|
|
529
|
+
if (grid.FAMILIES[f]) need(grid.FAMILIES[f].allow.some((t) => treatments.includes(t)), `the ${f} family allows none of the pack's treatments`);
|
|
530
|
+
}
|
|
531
|
+
for (const [key, kind, min] of [["covers", "cover", 3], ["sections", "section", 2], ["closings", "closing", 2]]) {
|
|
532
|
+
const list = pack[key] || [];
|
|
533
|
+
need(list.length >= min, `${key} must list at least ${min} variants`);
|
|
534
|
+
for (const v of list) need(grid.VARIANTS[kind].includes(v), `unknown ${kind} variant "${v}"`);
|
|
535
|
+
}
|
|
536
|
+
for (const key of ["table", "chart", "image"]) need(pack[key] && typeof pack[key] === "object", `${key} settings are missing`);
|
|
537
|
+
const order = pack["fill-order"] || [];
|
|
538
|
+
need(order.length === FILL_POLICIES.length && FILL_POLICIES.every((p) => order.includes(p)), `fill-order must order ${FILL_POLICIES.join(", ")}`);
|
|
539
|
+
if (problems.length) throw new Error(`Tonality pack ${file}: ${problems.join("; ")}`);
|
|
540
|
+
return pack;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Load and check one tonality pack by id. */
|
|
544
|
+
function loadPack(name) {
|
|
545
|
+
// Pack ids are lowercase; a display name such as "Ledger" names the same pack.
|
|
546
|
+
const id = String(name).toLowerCase();
|
|
547
|
+
const known = listTonalities();
|
|
548
|
+
const dir = tonalityDirs()
|
|
549
|
+
.map((base) => path.join(base, String(id)))
|
|
550
|
+
.find((candidate) => /^[a-z][a-z0-9-]*$/u.test(String(id)) && fs.existsSync(path.join(candidate, "pack.yaml")));
|
|
551
|
+
if (!dir) {
|
|
552
|
+
throw new Error(`Unknown tonality "${name}". Tonalities: ${known.join(", ") || "(none)"}; legacy templates: ${listTemplates().join(", ")}`);
|
|
553
|
+
}
|
|
554
|
+
const file = path.join(dir, "pack.yaml");
|
|
555
|
+
const pack = validatePack(parseYaml(fs.readFileSync(file, "utf-8")), file);
|
|
556
|
+
if (pack.id !== path.basename(dir)) throw new Error(`Tonality pack ${file}: id "${pack.id}" must match its folder name`);
|
|
557
|
+
return pack;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Today's hand-tuned geometry, now a token set. A legacy template draws with these values unless
|
|
561
|
+
// its template.yaml overrides one under `pack: tokens:`; a tonality pack derives its own from the dials.
|
|
562
|
+
const LEGACY_TOKENS = {
|
|
563
|
+
card: { pad: 0.30, chip: 0.52, minH: 1.6, gap: 0.28, radius: 0.16 },
|
|
564
|
+
kpi: { minCardWidth: 1.9, blockHeight: 1.75, gap: 0.26 },
|
|
565
|
+
bullet: { size: 16, x: 0.59, w: 8.8, h: 0.38, ySpacing: 0.42, lineSpacing: 1.25 },
|
|
566
|
+
secHeader: { size: 18, x: 0.39, w: 9.0, h: 0.5 },
|
|
567
|
+
summary: { headerW: 4.3, bulletW: 4.8, bulletSize: 14, bulletH: 0.34, ySpacing: 0.38, lineSpacing: 1.05 },
|
|
568
|
+
toc: { size: 18, x: 0.39, w: 7.5, h: 0.55, yStart: 0.957, ySpacing: 0.65 },
|
|
569
|
+
notice: { x: 0.45, h: 0.3, bottom: 0.14, size: 10.5 },
|
|
570
|
+
};
|
|
571
|
+
// Rich card, KPI and table rendering plus the accent, font and background switches, for a copy of
|
|
572
|
+
// an older template that still says `render_style: azure` instead of listing its capabilities.
|
|
573
|
+
const RICH_LEGACY_CAPABILITIES = ["rich-blocks", "recolor", "font-swap", "mesh-background"];
|
|
574
|
+
|
|
575
|
+
function mergeTokens(base, extra) {
|
|
576
|
+
const out = {};
|
|
577
|
+
for (const [group, values] of Object.entries(base)) out[group] = { ...values, ...((extra && extra[group]) || {}) };
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* A legacy template read as a pack: one title treatment, its own palette mapped onto the pack
|
|
583
|
+
* roles, its capabilities, and today's geometry as tokens. It renders through the legacy path, so
|
|
584
|
+
* a deck that names the template keeps its look.
|
|
585
|
+
*/
|
|
586
|
+
function legacyPack(name, template) {
|
|
587
|
+
const pal = template.palette || {};
|
|
588
|
+
const own = template.pack || {};
|
|
589
|
+
const capabilities = Array.isArray(own.capabilities)
|
|
590
|
+
? own.capabilities
|
|
591
|
+
: template.render_style === "azure" ? RICH_LEGACY_CAPABILITIES : [];
|
|
592
|
+
const accent = pal.primary || `#${String((template.global_typography || {}).bullet_color || "3C3836").replace("#", "")}`;
|
|
593
|
+
const palette = {
|
|
594
|
+
ground: pal.ground || pal.paper || "#FFFFFF",
|
|
595
|
+
surface: pal.tint || pal.paper_warm || pal.header_fill || "#F6F6F6",
|
|
596
|
+
ink: pal.ink || "#000000",
|
|
597
|
+
"ink-muted": pal.ink_muted || pal.muted || pal.ink || "#555555",
|
|
598
|
+
line: pal.line || "#CCCCCC",
|
|
599
|
+
accent,
|
|
600
|
+
"accent-deep": pal.primary_deep || pal.ink || accent,
|
|
601
|
+
"accent-tint": pal.tint || pal.paper_warm || "#F6F6F6",
|
|
602
|
+
field: pal.primary_deep || pal.ink || accent,
|
|
603
|
+
"on-field": "#FFFFFF",
|
|
604
|
+
};
|
|
605
|
+
const report = CONTRAST_PAIRS
|
|
606
|
+
.filter(([a, b]) => palette[a] && palette[b])
|
|
607
|
+
.map(([a, b, min]) => ({ pair: `${a} on ${b}`, ratio: +contrast(palette[a], palette[b]).toFixed(2), min }))
|
|
608
|
+
.filter((r) => r.ratio < r.min);
|
|
609
|
+
return {
|
|
610
|
+
id: name,
|
|
611
|
+
name,
|
|
612
|
+
legacy: true,
|
|
613
|
+
treatments: ["top-rule"],
|
|
614
|
+
capabilities,
|
|
615
|
+
palette,
|
|
616
|
+
tokens: mergeTokens(LEGACY_TOKENS, own.tokens),
|
|
617
|
+
contrast: report,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function hasCapability(templateObj, capability) {
|
|
622
|
+
return Boolean(templateObj && templateObj.pack && (templateObj.pack.capabilities || []).includes(capability));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Load a tonality pack as the template object the resolver and renderer read. The dials come from
|
|
627
|
+
* the deck (frontmatter or flags) or the pack defaults; the renderer never sees the pack's name.
|
|
628
|
+
*/
|
|
629
|
+
function loadTonality(id, options = {}) {
|
|
630
|
+
const pack = loadPack(id);
|
|
631
|
+
const density = grid.clampDial(options.density != null ? options.density : pack.density, pack.density);
|
|
632
|
+
const variance = grid.clampDial(options.variance != null ? options.variance : pack.variance, pack.variance);
|
|
633
|
+
const canvas = options.canvas || pack.canvas[0];
|
|
634
|
+
if (!pack.canvas.includes(canvas)) {
|
|
635
|
+
throw new Error(`Tonality "${pack.id}" supports ${pack.canvas.join(", ")}, not canvas ${canvas}`);
|
|
636
|
+
}
|
|
637
|
+
const tok = grid.densityTokens(density, pack.ramp);
|
|
638
|
+
const g = grid.makeGrid(canvas, tok.grid);
|
|
639
|
+
// Pretendard by default; the A2Z map only when the source asks for it (`faces: a2z`).
|
|
640
|
+
const wantsA2z = String(options.faces || "").toLowerCase() === "a2z" && pack["faces-a2z"];
|
|
641
|
+
const faceMap = wantsA2z ? pack["faces-a2z"] : pack.faces;
|
|
642
|
+
const faces = Object.fromEntries(FACE_KEYS.map((role) => [role, FACES[faceMap[role]]]));
|
|
643
|
+
const P = pack.palette;
|
|
644
|
+
const template = {
|
|
645
|
+
name: pack.name,
|
|
646
|
+
label: `${pack.name} tonality`,
|
|
647
|
+
version: pack.version,
|
|
648
|
+
fonts: { title: faces.title.font, body: faces.body.font, light: faces.body.font },
|
|
649
|
+
dimensions: { width: g.W / 72, height: g.H / 72, unit: "inches", format: canvas },
|
|
650
|
+
palette: {
|
|
651
|
+
ink: P.ink, ink_muted: P["ink-muted"], primary: P.accent, primary_deep: P["accent-deep"],
|
|
652
|
+
azure: P.series[1] || P.accent, azure_soft: P["accent-tint"], tint: P.surface, line: P.line, ground: P.ground, paper: P.ground,
|
|
653
|
+
},
|
|
654
|
+
global_typography: { char_spacing: 0, bullet_color: P.accent.replace("#", ""), section_accent: P.accent.replace("#", "") },
|
|
655
|
+
};
|
|
656
|
+
const pt = (v) => v / 72;
|
|
657
|
+
return {
|
|
658
|
+
name: pack.id,
|
|
659
|
+
version: pack.version,
|
|
660
|
+
template,
|
|
661
|
+
mapping: { layouts: {} },
|
|
662
|
+
capabilities: { font_roles: {} },
|
|
663
|
+
pack: {
|
|
664
|
+
...pack,
|
|
665
|
+
legacy: false,
|
|
666
|
+
capabilities: [],
|
|
667
|
+
faces,
|
|
668
|
+
dials: { density, variance, varianceMax: grid.varianceMax(variance, pack.treatments.length), canvas },
|
|
669
|
+
tok,
|
|
670
|
+
grid: g,
|
|
671
|
+
tokens: mergeTokens(LEGACY_TOKENS, {
|
|
672
|
+
card: { pad: pt(tok.pad), chip: 0, minH: 0, gap: pt(g.gutter), radius: pt(pack.radius) },
|
|
673
|
+
kpi: { minCardWidth: pt(g.span(1, 2).w), blockHeight: pt(tok.pad * 2 + tok.sizes.display * 1.15 + 6 + tok.sizes.label * 1.3), gap: pt(g.gutter) },
|
|
674
|
+
}),
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
module.exports = { loadTemplate, listTemplates, loadPack, listTonalities, loadTonality, hasCapability, validatePack, legacyPack, LEGACY_TOKENS };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Honest, renderer-agnostic QA gate for generated PPTX decks.
|
|
3
3
|
|
|
4
|
-
Brand-neutral: works for any enrolled template. Combines
|
|
4
|
+
Brand-neutral: works for any enrolled template. Combines these checks and
|
|
5
5
|
exits non-zero if ANY real defect is found:
|
|
6
6
|
|
|
7
7
|
1. Layout issues — layout_inventory.py (frame overflow / slide overflow / off-slide)
|
|
@@ -12,6 +12,13 @@ exits non-zero if ANY real defect is found:
|
|
|
12
12
|
5. Deck craft — deck_craft.py (sparse slides, table-only decks, shapes off
|
|
13
13
|
the canvas, stray empty boxes, [placeholder] blanks, long
|
|
14
14
|
foreign-language source lines on a Korean deck)
|
|
15
|
+
6. Craft floor — craft_extras.py (OF-101..OF-109 per slide; OF-110..OF-113
|
|
16
|
+
over the deck: title-treatment count, composition variety,
|
|
17
|
+
empty bottom band, one frame per title treatment; OF-114
|
|
18
|
+
titles and the cover subtitle as labels, not sentences;
|
|
19
|
+
OF-115 no empty region; OF-117 no light figure card on a
|
|
20
|
+
dark ground; OF-116 with --sibling: another tonality of
|
|
21
|
+
the same source draws another skeleton)
|
|
15
22
|
|
|
16
23
|
No LibreOffice / rendering engine required: everything is read from the OOXML
|
|
17
24
|
via python-pptx. (A renderer is only ever needed for the *visual* pass, which
|
|
@@ -107,10 +114,22 @@ def _bbox(shape):
|
|
|
107
114
|
return None
|
|
108
115
|
|
|
109
116
|
|
|
110
|
-
def
|
|
117
|
+
def _slide_ground(slide) -> tuple[int, int, int]:
|
|
118
|
+
"""The slide's own solid background, white when it has none (or inherits one)."""
|
|
119
|
+
try:
|
|
120
|
+
fill = slide.background.fill
|
|
121
|
+
if fill.type == 1: # solid
|
|
122
|
+
return _hex_to_rgb(fill.fore_color.rgb) or (255, 255, 255)
|
|
123
|
+
except (AttributeError, TypeError, ValueError):
|
|
124
|
+
pass
|
|
125
|
+
return (255, 255, 255)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _resolve_bg(shape, idx, filled, ground=(255, 255, 255)):
|
|
111
129
|
"""Effective background for a text shape: its own fill, else the topmost
|
|
112
130
|
filled shape drawn *behind* it (lower z-order) that contains its centre,
|
|
113
|
-
else
|
|
131
|
+
else the slide background. Prevents false positives like white text over a
|
|
132
|
+
charcoal band, or light text on a dark slide."""
|
|
114
133
|
own = _shape_fill_rgb(shape)
|
|
115
134
|
if own is not None:
|
|
116
135
|
return own
|
|
@@ -125,7 +144,7 @@ def _resolve_bg(shape, idx, filled):
|
|
|
125
144
|
best = f_rgb # later (higher idx) wins -> topmost behind text
|
|
126
145
|
if best is not None:
|
|
127
146
|
return best
|
|
128
|
-
return
|
|
147
|
+
return ground
|
|
129
148
|
|
|
130
149
|
|
|
131
150
|
def check_contrast(path: Path) -> dict[str, Any]:
|
|
@@ -136,10 +155,11 @@ def check_contrast(path: Path) -> dict[str, Any]:
|
|
|
136
155
|
for s_idx, slide in enumerate(prs.slides, start=1):
|
|
137
156
|
shapes = list(slide.shapes)
|
|
138
157
|
filled = [(i, rgb, _bbox(sh)) for i, sh in enumerate(shapes) if (rgb := _shape_fill_rgb(sh)) is not None]
|
|
158
|
+
ground = _slide_ground(slide)
|
|
139
159
|
for idx, shape in enumerate(shapes):
|
|
140
160
|
if not getattr(shape, "has_text_frame", False):
|
|
141
161
|
continue
|
|
142
|
-
bg = _resolve_bg(shape, idx, filled)
|
|
162
|
+
bg = _resolve_bg(shape, idx, filled, ground)
|
|
143
163
|
for para in shape.text_frame.paragraphs:
|
|
144
164
|
for run in para.runs:
|
|
145
165
|
if not run.text.strip():
|
|
@@ -457,7 +477,11 @@ def check_evidence_binding(path: Path) -> dict[str, Any]:
|
|
|
457
477
|
checked += 1
|
|
458
478
|
figure_box = _bbox_in(shape)
|
|
459
479
|
nearby: list[str] = []
|
|
460
|
-
|
|
480
|
+
page_area = (prs.slide_width / 914400.0) * (prs.slide_height / 914400.0)
|
|
481
|
+
if figure_box is not None and _is_full_bleed(figure_box, page_area):
|
|
482
|
+
# A full-bleed picture carries its caption on the panel laid over it.
|
|
483
|
+
nearby = [text for other in shapes if other is not shape and (text := _shape_text(other))]
|
|
484
|
+
elif figure_box is not None:
|
|
461
485
|
figure_width = figure_box[2] - figure_box[0]
|
|
462
486
|
for other in shapes:
|
|
463
487
|
text = _shape_text(other)
|
|
@@ -664,14 +688,17 @@ def run_validate(pptx: Path) -> dict[str, Any]:
|
|
|
664
688
|
|
|
665
689
|
|
|
666
690
|
# ── gate ───────────────────────────────────────────────────────────────────
|
|
667
|
-
def qa(pptx: Path) -> dict[str, Any]:
|
|
691
|
+
def qa(pptx: Path, sibling: Path | None = None) -> dict[str, Any]:
|
|
668
692
|
from ooxml_integrity import check_package
|
|
669
693
|
from deck_craft import analyse as analyse_craft
|
|
670
|
-
from craft_extras import analyse as analyse_extras
|
|
694
|
+
from craft_extras import analyse as analyse_extras, compare_skeletons
|
|
671
695
|
|
|
672
696
|
integrity = check_package(pptx)
|
|
673
697
|
craft = analyse_craft(pptx)
|
|
674
698
|
extras = analyse_extras(pptx)
|
|
699
|
+
# Another tonality of the same source, when given: the two must not draw one skeleton (OF-116).
|
|
700
|
+
if sibling is not None:
|
|
701
|
+
extras["findings"] += compare_skeletons(pptx, sibling)
|
|
675
702
|
validate_report = run_validate(pptx)
|
|
676
703
|
inv_summary = summarize_inventory(load_inventory_issues(pptx))
|
|
677
704
|
contrast = check_contrast(pptx)
|
|
@@ -695,7 +722,8 @@ def qa(pptx: Path) -> dict[str, Any]:
|
|
|
695
722
|
extras_high.setdefault(finding["check"], []).append(finding)
|
|
696
723
|
for check, found in extras_high.items():
|
|
697
724
|
slides = sorted({f["slide"] for f in found if f["slide"] is not None})
|
|
698
|
-
|
|
725
|
+
where = f" on slide(s) {', '.join(map(str, slides))}" if slides else f" ({found[0]['detail']})"
|
|
726
|
+
reasons.append(f"craft {check}{where} — {found[0]['hint']}")
|
|
699
727
|
if not validate_report.get("pass", False):
|
|
700
728
|
reasons.append("anti-slop: " + ", ".join(
|
|
701
729
|
k for k, v in validate_report.get("checks", {}).items() if isinstance(v, dict) and not v.get("pass", True)
|
|
@@ -756,8 +784,9 @@ def qa(pptx: Path) -> dict[str, Any]:
|
|
|
756
784
|
def main(argv: list[str] | None = None) -> int:
|
|
757
785
|
parser = argparse.ArgumentParser(description="Honest renderer-agnostic QA gate for PPTX decks.")
|
|
758
786
|
parser.add_argument("pptx", type=Path)
|
|
787
|
+
parser.add_argument("--sibling", type=Path, help="the same source compiled under another tonality (OF-116)")
|
|
759
788
|
args = parser.parse_args(argv)
|
|
760
|
-
report = qa(args.pptx.resolve())
|
|
789
|
+
report = qa(args.pptx.resolve(), args.sibling.resolve() if args.sibling else None)
|
|
761
790
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
762
791
|
return 0 if report["pass"] else 1
|
|
763
792
|
|
|
@@ -61,15 +61,24 @@ def _load_terms() -> tuple[list[str], list[str], bool, bool]:
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def _build_regex(terms: list[str], case_insensitive: bool, whole_word: bool) -> re.Pattern[str] | None:
|
|
64
|
+
def _build_regex(terms: list[str], case_insensitive: bool, whole_word: bool, single_words: bool = False) -> re.Pattern[str] | None:
|
|
65
65
|
if not terms:
|
|
66
66
|
return None
|
|
67
|
-
|
|
67
|
+
# An acronym (RAM, FTO) is a word in capitals: matched as that word, so "diagram" or "program"
|
|
68
|
+
# does not contain it. With single_words a term that is one Latin word (todo, tbd) is matched as
|
|
69
|
+
# that word or its plural (TODOs), so "Mastodon" or "photodocument" does not contain it. Every
|
|
70
|
+
# other term keeps the file's case and word settings.
|
|
71
|
+
acronyms = [term for term in terms if re.fullmatch(r"[A-Z]{2,5}", term)]
|
|
72
|
+
escaped = [rf"(?<![A-Za-z]){re.escape(term)}s?(?![A-Za-z])" if single_words and re.fullmatch(r"[A-Za-z]+", term) else re.escape(term)
|
|
73
|
+
for term in terms if term not in acronyms]
|
|
68
74
|
pattern = "|".join(escaped)
|
|
69
|
-
if whole_word:
|
|
75
|
+
if whole_word and pattern:
|
|
70
76
|
pattern = rf"(?:(?<=\W)|^)(?:{pattern})(?:(?=\W)|$)"
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
if case_insensitive and pattern:
|
|
78
|
+
pattern = f"(?i:{pattern})"
|
|
79
|
+
if acronyms:
|
|
80
|
+
pattern = "|".join(filter(None, [pattern, rf"(?<![A-Za-z])(?:{'|'.join(map(re.escape, acronyms))})(?![A-Za-z])"]))
|
|
81
|
+
return re.compile(pattern)
|
|
73
82
|
|
|
74
83
|
|
|
75
84
|
def _shape_text(shape) -> str:
|
|
@@ -112,6 +121,10 @@ def _slide_has_title(slide, slide_height: int | None) -> bool:
|
|
|
112
121
|
title_shape = getattr(slide.shapes, "title", None)
|
|
113
122
|
if title_shape is not None and _shape_text(title_shape):
|
|
114
123
|
return True
|
|
124
|
+
# A tonality deck names its title frame by treatment (title@side-rail, title@bottom-anchor, ...),
|
|
125
|
+
# wherever the treatment puts it on the slide.
|
|
126
|
+
if any(str(getattr(shape, "name", "")).startswith("title@") and _shape_text(shape) for shape in slide.shapes):
|
|
127
|
+
return True
|
|
115
128
|
for shape in slide.shapes:
|
|
116
129
|
if not getattr(shape, "is_placeholder", False):
|
|
117
130
|
continue
|
|
@@ -137,7 +150,7 @@ def _slide_has_title(slide, slide_height: int | None) -> bool:
|
|
|
137
150
|
def lint(path: str | Path) -> dict[str, Any]:
|
|
138
151
|
prs = Presentation(str(path))
|
|
139
152
|
hard_terms, soft_terms, case_insensitive, whole_word = _load_terms()
|
|
140
|
-
hard_re = _build_regex(hard_terms, case_insensitive, whole_word)
|
|
153
|
+
hard_re = _build_regex(hard_terms, case_insensitive, whole_word, single_words=True)
|
|
141
154
|
soft_re = _build_regex(soft_terms, case_insensitive, whole_word)
|
|
142
155
|
|
|
143
156
|
missing_titles: list[int] = []
|
|
@@ -175,7 +188,9 @@ def lint(path: str | Path) -> dict[str, Any]:
|
|
|
175
188
|
default_font_hits.append(f"slide {slide_idx}: {font_name}")
|
|
176
189
|
|
|
177
190
|
last_slide_text = "\n".join(_shape_text(shape) for shape in prs.slides[-1].shapes if getattr(shape, "has_text_frame", False)).lower() if prs.slides else ""
|
|
178
|
-
|
|
191
|
+
# A closing references page is the deck's source note in its long form.
|
|
192
|
+
has_source_note = "source:" in last_slide_text or "출처" in last_slide_text or bool(
|
|
193
|
+
re.search(r"^\s*(references|참고\s*문헌)\s*$", last_slide_text, re.MULTILINE))
|
|
179
194
|
|
|
180
195
|
failures = {
|
|
181
196
|
"missing_titles": bool(missing_titles),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Layout Mapping — AZURE-PRO (16:9, 13.333 x 7.5 in). Positions in inches.
|
|
2
|
-
#
|
|
2
|
+
# rich-blocks capability (template.yaml pack:) → body→cards, kpi-table→KPI cards, cover title **emphasis**.
|
|
3
3
|
|
|
4
4
|
layouts:
|
|
5
5
|
cover:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
name: AZURE-A2Z
|
|
2
2
|
version: "1.0.0"
|
|
3
3
|
label: "Azure Professional (에이투지체 / A2Z)"
|
|
4
|
-
render_style: azure
|
|
5
4
|
description: "Flagship SOTA blue-and-white deck — 16:9 widescreen, Pretendard, geometric gradient accents, rounded tint cards, numbered chips, KPI cards. Pro-designer aesthetic."
|
|
6
5
|
|
|
7
6
|
fonts:
|
|
@@ -35,3 +34,7 @@ global_typography:
|
|
|
35
34
|
bullet_char: "•"
|
|
36
35
|
bullet_color: "1D4ED8"
|
|
37
36
|
section_accent: "1D4ED8"
|
|
37
|
+
|
|
38
|
+
# Read as a legacy pack: what this template can draw beyond plain text regions.
|
|
39
|
+
pack:
|
|
40
|
+
capabilities: [rich-blocks, recolor, font-swap, mesh-background]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Layout Mapping — AZURE-PRO (16:9, 13.333 x 7.5 in). Positions in inches.
|
|
2
|
-
#
|
|
2
|
+
# rich-blocks capability (template.yaml pack:) → body→cards, kpi-table→KPI cards, cover title **emphasis**.
|
|
3
3
|
|
|
4
4
|
layouts:
|
|
5
5
|
cover:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
name: AZURE-PRO
|
|
2
2
|
version: "1.0.0"
|
|
3
3
|
label: "Azure Professional"
|
|
4
|
-
render_style: azure
|
|
5
4
|
description: "Flagship SOTA blue-and-white deck — 16:9 widescreen, Pretendard, geometric gradient accents, rounded tint cards, numbered chips, KPI cards. Pro-designer aesthetic."
|
|
6
5
|
|
|
7
6
|
fonts:
|
|
@@ -35,3 +34,7 @@ global_typography:
|
|
|
35
34
|
bullet_char: "•"
|
|
36
35
|
bullet_color: "1D4ED8"
|
|
37
36
|
section_accent: "1D4ED8"
|
|
37
|
+
|
|
38
|
+
# Read as a legacy pack: what this template can draw beyond plain text regions.
|
|
39
|
+
pack:
|
|
40
|
+
capabilities: [rich-blocks, recolor, font-swap, mesh-background]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Atlas: a picture book with captions. Images bleed, titles sit on dark panels over photographs
|
|
2
|
+
# or under large images, text stays short and calm, and the photographs bring the colour. Wrong
|
|
3
|
+
# when there are few real images: a look built around pictures turns into empty boxes without them.
|
|
4
|
+
id: atlas
|
|
5
|
+
name: Atlas
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
intent: "Image-led decks: products, portfolios, places, field work."
|
|
8
|
+
canvas: ["16:9", "4:3"]
|
|
9
|
+
|
|
10
|
+
# Pretendard by default; the A2Z map below applies only when the source asks (faces: a2z).
|
|
11
|
+
faces:
|
|
12
|
+
display: Pretendard Regular
|
|
13
|
+
title: Pretendard Bold
|
|
14
|
+
body: Pretendard Regular
|
|
15
|
+
label: Pretendard Regular
|
|
16
|
+
numeral: Pretendard Regular
|
|
17
|
+
faces-a2z:
|
|
18
|
+
display: A2Z Light
|
|
19
|
+
title: Pretendard Bold
|
|
20
|
+
body: Pretendard Regular
|
|
21
|
+
label: Pretendard Regular
|
|
22
|
+
numeral: A2Z Light
|
|
23
|
+
|
|
24
|
+
palette:
|
|
25
|
+
ground: "#FFFFFF"
|
|
26
|
+
surface: "#F3F4F5"
|
|
27
|
+
ink: "#1A1D21"
|
|
28
|
+
ink-muted: "#555B63"
|
|
29
|
+
line: "#D6D9DD"
|
|
30
|
+
accent: "#2B4C7E"
|
|
31
|
+
accent-deep: "#1B2F4E"
|
|
32
|
+
accent-tint: "#E6ECF4"
|
|
33
|
+
field: "#1A1D21"
|
|
34
|
+
on-field: "#FFFFFF"
|
|
35
|
+
positive: "#1E7A3F"
|
|
36
|
+
negative: "#B3261E"
|
|
37
|
+
series: ["#2B4C7E", "#555B63", "#8A6A2E", "#3E7A5A"]
|
|
38
|
+
|
|
39
|
+
ramp: reading
|
|
40
|
+
hero: false
|
|
41
|
+
type-voice:
|
|
42
|
+
title-tracking: 0
|
|
43
|
+
display-tracking: -1
|
|
44
|
+
|
|
45
|
+
density: 10
|
|
46
|
+
variance: 7
|
|
47
|
+
radius: 0
|
|
48
|
+
edge: fill
|
|
49
|
+
|
|
50
|
+
treatments: [overlay, bottom-anchor, side-rail, top-plain-large, statement]
|
|
51
|
+
role-defaults:
|
|
52
|
+
content: side-rail
|
|
53
|
+
data: top-plain-large
|
|
54
|
+
data-takeaway: bottom-anchor
|
|
55
|
+
sequence: top-plain-large
|
|
56
|
+
image: overlay
|
|
57
|
+
statement: statement
|
|
58
|
+
reference: side-rail
|
|
59
|
+
definition: side-rail
|
|
60
|
+
|
|
61
|
+
decoration: [caption-band, hairline-rule, colour-field]
|
|
62
|
+
layout-families: [image-full, image-split, photo-grid, figure-pair, asymmetric-feature, kpi-row, comparison, timeline, text-column, statement]
|
|
63
|
+
# Display slides: the device a type-led cover, a statement, a big number and a closing's next step
|
|
64
|
+
# are drawn with (see references/layout-families.md, "Display slides").
|
|
65
|
+
display:
|
|
66
|
+
cover: plate
|
|
67
|
+
statement: drench
|
|
68
|
+
number: tint
|
|
69
|
+
closing: band
|
|
70
|
+
covers: [cover-full-image, cover-split-image, cover-typographic]
|
|
71
|
+
sections: [section-image, section-field]
|
|
72
|
+
closings: [closing-contact-split, closing-ask]
|
|
73
|
+
|
|
74
|
+
table:
|
|
75
|
+
header: none
|
|
76
|
+
header-ink: ink
|
|
77
|
+
rules: rows
|
|
78
|
+
banding: none
|
|
79
|
+
totals: bold
|
|
80
|
+
chart:
|
|
81
|
+
gridlines: hairline
|
|
82
|
+
labels: direct
|
|
83
|
+
highlight: accent-on-muted
|
|
84
|
+
image:
|
|
85
|
+
frame: none
|
|
86
|
+
|
|
87
|
+
fill-order: [anchor-visual, change-family, distribute, step-up]
|
|
88
|
+
avoid:
|
|
89
|
+
- text placed straight on photographs
|
|
90
|
+
- images in card frames
|
|
91
|
+
- more than three images in one row
|
|
92
|
+
- grey placeholder boxes in a delivered deck
|