UI polish: framed stage, typography, motion#28
Conversation
Spec for making the new-tab viewport feel intentional via three CSS-only layers (ambient glow, vignette, horizon line) without touching the carousel physics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Glow was sized for the old 800x500 popup. Make it viewport-relative (min(1400px, 90vw) by min(700px, 70vh)) and slightly stronger (violet alpha 0.13 -> 0.18) so it scales with screen size while staying soft. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a body::after radial gradient that fades from transparent at the center to rgba(2,2,7,0.55) at the corners, framing the carousel and making the surrounding void feel composed rather than empty. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the bounded left:0/right:0 shelf with width:100vw centered via translateX(-50%). The vertical anchor (bottom of .stage-wrap) is unchanged so card reflections still align. The existing fade gradient dissolves the line into the void on each side. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Confident type scale for the new-tab UI: bigger search bar (400x42 at 15px / weight 500) and a clearer label ladder (counter 13/600, hints 12/500, card title 14/600, domain 11). Contrast bumps on dim-text labels. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bump from 256x38 / 13px / 400 to 400x42 / 15px / 500. Search icon grows 15->18 with contrast 0.38->0.55 so it reads at a glance. Padding adjusted (0 22px 0 46px) and icon left offset to 16px so the icon is centered inside the new bar. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Counter 11->13 / 0.38->0.55. Hint 11->12 / weight 400->500 / 0.55. Key pill 10->11 / weight 600->700 / 0.50->0.65. The 16px hints gap absorbs the slightly taller pills without reflow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Card title 12->14 / weight 500->600 / inactive 0.75->0.82. Active card title stays full white. Domain 10->11 / 0.38->0.50. Existing text-overflow ellipsis handles the slight reduction in characters that fit in a 176px-wide card. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Detail 12->13 / 0.38->0.55 (height 18->20 to fit). Detail .hl 0.55->0.70. Empty state 13->14 / 0.38->0.55. Group name 13->14 (matches card title). Group count 10->11 / 0.38->0.50 (matches card domain). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diff-based animation for filter (keep survivors, animate leavers out and enterers in) plus cross-fade for group enter/exit. Reuses the existing .card CSS transition; only one new class hook. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pulls per-card creation out of buildCards into a standalone createCardElement(item) helper. Click and drag handlers now look up the live index via cardEls.indexOf(card) instead of capturing the loop index, so a card's position can change without re-creating the element. Behavior is identical for the existing wholesale-rebuild flow; this is a correctness fix for the upcoming diff-based filter path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure CSS hook for the upcoming filter-out animation. The fade-and-shrink visual is driven by inline opacity and transform in JS; this rule just prevents leaving cards from intercepting clicks or covering survivors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the wholesale rebuild on filter with a diff that keeps surviving DOM nodes, fades and shrinks leavers, and seeds enterers at scale(0.6) / opacity 0 so they animate up to their Cover Flow target via the existing .card transition. A leavingByKey map tracks cards still animating out so a fast backspace re-match cancels the leave and reuses the same DOM node — no duplicates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wraps enterGroup and exitGroup bodies in a crossFade helper that fades the cards container out (180ms), runs the wholesale swap, then fades back in. Group transitions no longer snap; total perceived duration matches the filter diff animation so the two feel like one family. groupIdx is captured before the closure runs because the swap clears activeGroup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Center-out per-card transition-delay for filter enterers and group transitions. Reuses the existing .card transition; adds two constants and a helper. Caps stagger at ±5 from active so heavy tab counts don't slow the reveal. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
STAGGER_MS=50 and STAGGER_CAP=5 control the per-card reveal delay. staggerDelayMs(index, activeIdx) computes a center-out delay or returns null for cards beyond the cap. clearAllTransitionDelays wipes residual delays so subsequent arrow-key navigation glides cleanly. No callers yet — wired up in following commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When stagger is true, in-window cards are seeded at opacity 0 and scale(0.6) with a per-card transition-delay derived from distance to active. updatePositions then runs animated (not instant) so the existing .card transition reveals each card after its delay. Cleanup fires at 700ms to clear residual delays. Default behavior unchanged — buildCards() still uses instant positioning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
enterGroup and exitGroup now build with stagger=true so the new view's cards fade in center-out from active rather than appearing all at once when the cross-fade settles. enterGroup pops position 0 first; exitGroup pops the previously-entered group card (groupIdx) first, neighbors cascade outward at 50ms steps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Enterers in applyFilterDiff get a per-card transition-delay derived from their final position in newCardEls (active becomes 0 after a filter, so the helper is called with activeIdx=0). Cards beyond the ±5 cap are not seeded and animate as before. Survivors and re-matches explicitly clear any residual transitionDelay so a stale value can't bleed across diffs. Leavers also clear transitionDelay before the fade-and-shrink runs. Cleanup at 700ms wipes any remaining delays so subsequent arrow-key navigation glides cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
📝 WalkthroughWalkthroughIntroduces a comprehensive polish pass on the TabFlow new-tab carousel with visual framing (ambient glow, vignette, viewport-wide horizon line), typography adjustments (search prominence, label readability), and motion design (diff-based filter animations with DOM retention, staggered center-out card reveals). Four design specifications document the implementation details alongside CSS, HTML, and JavaScript changes. Changes
Sequence DiagramsequenceDiagram
participant User
participant applyFilter
participant applyFilterDiff
participant DOM as DOM Elements
participant CSS as CSS Transitions
participant Cleanup as Cleanup Timer
User->>applyFilter: Filter input changed
applyFilter->>applyFilterDiff: Compute newFiltered items
applyFilterDiff->>DOM: Retain survivors (no DOM change)
applyFilterDiff->>DOM: Seed enterers with opacity:0, scale(0.6)
applyFilterDiff->>DOM: Assign transitionDelay to enterers
applyFilterDiff->>CSS: Force reflow, trigger updatePositions()
CSS->>DOM: Animate enterers fade-in, scale-up
applyFilterDiff->>DOM: Add is-leaving class to leavers
CSS->>DOM: Animate leavers fade-out, shrink
CSS->>Cleanup: Wait for animation duration
Cleanup->>DOM: Remove leaver elements from DOM
Cleanup->>DOM: Clear stale transitionDelay
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 4/5 reviews remaining, refill in 12 minutes. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
newtab.js (1)
688-697: Derive leaver cleanup from the actual transition timing.The leave path removes the node after
280ms, but.cardis still transitioningtransformandopacityfor400ms. That makes the exit timing fragile and can clip the fade/shrink if the CSS easing or duration changes. Prefertransitionendor a shared duration constant/custom property here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newtab.js` around lines 688 - 697, The removal timeout uses a hardcoded 280ms which can clip the CSS transition; change the cleanup in the oldByKey.forEach leave path to rely on the card's actual transition timing by listening for the transitionend event on the element added in card.classList.add('is-leaving') (and only act when the transitioned property is opacity or transform), then remove the node and call leavingByKey.delete(key) in that handler; also add a safe fallback by computing the computedStyle transitionDuration/transitionDelay (or reading a shared CSS custom property) and using that calculated timeout if transitionend doesn't fire.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/superpowers/specs/2026-04-28-framed-stage-design.md`:
- Around line 44-45: The spec text incorrectly instructs moving the .shelf
element out of .stage-wrap; update the sentence to match the PR: state that
.shelf remains in the HTML inside .stage-wrap but is made viewport-wide via CSS
(e.g., positioned/fitted to span left→right in the viewport) rather than being
relocated in the DOM, and keep the existing fade-out gradient stops (transparent
→ violet → transparent) and the vertical positioning guidance (match the
on-screen y where the bounded shelf sits). Reference the .shelf and .stage-wrap
selectors when making this edit.
In `@newtab.css`:
- Around line 99-109: The font-family declaration inside the .search rule uses
quoted family name 'Syne' which trips stylelint; update the .search CSS rule's
font-family property to use Syne without quotes (font-family: Syne, sans-serif)
so the unquoted family name is lint-compliant and keeps the fallback intact.
In `@newtab.js`:
- Around line 248-250: Before wiping the DOM in buildCards({stagger}), cancel
and clear any pending "leaver" state: iterate leavingByKey (the map/object used
to track pending removals), call clearTimeout on each stored timer, remove any
references to detached nodes, then clear leavingByKey, and only after that set
cardsEl.innerHTML = '' and reset cardEls = []; this ensures timers won't later
try to restore nodes that have been removed and avoids resurrecting detached
elements in applyFilterDiff().
---
Nitpick comments:
In `@newtab.js`:
- Around line 688-697: The removal timeout uses a hardcoded 280ms which can clip
the CSS transition; change the cleanup in the oldByKey.forEach leave path to
rely on the card's actual transition timing by listening for the transitionend
event on the element added in card.classList.add('is-leaving') (and only act
when the transitioned property is opacity or transform), then remove the node
and call leavingByKey.delete(key) in that handler; also add a safe fallback by
computing the computedStyle transitionDuration/transitionDelay (or reading a
shared CSS custom property) and using that calculated timeout if transitionend
doesn't fire.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 80c35767-cf5c-424e-9af3-2bb2ac51dc72
📒 Files selected for processing (7)
docs/superpowers/specs/2026-04-28-filter-animation-design.mddocs/superpowers/specs/2026-04-28-framed-stage-design.mddocs/superpowers/specs/2026-04-28-stagger-animation-design.mddocs/superpowers/specs/2026-04-28-typography-pass-design.mdnewtab.cssnewtab.htmlnewtab.js
| - Move `.shelf` out of `.stage-wrap` in the HTML and reposition it as a fixed element spanning `left: 0; right: 0`. Vertical position: match the on-screen y where the bounded shelf currently sits (the bottom edge of `.stage-wrap`, which in the current centered layout is approximately the vertical midline of the viewport plus ~136px). Use `top: 50%` plus a fixed offset, or measure once and hardcode — whichever is cleaner once implementing. | ||
| - Keep the existing fade-out gradient stops (transparent → violet → transparent) so the line dies into the void naturally on wide screens. |
There was a problem hiding this comment.
Update the shelf step to match the implementation.
This still says .shelf should move out of .stage-wrap and become fixed, but the PR keeps the HTML unchanged and makes the shelf viewport-wide in CSS instead. Please align the spec so future work does not chase the wrong structure.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/superpowers/specs/2026-04-28-framed-stage-design.md` around lines 44 -
45, The spec text incorrectly instructs moving the .shelf element out of
.stage-wrap; update the sentence to match the PR: state that .shelf remains in
the HTML inside .stage-wrap but is made viewport-wide via CSS (e.g.,
positioned/fitted to span left→right in the viewport) rather than being
relocated in the DOM, and keep the existing fade-out gradient stops (transparent
→ violet → transparent) and the vertical positioning guidance (match the
on-screen y where the bounded shelf sits). Reference the .shelf and .stage-wrap
selectors when making this edit.
| .search { | ||
| width: 256px; | ||
| height: 38px; | ||
| padding: 0 18px 0 40px; | ||
| width: 400px; | ||
| height: 42px; | ||
| padding: 0 22px 0 46px; | ||
| background: rgba(255, 255, 255, 0.04); | ||
| border: 1px solid rgba(255, 255, 255, 0.09); | ||
| border-radius: 20px; | ||
| color: var(--text); | ||
| font-family: 'Syne', sans-serif; | ||
| font-size: 13px; | ||
| font-weight: 400; | ||
| font-size: 15px; | ||
| font-weight: 500; |
There was a problem hiding this comment.
Remove the quotes around Syne in this hunk.
Stylelint is flagging font-family-name-quotes here, so this block will keep failing lint until the family name is unquoted.
Suggested fix
- font-family: 'Syne', sans-serif;
+ font-family: Syne, sans-serif;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .search { | |
| width: 256px; | |
| height: 38px; | |
| padding: 0 18px 0 40px; | |
| width: 400px; | |
| height: 42px; | |
| padding: 0 22px 0 46px; | |
| background: rgba(255, 255, 255, 0.04); | |
| border: 1px solid rgba(255, 255, 255, 0.09); | |
| border-radius: 20px; | |
| color: var(--text); | |
| font-family: 'Syne', sans-serif; | |
| font-size: 13px; | |
| font-weight: 400; | |
| font-size: 15px; | |
| font-weight: 500; | |
| .search { | |
| width: 400px; | |
| height: 42px; | |
| padding: 0 22px 0 46px; | |
| background: rgba(255, 255, 255, 0.04); | |
| border: 1px solid rgba(255, 255, 255, 0.09); | |
| border-radius: 20px; | |
| color: var(--text); | |
| font-family: Syne, sans-serif; | |
| font-size: 15px; | |
| font-weight: 500; |
🧰 Tools
🪛 Stylelint (17.9.0)
[error] 107-107: Expected no quotes around "Syne" (font-family-name-quotes)
(font-family-name-quotes)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newtab.css` around lines 99 - 109, The font-family declaration inside the
.search rule uses quoted family name 'Syne' which trips stylelint; update the
.search CSS rule's font-family property to use Syne without quotes (font-family:
Syne, sans-serif) so the unquoted family name is lint-compliant and keeps the
fallback intact.
| function buildCards({ stagger = false } = {}) { | ||
| cardsEl.innerHTML = ''; | ||
| cardEls = []; |
There was a problem hiding this comment.
Clear pending leaver state before a wholesale rebuild.
buildCards() blows away the DOM and resets cardEls, but any entries already parked in leavingByKey keep their timers and detached nodes. If a re-match lands before those timers fire, applyFilterDiff() can restore a card that is no longer attached and never re-append it. Cancel and clear leavingByKey before cardsEl.innerHTML = ''.
Suggested fix
+function clearLeavingCards() {
+ leavingByKey.forEach(({ timer }) => clearTimeout(timer));
+ leavingByKey.clear();
+}
+
function buildCards({ stagger = false } = {}) {
+ clearLeavingCards();
cardsEl.innerHTML = '';
cardEls = [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@newtab.js` around lines 248 - 250, Before wiping the DOM in
buildCards({stagger}), cancel and clear any pending "leaver" state: iterate
leavingByKey (the map/object used to track pending removals), call clearTimeout
on each stored timer, remove any references to detached nodes, then clear
leavingByKey, and only after that set cardsEl.innerHTML = '' and reset cardEls =
[]; this ensures timers won't later try to restore nodes that have been removed
and avoids resurrecting detached elements in applyFilterDiff().
Closes #27
Summary
Polish pass on the TabFlow new-tab carousel — framed stage, typography scale, and motion design (diff-based filter transitions + center-out staggered card reveal). 17 commits across four feature areas; no changes to carousel physics or core interaction.
What changed
Framed stage (3 commits)
b37dbd8ambient stage glow scaled to viewport5c17935edge vignette to frame the stageab60e30shelf extended into a viewport-wide horizonTypography pass (4 commits + spec)
e6741a3design speca2c4c50search bar promoted as visual entry pointa8a9a69footer counter and hint label boosted6acc57dcard title/domain scaled up for at-a-glance readingf6e84e0unified detail/empty/group text scaleFilter & group transition animations (4 commits + spec)
600a92adesign spec299ff07refactor: extractcreateCardElement, switch handlers to live-index lookupf28e602.card.is-leavingCSS hookf626ac0applyFilterDiff— survivors slide, leavers fade-and-shrink, enterers fade-and-scale-upbcfd6c0cross-fade carousel container on group enter/exitStaggered card reveal (4 commits + spec)
021997ddesign specb261aa7STAGGER_MS=50,STAGGER_CAP=5,staggerDelayMs(i, active)helper574da28buildCards({ stagger: true })opt-in mode (default unchanged → first-paint stays instant)26d91c0group enter/exit triggers staggered reveal961becbfilter enterers stagger center-out from activeMechanism notes
transition-delayon top of the existing.card { transition: transform 0.4s, opacity 0.4s }rule. No new keyframes.transition-delayafter a staggered reveal so subsequent arrow-key navigation doesn't inherit the delay.leavingByKeyMap tracks in-flight leavers so a fast re-match cancels the leave.Non-goals
POSITIONS, reflection math, drag-to-close, undo toast, keyboard shortcuts, or carousel physicsbuildCards()behavior preserved)Test plan
↑from inside a group → cross-fade out, then center-out reveal from previously-entered slotgifast then backspace tog→ leaving cards snap back without stagger delay← →after a backspace reveal → smooth, no inherited delay← →glide,Enteropen-flash,/focuses search, drag-to-close,Escapeclose-tab arcVisual verification confirmed by maintainer (
LGTM).Summary by CodeRabbit
Release Notes
New Features
Visual Improvements