canShift(CodeIdent) returns false in LALR-merged states that arise after
reducing a complex first argument (e.g. figure(table(...), caption: ...)).
The previous guard `!couldBeIdent && !canShift(CodeArgKey)` then caused
an early exit before the character-level scan ran, silently dropping the
CodeArgKey token for any named arg key that follows such a reduction.
Fix: run the backward character scan first and derive `couldBeArgKey`
from the raw predecessor char ('(' or ',') rather than from canShift.
The early-exit now reads `!couldBeIdent && !couldBeArgKey`, so arg-key
positions always proceed to the full scan regardless of parser state.
Also stop calling canShift(CodeArgKey) entirely — it is unreliable here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Typst grammar now emits CodeArgKey (mapped to tok-attributeName) for
named argument keys like 'columns:', 'align:', 'caption:'. 15 of 41
editor themes had no .tok-attributeName rule, so those keys appeared in
the default text color (black) despite the correct CSS class being set.
Chose colors that complement each theme's existing palette:
light themes → warm dark-orange family (#994409 / #7B3814 / #735C0F)
dark themes → each theme's accent color (gold, warm red, lavender…)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canShift(CodeArgKey) was consistently returning false because LALR
state merging folds the codeArgItem start state into others where
CodeArgKey is not in the valid set. As a result, named arg keys like
'columns:', 'align:', 'caption:' were always falling through to
CodeIdent (black) instead of CodeArgKey (tok-attributeName).
Fix: detect named arg key position by inspecting the nearest
non-whitespace predecessor character instead of trusting canShift.
prev == '(' or ',' means we are inside a call argument list — the only
positions where a named arg key can appear. prev == last char of a
keyword word (e.g. 'w' of 'show') correctly excludes '#show heading:'
from being treated as a named arg.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Named value identifiers like 'left', 'center', 'right' were being
highlighted as tok-function (blue) because codeValue used CallExpr
(callSuffix*), which styled any identifier in value position as a
function. Fix: add FuncExpr { CodeIdent callSuffix+ } (requires at
least one argument list or method suffix) and use it in codeValue
instead of CallExpr. Plain identifiers in value position now fall
through to CodeIdent → tok-variableName. CallExpr (callSuffix*) is
kept for codeExprBody and KeywordExpr where zero-suffix idents are
valid.
Tokenizer safety: only acceptToken(CodeIdent) when canShift(CodeIdent)
is true, preventing emission in LALR-merged states where neither
CodeArgKey nor CodeIdent is expected.
Outline: track '$'-parity across lines so that lines inside a display
math block (e.g. '= b+c$') are not incorrectly reported as headings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Named arg keys (columns:, align:, caption:) were appearing in black
because LALR state merging broke the CodeArgs/CodeIdent path for
multi-line expressions. Fix: emit a dedicated CodeArgKey token from
codeIdentTokenizer (forward-peek for ':' to pre-disambiguate), declare
it in the grammar's codeArgItem rule, and map it to t.attributeName in
styleTags — bypassing LALR lookahead entirely.
Multi-line display math ($ ...\n... $) was consuming the rest of the
document as orange text when contextual:true caused a backward scan to
find a previous closing '$' and falsely set isDisplay=true. Fix:
revert mathContentTokenizer to contextual:false with '\n' stop (each
MathContent token covers one line), and change InlineMath to
MathContent* so @skip consumes the newlines between lines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mathContentTokenizer now detects inline vs display math by scanning back
to the opening '$': if @skip consumed whitespace between '$' and the
content the tokenizer removes the newline stop (display math), otherwise
it keeps it (inline math). contextual: true prevents the tokenizer from
firing outside InlineMath entirely, avoiding the orange-body-text
regression seen when this was previously attempted without the guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- codeIdentTokenizer: extend guard to scan back through the keyword word
and accept when '#' immediately precedes it, so 'text' in '#set text(...)'
and 'heading' in '#show heading:' are highlighted as function names
- classHighlighter: add tags.attributeName → tok-attributeName mapping;
all 26 themes already define .tok-attributeName colours but the tag was
never mapped to the class, leaving named arg keys (columns:, caption:)
completely unstyled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'\n' inside CodeArgs was an invalid token, triggering Lezer error recovery
and resetting parser state before codeIdentTokenizer could fire. Heading
detection is unaffected — headingTokenizer uses raw input.peek(-1) char
reads which see the '\n' byte regardless of what @skip consumes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codeIdentTokenizer's backward scan stopped at \n, so identifiers at the
start of a new indented line inside multi-line arg lists (e.g. image(),
table() inside #figure(...)) never matched the '(' guard and stayed black.
Extend the whitespace skip to also cross newlines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two LALR state-merging bugs prevented Strong/Emphasis nodes from ever being
produced (confirmed: tok-strong/tok-emphasis count = 0 in browser diagnostic).
Bug 1 — _italic_ consumed as CodeIdent:
CodeIdent was a @tokens rule with identHead = [A-Za-z_], so '_italic_' (the
entire string including both underscores) matched as one CodeIdent token.
LALR merging caused CodeIdent to be in item*'s valid set, and CodeIdent >
"_" in @precedence, so the parser never opened Emphasis.
Fix: move CodeIdent to an external tokenizer (codeIdentTokenizer) with a
character-level guard — only fires when the preceding non-whitespace char
is one of '#', '.', '(', ',' (genuine code-context positions). In body
text where peek-back finds a newline, space, or markup delimiter, the
tokenizer returns without emitting, letting '"_"' open Emphasis correctly.
Bug 2 — StrongText never produced inside Strong:
The strongItem* / emphItem* loops merged with item* states via Lezer's
aggressive LALR merging. In the merged state MarkupContent was in the
valid set (from the item* side) and MarkupContent > StrongText in
@precedence, so MarkupContent was always produced — not a valid strongItem,
leading to error recovery with no StrongText in the tree.
Fix: replace the recursive strongItem* / emphItem* loops with flat external
tokens StrongBody / EmphBody (contextual: true). These fire only inside
Strong → "*" . StrongBody? "*" and Emphasis → "_" . EmphBody? "_", states
specific enough that canShift is reliable. They read everything up to the
closing delimiter or newline in one token, bypassing the LALR merging
entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous change switched mathContentTokenizer to contextual:true with no
newline stop, intending to support multi-line Typst block math. However,
LALR state merging causes canShift(MathContent) to spuriously return true in
body-text positions (e.g. after a RawInline backtick close), so the tokenizer
consumed everything until the next '$' — turning a full paragraph orange.
Revert to contextual:false with newline stop. This correctly handles both
inline ($x^2$) and single-line block ($ integral ... $) math. Multi-line
block math ($ formula\n continuation $) remains a separate issue for later.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mathContentTokenizer was stopping at newlines, causing a parse error for
Typst block math that spans multiple lines. The parser then entered a bad
state that cascaded: the stale error-recovery left the item* parser in a
degraded mode, causing body text below the math block to be highlighted as
t.string (orange).
Fix: switch to contextual: true (only fires inside InlineMath where
MathContent is actually expected) and remove the newline restriction so
the tokenizer reads until the closing '$' regardless of line boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add .tok-strong and .tok-emphasis CSS to the static editor theme so
bold/italic markup actually renders visually.
Move CodeKeyword from @tokens to an external tokenizer (codeKeywordTokenizer)
with a peek(-1)==='#' guard. LALR state-merging causes code-mode states to be
reachable in markup positions, making common English words like "in", "for",
"while", "return" trigger CodeKeyword highlighting in body text. The '#' guard
ensures keywords only fire immediately after the '#' sigil, never in prose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canShift(RawBlockBody) returns true in states LALR-merged with the
post-RawBlockOpen state, causing the tokenizer to consume all remaining
body text as one giant RawBlockBody. Add a backward character scan:
require newline immediately before input.pos, then walk back past any
lang tag (A-Za-z0-9) and verify three backticks precede it. Body-text
positions never have backtick-backtick-backtick there, so the guard
rejects them.
This was the root cause of everything after the first heading being
black: RawBlockBody swallowed the entire document from the user-name
line onward, making headings, bold, italic and math invisible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The single-HeadingLine token approach caused everything after the first
heading to be unparsed. Reverting to the two-token structure but adding a
backward character scan in headingTitleTokenizer: after canShift(), walk
backward past whitespace and require '=' immediately before the current
position. Body-text positions in LALR-merged states will have a letter or
closing bracket there instead, so the tokenizer returns without accepting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The two-token approach (HeadingMark + HeadingTitle) caused LALR state
merging: the parser state waiting for HeadingTitle after HeadingMark was
merged into body-text item* states. In those merged states the
headingTitleTokenizer fired for every paragraph line, swallowing bold,
italic, math and inline function tokens — leaving body text black.
Fix: collapse the heading into a single HeadingLine external token that
covers the entire heading line (= prefix + title). A single-token Heading
rule leaves no post-token parser state waiting for a second token, so no
LALR merging can occur. The ViewPlugin and all HeadingMark/HeadingTitle
infrastructure are removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing HeadingTitle from the grammar left HeadingTitle? undeclared,
causing the Lezer grammar compiler to fail and producing no parser
output — hence everything rendered as unstyled black text.
Dual approach to prevent heading style bleed:
- HeadingTitle exists in grammar with contextual: true + canShift guard
(prevents it from matching in body-text LALR states)
- HeadingTitle is intentionally absent from styleTags so even spurious
matches cannot apply heading colour to body text
- ViewPlugin styles heading titles by finding HeadingMark nodes and
extending tok-heading decoration to end-of-line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Heading style bleeding (Typst): the HeadingTitle external token approach
was unreliable — even with contextual:true and canShift(), body text was
being styled as headings. Remove HeadingTitle from the grammar entirely.
Instead, a ViewPlugin (headingLinePlugin in languages/typst/index.ts)
walks the syntax tree, finds HeadingMark nodes, and decorates the rest of
the line with tok-heading class + bold. This is unconditionally correct
because it is based on the syntax tree rather than the LR tokenizer state.
2. smooth_pdf_transition raw key shown in all locales: the key was in the
JSON locale files but missing from extracted-translations.json, which is
the allowlist the webpack translation loader uses to decide what to bundle.
Add it there so all locales (including fr, es, de already added) resolve
to their translated strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Draft compile mode and stop-on-first-error are LaTeX-only features not
supported by TypstRunner or QuartoRunner. Hide both sections from the
recompile dropdown for non-LaTeX projects. Also detect Quarto root files
(.qmd/.md/.Rmd) alongside Typst (.typ) to correctly set isLatexProject.
Add missing smooth_pdf_transition translations for French, Spanish, and
German (the English key already existed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two unrelated fixes:
1. quarto-log-parser: handle the two-line Quarto schema-validation
error format:
ERROR: In file main.qmd
(line 6, columns 24--27) Field "section-numbering" has value …
Previously neither the file name nor the line number were extracted,
so the error appeared without a red highlight. Now the first line
stores the filename in pendingLocation and the second line creates
the log entry with the correct file and line so the editor can jump
to and highlight it.
2. headingTitleTokenizer: change contextual: false → contextual: true
and guard with stack.canShift(HeadingTitle). With contextual: false
Lezer calls the tokenizer speculatively at positions beyond the strict
post-HeadingMark state; in some LALR-merged states the resulting token
was accepted for body-text lines, making them render as bold-blue
heading text. The contextual guard ensures the tokenizer only fires
in the one state where HeadingTitle is legitimately valid.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two ideas borrowed from the Collabst project (a Typst-native
collaborative editor): typst.ts WASM in-browser preview and Tinymist
LSP integration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
typst watch outputs the "[HH:MM:SS] compiled with errors" status line
FIRST, then the full diagnostic output (file:line:col, source snippets,
hints) AFTERWARDS. The previous code resolved the pending compile
promise as soon as COMPILE_DONE_RE fired, discarding all post-status
diagnostic lines. Those lines then got cleared by the next cycle's
COMPILE_START_RE, so output.log only ever contained the bare status
line — explaining the "zero verbosity" symptom.
Fix: introduce a two-phase buffering model. When COMPILE_DONE_RE fires,
enter "post-done" phase (storing doneResult) and keep accumulating into
currentLines. _finalizeCompile() is called either when the next
COMPILE_START_RE arrives (zero added latency) or after FLUSH_DELAY_MS
(150 ms fallback for the last compile). It concatenates pre-done and
post-done lines before resolving, so output.log now contains the full
diagnostic output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When typst watch detects a file change and compiles before the CLSI
resolver is registered (ResourceWriter writes files → typst compiles →
runTypst is called), _resolveAllPending was discarding the result
because pendingResolvers was empty. This caused two symptoms:
1. output.log only contained "compiled with errors" (no diagnostics)
because the result carrying the full stdout was thrown away.
2. Every other manual compile failed with "compilation already gone"
because the missed result caused a timeout, which killed the watcher
and triggered a watcher restart cycle (success → miss → timeout →
kill → restart → success → miss → ...).
Fix: when _resolveAllPending fires with no pending resolvers, store the
result in entry.pendingResult. _waitForNextCompile checks this field
first and resolves immediately if a cached result is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- local-compile-context: suppress failure/exited error state when
changedAt > 0 (another compile is already queued), preventing the UI
from flashing an error banner mid-typing that resolves moments later
- TypstRunner + CompileController: detect "compiled with errors" from
typst watch and non-zero exit from typst compile, and signal
status:'failure' to the frontend so the log panel opens automatically
with the parsed error details (previously always returned 'success')
- footer.scss: add dark-mode overrides for footer.site-footer so the
thin footer on project/marketing pages uses bg-dark-primary and
content-primary-dark text in dark theme instead of hardcoded light bg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KeywordExpr { CodeKeyword CallExpr? } merges the post-keyword LR state
with document-level markup states, where "_" opens Emphasis. CodeIdent
starts with identHead which includes "_", so the two tokens overlap.
Adding "_" after CodeIdent in @precedence resolves the conflict: CodeIdent
wins in the merged state (correct for '#set _name(...)'), and in pure markup
states CodeIdent is not in the valid set so "_" still opens Emphasis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
yamlFrontmatter() embeds the Markdown content as an overlay on the top-level
YAML-frontmatter tree. The previous mode (IgnoreMounts | IgnoreOverlays)
skipped that overlay entirely, so ATXHeading nodes were never visited and the
Quarto (.qmd) file outline was always empty.
Dropping the mode flag lets the iterator descend into overlay and mounted
subtrees. This is safe because every enterNode function already filters by
node name — visiting extra nodes from foreign-language mounts is a no-op.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KeywordExpr now optionally includes a CallExpr, so '#set text(size: 12pt)'
parses 'text' as a CallExpr/CodeIdent and gets the function-name highlight
colour. The optional CallExpr only shifts when the lookahead is CodeIdent,
so there is no shift/reduce conflict with other items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
headingTitleTokenizer now stops reading when it encounters '//' or '/*',
so '= Heading // note' correctly produces a HeadingTitle token for 'Heading'
and a LineComment for the rest of the line. Without this, the comment was
consumed into HeadingTitle, getting heading highlight and appearing verbatim
in the file outline.
Also strip trailing line comments from heading titles in the regex-based
document outline scanner, which reads raw text independently of the tree.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All cm6 themes define .tok-function but the classHighlighter had no entry
for tags.function(tags.variableName), so function-name tokens fell back to
tok-variableName (which themes leave unstyled). This affected Typst function
calls (#func(...)) and would affect any future language that tags function
names with t.function(t.variableName).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both tokens are "read until delimiter" catchalls that match almost every
non-newline character, causing buildTokenGroups conflicts with every other
literal token in LALR-merged states. Moving them to ExternalTokenizer (the
same pattern already used for HeadingTitle, RawBlockBody, etc.) makes them
context-isolated: the LR state machine only calls them when those tokens are
actually valid, so they never participate in the static token-group overlap
check.
Also exclude '<' from StrongText/EmphText so Label ('<' LabelName '>') is
recognised inside strong/emphasis spans rather than being consumed as plain
text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lezer's buildTokenGroups rejects grammars with ambiguous token sets.
Eight overlaps existed:
EscapeChar vs spaces — EscapeChar { _ } matches \t; after '\'
it must win over the @skip spaces token.
"(" / "." vs text tokens — in the LALR-merged state after #CodeIdent,
callSuffix delimiters must beat
MarkupContent / StrongText / EmphText.
"]" vs LineCommentContent — inside #[...], the ContentBlock closer
must win even if it follows "//".
One extended @precedence declaration resolves all eight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LineCommentContent { ![\n]* } matches the empty string, which Lezer
rejects as a zero-length token (infinite-loop risk). Change to ![\n]+
and mark it optional in the LineComment rule so empty // comments parse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Any item shared between headingTitleItem and document-level item causes
a shift/reduce conflict: the LALR automaton merges the two contexts and
makes the shared token ambiguous. The only structural fix is to make
HeadingTitle a terminal (external tokenizer) that reads greedily to EOL,
giving the LR state machine a context-isolated token that can never
collide with document-level item tokens.
Removes headingTitleItem sub-rule, HeadingText token, and updates
styleTags to match HeadingTitle directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing Strong and Emphasis from headingTitleItem eliminates the
conflict: both appear in document-level item, causing the LR automaton
to merge heading-title states with document-item states and make "*"
ambiguous (Strong opener vs. end of heading).
HeadingText is widened to ![\n$#`<@\\]+ so "*" and "_" inside headings
are consumed as plain text rather than producing error nodes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
headingTitleItem* allowed an empty HeadingTitle, causing a shift/reduce
conflict: after HeadingMark, seeing "*" the LR parser couldn't decide
whether to shift it as a Strong inside the heading or reduce HeadingTitle
to empty and treat "*" as a document-level item.
Changing to headingTitleItem+ forces HeadingTitle to be non-empty, so
"*" after HeadingMark must be inside the heading. Empty headings are
handled by Lezer's error recovery.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strong{strongItem{Emphasis}} and Emphasis{emphItem{Strong}} created a
mutual-recursion cycle that caused Lezer's LR automaton builder to
produce exponentially many states and crash.
Remove each construct from the other's item list. StrongText already
includes '_' and EmphText already includes '*', so nested delimiters
render as plain text inside the opposite construct rather than errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the StreamLanguage tokenizer with a full LR grammar compiled by
@lezer/generator, giving Typst the same parse-tree infrastructure that
LaTeX and BibTeX already use.
Grammar features:
- Headings (=, ==, …) via SOL-detecting external tokenizer
- Code expressions (#keyword, #func(args), #ident.method, #{…}, #[…])
- Named argument highlighting (key: value in function calls)
- Inline and display math ($…$)
- Strong (*…*) and emphasis (_…_) with bold/italic formatting
- Raw blocks (```lang…```) and inline raw (`…`)
- Nested block comments (/* /* */ */) via depth-tracking external tokenizer
- Labels (<name>) and references (@name)
- Backslash escapes
Infrastructure changes:
- lezer-typst/typst.grammar — new Lezer grammar
- lezer-typst/tokens.mjs — external tokenizers for context-sensitive lexing
- scripts/lezer-latex/generate.mjs — Typst added to grammars array so the
existing lezer-latex:generate script (and Dockerfile step) compile it
- .gitignore — generated typst.mjs / typst.terms.mjs excluded from git
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the custom regex-based ViewPlugin with the official
@codemirror/lang-yaml package. yamlFrontmatter({ content: mdLS })
wraps the Markdown language with a mixed parser: the leading ---/---
block is handed to the full Lezer YAML parser (proper key/value/scalar/
anchor/alias highlighting), while the document body continues to use
the Markdown parser. The manual Frontmatter extension import is also
removed since yamlFrontmatter handles frontmatter recognition itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typst: heading tokenizer now colors the entire heading line (not just the
'=' prefix), and bold/italic markers (*/_) map to strong/emphasis tags
rather than the generic operator tag. A typstHighlightStyle applies
bold/italic formatting even when the active theme lacks .tok-heading.
Quarto: enable @lezer/markdown's Frontmatter extension so the YAML header
is no longer mis-parsed as Setext headings. A new ViewPlugin decorates
frontmatter lines with type-appropriate CSS classes: keys (tok-typeName),
string/bool/number values, comments, and the --- delimiter markers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply the same CSS inversion filter to the HTML iframe as is already
applied to pdfjs PDF pages, so Quarto RevealJS presentations respect
the dark mode toggle. Also show the theme button for HTML outputs and
relax the darkModePdf condition to activate for iframes regardless of
the pdfViewer setting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When output.html exists, findOutputFiles includes project media files
(images, videos) in outputFiles via the MEDIA_REGEX exception so they
get served from the cache. _removeExtraneousFiles then treated them
as extraneous and deleted them. On the next incremental compile,
unchanged binary files are not re-synced, so the files were gone when
Quarto ran and when _appendMissingResourceWarnings checked for them.
Fix: skip deletion for any file that is a project input resource.
Those files appear in outputFiles to be served, not cleaned up.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When typst watch doesn't emit "compiled with errors" after a failed
compile, currentLines accumulates indefinitely. The next successful
compile then flushes the buffer including the stale error from the
prior cycle. Reset currentLines at the start of each compile cycle
("[HH:MM:SS] compiling ...") so each log only contains output from
one compile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-project-type setting: Typst defaults to on, LaTeX defaults to off.
Toggle appears in the compile dropdown under "Smooth PDF transition".
The enableTransition flag is read via a ref so toggling does not
reload the current PDF.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The auto-compile effect was calling debouncedAutoCompile() on every changedAt
update (every keystroke), including while a compile was already running. With
a 1000ms maxWait the debounce fired every second even mid-compile, chaining
compiles back-to-back and making the user wait for all of them to drain.
Fix: add `compiling` to the effect's dependency array.
- While compiling: the effect cancels the debounce immediately, preventing
any new compile from being queued.
- When compile finishes (compiling → false): the effect re-runs; if changedAt
is still > 0 (changes were made during the compile), it re-arms the debounce
exactly once. One follow-up compile, then idle.
Also remove the debouncedAutoCompile() re-queue from compiler.ts's
wasCompiling guard — the effect now owns that responsibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
document.startViewTransition with an async callback places a ::view-transition
overlay on top of the entire page, intercepting pointer events for the duration
of the callback (up to the 1s safety timeout + 250ms animation). With rapid
auto-compiles this created interface freezes and overlapping transitions that
could leave the visual lock in a broken state, causing 'stuck on compiling'.
Replace with a canvas snapshot overlay + CSS opacity fade-out:
- pointer-events:none so the overlay never blocks input
- snapshot covers the canvas-clear from setDocument() (no white flash)
- on pagerendered: opacity transitions to 0 over 250ms, then overlay removed
- gives the same smooth visual crossfade, reliably, in all browsers
Chrome 126+ retains the element-level startViewTransition path which is
scoped to the PDF container and does not affect the rest of the page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
document.startViewTransition generates both a named pseudo-element for the PDF
container (ol-pdf-viewer) and a root-level pseudo-element that covers the entire
page, causing the editor to fade along with the PDF.
Inject a temporary <style> that sets animation:none on the root pseudo-elements
before starting the transition, then remove it in transition.finished. Only the
named PDF container crossfades; the editor is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>