Skip to content

Feat/scroll scenes - #1060

Merged
ionutpasca merged 51 commits into
developmentfrom
feat/scroll-scenes
Oct 5, 2026
Merged

ionutpasca merged 51 commits into
developmentfrom
feat/scroll-scenes

Conversation

@vladgrecu

Copy link
Copy Markdown
Member

No description provided.

vladgrecu and others added 30 commits August 17, 2026 21:44
The generated TqMotion's scroll trigger animated ONLY the y axis (one
useTransform into style.y), silently dropping x/scale/rotate/opacity/filter
from the from/to keyframes — while the editor canvas interpolated all of
them. Same UIDL, two behaviors. The scroll path now mirrors the canvas
runtime: progress (windowed by scrollOffset, optionally useSpring-smoothed
by scrub) writes the FULL interpolated state straight to the DOM node via
useMotionValueEvent — zero React re-renders per frame, blur() interpolation
included. End-to-end test asserts the parity markers in the generated
wrapper.

First slice of the feat/scroll-scenes initiative (P0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tes (P1)

New TqScrollScene widget: a tall track with a sticky pinned stage whose
scroll progress (useScroll, optionally useSpring-smoothed) drives
data-scroll-bind property lanes on descendants — multi-keyframe, compositor-
only props (x/y/scale/rotate/opacity/blur), named presets (depth-1/2/3,
fade-window, rail-x, zoom-through, unblur-in) with JSON as the escape hatch.
Lanes are applied by DOM query with a MutationObserver so runtime Repeater
items are picked up — never React child introspection. Reduced-motion
settles on final or static per scene config; --scene-progress exposed only
when configured; dev warning when an ancestor's overflow disables sticky.
Element mapping scroll-scene-node → TqScrollScene; framer-motion reused.
End-to-end test covers page emission, binding passthrough and the wrapper's
guardrail markers. Lane semantics mirror the GUI renderer's
scroll-scene-lanes module verbatim.

Part of feat/scroll-scenes P1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…editor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pective

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pop-in, tilt-in, blur-up, pulse and float — same states, verbatim, as the
canvas renderer's motion-keyframes, so the published site matches the
editor for every new preset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the canvas renderer: exact heights let content taller than one
screen be painted over by the following section. Regression-guarded in the
end2end test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors the canvas renderer: travelling lanes no longer spill a scrollbar
onto the published page, and scene stacking is contained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
layout='chapters' makes the stage a centering grid and same-cell-stacks
every direct child (scoped child rule) — mirrors the canvas renderer.
End2end guarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pers

Mirrors the canvas: the CSS child rule stays for plain children, and a
restack pass pins the real grid items inline on mount and on observer
ticks, so wrapped/late-rendered content stacks too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…afe style tag

- The style tag emits via dangerouslySetInnerHTML (React SSR escapes text
  children of style, and style is a raw-text element that never decodes
  entities — the selector broke server-side and hydration failed on the
  text mismatch).
- The stacking pass mirrors the editor runtime: pierces open shadow roots
  and slots, re-runs on customElements.whenDefined for not-yet-upgraded
  hosts, self-heals stale grid-area off contents wrappers, and is fully
  duck-typed (no instanceof).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve watcher

The clean-then-rewrite regeneration deleted every generated directory and
streamed the tree back while the generated project's next dev was watching
it. That fed the watcher seconds of half-written state (transient compile
errors that sometimes wedged the dev server until restart), and webpack
drops its watch on a deleted-then-recreated directory — after which hot
reload silently stops and re-syncs appear to do nothing.

Generation now packs into a staging directory and reconciles into the
project dir: byte-identical files produce no watch event at all, orphans
are still deleted file-by-file (the stale-orphan guarantee that motivated
the clean), live directories are never deleted, and PRESERVE_ON_CLEAN is
honored at the root exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pper

The scroll-linked parity work replaced useTransform with
useMotionValueEvent + useSpring in the emitted framer-motion import; this
suite still asserted the old line and failed only on CI's full run — the
targeted suites run during development never included it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pure-DOM TqScrollVideo: scene discovery via data-scene-track, window
  start/end mapping, exp smoothing, reduced-motion poster/end, seek replay on
  loadedmetadata; the wrapper owns the size (absolute video + contain:layout
  that survives an authored position:static)
- styled-jsx plugin emits widget-element classes as :global(...) — styled-jsx
  stamps its scope hash only on host DOM elements, so every class handed to a
  Tq* React component compiled into a rule that matched nothing and widgets
  silently dropped their authored styles (a Scroll Video shipped with zero
  height)
- scene track stamps data-scene-track; TqScrollScene + TqScrollVideo complete
  the five-registry ceremony (mapping, widgets, component-path plugin,
  rules-of-hooks + plugin tests, end-to-end suite)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings in v0.43.54 and the any-AI-provider preparation for the assistant
(#1062) — provider utils across the workflow AI nodes, the ai-chat route and
provider generator, plus development's in-place stagger rewrite of the motion
widget.

git called this merge clean, and it was not. Only one file is touched by both
sides, widgets/motion-component.ts, and both sides independently added the
SAME two helpers to the generated runtime — ours near EASINGS for the
scroll-linked interpolation, theirs above cssFromState for the in-place
cascade. Different regions, so git kept both, and the emitted component
declared TRANSFORM_KEYS and transformComponent twice: every generated project
with a motion node would have died at parse time with "Identifier
'TRANSFORM_KEYS' has already been declared". The bodies were byte-identical;
one copy survives, carrying both comments' reasoning, and both features build
on it.

Proof: the generated runtime parses (babel, jsx) at 513 lines with no
duplicate top-level const, and carries our scroll path (applyScrollState, the
tilt-in/blur-up presets) alongside their cascade (cssFromState). 2774 tests
pass across 269 suites after a full lerna build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings in v0.43.55 and the e-commerce fixes (#1063): email rendering, cart
resurrection after checkout, and product-variant availability.

Zero conflicts and zero file overlap with the scroll work — their changes are
confined to the e-commerce and workflow-node generators. Verified beyond the
suite, since a clean merge is not proof for template-string generators: the
motion (513 lines), scroll-video (202) and scroll-scene (425) runtimes all
parse with no duplicate top-level declarations, and so do the workflow shared
(1306) and client (516) runtimes, whose executor-generator they edited.
2822 tests across 272 suites pass after a full lerna build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The generated TqScrollVideo wrapper no longer forces an inline
  position:relative — inline beats the CSS class carrying the backdrop's
  absolute-fill, which collapsed the video to a 0px in-flow div (in the
  JSX, URL live, nothing on screen). contain:layout alone keeps the
  wrapper the containing block, mirroring the canvas renderer. Regression
  test pins the emitted wrapper style.
- Scroll scene default layout is chapters; viewport-pass scrubbing is a
  legacy fallback (contract comments).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An absolutely positioned clip paints above static in-flow siblings, so a
plain heading dropped next to the backdrop video vanished behind it.
TqScrollScene now always injects a backdrop rule pinning any direct
scroll-video child into the stage's negative z band (isolation on the stage
keeps it inside the scene), targeting a new data-scroll-video marker stamped
on the TqScrollVideo wrapper — scene children layer above the clip however
the video arrived. Mirrors the canvas renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…w (Next controller + HTML CSS)

- Scene/Motion widgets write translate/scale/rotate, never transform;
  scene stage gets perspective; overscroll-behavior-y none on pages with a
  scene and html background synced from body (no white band).
- Scene backdrop rule pins the scroll video under the chapters.
- Next: NextSnapIntoViewProjectPlugin emits components/tq-snap-into-view.js
  (proximity CSS + one rest-triggered settle, gentle/firm reach,
  nearest-wins, reduced-motion aware) and injects it into _app only when an
  element opts in.
- HTML: pluginSnapIntoView appends the native proximity rule to the global
  stylesheet (inline in each page when there is none), registered in
  packProject's HTML branch.
- e2e for both, absent-when-unused asserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ript)

- Next: NextScrollRailProjectPlugin emits components/tq-scroll-rail.js —
  rail CSS (item snapping gentle/firm, hidden scrollbar) and the wheel
  driving (one item per gesture on a snapping rail, free otherwise, hands
  back to the page at either edge) — injected into _app only when a rail
  opted in.
- HTML: pluginScrollRail appends the rail CSS to the global stylesheet and,
  when a rail asks for wheel driving, one inline script per page; shared
  global-css helper (appendGlobalCss / appendPageScript) also used by the
  Snap-into-view plugin; registered in packProject's HTML branch.
- e2e for both, absent-when-unused asserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ydration

- uidl-resolver: data-scroll-*/data-snap-*/data-chapter-* stay on the element
  that has a box instead of moving to the (display:contents) link wrapper —
  a linked chapter element ignored its scroll lanes.
- uidl-resolver: editor bookkeeping (data-scroll-bind-rel, data-chapter-window,
  data-chapter-template) is dropped from every generator's output.
- next: body custom code ships inert in <template data-tq-body-code> and the
  TqBodyCode _app sibling unpacks it after hydration (scripts recreated in
  document order; noscript stays live) — the slider bundle no longer rewrites
  slides before React hydrates, which reported every slide as a mismatch.
- next: custom-property element styles (--swiper-*) proven to pass through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er rule

Mirrors the editor's lane contract: clip-path insets for the reveal lanes,
a rounded --tq-count variable plus the [data-scroll-count]::before counter
rule for the count lane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
data-scene-hidden attribute + stage CSS rule (mirrors the editor's lane
contract) instead of inline pointer-events, so authored pointer-events
survive; e2e asserts the attribute ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…soft cap

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chapters stack inside the sticky stage, so a native hash jump to a
chapter's id lands at the track top, never at the chapter. The scene now
translates hash navigation (link clicks, hashchange, arriving with a
hash) into track progress — the midpoint of the chapter's story window
(data-chapter-window, else its lane stops) — and scrolls there smoothly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…geometry

chapterSnap="gentle": when scrolling pauses mid-scene, progress settles
smoothly on the nearest chapter's moment. Never intercepts an in-flight
scroll; reduced-motion visitors are never moved; a scrubbed background
video glides to the chapter's frame. Chapter-window helpers are now
shared between hash navigation and the snap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vladgrecu and others added 16 commits September 2, 2026 14:48
…ment

TqScrollScene aimed its gentle snap and anchor navigation at the
midpoint of a chapter's window — the last beat still rising. The
runtime now derives each chapter's SETTLED MOMENT (every lane arrived,
none leaving) from the root's and descendants' lanes, honours the
author's data-chapter-moment pin first, falls back to the window's
middle only for pure through-motion, includes chapters whose root has
no lanes, and settles in the direction the visitor was travelling.

settledMomentForLanes lives in scroll-scene-moment.ts as plain JS whose
source is injected into the generated runtime — the same function the
editor draws on its timeline (mirrored in teleport-gui).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Development locked the emitted-component roster at 18; this branch's
tq-scroll-scene and tq-scroll-video rows make it 20. Both pass the analyser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tions

- NextPageTransitionProjectPlugin wraps the page mount in _app with a
  generated TqPageTransition (AnimatePresence mode wait, frozen exit at the
  scroll offset, reduced-motion passthrough, first load never animates).
- Direction-aware: back button plays the reverse preset, Circle grows from
  the last pointer press; cover family paints overlay panels outside the
  keyed page.
- Skipped routes appear instantly (arrival-only); [param] segments match any
  value.
- Preset options resolved at generation time (defaults filled, unknown words
  replaced); cover colour is a project custom property, a literal colour, or
  the brand token chain — nothing else reaches the generated file.
- UIDL types + validator decoder carry globals.settings.pageTransition
  (preset, duration, easing, skipRoutes, options).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ions

- A `custom` preset carries the arriving-start and leaving-end states
  (opacity, x, y, scale, blur) in the UIDL; duration and easing travel in
  their existing fields. Values are clamped and the easing validated at
  generation time (mirrored from the editor), so a hand-edited file never
  breaks a build; the generated wrapper plays them through the same variants.
- UIDL types and the validator decoder accept `custom`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…xt listener

The published TqScrollScene tracks the chapter on stage (shared chapter
enumerator for pinned and un-pinned scenes, reduced-motion visitors
included), stamps data-chapter-active / data-chapter-count and dispatches
tq-chapter-reached for every chapter passed, in travel order, via a
microtask; replaced nodes keep the stamp, effect re-runs re-announce.
The workflow plugin emits a listener bound to the chapter root that
ignores nested scenes' events, honours once, and reads the stamp when it
attaches late. activeChapterIndex picks the latest moment passed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e, Next listeners

The published TqScrollScene announces every point (quarter, half,
three-quarters, end) the visitor scrolls DOWN past as a bubbling
`tq-scene-point-passed` event on its track, one microtask after the
progress change, in scroll order, and stamps the furthest point passed as
`data-scene-point` together with the dispatch so a listener attaching in
the same effect flush gets the event and a later one reads the stamp. The
first measure starts below the scene, so a page that opens already past a
point (anchor link, restored scroll) counts it. Going up announces nothing.

The workflows plugin gains two lifecycle triggers: `event-scene-scrolled-past`
(bound to the scene, resolved to its track, nested-scene bubbles ignored,
`once` detaches, late attach fires from the stamp) and
`event-page-scrolled-past` (window scroll progress over the scrollable
height, non-scrollable page = 1, rAF-throttled scroll + resize, checked on
attach, re-armed above the point). The scene trigger is pruned per page like
element-visible. `SCROLL_POINT_PROGRESS` is the one map both sides pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plays, pauses, play-or-pauses, restarts, mutes or unmutes the bound Video
element (or the first <video> inside the bound element). A play the
browser blocks for sound is retried muted and reported as `mutedFallback`;
blocked even muted reports `autoplay-blocked`; a missing video reports
`video-not-found` instead of throwing. Registered as client-only. Written
without object spread so the ES5 build never pulls a helper into the
handler that ships by toString.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n, full-width chapters

A scene's direct media or data-scene-backdrop child renders behind the
chapters and is excluded from chapter enumeration; ancestors with
overflow hidden are unclipped at mount so the pin holds; the motion wrapper
isolates its paint order; chapters take the full stage width.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ainer becomes the anchor

- reserveProjectStyleNames: the project style sheet is global, so a generated
  element class that equals one of its classes inherits it. A page named
  "Manifesto" keyed its root `manifesto-container` while another page had
  authored a `.manifesto-container` grid, and the whole page became that grid.
  The project's class names are reserved before keys are generated; a colliding
  element takes a numbered key through the existing counter.
- A container that is nothing but a link (no style, class, binding, event or
  runtime attribute) becomes the anchor itself instead of `<a><div>…</div></a>`.
  Imported markup writes `<a><img></a>` and stylesheets size the image by its
  place in the tree (`.surface-media > a > img`); the extra <div> broke that
  chain and the image fell back to its natural size. An element with a box of
  its own keeps the wrapper.
- scroll-video end-to-end test: the backdrop rule now lists several selectors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… link itself

- Motion in the static HTML export: scroll scenes, element motion (load, in
  view, stagger in place, hover, tap, scroll-linked), scroll video and page
  transitions (cross-document View Transitions: the back button plays the
  preset reversed, a circle grows from the press point, cover and curtain
  sweep the chosen colour, a page that opted out appears at once). One runtime
  file with only the parts a project uses; no JavaScript, a blocked runtime
  and reduced motion all fall back to readable content.
- One engine for both exports: the plain-JS parts of TqScrollScene, TqMotion,
  TqScrollVideo and the page-transition registry live in teleport-shared as
  source text. The emitted Next components are byte-identical.
- Links: a plain div that is a flex or grid item becomes the anchor itself.
  A display: contents wrapper cannot take keyboard focus in Chrome; on a
  generated store 124 of 124 links are now reachable (was 24), with no size
  change. Wrappers that remain carry data-thq-link-wrapper for the editor's
  focus-ring rule. Every link rule is pinned by a test.
- teleport-test/scripts: real-browser checks for the HTML export and its page
  transitions, a Next-versus-HTML comparison and a link-shape measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… fixed; reveals fit the screen

- On Visible: one settings reader and one observer for both generator paths.
  The editor stores the threshold as a percent and the run choice as
  triggerOnce; the generated IntersectionObserver threw on any typed threshold
  and "Trigger once" was never read. Percent -> fraction, once ?? triggerOnce,
  fires on becoming visible.
- HTML download: state conditions resolve against the page state's starting
  value (the shipped generator dropped every state-conditioned element), and
  EXPORT_PLAYBACK in teleport-types tells the editor what each export plays.
- A picture flies between pages, in both downloads: following a link that
  holds a picture to a page showing the same picture flies it onto its twin
  while the page plays its transition. Stylesheet and pairing helpers live in
  teleport-shared; Next plays that one change as a same-document View
  Transition. pageTransition.flyingPictures: false (UIDL type + validator)
  turns it off in both.
- Scroll video: the whole clip is held in memory on published sites, so a seek
  no longer waits on the network; "Play back to the start" maps the scroll to
  the clip there and back.
- Page transitions (Next): a reveal (Circle, the Wipes) is measured on the
  screen, not on the page. On a long page it covered the screen almost at once
  and the new page popped in; leaving a scrolled page it closed above the
  screen. Its clip never stays on the page, the first page included (a clip
  left there broke the fixed header and pinned scenes while scrolling); it runs
  frame by frame (no blink as it ends); the arriving page waits up to 300 ms for
  the pictures on its first screen.
- HTML download: an element's own styles weigh two classes (.name.name), as in
  the Next export and the editor, so its own colour beats a two-class
  combinator there too.
- teleport-test/scripts: Next browser checks (the flight, reveals on tall
  pages), one test site for both exports, a clip-seek measurement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eamed by the second

- Page transitions: Circle and the Wipes open the arriving page OVER the
  leaving page, which holds still underneath (view-transition-css.ts
  plan.revealsOver). Played one after the other, the leaving page was wiped
  away to the site's bare background first — a white sheet between every two
  pages. The HTML download keeps the old picture unanimated and plays the
  arrival with no delay (back: tq-page-arrive-back on the arriving picture
  only). The Next export plays every reveal navigation as a same-document
  View Transition where the browser has them (startViewChange, HoldPage, the
  arriving page pictured once its first-screen pictures are decoded); without
  them the two pages overlap (AnimatePresence "sync"), the arriving page laid
  above the frozen one until its animation completes. Pixel proof on a
  production copy: the share of the screen showing neither page fell from
  0.82 to 0.00.
- Scroll video engine (teleport-shared scroll-video-engine-source, one text
  for the Next export and the HTML download): holdClip reads the fragmented
  encode's index from its first 64 KB and, where MediaSource supports the
  codec, streams the seconds around the scroll position (streamClip: the
  second on screen first, then ahead, then behind, out to ±12 s); whole in
  memory where it cannot, as before. A 12 s clip is scrubbable after 315 ms
  instead of 1,712 ms, downloading 1.03× the clip instead of 1.25×.
- teleport-test html-motion scripts: verify-transitions (21 checks),
  verify-next-transitions (16), measure-scrub, README.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vladgrecu
vladgrecu marked this pull request as ready for review September 24, 2026 17:38
vladgrecu and others added 5 commits October 1, 2026 18:19
Two conflicts, both sides kept: the workflow component plugin imports
resolveScrollPoint and findFormStateKeyForHydration; teleport-shared exports
ScrollSceneRuntime, MotionRuntime, PageTransition and WebPush. Development's
link-prop-case test now expects the boxless link wrapper's marker attribute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unit glide on the scroll-linked path

- TqMotion (Next): the load trigger is driven by the "played" flag set on the
  first frame after mount (animate: played ? toVars : fromVars) instead of
  framer's initial -> animate pair. A page transition wraps every page in an
  AnimatePresence with initial={false}, which skips the mount animation of
  every motion component under it: an entrance never played on the first
  page a visitor opened, and a loop (repeat -1) never started and stood at
  its end state.
- Shared motion engine: a value with a unit (0% -> -50%, 2rem) is
  interpolated in that unit when both ends share it, as the canvas does.
- Tests: motion-end2end (the load flag), teleport-shared motion-engine (4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…trances are never left invisible; strict HTML keeps its runtime

- teleport-shared: storyLanes, clearMovement and letRowsSwipe in the scene
  engine, letMarqueeSwipe in the motion engine, run by the Next widgets and
  the HTML runtime alike.
- Next, TqMotion: the still element for less motion is drawn only once the
  page is mounted. The server renders the animated element with its
  starting style (opacity 0) and React keeps markup it disagrees with, so
  every entrance stayed invisible for those visitors (published since
  0.43.22). TqPageTransition reads less motion when a page change starts
  instead of rendering the bare page (React error 418 on every load).
- HTML: insertBeforeClosingTag (global-css.ts). With strict HTML whitespace
  Prettier writes the head's closing tag across two lines, so the motion
  runtime was never linked, the page-transition script never injected and
  the CSS fallback landed before the doctype. The runtime link, the
  page-transition script and both global-css helpers now find the tag in
  either form.
- Tests: story-lanes (5), motion-engine (+2), global-css (2, new), the HTML
  motion and page-transition strict-way tests, the Next motion and
  page-transition tests (one each, three assertions updated).
- Browser proof on a generated HTML and Next site, with and without
  reduced motion: 46 of 46 checks; 0 blocks left hidden, 0 hydration errors.

Full suite: Test Suites 514 passed, 514 total; Tests 1 skipped, 5930
passed, 5931 total; tslint 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y one small script

A downloaded HTML site now behaves like the Next export for state: a
phone menu opens, a price switch flips, tabs switch their panel and move
their active class. Every branch of a condition is in the page; the ones
a visitor does not start on carry `hidden`, so the first paint, a visitor
without JavaScript and search engines see the page as it first loads.

The generator writes readable bindings (`data-tq-if`, `data-tq-on-click`,
`data-tq-class`, and `data-tq-scope` / `data-tq-state` on the page root
and on each component instance, which keeps its own state) and ships
`tq-state.js` beside the stylesheet, linked only from pages that use it,
the way `tq-motion.js` is. The script reads the bindings with a small
parser and never evaluates code, so it runs under a strict
Content-Security-Policy. Click workflows that only set local state, such
as the editor's Tabs, compile to the same bindings. Shapes it cannot
express keep today's first-load rendering.

`EXPORT_PLAYBACK` now says HTML runs conditions, which lets the editor
keep them in the UIDL and drop its download warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts, both sides kept:
- next generator utils: the base's inline script error guard beside the
  branch's body code wrapper.
- uidl-resolver abilities: the base's data-source link pass beside the
  branch's box-less link wrapper mark.

One behaviour git could not see: the base's guard knows the project's own
scripts by the document's address, and the branch runs body custom code after
hydration, as scripts inserted later. Chromium reports no file for an error in
such a script, so the guard let it through (measured over http with the built
packages: 2 of 2 errors reached the app's own listener; WebKit names the
document and stopped both). The unpacker now names the document as the source
of every inline script it recreates; data blocks (JSON-LD, templates) and
external scripts stay as written. With it 0 of 2 reach the app in Chromium and
in WebKit.

Whole suite on the merged code: 526 suites, 6022 tests passed, 1 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ionutpasca
ionutpasca merged commit 2e2ca19 into development Oct 5, 2026
1 check passed
@ionutpasca
ionutpasca deleted the feat/scroll-scenes branch October 5, 2026 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants