Table of Contents

Collapsing Across the Report

Note

Status: implemented. Every surface that folds follows one contract and one set of glyphs: the Source editor, the Preview, and the Dialogue Graph each fold their own unit, each with a per-item chevron and a pair of all-commands.

Table of contents

Goal and scope

Three surfaces of the report let a reader put something away: the Source editor folds line ranges and ignored regions, the Preview hides ignored regions, and the Dialogue Graph folds scenes.

This note owns one contract and one design language for collapsing. It deliberately does not synchronize state across surfaces; the measurement below shows why that cannot work in general, and D1 explains why the panes should differ even where it could.

In scope: the shared contract and glyph set; the Dialogue Graph's all-commands; the ignored region as a unit Source folds, so Source and Preview fold the same thing.

Out of scope: changing what the compiler ignores or which grouping a scene is; folding nested scenes; serializing graph fold state across a reload.

Vocabulary

One act, one set of words, on every surface.

Term Meaning
Fold The act of putting an item away, and the act of bringing it back. The gesture, whatever the surface.
Collapsed / expanded The state of one item.
Item The thing a surface folds — a line range, an ignored region, or a scene. Each surface has exactly one.
Per-item control The one focusable control that folds its item.
All-command Expand all or Collapse all — a command over every item on that surface.
Baseline The state an item has when the reader has not chosen otherwise.
Override One item's deviation from the baseline.

One rule applies on every surface: a static mark states a status, a chevron performs an action.

The problem: one word, three units

The three surfaces fold genuinely different units, and the difference is not incidental:

Surface Unit Why that unit
Source A line range The editor edits lines; folding is an editing convenience.
Preview An ignored region Only content the compiler excluded may be hidden — the rest is the compiled result.
Dialogue Graph A scene region The only grouping the compiler owns, so folding it cannot make the drawing lie.

Because the units differ, state cannot be synchronized. Measured on a representative script, only 3 of 7 ignored regions were foldable in Source — a table and two fenced code blocks. The other four cannot fold at all: a --- divider is a single line, and an inline autolink is a span inside a line. A rule that syncs the intersection is a rule a writer cannot predict.

flowchart TB
    C["Contract + design language:<br/>per-item chevron · two all-commands · stated state"]
    C --> S["Source<br/>unit: line range → ignored region"]
    C --> P["Preview<br/>unit: ignored region ✓ already follows"]
    C --> G["Dialogue Graph<br/>unit: scene region"]

What can be shared is the contract and the look: how folding is offered, not what is folded or what each surface currently holds folded.

The collapse contract

Every surface that folds anything offers all four of these.

  1. A per-item control. One focusable control on the item, carrying aria-expanded and an accessible name that says what it will do. It does not move between states, so folding the same item twice needs no pointer movement.
  2. Two all-commands, never one toggle. Expand all and Collapse all, both available whenever the surface has at least one item. They are commands: each adopts a baseline for the whole surface and discards every override. A single toggle cannot name its action from a mixed state, which is exactly when a reader needs it most.
  3. The state is stated exactly, including mixed — 5 of 7 shown, not a word that implies all or nothing. Each surface counts in its own terms: the Preview says how much is shown, because that is what a reader of the compiled result cares about, while the graph says how many scenes are folded, which is what its commands act on.
  4. Folding is not selecting. A fold changes what is drawn, never what the reader has chosen, unless the fold hid the chosen thing.

Each surface keeps its own state and its own pair of all-commands. The contract is a shape every surface honors, not a state they share — see D1.

The design language

One act deserves one look. The report uses one set of glyphs, all from the codicon font it already loads:

Role Glyph Where
Action — expanded chevron-down Every per-item control
Action — collapsed chevron-right Every per-item control
All-commands expand-all / collapse-all Every surface that folds more than one item
Status — excluded from dialogue circle-slash Ignored regions and the Preview footer
Status — conditional dialogue question Conditional blockquotes

A status glyph is a static mark and never focusable; an action glyph is always a real control with aria-expanded. Where an item has both, the action leads the region and the status trails it: the chevron sits in the leading gutter, and the status mark at the trailing edge, where a conditional blockquote's own marker already sits. Status therefore means the same place whatever kind of region a reader is looking at, and the two marks frame the content between them.

A status mark may also carry an item's own fold state where a list already shows one mark per item. The legend's scene rows do this: a filled swatch still holds its nodes, a hollow one has put them away. It stays a mark rather than borrowing the chevron, because the row's click already means something else — dim this region — and folding has its own control on the band.

The Dialogue Graph draws SVG rather than HTML, so its chevron becomes an SVG <text> node in the codicon font instead of a path — the font is declared for the whole document, so this is the same glyph, not a lookalike.

Two more surfaces fold something and use the same language: the legend's own group disclosure, and the file Explorer's folders. A submenu marker keeps its own chevron, because it points at a menu opening beside it rather than at content that folds away.

No exceptions. An inline ignored region in the Preview takes the same chevron on the left and the same circle-slash on the right as a block region, even though it is narrow: one glyph doing both jobs would mean press me in one place and this is ignored in another.

Between those two marks it also keeps a brief: a block region collapses to Table · 4 lines, so an inline one must not collapse to nothing. A link collapses to its host, still a link, with the whole address in its tooltip — the reader can see what was set aside, and still follow it.

All-commands for scenes

The graph's fold takes a set of collapsed region names, so the all-commands fill or empty that set rather than adding fold machinery.

Question Answer
Where the control lives Beside the legend's Scene regions group heading — the group that already lists exactly the items being folded, with their counts.
What "all" means in a mixed state Two commands, per the contract. Neither has to guess.
Framing after the fold Re-fit the camera on an all-command only. A single fold must leave the reader where they were; an all-command is a deliberate whole-view change, and collapsing every scene otherwise leaves the reader staring at empty canvas.

The legend states the current view, including mixed, beside the group's name. What selection does across a fold is Dialogue Graph Region Fold's.

Ignored regions in Source

Source folds the ignored region as a unit, so both panes fold the same thing.

Source already folds ranges of lines from its gutter, and an ignored block region is a range of lines. So it folds from the same place, through the same mechanism: the compiler's ignored spans are published to CodeMirror as another source of foldable ranges, and the gutter chevron the writer already knows appears beside them.

What sits on the region is therefore a cue, not a control: a circle-slash trailing the region's first line that says the compiler left this out, in the manner of an editor's inline annotations. It states a status and takes no click — exactly what the shared rule asks of a static mark. It trails the first line rather than leading it because a mark before a table's first row would push that row out of line with the rows beneath it, and an editor's columns are part of what a writer is reading.

The cue marks every region that owns its lines, including a one-line --- divider that cannot fold — dimmed ink alone is nearly invisible on three dashes. An inline span inside a sentence gets no cue: mid-sentence it would read as a stray character, and the dimmed ink there sits against words that make the omission legible. So the two marks answer two questions independently — circle-slash says this is ignored, the gutter chevron says this can fold.

The all-commands live in the editor's context menu and keymap (Alt-i folds every ignored region, Alt-o opens them) rather than in a footer of their own, and they drive the editor's own fold state, so a region folded by hand and a region folded by the command are the same thing afterwards. Source is an editor, where commands conventionally live in menus and keys; a second footer row would also have broken the #END row's deliberate alignment with the Preview footer.

Because the folding is CodeMirror's, its guarantees come along unearned: the cursor cannot be placed inside a folded range, the editor's own placeholder opens it on click, and a fold survives edits elsewhere in the document by mapping through them.

The ignored spans survive the re-render that follows every keystroke, keyed as the Preview keys regions.

Key design decisions

D1 — Unify the language, not the state

Two things could be unified: how folding is offered, and what is folded where. Only the first can be.

Synchronizing state would require a mapping between units that does not exist in either direction: line ranges that are not ignored (a heading section) have no Preview counterpart, and ignored spans that are not line ranges (a divider, an inline autolink) have no Source counterpart. Any partial sync teaches the reader a rule with unpredictable exceptions.

Even where the units match, Source and Preview keep separate state, because the two panes answer different questions. Source is the editable truth: its ignored text is deliberately dimmed but visible so a writer can find and change it. Hiding a region while reading the compiled result must not remove the text the writer may need to edit. So each surface owns its state and its own pair of all-commands — which also keeps the commands reachable in Zen mode, where the Preview is hidden.

Unifying the language costs nothing at the boundaries and makes every surface behave the way the reader already learned on the first one they used.

D2 — Each surface keeps its own unit

A surface's unit follows from what that surface is for. Source edits text, so line ranges are a real convenience there and stay. Preview shows the compiled result, so only excluded content may be hidden. The graph draws flow, so only a compiler-owned grouping may be contracted without lying.

Source keeps line-range folding and adds the ignored-region unit alongside it. Source is therefore the one surface with two units — but deliberately with only one gesture: both fold from the gutter, because both are ranges of lines, and CodeMirror's gutter is already the affordance for exactly that.

A second control on the region itself would put two chevrons on one line, each folding a different thing. The distinction the reader needs is what is ignored, which a static cue answers better.

Dropping line-range folding to leave one unit was rejected: folding a scene's prose while writing is genuinely useful and has nothing to do with what the compiler ignores.

D3 — All-commands override, single folds do not

An all-command is a statement about the whole surface, so it discards overrides and — on the graph — re-fits the camera. A single fold is a local act, so it leaves the baseline, the other items, the selection, and the camera alone. This is the rule that lets mixed state exist safely: however scattered a view becomes, one command returns it to a state the reader can name.

D4 — Persist a baseline, never the overrides

A baseline is a preference — how the reader wants to read this project — and is worth remembering. An override is working state about one item in one sitting; persisting it would accumulate entries for items that no longer exist and would restore a scattered view days later with no visible cause.

The contract states that principle and lets each surface apply it. The Preview persists its baseline in one conditional storage key. The Dialogue Graph persists nothing, because its fold rides with a camera the report deliberately does not serialize so the offline file stays self-contained. The two therefore differ across a reload, which is honest: their state means different things.

D5 — Fold is the gesture; shown/hidden describes the Preview's result

The report keeps one word for the act. The Preview's prose still reports visibility (5 of 7 shown in Preview), because in that pane the outcome a reader cares about is whether the content is there, not the mechanism that put it away.

D6 — Content first, chrome second

An ignored region is usually an authoring aid the writer still reads — a table of prices, a block of notes — so the region's own furniture must not out-shout what it frames.

The chrome recedes while the content shows and comes forward on hover or focus, and the content is set back from dialogue rather than faded toward the page. Two floors keep the quieting honest, and both are tested: the control's glyph stays above the 3:1 a UI component owes even at rest, and the content keeps at least 55% of normal prose's contrast.

The one exception is a collapsed region, where the control keeps its surface unconditionally. There is no content left for it to defer to — the control is standing in for what it hid, so it should read as the thing to press.

Boundary cases

Case Behavior
Surface has no items Both all-commands disabled; the surface says so.
Mixed state Stated exactly; both commands remain available.
An all-command when the view already matches Still valid — it clears overrides.
Graph: collapse all with a selection inside a scene Selection moves to that scene's box, per the existing rule.
Graph: collapse all, then navigate to a hidden node The scene expands first, per the existing rule.
Source: edit inside a folded ignored run The cursor cannot enter a folded range, so hidden text is never silently edited.
Source: an ignored run inside a folded heading section One fold state holds both, so the outer fold simply wins.
Source: an ignored run of one line (a --- divider) Carries the cue, offers no chevron — there is nothing beneath to hide.
Source: an ignored span inside a line Keeps its dimmed ink and neither mark; only the Preview collapses inline regions.
Source folded, Preview not (or the reverse) Expected: the two panes hold separate state by design.
Preview: a collapsed region The control keeps its surface — with the content gone, it is what the reader must find.
Preview: a shown thematic break The rule takes the marks' own line and full-strength muted ink — it is the region's whole content, and it has no text to align against.
Preview: collapsed inline ignored chip Keeps both marks and its place in the sentence; the content between them shrinks to a brief.
Preview: collapsed inline autolink Shows its host, keeps its href, and carries the full address as its tooltip.
Codicon font unavailable Controls keep their accessible names and hit targets; only the glyph degrades.

Testability

  • Projection/unit tests for the graph's collapse-all: filling and emptying the set, mixed state, and that the projection is unchanged for a set it already held.
  • Unit tests for Source's ignored-region fold: which runs own their lines and which can fold, what the gutter offers on a given line, and that the all-commands drive the editor's fold state without folding a run twice.
  • Browser tests per surface: the per-item control keeps its box across a toggle, an all-command overrides individual choices, the stated view matches reality, and axe passes in a mixed view.
  • A design-language test reads the sources and fails if any module outside the shared one names a chevron itself, so a new surface cannot quietly introduce another rendering. It covers behavior rather than stylesheets: a CSS ::before cannot call a helper, so the legend group's disclosure names the codepoint directly.
  • A cross-surface state test is deliberately absent: the language is a shape each surface honors, and the panes are meant to differ.

Open questions

None blocking. Two settled points worth revisiting if the report grows:

  1. Folding nested scenes. Regions are flat, so a nested scene folds separately. If the compiler ever nests them, the all-commands need to say whether "all" means every region or only the top level.
  2. A fifth surface. The Config tab folds TOML sections through its own fold service. It is out of scope here because it collapses neither dialogue content nor a compiler grouping, but it should adopt the design language if it ever grows all-commands.