|
| 1 | +# Reveal render/preview theme parity |
| 2 | + |
| 3 | +**Strand:** bd-y259zb57 |
| 4 | +**Found:** 2026-06-16, during e2e testing on `main` (post-#297). |
| 5 | + |
| 6 | +## Problem |
| 7 | + |
| 8 | +Two render/preview divergences, **one root cause**: `q2 render` uses the |
| 9 | +*compiled Quarto reveal theme*; `q2 preview` uses *stock vendored `white.css`*. |
| 10 | +They diverge in opposite directions: |
| 11 | + |
| 12 | +| | `q2 render` | `q2 preview` | |
| 13 | +|---|---|---| |
| 14 | +| Theme | compiled Quarto theme | stock `white.css` + `quarto-reveal.css` | |
| 15 | +| Text align | left ✓ | center ✗ | |
| 16 | +| Headings | none ✓ | uppercase ✗ | |
| 17 | +| Font | (was) Helvetica ✗ → now SSP via @import ✓ | SSP (white.css embeds it) ✓ | |
| 18 | + |
| 19 | +## Bug 1 — render fonts — DONE (commit 5b46b50e) |
| 20 | + |
| 21 | +The compiled theme named `Source Sans Pro` but loaded nothing → Helvetica. |
| 22 | +**Fixed**: the Quarto reveal layer (`resources/scss/revealjs/quarto-revealjs.scss` |
| 23 | +`scss:defaults`) now `@import`s Source Sans Pro from Google Fonts (foundry |
| 24 | +import, no bundling — user decision, matches Q1's `_brand.yml` font model). |
| 25 | +Sass hoists the CSS `@import` to the top. Test: |
| 26 | +`default_theme_imports_source_sans_pro_from_google`. E2E: Chrome confirmed the |
| 27 | +SSP 400 face loads + applies on render. |
| 28 | + |
| 29 | +## Bug 2 — preview centered + uppercase (Level 2: full per-document parity) — TODO |
| 30 | + |
| 31 | +The q2-slides preview **never applies the Quarto reveal layer**, for *any* |
| 32 | +theme/brand. So preview shows reveal's stock defaults (centered, uppercase) and, |
| 33 | +once Level 2 lands, must instead compile and apply the document's actual reveal |
| 34 | +theme (default / named / custom `.scss` / `_brand.yml`). |
| 35 | + |
| 36 | +### Corrected architecture map (the 2026-06-16 Explore agent got two key things wrong) |
| 37 | + |
| 38 | +- **WRONG (agent):** "the reveal branch in `CompileThemeCssStage` fires for |
| 39 | + q2-slides." **Reality:** that branch is gated on |
| 40 | + `ctx.format.identifier == FormatIdentifier::Revealjs` |
| 41 | + (`compile_theme_css.rs:267`); `q2-slides` → `("html", preview)` |
| 42 | + (`format.rs:125`), so `identifier == Html`. **The preview never compiles the |
| 43 | + reveal theme at all.** (The agent confused the `is_revealjs_target()` helper — |
| 44 | + true for `q2-slides` — with the identifier check.) |
| 45 | +- **WRONG (agent):** "reveal registers `css:theme:<fp>` like HTML." **Reality:** |
| 46 | + `register_reveal_assets` uses `css:revealjs:*` keys (`assemble.rs:153`); the |
| 47 | + WASM `extract_theme_fingerprint` looks for `css:theme:` (`wasm-…/lib.rs:1463`). |
| 48 | + They do not match. |
| 49 | + |
| 50 | +### HTML theme-delivery model to mirror (verified parts) |
| 51 | + |
| 52 | +1. `CompileThemeCssStage` compiles the HTML theme → `store_css` registers |
| 53 | + `css:theme:<fingerprint>` (`compile_theme_css.rs`). |
| 54 | +2. WASM `render_page_for_preview` (`wasm-quarto-hub-client/src/lib.rs:1188`) → |
| 55 | + `extract_theme_fingerprint` (1461) → `RenderResponse.theme_fingerprint`. |
| 56 | +3. SPA reads VFS bytes at `DEFAULT_CSS_ARTIFACT_PATH` |
| 57 | + (`/.quarto/project-artifacts/styles.css`), mints a blob URL, posts |
| 58 | + `UPDATE_THEME` to the iframe (`Q2PreviewIframe.tsx:281`). |
| 59 | +4. Iframe applies it. |
| 60 | + |
| 61 | +### WASM reveal compile — AVAILABLE |
| 62 | + |
| 63 | +`compile_reveal_theme_css` is cfg-split: native `grass` |
| 64 | +(`quarto-sass/src/compile.rs:373`), WASM dart-sass via the JS bridge (`:575`, |
| 65 | +async). So the preview can compile the reveal theme on WASM. |
| 66 | + |
| 67 | +### Work (each TDD; verify through real `q2 preview` + Chrome) |
| 68 | + |
| 69 | +- [ ] **L2.1 (Rust).** Make the preview pipeline compile the document's reveal |
| 70 | + theme. The reveal branch must fire for the reveal-preview pseudo-format. |
| 71 | + Decide the detection signal (target_format `q2-slides` / |
| 72 | + `is_revealjs_target`, vs `ast.meta` `format: revealjs`) since |
| 73 | + `format.identifier` is `Html` in preview. Register the compiled theme so |
| 74 | + the WASM extractor + VFS flush find it (either reuse `css:theme:<fp>` + |
| 75 | + the styles.css artifact path, or extend the extractor for the reveal key). |
| 76 | +- [ ] **L2.2 (WASM).** Ensure `render_page_for_preview` surfaces the reveal |
| 77 | + theme (fingerprint + bytes flushed to the VFS artifact path the SPA reads). |
| 78 | +- [ ] **L2.3 (hub-client).** The preview reveal renderer must consume the theme |
| 79 | + (UPDATE_THEME / prop), inject it as a stylesheet, and **drop the static |
| 80 | + `white.css` import** (keep reset + reveal core + quarto-reveal). Bundle the |
| 81 | + Source Sans Pro `@import` comes along via the compiled theme automatically. |
| 82 | + Files: `ts-packages/preview-renderer/src/q2-preview/RevealDeck.tsx`, |
| 83 | + `Q2PreviewIframe.tsx`, q2-preview-spa entry. **hub-client change → two-commit |
| 84 | + changelog; WASM/SPA rebuild required.** |
| 85 | +- [ ] **L2.4.** Tests + E2E parity check (`q2 render` vs `q2 preview` on a deck |
| 86 | + with default, a named theme, and `_brand.yml` — all should match). |
| 87 | + |
| 88 | +### Notes / gotchas |
| 89 | + |
| 90 | +- Per-document theme means the SPA can no longer statically import one theme; |
| 91 | + it must inject the per-document compiled CSS at runtime (blob/style), like the |
| 92 | + HTML path does. |
| 93 | +- `q2 preview` embeds the SPA bundle; after hub-client changes, rebuild WASM + |
| 94 | + SPA + re-embed (`hub-client npm run build:wasm`, `cargo xtask |
| 95 | + build-q2-preview-spa`, `cargo build --bin q2`) — see CLAUDE.md "Verifying Rust |
| 96 | + changes in q2 preview". |
| 97 | +- Existing related strands: bd-v053sk3s (preview revealjs Tier-1 parity GA |
| 98 | + gate), bd-qn8yi1su (golden render↔preview parity test). |
0 commit comments