portable-agent-layer 0.32.0 → 0.34.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 +1 -0
- package/assets/skills/presentation/SKILL.md +124 -5
- package/assets/skills/presentation/WORKSHOP.md +128 -0
- package/assets/skills/presentation/theme-base/base.css +113 -0
- package/assets/skills/presentation/theme-base/layouts.css +11 -2
- package/assets/skills/presentation/tools/build.ts +136 -6
- package/assets/skills/presentation/tools/doctor.ts +106 -317
- package/assets/skills/presentation/tools/lib/lint-helpers.ts +150 -0
- package/assets/skills/presentation/tools/lib/lint-rules.ts +744 -0
- package/assets/skills/presentation/tools/lib/lint-types.ts +40 -0
- package/assets/skills/presentation/tools/new-deck.ts +9 -4
- package/assets/skills/presentation/vendor/reveal/plugin/highlight/github-dark.css +118 -0
- package/assets/skills/projects/SKILL.md +111 -0
- package/assets/skills/telos/SKILL.md +4 -1
- package/assets/templates/AGENTS.md.template +28 -7
- package/assets/templates/PAL/ALGORITHM.md +2 -0
- package/assets/templates/PAL/README.md +1 -2
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +1 -1
- package/assets/templates/pal-settings.json +2 -2
- package/package.json +2 -3
- package/src/cli/index.ts +7 -0
- package/src/hooks/UserPromptOrchestrator.ts +3 -1
- package/src/hooks/handlers/auto-graduate.ts +169 -0
- package/src/hooks/handlers/inject-retrieval.ts +50 -0
- package/src/hooks/handlers/project-touch.ts +39 -0
- package/src/hooks/lib/context.ts +9 -8
- package/src/hooks/lib/paths.ts +2 -0
- package/src/hooks/lib/projects.ts +270 -0
- package/src/hooks/lib/retrieval-index.ts +223 -0
- package/src/hooks/lib/retrieval.ts +170 -0
- package/src/hooks/lib/security.ts +2 -0
- package/src/hooks/lib/stop.ts +9 -1
- package/src/hooks/lib/text-similarity.ts +13 -9
- package/src/hooks/lib/wisdom.ts +155 -1
- package/src/tools/agent/project.ts +336 -0
- package/src/tools/self-model.ts +3 -3
- package/src/tools/token-cost.ts +4 -4
- package/assets/templates/PAL/CONTEXT_ROUTING.md +0 -30
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ pal cli status # check your setup
|
|
|
82
82
|
| `pal cli import` | Import user state from a zip |
|
|
83
83
|
| `pal cli status` | Show current PAL configuration |
|
|
84
84
|
| `pal cli doctor` | Check prerequisites and system health |
|
|
85
|
+
| `pal cli usage` | Summarize token usage and estimated cost |
|
|
85
86
|
|
|
86
87
|
### Target flags
|
|
87
88
|
|
|
@@ -59,9 +59,10 @@ Authoring this way means: a malformed edit only takes down its own slide, slides
|
|
|
59
59
|
Per-slide conventions (inside each file):
|
|
60
60
|
- Speaker notes: lines starting with `Note:`.
|
|
61
61
|
- Layout directive: `<!-- .slide: data-layout="..." -->` at the top.
|
|
62
|
+
- Image references: `` — the natural relative path from `slides/*.md` to the deck's `assets/` folder. The build rewrites this to `assets/foo.png` in the concatenated `<deck-name>.md` (which sits at the deck root, so the path stays valid for direct preview), and inlines the image bytes as a `data:` URI in the HTML so the output is truly self-contained (emailable, USB-stickable). Remote refs (`https://…`) and `data:` URIs pass through untouched.
|
|
62
63
|
- See "Layouts" below for the available layout names.
|
|
63
64
|
|
|
64
|
-
Backwards compatible: if `slides/` doesn't exist, the build falls back to a single `<deck-dir>/content.md` with `---` separators between slides.
|
|
65
|
+
Backwards compatible: if `slides/` doesn't exist, the build falls back to a single `<deck-dir>/content.md` with `---` separators between slides. Image refs there should use `assets/foo.png` (root-relative), since `content.md` already lives at the deck root.
|
|
65
66
|
|
|
66
67
|
### Step 3: Build
|
|
67
68
|
|
|
@@ -69,12 +70,14 @@ Backwards compatible: if `slides/` doesn't exist, the build falls back to a sing
|
|
|
69
70
|
bun ~/.pal/skills/presentation/tools/build.ts <deck-dir> [--out <dir>] [--force]
|
|
70
71
|
```
|
|
71
72
|
|
|
72
|
-
Output
|
|
73
|
+
Output files (where `<deck-name>` = basename of `<deck-dir>`):
|
|
73
74
|
|
|
74
75
|
- `<deck-name>.md` — the concatenated source (all `slides/*.md` joined with `---` separators), written to disk **first** so you can inspect, diff, or feed it into other tooling.
|
|
75
76
|
- `<deck-name>.html` — the self-contained presentation (CSS, JS, fonts, logo all inlined). Email it, USB-stick it, host it anywhere.
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
Default location: `--out` defaults to the **deck-dir itself**, and the files land flat at `<deck-dir>/<deck-name>.{html,md}` — next to your slides, regardless of where you ran the build from. The scaffolder's `.gitignore` already covers them.
|
|
79
|
+
|
|
80
|
+
Override: pass `--out <dir>` to redirect elsewhere. When `--out` is *not* the deck-dir, a `<deck-name>/` subdir is created under it so multiple decks can coexist in one collection folder.
|
|
78
81
|
|
|
79
82
|
`--force` overwrites an existing build. Without it, the build refuses to clobber `<deck-name>.{md,html}`.
|
|
80
83
|
|
|
@@ -90,7 +93,8 @@ Catches authoring failures before you ever open the browser:
|
|
|
90
93
|
- Title or subtitle exceeding the visual budget (h1 > 60 chars, h2 > 100).
|
|
91
94
|
- Layout-content mismatch — `comparison` without a `<div class="compare">`, `metric-grid` without `<div class="metrics">`, `two-column` missing column wrappers, etc.
|
|
92
95
|
- Overflow heuristics — `agenda` > 10 items, `content` > 7 bullets, `code` block > 25 lines, `table` > 10 rows, `metric-grid` ≠ 3 metrics.
|
|
93
|
-
-
|
|
96
|
+
- **Visual-line budget**: any bullet-bearing slide (`content`, `agenda`, `comparison`, `two-column`) with > 10 flattened list lines (top-level + sub-bullets combined). 10 fits cleanly; 11+ overflows even when each line is short.
|
|
97
|
+
- Image referenced via `` (slide-relative) or `` (root-relative, for legacy `content.md` decks) but the file is missing.
|
|
94
98
|
- Layout requirements — `big-stat` without an h1, `quote` / `pull-quote` without a `> blockquote`.
|
|
95
99
|
|
|
96
100
|
Exit codes: `0` = clean, `1` = errors found (or warnings under `--strict`), `2` = usage error. Run before each build in your iteration loop, or wire it into a pre-commit hook.
|
|
@@ -112,10 +116,105 @@ Open `<out>/<deck-name>/<deck-name>.html` in your browser. Iterate by editing a
|
|
|
112
116
|
└── assets/ # images / videos referenced from slides/*.md
|
|
113
117
|
```
|
|
114
118
|
|
|
115
|
-
Build output
|
|
119
|
+
Build output lands flat inside the deck-dir by default (`<deck-dir>/<deck-name>.{html,md}`), or under `<out>/<deck-name>/` when `--out` is explicit. Both shapes are covered by the scaffolder's `.gitignore`.
|
|
116
120
|
|
|
117
121
|
(Legacy: a single `content.md` at the deck root still works — see Step 2.)
|
|
118
122
|
|
|
123
|
+
## Content principles
|
|
124
|
+
|
|
125
|
+
The doctor enforces *structural* discipline (slide budgets, layout-content match, missing assets). These rules are about *content* — what goes on the slide and in the notes. They apply to every deck regardless of type. For type-specific rules, see the "see also" links at the bottom of this section.
|
|
126
|
+
|
|
127
|
+
### Core rules
|
|
128
|
+
|
|
129
|
+
- **A slide carries what the *learner* takes away — never what happens in the room.** "We will discuss CI/CD agents" is not a slide; "CodeRabbit and CodeAnt cost ~$15/dev/mo at this scale" is.
|
|
130
|
+
- **Less is more on bullets.** The doctor caps `content` at 7 bullets; the content rule is stricter — 3–5 is usually right. One bullet is not a slide; either expand or fold into the previous one.
|
|
131
|
+
- **Two ideas → two slides.** If a slide carries two takeaways, split.
|
|
132
|
+
- **No sentences on slides or in notes.** Exceptions: quotes, code comments, single callout sentences. Everything else is bullets, including notes.
|
|
133
|
+
- **A title-only slide is valid only as a `section` divider.** Single-word slides and single-bullet slides are not valid — split or expand.
|
|
134
|
+
- **Vocabulary consistency.** Pick one word per concept ("agent" vs "assistant", "tool" vs "capability") and use it everywhere. Drift confuses the audience.
|
|
135
|
+
- **Layout choice = content choice, not decoration.** `big-stat` says "this number *is* the point." `comparison` says "you have to choose between these." `quote` says "someone earned the right to say this." Wrong layout muddies the message.
|
|
136
|
+
- **Define jargon before using it.** First use of a non-obvious term gets a parenthesized gloss or its own glossary slide. After that, no.
|
|
137
|
+
|
|
138
|
+
### Bullet & sub-bullet structure
|
|
139
|
+
|
|
140
|
+
- **Top-level bullet = one fact or one claim.** 6–12 words. If you need more, split or use a sub-bullet.
|
|
141
|
+
- **Sub-bullets = elaboration of the parent.** A list, an example, a clarification. 2–8 words each. Don't start a new claim at a sub-bullet — that's a top-level bullet.
|
|
142
|
+
- **Never use em-dash continuations to extend a bullet.** `- Foo — bar — baz` is prose pretending to be a bullet. Convert to:
|
|
143
|
+
```markdown
|
|
144
|
+
- Foo
|
|
145
|
+
- bar
|
|
146
|
+
- baz
|
|
147
|
+
```
|
|
148
|
+
Em-dash is reserved for: title qualifiers ("Block 4 — Landscape"), and anticipated Q&A in notes (`"Question?" — short answer`).
|
|
149
|
+
- **Stable parallel structure inside a bullet group.** If bullet 1 starts with a verb, bullets 2–N start with verbs. If bullet 1 is `Name: description`, the rest match.
|
|
150
|
+
- **Concrete over abstract.** Show paths, file names, numbers, command snippets in backticks. "the project file" is weaker than `./CLAUDE.md`.
|
|
151
|
+
|
|
152
|
+
### Notes structure
|
|
153
|
+
|
|
154
|
+
Notes are the speaker's working surface during delivery. They must be scannable, not readable. Strict format:
|
|
155
|
+
|
|
156
|
+
1. **Source links first**, one per line, before any explanation. Multiple links are fine if the slide cites multiple sources. The speaker should never scroll to find a link they're about to open in the browser.
|
|
157
|
+
2. **Named beats as top-level bullets.** A "beat" is a named chunk of context — `Eval setup`, `Headline numbers`, `Why X wins`, `Common output`, `Anticipated questions`. The beat name lets the speaker jump.
|
|
158
|
+
3. **Sub-bullets under each beat.** Same 2–8 word budget. Lists, edge cases, examples. No prose.
|
|
159
|
+
4. **Quotes nest under a `- Quote` beat** as a markdown blockquote (`>`). Quotes are the only place full sentences are allowed in notes.
|
|
160
|
+
5. **Anticipated Q&A near the end.** Format: `- "Question?" — short answer`. The em-dash here separates the quote from the answer; this is the legal use.
|
|
161
|
+
6. **Forward references are terse.** "more in Day 2 when you build your own", "see Block 2 for X". Not "we'll discuss this later" prose.
|
|
162
|
+
|
|
163
|
+
### Voice
|
|
164
|
+
|
|
165
|
+
- **Declarative, not hedging.** "Anthropic's holdout" beats "Anthropic has chosen a different approach." If you mean it, say it; if you don't, cut it.
|
|
166
|
+
- **Opinionated where the room expects opinion.** Workshops are taught, not narrated. State which option you'd pick and why.
|
|
167
|
+
- **Concrete identifiers visible.** File paths, version numbers, URLs, exact tool names. The audience should be able to type what's on the slide and have it work.
|
|
168
|
+
|
|
169
|
+
### Examples
|
|
170
|
+
|
|
171
|
+
**Bad slide — describes the room, not the takeaway:**
|
|
172
|
+
|
|
173
|
+
```markdown
|
|
174
|
+
<!-- .slide: data-layout="content" -->
|
|
175
|
+
## Code Review Agents
|
|
176
|
+
|
|
177
|
+
- We will look at three vendors
|
|
178
|
+
- Pricing comparison
|
|
179
|
+
- Live demo of CodeRabbit
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Good slide — what the learner walks away with:**
|
|
183
|
+
|
|
184
|
+
```markdown
|
|
185
|
+
<!-- .slide: data-layout="content" -->
|
|
186
|
+
## Code review agents — when each wins
|
|
187
|
+
|
|
188
|
+
- CodeRabbit: best for large PRs, weakest at C# specifics
|
|
189
|
+
- CodeAnt: tightest C# rules, no PR summary
|
|
190
|
+
- Custom (Claude Code in CI): controllable, you pay per token
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Bad notes — prose, no source link:**
|
|
194
|
+
|
|
195
|
+
```markdown
|
|
196
|
+
Note: We should talk about EchoLeak here. It was a vulnerability in Microsoft 365 Copilot discovered in 2025 that allowed prompt injection through email content. The attack worked by sending a crafted email that the Copilot assistant would later read and execute instructions from. This is a good example of why we need to think about prompt injection in agentic systems.
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Good notes — link first, bullets, anticipated questions:**
|
|
200
|
+
|
|
201
|
+
```markdown
|
|
202
|
+
Note:
|
|
203
|
+
- [EchoLeak (CVE-2025-32711) — Microsoft writeup](https://msrc.microsoft.com/...)
|
|
204
|
+
- Zero-click prompt injection in M365 Copilot via crafted email
|
|
205
|
+
- Attack surface: any agent that reads attacker-controlled content
|
|
206
|
+
- Mitigations
|
|
207
|
+
- Tool allowlists, not denylists
|
|
208
|
+
- Human-in-loop on data exfil tools
|
|
209
|
+
- "Could this happen with Claude Code?" — yes, via any tool that reads external text (web fetch, MCP server, log file)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### See also (type-specific rules)
|
|
213
|
+
|
|
214
|
+
- **Workshops, training, hands-on sessions:** [`WORKSHOP.md`](WORKSHOP.md)
|
|
215
|
+
|
|
216
|
+
(Future: `PITCH.md`, `LECTURE.md`, `INTERNAL_REVIEW.md` — coming soon.)
|
|
217
|
+
|
|
119
218
|
## Content conventions
|
|
120
219
|
|
|
121
220
|
```markdown
|
|
@@ -188,6 +287,26 @@ Composable on any `<img>` or wrapper element:
|
|
|
188
287
|
| `image-duotone` | Brand-tinted monochrome via filter chain |
|
|
189
288
|
| `image-overlay` | Adds a brand-gradient scrim from transparent → primary at 75% bottom |
|
|
190
289
|
|
|
290
|
+
## Text-alignment utilities
|
|
291
|
+
|
|
292
|
+
Composable on any block element. Use these instead of inline `style="text-align: …"` — Reveal's print stylesheet forces `text-align: left !important` on every `div`/`p`/`ol`/`ul`, which silently kills inline alignment styles in print (the on-screen view looks fine, the printed PDF lands left-aligned).
|
|
293
|
+
|
|
294
|
+
| Class | Effect |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `text-center` | Center inline content (typical use: wrapping a centered `<img>` or caption) |
|
|
297
|
+
| `text-right` | Right-align inline content |
|
|
298
|
+
| `text-left` | Left-align inline content (rarely needed; default) |
|
|
299
|
+
|
|
300
|
+
Example — centering an image on a `content` slide:
|
|
301
|
+
|
|
302
|
+
```markdown
|
|
303
|
+
<div class="text-center">
|
|
304
|
+
|
|
305
|
+

|
|
306
|
+
|
|
307
|
+
</div>
|
|
308
|
+
```
|
|
309
|
+
|
|
191
310
|
## Design tokens
|
|
192
311
|
|
|
193
312
|
The skill ships a token system in `theme-base/base.css`. Templates only declare two anchors (`--brand-primary`, `--brand-accent`); the rest derives via `color-mix(in oklch, …)`.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Workshop-specific content rules
|
|
2
|
+
|
|
3
|
+
Read [`SKILL.md`](SKILL.md) first — the general content principles apply unchanged. This file only adds the rules that are specific to workshops, training sessions, and hands-on formats.
|
|
4
|
+
|
|
5
|
+
A workshop is not a talk. The deck is scaffolding for the room's work, not the work itself. If the slides could be read alone and convey the value, it isn't a workshop — it's a recorded lecture.
|
|
6
|
+
|
|
7
|
+
## Rules
|
|
8
|
+
|
|
9
|
+
- **Block arc: opener → core → demo/exercise → synthesis.** Every block follows this shape.
|
|
10
|
+
- **Opener** — one slide that earns the block: a stat, a story, a failure case. "Why should you care for the next 45 minutes?"
|
|
11
|
+
- **Core** — the actual material. The teaching slides.
|
|
12
|
+
- **Demo or exercise** — the participants do something or watch something done. Live, not recorded.
|
|
13
|
+
- **Synthesis** — one slide that closes the loop: "what you now know" or "what you can do tomorrow."
|
|
14
|
+
- Skip a beat and the block falls flat. Especially the opener.
|
|
15
|
+
- **Topic shape inside a block — preferred, not required: what → proof → mechanics → advanced → exercise.** A useful default ordering for depth topics. Skip any beat that doesn't fit the topic. Reorder if a different sequence teaches better. Variability is expected; this is the shape to fall back to when nothing better suggests itself.
|
|
16
|
+
- **What** — what it is + landscape framing (who built it, who supports it, who doesn't), if it matters.
|
|
17
|
+
- **Proof** — only if you have a *genuinely* interesting stat, eval, or incident. See the big-stat rule below.
|
|
18
|
+
- **Mechanics** — how it actually works: scopes, locations, semantics, limits.
|
|
19
|
+
- **Advanced** — patterns, extensions, teaser hooks for later blocks/days.
|
|
20
|
+
- **Exercise** — bullets are *principles* (learner takeaway), notes carry facilitation.
|
|
21
|
+
- **Big-stat is for genuinely interesting numbers, not a checkbox.** Use the `big-stat` layout when you have a stat that *changes the room's mind* — a real eval result, a striking incident metric, a non-obvious cost number. Don't manufacture one. A topic without a strong stat skips the proof slide; a generic stat dilutes every later one. Format when used: h1 = the number with `<em class="unit">`, h2 = the comparison-with-context (named alternatives, named source).
|
|
22
|
+
- **~50% slides / ~50% hands-on.** If deck-time exceeds half the block's runtime, cut slides — not exercise time.
|
|
23
|
+
- **Recall check at every block boundary.** A question slide between blocks: "Before we move on — what would you do if X?" One question, no answer, deliberate silence. Forces consolidation.
|
|
24
|
+
- **Question slides as discussion prompts.** Bold question, no answer, no bullets. Use sparingly (2–3 per day max) — overuse breaks the room's trust that you have answers.
|
|
25
|
+
- **Energy curve matters.**
|
|
26
|
+
- Hardest cognitive lift in the morning, before lunch.
|
|
27
|
+
- Live coding right after lunch is malpractice (post-meal energy crash).
|
|
28
|
+
- Demos and discussion in the afternoon.
|
|
29
|
+
- End each day on synthesis or a payoff demo, never on dense content.
|
|
30
|
+
- **Buffer / reserve slides per block.** Every block has 1–2 slides marked optional (in the deck, not separate) used only if pace allows. Lets you stretch comfortably without scrambling. Marker convention: filename suffix `-reserve` (e.g., `045-reserve-extra-eval-example.md`) and a `<!-- reserve -->` comment at the top of the slide so the doctor can flag if any are still flagged at build time.
|
|
31
|
+
- **Exercise slides follow a strict template.**
|
|
32
|
+
- **Title prefix `Exercise — `** so the room sees the mode change.
|
|
33
|
+
- **Bullets = principles, not procedure.** What the learner *takes away* by doing the exercise. Procedure ("5 minutes solo, then we compare two") goes in notes.
|
|
34
|
+
- **Standard note beats, in order:** `Facilitation`, `Common output`, `Common mistakes`, `Anticipated questions`. Use these names verbatim — the speaker scans by them.
|
|
35
|
+
- **Keep principles transferable.** A bullet that only makes sense in the context of this specific repo isn't a principle; demote it to a note example.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
**Bad opener — describes the agenda instead of earning the block:**
|
|
40
|
+
|
|
41
|
+
```markdown
|
|
42
|
+
<!-- .slide: data-layout="content" -->
|
|
43
|
+
## Security block
|
|
44
|
+
|
|
45
|
+
- Prompt injection
|
|
46
|
+
- Tool aliasing
|
|
47
|
+
- Credential exposure
|
|
48
|
+
- Hook-based guardrails
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Good opener — a real incident earns the next 90 minutes:**
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
<!-- .slide: data-layout="big-stat" -->
|
|
55
|
+
# 9 seconds
|
|
56
|
+
## Replit production database, deleted by an agent
|
|
57
|
+
|
|
58
|
+
Note:
|
|
59
|
+
- [Replit prod-DB deletion — postmortem](https://...)
|
|
60
|
+
- Agent had unscoped DB credentials in its environment
|
|
61
|
+
- "Drop the dev tables" → matched against prod by mistake
|
|
62
|
+
- This is the failure mode the next 90 minutes prevent
|
|
63
|
+
- "Could Claude Code do this?" — yes, if you give it `.env` with prod creds
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Bad recall slide — gives the answer:**
|
|
67
|
+
|
|
68
|
+
```markdown
|
|
69
|
+
<!-- .slide: data-layout="content" -->
|
|
70
|
+
## Recap
|
|
71
|
+
|
|
72
|
+
- Tier 1 = enterprise sanctioned
|
|
73
|
+
- Tier 2 = paid + settings off
|
|
74
|
+
- Tier 3 = prohibited
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Good recall slide — forces the room to retrieve:**
|
|
78
|
+
|
|
79
|
+
```markdown
|
|
80
|
+
<!-- .slide: data-layout="quote" -->
|
|
81
|
+
> Your colleague asks if she can paste a 20-line C# snippet
|
|
82
|
+
> from our payment service into ChatGPT Plus. What do you say?
|
|
83
|
+
|
|
84
|
+
Note:
|
|
85
|
+
- Let the room answer first, ~30 seconds of silence is fine
|
|
86
|
+
- Correct: depends on classification — if Confidential, no; if Internal, sanitize and yes (settings off)
|
|
87
|
+
- Don't give the answer until at least one person tries
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Bad exercise slide — procedure on the slide, no transferable principle:**
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
<!-- .slide: data-layout="content" -->
|
|
94
|
+
## Write a CLAUDE.md
|
|
95
|
+
|
|
96
|
+
- Open your favorite repo
|
|
97
|
+
- Spend 5 minutes writing CLAUDE.md
|
|
98
|
+
- We'll pick 2 to share
|
|
99
|
+
- Discuss what worked
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Good exercise slide — principles on the slide, procedure in notes:**
|
|
103
|
+
|
|
104
|
+
```markdown
|
|
105
|
+
<!-- .slide: data-layout="content" -->
|
|
106
|
+
## Exercise — write your CLAUDE.md
|
|
107
|
+
|
|
108
|
+
- Capture conventions, not procedures (procedures belong in skills)
|
|
109
|
+
- One fact per line, no prose paragraphs
|
|
110
|
+
- Glossary entries earn their place — only project-specific terms
|
|
111
|
+
- Order matters — most-violated rules first, model reads top-down
|
|
112
|
+
- Commit it; use `.local.md` only for personal overrides
|
|
113
|
+
|
|
114
|
+
Note:
|
|
115
|
+
- Facilitation
|
|
116
|
+
- 5 min solo on a repo they know
|
|
117
|
+
- 2 volunteers show on screen
|
|
118
|
+
- group critiques against the failure modes from prior slides
|
|
119
|
+
- Common output
|
|
120
|
+
- "Use Polly for retries, not custom code"
|
|
121
|
+
- "Tests live in `tests/Unit/`, not `src/__tests__/`"
|
|
122
|
+
- Common mistakes
|
|
123
|
+
- writing instructions that should be skills
|
|
124
|
+
- copying a generic style guide instead of capturing real violations
|
|
125
|
+
- Anticipated questions
|
|
126
|
+
- "Should this be in AGENTS.md or CLAUDE.md?" — symlink and stop choosing
|
|
127
|
+
- "What if there's already one?" — diff against it; usually adds 30%
|
|
128
|
+
```
|
|
@@ -244,6 +244,14 @@
|
|
|
244
244
|
.reveal tr:nth-child(even) td { background: var(--brand-surface); }
|
|
245
245
|
.reveal tr:last-child td { border-bottom: 0; }
|
|
246
246
|
|
|
247
|
+
/* ── Text alignment utilities — composable on any block element.
|
|
248
|
+
* `!important` is needed because Reveal's print stylesheet forces
|
|
249
|
+
* `text-align: left !important` on every div/p/ol/ul, which would otherwise
|
|
250
|
+
* override author intent (inline styles or any non-`!important` rule). */
|
|
251
|
+
.reveal .text-center { text-align: center !important; }
|
|
252
|
+
.reveal .text-right { text-align: right !important; }
|
|
253
|
+
.reveal .text-left { text-align: left !important; }
|
|
254
|
+
|
|
247
255
|
/* ── Image utilities (composable on any <img> or wrapper) */
|
|
248
256
|
.reveal .image-rounded img,
|
|
249
257
|
.reveal img.image-rounded { border-radius: var(--radius-md); }
|
|
@@ -323,3 +331,108 @@
|
|
|
323
331
|
color: var(--brand-accent);
|
|
324
332
|
height: 2px;
|
|
325
333
|
}
|
|
334
|
+
|
|
335
|
+
/* ── Print
|
|
336
|
+
* Reveal's vendored print stylesheet (`@media print { html:not(.print-pdf) … }`)
|
|
337
|
+
* forces every heading to `color:#000!important` and `p/li/td` to `#000`. That
|
|
338
|
+
* inverts the white-on-brand text we use on `section` and `closing` dividers
|
|
339
|
+
* (whose gradient background is on the section element itself, so it still
|
|
340
|
+
* renders blue) — producing black-on-blue when the user hits Cmd+P without
|
|
341
|
+
* `?print-pdf`. Restore brand text colors here, and force color-adjust:exact
|
|
342
|
+
* so the gradient prints reliably. */
|
|
343
|
+
@media print {
|
|
344
|
+
.reveal section[data-layout="section"],
|
|
345
|
+
.reveal section[data-layout="closing"] {
|
|
346
|
+
-webkit-print-color-adjust: exact !important;
|
|
347
|
+
print-color-adjust: exact !important;
|
|
348
|
+
}
|
|
349
|
+
.reveal section[data-layout="section"] h1,
|
|
350
|
+
.reveal section[data-layout="section"] h4,
|
|
351
|
+
.reveal section[data-layout="closing"] h1 {
|
|
352
|
+
color: #fff !important;
|
|
353
|
+
}
|
|
354
|
+
.reveal section[data-layout="section"] h2,
|
|
355
|
+
.reveal section[data-layout="section"] h3 {
|
|
356
|
+
color: rgba(255, 255, 255, 0.7) !important;
|
|
357
|
+
}
|
|
358
|
+
.reveal section[data-layout="section"] p,
|
|
359
|
+
.reveal section[data-layout="section"] li,
|
|
360
|
+
.reveal section[data-layout="closing"] h2,
|
|
361
|
+
.reveal section[data-layout="closing"] p,
|
|
362
|
+
.reveal section[data-layout="closing"] li {
|
|
363
|
+
color: rgba(255, 255, 255, 0.85) !important;
|
|
364
|
+
}
|
|
365
|
+
.reveal section[data-layout="title"] h1 {
|
|
366
|
+
color: var(--brand-primary) !important;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* ── Cmd+P scope (browser print on the regular URL, NOT ?print-pdf)
|
|
370
|
+
* Reveal forces every section to `display:block; position:static; padding:60px 20px;
|
|
371
|
+
* transform:none`, which strips the flex centering from title/section/closing/quote/
|
|
372
|
+
* big-stat/pull-quote and leaves their content top-aligned. Restore centering, then
|
|
373
|
+
* frame every printed page with a hairline so it reads like a printed sheet. */
|
|
374
|
+
|
|
375
|
+
/* Hairline frame — inset so it lives inside the printable area and survives
|
|
376
|
+
* the printer's unprintable-margin clipping. */
|
|
377
|
+
html:not(.print-pdf) .reveal .slides section {
|
|
378
|
+
box-shadow: inset 0 0 0 1px var(--neutral-300) !important;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* Make every section fill the printable page. Two effects, no layout
|
|
382
|
+
* coercion:
|
|
383
|
+
* 1. The inset frame above wraps the full page edge instead of
|
|
384
|
+
* shrink-wrapping content.
|
|
385
|
+
* 2. The flex-centered layouts below have a tall container to center
|
|
386
|
+
* within (without a tall container, `justify-content: center` has
|
|
387
|
+
* nothing to center against). */
|
|
388
|
+
html:not(.print-pdf) .reveal .slides section {
|
|
389
|
+
min-height: 100vh !important;
|
|
390
|
+
box-sizing: border-box !important;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Apply `--table-scale` to td/th in print. Reveal's print stylesheet forces
|
|
394
|
+
* `font-size: 20pt !important` on all td, which would otherwise override
|
|
395
|
+
* the screen rule. Scale font AND padding together — scaling font alone
|
|
396
|
+
* leaves the row height dominated by static cell padding, dampening the
|
|
397
|
+
* shrink effect. */
|
|
398
|
+
html:not(.print-pdf) .reveal section[data-layout="table"] td,
|
|
399
|
+
html:not(.print-pdf) .reveal section[data-layout="table"] th {
|
|
400
|
+
font-size: calc(20pt * var(--table-scale, 1)) !important;
|
|
401
|
+
padding: calc(0.5rem * var(--table-scale, 1)) calc(1rem * var(--table-scale, 1)) !important;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Reveal's print stylesheet forces every section to `display: block`,
|
|
405
|
+
* stripping the on-screen flex centering from the layouts that use it.
|
|
406
|
+
* Restore `display: flex` for those layouts ONLY — `flex-direction`,
|
|
407
|
+
* `justify-content`, and `align-items` come from layouts.css unchanged.
|
|
408
|
+
* Layouts not listed here stay as `display: block` and sit at the top of
|
|
409
|
+
* the page; that preserves the natural flow of layouts that depend on it
|
|
410
|
+
* (image-text and two-column rely on inline-block siblings, content/agenda/
|
|
411
|
+
* comparison/metric-grid/code/table all flow as block stacks). */
|
|
412
|
+
html:not(.print-pdf) .reveal .slides section[data-layout="title"],
|
|
413
|
+
html:not(.print-pdf) .reveal .slides section[data-layout="section"],
|
|
414
|
+
html:not(.print-pdf) .reveal .slides section[data-layout="closing"],
|
|
415
|
+
html:not(.print-pdf) .reveal .slides section[data-layout="quote"],
|
|
416
|
+
html:not(.print-pdf) .reveal .slides section[data-layout="big-stat"],
|
|
417
|
+
html:not(.print-pdf) .reveal .slides section[data-layout="pull-quote"] {
|
|
418
|
+
display: flex !important;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* ── Print-view polish (`?print-pdf` URL — Reveal stacks each slide as a `.pdf-page`)
|
|
423
|
+
* Reveal sizes `.pdf-page` to slide dims and stacks them flush-left with no
|
|
424
|
+
* separation. Center each one and draw a hairline frame so the print preview
|
|
425
|
+
* reads like a sheaf of printed pages, not a left-aligned wall of slides.
|
|
426
|
+
*
|
|
427
|
+
* Why box-shadow inset and not `border`: Reveal forces `@page { margin: 0 }`,
|
|
428
|
+
* so an outside border would land at the edge of paper and be clipped by the
|
|
429
|
+
* printer's unprintable margin. An inset shadow draws the line *inside* the
|
|
430
|
+
* page box and survives the print. */
|
|
431
|
+
html.print-pdf .reveal .slides .pdf-page {
|
|
432
|
+
box-shadow: inset 0 0 0 1px var(--neutral-300);
|
|
433
|
+
}
|
|
434
|
+
@media screen {
|
|
435
|
+
html.print-pdf .reveal .slides .pdf-page {
|
|
436
|
+
margin: 16px auto !important;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -213,9 +213,13 @@
|
|
|
213
213
|
letter-spacing: var(--tracking-wide);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/* ── 9. table ── styling already in base; just ensure breathing room
|
|
216
|
+
/* ── 9. table ── styling already in base; just ensure breathing room.
|
|
217
|
+
* `--table-scale` is set per-slide by build.ts (`injectTableScale`) when row
|
|
218
|
+
* count exceeds 6 — multiplies the cell font-size to keep dense tables on a
|
|
219
|
+
* single page. Defaults to 1 (no change) when the var is unset. */
|
|
217
220
|
.reveal section[data-layout="table"] table {
|
|
218
221
|
margin-top: var(--space-3);
|
|
222
|
+
font-size: calc(var(--text-sm) * var(--table-scale, 1));
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
/* ── 10. comparison ── 2-3 option boxes with numbered badge + top stripe */
|
|
@@ -265,7 +269,12 @@
|
|
|
265
269
|
max-height: 65vh;
|
|
266
270
|
margin: var(--space-2) 0;
|
|
267
271
|
}
|
|
268
|
-
|
|
272
|
+
/* Code font size scales down for long blocks. The build script sets
|
|
273
|
+
--code-scale on each code-layout section based on line count: 1.0 for ≤15
|
|
274
|
+
lines, 0.6 for ≥25 lines, linear in between. Defaults to 1.0 if unset. */
|
|
275
|
+
.reveal section[data-layout="code"] pre code {
|
|
276
|
+
font-size: calc(0.9em * var(--code-scale, 1));
|
|
277
|
+
}
|
|
269
278
|
|
|
270
279
|
/* ── 12. big-stat ── One giant number + a muted caption beneath. Use sparingly.
|
|
271
280
|
* Author surface:
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// <out>/<deck-name>/<deck-name>.md concatenated slides (written first)
|
|
9
9
|
// <out>/<deck-name>/<deck-name>.html self-contained presentation
|
|
10
10
|
//
|
|
11
|
-
// --out defaults to
|
|
11
|
+
// --out defaults to the deck-dir itself (output lands at <deck-dir>/<deck-name>/,
|
|
12
|
+
// which the scaffolder gitignores). The deck-name subdir is always created
|
|
12
13
|
// inside --out, even when --out is explicitly provided. Existing files in
|
|
13
14
|
// the subdir are preserved unless --force is passed.
|
|
14
15
|
|
|
@@ -19,6 +20,71 @@ import { dataUri, escapeForTextarea, readText } from "./lib/inline";
|
|
|
19
20
|
import { THEME_BASE, VENDOR_REVEAL } from "./lib/paths";
|
|
20
21
|
import { getTemplate } from "./lib/registry";
|
|
21
22
|
|
|
23
|
+
// Walk markdown line-by-line, skipping fenced code blocks, applying `transform`
|
|
24
|
+
// to each non-fenced line. Used by both the concat-step path rewrite and the
|
|
25
|
+
// HTML-step image inliner so neither touches example image syntax inside ``` blocks.
|
|
26
|
+
async function mapMarkdownOutsideFences(
|
|
27
|
+
md: string,
|
|
28
|
+
transform: (line: string) => string | Promise<string>
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
const out: string[] = [];
|
|
31
|
+
let inFence = false;
|
|
32
|
+
for (const line of md.split("\n")) {
|
|
33
|
+
if (/^```/.test(line)) {
|
|
34
|
+
inFence = !inFence;
|
|
35
|
+
out.push(line);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
out.push(inFence ? line : await transform(line));
|
|
39
|
+
}
|
|
40
|
+
return out.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Rewrite `../assets/X` (the natural relative path from a `slides/*.md` file)
|
|
44
|
+
// to `assets/X` so the concatenated markdown — which lives at the deck root —
|
|
45
|
+
// resolves images correctly when previewed directly. Bare `X.png` references
|
|
46
|
+
// (no path) are left to the doctor to flag; we don't guess where they live.
|
|
47
|
+
function rewriteImageRefsForConcat(md: string): Promise<string> {
|
|
48
|
+
return mapMarkdownOutsideFences(md, (line) =>
|
|
49
|
+
line.replace(/(!\[[^\]]*\]\()([^)]+)(\))/g, (whole, open, ref, close) => {
|
|
50
|
+
const trimmed = ref.trim();
|
|
51
|
+
if (/^(https?:|data:)/i.test(trimmed)) return whole;
|
|
52
|
+
if (trimmed.startsWith("../assets/")) {
|
|
53
|
+
return `${open}${trimmed.slice(3)}${close}`;
|
|
54
|
+
}
|
|
55
|
+
return whole;
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Inline every local image reference in the concatenated markdown as a data: URI
|
|
61
|
+
// so the resulting HTML is truly self-contained (emailable, USB-stickable).
|
|
62
|
+
// Resolves refs against `deckDir` (the concat-md's location). Missing files are
|
|
63
|
+
// left untouched — the doctor flags them; build doesn't crash on author errors.
|
|
64
|
+
async function inlineImagesInMarkdown(md: string, deckDir: string): Promise<string> {
|
|
65
|
+
return mapMarkdownOutsideFences(md, async (line) => {
|
|
66
|
+
const matches = [...line.matchAll(/(!\[[^\]]*\]\()([^)]+)(\))/g)];
|
|
67
|
+
if (matches.length === 0) return line;
|
|
68
|
+
let result = line;
|
|
69
|
+
for (const m of matches) {
|
|
70
|
+
const [whole, open, ref, close] = m;
|
|
71
|
+
const trimmed = ref.trim();
|
|
72
|
+
if (/^(https?:|data:)/i.test(trimmed)) continue;
|
|
73
|
+
const abs = resolve(deckDir, trimmed);
|
|
74
|
+
try {
|
|
75
|
+
await access(abs, fsConst.F_OK);
|
|
76
|
+
} catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// dataUri returns `url("data:...")` for CSS use; strip the `url("…")` wrapper for <img>.
|
|
80
|
+
const wrapped = await dataUri(abs);
|
|
81
|
+
const inner = wrapped.replace(/^url\("/, "").replace(/"\)$/, "");
|
|
82
|
+
result = result.replace(whole, `${open}${inner}${close}`);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
22
88
|
const ASPECTS: Record<string, [number, number]> = {
|
|
23
89
|
"16:9": [1920, 1080],
|
|
24
90
|
"16:10": [1920, 1200],
|
|
@@ -65,6 +131,62 @@ function deckSlug(deckDir: string): string {
|
|
|
65
131
|
return slug || "deck";
|
|
66
132
|
}
|
|
67
133
|
|
|
134
|
+
// Build-time injection: code-layout slides with > 15 lines of code get a
|
|
135
|
+
// `style="--code-scale: X"` attribute baked into the slide directive. CSS in
|
|
136
|
+
// layouts.css multiplies the base code font by this variable. Linear from 1.0
|
|
137
|
+
// at 15 lines to 0.6 at 25 lines; clamped at 0.6 beyond. Build-time keeps the
|
|
138
|
+
// attribute on the rendered <section>, so navigation/Highlight re-runs cannot
|
|
139
|
+
// strip it.
|
|
140
|
+
function injectCodeScale(slideMarkdown: string): string {
|
|
141
|
+
const layoutRe = /<!--\s*\.slide:\s*data-layout="code"([^>]*)-->/i;
|
|
142
|
+
const layoutMatch = layoutRe.exec(slideMarkdown);
|
|
143
|
+
if (!layoutMatch) return slideMarkdown;
|
|
144
|
+
|
|
145
|
+
const codeRe = /```[^\n]*\n([\s\S]*?)\n```/;
|
|
146
|
+
const codeMatch = codeRe.exec(slideMarkdown);
|
|
147
|
+
if (!codeMatch) return slideMarkdown;
|
|
148
|
+
|
|
149
|
+
const lines = codeMatch[1].split("\n").length;
|
|
150
|
+
if (lines <= 15) return slideMarkdown;
|
|
151
|
+
|
|
152
|
+
const scale = Math.max(0.6, 1 - (lines - 15) * 0.04).toFixed(2);
|
|
153
|
+
// Don't double-inject if a previous build already set it.
|
|
154
|
+
const extras = layoutMatch[1].replace(/\s+style="--code-scale:\s*[^"]+"/i, "").trim();
|
|
155
|
+
const attrs = extras
|
|
156
|
+
? `${extras} style="--code-scale: ${scale}"`
|
|
157
|
+
: `style="--code-scale: ${scale}"`;
|
|
158
|
+
const newDirective = `<!-- .slide: data-layout="code" ${attrs} -->`;
|
|
159
|
+
return slideMarkdown.replace(layoutMatch[0], newDirective);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Build-time injection: table-layout slides with > 4 rows get a
|
|
163
|
+
// `style="--table-scale: X"` attribute baked into the slide directive. CSS
|
|
164
|
+
// multiplies cell font-size AND cell padding by this var so both shrink
|
|
165
|
+
// together (font-only shrinking is dampened by static padding). Linear from
|
|
166
|
+
// 1.0 at 4 rows to 0.6 at ≥10 rows. Mirrors `injectCodeScale`.
|
|
167
|
+
function injectTableScale(slideMarkdown: string): string {
|
|
168
|
+
const layoutRe = /<!--\s*\.slide:\s*data-layout="table"([^>]*)-->/i;
|
|
169
|
+
const layoutMatch = layoutRe.exec(slideMarkdown);
|
|
170
|
+
if (!layoutMatch) return slideMarkdown;
|
|
171
|
+
|
|
172
|
+
// Count markdown table rows (lines starting with `|`) excluding the
|
|
173
|
+
// separator (`| --- | --- |`) which doesn't render as a row.
|
|
174
|
+
const sepRe = /^\s*\|(\s*:?-+:?\s*\|)+\s*$/;
|
|
175
|
+
let rows = 0;
|
|
176
|
+
for (const line of slideMarkdown.split("\n")) {
|
|
177
|
+
if (/^\s*\|/.test(line) && !sepRe.test(line)) rows++;
|
|
178
|
+
}
|
|
179
|
+
if (rows <= 4) return slideMarkdown;
|
|
180
|
+
|
|
181
|
+
const scale = Math.max(0.6, 1 - (rows - 4) * 0.067).toFixed(2);
|
|
182
|
+
const extras = layoutMatch[1].replace(/\s+style="--table-scale:\s*[^"]+"/i, "").trim();
|
|
183
|
+
const attrs = extras
|
|
184
|
+
? `${extras} style="--table-scale: ${scale}"`
|
|
185
|
+
: `style="--table-scale: ${scale}"`;
|
|
186
|
+
const newDirective = `<!-- .slide: data-layout="table" ${attrs} -->`;
|
|
187
|
+
return slideMarkdown.replace(layoutMatch[0], newDirective);
|
|
188
|
+
}
|
|
189
|
+
|
|
68
190
|
async function buildConcat(deckDir: string): Promise<string> {
|
|
69
191
|
const slidesDir = join(deckDir, "slides");
|
|
70
192
|
if (await exists(slidesDir)) {
|
|
@@ -73,7 +195,8 @@ async function buildConcat(deckDir: string): Promise<string> {
|
|
|
73
195
|
throw new Error(`slides/ is empty at ${slidesDir}`);
|
|
74
196
|
}
|
|
75
197
|
const parts = await Promise.all(files.map((f) => readText(join(slidesDir, f))));
|
|
76
|
-
|
|
198
|
+
const joined = `${parts.map((p) => injectTableScale(injectCodeScale(p.trim()))).join("\n\n---\n\n")}\n`;
|
|
199
|
+
return rewriteImageRefsForConcat(joined);
|
|
77
200
|
}
|
|
78
201
|
const legacy = join(deckDir, "content.md");
|
|
79
202
|
if (await exists(legacy)) {
|
|
@@ -90,7 +213,7 @@ async function main() {
|
|
|
90
213
|
}
|
|
91
214
|
const deckDir = resolve(argv[0]);
|
|
92
215
|
|
|
93
|
-
let outRoot =
|
|
216
|
+
let outRoot = deckDir;
|
|
94
217
|
let force = false;
|
|
95
218
|
for (let i = 1; i < argv.length; i++) {
|
|
96
219
|
if (argv[i] === "--out") outRoot = resolve(argv[++i]);
|
|
@@ -110,7 +233,10 @@ async function main() {
|
|
|
110
233
|
}
|
|
111
234
|
|
|
112
235
|
const slug = deckSlug(deckDir);
|
|
113
|
-
|
|
236
|
+
// When --out is the deck-dir itself (the default), write directly into it —
|
|
237
|
+
// no extra <slug>/ subdir. Otherwise create the subdir so multiple decks
|
|
238
|
+
// can coexist under one shared --out.
|
|
239
|
+
const outDir = resolve(outRoot) === deckDir ? deckDir : join(outRoot, slug);
|
|
114
240
|
const concatPath = join(outDir, `${slug}.md`);
|
|
115
241
|
const htmlPath = join(outDir, `${slug}.html`);
|
|
116
242
|
|
|
@@ -145,7 +271,7 @@ async function main() {
|
|
|
145
271
|
const skeleton = await readText(join(THEME_BASE, "skeleton.html"));
|
|
146
272
|
const revealCss = await readText(join(VENDOR_REVEAL, "reveal.css"));
|
|
147
273
|
const highlightCss = await readText(
|
|
148
|
-
join(VENDOR_REVEAL, "plugin", "highlight", "
|
|
274
|
+
join(VENDOR_REVEAL, "plugin", "highlight", "github-dark.css")
|
|
149
275
|
);
|
|
150
276
|
const revealJs = await readText(join(VENDOR_REVEAL, "reveal.js"));
|
|
151
277
|
const markdownJs = await readText(
|
|
@@ -156,7 +282,11 @@ async function main() {
|
|
|
156
282
|
);
|
|
157
283
|
const notesJs = await readText(join(VENDOR_REVEAL, "plugin", "notes", "notes.js"));
|
|
158
284
|
|
|
159
|
-
|
|
285
|
+
// Read the concat back from disk, then inline image refs as data: URIs so
|
|
286
|
+
// the HTML is self-contained. The on-disk concat keeps plain `assets/X` paths
|
|
287
|
+
// for direct markdown preview; only the HTML embeds full image bytes.
|
|
288
|
+
const contentMdRaw = await readFile(concatPath, "utf8");
|
|
289
|
+
const contentMd = await inlineImagesInMarkdown(contentMdRaw, deckDir);
|
|
160
290
|
|
|
161
291
|
let deckOverridesCss = "";
|
|
162
292
|
const overridesPath = join(deckDir, "overrides.css");
|