45 Commits

Author SHA1 Message Date
88lin fd16290679 chore: remove tracked .DS_Store from templates (#10) 2026-09-14 15:36:58 +08:00
Hao d4635309f5 fix(fx-runtime): section-level data-fx never initialized (#26)
initFxIn/stopFxIn use root.querySelectorAll('[data-fx]'), which never
matches the root element itself — so data-fx placed directly on a
<section class="slide"> (the documented way to give a cover slide a
canvas background) was silently ignored: no canvas, no handle in
__hpxActive.

Add fxEls() that includes the root when it carries data-fx, and use it
in both init and stop so lifecycle stays symmetric.

Verified: a cover slide with data-fx="starfield" now mounts its canvas
on activation and stops on leave; descendant [data-fx] behavior unchanged.

Co-authored-by: Ynchnnn <17665010888@163.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-14 15:36:53 +08:00
luuuyi 86e47d79ca fix(contrast): text on an --accent fill was a hardcoded ink, unreadable in some themes (#43)
templates/single-page/cta.html hardcoded `color:#0b1024` on top of
`background:var(--accent)`. Under minimal-white the accent is #111216, so the
button's label rendered near-black on near-black: measured 1.01:1. The button
still looked like a button — correct shape, correct shadow, no text.

No literal ink can work here: accents across the 36 themes run from #ffffff
(blueprint) to #000000 (sharp-mono). So the ink becomes a token.

  assets/base.css            :root default --accent-ink
  assets/themes/*.css        all 36, each set to whichever of white /
                             near-black clears WCAG AA against its own accent
  templates/full-decks/*/    the 6 decks that redefine --accent get their own

Five more sites had the same defect and are switched over with cta.html —
they hardcoded `color:#fff`, which fails in the opposite direction on the
light-accent themes:

  single-page/process-steps.html   .step .num
  single-page/mindmap.html         .mm .root
  single-page/roadmap.html         .col.now .tag
  full-decks/course-module         .mcq.correct .letter
  full-decks/xhs-post              .num-circle
  themes/news-broadcast.css        .kicker (same value, keeps the invariant)

Verified two independent ways.

Computed from source, every file that defines --accent must pair it with an
--accent-ink clearing 4.5:1 — 43 files (36 themes, 6 deck stylesheets,
base.css), 0 failures, tightest y2k-chrome at 4.54:1.

Measured in headless Chrome, reading computed color against computed
background rather than the stylesheet: 292 rendered elements, 0 failures.
That is 8 per theme across the four single-page layouts (2 .cta .btn,
4 .step .num, 1 .mm .root, 1 .rm .col.now .tag) x 36 themes, plus 4 in the two
decks that carry their own accent.

Three of the six components are normal text and must clear 4.5:1 rather than
the 3:1 large-text threshold — the 11px/400 roadmap tag, the 20px/600 mindmap
root (20px counts as large only at weight 700+), and the 13px/700 course-module
answer letter. The floor across all 292, at either threshold, is 4.54:1.

The :root fallback is #000000, not #ffffff. Against the default --accent
#3b6cff, white measures 4.40:1 — below AA for normal text, and tighter than
any of the 36 themes — while black measures 4.77:1. That fallback is what a
deck linking no theme gets, which is 8 of the 15 bundled full-decks; none of
them currently paints text on an accent fill, so this changes no rendered
pixel in the repo, but it makes the documented pattern safe to follow.
--accent itself is unchanged.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:47 +08:00
luuuyi cfad0054da fix(xhs-post): 3:4 card rendered at 42% width because the canvas size was CSS-only (#40)
The deck is 810x1080, but that size lived only in style.css. runtime.js reads
the canvas from `data-w`/`data-h` on `.deck` and fell back to 1920x1080, so the
fit scale was min(vw/1920, vh/1080) — correct for a 16:9 deck, far too small
for a 3:4 one.

Measured rendered width of .deck (getBoundingClientRect):

  window       before          after
  810x1080     342px  42.2%    745px  91.9%   (fills 100% of the height)
  1080x1440    456px  42.2%   1015px  94.0%
  1920x1080    745px  38.8%    745px  38.8%   (unchanged: 3:4 in 16:9 is height-bound)

Declare the size once as `--deck-w`/`--deck-h` custom properties, which
base.css already consumes, and mirror it on `data-w`/`data-h` for the runtime.
A JS-less deck still gets an 810x1080 canvas, as before.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:41 +08:00
luuuyi be4eefe4a5 fix(print): a template's scoped CSS silently kept its screen layout in print (#39)
`@media print .slide{position:relative;…;page-break-after:always;height:100vh}`
is specificity (0,1,0). Every full-deck template scopes its rules as
`.tpl-name .slide` — (0,2,0) — so it won. Only `opacity` and `transform` on
that rule carried !important, so the three declarations that decide pagination
lost: the defence was per-property, not per-rule.

Measured, Chrome --print-to-pdf, pages counted from /Type /Page objects
(identical with and without --print-to-pdf-no-margins, so margins were never
the cause):

  deck                  slides   before   after
  dir-key-nav-minimal        8        1       8    .tpl-… .slide{position:absolute;inset:0}
  xhs-post                   9        2       9    …{position:absolute;inset:0;height:1080px}
  presenter-mode-reveal      6        7       6    overflow on the last slide
  other 12 decks + deck.html          ok      ok

Every layout declaration in the print reset is now !important. The one pair
left unflagged is `page-break-after`/`break-after`, so that
`.slide:last-child` can still opt out of the trailing blank page — no template
or theme stylesheet in the repo declares either property, so nothing competes
for them. Also adds `inset:auto` (a template's `inset:0` outlived
`position:relative`) and caps `.slide` at `max-height:100vh`.

Verified: all 15 bundled decks plus templates/deck.html produce exactly one
PDF page per slide, at default margins and at zero margins.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:35 +08:00
luuuyi b77ba9ea82 fix(notes): .speaker-notes was hidden from the presenter and shown to the room (#38)
runtime.js read speaker notes from `.notes, aside.notes, .speaker-notes` in
three places (preview-mode hiding, the N drawer, the presenter popup), but
base.css only hid `.notes`. Computed `display` for a `.speaker-notes` block:

  audience view (what the room sees)   block
  ?preview=1   (what the author sees)  none

So the defect is invisible in the only view the author looks at while writing.

- base.css hides `.notes, .speaker-notes`
- the three selector lists in runtime.js collapse into one `NOTE_SEL`
  constant, so the runtime's idea of "a note" can no longer drift from the
  stylesheet's; both sides carry a comment pointing at the other

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:30 +08:00
luuuyi 9516a811c6 fix(render): silent no-op writes, and all undercounting slides by 20% (#37)
Two independent defects, both of which hand the user a wrong artefact while
reporting success.

1. `render.sh deck.html 3 out/png` wrote nothing and still printed three ✔.
   `mkdir -p "$OUT"` only ran in the branch that *derives* an output directory,
   so an explicitly-passed directory was never created. Chrome exits 0 when it
   cannot write the screenshot, and `render_one()`'s ✔ was unconditional — the
   run ended with "done: rendered 3 slide(s)" and zero files on disk.

2. `render.sh deck.html all` under-rendered 13 of the 15 bundled decks.
   `grep -c 'class="slide"'` is an exact-string match, but real slides carry
   extra classes (`slide is-active`, `slide dark`, `slide t-violet`). Across
   templates/full-decks that counted 95 of 119 slides; dir-key-nav-minimal
   counted 0, fell through to the `COUNT=1` fallback, and exported a single PNG
   for an 8-slide deck without a warning.

Changes:
- create the output directory on every path that has one (accepting either a
  directory, as documented, or an explicit *.png file path)
- verify each screenshot exists and is non-empty before printing ✔; otherwise
  print ✗ to stderr and exit non-zero
- count slides by matching `slide` as a whole class token on `<section>`,
  which yields 119/119 and does not match `class="slide-number"`
- warn on stderr when the fallback count of 1 is used
- single-slide renders with an out-dir now write <out-dir>/<stem>.png instead
  of treating the directory as the file name
- reject a non-numeric slide count instead of passing it to seq

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:23 +08:00
luuuyi a7d960e25e fix(new-deck): the path self-check skipped one reference per deck — its own style.css (#41)
The check enumerated references by matching `assets/` in the path, so it only
ever saw the references that already looked like asset paths. Across the 15
bundled decks it verified 53 of 68 local references. The 15 it missed were one
`<link rel="stylesheet" href="style.css">` per deck — the reference that breaks
when a deck moves, which is the bug this check exists to catch (#19).

Enumerate by *reference*, not by path shape: every local href / src / url(),
minus absolute URLs, protocol-relative, data: and mailto:, then drop the empty
strings that a fragment-only reference leaves behind.

Blank lines are dropped in their own sed stage rather than as a `^$` branch of
the protocol grep's alternation. Under ugrep — which ships as `grep` on some
machines — `^$` inside an ERE alternation does not reliably match empty lines;
measured on a 5-empty-line input, 4 survived. A surviving empty string resolves
to the deck directory itself, which exists, so it was silently counted as a
verified reference: `url(#arrow)` in knowledge-arch-blueprint and `url(#arrow-r)`
in product-launch each inflated that deck's count by one. Protocol filtering was
unaffected.

Verified: scaffolding all 15 full-deck templates reports 68 references verified
(was 53), a default scaffold (no -t) still reports 5, and a deck whose style.css
link is renamed to a non-existent file is rejected with exit 1 — the old check
passed it.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:18 +08:00
luuuyi 1e6df68248 docs: correct full-deck count (14→15) and the image-layout table (#42)
Two inconsistencies, both of which sent a reader to a file that does not do
what the docs say:

1. Twelve occurrences across four files still said 14 full-deck templates,
   while SKILL.md:29/:255, README.md:4 and the disk all said 15 —
   SKILL.md:88/:280/:281, README.md:149/:224/:302/:318/:319,
   montage-templates.html (title, headline, subtitle) and
   full-decks-index.html's own cover headline.

2. SKILL.md's "pick a layout by how many images you have" table introduced
   five layouts above it and then listed six rows. The sixth, image-grid.html,
   contains no <img> at all — it is a gradient-placeholder wall. A reader
   following the table would drop real photos into a layout with nowhere to
   put them. Removed that row and named both placeholder layouts
   (image-grid, image-hero) explicitly for what they are.

Verified on disk: 15 full-decks, 36 single-page layouts, 36 themes, and
image-grid.html / image-hero.html contain zero <img> elements while the five
layouts left in the table contain 1, 1, 1, 6 and 2 respectively.
README.zh-CN.md was already correct on both counts.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 15:36:12 +08:00
luuuyi 3654d17666 Merge pull request #35 from lewislulu/feat/custom-logo
feat: custom deck logo via data-logo (closes #11)
2026-09-13 21:00:56 +08:00
lewis c2824b17dc fix: print the deck logo on every page, not just one
The .deck-logo element is position:absolute inside .deck. @media print sets
.deck{transform:none;height:auto} and stacks the slides, which removes the
transform-created containing block and leaves the element anchoring ONCE
against the whole stacked document. Measured on a 3-slide deck rendered with
--print-to-pdf: bottom-right printed only on the last page, and top-right
(the default) printed on no page at all.

Hide the element in print and paint the logo per slide instead — in print one
.slide is exactly one page. runtime.js mirrors the image URL and the corner
onto .deck as --logo-print / data-logo-print. The URL must be absolute
(el.src, not getAttribute('src')): a relative url() inside a custom property
resolves against the stylesheet that *uses* the var(), i.e. assets/base.css,
not against the deck.

Verified by rasterising the PDF and counting logo pixels per page: all four
corners now print on every page at the correct corner, and data-no-logo slides
are skipped in print as they are on screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:00:10 +08:00
lewis 9341f613fb feat: custom deck logo via data-logo (closes #11)
Presentations often need a company or product logo on every slide. Until now
that meant pasting an <img> into each .slide by hand — tedious, and easy to get
out of sync when slides are reordered or copied between decks.

Declare it once instead:

    <body data-logo="logo.svg"
          data-logo-position="bottom-right"
          data-logo-size="40px">

- position: top-left / top-right / bottom-left / bottom-right (default top-right)
- size sets the height, width follows the aspect ratio
- data-logo-opacity / data-logo-alt for the rest
- per-slide opt-out with <section class="slide" data-no-logo> (covers usually
  carry their own branding)
- --logo-inset-x / --logo-inset-y on .deck-logo to fine-tune the margin

Two paths into the same CSS: runtime.js creates the element from the attribute,
or the author writes <img class="deck-logo" data-pos="…"> inside .deck by hand.
The hand-written path needs no JS at all, since the styling lives in base.css.

The element sits inside .deck, so it scales with the deck. The logo is
initialised before the preview-mode branch on purpose — the presenter's
pixel-perfect preview is the audience view and must carry it too — and it is
deliberately NOT in the @media print hide list: a logo is the one piece of
chrome you do want on an exported PDF.

Verified with headless Chrome at 1920x1080, measuring rendered pixels:
- all four positions land in their own corner, zero red pixels in the other
  three; data-logo-size="60px" renders exactly 60px tall
- data-no-logo slide: 0 logo pixels; neighbouring slides unaffected
- ?preview=N renders pixel-identical to the audience view (4766 px both)
- hand-authored <img class="deck-logo"> with no runtime.js: renders correctly
- deck with no data-logo: zero change
2026-09-13 20:56:00 +08:00
luuuyi ad71e47f9f Merge pull request #36 from lewislulu/feat/image-layouts
feat: five real-image layouts + .img-frame primitive (closes #17)
2026-09-13 20:50:20 +08:00
luuuyi 1429db0afd Merge pull request #34 from lewislulu/feat/touch-navigation
feat: swipe left/right to change slides on touch devices (#15)
2026-09-13 20:50:17 +08:00
lewis d9c09e6208 feat: five real-image layouts + .img-frame primitive (closes #17)
Before this, `grep -l '<img' templates/single-page/*.html` returned nothing:
image-hero and image-grid were gradient placeholders, so the skill had no
actual answer to "which layout shows 1 or more images".

Adds one framing primitive in base.css and five layouts built on it:

  image-single       one screenshot/diagram, letterboxed, never cropped
  image-full-bleed   one photo filling the slide, scrim keeps the title legible
  image-text-split   50/50 image + argument, `flip` swaps sides
  image-gallery      3-6 images, uniform grid, one caption each
  image-compare      before/after, both sides identical size

.img-frame owns the aspect ratio (--img-ratio) and the crop; the <img> just
fills it. That is what lets a user drop in a portrait, square or ultrawide
source without touching the layout. `.contain` letterboxes instead of cropping
for screenshots and diagrams, where a crop loses information.

Each layout takes its height from what is left after the title (flex:1;
min-height:0) rather than from a fixed aspect ratio, so it survives viewports
shorter than 16:9 instead of pushing captions off the page.

Demo artwork lives in assets/demo-images/ as hand-written SVG (~1 KB each,
~5 KB total, zero external references) so the layouts keep the offline
guarantee.

Verified with a DOM probe at window heights 1167/1080/900/733 (viewports
1080/993/813/646), measured after entry animations settle: all five report
broken=0 overflow=NONE at every height.
2026-09-13 19:36:13 +08:00
lewis acbf0413ca feat: swipe left/right to change slides on touch devices (#15)
Phones and tablets have no arrow keys, so the deck was unnavigable on
them. Swipe left for the next slide, right for the previous one.

A gesture only counts as a swipe if it is single-finger, at least 50px,
more horizontal than vertical by 1.5x, and completed within 800ms.
Everything else falls through untouched — taps, vertical scrolling,
pinch-zoom (a second finger abandons the gesture), and anything inside
the overview grid, the notes drawer, or an author's own links, buttons
and form controls. `[data-no-swipe]` opts any element out.

All three listeners are passive and never call preventDefault, so the
browser stays free to scroll and zoom while we are still deciding.

No capability sniffing: `ontouchstart in window` and maxTouchPoints both
misreport on touchscreen laptops and some tablets, and a guard there
would silently remove the feature on exactly the devices that need it.
Touch listeners cost nothing on a device that never fires them.

Verified in headless Chrome with synthetic touch sequences dispatched on
the deck element: left advances, right goes back, and each of
too-short / mostly-vertical / too-slow / two-finger / started-on-overview
is correctly ignored. Identical results with and without Chrome's touch
emulation.
2026-09-13 19:36:10 +08:00
luuuyi 10ec28490b Merge pull request #31 from lewislulu/fix/asset-paths
fix: compute asset paths from the deck's real location (#19)
2026-09-13 19:35:46 +08:00
luuuyi 9a129a300c Merge pull request #30 from lewislulu/fix/deck-scaling-consistency
fix: render the deck on a fixed design canvas so all viewports match (#20)
2026-09-13 19:35:43 +08:00
luuuyi c4d9e93c28 Merge pull request #33 from lewislulu/docs/offline-install
docs: document offline / manual install (#16)
2026-09-13 19:35:39 +08:00
luuuyi 1a4fa148e7 Merge pull request #32 from lewislulu/fix/presenter-cards
fix: presenter cards no longer depend on the init reaching its last line (#14)
2026-09-13 19:35:36 +08:00
lewis 05d2df47d6 docs: document offline / manual install (#16)
The READMEs only offered `npx skills add <url>`, which needs network on
the target machine. Document the three alternatives, in decreasing order
of network dependence:

1. `npx skills add ./html-ppt-skill` — point the CLI at a local copy
   moved over by git/zip/USB.
2. Copy the folder into the agent's skills directory by hand — no Node,
   no CLI. A skill is just a folder with SKILL.md at its root. Table of
   project and global paths for Claude Code, Codex, Cursor, OpenCode,
   Gemini CLI and Windsurf.
3. Skip the agent entirely — the templates are plain static files.

Also answers the question behind the question: whether it runs without a
network. It does. The only remote dependency is assets/fonts.css, which
@imports Google Fonts; offline those imports fail and the browser falls
back to the system stack already declared in assets/base.css.

Verified rather than assumed: a copy containing only SKILL.md, assets/,
templates/, references/ and scripts/, with assets/fonts.css emptied,
scaffolds and renders correctly in headless Chrome — same layout, system
typefaces.

English in README.md, Chinese in README.zh-CN.md, short pointer in
SKILL.md's Install section.
2026-09-13 18:58:13 +08:00
lewis bdcae0d25d fix: presenter cards no longer depend on the init reaching its last line (#14)
Reported as "S opens the presenter but there are no cards, only the hint
bar at the bottom". Reproduced: the four cards were positioned ONLY from
JavaScript. `.pcard` is `position:absolute` with no left/top/width/height
in CSS, and `applyLayout(readLayout())` was the LAST statement of the
presenter init — after every listener registration. So the cards' entire
visibility rode on that one line being reached, while `.hint-bar` is
`position:fixed` in CSS and always renders. Anything that stopped the
script produced precisely the reported screenshot.

At least three independent things do stop it, so this fixes the class
rather than one trigger:

1. **CSS default geometry.** `#card-cur/nxt/notes/timer` now carry the
   same 2x2 arrangement in percentages. The presenter is laid out with
   zero JavaScript; applyLayout() overrides with px when it runs.
2. **Layout is applied first,** right after the element lookups, instead
   of after ~40 lines of wiring. Function declarations hoist.
3. **Saved layouts are sanitised.** readLayout() trusted localStorage
   verbatim. A layout saved while presenting on a 4K display and
   replayed on a laptop puts every card off-screen — and it is sticky,
   so reopening never recovers. Coordinates are now clamped into the
   viewport and malformed entries fall back to the default.
4. **`</script>` in speaker notes can no longer kill the init.** The
   slide metadata is interpolated into an inline <script>; "<" is now
   escaped to < in every embedded JSON value.
5. The remaining init is wrapped in try/catch that reports to the
   console instead of blanking the cards.
6. defaultLayout() floors the window size, so a popup reporting 0x0
   (opened minimised or backgrounded) no longer yields negative widths.

Verified by rendering the generated presenter document in headless
Chrome. Stale 4K layout replayed at 1280x820: before = empty except the
hint bar (matches the report), after = four cards clamped into view.
Script removed entirely: before = same empty state, after = full 2x2
layout. Literal </script> in notes: before = dead init, after = presenter
works and the notes render. Happy path unchanged at 1280x820 and 1024x700.
2026-09-13 18:54:39 +08:00
lewis ffc4ef1c39 fix: compute asset paths from the deck's real location (#19)
new-deck.sh hardcoded a single depth assumption. It forced the output
under the skill root (`$HERE/$PARENT/$NAME`) and rewrote the template's
`../assets/` to `../../assets/` unconditionally, which is only correct
when the deck lands exactly two levels down. `new-deck.sh my-talk .`
put index.html one level down and still emitted `../../assets/` — the
extra level in the report.

- Output dir: an absolute parent is honoured as-is, a relative one
  resolves against the caller's cwd. Only the default stays
  `<skill>/examples`.
- Asset prefix is computed: relpath() from the deck directory back to
  the skill root, so any depth works, inside the tree or outside it.
- The rewrite now collapses any run of `../` before `assets/`, so it is
  template-agnostic — deck.html (`../`), single-page (`../../`) and
  full-decks (`../../../`) all land correctly.
- `-t/--template` accepts a full-deck name, a single-page layout name or
  a path, so full-deck templates can be scaffolded instead of copied by
  hand at the wrong depth (SKILL.md step 5 used to instruct exactly that).
- Self-check: every emitted assets/ reference must resolve before the
  script reports success. It joins paths lexically, the way a browser
  resolves them against a file:// URL — the shell's own `..` follows
  symlinks (/tmp -> /private/tmp) and disagrees.

Docs: SKILL.md, references/authoring-guide.md and both READMEs no longer
teach a fixed `../` depth; they state that each template family's prefix
is relative to its own location and that the scaffold computes it.

Verified at depths 1/2/3, outside the skill tree, and for the base,
full-deck and single-page templates. An out-of-tree deck renders
pixel-identical (RMSE 0) to the in-tree one in headless Chrome.
2026-09-13 18:50:04 +08:00
lewis 5acdad9ff2 fix: render the deck on a fixed design canvas so all viewports match (#20)
The deck box was `width:100vw; height:100vh` while every type size,
gap and offset inside it is authored in absolute px against a
1920x1080 design. So the box reflowed with the window but its contents
did not: a 1512x850 browser window, the presenter preview iframe and
the headless 1920x1080 PNG each produced a different layout.

Render onto a fixed design canvas instead and scale it to fit:

- `.deck` is now `var(--deck-w, 1920px)` x `var(--deck-h, 1080px)` with
  `transform: scale(var(--deck-scale, 1))`; `body` centres it and
  letterboxes the remainder.
- `initCanvasFit()` reads `data-w` / `data-h` off the deck, computes
  `min(innerWidth/w, innerHeight/h)` and re-fits on resize,
  visualViewport resize and `document.fonts.ready`.
- `body[data-fit="fluid"]` opts a deck out and restores the old
  100vw/100vh behaviour for decks authored fluid.
- `@media print` drops the transform so PDF export is unaffected.

Verified numerically: RMSE between a 1920x1080 render and a 1512x850
render of examples/demo-deck went from 14.29% to 1.98% (the remainder
is resampling noise). 5:4 (1280x1024) and mobile (390x844) letterbox
with no clipping.
2026-09-13 18:45:31 +08:00
lewis f3a8435d39 Merge pull request #2 from g199209/feat-overview-thumbnails
feat(runtime): render actual slide thumbnails in overview

Resolved conflict: kept PR's enhanced toggleOverview alongside main's presenter mode.
2026-04-26 15:13:33 +08:00
lewis 15fb85f05b docs: add presenter mode section + Chinese README
README.md:
- Update subtitle to mention 15 full-decks (was 14) and presenter mode
- Add 中文文档 link pointing to README.zh-CN.md
- New '🎤 Presenter Mode (new!)' section near top with the 4-card screenshot
  explaining magnetic cards, pixel-perfect iframe previews, no-flicker
  postMessage navigation, and the 3 golden rules of 逐字稿
- Skills table now includes presenter mode as first row
- Full-decks list updated from 14 to 15 with presenter-mode-reveal entry
- Keyboard cheat sheet: S=presenter window, N=notes drawer, R=timer reset,
  ?preview=N URL param

README.zh-CN.md (new):
- Full Chinese translation parallel to README.md
- Emphasizes the presenter mode launch as the headline feature
- All 36 themes / 15 decks / 47 animations / 31 layouts described
- Same structure and visual assets as the English version

docs/readme/presenter-mode.png:
- 2536×1614 screenshot of the actual presenter window showing all 4 cards
  (CURRENT/NEXT/SPEAKER SCRIPT/TIMER) in action with a real slide loaded
2026-04-18 02:31:21 +08:00
lewis 36ecd2cf30 fix(presenter): sync theme across audience + presenter iframes (T key)
Problem: pressing T in audience window only swapped the host page's
theme CSS link. Preview iframes in the presenter window stayed on the
theme that was active when the popup was opened (or the HTML default).

Fix: 3-hop theme propagation via message types
1. audience cycleTheme() → BroadcastChannel 'theme' message
2. presenter window receives BC msg → postMessage 'preview-theme'
   to both iframes
3. iframe preview mode listens for 'preview-theme' → swaps its own
   <link id='theme-link'> href

Also:
- Audience window listens for 'theme' BC events → applies theme
  (so pressing T in one window cycles theme in BOTH)
- Presenter window captures audience's current theme at open time
  (data-theme attr) and forwards it to each iframe on 'preview-ready'
  so previews match audience from frame 1, even if audience had
  already cycled theme before opening presenter
- preview mode auto-detects theme base path from existing
  theme-link href (same logic as audience cycleTheme)

Verified in headless Chrome: BC msg {type:theme, name:dracula} →
both audience and preview iframes show data-theme=dracula with
matching colors.
2026-04-17 23:34:09 +08:00
lewis b64ce0f832 perf(presenter): smooth navigation via postMessage (no reload, no flicker)
Problem: Switching slides reset iframe.src each time, causing visible
flicker (white flash → load → render) on every navigation.

Solution: load each preview iframe ONCE, then use postMessage for all
subsequent slide changes. The iframe stays mounted; only .is-active
class toggles between slides inside it.

Changes:
- runtime.js preview mode: now exposes a showSlide() function and
  listens for window.message events of type 'preview-goto'. Also
  posts 'preview-ready' to parent on init so the presenter window
  knows when each iframe is loaded.
- presenter window: tracks iframeReady state per iframe; first frame
  renders via iframe.src = ?preview=N (one-time), all subsequent
  navigation goes through postPreviewGoto() → postMessage. Notes,
  meta, and timer update directly without touching iframes.
- Init no longer calls update(idx) which used to reset iframe.src;
  instead inits notes/meta/count once and lets the load→ready flow
  populate previews.

Docs synced to match new architecture:
- SKILL.md: describes 4 magnetic cards, draggable/resizable, with
  the iframe ?preview=N pattern explained for AI consumers
- references/presenter-mode.md: updated ASCII diagram to show 4
  cards, removed old 58%/35% layout description, added explanation
  for why previews are pixel-perfect (iframe loads same HTML) and
  why navigation is flicker-free (postMessage, not reload)
- presenter-mode-reveal README: updated 4-card description
2026-04-17 23:27:05 +08:00
lewis 832f5be212 feat(presenter): magnetic-card UI with pixel-perfect iframe previews
Complete rewrite of presenter mode. Two-part solution:

1. Single-slide preview mode in runtime.js
   - Detect ?preview=N query param on page load
   - When present: show only slide N, hide all chrome (progress-bar,
     notes-overlay, overview), skip key handlers, skip hash/broadcast
   - This lets the presenter window load the deck file in iframes with
     specific slides — CSS/theme/colors 100% identical to audience view

2. Magnetic-card presenter window
   - 4 independent cards (CURRENT / NEXT / SCRIPT / TIMER)
   - Each card is position:absolute, draggable by header, resizable by
     bottom-right corner handle
   - Cards have colored indicator dots matching their role
   - Dragging shows blue outline; resizing shows green
   - Layout persists to localStorage per-deck, with 'Reset layout' button
   - Default layout: CURRENT big top-left, NEXT top-right, SCRIPT
     bottom-right, TIMER bottom-left

Previews use iframe.src = deckUrl + '?preview=N':
  - iframe loads THE SAME HTML file as the audience
  - runtime.js detects ?preview=N and renders only that slide
  - CSS transform:scale() fits 1920x1080 into card body, centered
  - Zero style injection / zero HTML escaping — can't break

Sync: BroadcastChannel keeps audience ↔ presenter in lockstep.

Verified with headless Chrome: tokyo-night theme, gradient headlines,
speaker avatars, agenda rows — all render identically between windows.
2026-04-17 23:21:45 +08:00
lewis 0fc41be493 feat(presenter): preserve slide layout + draggable splitters
Two user-reported issues fixed:

1. Slide layout broken in preview (content stuck to top, not centered)
   Root cause: iframe CSS was forcing .slide { display:block !important }
   which overrode base.css's .slide { display:flex; justify-content:center }
   Fix: Do NOT override .slide or .deck styling in iframe. Let host CSS
   (base.css + theme + scoped) handle flex centering, padding, etc.
   Only override .is-active visibility and hide notes/chrome.

2. Users can now resize preview regions
   - Horizontal splitter between current (left) and right column
   - Vertical splitter between next preview (top) and notes (bottom)
   - Splitters show blue hover state, drag updates flex sizes
   - reScale() called on every drag frame to keep iframe 1:1 accurate
   - Min sizes enforced (200px width, 80-100px height) to prevent collapse

Verified with headless Chrome: cover slide now correctly vertically
centered in the preview, matching audience view pixel-perfect.
2026-04-17 23:05:48 +08:00
lewis 647a908eab fix(presenter): use contentDocument.write() instead of srcdoc
Previous iframe srcdoc approach completely broke because .replace(/"/g, '&quot;')
mangled the stylesheet <link href="..."> into <link href=&quot;...&quot;>,
causing browser to treat the entire &quot;file://...&quot; as the attribute
value literal. All CSS failed to load → blank/unstyled preview.

New approach:
- iframe.contentDocument.write() takes raw HTML string, NO escaping needed
- All quotes (double and single) pass through untouched
- stylesheet href attributes remain valid
- body class and html attrs pass through cleanly
- Added initWhenReady() polling to ensure iframe contentDocument is available
  before first render (avoids race condition where document.write fires
  before iframe is fully initialized)

Verified with headless Chrome render — current/next slides now show correct
colors, fonts, layout matching audience view pixel-for-pixel.
2026-04-17 22:57:23 +08:00
lewis 06d6283ff7 fix(presenter): use iframe srcdoc for pixel-perfect slide preview
Root cause: CSS viewport units (vw/vh), clamp(), and percentage-based
layouts resolve against the actual viewport, not a fixed-size div.
DOM clones in a 1920x1080 container still render differently from the
audience view because the iframe-less context has the wrong viewport.

Fix: replace DOM clone approach with <iframe srcdoc='...'> for both
current and next slide previews. Each iframe:
- Is sized 1920x1080 (matching the design resolution)
- Creates its own viewport context (100vw=1920px, 100vh=1080px)
- Loads the full page CSS (absolute URLs) + body classes + html attrs
- Wraps the single slide in a .deck container
- CSS transform:scale() shrinks it to fit the preview area
- Result: pixel-perfect match with audience view

Also: srcdoc template carries the host page's body classes (e.g.
.tpl-presenter-mode-reveal) and html attributes (data-themes, lang)
so scoped CSS and theme variables work correctly inside the iframe.
2026-04-17 22:50:34 +08:00
lewis 0ce226f87c fix(presenter): theme path + popup slide rendering fidelity
3 bugs fixed:

1. T key theme path error (ERR_FILE_NOT_FOUND)
   - cycleTheme now auto-detects theme base path from existing
     <link id='theme-link'> href instead of hardcoding 'assets/themes/'
   - Works correctly from any subdirectory depth (e.g. templates/full-decks/xxx/)

2. Presenter popup colors wrong (gray/missing styles)
   - Popup <body> now inherits host page's body classes (e.g. .tpl-presenter-mode-reveal)
     so scoped CSS selectors like .tpl-xxx .slide work correctly
   - Popup <html> inherits all host attributes (lang, data-themes, data-theme, etc.)
   - Added .pv-body class with !important overrides to ensure presenter layout
     wins over any host CSS that might affect body

3. Presenter popup slide layout mismatch
   - Slides are now wrapped in a proper <div class='deck'> container at
     exactly 1920×1080px, matching the audience window's DOM structure
   - .pv-stage-inner .deck .slide forced to 1920×1080 with !important
   - Removed all manual style.xxx = overrides in renderSlide(); layout comes
     purely from CSS now, matching the audience view pixel-for-pixel
   - Hidden runtime chrome (.progress-bar, .notes-overlay, .overview, etc.)
     inside preview clones
2026-04-17 22:42:54 +08:00
lewis 23d9f5d369 fix(presenter): rewrite as popup window with CSS scale() rendering
BREAKING: S key no longer overlays on the same page. It now opens a
separate browser window (window.open) for the presenter.

Audience window (original page):
- Stays completely untouched — normal slide view
- S key opens the presenter popup
- ← → / T / F / O all work as before
- Navigation syncs to presenter via BroadcastChannel

Presenter window (popup):
- Current slide rendered at 1920×1080 then CSS transform:scale() to fit
- Next slide preview also at 1920×1080 scale() — layout never breaks
- Large-font speaker script (18px, scrollable)
- Elapsed timer + page counter + current slide title
- ← → navigates (syncs back to audience window)
- R resets timer, Esc closes popup

Technical:
- runtime.js builds presenter HTML dynamically (buildPresenterHTML)
- Collects all slide outerHTML + notes + all stylesheets from host page
- Injects into popup via document.write()
- BroadcastChannel keyed by pathname for multi-deck isolation
- base.css: removed all old inline .pv-* / .presenter-view styles
  (presenter styles now self-contained in popup HTML)

Docs updated:
- SKILL.md: describes popup behavior, separate keyboard sections
- references/presenter-mode.md: new dual-window diagram, updated flow
- presenter-mode-reveal/README.md: updated S key and dual-screen guide
2026-04-17 22:36:04 +08:00
lewis 43c4a74f63 feat(presenter): add true presenter view (S) with speaker script + timer
Runtime (assets/runtime.js):
- S key now opens a split-view presenter mode: current slide (left 55%) +
  next-slide preview + large-font speaker script (150-300 words, scrollable)
  + elapsed timer + page counter
- Legacy bottom notes drawer moved to N key (keeps backward compat)
- R key resets timer (only active inside presenter view)
- Esc closes all overlays (overview / notes / presenter)
- Auto-picks up <aside class="notes">, <div class="notes">, or .speaker-notes

Styles (assets/base.css):
- New .presenter-view grid layout with pv-main / pv-side / pv-notes / pv-bar
- body.presenter-mode hides deck chrome from the operator's eye
- Timer styled with JetBrains Mono green (#3fb950)

New template (templates/full-decks/presenter-mode-reveal/):
- 6-slide demo deck designed around the S key presenter flow
- 5 presets via T key: tokyo-night, dracula, catppuccin-mocha, nord, corporate-clean
- Every slide ships a complete 150-300 word <aside class="notes"> example
  written in the "3 rules of speaker script" style
- Scoped .tpl-presenter-mode-reveal CSS with rule-row, feature-row, code-block
- README explains the full authoring flow

Docs:
- New references/presenter-mode.md — when to use, 3 rules of \u9010\u5b57\u7a3f writing
  (\u63d0\u793a\u4fe1\u53f7 / 150-300 \u5b57 / \u53e3\u8bed), required HTML structure, common mistakes,
  standard AI prompt for generating speaker scripts
- SKILL.md: upgraded template count 14 -> 15, added Presenter Mode section
  in 'When to use' with trigger keywords
- full-decks.md: row 15 added with \ud83c\udfa4 marker
- full-decks-index.html gallery now lists presenter-mode-reveal
2026-04-17 22:19:53 +08:00
lewis c52acbff47 fix: resolve DANGEROUS scan warnings on install
- HTML-encode destructive command text in testing-safety-alert template
  to avoid false positive CRITICAL destructive scan hits
- Add .clawscan-allow allowlist for path traversal false positives
  (templates correctly reference root assets via ../../../)

Closes #3
2026-04-17 21:47:11 +08:00
Gao Mingfei 3398633d26 feat(runtime): render actual slide thumbnails in overview
- Replace text-only title blocks with scaled-down clones of the actual slides in the overview grid (O key).
- Force a strict 16:9 aspect ratio (1920x1080 base) for the mini-slides to prevent layout breakage when the browser window is resized to non-standard proportions.
- Add a gradient overlay to ensure slide numbers and titles remain legible on top of the thumbnail content.

Made-with: Cursor
2026-04-16 13:39:01 +08:00
lewis 376dfe5e77 fix: hide .notes from slides + add explicit no-presenter-text rule
- base.css: add .notes{display:none!important} so speaker notes are never
  visible on the rendered slide (only in S overlay)
- SKILL.md: add NEVER-put-presenter-text rule in authoring rules
- authoring-guide.md: add same rule in What to NOT do section

Fixes issue where AI would put small descriptive/presenter-only text
directly on slide markup instead of inside <div class='notes'>.
2026-04-16 11:27:32 +08:00
lewis 9f99b12b12 docs(readme): add hero GIF (cover with live previews) + layouts cycling GIF
- hero.gif (~2.4 MB): intro-deck cover page captured live showing the
  3 preview strips all running (full decks / canvas FX / cycling layouts)
- layouts-live.gif (~570 KB): the 31-layout auto-rotator cycling through
  kpi-grid -> chart-line -> timeline in real templates/single-page files
- README: hero GIF moved to the very top as the opening visual,
  layouts-live.gif inserted below the 31-layout section next to layouts.png
2026-04-15 22:36:33 +08:00
lewis 0824d3648b docs(readme): embed themes / layouts / animations / templates showcase screenshots
- add docs/readme/montage-{themes,layouts,animations,templates}.html
- pre-render each montage to a 1920x1080 PNG via headless Chrome
- embed all 4 hero screenshots into README.md
- themes.png shows 8 live theme previews
- layouts.png shows 8 real single-page layouts
- animations.png shows 8 canvas FX tiles with running particles
- templates.png shows 6 full-deck iframes
2026-04-15 22:32:28 +08:00
lewis 9bbca1697e docs: audit README + SKILL — accurate counts, install command, pre-author checklist
- README fully rewritten: correct 36 themes / 31 layouts / 27 CSS + 20 FX anims / 14 full decks
- Add one-line install: npx skills add https://github.com/lewislulu/html-ppt-skill
- List all 36 theme names, 14 full-deck names, and the animation catalogs
- SKILL.md: fix 30 → 31 layouts, 25 → 27 CSS animations
- SKILL.md: add 'Before you author anything' section — always ask user about
  content/style/starting-point, or recommend themes based on audience
- SKILL.md: fix duplicate '5.' in Quick start numbering
2026-04-15 17:08:58 +08:00
lewis 3f6558d11e chore: drop fix-brief 2026-04-15 16:38:23 +08:00
lewis db0f0cee33 fix: v2 — iframe theme isolation, 8 full-deck templates from source decks, 20 FX animations (particles/graph/fireworks), +12 themes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 16:29:08 +08:00
lewis a93138e1bf feat: html-ppt AgentSkill — 24 themes, 31 layouts, 20+ animations
World-class HTML presentation skill with keyboard navigation, theme/animation
switching, headless Chrome rendering, and showcase decks for themes/layouts/animations.

- 24 themes (minimal-white, dracula, catppuccin, tokyo-night, xiaohongshu-white, …)
- 31 single-page layouts (cover, code, charts, timeline, gantt, mindmap, …)
- 20+ named animations (rise-in, typewriter, confetti-burst, card-flip-3d, …)
- theme/layout/animation showcase decks
- runtime.js: arrow/space/F/S/O/T/A keybinds, hash routing, progress bar
- render.sh headless Chrome PNG export
- MIT, author lewis <sudolewis@gmail.com>
2026-04-15 15:36:16 +08:00
lewis 5554a11c85 feat: html-ppt AgentSkill — HTML PPT Studio
Token-driven static HTML presentation system. One skill directory under
html-ppt/ implementing the full AgentSkills spec.

- 24 themes (assets/themes/*.css): minimal-white, editorial-serif,
  soft-pastel, sharp-mono, arctic-cool, sunset-warm, catppuccin-latte/mocha,
  dracula, tokyo-night, nord, solarized-light, gruvbox-dark, rose-pine,
  neo-brutalism, glassmorphism, bauhaus, swiss-grid, terminal-green,
  xiaohongshu-white, rainbow-gradient, aurora, blueprint, memphis-pop.
  Each overrides only CSS custom properties consumed by base.css.
- 31 single-page layout templates (templates/single-page/) with realistic
  demo data: cover, toc, section-divider, bullets, two/three-column,
  big-quote, stat-highlight, kpi-grid, table, code (highlight.js), diff,
  terminal, flow/arch/mindmap diagrams, timeline, roadmap, gantt,
  comparison, pros-cons, checklist, image-hero/grid, chart-bar/line/pie/
  radar (chart.js), process-steps, cta, thanks.
- 27 named animations (assets/animations/animations.css) applied via
  class="anim-<name>" or data-anim="<name>"; runtime re-triggers on slide
  enter. Respects prefers-reduced-motion.
- Runtime (assets/runtime.js): ←/→/Space/PgUp/PgDn/Home/End nav, F
  fullscreen, S speaker notes overlay, O overview grid, T cycle themes,
  A cycle animations, #/N deep-link, progress bar, counter-up tick-up.
- Showcase decks: theme-showcase.html (24), animation-showcase.html (27),
  layout-showcase.html (iframe tour of 30 layouts), deck.html starter.
- Scripts: render.sh (headless Chrome at /Applications/Google Chrome.app,
  1920×1080, multi-slide via #/N) and new-deck.sh (scaffold from deck.html).
- References: themes.md, layouts.md, animations.md, authoring-guide.md.
- examples/demo-deck/ — complete 8-slide deck using aurora theme + chart.js.
- SKILL.md with trigger keywords (presentation, PPT, slides, keynote, deck,
  幻灯片, 演讲稿, reveal, 小红书图文) and authoring rules.
- MIT LICENSE, README.md.

Author: lewis <sudolewis@gmail.com>
2026-04-15 15:35:03 +08:00