• Гильмуллин Руслан Ниязович's avatar
    feat: showcase panel overhaul — execution-trace table, lesson notes,...
    · a207d177
    feat: showcase panel overhaul — execution-trace table, lesson notes, single-snippet orchestration (#112) (#113)
    
    * feat: showcase panel overhaul — execution-trace table, lesson notes, single-snippet orchestration (#112)
    
    The landing-page snippet showcases used to be a single column with a
    {frameIndex} counter under each animation. Reworked around stepped-
    equivalent learning artifacts: a per-iter trace table, prose lesson
    notes beside the player, and Landing-orchestrated playback so only one
    snippet runs at a time.
    
    Execution-trace table (src/components/ExecutionTraceTable.svelte)
    - Six columns — Step | State | Head reads | Write | Move | Goto — same
      shape as the engine README's trace example, minus the Tape after
      column (the live tape strip already covers that).
    - Highlighted row carries aria-current="step" and the
      --graph-highlight-soft-fill background so the trace matches the
      graph's "you are here" feel.
    - Sticky thead inside max-height: 280px overflow-y: auto.
    - Scroll-into-view is scoped to the trace's wrap container via manual
      scrollTop math — Element.scrollIntoView walks every overflow
      ancestor (including the page) and would yank the whole landing
      layout each time a partially-visible panel ticked.
    - Multi-tape cells render as bracketed arrays ([a, _] / [*, b] /
      [R, L]). Blank cells use the .blank dimmed class instead of UI
      substitution (matches Tape.svelte policy).
    - Halt resolves toId === 0 → 'halt' directly so the engine graph
      node's literal "id:0" placeholder name doesn't leak through.
    
    Two-column SnippetPanel + per-showcase lesson notes
    - Below the caption, a CSS grid: left column is the player content
      (graph + tapes + trace + controls) on a framed surface
      (background: var(--bg) + border + radius — reads as an embedded
      screen within the outer card); right column carries lesson prose.
      Stacks to a single column at <=768px.
    - Landing's snippet grid switched from a multi-column auto-fill
      track to one panel per row so the internal two-column layout has
      room. Showcase Tape viewport widened to 17 / 13 / 9 cells across
      the desktop / tablet / phone breakpoints.
    - New Example.lessonNotes?: string field carries the prose. All six
      showcases got multi-paragraph notes — what the program does, the
      role of each state or instruction, and (for callable-subtree and
      call-subroutine) the subroutine boundary, the call/return arrows,
      and the wrapper-vs-bare distinction.
    - src/lib/lessonMarkdown.ts renders a tight subset (paragraphs,
      bullet lists, inline backtick code) — HTML-escapes input then
      emits the formatted block. Injected with {@html} under the demo's
      existing build-time-author-content policy (same policy the SVG
      icons use). Renderer tests at src/lib/lessonMarkdown.test.ts.
    
    Single-snippet playback orchestration
    - Landing.svelte owns a single IntersectionObserver across all
      rendered panels (one observer; ratios tracked across callbacks
      since IO reports only what changed). A Svelte use:panelSlot
      action registers each panel's slot wrapper with the parent
      registry on mount. The panel with the highest visible ratio
      becomes active. Engine switch clears the activeSnippetId + ratios.
      Under prefers-reduced-motion the IO is never created.
    - SnippetPanel becomes a controlled playback consumer. The internal
      IO is gone. Receives an active: boolean prop and dispatches a
      4-state playback machine through one $effect:
    
        state    | becomes active                     | becomes inactive
        ---------|------------------------------------|-------------------
        idle     | reset to frame 0 + start timer →   | stays idle
                 | playing                            |
        playing  | (already playing — no-op)          | clear timer → paused
        paused   | resume timer from current frame →  | stays paused
                 | playing                            |
        done     | no auto-replay — Replay only       | stays done
    
      First viewing plays from the top; scroll-away pauses without
      reset; scroll-back resumes. Finished snippets sit at end-state
      until the visitor clicks Replay — re-scrolling past a completed
      snippet doesn't restart it. Reduced motion pins to the final
      frame on mount and ignores active changes.
    - The active-toggle $effect's body wraps in untrack() so $state
      reads inside dispatch helpers (machineGraphRef, tapesStackRef,
      graphReady) don't become effect deps and form an infinite loop.
      replayTimerId demoted from $state to plain let to break a
      separate read/write feedback loop on the timer handle.
    - Trace-row highlight clears at natural playback end too —
      parallels the graph-highlight clear at the same moment (#108).
      Under reduced motion the last row stays highlighted to mirror the
      graph holding the final-frame snapshot.
    
    Stepped-shaped graph highlight during playback
    - recordSnippet emits after-iter-shaped highlights (strong: 'from',
      paused: false, fromId = bareIdOf(m.state.id) — already
      canonicalized). For wrapper-entry iters that canonicalization
      discards the wrapper info, so the callable-subtree showcase's
      wrapper node + the bold "call" arrow stayed dim during playback
      even though the cluster lit up via §9.
    - Fix lives entirely in the demo, no upstream visuals change.
      src/vite-plugins/snippets.ts wraps machine.runStepByStep BEFORE
      calling recordSnippet so the wrapper rides the same iteration and
      shares the same State id space the graph was built from.
      Captures m.state.id / m.nextState.id per iter into rawStateIds /
      rawNextStateIds, attached to each snippet as extension fields.
    - SnippetPanel.applyGraph now builds the highlight from rawStateIds:
      fromId = bareIdOf(rawStateIds[step - 2], graph) (or 'idle' for
      the first iter), toId = rawStateIds[step - 1] raw, strong: 'to',
      paused: true — the before-pause shape live stepped execution
      emits, so §2 expansion in applyHighlight lights up [wrapper,
      bare] and §10 lights the call edge during the wrapper-entry iter.
      The recorded Frame.highlight stays untouched (the trace table
      reads it as-is).
    - A first attempt re-eval'd the example to capture raw ids in a
      separate machine — that minted fresh State runtime ids that
      didn't match graph.nodes keys, and the symptom was "only idle
      highlighted, nothing else." Wrapping the same machine fixed it.
    
    Layout plumbing fixes that surfaced along the way
    - src/app.css body gets overflow: hidden + the flex chain (body →
      main → .landing) gains explicit min-height: 0 so internal
      containers bound correctly and .landing's overflow-y: auto owns
      the only vertical scroll. Without it, Safari's rubber-band
      overscroll exposes empty page-background area below the footer
      when content is short.
    
    Tests + tooling
    - SnippetPanel.test.ts rewritten: FakeIntersectionObserver harness
      gone, tests drive the new active prop directly. New cases:
      inactive-pauses-preserves-highlight (pause-and-resume keeps frame
      position), done-no-autoreplay-on-scroll (finished snippets need
      explicit Replay even after re-scrolling in). Trace-row
      assertions go through [aria-current="step"] +
      data-testid="trace-row" + data-step.
    - ExecutionTraceTable.test.ts covers row count, current-row
      marking, multi-tape brackets, blank-cell class, halt rendering,
      unknown-node fallback.
    - Stub Snippet.graph.nodes entries now include the full GraphNode
      shape (transitions, isWrapper, isHaltMarker, tags) because the
      embedded MachineGraph's summariseGraph walks them — happy-dom no
      longer trips on missing fields.
    - 281 tests across 27 files; Playwright landing.spec.ts unchanged
      (still 18 e2e green).
    
    Docs
    - CLAUDE.md component tree gains Landing.svelte / SnippetPanel.svelte
      / ExecutionTraceTable.svelte / lessonMarkdown.ts entries with
      their new responsibilities (IO orchestration, 4-state machine,
      manual scrollTop, lesson-prose policy).
    - README.md intro opens with the landing page; layout tree gets
      the three new components + lessonMarkdown.ts; e2e section lists
      all three spec files.
    - docs/execution-model.md gains §1.1 "Scope: engine pages only" —
      SnippetPanel playback is orthogonal to MachineView's DebugSession
      modes.
    - docs/machine-graph-palette.md highlight-tokens table notes
      ExecutionTraceTable reuses --graph-highlight-soft-fill so the
      trace and graph stay in lockstep when re-tuning.
    
    Closes #112.
    
    * chore(release): 1.0.0-alpha.25
    
    Showcase panel overhaul shipped in the preceding commit (#112):
    execution-trace table replacing the {frameIndex} counter, two-column
    panel with per-showcase lesson notes, Landing-orchestrated playback
    (4-state machine: idle / playing / paused / done — finished snippets
    need explicit Replay), stepped-shaped graph highlight that lights up
    the wrapper + call edge during wrapper-entry iters.
    a207d177