feat(aesthetic): look-and-feel uplift on the live render path - #116
Merged
Conversation
Make generated UI read as genuinely distinct per aesthetic the moment it appears — not "noir with different colors" — by wiring per-preset identity that was already defined but never reached the live SurfaceRenderer. - ChildList: per-preset entrance physics (cinematic/glitch/terminal/candle/ crisp) via entranceHiddenVariant, instead of one shared fade-up. The reduced-motion path (plain, un-animated children) is untouched. - TextRenderer: tag generated h1–h5 with `a2ui-heading`, the hook for a per-preset editorial treatment (letter-spacing + weight) in globals.css. The loaded face is deliberately NOT swapped — noir's typewriter headers are its identity; only spacing/weight diverge. - Cards: material-aware top border + a subtle directional gradient over each material's fill, plus an accent glow halo scaled by --aesthetic-glow-strength (paper/parchment). Minimal gets a faint anchor (accent hairline + layered shadow), staying clean by design. - Text glow: noir (amber) + gothic (blood-candle) headings/accent text, scaled by glow-strength; minimal none. Light paper/parchment cards reset text-shadow on all descendants so dark ink stays crisp. - Controls: Button/TextField/Table corner radius now reads --aesthetic-radius (badge stays rounded-full; card base unchanged). +5 renderer tests (heading hook, noir font NOT swapped, control radius var, card material attr). Full gate green: tsc/eslint/stylelint/prettier, 1198 vitest, next build. Adversarially reviewed (4 angles); the one low-severity finding — incomplete text-shadow reset scope — is fixed here.
The +5 tests were added after the prettier --write pass, so two makeSurface calls weren't wrapped to the print width. CI's prettier --check flagged them. Formatting-only; no logic change.
…dren) A "generate 3 takes" run could leave take 3 partially blank: the top-level hr/figure-row/warning children stayed frozen at opacity:0 with the gothic candle hidden transform, while children 1–2 and all nested content rendered fine. Cause: ChildList revealed children via a parent `staggerChildren` orchestrator (a `display: contents` motion.div propagating the show label down the variant cascade). Take 3 is shown the instant generation finishes — while its <img> is still loading. When the image resolves, the subtree re-renders mid-cascade, the orchestration resets, and the siblings that hadn't been cued yet are stranded at their hidden opacity:0 forever. Takes 1/2 escape only because they're viewed later when nothing is re-rendering. Fix: drop the parent conductor; each child animates itself with its own initial→animate + a per-index delay reproducing the stagger. `animate` is a fixed target framer-motion settles on and never reverts from across re-renders/remounts, so no mid-flight re-render can leave a sibling behind. The per-preset entrance physics and reduced-motion path are unchanged; children remain the parent flex/grid's direct items (as with the old display:contents). +2 regression tests: no display:contents orchestrator is rendered, and every child gets its own animated wrapper. Verified the first fails if the orchestrator is reintroduced. Gate green: tsc/eslint/prettier, 1200 vitest, build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Generated UI currently collapses toward sameness the moment it renders — one shared heading treatment, one flat card border + shadow, one identical fade-up entrance — even though each aesthetic defines its own font, glow strength, radius, and motion personality. This PR wires that already-defined per-preset identity into the live
SurfaceRendererso a noir dossier, a cyber hologram, a nostromo terminal, a gothic manuscript, and a minimal sheet read as genuinely different objects on screen.This is the "look-and-feel uplift" flagship from a multi-agent visual audit (56 ideas → ground-checked against real code). Everything here touches the live render path and reaches all 5 presets.
Changes
ChildListnow deriveschildVariants.hiddenfromentranceHiddenVariant(motionPersonality.entrance): cinematic (noir, drifts up+in), glitch (cyber, snaps from a shrunk scale), terminal (nostromo, prints down), candle (gothic, swells from small scale), crisp (minimal, short hop). The reduced-motion branch (plain children, no motion) is unchanged.h1–h5get ana2ui-headinghook; globals.css applies per-preset letter-spacing + weight. The loaded font is deliberately not swapped — noir's typewriter headers are its identity (see note below).data-effect-cardmaterial gains a directional gradient over its fill, a material-aware top border, and (paper/parchment) an accent glow halo scaled by--aesthetic-glow-strength. Minimal gets a faint anchor (accent hairline + layered shadow), staying clean by design.text-shadowon all descendants so dark ink stays crisp.--aesthetic-radius(badge staysrounded-full; card base class unchanged so the existingborder-t-2test holds).A note on the "wire the heading font" idea
The audit's #1 suggestion was to swap headings to each preset's
fonts.heading. On inspection that's a noir regression: fonts resolve via CSS vars (font-typewriter→--aesthetic-font-body), 4/5 presets already share body+heading family, and the only one that would change is noir — swapping its signature typewriter headers for sans. So I delivered the "editorial swing" via spacing/weight treatment instead.Testing
tsc0,eslint0,stylelint0,prettierclean, 1198 vitest pass,next buildOK.Decisions baked in (per request)
Minimal → faint anchor (not bare-flat) · type voice → full editorial swing · button tap sound → out of scope (opt-in, separate track).
Branched off
main, independent of the open PRs #114/#115.