Skip to content

Commit c27f8fd

Browse files
authored
Merge pull request #210 from perf/chrome-motion-smoothness
perf(ui): unify chrome motion for smoother drawers and marketing
2 parents 25e3283 + 66a4729 commit c27f8fd

71 files changed

Lines changed: 2797 additions & 1300 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/AGENTS_REFERENCE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
- Contribution flow: PRs target `develop`, not `main`; PRs to `main` are gated by `.github/workflows/ensure-pr-source-develop.yml`
2222
- Algorithm inventory: **45** algorithms across **5** categories (14 sorting, 9 pathfinding, 9 searching, 6 tree traversal, 7 graph algorithms)
2323
- Python parity: **45** `.py` files under `src/algorithms/python/` (one per algorithm)
24-
- Test surface: **151** `*.test.js` / `*.test.jsx` files under `src/` (~**1,817** tests)
25-
- Source surface: **223** non-test `*.js` / `*.jsx` files under `src/`, **45** Python files under `src/algorithms/python/`, **1** `src/index.css`
24+
- Test surface: **152** `*.test.js` / `*.test.jsx` files under `src/` (~**1,823** tests)
25+
- Source surface: **224** non-test `*.js` / `*.jsx` files under `src/`, **45** Python files under `src/algorithms/python/`, **1** `src/index.css`
2626
- Path alias: `@/``src/` (Vite + Vitest)
2727

2828
## Source Of Truth
@@ -40,6 +40,7 @@
4040
- `src/registry/graphAlgorithmRegistry.js` — profiles, scenarios, `GRAPH_ALGORITHM_KEYS`, `GRAPH_ALGORITHM_GROUPS`
4141
- `src/config/algorithmConfig.js``useAlgorithmConfig()`, `buildAlgorithmsForCategory()`, `buildGroupsForCategory()`
4242
- `src/config/settingsConfig.js``useSettingsConfig()` (grid size + speed option labels)
43+
- `src/motion/chromeMotion.js` — shared chrome UI motion presets (drawers/modals/menus/marketing); do not invent local spring params for chrome
4344
- `docs/ARCHITECTURE.md`, `docs/DEVELOPMENT.md`, `README.md`, and `CONTRIBUTING.md` are useful, but they can lag behind the live category/runtime wiring.
4445
- Runtime completeness tests are important because the app depends heavily on synchronized registries (`categoryRuntimeCompleteness.test.js`).
4546

docs/ARCHITECTURE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,20 @@ description: getAlgorithmDescription(
610610
611611
## Animation System
612612
613+
### Chrome motion (UI chrome)
614+
615+
Non-visualization motion (drawers, modals, menus, banners, marketing entrances, completion overlays) uses shared presets in `src/motion/chromeMotion.js`.
616+
617+
**Contract:**
618+
- Tween easing (`CHROME_EASE`, ~280ms) — do **not** invent local spring params for drawers/modals/menus
619+
- Sliding panels: transform (`x`/`y`) only — no opacity on the panel itself
620+
- Overlays: solid dim classes from the module — never animate `backdrop-blur` with opacity
621+
- Respect `useReducedMotion()` (duration `0` / skip offset)
622+
- One Framer tree per chrome surface (no dual desktop+mobile `motion` mounts)
623+
- Defer heavy content (e.g. Monaco) until drawer `onAnimationComplete`
624+
625+
Visualization step springs (`ArrayBar`, `GridCell`, node fills) and Remotion remain separate from this contract.
626+
613627
### Framer Motion Integration
614628
615629
**ArrayBar Component Animation:**

index.html

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,6 @@
211211
}
212212
})();
213213
</script>
214-
215-
<!-- Cloudflare Turnstile — invisible CAPTCHA for bot signup prevention -->
216-
<script
217-
src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
218-
async
219-
defer
220-
></script>
221214
</head>
222215
<body>
223216
<div id="root"></div>

src/components/AlgorithmDropdown.jsx

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@
44
* See LICENSE for details.
55
*/
66

7-
import { motion, AnimatePresence } from 'framer-motion';
7+
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
88
import { Check, CaretDown, Lock, Star } from '@phosphor-icons/react';
99
import { useTranslation } from 'react-i18next';
1010
import { canAccessAlgorithm } from '@/services/entitlementService';
11+
import {
12+
menuInitial,
13+
menuAnimate,
14+
menuExit,
15+
menuTransition,
16+
getChromeTransition,
17+
CHROME_DURATION_FAST,
18+
} from '@/motion/chromeMotion';
1119

1220
function AlgorithmDropdown({
1321
algorithms,
@@ -27,6 +35,7 @@ function AlgorithmDropdown({
2735
isAuthenticated,
2836
}) {
2937
const { t } = useTranslation();
38+
const reduceMotion = useReducedMotion();
3039

3140
const handleStarClick = (event, algo) => {
3241
event.stopPropagation();
@@ -58,7 +67,7 @@ function AlgorithmDropdown({
5867
</div>
5968
<motion.div
6069
animate={{ rotate: isDropdownOpen ? 180 : 0 }}
61-
transition={{ duration: 0.2 }}
70+
transition={getChromeTransition(reduceMotion, CHROME_DURATION_FAST)}
6271
>
6372
<CaretDown
6473
size={20}
@@ -70,26 +79,21 @@ function AlgorithmDropdown({
7079
<AnimatePresence>
7180
{isDropdownOpen && (
7281
<motion.div
73-
initial={{ opacity: 0, y: -10 }}
74-
animate={{ opacity: 1, y: 0 }}
75-
exit={{ opacity: 0, y: -10 }}
76-
transition={{ duration: 0.2 }}
82+
initial={menuInitial(reduceMotion)}
83+
animate={menuAnimate()}
84+
exit={menuExit(reduceMotion)}
85+
transition={menuTransition(reduceMotion)}
7786
className="absolute z-10 w-full mt-2 bg-surface-elevated border-2 border-[var(--color-border-strong)] rounded-lg shadow-xl overflow-hidden max-h-72 overflow-y-auto algo-dropdown"
7887
>
79-
{algorithmGroups.map((group, groupIndex) => (
88+
{algorithmGroups.map(group => (
8089
<div key={group.label}>
8190
<div className="px-4 py-2 text-xs font-bold text-text-tertiary uppercase tracking-wider sticky top-0 bg-surface-elevated z-10 border-b border-[var(--color-border-strong)]">
8291
{group.label}
8392
</div>
84-
{group.algorithms.map((algoValue, index) => {
93+
{group.algorithms.map(algoValue => {
8594
const algo = algorithms.find(a => a.value === algoValue);
8695
if (!algo) return null;
8796

88-
const itemIndex =
89-
algorithmGroups
90-
.slice(0, groupIndex)
91-
.reduce((acc, g) => acc + g.algorithms.length, 0) + index;
92-
9397
const isLocked =
9498
categoryType &&
9599
!canAccessAlgorithm(algo.value, categoryType, user);
@@ -98,11 +102,8 @@ function AlgorithmDropdown({
98102
isFavorite?.(categoryType, algo.value) ?? false;
99103

100104
return (
101-
<motion.div
105+
<div
102106
key={algo.value}
103-
initial={{ opacity: 0, x: -20 }}
104-
animate={{ opacity: 1, x: 0 }}
105-
transition={{ delay: itemIndex * 0.03 }}
106107
className={`group flex items-center w-full transition-colors duration-150 hover:bg-surface-elevated focus-within:bg-surface-elevated ${
107108
isSelected
108109
? 'bg-theme-primary-light text-theme-primary dark:text-white'
@@ -139,22 +140,11 @@ function AlgorithmDropdown({
139140
</span>
140141
</div>
141142
{isSelected ? (
142-
<motion.div
143-
initial={{ scale: 0 }}
144-
animate={{ scale: 1 }}
145-
transition={{
146-
type: 'spring',
147-
stiffness: 500,
148-
damping: 25,
149-
}}
150-
className="shrink-0 ms-2"
151-
>
152-
<Check
153-
size={18}
154-
weight="bold"
155-
className="text-[#3b82f6] dark:text-white"
156-
/>
157-
</motion.div>
143+
<Check
144+
size={18}
145+
weight="bold"
146+
className="shrink-0 ms-2 text-[#3b82f6] dark:text-white"
147+
/>
158148
) : isLocked ? (
159149
<Lock
160150
size={18}
@@ -193,7 +183,7 @@ function AlgorithmDropdown({
193183
/>
194184
</button>
195185
)}
196-
</motion.div>
186+
</div>
197187
);
198188
})}
199189
</div>

0 commit comments

Comments
 (0)