Presentation runtime: lifecycle states, :thisSlide, history #1

Closed
navi wants to merge 0 commits from navi/psslides:slide-runtime into main
First-time contributor

What this PR does

Adds a real presentation runtime to p/slides: the preview now displays the deck, navigation is resilient, and everything is authored in HTML + Less only — no JS to write.

Authoring

  • Styles live in <style> tags inside the slide HTML (one editor buffer, no Styles tab). Every <style> in the deck — global slide first, then each slide — contributes to one project stylesheet.
  • <style> tags are the project Less, so variables, nesting, mixins and imports all work. Raw Less never reaches the DOM: the tags are stripped from the markup and the compiled CSS is injected by the runtime.
  • :thisSlide compiles to the selector of the slide whose stylesheet is being compiled ([data-ps-slide="slide-N"]), so deck styles stay scoped without inventing extra CSS syntax.

The global slide is a template: <?= html ?> / <?= css ?>

The runtime injects into the global slide:

<style>
@frame: #161a22;
.frame { background: @frame; padding: 14px; }
</style>
<div class="frame">
    <style><?= css ?></style>
    <?= html ?>
</div>
  • The global slide is rendered once as the scene; slide markup replaces <?= html ?>, the compiled stylesheet replaces <?= css ?> (wherever <style><?= css ?></style> sits).
  • Without <?= html ?> the global slide keeps its old meaning: markup shared by every slide.
  • Without <?= css ?> the stylesheet falls back to a single data-ps-slides sheet in <head> — never both, so nothing fights over the rules.

Runtime

  • All slides stay mounted; lifecycle is published as data-ps-state (idle → entering → current → leaving). CSS owns the animation, JS owns the lifecycle.
  • Interruptible navigation with a generation counter: rapid arrow presses never queue up, stale settles are discarded.
  • runtime.goTo(index) / next() / previous(); back/forward and reload restore the slide index (state key psslides-slide, merged into history.state).
  • Preview and Present use the same runtime, no iframe.
  • Compiler (compile.ts) and runtime (runtime.ts) are separate modules; placeholders are shared via a tiny placeholders.ts so the runtime never pulls in the Less compiler.

Notes / out of scope

  • Single-file standalone HTML export would need a bundler plugin.
  • Slide persistence across a hard reload (the app has no document persistence yet — pre-existing).

Verification

prettier --check, tsc --noEmit, oxlint, npm run build all green. Browser-tested: template shell render, <?= css ?> injection (frame background compiled from the Less var), slide element identity across recompiles and template switches, head-fallback when there is no <?= css ?>, keyboard navigation + history inside the shell, thumbnails, and the broken-Less error path (last good preview kept).

Thumbnails show compiled styles + example HTML

  • The project Less is compiled once per rebuild; the resulting CSS goes into the layout sandbox's head, so each tile snapshots the same styling the stage has (Less variables and all).
  • Tiles are composed like the runtime composes real slides (<section data-ps-slide> around the slide, state current), and the global tile mounts example HTML instead of duplicating a real slide's content.
  • code-to-element now wraps styles[] entries in <style> blocks — previously raw CSS text in the iframe head fell through to <body> and rendered as literal garbage in the snapshot.

Less compiles in a worker

  • less-worker compiles off the main thread; worker-globals shims window/document and sets onReady: false before the browser build's auto-init runs, so no page stylesheet scan or <body> hiding happens on import.
  • If the worker can't start, compilation degrades to an inline less.render on the main thread — same output either way.
## What this PR does Adds a real presentation runtime to p/slides: the preview now displays the deck, navigation is resilient, and everything is authored in **HTML + Less only** — no JS to write. ### Authoring - **Styles live in `<style>` tags inside the slide HTML** (one editor buffer, no Styles tab). Every `<style>` in the deck — global slide first, then each slide — contributes to one project stylesheet. - **`<style>` tags are the project Less**, so variables, nesting, mixins and imports all work. Raw Less never reaches the DOM: the tags are stripped from the markup and the compiled CSS is injected by the runtime. - **`:thisSlide`** compiles to the selector of the slide whose stylesheet is being compiled (`[data-ps-slide="slide-N"]`), so deck styles stay scoped without inventing extra CSS syntax. ### The global slide is a template: `<?= html ?>` / `<?= css ?>` The runtime injects into the global slide: ```html <style> @frame: #161a22; .frame { background: @frame; padding: 14px; } </style> <div class="frame"> <style><?= css ?></style> <?= html ?> </div> ``` - The global slide is rendered **once** as the scene; slide markup replaces `<?= html ?>`, the compiled stylesheet replaces `<?= css ?>` (wherever `<style><?= css ?></style>` sits). - Without `<?= html ?>` the global slide keeps its old meaning: markup shared by every slide. - Without `<?= css ?>` the stylesheet falls back to a single `data-ps-slides` sheet in `<head>` — never both, so nothing fights over the rules. ### Runtime - All slides stay mounted; lifecycle is published as `data-ps-state` (`idle` → `entering` → `current` → `leaving`). CSS owns the animation, JS owns the lifecycle. - Interruptible navigation with a generation counter: rapid arrow presses never queue up, stale settles are discarded. - `runtime.goTo(index)` / `next()` / `previous()`; back/forward and reload restore the slide index (state key `psslides-slide`, merged into `history.state`). - Preview and Present use the same runtime, no iframe. - Compiler (`compile.ts`) and runtime (`runtime.ts`) are separate modules; placeholders are shared via a tiny `placeholders.ts` so the runtime never pulls in the Less compiler. ### Notes / out of scope - Single-file standalone HTML export would need a bundler plugin. - Slide persistence across a hard reload (the app has no document persistence yet — pre-existing). ### Verification `prettier --check`, `tsc --noEmit`, `oxlint`, `npm run build` all green. Browser-tested: template shell render, `<?= css ?>` injection (frame background compiled from the Less var), slide element identity across recompiles and template switches, head-fallback when there is no `<?= css ?>`, keyboard navigation + history inside the shell, thumbnails, and the broken-Less error path (last good preview kept). ## Thumbnails show compiled styles + example HTML - The project Less is compiled once per rebuild; the resulting CSS goes into the layout sandbox's head, so each tile snapshots the same styling the stage has (Less variables and all). - Tiles are composed like the runtime composes real slides (`<section data-ps-slide>` around the slide, state `current`), and the global tile mounts example HTML instead of duplicating a real slide's content. - `code-to-element` now wraps `styles[]` entries in `<style>` blocks — previously raw CSS text in the iframe head fell through to `<body>` and rendered as literal garbage in the snapshot. ## Less compiles in a worker - `less-worker` compiles off the main thread; `worker-globals` shims `window`/`document` and sets `onReady: false` before the browser build's auto-init runs, so no page stylesheet scan or `<body>` hiding happens on import. - If the worker can't start, compilation degrades to an inline `less.render` on the main thread — same output either way.
navi added 1 commit 2026-09-27 19:22:48 +00:00
- src/slides/compile.ts compiles a project into runtime-ready slides.
  :thisSlide is resolved to each slide's own selector before Less runs,
  so output is deterministic and never depends on which slide is visible.
- src/slides/runtime.ts mounts every slide once and moves them through
  idle -> entering -> current -> leaving via a data-ps-state attribute;
  CSS owns the animation, JS only owns the lifecycle. Rapid input
  coalesces into a single settle instead of a queue of stale operations.
  Arrow/space/Home/End keys, plus history integration: back/forward step
  through slides and a reload restores the slide you were on.
- The editor gained two buffers: HTML slides and the project Less. The
  preview and the presentation are the same runtime (present = fullscreen
  of the same stage), and broken Less keeps the last good preview while
  reporting the error.

Adds `less` (the authoring language) and `@types/less`.
navi added 1 commit 2026-09-27 19:58:40 +00:00
Less now lives in <style> tags alongside the slide HTML, so there is one
editor buffer and no separate stylesheet tab.

- compile.ts extracts every <style> tag in the deck (global slide first,
  then each slide in order) into one project stylesheet, strips the raw tags
  from the markup so uncompiled Less never reaches the DOM, and compiles that
  source once per slide with :thisSlide resolving to the slide being compiled.
- editor.ts is back to a single buffer; the CSS/LESS workers still highlight,
  validate and complete the Less inside the tag.
- thumbnails render markup with style tags removed, exactly like the
  presentation does.
navi added 1 commit 2026-09-27 20:36:53 +00:00
navi added 1 commit 2026-09-27 21:00:31 +00:00
navi added 1 commit 2026-09-28 11:05:59 +00:00
- thumbnails: compile the project stylesheet once per rebuild and feed it
  into the layout sandbox, compose each tile like the runtime does
  (<section data-ps-slide> around the slide), and show example HTML on
  the global tile instead of another real slide's content.
- worker: less compiles off the main thread in less-worker; a side-effect
  module shims window/document and disables the browser build's page
  auto-init, and compilation falls back inline if the worker can't start.
- code-to-element: wrap styles[] entries in <style> blocks - raw CSS text
  in the iframe head fell into <body> and rendered as literal garbage.
navi added 1 commit 2026-09-28 12:29:46 +00:00
Every slide's :thisSlide rules leaked into every other slide: the whole
joined source was recompiled once per slide with all occurrences rebound,
so each pass attached every slide's scoped rules to that pass's slide and
the concatenated output applied them all to everyone. Bind the token while
each block still belongs to one slide (global block -> any slide) and
compile the project once - also drops the O(n) duplicated CSS output.
navi added 1 commit 2026-09-28 12:32:44 +00:00
navi added 1 commit 2026-09-28 13:40:37 +00:00
Binding only the `:thisSlide` token still left every untokened rule in a
slide's tag compiling deck-wide, so one slide's styles kept applying to
all slides. Wrap each slide's whole style block in its section selector
instead, with `:thisSlide` resolving to `&` — Less hoists `@keyframes`/
`@font-face` out of the wrapper itself and keeps `@media` nested inside,
so only rules end up scoped. The global block stays the shared layer and
the deck still compiles exactly once.
jan closed this pull request 2026-10-02 11:56:37 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: jan/psslides#1
No description provided.