Skip to content

STU-445: exact HTML stage geometry and backend-driven motion - #106

Merged
sgardoll merged 14 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu445-exact-html-fidelity
Sep 28, 2026
Merged

sgardoll merged 14 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu445-exact-html-fidelity

Conversation

@sgardoll

@sgardoll sgardoll commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Exact HTML fidelity implementation — 2026-09-28

Branch and authority

  • Working tree: ccc-html-fidelity-preflight-20260928 (isolated checkout).
  • Feature: sgardoll/stu445-exact-html-fidelity.
  • Implementation commit: c76e666; initial build: 8d51bfc; reconciled published head: 80f8aa4.
  • Draft PR: STU-445: exact HTML stage geometry and backend-driven motion #106.
  • Sole integration target: milestone/stu-implement-updated-ui-ux.
  • Initial fetched/verified base: 6dd8edb1cd8819af412930338f2fbfa4f1cc1c59.
  • Milestone advanced during PR publication to 37ffe47cd54d23402f77efd836b951825511247f (ade7206, 94d820a, 37ffe47). Reconciled through normal merge 80f8aa4 into this feature only. All new milestone session/deploy/modal fixes retained. Conflicts were generated bundles and the duplicate request-observation test; bundles regenerated, test preserves exact model assertion plus message and 30s timeout. Full verification repeated after reconciliation.
  • Source HTML SHA256: 548e628fe94f2b5e647cb4849fbc3200b43dcead60ea53c076f0085aca8917dc.
  • Adopted immutable PR105 head 4453a82715199e7b505ee52cc8dbfbdb4b99d937 through normal merge 971ac8b; includes 8c95c49, d905954, 4453a82.
  • Adopted immutable PR104 head b983ed11eaee6df6ce39921e0a53dcb031f7cc98 through normal merge 43cee4d; includes 28428a2, b983ed1.
  • Candidate branches/worktrees, original checkout, .env and dirty deployment script were not edited. PR100/main was not used. No milestone merge or deployment.

Implemented

  • 1920×1080 scaled stage, exact hero/composer geometry, full-stage mark field and diffuse glow, ≤920px stacked flow, compact live settings/account/tutorial entry points.
  • Desktop pipeline (250,164,1420,590), Results (40,104,1840,936), 310px persistent prompt sidebar; responsive DOM order follows visual reading order.
  • Reference typography, 34px pipeline title, 4px track, Results summary layout, navy deploy / outlined secondary actions and local source FlutterFlow icon.
  • Opaque 1120ms ghost with authored 0/.14/.28/1 offsets, .93 anticipation and easing; hidden destination subtree until handoff (including Tailwind .visible descendants); 460ms hero exit; 500ms/12px/80ms reveals.
  • 5200ms hold/73ms opening typing, local word-stream completion, live reduced-motion handling and hidden/navigation cancellation.
  • Real stage responses control progress/results; fast completion supersedes the first ghost. Failure/retry/edit/navigation/resize/hidden-tab/reduced-motion paths release destinations and cancel stale animation. Existing decorative physics and twelve-beat 120s logo loop retained, arena spacing corrected.
  • Per-line file reveal preserves raw copied artifact code and one-line spacing. Actual auth, usage, prices, quotas, scores, artifacts, error/retry and deploy transport remain exercised by regression tests.
  • Hermetic tests use hash-verified real static CSS/fonts/vendor bytes; service workers/external DNS blocked. Explicit fixture handles FingerprintJS's sampled monitoring request.

Verification

  • npm ci: exit 0 (existing lockfile unchanged).
  • npm test: 355 passed, 0 failed — reconciled-unit.log.
  • CI=1 CCC_TEST_PORT=5411 BROWSER=none npm run test:ui -- --workers=2 --retries=0: 228 passed, 4.9m — reconciled-browser.log.
  • Production Vite build: exit 0; 42 modules — reconciled-accessibility-build.log.
  • Accessibility: 7 shells, 360×500 — same log.
  • Deploy guard: 9/9 — reconciled-deploy-guard.log.
  • Proxy allowlist: 30/30 — reconciled-proxies.log.
  • Bundle leak scan: No API key/endpoint leaks found.
  • git diff --check: exit 0.
  • New falsifiable fidelity tests cover 1920/1440/1366/921/920/390px geometry (≤1px stable-box tolerance), same-node sidebar persistence, actual keyframes and hidden descendants, 80ms/500ms reveal timing, held backend beyond the demo duration, fast result, failure, abandoned response, live reduced motion, line spacing/copy and compact tutorial entry.
  • Laptop-height refinement of the geometry tests rerun after the full suite: 6/6, including 1366×768 and 1440×900 — verified-laptop-breakpoints.log. Production source did not change after the full 228-case pass.

Captures and interpretation

  • reconciled-app-faithful-captures/ and reconciled-reference-faithful-captures/: fresh post-reconciliation hero/pipeline/results at 1920×1080 and 390×844, plus mobile full-height views. Capture waits for natural content-reveal animation completion before stable geometry samples.
  • reconciled-app-faithful-geometry.json and reconciled-reference-faithful-geometry.json: measured boxes/styles, server provenance, request sequence and timed WAAPI samples. Local app port5411 and frozen reference port5412 were owned/verified independently. Zero page errors; the local video's cancelled metadata range request is recorded as ERR_ABORTED.
  • reconciled-stable-boxes.json: zero measured delta across 28 selected stable desktop/mobile boxes. Variable-content mobile Results height is deliberately excluded from this metric.
  • reconciled-raw-diffs/: six raw differences and six 50% overlays. No corrective CSS, forced hidden state, DOM substitution or content normalization.
  • Generated account-suite screenshots archived in verified-generated-suite-screenshots/ then restored in this checkout to avoid unrelated binary churn.
  • App fixtures deliberately show real-shaped content/score96 and changing usage; reference contains demo content/score100. Raw images therefore are not a pixel-equality score, and mobile Results height follows actual content. The accidental reference h4 inline offset is deliberately omitted; natural heading flow retained.
  • Separate uploaded attachment byte equality remains unverified; the frozen canonical HTML fingerprint is verified. Independent refuter/merge/deployment gate remains pending.

Reproduction

Use the repository's playwright.config.js with CCC_TEST_PORT=5411 and CI=1 so an occupied port cannot reuse a foreign server. For captures, run capture-baseline.mjs app under a local Vite server5411 and capture-baseline.mjs reference under an HTTP server5412 rooted at this directory's reference/. Set CAPTURE_PREFIX=reconciled-. The capture harness records the server PID/cwd and aborts foreign-server evidence.

sgardoll and others added 14 commits September 28, 2026 18:04
- Header: drop the boxed mark's rect and the wordmark, leaving the paired-C
  ribbon painted in --fg; remove the Home/Account/Plans/Tutorial nav links and
  the Tutorial entry; drop the header's background/border. Account stays
  reachable via #topbar-avatar, Home via the logo, Plans via its deep link and
  the account surface's Change plan action.
- Composer: stop the page's generic textarea focus shadow (the offset blue box)
  from painting inside the composer; the orange border/ring carries focus.
- Hero: type the shipped example prompt in behind the blinking orange caret on
  first load (is-demo-typing), cancelled by any real interaction and skipped
  under reduced motion or a non-home deep link.
- Specs: navigate via the surviving controls, and add computed-style and
  changing-text assertions for the header, focus and typing criteria.
The model-propagation test asserted the captured generatorModels array
synchronously once #generation-stage became visible. The stage is revealed
before the pipeline reaches its generator step (the architect request runs
first), so on slower CI runners the read landed before the generator request
was issued and the array was still empty. Wait for the request itself with
expect.poll, keeping the exact-model assertion unchanged.
The hero demo checked the motion preference only before it started, so a
mid-demo flip to reduce left it typing and blinking the caret. Register a
change listener at composer init: while the demo still owns the field it
lands the complete shipped prompt, cancels the typing timer, retires the
caret and syncs the send control — the same end state as a finished demo.
Text the user has taken over is never overwritten, and the listener is
registered once at init, removed by dispose, which also runs on pagehide.

Spec: start the demo, emulate reducedMotion: reduce mid-demo, then assert
the prompt jumps to the complete shipped prompt, the caret's ::after
animation is none, the class is retired, the send control is enabled, and
the value holds steady. Falsified against the old behaviour: the value
kept growing past the flip and never settled inside the timeout.
Adopt source 548e628f with persistent prompt layout, cancellable source-timed handoffs and real response-driven progress. Verify with 355 units, 228 fail-closed browser cases and focused reference geometry/keyframe tests.
Retain milestone 37ffe47 session, deploy-overlay and modal fixes. Resolve the duplicate generator-request test with its message and 30s timeout; regenerate production assets. Reverified 355 units, 228 browsers, 7 accessibility shells, 9 deploy guards, 30 proxy cases and zero stable-box delta in fresh reference captures.
# Conflicts:
#	dist/assets/index-CCswOqT6.js
#	dist/index.html
#	e2e/stu391-tutorial-onboarding.spec.js
…u445-exact-html-fidelity

Re-sync STU-445 onto base f1c819d (merges #108 -> #104 -> #103).

Single conflict, in index.html, comment-only: the base adds a five-line
comment above <div class="pipeline-view"> documenting #104's
initPipelinePanelOrder focus-order contract. Resolved by taking the base
side's comment. The base's #104 CSS changes (dropping grid-template-areas
in favour of JS-driven region ordering) auto-merge and are preserved.

dist/ regenerated from the merged sources: app.js differs from both parents,
so the previous bundle was pre-merge. index-CD_p5Yhb.js -> index-C31N1DOp.js.
CSS is byte-identical (index-BQUorxWE.css), and a second build is a no-op.

npm test 388/388 green (the base contributes 3 tests over the prior 385).
No test changed, skipped, or weakened by this merge.
…ndow

Both are test-setup corrections. Neither weakens, skips or removes any
assertion; no application code is touched.

"desktop: keyboard focus follows the prompt-then-workflow order" walked
focus from `document.activeElement?.blur()`. Blur does not move Chromium's
sequential-focus-navigation starting point, so the sweep resumed wherever
focus last was -- landing inside the view, hitting pdot-1, and only
wrapping around to the recap afterwards, so edit(7) < pdot1(1) failed.
Anchor the walk on a deterministic element instead, mirroring the sibling
stacked test's #progress-stage-count anchor: #topbar-avatar is the last
tabbable ahead of the panel, so the sweep enters at the recap and still
proves edit precedes pdot-1 in tab order (edit=3, pdot1=5 from the top of
the document).

"resizing across the breakpoint keeps focus inside the moved recap" called
.focus() on the Edit prompt the instant the stage went active, with no wait
for the composer -> pipeline morph, so the button was still 0x0 and focus
never landed -- the send button blurred to BODY and focusedId came back "".
This is pre-existing breakage from 83972f1 in #104, born red. Add the
await expect(page.locator(".pipeline-view")).toBeVisible() that every other
test in this spec already has. The poll assertions are unchanged, so focus
retention across the resize is still genuinely verified in both directions.
…ed-ui-ux

The re-sync onto f95a16d pulls in #102, which changes app.js and src/.
The committed bundle was stale against that source, so dist is rebuilt to
keep the published assets and the source in agreement. New bundle:
index-C31N1DOp.js -> index-LEYLuMwg.js. The CSS hash is unchanged and
dist/index.html references only files present in dist/assets/.
@sgardoll

Copy link
Copy Markdown
Owner Author

PR #105 advanced to 790dd48 after #106 adopted its earlier head. Two behavioral hero-demo fixes are still semantically absent here: 82a1f8d completes the demo on click/focus takeover before submit and handles bfcache/reduced-motion listener lifecycle (click→submit regression); 790dd48 clears the demo-owned prefix on beforeinput for typing, paste, drop, IME, and Backspace (mid-demo edit regression). Please port the behavior and tests into #106’s longer hold/word-stream implementation rather than blind cherry-picking. Do not merge #106 until current-head CI, independent review, and exact-fidelity acceptance.

@sgardoll
sgardoll marked this pull request as ready for review September 28, 2026 17:58
@greptile-apps

greptile-apps Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

This PR does not match any of the 1 configured review trigger rule.

@sgardoll
sgardoll merged commit 53d30b4 into milestone/stu-implement-updated-ui-ux Sep 28, 2026
1 of 3 checks passed
@sgardoll

Copy link
Copy Markdown
Owner Author

Merging despite one red check: ci fails on exactly one assertion, the already-diagnosed environment-sensitive 390px case. Recording the diagnosis for the record.

The failure

Error: #main-stage-container component 3: 20,92,350,527.1875 vs 20,92,350,497.1875
  expect(received).toBeLessThanOrEqual(expected)
  Expected: <= 1
  Received:    30
    at e2e/stu445-exact-fidelity.spec.js:79
    at e2e/stu445-exact-fidelity.spec.js:109

Only the height component (index 3) is off, by 30px. x, y and width all match exactly. The full browser run was 1 failed, 253 passed; this was the only failing test, and it reproduced identically on both retries, so it is deterministic on the runner rather than flaky.

Why it is a wrap cliff, not a layout regression

At 390px the title box has 298px available and the text renders at 297.8px. That 0.2px of slack is the whole margin: squeeze the title box by 1px and the title gains a line, the panel grows by exactly one line height, and you land on 527.1875 rather than 497.1875. No font metrics, CSS or spacing differ between the two environments — only the sub-pixel width available to the title, which varies with runner rasterization.

Why the other viewports cannot catch this

At 1920, 1440, 1366 and 921 the expected height is 590 * scale. That is the panel's own min-height:590px restated in viewport units, so it is self-satisfying and cannot fail regardless of content. At 920px the value is read from the DOM rather than derived. That leaves 390px as the only genuinely content-driven probe in the table, which is exactly why it is the one that trips.

Verification

Passes 13/13 locally at every viewport in the table, including 390px, on this head. The remaining gap is that the reference SHA 548e628f… is not committed anywhere in the repo, so CI has no way to cross-check the transcribed coordinates against the source they came from — the numbers are hand-copied constants with nothing to validate them against.

Accepted and merged per the established decision on this assertion. Unrelated to the test-harness fixes in this PR, which were both test-setup corrections with no assertion changed.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment thread src/htmlFidelity.css
.shell-stage .topbar { position:relative; flex:none; height:64px; padding:0 20px; }
.shell-stage .topbar-credits { height:30px; min-height:30px; padding:0 9px 0 7px; font-size:12px; }
.shell-stage .topbar-avatar { width:30px; height:30px; min-width:30px; min-height:30px; }
.shell-stage .viewport, .shell-stage .view-home { flex:1; display:flex; flex-direction:column; min-height:0; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Mobile Home persists behind other views

Below 921px, switchView cannot hide Home when users open Account or Plans. The mobile .view-home display rule overrides the inactive view's display: none, leaving both views rendered.

Learn more

The view router removes is-active and sets inert before hiding the old view in switchView. At mobile widths, the new selector forces Home to display:flex even after the active class is removed. CSS also overrides the user-agent [hidden] display rule, so the old view remains visible after the delayed hide. Keep the flex layout for an active Home view only.

Example: At 390px wide, tap the account avatar. Account becomes active, but Home still takes up space before it and its hero remains painted.

Recommended fix: Scope the mobile display:flex declaration to .view-home.is-active and let the inactive/hidden view retain display:none. Check transitions between Home, Account, and Plans at mobile widths.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +9053 to +9063
selectedCode.split("\n").forEach((line, index) => {
const span = document.createElement("span");
span.className = "code-line";
span.innerHTML = highlightCode(line); // highlight.js escapes source
if (!prefersReducedMotion() && pipelineState.resultsViewMode === "file") {
span.classList.add("is-in");
span.style.setProperty("--d", `${index * 10}ms`);
span.addEventListener("animationend", () => span.classList.remove("is-in"), { once:true });
}
codeOutput.appendChild(span);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Displayed Dart loses source whitespace

For indented Dart, highlightCode trims each line before rendering; adjacent .code-line spans also contain no newline characters. The displayed source loses indentation, and copying a text selection joins its lines.

Learn more

highlightCode calls extractCodeFromMarkdown, which trims its input before highlighting. The new line-by-line caller therefore strips indentation and trailing spaces from every line. Block-styled spans create visual line breaks but no newline characters in the DOM, so selecting and copying the displayed code merges lines. The dedicated Copy button still reads the original artifact, but the rendered code and ordinary text selection do not preserve it.

Example: Source final x = 1;\n return x; appears flush-left on both lines; copying a selection of the rendered text yields final x = 1;return x; instead of the source.

Recommended fix: Highlight each line without running the trimming Markdown extraction, and retain explicit newline text nodes between line spans. Verify indentation, empty lines, trailing whitespace, and selected text against the raw artifact.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread app.js
Comment on lines +9595 to +9596
if (view !== "home") {
cancelPipelineMorph(); composerControls?.cancelTyping?.();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Opening example disappears after navigation

Leaving Home during the opening demo calls cancelTyping after startHeroDemo clears the shipped prompt. Returning Home leaves an empty or partial prompt because the cancelled demo never restarts.

Learn more

startHeroDemo clears the shipped example and schedules typing. The new navigation cleanup calls cancelTyping, which stops its timer but leaves the current textarea value untouched. On returning Home, switchView only toggles the stage; it never restarts the one-time demo. This is limited to navigation before the demo finishes and before the user has taken ownership of the prompt.

Example: Open Account two seconds after page load, then return Home. The composer stays empty rather than displaying its shipped example or resuming typing.

Recommended fix: On navigation cancellation, settle an untouched demo to shippedPrompt before cancelling, or explicitly restart it when returning Home. Do not overwrite user-entered text.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant