skilldrop-cli 0.2.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/LICENSE +21 -0
- package/README.md +413 -0
- package/bin/skilldrop.js +338 -0
- package/model-routing.json +147 -0
- package/package.json +36 -0
- package/packs.json +90 -0
- package/skills/accessibility-audit/SKILL.md +74 -0
- package/skills/accessibility-audit/examples/login-form.md +76 -0
- package/skills/accessibility-audit/manifest.json +11 -0
- package/skills/accessibility-audit/reference.md +69 -0
- package/skills/adr-generator/SKILL.md +49 -0
- package/skills/adr-generator/manifest.json +11 -0
- package/skills/adr-generator/templates/madr.md +56 -0
- package/skills/adr-generator/templates/nygard.md +19 -0
- package/skills/ai-usage-report/SKILL.md +111 -0
- package/skills/ai-usage-report/examples/weekly-engineering-team.md +133 -0
- package/skills/ai-usage-report/manifest.json +11 -0
- package/skills/ai-usage-report/scripts/build_report.py +436 -0
- package/skills/ai-usage-report/templates/report-per-user.md +42 -0
- package/skills/ai-usage-report/templates/report-team-rollup.md +49 -0
- package/skills/ai-usage-report/templates/usage-event-schema.md +76 -0
- package/skills/api-contract-draft/SKILL.md +69 -0
- package/skills/api-contract-draft/manifest.json +11 -0
- package/skills/api-contract-draft/reference.md +73 -0
- package/skills/api-contract-draft/templates/openapi-skeleton.yaml +148 -0
- package/skills/architecture-diagrams/SKILL.md +66 -0
- package/skills/architecture-diagrams/examples/aws-three-tier.md +44 -0
- package/skills/architecture-diagrams/examples/c4-container-saas.md +51 -0
- package/skills/architecture-diagrams/examples/microservices-sequence.md +49 -0
- package/skills/architecture-diagrams/manifest.json +28 -0
- package/skills/architecture-diagrams/templates/c4-container.puml +31 -0
- package/skills/architecture-diagrams/templates/mermaid-aws.md +52 -0
- package/skills/architecture-diagrams/templates/mermaid-flowchart.md +37 -0
- package/skills/architecture-diagrams/templates/mermaid-sequence.md +39 -0
- package/skills/audience-profile/SKILL.md +73 -0
- package/skills/audience-profile/manifest.json +11 -0
- package/skills/audience-profile/reference.md +175 -0
- package/skills/brief-intake/SKILL.md +78 -0
- package/skills/brief-intake/manifest.json +11 -0
- package/skills/brief-intake/templates/brief-adr.md +41 -0
- package/skills/brief-intake/templates/brief-decision-log.md +35 -0
- package/skills/brief-intake/templates/brief-deck.md +43 -0
- package/skills/brief-intake/templates/brief-design-doc.md +48 -0
- package/skills/brief-intake/templates/brief-exec-summary.md +47 -0
- package/skills/brief-intake/templates/brief-generic.md +45 -0
- package/skills/brief-intake/templates/brief-runbook.md +44 -0
- package/skills/brief-intake/templates/brief-tech-comparison.md +38 -0
- package/skills/bug-triage/SKILL.md +67 -0
- package/skills/bug-triage/manifest.json +11 -0
- package/skills/bug-triage/templates/bug-ticket.md +86 -0
- package/skills/business-case/SKILL.md +64 -0
- package/skills/business-case/manifest.json +11 -0
- package/skills/business-case/templates/business-case.md +60 -0
- package/skills/capacity-cost-model/SKILL.md +64 -0
- package/skills/capacity-cost-model/manifest.json +11 -0
- package/skills/capacity-cost-model/reference.md +54 -0
- package/skills/capacity-cost-model/templates/cost-model.md +68 -0
- package/skills/council-review/SKILL.md +106 -0
- package/skills/council-review/examples/redis-cache-decision.md +75 -0
- package/skills/council-review/manifest.json +11 -0
- package/skills/council-review/reference.md +76 -0
- package/skills/council-review/seats/architect.md +29 -0
- package/skills/council-review/seats/bench.md +39 -0
- package/skills/council-review/seats/operator.md +27 -0
- package/skills/council-review/seats/pragmatist.md +29 -0
- package/skills/council-review/seats/security-engineer.md +29 -0
- package/skills/council-review/seats/user-advocate.md +27 -0
- package/skills/council-review/templates/council-review.md +59 -0
- package/skills/data-contract/SKILL.md +67 -0
- package/skills/data-contract/manifest.json +11 -0
- package/skills/data-contract/reference.md +46 -0
- package/skills/data-contract/templates/data-contract.md +69 -0
- package/skills/db-schema-design/SKILL.md +68 -0
- package/skills/db-schema-design/manifest.json +11 -0
- package/skills/db-schema-design/reference.md +56 -0
- package/skills/db-schema-design/templates/schema-design.md +75 -0
- package/skills/decision-log/SKILL.md +67 -0
- package/skills/decision-log/manifest.json +11 -0
- package/skills/deck-builder/SKILL.md +114 -0
- package/skills/deck-builder/examples/exec-board-update.md +108 -0
- package/skills/deck-builder/manifest.json +17 -0
- package/skills/deck-builder/reference.md +92 -0
- package/skills/deck-builder/requirements.txt +1 -0
- package/skills/deck-builder/scripts/build_deck.py +309 -0
- package/skills/deck-builder/templates/deck-spec.json +97 -0
- package/skills/deck-builder/templates/palettes.json +59 -0
- package/skills/design-doc/SKILL.md +49 -0
- package/skills/design-doc/manifest.json +11 -0
- package/skills/design-doc/templates/design-doc.md +98 -0
- package/skills/devils-advocate/SKILL.md +142 -0
- package/skills/devils-advocate/lenses/adversarial-review.md +88 -0
- package/skills/devils-advocate/lenses/edge-cases.md +95 -0
- package/skills/devils-advocate/lenses/future-proofing.md +75 -0
- package/skills/devils-advocate/lenses/test-coverage.md +90 -0
- package/skills/devils-advocate/manifest.json +11 -0
- package/skills/devils-advocate/templates/challenge.md +118 -0
- package/skills/doc-critique/SKILL.md +119 -0
- package/skills/doc-critique/manifest.json +11 -0
- package/skills/doc-critique/rubrics/adr.md +36 -0
- package/skills/doc-critique/rubrics/decision-log.md +39 -0
- package/skills/doc-critique/rubrics/deck.md +45 -0
- package/skills/doc-critique/rubrics/design-doc.md +42 -0
- package/skills/doc-critique/rubrics/exec-summary.md +41 -0
- package/skills/doc-critique/rubrics/generic.md +40 -0
- package/skills/doc-critique/rubrics/runbook.md +42 -0
- package/skills/doc-critique/rubrics/tech-comparison.md +40 -0
- package/skills/doc-critique/templates/critique.md +49 -0
- package/skills/exec-summary/SKILL.md +45 -0
- package/skills/exec-summary/manifest.json +11 -0
- package/skills/exec-summary/templates/exec-summary.md +48 -0
- package/skills/feature-implement-loop/SKILL.md +83 -0
- package/skills/feature-implement-loop/manifest.json +11 -0
- package/skills/figma-diagrams/SKILL.md +85 -0
- package/skills/figma-diagrams/manifest.json +35 -0
- package/skills/figma-diagrams/reference.md +91 -0
- package/skills/figma-diagrams/requirements.txt +1 -0
- package/skills/figma-diagrams/scripts/_figma_client.py +71 -0
- package/skills/figma-diagrams/scripts/frame_to_mermaid.py +102 -0
- package/skills/figma-diagrams/scripts/inspect_file.py +46 -0
- package/skills/figma-diagrams/scripts/list_comments.py +35 -0
- package/skills/figma-diagrams/scripts/post_comment.py +52 -0
- package/skills/figma-diagrams/templates/figjam-import.json +20 -0
- package/skills/guide-builder/SKILL.md +76 -0
- package/skills/guide-builder/examples/local-setup-guide.md +127 -0
- package/skills/guide-builder/manifest.json +11 -0
- package/skills/guide-builder/templates/api-reference.md +120 -0
- package/skills/guide-builder/templates/design-walkthrough.md +72 -0
- package/skills/guide-builder/templates/setup-guide.md +109 -0
- package/skills/incident-comms/SKILL.md +68 -0
- package/skills/incident-comms/manifest.json +11 -0
- package/skills/incident-comms/reference.md +43 -0
- package/skills/incident-comms/templates/messages.md +82 -0
- package/skills/llm-eval-harness/SKILL.md +73 -0
- package/skills/llm-eval-harness/manifest.json +11 -0
- package/skills/llm-eval-harness/reference.md +58 -0
- package/skills/llm-eval-harness/templates/eval-plan.md +63 -0
- package/skills/llm-eval-harness/templates/golden-case.jsonl +5 -0
- package/skills/migration-plan/SKILL.md +68 -0
- package/skills/migration-plan/manifest.json +11 -0
- package/skills/migration-plan/templates/migration-plan.md +68 -0
- package/skills/nfr-spec/SKILL.md +63 -0
- package/skills/nfr-spec/manifest.json +11 -0
- package/skills/nfr-spec/reference.md +74 -0
- package/skills/nfr-spec/templates/nfr-spec.md +56 -0
- package/skills/observability-plan/SKILL.md +70 -0
- package/skills/observability-plan/manifest.json +11 -0
- package/skills/observability-plan/reference.md +67 -0
- package/skills/observability-plan/templates/observability-plan.md +62 -0
- package/skills/okr-cascade/SKILL.md +59 -0
- package/skills/okr-cascade/evals/eval_queries.json +12 -0
- package/skills/okr-cascade/evals/evals.json +28 -0
- package/skills/okr-cascade/manifest.json +11 -0
- package/skills/okr-cascade/templates/okr-cascade.md +46 -0
- package/skills/postmortem-generator/SKILL.md +65 -0
- package/skills/postmortem-generator/manifest.json +11 -0
- package/skills/postmortem-generator/templates/postmortem.md +67 -0
- package/skills/prd-draft/SKILL.md +62 -0
- package/skills/prd-draft/manifest.json +11 -0
- package/skills/prd-draft/templates/prd.md +66 -0
- package/skills/prfaq/SKILL.md +75 -0
- package/skills/prfaq/evals/eval_queries.json +13 -0
- package/skills/prfaq/evals/evals.json +32 -0
- package/skills/prfaq/manifest.json +11 -0
- package/skills/prfaq/templates/prfaq.md +60 -0
- package/skills/release-notes/SKILL.md +61 -0
- package/skills/release-notes/manifest.json +11 -0
- package/skills/release-notes/templates/release-notes.md +83 -0
- package/skills/requirements-interview/SKILL.md +61 -0
- package/skills/requirements-interview/manifest.json +11 -0
- package/skills/requirements-interview/templates/interview-kit.md +63 -0
- package/skills/reverse-architecture/SKILL.md +103 -0
- package/skills/reverse-architecture/manifest.json +11 -0
- package/skills/reverse-architecture/reference.md +150 -0
- package/skills/reverse-architecture/templates/description.md +52 -0
- package/skills/reverse-architecture/templates/extraction.md +55 -0
- package/skills/runbook-generator/SKILL.md +42 -0
- package/skills/runbook-generator/manifest.json +11 -0
- package/skills/runbook-generator/templates/runbook.md +109 -0
- package/skills/slide-outliner/SKILL.md +58 -0
- package/skills/slide-outliner/manifest.json +11 -0
- package/skills/slide-outliner/templates/deck-outline.md +131 -0
- package/skills/sonar-onboard/SKILL.md +170 -0
- package/skills/sonar-onboard/manifest.json +11 -0
- package/skills/sonar-onboard/reference.md +151 -0
- package/skills/sonar-onboard/templates/github-actions-sonar-server.yml.tmpl +50 -0
- package/skills/sonar-onboard/templates/github-actions-sonarcloud.yml.tmpl +53 -0
- package/skills/sonar-onboard/templates/readme-snippet.md +38 -0
- package/skills/sonar-onboard/templates/sonar-project.properties.tmpl +31 -0
- package/skills/sonar-review/SKILL.md +220 -0
- package/skills/sonar-review/lenses/bugs.md +48 -0
- package/skills/sonar-review/lenses/code-smells.md +64 -0
- package/skills/sonar-review/lenses/coverage-and-duplication.md +74 -0
- package/skills/sonar-review/lenses/security-hotspots.md +56 -0
- package/skills/sonar-review/lenses/vulnerabilities.md +52 -0
- package/skills/sonar-review/manifest.json +37 -0
- package/skills/sonar-review/reference.md +167 -0
- package/skills/sonar-review/templates/report.md +84 -0
- package/skills/strategy-analysis/SKILL.md +63 -0
- package/skills/strategy-analysis/evals/eval_queries.json +13 -0
- package/skills/strategy-analysis/evals/evals.json +36 -0
- package/skills/strategy-analysis/manifest.json +11 -0
- package/skills/success-metrics/SKILL.md +63 -0
- package/skills/success-metrics/evals/eval_queries.json +11 -0
- package/skills/success-metrics/evals/evals.json +27 -0
- package/skills/success-metrics/manifest.json +11 -0
- package/skills/success-metrics/templates/metrics-plan.md +55 -0
- package/skills/tech-comparison-matrix/SKILL.md +56 -0
- package/skills/tech-comparison-matrix/manifest.json +11 -0
- package/skills/tech-comparison-matrix/templates/matrix.md +45 -0
- package/skills/test-plan-generator/SKILL.md +59 -0
- package/skills/test-plan-generator/manifest.json +11 -0
- package/skills/test-plan-generator/templates/test-plan.md +83 -0
- package/skills/threat-model/SKILL.md +65 -0
- package/skills/threat-model/examples/document-upload.md +56 -0
- package/skills/threat-model/manifest.json +11 -0
- package/skills/threat-model/reference.md +69 -0
- package/skills/user-journey-map/SKILL.md +64 -0
- package/skills/user-journey-map/evals/eval_queries.json +12 -0
- package/skills/user-journey-map/evals/evals.json +30 -0
- package/skills/user-journey-map/examples/b2b-saas-onboarding.md +69 -0
- package/skills/user-journey-map/manifest.json +11 -0
- package/skills/user-journey-map/templates/journey-map.md +47 -0
- package/skills/user-story-splitter/SKILL.md +88 -0
- package/skills/user-story-splitter/examples/notifications-epic.md +123 -0
- package/skills/user-story-splitter/manifest.json +11 -0
- package/skills/user-story-splitter/templates/story.md +40 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Example: Quarterly board update for a migration project
|
|
2
|
+
|
|
3
|
+
**Prompt the user gave:** *"Build me a board update deck for our migration project. We're 60% through, on track for Q1 close, $2.4M annualised infra savings so far, but we discovered two services need a schema rewrite and we now have a multi-region regulatory requirement. We're asking for a $620k budget overrun. Use corporate-blue palette."*
|
|
4
|
+
|
|
5
|
+
## Step 1 — Audience archetype lookup
|
|
6
|
+
|
|
7
|
+
Board update → use `audience-profile.board` archetype:
|
|
8
|
+
|
|
9
|
+
- **Slide count target:** 8–12
|
|
10
|
+
- **Bullet density:** ≤ 3 bullets/slide, ≤ 6 words/bullet
|
|
11
|
+
- **Required sections:** Cover, TL;DR, Strategic context, Financials, Risks, Ask
|
|
12
|
+
- **Tone:** Outcomes-led, business-first, big numbers welcome
|
|
13
|
+
- **Layouts to favour:** `big_number` (for the $2.4M and the $620k), `quote` (for credibility), `two_column` (for scope changes)
|
|
14
|
+
|
|
15
|
+
## Step 2 — Build the deck spec
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"title": "ProjectX Migration — Board Update",
|
|
20
|
+
"audience": "board",
|
|
21
|
+
"palette": "corporate-blue",
|
|
22
|
+
|
|
23
|
+
"slides": [
|
|
24
|
+
{
|
|
25
|
+
"layout": "title",
|
|
26
|
+
"title": "ProjectX Migration",
|
|
27
|
+
"subtitle": "Board update — Q3 FY26",
|
|
28
|
+
"presenter": "John Doe",
|
|
29
|
+
"date": "2026-09-15",
|
|
30
|
+
"notes": "Open with: 'We're 60% through, on track, and asking for an overrun. Here's why it's still a good deal.'"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"layout": "content",
|
|
34
|
+
"title": "60% complete, zero customer incidents, on track for Q1 close",
|
|
35
|
+
"bullets": [
|
|
36
|
+
"8 of 14 services cut over",
|
|
37
|
+
"p99 latency down 22% on migrated services",
|
|
38
|
+
"Two services moved to phase 3 — schema rewrite"
|
|
39
|
+
],
|
|
40
|
+
"notes": "Lead with zero incidents. Boards remember that more than any other number on this slide."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"layout": "big_number",
|
|
44
|
+
"title": "Annualised infra savings already realised",
|
|
45
|
+
"number": "$2.4M",
|
|
46
|
+
"caption": "From the 8 migrated services. Projected $5.8M at completion.",
|
|
47
|
+
"notes": "Pause. Let the number land. Don't rush to the next slide."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"layout": "two_column",
|
|
51
|
+
"title": "What changed since the original plan",
|
|
52
|
+
"left": {
|
|
53
|
+
"title": "Original scope",
|
|
54
|
+
"bullets": ["14 services migrated", "Single-region active", "Legacy DB decommissioned"]
|
|
55
|
+
},
|
|
56
|
+
"right": {
|
|
57
|
+
"title": "Discovered in flight",
|
|
58
|
+
"bullets": ["Schema rewrite for 2 services", "Multi-region (EU regulator)", "Auth refactor coupling"]
|
|
59
|
+
},
|
|
60
|
+
"notes": "Multi-region is a regulator change, not a planning miss. Say so explicitly."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"layout": "quote",
|
|
64
|
+
"quote": "The migration went smoother than any I've seen in 20 years.",
|
|
65
|
+
"attribution": "VP Engineering, post phase-2 cut-over",
|
|
66
|
+
"notes": "Optional, but de-risks the perception that we're behind."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"layout": "content",
|
|
70
|
+
"title": "Top 3 risks for the remaining 40%",
|
|
71
|
+
"bullets": [
|
|
72
|
+
"Schema rewrite: 6 weeks, dry-runs underway",
|
|
73
|
+
"Auth refactor coupling: increases coordination overhead",
|
|
74
|
+
"Holiday freeze: -4 calendar weeks"
|
|
75
|
+
],
|
|
76
|
+
"notes": "Don't soften the risks. Boards trust presenters who name them."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"layout": "big_number",
|
|
80
|
+
"title": "Budget overrun we're requesting",
|
|
81
|
+
"number": "$620k",
|
|
82
|
+
"caption": "Covers schema rewrite + multi-region buildout. Payback: < 8 months.",
|
|
83
|
+
"notes": "Position the overrun against the $5.8M projected savings — net positive within the same fiscal year."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"layout": "closing",
|
|
87
|
+
"title": "Approval requested: $620k FY26 budget overrun",
|
|
88
|
+
"subtitle": "Decision needed by 2026-10-01 to keep Q1 close",
|
|
89
|
+
"notes": "Land here. Take questions. Don't add a thank-you slide."
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Step 3 — Generate
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 scripts/build_deck.py /tmp/board-update.json -o ./out/board-update.pptx
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Step 4 — Hand-off to the user
|
|
102
|
+
|
|
103
|
+
> Generated `out/board-update.pptx` — 8 slides, corporate-blue palette. Two notes before you present:
|
|
104
|
+
>
|
|
105
|
+
> 1. **Verify the speaker notes** — I wrote them based on the content you gave me. Read through and adjust your phrasing.
|
|
106
|
+
> 2. **Replace the slide 5 quote** — I used a placeholder attribution. If you have the actual quote (and clearance to use it), paste it in slide 5.
|
|
107
|
+
>
|
|
108
|
+
> Want me to also produce an exec-summary one-pager from the same content?
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deck-builder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate a real PowerPoint (.pptx) file from content, target audience, and a color palette. Produces structurally-appropriate decks (slide count, density, layout choice) tuned for the audience — execs, technical reviewers, sales prospects, investors, internal teams. Use when the user wants an actual editable .pptx, not just an outline.",
|
|
5
|
+
"entrypoint": "SKILL.md",
|
|
6
|
+
"deps": {
|
|
7
|
+
"npm": [],
|
|
8
|
+
"pip": ["python-pptx>=0.6.21"]
|
|
9
|
+
},
|
|
10
|
+
"env": { "required": [], "optional": [] },
|
|
11
|
+
"related": ["audience-profile", "slide-outliner"],
|
|
12
|
+
"install": {
|
|
13
|
+
"pip": "python3 -m pip install -r requirements.txt"
|
|
14
|
+
},
|
|
15
|
+
"tags": ["powerpoint", "pptx", "deck", "presentation", "stakeholder-communication"],
|
|
16
|
+
"model": { "tier": "standard", "rationale": "Generates pptx content + runs a script. Content shaping, not hard reasoning." }
|
|
17
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# deck-builder — design reference
|
|
2
|
+
|
|
3
|
+
## Color palette principles
|
|
4
|
+
|
|
5
|
+
A presentation palette needs five roles. If the user gives you fewer than five colors, derive the missing ones following these rules:
|
|
6
|
+
|
|
7
|
+
| Role | Where it's used | Picking rule |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **primary** | Title bars, section dividers, big-number captions | The user's brand colour, or the dominant colour in their material |
|
|
10
|
+
| **secondary** | Sparingly — second-tier emphasis, alt charts | Analogous or complementary to primary; ~30° apart on the wheel |
|
|
11
|
+
| **accent** | Highlight bars, the actual big numbers, quote bars | High-saturation pop colour; should NOT match background or primary |
|
|
12
|
+
| **background** | Slide background | Near-white (#FFFFFF or #F8F9FA) for most decks; near-black for dark-mode decks |
|
|
13
|
+
| **text** | Body text, bullet text | ≥ 4.5:1 contrast against background. Default `#222222` on white, `#EAEAEA` on dark |
|
|
14
|
+
|
|
15
|
+
### Hard rules
|
|
16
|
+
|
|
17
|
+
- **Contrast ≥ 4.5:1** between text and background (WCAG AA). If the user-supplied palette fails this, *substitute* and explain — don't ship an unreadable deck.
|
|
18
|
+
- **Never put primary text on primary background** — use white-on-primary or vice versa for title bars/section slides.
|
|
19
|
+
- **Accent appears on ≤ 20% of slides.** It's a pop colour, not a body colour.
|
|
20
|
+
|
|
21
|
+
### Preset palettes
|
|
22
|
+
|
|
23
|
+
These are bundled in [`templates/palettes.json`](templates/palettes.json) and selectable by name:
|
|
24
|
+
|
|
25
|
+
| Name | When to use |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `corporate-blue` | Default for B2B / enterprise audiences when no brand colour is given |
|
|
28
|
+
| `monochrome` | When the deck needs to feel restrained / when content is sensitive |
|
|
29
|
+
| `vibrant` | Internal / sales / pitch decks — energetic |
|
|
30
|
+
| `dark-mode` | Engineering audiences who prefer dark, or for live demos |
|
|
31
|
+
| `editorial` | Long-form / read-ahead decks with lots of text |
|
|
32
|
+
|
|
33
|
+
## Layout → content matching
|
|
34
|
+
|
|
35
|
+
The biggest deck-design mistake is "everything is a bulleted content slide". Use this mapping to pick the right layout for the content:
|
|
36
|
+
|
|
37
|
+
| Content shape | Right layout | Wrong layout |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| One key takeaway you want to land | `big_number` | `content` (gets lost in bullets) |
|
|
40
|
+
| A list of 2–6 related points | `content` | `two_column` (over-engineered) |
|
|
41
|
+
| A comparison (us vs. them, before vs. after) | `two_column` | `content` with sub-bullets |
|
|
42
|
+
| A pithy customer / SME quote | `quote` | `content` ("Customer says…") |
|
|
43
|
+
| A topic break / new section | `section` | another title slide |
|
|
44
|
+
| First slide | `title` | jumping straight into `content` |
|
|
45
|
+
| Final slide with The Ask | `closing` | `content` titled "Thank you" |
|
|
46
|
+
|
|
47
|
+
## Per-audience density rules
|
|
48
|
+
|
|
49
|
+
How many bullets a slide can carry without falling apart, by audience:
|
|
50
|
+
|
|
51
|
+
| Audience | Bullets / slide | Words / bullet | Slide-title style |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `exec` | 3 max | ≤ 8 | Assertion ("Postgres meets latency target") |
|
|
54
|
+
| `board` | 3 max | ≤ 6 | Outcome ("$24M ARR protected by Q3") |
|
|
55
|
+
| `technical` | 5–6 | ≤ 14 | Descriptive ok ("Migration approach") |
|
|
56
|
+
| `sales` | 3–4 | ≤ 10 | Benefit-led ("Cut close time by 40%") |
|
|
57
|
+
| `internal` | 4–5 | ≤ 12 | Topic ok ("Q3 priorities") |
|
|
58
|
+
| `investor` | 3–4 | ≤ 10 | Assertion + number where possible |
|
|
59
|
+
| `partner` | 3–4 | ≤ 10 | Mutual benefit ("Joint customers see X") |
|
|
60
|
+
| `customer` | 3–4 | ≤ 12 | Outcome for them ("You'll save…") |
|
|
61
|
+
|
|
62
|
+
## Slide-count budgets
|
|
63
|
+
|
|
64
|
+
| Audience / format | Slide count target | Hard upper bound |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `exec` quick update (5 min) | 5 | 8 |
|
|
67
|
+
| `exec` decision review (15 min) | 8–10 | 12 |
|
|
68
|
+
| `board` quarterly (20 min) | 10 | 15 |
|
|
69
|
+
| `technical` design review (30 min) | 12–15 | 20 |
|
|
70
|
+
| `sales` first pitch (15 min) | 8 | 12 |
|
|
71
|
+
| `investor` Series-style | 10–12 | 15 |
|
|
72
|
+
| `internal` team alignment | 5–8 | 10 |
|
|
73
|
+
|
|
74
|
+
If the user's content can't fit in the upper bound, *say so* — recommend splitting into two decks rather than cramming.
|
|
75
|
+
|
|
76
|
+
## Speaker notes patterns
|
|
77
|
+
|
|
78
|
+
Every slide should have notes. The pattern that works:
|
|
79
|
+
|
|
80
|
+
1. **Hook sentence** — what the presenter opens with on this slide.
|
|
81
|
+
2. **Key emphasis** — which bullet/number to dwell on.
|
|
82
|
+
3. **Transition** — one sentence that flows into the next slide.
|
|
83
|
+
|
|
84
|
+
Speaker notes should be *what's not on the slide*, not a redundant copy.
|
|
85
|
+
|
|
86
|
+
## Common gotchas
|
|
87
|
+
|
|
88
|
+
- **Don't centre body text.** Centred bullets are harder to scan. Reserve centring for `big_number`, `quote`, and `title`/`closing` slides.
|
|
89
|
+
- **Don't underline links** — coloured-and-bold is the modern convention.
|
|
90
|
+
- **Don't use both italic and bold** in the same sentence for emphasis.
|
|
91
|
+
- **Don't mix serif and sans-serif** unless you really know what you're doing.
|
|
92
|
+
- **Slide numbers** — include them for decks > 10 slides; omit for short decks.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python-pptx>=0.6.21
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build a .pptx from a JSON deck spec.
|
|
3
|
+
|
|
4
|
+
The spec schema is documented in templates/deck-spec.json. Top-level fields:
|
|
5
|
+
- title: overall deck title
|
|
6
|
+
- audience: archetype (informational; doesn't change rendering)
|
|
7
|
+
- palette: either a hex-color block, or a string name resolvable from templates/palettes.json
|
|
8
|
+
- slides: list of slide specs, each with a `layout` field
|
|
9
|
+
|
|
10
|
+
Supported layouts: title, section, content, two_column, big_number, quote, closing.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 build_deck.py <spec.json> [-o output.pptx]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from pptx import Presentation
|
|
24
|
+
from pptx.dml.color import RGBColor
|
|
25
|
+
from pptx.enum.shapes import MSO_SHAPE
|
|
26
|
+
from pptx.enum.text import PP_ALIGN
|
|
27
|
+
from pptx.util import Inches, Pt
|
|
28
|
+
|
|
29
|
+
SLIDE_W = Inches(13.333)
|
|
30
|
+
SLIDE_H = Inches(7.5)
|
|
31
|
+
|
|
32
|
+
DEFAULT_PALETTE = {
|
|
33
|
+
"primary": "#1A2A6C",
|
|
34
|
+
"secondary": "#4A5A8C",
|
|
35
|
+
"accent": "#FDBB2D",
|
|
36
|
+
"background": "#FFFFFF",
|
|
37
|
+
"text": "#222222",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def hex_color(s: str) -> RGBColor:
|
|
42
|
+
return RGBColor.from_string(s.lstrip("#"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_palette(p, script_dir: Path) -> dict:
|
|
46
|
+
"""Accepts a dict (use directly) or a string name (look up in templates/palettes.json)."""
|
|
47
|
+
if isinstance(p, dict):
|
|
48
|
+
return {**DEFAULT_PALETTE, **p}
|
|
49
|
+
if isinstance(p, str):
|
|
50
|
+
palettes_path = script_dir.parent / "templates" / "palettes.json"
|
|
51
|
+
if not palettes_path.exists():
|
|
52
|
+
sys.stderr.write(f"warning: palettes file missing at {palettes_path}; using defaults\n")
|
|
53
|
+
return DEFAULT_PALETTE
|
|
54
|
+
all_palettes = json.loads(palettes_path.read_text())
|
|
55
|
+
if p not in all_palettes:
|
|
56
|
+
sys.stderr.write(f"warning: palette {p!r} not found; using defaults\n")
|
|
57
|
+
return DEFAULT_PALETTE
|
|
58
|
+
return {**DEFAULT_PALETTE, **all_palettes[p]}
|
|
59
|
+
return DEFAULT_PALETTE
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def set_background(slide, hex_str: str) -> None:
|
|
63
|
+
fill = slide.background.fill
|
|
64
|
+
fill.solid()
|
|
65
|
+
fill.fore_color.rgb = hex_color(hex_str)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def add_textbox(slide, text: str, *, left, top, width, height,
|
|
69
|
+
size: int = 18, bold: bool = False, color: str = "#222222",
|
|
70
|
+
align=PP_ALIGN.LEFT) -> None:
|
|
71
|
+
box = slide.shapes.add_textbox(left, top, width, height)
|
|
72
|
+
tf = box.text_frame
|
|
73
|
+
tf.word_wrap = True
|
|
74
|
+
p = tf.paragraphs[0]
|
|
75
|
+
p.alignment = align
|
|
76
|
+
run = p.add_run()
|
|
77
|
+
run.text = text
|
|
78
|
+
run.font.size = Pt(size)
|
|
79
|
+
run.font.bold = bold
|
|
80
|
+
run.font.color.rgb = hex_color(color)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def add_bullets(slide, bullets, *, left, top, width, height,
|
|
84
|
+
size: int = 18, color: str = "#222222") -> None:
|
|
85
|
+
box = slide.shapes.add_textbox(left, top, width, height)
|
|
86
|
+
tf = box.text_frame
|
|
87
|
+
tf.word_wrap = True
|
|
88
|
+
for i, b in enumerate(bullets):
|
|
89
|
+
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
90
|
+
p.text = f"• {b}"
|
|
91
|
+
p.space_after = Pt(10)
|
|
92
|
+
for run in p.runs:
|
|
93
|
+
run.font.size = Pt(size)
|
|
94
|
+
run.font.color.rgb = hex_color(color)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def add_rect(slide, *, left, top, width, height, fill: str) -> None:
|
|
98
|
+
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
|
|
99
|
+
shape.fill.solid()
|
|
100
|
+
shape.fill.fore_color.rgb = hex_color(fill)
|
|
101
|
+
shape.line.fill.background()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def set_notes(slide, notes: str) -> None:
|
|
105
|
+
nf = slide.notes_slide.notes_text_frame
|
|
106
|
+
nf.text = notes
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ----------------------------------------------------------------- renderers
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def render_title(slide, palette, spec) -> None:
|
|
113
|
+
set_background(slide, palette["background"])
|
|
114
|
+
add_rect(slide, left=0, top=Inches(3.0), width=SLIDE_W, height=Inches(0.15),
|
|
115
|
+
fill=palette["accent"])
|
|
116
|
+
add_textbox(slide, spec.get("title", "Untitled"),
|
|
117
|
+
left=Inches(0.75), top=Inches(2.0),
|
|
118
|
+
width=Inches(11.8), height=Inches(1.2),
|
|
119
|
+
size=44, bold=True, color=palette["primary"])
|
|
120
|
+
if spec.get("subtitle"):
|
|
121
|
+
add_textbox(slide, spec["subtitle"],
|
|
122
|
+
left=Inches(0.75), top=Inches(3.4),
|
|
123
|
+
width=Inches(11.8), height=Inches(0.8),
|
|
124
|
+
size=24, color=palette["text"])
|
|
125
|
+
footer_bits = [spec.get("presenter"), spec.get("date")]
|
|
126
|
+
footer = " · ".join([s for s in footer_bits if s])
|
|
127
|
+
if footer:
|
|
128
|
+
add_textbox(slide, footer,
|
|
129
|
+
left=Inches(0.75), top=Inches(6.5),
|
|
130
|
+
width=Inches(11.8), height=Inches(0.5),
|
|
131
|
+
size=14, color=palette["text"])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_section(slide, palette, spec) -> None:
|
|
135
|
+
set_background(slide, palette["primary"])
|
|
136
|
+
add_textbox(slide, spec.get("title", ""),
|
|
137
|
+
left=Inches(0.75), top=Inches(3.0),
|
|
138
|
+
width=Inches(11.8), height=Inches(1.5),
|
|
139
|
+
size=48, bold=True, color="#FFFFFF")
|
|
140
|
+
add_rect(slide, left=Inches(0.75), top=Inches(4.6),
|
|
141
|
+
width=Inches(2), height=Inches(0.1),
|
|
142
|
+
fill=palette["accent"])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _title_bar(slide, palette, title: str) -> None:
|
|
146
|
+
add_rect(slide, left=0, top=0, width=SLIDE_W, height=Inches(1.0),
|
|
147
|
+
fill=palette["primary"])
|
|
148
|
+
add_textbox(slide, title,
|
|
149
|
+
left=Inches(0.5), top=Inches(0.22),
|
|
150
|
+
width=Inches(12.3), height=Inches(0.7),
|
|
151
|
+
size=26, bold=True, color="#FFFFFF")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def render_content(slide, palette, spec) -> None:
|
|
155
|
+
set_background(slide, palette["background"])
|
|
156
|
+
_title_bar(slide, palette, spec.get("title", ""))
|
|
157
|
+
bullets = spec.get("bullets") or []
|
|
158
|
+
if bullets:
|
|
159
|
+
add_bullets(slide, bullets,
|
|
160
|
+
left=Inches(0.75), top=Inches(1.5),
|
|
161
|
+
width=Inches(11.8), height=Inches(5.5),
|
|
162
|
+
size=20, color=palette["text"])
|
|
163
|
+
if spec.get("body"):
|
|
164
|
+
add_textbox(slide, spec["body"],
|
|
165
|
+
left=Inches(0.75), top=Inches(1.5),
|
|
166
|
+
width=Inches(11.8), height=Inches(5.5),
|
|
167
|
+
size=18, color=palette["text"])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def render_two_column(slide, palette, spec) -> None:
|
|
171
|
+
set_background(slide, palette["background"])
|
|
172
|
+
_title_bar(slide, palette, spec.get("title", ""))
|
|
173
|
+
for col_key, col_left in (("left", Inches(0.5)), ("right", Inches(6.85))):
|
|
174
|
+
col = spec.get(col_key, {})
|
|
175
|
+
if col.get("title"):
|
|
176
|
+
add_textbox(slide, col["title"],
|
|
177
|
+
left=col_left, top=Inches(1.3),
|
|
178
|
+
width=Inches(6), height=Inches(0.6),
|
|
179
|
+
size=20, bold=True, color=palette["primary"])
|
|
180
|
+
if col.get("bullets"):
|
|
181
|
+
add_bullets(slide, col["bullets"],
|
|
182
|
+
left=col_left, top=Inches(2.0),
|
|
183
|
+
width=Inches(6), height=Inches(5),
|
|
184
|
+
size=18, color=palette["text"])
|
|
185
|
+
elif col.get("body"):
|
|
186
|
+
add_textbox(slide, col["body"],
|
|
187
|
+
left=col_left, top=Inches(2.0),
|
|
188
|
+
width=Inches(6), height=Inches(5),
|
|
189
|
+
size=18, color=palette["text"])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def render_big_number(slide, palette, spec) -> None:
|
|
193
|
+
set_background(slide, palette["background"])
|
|
194
|
+
_title_bar(slide, palette, spec.get("title", ""))
|
|
195
|
+
add_textbox(slide, spec.get("number", "—"),
|
|
196
|
+
left=Inches(0.5), top=Inches(1.8),
|
|
197
|
+
width=Inches(12.3), height=Inches(3.5),
|
|
198
|
+
size=140, bold=True, color=palette["accent"],
|
|
199
|
+
align=PP_ALIGN.CENTER)
|
|
200
|
+
if spec.get("caption"):
|
|
201
|
+
add_textbox(slide, spec["caption"],
|
|
202
|
+
left=Inches(0.5), top=Inches(5.6),
|
|
203
|
+
width=Inches(12.3), height=Inches(1),
|
|
204
|
+
size=22, color=palette["text"],
|
|
205
|
+
align=PP_ALIGN.CENTER)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def render_quote(slide, palette, spec) -> None:
|
|
209
|
+
set_background(slide, palette["background"])
|
|
210
|
+
add_rect(slide, left=Inches(1.4), top=Inches(2.0),
|
|
211
|
+
width=Inches(0.12), height=Inches(3.5),
|
|
212
|
+
fill=palette["accent"])
|
|
213
|
+
add_textbox(slide, f"“{spec.get('quote', '')}”",
|
|
214
|
+
left=Inches(1.9), top=Inches(2.0),
|
|
215
|
+
width=Inches(10), height=Inches(3),
|
|
216
|
+
size=30, color=palette["primary"])
|
|
217
|
+
if spec.get("attribution"):
|
|
218
|
+
add_textbox(slide, f"— {spec['attribution']}",
|
|
219
|
+
left=Inches(1.9), top=Inches(5.4),
|
|
220
|
+
width=Inches(10), height=Inches(0.7),
|
|
221
|
+
size=18, color=palette["text"])
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def render_closing(slide, palette, spec) -> None:
|
|
225
|
+
set_background(slide, palette["primary"])
|
|
226
|
+
add_textbox(slide, spec.get("title", "Thank you"),
|
|
227
|
+
left=Inches(0.5), top=Inches(2.5),
|
|
228
|
+
width=Inches(12.3), height=Inches(1.5),
|
|
229
|
+
size=54, bold=True, color="#FFFFFF",
|
|
230
|
+
align=PP_ALIGN.CENTER)
|
|
231
|
+
if spec.get("subtitle"):
|
|
232
|
+
add_textbox(slide, spec["subtitle"],
|
|
233
|
+
left=Inches(0.5), top=Inches(4.3),
|
|
234
|
+
width=Inches(12.3), height=Inches(0.8),
|
|
235
|
+
size=22, color=palette["accent"],
|
|
236
|
+
align=PP_ALIGN.CENTER)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
RENDERERS = {
|
|
240
|
+
"title": render_title,
|
|
241
|
+
"section": render_section,
|
|
242
|
+
"content": render_content,
|
|
243
|
+
"two_column": render_two_column,
|
|
244
|
+
"big_number": render_big_number,
|
|
245
|
+
"quote": render_quote,
|
|
246
|
+
"closing": render_closing,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ----------------------------------------------------------------- driver
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def build(spec_path: Path, out_path: Path, script_dir: Path) -> None:
|
|
254
|
+
spec = json.loads(spec_path.read_text())
|
|
255
|
+
palette = resolve_palette(spec.get("palette"), script_dir)
|
|
256
|
+
|
|
257
|
+
prs = Presentation()
|
|
258
|
+
prs.slide_width = SLIDE_W
|
|
259
|
+
prs.slide_height = SLIDE_H
|
|
260
|
+
blank_layout = prs.slide_layouts[6]
|
|
261
|
+
|
|
262
|
+
slides = spec.get("slides", [])
|
|
263
|
+
if not slides:
|
|
264
|
+
sys.stderr.write("error: spec has no slides\n")
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
|
|
267
|
+
for i, slide_spec in enumerate(slides, start=1):
|
|
268
|
+
layout = slide_spec.get("layout", "content")
|
|
269
|
+
renderer = RENDERERS.get(layout)
|
|
270
|
+
if renderer is None:
|
|
271
|
+
sys.stderr.write(
|
|
272
|
+
f"warning: unknown layout {layout!r} on slide {i}; falling back to content\n"
|
|
273
|
+
)
|
|
274
|
+
renderer = render_content
|
|
275
|
+
slide = prs.slides.add_slide(blank_layout)
|
|
276
|
+
renderer(slide, palette, slide_spec)
|
|
277
|
+
if slide_spec.get("notes"):
|
|
278
|
+
set_notes(slide, slide_spec["notes"])
|
|
279
|
+
|
|
280
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
prs.save(str(out_path))
|
|
282
|
+
print(f"wrote {out_path} ({len(slides)} slides)")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main(argv) -> int:
|
|
286
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
287
|
+
ap.add_argument("spec", help="Path to deck spec JSON")
|
|
288
|
+
ap.add_argument("-o", "--output", default=None,
|
|
289
|
+
help="Output .pptx path (default: alongside the spec)")
|
|
290
|
+
args = ap.parse_args(argv)
|
|
291
|
+
|
|
292
|
+
spec_path = Path(args.spec).expanduser().resolve()
|
|
293
|
+
if not spec_path.exists():
|
|
294
|
+
sys.stderr.write(f"error: spec file not found: {spec_path}\n")
|
|
295
|
+
return 1
|
|
296
|
+
|
|
297
|
+
out_path = (
|
|
298
|
+
Path(args.output).expanduser().resolve()
|
|
299
|
+
if args.output
|
|
300
|
+
else spec_path.with_suffix(".pptx")
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
script_dir = Path(__file__).resolve().parent
|
|
304
|
+
build(spec_path, out_path, script_dir)
|
|
305
|
+
return 0
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Full deck-spec schema. The build_deck.py script consumes this. Top-level fields: title, audience, palette, slides[].",
|
|
3
|
+
|
|
4
|
+
"title": "ProjectX Migration — Board Update",
|
|
5
|
+
"audience": "board",
|
|
6
|
+
|
|
7
|
+
"palette": {
|
|
8
|
+
"primary": "#1A2A6C",
|
|
9
|
+
"secondary": "#4A5A8C",
|
|
10
|
+
"accent": "#FDBB2D",
|
|
11
|
+
"background": "#FFFFFF",
|
|
12
|
+
"text": "#222222"
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"slides": [
|
|
16
|
+
{
|
|
17
|
+
"layout": "title",
|
|
18
|
+
"title": "ProjectX Migration",
|
|
19
|
+
"subtitle": "Board update — Q3 FY26",
|
|
20
|
+
"presenter": "John Doe",
|
|
21
|
+
"date": "2026-09-15",
|
|
22
|
+
"notes": "Set the scene in one sentence — we're 60% through, ask is to approve overrun budget."
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
"layout": "section",
|
|
27
|
+
"title": "Where we are",
|
|
28
|
+
"notes": "Quick orientation before diving into the numbers."
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
"layout": "content",
|
|
33
|
+
"title": "Migration is 60% complete and on track for Q1 close",
|
|
34
|
+
"bullets": [
|
|
35
|
+
"8 of 14 services cut over with zero customer-visible incidents",
|
|
36
|
+
"Performance up 22% on migrated services (p99 latency)",
|
|
37
|
+
"Two services pushed to phase 3 — schema rewrite needed first"
|
|
38
|
+
],
|
|
39
|
+
"notes": "Lead with the positive — zero incidents matters. Note the slipped services so it isn't a surprise later."
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
"layout": "big_number",
|
|
44
|
+
"title": "Cost savings already realised",
|
|
45
|
+
"number": "$2.4M",
|
|
46
|
+
"caption": "Annualised infra savings from the 8 migrated services",
|
|
47
|
+
"notes": "Pause here. Let the number land. Then say: we expect $5.8M annualised at full completion."
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
"layout": "two_column",
|
|
52
|
+
"title": "What's left vs. what's added",
|
|
53
|
+
"left": {
|
|
54
|
+
"title": "Original scope",
|
|
55
|
+
"bullets": [
|
|
56
|
+
"14 services migrated",
|
|
57
|
+
"Legacy DB decommissioned",
|
|
58
|
+
"Single-region active"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"right": {
|
|
62
|
+
"title": "Discovered in flight",
|
|
63
|
+
"bullets": [
|
|
64
|
+
"2 services need schema rewrite",
|
|
65
|
+
"Multi-region requirement (regulatory)",
|
|
66
|
+
"Auth refactor coupled with cut-over"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"notes": "Frame the scope additions honestly. Multi-region was a regulator change, not a planning miss."
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
"layout": "quote",
|
|
74
|
+
"quote": "The migration went smoother than any I've seen in 20 years — and our team learned more in 6 months than the prior 2.",
|
|
75
|
+
"attribution": "VP Engineering, after phase 2 cut-over",
|
|
76
|
+
"notes": "Optional, but lands well with a board that's worried about morale."
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
"layout": "content",
|
|
81
|
+
"title": "Top risks for the remaining 40%",
|
|
82
|
+
"bullets": [
|
|
83
|
+
"Schema rewrite — 6 weeks, narrowly de-risked by dry runs",
|
|
84
|
+
"Auth refactor coupling — increases coordination overhead",
|
|
85
|
+
"Holiday-period freeze cuts available calendar by 4 weeks"
|
|
86
|
+
],
|
|
87
|
+
"notes": "Don't soften the risks. Boards trust presenters who name them."
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
"layout": "closing",
|
|
92
|
+
"title": "Asking for $620k FY26 budget overrun",
|
|
93
|
+
"subtitle": "Covers schema rewrite + multi-region; payback < 8 months",
|
|
94
|
+
"notes": "Land on the Ask. Take questions here, not after a thank-you slide."
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Pre-built palettes. Reference by name in a deck spec via `palette: \"corporate-blue\"` (the script resolves the name to these values), or copy/paste the block into the spec's `palette` field.",
|
|
3
|
+
|
|
4
|
+
"corporate-blue": {
|
|
5
|
+
"primary": "#1A2A6C",
|
|
6
|
+
"secondary": "#4A5A8C",
|
|
7
|
+
"accent": "#FDBB2D",
|
|
8
|
+
"background": "#FFFFFF",
|
|
9
|
+
"text": "#222222"
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"monochrome": {
|
|
13
|
+
"primary": "#212121",
|
|
14
|
+
"secondary": "#555555",
|
|
15
|
+
"accent": "#888888",
|
|
16
|
+
"background": "#FFFFFF",
|
|
17
|
+
"text": "#1A1A1A"
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"vibrant": {
|
|
21
|
+
"primary": "#FF3366",
|
|
22
|
+
"secondary": "#FF9933",
|
|
23
|
+
"accent": "#33CCFF",
|
|
24
|
+
"background": "#FFFFFF",
|
|
25
|
+
"text": "#1F1F1F"
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"dark-mode": {
|
|
29
|
+
"primary": "#4FC3F7",
|
|
30
|
+
"secondary": "#7E57C2",
|
|
31
|
+
"accent": "#FFD54F",
|
|
32
|
+
"background": "#121212",
|
|
33
|
+
"text": "#EAEAEA"
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
"editorial": {
|
|
37
|
+
"primary": "#2C3E50",
|
|
38
|
+
"secondary": "#7F8C8D",
|
|
39
|
+
"accent": "#E67E22",
|
|
40
|
+
"background": "#FDFBF7",
|
|
41
|
+
"text": "#2C3E50"
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"forest": {
|
|
45
|
+
"primary": "#1B5E20",
|
|
46
|
+
"secondary": "#558B2F",
|
|
47
|
+
"accent": "#FBC02D",
|
|
48
|
+
"background": "#FFFFFF",
|
|
49
|
+
"text": "#1B1B1B"
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"sunset": {
|
|
53
|
+
"primary": "#C2185B",
|
|
54
|
+
"secondary": "#F57C00",
|
|
55
|
+
"accent": "#FFEB3B",
|
|
56
|
+
"background": "#FFF8F0",
|
|
57
|
+
"text": "#2A1A1A"
|
|
58
|
+
}
|
|
59
|
+
}
|