Skip to content

perf(stackflow): replace CSS animation with WAAPI for AppScreen transitions#1444

Open
junghyeonsu wants to merge 22 commits intodevfrom
feature/des-1079-waapi
Open

perf(stackflow): replace CSS animation with WAAPI for AppScreen transitions#1444
junghyeonsu wants to merge 22 commits intodevfrom
feature/des-1079-waapi

Conversation

@junghyeonsu
Copy link
Copy Markdown
Contributor

@junghyeonsu junghyeonsu commented Apr 9, 2026

Summary

  • Migrate all AppScreen transition animations (push, pop, swipe back completing/canceling) from CSS recipe-based seed-enter/seed-exit keyframes to Web Animations API (WAAPI)
  • Remove all transition animation selectors (push, pop, idle, swipeBack*) from app-screen.ts and app-bar.ts CSS recipes — static layout/color/z-index styles are preserved
  • Add transition-animation.ts with WAAPI implementations for all three transition styles (slideFromRightIOS, fadeFromBottomAndroid, fadeIn)
  • Rewrite useGlobalInteraction to detect stackflow transitionState changes and trigger WAAPI, replacing CSS variable cascade with explicit inline style management
  • Add rAF lock pattern in useSwipeBack for throttled touch event processing

Why

The previous CSS animation ↔ inline style handoff during swipe-back caused flicker, double-animation on exit-done, and AppBar desync bugs. Unifying all transitions under a single JS animation system eliminates the handoff entirely.

Test plan

  • bun test:all — 596 pass, 0 fail
  • Manual test in examples/stackflow-spa: push, pop, swipe complete, swipe cancel
  • Verify AppBar title/icon/background animate in sync during all transitions
  • Verify no flicker or double-animation on exit-done
  • Test rapid push → pop sequences

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Summary by CodeRabbit

  • 버그 수정

    • 스와이프 백 제스처 응답성 및 안정성 추가 개선 — 취소/완료 애니메이션 처리 및 상태 중복 방지
    • 화면 전환 중 시각적 깜박임 감소
  • 성능 개선

    • 애니메이션 렌더링 성능 최적화 및 RAF 기반 처리로 부드러움 향상
    • 네비게이션 상호작용 반응성 향상
  • 신규 기능

    • 전환·스와이프 애니메이션 처리 개선으로 더 일관된 이동·페이드 효과 제공

…creen transitions

Migrate all AppScreen transition animations (push, pop, swipe back) from
CSS recipe-based animations to Web Animations API (WAAPI). This eliminates
the CSS-to-JS handoff that caused flicker, double-animation, and timing bugs
during swipe-back gestures.

Key changes:
- Remove all transition animation selectors from app-screen.ts and app-bar.ts recipes
- Add transition-animation.ts with WAAPI implementations for iOS/Android/fadeIn styles
- Rewrite useGlobalInteraction to detect transitionState changes and trigger WAAPI
- Add rAF lock pattern and passive-ready touchmove in useSwipeBack
- Manage element positions via explicit inline styles instead of CSS variables

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 4d50dd9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@seed-design/stackflow Patch
@seed-design/css Patch
@seed-design/figma Patch
@seed-design/mcp Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

스택플로우 전환 로직을 WAAPI 기반 애니메이션으로 재구성하고, 스와이프 백을 ref 기반으로 변경했으며, DOM 타겟 검색·인라인 스타일 유틸리티와 데이터 속성 식별자를 추가했습니다.

Changes

Cohort / File(s) Summary
Animation & DOM utilities
packages/stackflow/src/primitive/GlobalInteraction/animation.ts, packages/stackflow/src/primitive/GlobalInteraction/dom.ts
WAAPI 기반 전환·스와이프 애니메이션 구현 추가. 애니메이션 집계형 결과(AnimationResult), 취소 유틸, 전환/스와이프용 animate 함수들과 DOM 타겟 탐색·인라인 스타일 적용 함수(findTransitionTargets, applySwipeStyles, setPostExitPositions, 등) 추가.
Global interaction & swipe hooks
packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts, packages/stackflow/src/primitive/GlobalInteraction/useSwipeBack.ts
스와이프 상태를 state → ref로 전환, RAF 기반 스로틀링 추가, 제스처별 타겟 캐싱 및 WAAPI 호출로 완료/취소 애니메이션 구동, 인라인 스타일 설정·삭제 로직 및 애니메이션 취소 추적 도입.
AppBar DOM attributes
packages/stackflow/src/components/AppBar/AppBar.tsx, packages/stackflow/src/primitive/AppBar/AppBar.tsx
AppBar 관련 Slot/요소에 data-part 속성(appBarIcon, appBarCustom, appBarMain)을 추가하여 DOM에서 전환 타겟을 식별하도록 변경.
Preset variant cleanup
packages/qvism-preset/src/stackflow/app-bar.ts, packages/qvism-preset/src/stackflow/app-screen.ts
애니메이션 상수 및 pseudo-transition 키 imports 제거. slideFromRightIOS, fadeFromBottomAndroid, fadeIn 변형을 내부 스타일 매핑에서 비워 전환 스타일의 내부 매핑 제거.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자 제스처
    participant SB as useSwipeBack
    participant GI as useGlobalInteraction
    participant DOM as DOM 유틸리티
    participant WAAPI as WAAPI 애니메이션

    User->>SB: touchmove / touchend 이벤트
    SB->>SB: RAF 스로틀 확인
    SB->>GI: events.moveSwipeBack / events.endSwipeBack
    GI->>DOM: findTransitionTargets(stackEl)
    GI->>DOM: applySwipeStyles(targets, displacement, ratio)
    DOM->>DOM: 인라인 transform/opacity 설정
    alt 스와이프 종료(완료)
        GI->>WAAPI: animateSwipeComplete(targets, displacement, velocity)
        WAAPI->>DOM: 애니메이션으로 transform/opacity 변화
        WAAPI-->>GI: finished Promise
    else 스와이프 종료(취소)
        GI->>WAAPI: animateSwipeCancel(targets, displacement, velocity)
        WAAPI->>DOM: 복귀 애니메이션 실행
        WAAPI-->>GI: finished Promise
    end
    GI->>DOM: setPostExitPositions(targets, style)
    GI->>GI: 상태 및 인라인 스타일 정리
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 춤추는 발톱으로 말하네
WAAPI로 길을 닦고, 스와이프를 품어
DOM을 찾아 속성을 속삭이며,
ref에 숨결을 담아 움직임을 완성하네.
새로운 전환이 퐁당, 기쁘게 번진다.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 변경사항의 핵심을 명확하게 나타냅니다. CSS 애니메이션에서 WAAPI로의 마이그레이션이 주요 변경사항이며, 제목은 이를 정확하게 반영합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/des-1079-waapi

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@junghyeonsu
Copy link
Copy Markdown
Contributor Author

/snapshot

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

Alpha Preview (Stackflow SPA)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

Alpha Preview (Storybook)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

📦 Snapshot Release

@seed-design/cli: https://pkg.pr.new/@seed-design/cli@0217ba3
@seed-design/codemod: https://pkg.pr.new/@seed-design/codemod@0217ba3
@seed-design/css: https://pkg.pr.new/@seed-design/css@0217ba3
@seed-design/design-token: https://pkg.pr.new/@seed-design/design-token@0217ba3
@seed-design/docs-mcp: https://pkg.pr.new/@seed-design/docs-mcp@0217ba3
@seed-design/figma: https://pkg.pr.new/@seed-design/figma@0217ba3
@seed-design/mcp: https://pkg.pr.new/@seed-design/mcp@0217ba3
@seed-design/migration-index: https://pkg.pr.new/@seed-design/migration-index@0217ba3
@seed-design/react: https://pkg.pr.new/@seed-design/react@0217ba3
@seed-design/rootage-artifacts: https://pkg.pr.new/@seed-design/rootage-artifacts@0217ba3
@seed-design/rsbuild-plugin: https://pkg.pr.new/@seed-design/rsbuild-plugin@0217ba3
@seed-design/stackflow: https://pkg.pr.new/@seed-design/stackflow@0217ba3
@seed-design/stylesheet: https://pkg.pr.new/@seed-design/stylesheet@0217ba3
@seed-design/tailwind3-plugin: https://pkg.pr.new/@seed-design/tailwind3-plugin@0217ba3
@seed-design/tailwind4-theme: https://pkg.pr.new/@seed-design/tailwind4-theme@0217ba3
@seed-design/vite-plugin: https://pkg.pr.new/@seed-design/vite-plugin@0217ba3
@seed-design/webpack-plugin: https://pkg.pr.new/@seed-design/webpack-plugin@0217ba3
@seed-design/react-avatar: https://pkg.pr.new/@seed-design/react-avatar@0217ba3
@seed-design/react-checkbox: https://pkg.pr.new/@seed-design/react-checkbox@0217ba3
@seed-design/react-collapsible: https://pkg.pr.new/@seed-design/react-collapsible@0217ba3
@seed-design/react-dialog: https://pkg.pr.new/@seed-design/react-dialog@0217ba3
@seed-design/react-drawer: https://pkg.pr.new/@seed-design/react-drawer@0217ba3
@seed-design/react-field: https://pkg.pr.new/@seed-design/react-field@0217ba3
@seed-design/react-field-button: https://pkg.pr.new/@seed-design/react-field-button@0217ba3
@seed-design/react-fieldset: https://pkg.pr.new/@seed-design/react-fieldset@0217ba3
@seed-design/react-image: https://pkg.pr.new/@seed-design/react-image@0217ba3
@seed-design/react-popover: https://pkg.pr.new/@seed-design/react-popover@0217ba3
@seed-design/react-portal: https://pkg.pr.new/@seed-design/react-portal@0217ba3
@seed-design/react-primitive: https://pkg.pr.new/@seed-design/react-primitive@0217ba3
@seed-design/react-progress: https://pkg.pr.new/@seed-design/react-progress@0217ba3
@seed-design/react-pull-to-refresh: https://pkg.pr.new/@seed-design/react-pull-to-refresh@0217ba3
@seed-design/react-radio-group: https://pkg.pr.new/@seed-design/react-radio-group@0217ba3
@seed-design/react-scrollable: https://pkg.pr.new/@seed-design/react-scrollable@0217ba3
@seed-design/react-segmented-control: https://pkg.pr.new/@seed-design/react-segmented-control@0217ba3
@seed-design/react-slider: https://pkg.pr.new/@seed-design/react-slider@0217ba3
@seed-design/react-snackbar: https://pkg.pr.new/@seed-design/react-snackbar@0217ba3
@seed-design/react-supports: https://pkg.pr.new/@seed-design/react-supports@0217ba3
@seed-design/react-switch: https://pkg.pr.new/@seed-design/react-switch@0217ba3
@seed-design/react-tabs: https://pkg.pr.new/@seed-design/react-tabs@0217ba3
@seed-design/react-text-field: https://pkg.pr.new/@seed-design/react-text-field@0217ba3
@seed-design/react-toggle: https://pkg.pr.new/@seed-design/react-toggle@0217ba3
@seed-design/react-use-controllable-state: https://pkg.pr.new/@seed-design/react-use-controllable-state@0217ba3
@seed-design/dom-utils: https://pkg.pr.new/@seed-design/dom-utils@0217ba3

Triggered by @junghyeonsu via /snapshot

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stackflow/src/primitive/GlobalInteraction/useSwipeBack.ts (1)

31-46: ⚠️ Potential issue | 🟠 Major

마지막 touchmove를 종료 전에 반영해야 합니다.

지금은 moveSwipeBack()가 다음 frame으로 미뤄지기 때문에, 그 전에 onTouchEnd/onTouchCancel이 오면 endSwipeBack()가 이전 displacement/velocity로 판정합니다. 빠른 스와이프가 취소로 오판될 수 있고, 다음 frame의 지연된 moveSwipeBack()이 이미 시작된 cancel/completing 애니메이션 위에 다시 스타일을 덮어쓸 수도 있습니다. 마지막 좌표와 rAF id를 저장해 두고 종료 시 동기 flush 하거나, 최소한 pending frame을 취소한 뒤 종료하는 쪽이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/useSwipeBack.ts` around
lines 31 - 46, The touchend/touchcancel path can race with a pending
requestAnimationFrame in onTouchMove, causing endSwipeBack() to use stale
displacement/velocity; modify useSwipeBack so onTouchMove saves the last touch
{x,t} and the rAF id (e.g. rafIdRef) when calling requestAnimationFrame, and
then in onTouchEnd and onTouchCancel first cancel any pending rAF
(cancelAnimationFrame(rafIdRef.current)) and synchronously flush the last saved
touch by calling events.moveSwipeBack with the saved {x,t} before calling
events.endSwipeBack({}), ensuring rAFLockRef is reset appropriately to avoid
deadlock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/qvism-preset/src/stackflow/app-bar.ts`:
- Around line 231-235: 현재 transitionStyle의 slideFromRightIOS /
fadeFromBottomAndroid / fadeIn 변형을 비워서 transition-animation.ts의
applySwipeStyles()가 업데이트하는 CSS 변수 --swipe-back-displacement를 읽는 transform 동작이
사라졌습니다; transitionStyle 객체의 해당 variants에 swipe 중 요소 이동을 반영하는 transform(예:
translateX(var(--swipe-back-displacement))) 규칙을 복원하고, 이 파일의 AppBar 스타일 내
&:before(배경)에도 같은 transform(translateX(var(--swipe-back-displacement)))을 적용해
제목/아이콘과 동일하게 배경이 손가락을 따라 움직이도록 만드세요; applySwipeStyles()가 설정하는
--swipe-back-displacement 변수를 사용하도록 정확한 변수 이름을 참조하는지 확인하세요.

In `@packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts`:
- Around line 250-259: The problem is waitAll treats canceled animations as
successful by swallowing AbortError, causing callers (useGlobalInteraction.ts
callbacks that call setIdlePositions()/setPostExitPositions()) to run for
canceled animations; update waitAll to rethrow AbortError so Promise.all rejects
on cancellation (i.e., change the finished.catch handler inside waitAll to
rethrow when err.name === 'AbortError' or instanceof DOMException with name
'AbortError', but continue swallowing/ignoring other errors), ensuring canceled
animations are identified and downstream cleanup callbacks are skipped.

In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`:
- Around line 311-321: The returned swipeBackState currently exposes a snapshot
(swipeBackStateRef.current) so consumers never see updates when
setSwipeBackState is called; update the API by either removing swipeBackState
from the returned object or making it reactive — e.g., add a stable getter
getSwipeBackState() or maintain a synced piece of state inside
useGlobalInteraction that is updated inside setSwipeBackState (keep
setSwipeBackState, getSwipeBackEvents, stackProps as-is), and update the useMemo
to return the reactive getter/value (or remove swipeBackState) so consumers
observe changes; refer to useGlobalInteraction, swipeBackStateRef,
setSwipeBackState and getSwipeBackEvents when implementing.
- Around line 96-119: When starting a swipe in
useGlobalInteraction.startSwipeBack you cancel runningAnimsRef but you also must
cancel any scheduled frame-based push WAAPI reserved via pendingRAFRef; if you
don't, that RAF can fire after the swipe begins and apply push styles over the
swipe. Fix by checking pendingRAFRef.current in startSwipeBack, call
cancelAnimationFrame on it (or clearTimeout if using setTimeout), set
pendingRAFRef.current = null, and ensure any associated queued push state is
cleared so the reserved push won't run during the swipe.

---

Outside diff comments:
In `@packages/stackflow/src/primitive/GlobalInteraction/useSwipeBack.ts`:
- Around line 31-46: The touchend/touchcancel path can race with a pending
requestAnimationFrame in onTouchMove, causing endSwipeBack() to use stale
displacement/velocity; modify useSwipeBack so onTouchMove saves the last touch
{x,t} and the rAF id (e.g. rafIdRef) when calling requestAnimationFrame, and
then in onTouchEnd and onTouchCancel first cancel any pending rAF
(cancelAnimationFrame(rafIdRef.current)) and synchronously flush the last saved
touch by calling events.moveSwipeBack with the saved {x,t} before calling
events.endSwipeBack({}), ensuring rAFLockRef is reset appropriately to avoid
deadlock.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8d7b97e8-99d6-48cc-9bc7-e462e0c12b96

📥 Commits

Reviewing files that changed from the base of the PR and between 9af1f1d and 0217ba3.

⛔ Files ignored due to path filters (10)
  • packages/css/all.css is excluded by !packages/css/**/*
  • packages/css/all.layered.css is excluded by !packages/css/**/*
  • packages/css/all.layered.min.css is excluded by !packages/css/**/*
  • packages/css/all.min.css is excluded by !packages/css/**/*
  • packages/css/recipes/app-bar-main.css is excluded by !packages/css/**/*
  • packages/css/recipes/app-bar-main.layered.css is excluded by !packages/css/**/*
  • packages/css/recipes/app-bar.css is excluded by !packages/css/**/*
  • packages/css/recipes/app-bar.layered.css is excluded by !packages/css/**/*
  • packages/css/recipes/app-screen.css is excluded by !packages/css/**/*
  • packages/css/recipes/app-screen.layered.css is excluded by !packages/css/**/*
📒 Files selected for processing (5)
  • packages/qvism-preset/src/stackflow/app-bar.ts
  • packages/qvism-preset/src/stackflow/app-screen.ts
  • packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts
  • packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts
  • packages/stackflow/src/primitive/GlobalInteraction/useSwipeBack.ts

Comment thread packages/qvism-preset/src/stackflow/app-bar.ts
Comment thread packages/stackflow/src/primitive/GlobalInteraction/animation.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

Alpha Preview (Docs)

Extract shared helpers to reduce duplication without changing behavior:
- Unify iosAnimatePush/Pop into parameterized iosAnimate
- Unify animateSwipeComplete/Cancel into parameterized animateSwipe
- Extract collectAnimations helper for repeated filter+waitAll pattern
- Fix file header comment to match actual fill:"forwards" strategy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts (1)

346-352: 타입 단언 대신 명시적 타입 정의를 고려해보세요.

from.topTitle["transform"] as string 패턴은 현재 IOS_ONSCREEN/IOS_OFFSCREEN 상수에서 안전하지만, 향후 유지보수 시 타입 안전성을 저해할 수 있습니다.

♻️ 타입 안전성 개선 제안
 interface IosPositions {
   topLayer: string;
   behindLayer: string;
   dim: string;
-  topTitle: Keyframe;
-  behindTitle: Keyframe;
+  topTitle: { opacity: string; transform: string };
+  behindTitle: { opacity: string; transform: string };
   topIconOpacity: string;
   behindIconOpacity: string;
   appBarPseudo: string;
 }

이렇게 하면 from.topTitle.transform으로 직접 접근 가능하며 타입 단언이 불필요해집니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts`
around lines 346 - 352, The code uses type assertions like
from.topTitle["transform"] as string; instead, update the type definition for
the topTitle object so transform is explicitly typed as string (e.g., refine the
TopTitle type/interface or the IOS_ONSCREEN/IOS_OFFSCREEN constant typings) and
then replace bracket-assertions with direct property access
(from.topTitle.transform and to.topTitle.transform) to eliminate the need for
"as string" and preserve type safety.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts`:
- Around line 346-352: The code uses type assertions like
from.topTitle["transform"] as string; instead, update the type definition for
the topTitle object so transform is explicitly typed as string (e.g., refine the
TopTitle type/interface or the IOS_ONSCREEN/IOS_OFFSCREEN constant typings) and
then replace bracket-assertions with direct property access
(from.topTitle.transform and to.topTitle.transform) to eliminate the need for
"as string" and preserve type safety.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 43afaef9-959e-48c7-9b48-e69533413d0d

📥 Commits

Reviewing files that changed from the base of the PR and between 0217ba3 and e61a565.

📒 Files selected for processing (1)
  • packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts

setIdlePositions and setPostExitPositions now accept TransitionStyle
parameter. iOS-specific behind layer offset (-30%) and title/icon
hiding only apply to slideFromRightIOS. Android and fadeIn transitions
no longer incorrectly offset the behind activity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts (1)

34-37: 클래스 substring 셀렉터 사용에 대한 참고

[class*="seed-app-bar..."] 패턴은 CSS-in-JS 환경에서 클래스명에 hash가 붙는 경우를 대응하기 위한 것으로 보입니다. 다만 이 방식은:

  • 유사한 이름의 다른 클래스와 우발적 매칭 가능성
  • 클래스 네이밍 컨벤션 변경 시 깨질 수 있음

현재 구현이 의도된 것이라면 문제없으나, 가능하다면 data-part 속성처럼 명시적 attribute 기반 셀렉터가 더 안정적입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts`
around lines 34 - 37, The substring class selectors (SEL_APP_BAR_MAIN_ROOT,
SEL_APP_BAR_ROOT, SEL_APP_BAR_ICON, SEL_APP_BAR_CUSTOM) are brittle and may
match unintended hashed classnames; change the selectors to use an explicit data
attribute (e.g., data-part="app-bar-main", "app-bar-root", "app-bar-icon",
"app-bar-custom") and update the consuming components to render those data-part
attributes so the animation code queries by attribute instead of class
substring; ensure the new attribute names are documented and replace all uses of
the four SEL_* constants in transition-animation.ts with the corresponding
data-part selectors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`:
- Around line 87-230: The getSwipeBackEvents function violates React Hooks rules
by calling hooks (useCallbackRef, useCallback, useMemo) inside a callback; move
all hook calls to the top level of the useGlobalInteraction hook and have
getSwipeBackEvents only compose/return pre-created callbacks. Concretely: hoist
useCallbackRef calls for props.onSwipeBackStart/onSwipeBackMove/onSwipeBackEnd
out of getSwipeBackEvents into the top of useGlobalInteraction (keep names
onSwipeStartRef/onSwipeMoveRef/onSwipeEndRef or similar), create top-level
stable callbacks startSwipeBack, moveSwipeBack, endSwipeBack and reset with
useCallback that reference refs and refs like
swipeBackContextRef/cachedTargetsRef/runningAnimsRef, and create the returned
object with useMemo at top-level; then change getSwipeBackEvents to simply
return { startSwipeBack, moveSwipeBack, endSwipeBack, reset } (or remove it and
return that object directly) so no hooks are invoked inside a nested callback.

---

Nitpick comments:
In `@packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts`:
- Around line 34-37: The substring class selectors (SEL_APP_BAR_MAIN_ROOT,
SEL_APP_BAR_ROOT, SEL_APP_BAR_ICON, SEL_APP_BAR_CUSTOM) are brittle and may
match unintended hashed classnames; change the selectors to use an explicit data
attribute (e.g., data-part="app-bar-main", "app-bar-root", "app-bar-icon",
"app-bar-custom") and update the consuming components to render those data-part
attributes so the animation code queries by attribute instead of class
substring; ensure the new attribute names are documented and replace all uses of
the four SEL_* constants in transition-animation.ts with the corresponding
data-part selectors.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eff0c742-5717-4f8e-856c-92761d806799

📥 Commits

Reviewing files that changed from the base of the PR and between e61a565 and ed6b982.

📒 Files selected for processing (2)
  • packages/stackflow/src/primitive/GlobalInteraction/transition-animation.ts
  • packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts

…on.ts

- Add data-part attributes to AppBarMain, AppBarIconButton, AppBarSlot
  to replace fragile className-based selectors
- Split transition-animation.ts into:
  - dom.ts: DOM discovery (findTransitionTargets) and inline style management
  - animation.ts: WAAPI animation functions
- Unify SwipeEndpoints with IOS_ONSCREEN/IOS_OFFSCREEN constants
- Remove transition-animation.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (4)
packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts (3)

98-121: ⚠️ Potential issue | 🟠 Major

swipe 시작 시 대기 중인 push rAF도 취소해야 합니다.

Lines 261-263에서 enter-active는 다음 frame에 push WAAPI를 예약하는데, startSwipeBack에서는 실행 중인 animation만 취소하고 pendingRAFRef는 그대로 둡니다. 사용자가 그 frame 전에 edge swipe를 시작하면 예약된 push가 뒤늦게 실행되어 swipe 스타일과 겹칠 수 있습니다.

,

권장 수정
     const startSwipeBack = useCallback(
       ({ x0, t0 }: StartSwipeBackProps) => {
+        // Cancel any pending RAF-scheduled push animation
+        if (pendingRAFRef.current !== null) {
+          cancelAnimationFrame(pendingRAFRef.current);
+          pendingRAFRef.current = null;
+        }
+
         // Cancel any running transition animations
         cancelAll(runningAnimsRef.current);
         runningAnimsRef.current = [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`
around lines 98 - 121, startSwipeBack currently cancels only running animations
via cancelAll(runningAnimsRef.current) but does not clear any scheduled frame
callbacks, so pending push WAAPI scheduled by enter-active (via pendingRAFRef)
can still run during a swipe; update startSwipeBack to also cancel and clear
pendingRAFRef (e.g., call cancelAnimationFrame or equivalent on
pendingRAFRef.current and set pendingRAFRef.current = null) and ensure any
helper that schedules push WAAPI uses pendingRAFRef so this cleanup prevents the
delayed push from applying styles during a swipe.

313-324: ⚠️ Potential issue | 🟠 Major

반환하는 swipeBackState는 reactive하지 않습니다.

Line 317에서 swipeBackStateRef.current 스냅샷을 그대로 반환하므로, setSwipeBackState()가 호출되어도 hook 소비자는 변경된 값을 받지 못합니다. DOM dataset 갱신과는 별개로, 공개 API로 유지할 경우 reactive하게 노출하거나 API surface에서 제거하는 것이 안전합니다.

,

권장 수정 옵션

옵션 1: 제거 — 외부에서 필요 없다면 반환 객체에서 swipeBackState 제거

옵션 2: getter 함수로 노출

   return useMemo(
     () => ({
       stackRef,
       swipeBackContextRef,
-      swipeBackState: swipeBackStateRef.current,
+      getSwipeBackState: () => swipeBackStateRef.current,
       setSwipeBackState,
       getSwipeBackEvents,
       stackProps,
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`
around lines 313 - 324, The returned swipeBackState currently returns a
non-reactive snapshot (swipeBackStateRef.current) from useGlobalInteraction;
update the API so consumers see updates by either removing swipeBackState from
the returned object or replacing it with a getter function that reads
swipeBackStateRef.current (e.g., expose getSwipeBackState) or convert to React
state updated by setSwipeBackState; adjust the returned object (which currently
lists stackRef, swipeBackContextRef, swipeBackState: swipeBackStateRef.current,
setSwipeBackState, getSwipeBackEvents, stackProps) and its dependency array
accordingly so consumers receive reactive updates or the property is no longer
exposed.

89-232: ⚠️ Potential issue | 🔴 Critical

React Hooks 규칙 위반: 콜백 내부에서 hooks 호출

getSwipeBackEventsuseCallback으로 감싸진 함수인데, 내부에서 useCallbackRef (lines 94-96), useCallback (lines 98, 123, 147, 206), useMemo (line 223)를 호출하고 있습니다. Hooks는 반드시 컴포넌트/커스텀 hook의 최상위에서만 호출해야 합니다.

hooks를 useGlobalInteraction 최상위로 이동하고, getSwipeBackEvents는 이미 생성된 callbacks를 조합하여 반환하도록 리팩토링이 필요합니다.

,

권장 리팩토링 방향
 export function useGlobalInteraction() {
   const swipeBackStateRef = useRef<SwipeBackState>("idle");
   // ...existing refs...

+  // Move callback refs to top level
+  const onSwipeStartRef = useRef<(() => void) | undefined>();
+  const onSwipeMoveRef = useRef<((props: { displacement: number; displacementRatio: number }) => void) | undefined>();
+  const onSwipeEndRef = useRef<((props: { swiped: boolean }) => void) | undefined>();
+
+  // Move callbacks to top level
+  const startSwipeBack = useCallback(({ x0, t0 }: StartSwipeBackProps) => {
+    // ... implementation using onSwipeStartRef.current ...
+  }, []);
+
+  // ... other callbacks at top level ...

   const getSwipeBackEvents = useCallback((props: SwipeBackProps) => {
-    const onSwipeStart = useCallbackRef(props.onSwipeBackStart);
+    // Update refs instead of creating new hooks
+    onSwipeStartRef.current = props.onSwipeBackStart;
+    onSwipeMoveRef.current = props.onSwipeBackMove;
+    onSwipeEndRef.current = props.onSwipeBackEnd;
+
+    return { startSwipeBack, moveSwipeBack, endSwipeBack, reset };
   }, [startSwipeBack, moveSwipeBack, endSwipeBack, reset]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`
around lines 89 - 232, getSwipeBackEvents currently calls hooks (useCallbackRef,
useCallback, useMemo) inside its body which violates React Hooks rules; move
those hook calls to the top-level of the containing useGlobalInteraction hook
and have getSwipeBackEvents only compose and return the already-created
callbacks. Concretely: lift the useCallbackRef(...) for
onSwipeStart/onSwipeMove/onSwipeEnd and the useCallback definitions for
startSwipeBack, moveSwipeBack, endSwipeBack, reset, and the useMemo return
wrapper out of getSwipeBackEvents into useGlobalInteraction's top scope; keep
the internal logic of startSwipeBack/moveSwipeBack/endSwipeBack/reset identical
but reference shared refs (swipeBackContextRef, cachedTargetsRef,
runningAnimsRef, skipNextExitRef, setSwipeBackState, stackRef) from the outer
hook, and change getSwipeBackEvents to simply return { startSwipeBack,
moveSwipeBack, endSwipeBack, reset } (or a memoized object using the
already-lifted useMemo) so no hooks are invoked inside getSwipeBackEvents
itself.
packages/stackflow/src/primitive/GlobalInteraction/animation.ts (1)

68-78: ⚠️ Potential issue | 🟠 Major

cancel()된 animation도 정상 완료처럼 처리됩니다.

waitAllAbortError를 삼킨 뒤 resolve하므로, 호출부의 finished.then(...)이 cancel 경로에서도 항상 실행됩니다. useGlobalInteraction.ts에서는 그 callback 안에서 setIdlePositions()/setPostExitPositions()를 호출하므로, 빠른 push→pop 전환 시 취소된 이전 animation의 callback이 뒤늦게 스타일을 덮어쓸 수 있습니다.

,

권장 수정

cancel 여부를 식별하여 후속 cleanup을 건너뛰도록 수정:

 function waitAll(animations: (Animation | null)[]): Promise<void> {
   const valid = animations.filter((a): a is Animation => a !== null);
   if (valid.length === 0) return Promise.resolve();
   return Promise.all(
     valid.map((a) =>
       a.finished.catch((err) => {
-        /* AbortError from cancel */
+        // Re-throw AbortError so callers can distinguish cancellation
+        if (err instanceof DOMException && err.name === "AbortError") {
+          throw err;
+        }
+        // Swallow other errors
       }),
     ),
   ).then(() => {});
 }

또는 호출부에서 animation 참조가 유효한지 확인:

finished.then(() => {
  // Only apply if these are still the current animations
  if (runningAnimsRef.current === animations) {
    setIdlePositions(targets, style);
    cancelAll(animations);
    runningAnimsRef.current = [];
  }
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stackflow/src/primitive/GlobalInteraction/animation.ts` around lines
68 - 78, waitAll currently swallows AbortError from cancelled animations so
callers' finished.then callbacks run even for cancelled sequences; change
waitAll to rethrow AbortError (or otherwise propagate cancellation) instead of
swallowing it: inside waitAll's Promise.all mapping for each Animation a use
a.finished.catch(err => { if (err && err.name === 'AbortError') throw err; /*
swallow other errors if desired */ }); this preserves the existing behavior for
non-cancellation errors but ensures cancelled animations cause the returned
promise to reject so callers (e.g. useGlobalInteraction.ts finished handlers)
can skip cleanup or verify runningAnimsRef/current animations before applying
style changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/stackflow/src/primitive/GlobalInteraction/animation.ts`:
- Around line 68-78: waitAll currently swallows AbortError from cancelled
animations so callers' finished.then callbacks run even for cancelled sequences;
change waitAll to rethrow AbortError (or otherwise propagate cancellation)
instead of swallowing it: inside waitAll's Promise.all mapping for each
Animation a use a.finished.catch(err => { if (err && err.name === 'AbortError')
throw err; /* swallow other errors if desired */ }); this preserves the existing
behavior for non-cancellation errors but ensures cancelled animations cause the
returned promise to reject so callers (e.g. useGlobalInteraction.ts finished
handlers) can skip cleanup or verify runningAnimsRef/current animations before
applying style changes.

In `@packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts`:
- Around line 98-121: startSwipeBack currently cancels only running animations
via cancelAll(runningAnimsRef.current) but does not clear any scheduled frame
callbacks, so pending push WAAPI scheduled by enter-active (via pendingRAFRef)
can still run during a swipe; update startSwipeBack to also cancel and clear
pendingRAFRef (e.g., call cancelAnimationFrame or equivalent on
pendingRAFRef.current and set pendingRAFRef.current = null) and ensure any
helper that schedules push WAAPI uses pendingRAFRef so this cleanup prevents the
delayed push from applying styles during a swipe.
- Around line 313-324: The returned swipeBackState currently returns a
non-reactive snapshot (swipeBackStateRef.current) from useGlobalInteraction;
update the API so consumers see updates by either removing swipeBackState from
the returned object or replacing it with a getter function that reads
swipeBackStateRef.current (e.g., expose getSwipeBackState) or convert to React
state updated by setSwipeBackState; adjust the returned object (which currently
lists stackRef, swipeBackContextRef, swipeBackState: swipeBackStateRef.current,
setSwipeBackState, getSwipeBackEvents, stackProps) and its dependency array
accordingly so consumers receive reactive updates or the property is no longer
exposed.
- Around line 89-232: getSwipeBackEvents currently calls hooks (useCallbackRef,
useCallback, useMemo) inside its body which violates React Hooks rules; move
those hook calls to the top-level of the containing useGlobalInteraction hook
and have getSwipeBackEvents only compose and return the already-created
callbacks. Concretely: lift the useCallbackRef(...) for
onSwipeStart/onSwipeMove/onSwipeEnd and the useCallback definitions for
startSwipeBack, moveSwipeBack, endSwipeBack, reset, and the useMemo return
wrapper out of getSwipeBackEvents into useGlobalInteraction's top scope; keep
the internal logic of startSwipeBack/moveSwipeBack/endSwipeBack/reset identical
but reference shared refs (swipeBackContextRef, cachedTargetsRef,
runningAnimsRef, skipNextExitRef, setSwipeBackState, stackRef) from the outer
hook, and change getSwipeBackEvents to simply return { startSwipeBack,
moveSwipeBack, endSwipeBack, reset } (or a memoized object using the
already-lifted useMemo) so no hooks are invoked inside getSwipeBackEvents
itself.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e86140b6-9815-4f70-b4e5-b756c483a086

📥 Commits

Reviewing files that changed from the base of the PR and between ed6b982 and 8778bef.

📒 Files selected for processing (5)
  • packages/stackflow/src/components/AppBar/AppBar.tsx
  • packages/stackflow/src/primitive/AppBar/AppBar.tsx
  • packages/stackflow/src/primitive/GlobalInteraction/animation.ts
  • packages/stackflow/src/primitive/GlobalInteraction/dom.ts
  • packages/stackflow/src/primitive/GlobalInteraction/useGlobalInteraction.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/stackflow/src/primitive/AppBar/AppBar.tsx
  • packages/stackflow/src/components/AppBar/AppBar.tsx

junghyeonsu and others added 7 commits April 10, 2026 00:23
…sitions

Set start positions as inline styles before animation begins for
fadeFromBottomAndroid (opacity:0 + translateY:8vh) and fadeIn (opacity:0),
matching the existing pattern in iOS push.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…state

useTopActivity().transitionStyle updates one render cycle late because it
uses useState + useLayoutEffect. During push, the captured value was the
previous activity's style, so fadeFromBottomAndroid/fadeIn always fell back
to slideFromRightIOS.

Add readTransitionStyle() that reads data-transition-style directly from
the top activity DOM element at animation time, bypassing the stale state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For fadeFromBottomAndroid and fadeIn transitions, the behind activity
stays in place (no parallax offset). After push completes, its appBar
was visible behind the top activity's appBar. Hide it with opacity:0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ment

Root cause: clearAllStyles didn't clear transform/opacity on appBar roots,
leaving stale inline styles from pre-animation setup (e.g. Android push
sets transform:translate3d(0,8vh,0) which was never cleaned up).

Pattern change: setIdlePositions and setPostExitPositions now call
clearAllStyles first (clean slate), then set only non-default positions.
No more selective clearing that misses elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Behind appBar is hidden (opacity:0) during idle to prevent bleed-through.
Pop must restore it before animation starts so it doesn't flash in at the end.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If a push rAF is scheduled but hasn't fired yet when the user starts
swiping, the rAF would execute after swipe begins and overlay push
animations on top of swipe styles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@malangcat
Copy link
Copy Markdown
Contributor

good

@junghyeonsu
Copy link
Copy Markdown
Contributor Author

/snapshot

@github-actions
Copy link
Copy Markdown
Contributor

📦 Snapshot Release

@seed-design/cli: https://pkg.pr.new/@seed-design/cli@89c397c
@seed-design/codemod: https://pkg.pr.new/@seed-design/codemod@89c397c
@seed-design/css: https://pkg.pr.new/@seed-design/css@89c397c
@seed-design/design-token: https://pkg.pr.new/@seed-design/design-token@89c397c
@seed-design/docs-mcp: https://pkg.pr.new/@seed-design/docs-mcp@89c397c
@seed-design/figma: https://pkg.pr.new/@seed-design/figma@89c397c
@seed-design/mcp: https://pkg.pr.new/@seed-design/mcp@89c397c
@seed-design/migration-index: https://pkg.pr.new/@seed-design/migration-index@89c397c
@seed-design/react: https://pkg.pr.new/@seed-design/react@89c397c
@seed-design/rootage-artifacts: https://pkg.pr.new/@seed-design/rootage-artifacts@89c397c
@seed-design/rsbuild-plugin: https://pkg.pr.new/@seed-design/rsbuild-plugin@89c397c
@seed-design/stackflow: https://pkg.pr.new/@seed-design/stackflow@89c397c
@seed-design/stylesheet: https://pkg.pr.new/@seed-design/stylesheet@89c397c
@seed-design/tailwind3-plugin: https://pkg.pr.new/@seed-design/tailwind3-plugin@89c397c
@seed-design/tailwind4-theme: https://pkg.pr.new/@seed-design/tailwind4-theme@89c397c
@seed-design/vite-plugin: https://pkg.pr.new/@seed-design/vite-plugin@89c397c
@seed-design/webpack-plugin: https://pkg.pr.new/@seed-design/webpack-plugin@89c397c
@seed-design/react-avatar: https://pkg.pr.new/@seed-design/react-avatar@89c397c
@seed-design/react-checkbox: https://pkg.pr.new/@seed-design/react-checkbox@89c397c
@seed-design/react-collapsible: https://pkg.pr.new/@seed-design/react-collapsible@89c397c
@seed-design/react-dialog: https://pkg.pr.new/@seed-design/react-dialog@89c397c
@seed-design/react-drawer: https://pkg.pr.new/@seed-design/react-drawer@89c397c
@seed-design/react-field: https://pkg.pr.new/@seed-design/react-field@89c397c
@seed-design/react-field-button: https://pkg.pr.new/@seed-design/react-field-button@89c397c
@seed-design/react-fieldset: https://pkg.pr.new/@seed-design/react-fieldset@89c397c
@seed-design/react-image: https://pkg.pr.new/@seed-design/react-image@89c397c
@seed-design/react-popover: https://pkg.pr.new/@seed-design/react-popover@89c397c
@seed-design/react-portal: https://pkg.pr.new/@seed-design/react-portal@89c397c
@seed-design/react-primitive: https://pkg.pr.new/@seed-design/react-primitive@89c397c
@seed-design/react-progress: https://pkg.pr.new/@seed-design/react-progress@89c397c
@seed-design/react-pull-to-refresh: https://pkg.pr.new/@seed-design/react-pull-to-refresh@89c397c
@seed-design/react-radio-group: https://pkg.pr.new/@seed-design/react-radio-group@89c397c
@seed-design/react-scrollable: https://pkg.pr.new/@seed-design/react-scrollable@89c397c
@seed-design/react-segmented-control: https://pkg.pr.new/@seed-design/react-segmented-control@89c397c
@seed-design/react-slider: https://pkg.pr.new/@seed-design/react-slider@89c397c
@seed-design/react-snackbar: https://pkg.pr.new/@seed-design/react-snackbar@89c397c
@seed-design/react-supports: https://pkg.pr.new/@seed-design/react-supports@89c397c
@seed-design/react-switch: https://pkg.pr.new/@seed-design/react-switch@89c397c
@seed-design/react-tabs: https://pkg.pr.new/@seed-design/react-tabs@89c397c
@seed-design/react-text-field: https://pkg.pr.new/@seed-design/react-text-field@89c397c
@seed-design/react-toggle: https://pkg.pr.new/@seed-design/react-toggle@89c397c
@seed-design/react-use-controllable-state: https://pkg.pr.new/@seed-design/react-use-controllable-state@89c397c
@seed-design/dom-utils: https://pkg.pr.new/@seed-design/dom-utils@89c397c

Triggered by @junghyeonsu via /snapshot

@junghyeonsu junghyeonsu marked this pull request as ready for review April 20, 2026 10:45
@junghyeonsu junghyeonsu force-pushed the feature/des-1079-waapi branch from 91f7bf1 to 8026c1a Compare April 21, 2026 06:03
The top AppBar background (::before) was not moving with the swipe
gesture because nothing updated the pseudo-element's transform while
the finger dragged. Add a WAAPI scrub animation that replaces itself
on every touchmove and keeps the top app-bar background aligned with
the displacement, uncovering the behind AppBar as intended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@junghyeonsu junghyeonsu force-pushed the feature/des-1079-waapi branch from 8026c1a to f1c74c9 Compare April 21, 2026 06:25
@junghyeonsu junghyeonsu requested review from SeieunYoo and te6-in April 21, 2026 07:41
fadeFromBottomAndroid and fadeIn activities should not follow the
finger during swipe back. Cache the top activity's transition style
at gesture start and only apply displacement/complete animations for
slideFromRightIOS; for other styles, rely on stackflow's normal
exit-active lifecycle so the activity's own exit animation runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@junghyeonsu
Copy link
Copy Markdown
Contributor Author

/snapshot

@github-actions
Copy link
Copy Markdown
Contributor

📦 Snapshot Release

@seed-design/cli: https://pkg.pr.new/@seed-design/cli@be24ed0
@seed-design/codemod: https://pkg.pr.new/@seed-design/codemod@be24ed0
@seed-design/css: https://pkg.pr.new/@seed-design/css@be24ed0
@seed-design/design-token: https://pkg.pr.new/@seed-design/design-token@be24ed0
@seed-design/docs-mcp: https://pkg.pr.new/@seed-design/docs-mcp@be24ed0
@seed-design/figma: https://pkg.pr.new/@seed-design/figma@be24ed0
@seed-design/mcp: https://pkg.pr.new/@seed-design/mcp@be24ed0
@seed-design/migration-index: https://pkg.pr.new/@seed-design/migration-index@be24ed0
@seed-design/react: https://pkg.pr.new/@seed-design/react@be24ed0
@seed-design/rootage-artifacts: https://pkg.pr.new/@seed-design/rootage-artifacts@be24ed0
@seed-design/rsbuild-plugin: https://pkg.pr.new/@seed-design/rsbuild-plugin@be24ed0
@seed-design/stackflow: https://pkg.pr.new/@seed-design/stackflow@be24ed0
@seed-design/stylesheet: https://pkg.pr.new/@seed-design/stylesheet@be24ed0
@seed-design/tailwind3-plugin: https://pkg.pr.new/@seed-design/tailwind3-plugin@be24ed0
@seed-design/tailwind4-theme: https://pkg.pr.new/@seed-design/tailwind4-theme@be24ed0
@seed-design/vite-plugin: https://pkg.pr.new/@seed-design/vite-plugin@be24ed0
@seed-design/webpack-plugin: https://pkg.pr.new/@seed-design/webpack-plugin@be24ed0
@seed-design/react-avatar: https://pkg.pr.new/@seed-design/react-avatar@be24ed0
@seed-design/react-checkbox: https://pkg.pr.new/@seed-design/react-checkbox@be24ed0
@seed-design/react-collapsible: https://pkg.pr.new/@seed-design/react-collapsible@be24ed0
@seed-design/react-dialog: https://pkg.pr.new/@seed-design/react-dialog@be24ed0
@seed-design/react-drawer: https://pkg.pr.new/@seed-design/react-drawer@be24ed0
@seed-design/react-field: https://pkg.pr.new/@seed-design/react-field@be24ed0
@seed-design/react-field-button: https://pkg.pr.new/@seed-design/react-field-button@be24ed0
@seed-design/react-fieldset: https://pkg.pr.new/@seed-design/react-fieldset@be24ed0
@seed-design/react-image: https://pkg.pr.new/@seed-design/react-image@be24ed0
@seed-design/react-popover: https://pkg.pr.new/@seed-design/react-popover@be24ed0
@seed-design/react-portal: https://pkg.pr.new/@seed-design/react-portal@be24ed0
@seed-design/react-primitive: https://pkg.pr.new/@seed-design/react-primitive@be24ed0
@seed-design/react-progress: https://pkg.pr.new/@seed-design/react-progress@be24ed0
@seed-design/react-pull-to-refresh: https://pkg.pr.new/@seed-design/react-pull-to-refresh@be24ed0
@seed-design/react-radio-group: https://pkg.pr.new/@seed-design/react-radio-group@be24ed0
@seed-design/react-scrollable: https://pkg.pr.new/@seed-design/react-scrollable@be24ed0
@seed-design/react-segmented-control: https://pkg.pr.new/@seed-design/react-segmented-control@be24ed0
@seed-design/react-slider: https://pkg.pr.new/@seed-design/react-slider@be24ed0
@seed-design/react-snackbar: https://pkg.pr.new/@seed-design/react-snackbar@be24ed0
@seed-design/react-supports: https://pkg.pr.new/@seed-design/react-supports@be24ed0
@seed-design/react-switch: https://pkg.pr.new/@seed-design/react-switch@be24ed0
@seed-design/react-tabs: https://pkg.pr.new/@seed-design/react-tabs@be24ed0
@seed-design/react-text-field: https://pkg.pr.new/@seed-design/react-text-field@be24ed0
@seed-design/react-toggle: https://pkg.pr.new/@seed-design/react-toggle@be24ed0
@seed-design/react-use-controllable-state: https://pkg.pr.new/@seed-design/react-use-controllable-state@be24ed0
@seed-design/dom-utils: https://pkg.pr.new/@seed-design/dom-utils@be24ed0

Triggered by @junghyeonsu via /snapshot

junghyeonsu and others added 3 commits April 23, 2026 14:53
… slot

The previous WAAPI implementation animated the top AppBar background via
`el.animate(frames, { pseudoElement: "::before" })`. That option is only
supported on Chrome 82+ / Safari 16.4+ / Firefox 97+, so on older WebViews
(e.g. Chrome 77) the pseudo-element animation became a no-op and the front
AppBar background stayed put during swipe back, hiding the back AppBar.

Relocate the background into a dedicated `appBarBackground` slot rendered
as a regular `<div>`. Query targets and the `scrubAppBarBackground` swipe
helper now operate on the real element.

- Add `background` slot to `appBar` recipe and anatomy
- Render `<div data-part="appBarBackground" aria-hidden>` inside AppBarRoot
- Move `::before` layout/background/divider styles to the slot
- Expose `topAppBarBackground` / `behindAppBarBackground` on TransitionTargets
- Regenerate css artifacts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes bundled together since they touch the same transition
coordination code path.

1. Pre-pin inline styles before push (`pinIosInlineStyles` helper).
   Browsers could paint the freshly mounted top activity at its final
   onscreen position for one frame before the compositor picked up
   `el.animate()`, so push animations appeared to start mid-way on heavy
   activities. Pin the iOS offscreen coordinates (and the Android /
   fadeIn equivalents) inline within the same task before invoking
   animate, guaranteeing the first paint lands at the start of the
   animation curve.

2. Polyfill `Animation.finished` for Chrome <84.
   Older WebViews ship WAAPI without the `finished` promise. The old
   `waitAll` simply called `a.finished.catch(...)` which threw and left
   the transition's cleanup handler unreachable — subsequent pops ran
   over stale inline styles and rendered a blank screen. `waitOne` now
   falls back to `onfinish` / `oncancel` listeners and races every
   animation against a `duration + 100ms` timeout so cleanup always
   runs even if the browser never dispatches finish.

3. Defer push animation one rAF for DOM readiness.
   Running push sync inside `useLayoutEffect` raced with stackflow's
   own subscription update — `[data-activity-is-top]` was not yet
   observable on the new top, so `findTransitionTargets` returned nulls
   and no enter animation played. Wrap the push path in
   `requestAnimationFrame` (guarded by `pendingPushRAFRef`) to let
   stackflow finish committing before querying targets. Pop stays sync
   since its top activity already lives in the DOM.

Also keep the scrub animation `appBarBgScrubAnimRef` in sync with the
new real background element renamed from the legacy pseudo naming.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend the existing AppScreen WAAPI changeset with the three follow-up
bullets for the jank, older-WebView, and pseudo-element regressions
addressed in this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@junghyeonsu
Copy link
Copy Markdown
Contributor Author

/snapshot

@github-actions
Copy link
Copy Markdown
Contributor

📦 Snapshot Release

@seed-design/cli: https://pkg.pr.new/@seed-design/cli@4d50dd9
@seed-design/codemod: https://pkg.pr.new/@seed-design/codemod@4d50dd9
@seed-design/css: https://pkg.pr.new/@seed-design/css@4d50dd9
@seed-design/design-token: https://pkg.pr.new/@seed-design/design-token@4d50dd9
@seed-design/docs-mcp: https://pkg.pr.new/@seed-design/docs-mcp@4d50dd9
@seed-design/figma: https://pkg.pr.new/@seed-design/figma@4d50dd9
@seed-design/mcp: https://pkg.pr.new/@seed-design/mcp@4d50dd9
@seed-design/migration-index: https://pkg.pr.new/@seed-design/migration-index@4d50dd9
@seed-design/react: https://pkg.pr.new/@seed-design/react@4d50dd9
@seed-design/rootage-artifacts: https://pkg.pr.new/@seed-design/rootage-artifacts@4d50dd9
@seed-design/rsbuild-plugin: https://pkg.pr.new/@seed-design/rsbuild-plugin@4d50dd9
@seed-design/stackflow: https://pkg.pr.new/@seed-design/stackflow@4d50dd9
@seed-design/stylesheet: https://pkg.pr.new/@seed-design/stylesheet@4d50dd9
@seed-design/tailwind3-plugin: https://pkg.pr.new/@seed-design/tailwind3-plugin@4d50dd9
@seed-design/tailwind4-theme: https://pkg.pr.new/@seed-design/tailwind4-theme@4d50dd9
@seed-design/vite-plugin: https://pkg.pr.new/@seed-design/vite-plugin@4d50dd9
@seed-design/webpack-plugin: https://pkg.pr.new/@seed-design/webpack-plugin@4d50dd9
@seed-design/react-avatar: https://pkg.pr.new/@seed-design/react-avatar@4d50dd9
@seed-design/react-checkbox: https://pkg.pr.new/@seed-design/react-checkbox@4d50dd9
@seed-design/react-collapsible: https://pkg.pr.new/@seed-design/react-collapsible@4d50dd9
@seed-design/react-dialog: https://pkg.pr.new/@seed-design/react-dialog@4d50dd9
@seed-design/react-drawer: https://pkg.pr.new/@seed-design/react-drawer@4d50dd9
@seed-design/react-field: https://pkg.pr.new/@seed-design/react-field@4d50dd9
@seed-design/react-field-button: https://pkg.pr.new/@seed-design/react-field-button@4d50dd9
@seed-design/react-fieldset: https://pkg.pr.new/@seed-design/react-fieldset@4d50dd9
@seed-design/react-image: https://pkg.pr.new/@seed-design/react-image@4d50dd9
@seed-design/react-popover: https://pkg.pr.new/@seed-design/react-popover@4d50dd9
@seed-design/react-portal: https://pkg.pr.new/@seed-design/react-portal@4d50dd9
@seed-design/react-primitive: https://pkg.pr.new/@seed-design/react-primitive@4d50dd9
@seed-design/react-progress: https://pkg.pr.new/@seed-design/react-progress@4d50dd9
@seed-design/react-pull-to-refresh: https://pkg.pr.new/@seed-design/react-pull-to-refresh@4d50dd9
@seed-design/react-radio-group: https://pkg.pr.new/@seed-design/react-radio-group@4d50dd9
@seed-design/react-scrollable: https://pkg.pr.new/@seed-design/react-scrollable@4d50dd9
@seed-design/react-segmented-control: https://pkg.pr.new/@seed-design/react-segmented-control@4d50dd9
@seed-design/react-slider: https://pkg.pr.new/@seed-design/react-slider@4d50dd9
@seed-design/react-snackbar: https://pkg.pr.new/@seed-design/react-snackbar@4d50dd9
@seed-design/react-supports: https://pkg.pr.new/@seed-design/react-supports@4d50dd9
@seed-design/react-switch: https://pkg.pr.new/@seed-design/react-switch@4d50dd9
@seed-design/react-tabs: https://pkg.pr.new/@seed-design/react-tabs@4d50dd9
@seed-design/react-text-field: https://pkg.pr.new/@seed-design/react-text-field@4d50dd9
@seed-design/react-toggle: https://pkg.pr.new/@seed-design/react-toggle@4d50dd9
@seed-design/react-use-controllable-state: https://pkg.pr.new/@seed-design/react-use-controllable-state@4d50dd9
@seed-design/dom-utils: https://pkg.pr.new/@seed-design/dom-utils@4d50dd9

Triggered by @junghyeonsu via /snapshot

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