diff --git a/.agents/skills/frontend-workflow/SKILL.md b/.agents/skills/frontend-workflow/SKILL.md new file mode 100644 index 0000000000..a9015c47cd --- /dev/null +++ b/.agents/skills/frontend-workflow/SKILL.md @@ -0,0 +1,110 @@ +--- +name: frontend-workflow +description: Frontend and UI work in surfsense_web and surfsense_local/frontend — building, refactoring, styling, animating, or reviewing React and Next.js components, shadcn/ui, color tokens and themes, visual polish, and motion. Load this first for any UI task, including adding or changing a shadcn/ui component (dialog, button, dropdown, form, sidebar, table) in a tree with components.json — it owns the color palette, polish rules, and the precedence order, then routes to the separate shadcn skill for component wiring and the CLI. Use for any frontend component, page, style, animation, hover state, icon, layout, or UI review task. +--- + +# Frontend Workflow + +Single entry point for frontend work in `surfsense_web` and +`surfsense_local/frontend`. Both are React + Tailwind + shadcn/ui with their own +`components.json`. Detect which tree the task touches, and treat that tree's +`package.json`, `components.json`, and styling setup as authoritative. All +guidance lives in this skill's own folders; load only what the task touches. + +## Reference Map + +| Read this | When | +|---|---| +| [react-performance/SKILL.md](./react-performance/SKILL.md) | React or Next.js code — components, pages, data fetching, bundles, re-renders | +| [../shadcn/SKILL.md](../shadcn/SKILL.md) | shadcn/ui components, or any project with `components.json`. Separate skill — it inspects the project live and grants its own CLI. | +| [color/SKILL.md](./color/SKILL.md) | Colors, themes, charts, design tokens, borders, shadows | +| [polish/SKILL.md](./polish/SKILL.md) | Typography, surfaces, icons, micro-interactions, enter/exit transitions | +| [motion/apple-design.md](./motion/apple-design.md) | Gesture-driven or physical motion — drag, swipe, sheets, springs, momentum, interruptible transitions, translucent materials | + +Each entry is an index. Open its supporting files only when the touched code +needs them: + +- `react-performance/rules/` holds one file per rule. Load the applicable ones. + `react-performance/rules-compiled.md` is the same rules compiled into one + document — do not load it by default. +- `polish/` splits into `typography.md`, `surfaces.md`, `animations.md`, + `icons.md`, `performance.md`. +- `../shadcn/rules/` splits by concern; `../shadcn/cli.md`, `registry.md`, + `customization.md` cover tooling and theming. +- `color/PALETTE.css` is the canonical palette contract. + +Do not load motion references for work with no motion concern. +Apple-style motion is for gesture, physics, and material work; a hover state or +a color change does not need it. + +## Workflow + +1. **Understand the task** + - Inspect the relevant implementation and trace the affected interaction. + - Clarify only decisions that materially change behavior or design. + - Reuse existing components, helpers, tokens, and patterns. + +2. **Select guidance** + - Use the reference map above. + - Read detailed files only when the touched code needs them. + - Treat current project configuration and installed APIs as authoritative. + +3. **Implement** + - Make the smallest complete change that satisfies the request. + - Preserve established visual language and component APIs. + - Cover loading, empty, error, disabled, responsive, keyboard, focus, and + reduced-motion states when they are relevant. + +4. **Validate** + - Run the smallest relevant lint, type, and test checks. + - For visible interaction changes, verify the rendered behavior when a + runnable frontend is available. + - If visual details or motion changed, apply the polish review only after + functional implementation is complete and resolve blocking findings + within scope. + +5. **Report** + - Summarize the user-visible result, checks run, and unresolved risks. + - Use the polish review format only when the user requested a review. For + implementation tasks, include relevant visual or motion findings in the + normal completion summary. + +## Precedence and Conflicts + +Resolve conflicting guidance in this order: + +1. The user's explicit requirements. +2. Correctness, security, and accessibility. +3. Existing project conventions and configuration. +4. The canonical color system and shadcn/ui composition rules. +5. React and Next.js performance guidance. +6. Motion behavior for gesture-driven and interruptible interactions. +7. Interface and motion polish. + +Specific overlaps: + +- **Icons** — `../shadcn/rules/icons.md` governs icon usage inside shadcn + components (`data-icon`, sizing, passing icons as objects). `polish/icons.md` + governs stroke weight, optical detail, states via `currentColor`, and RTL + flipping. Apply the shadcn rule to component wiring, the polish rule to + visual detail. +- **Color** — `color/SKILL.md` and `color/PALETTE.css` are canonical. Where + `../shadcn/rules/styling.md` or `../shadcn/customization.md` describe theming, + follow them for mechanism and the palette for values. +- **Motion** — `motion/apple-design.md` governs how motion behaves; + `polish/animations.md` governs concrete values and static detail. Where they + conflict, prefer springs and current-value interpolation for anything the + user can touch or interrupt, and CSS transitions for everything else. + +Never sacrifice correctness or accessibility for visual polish or a +micro-optimization. If a rule conflicts with the installed library version or +project configuration, verify the current API and follow the project's actual +version. + +## Notes + +The `SKILL.md` and `.md` files inside `react-performance/`, `color/`, `polish/`, +and `motion/` retain their original frontmatter from when they were separate +skills. That frontmatter is inert here — these are reference files, not +independently discovered skills. Read their bodies and ignore their +`name`, `description`, and invocation fields. diff --git a/.agents/skills/frontend-workflow/color/PALETTE.css b/.agents/skills/frontend-workflow/color/PALETTE.css new file mode 100644 index 0000000000..d9e2283e92 --- /dev/null +++ b/.agents/skills/frontend-workflow/color/PALETTE.css @@ -0,0 +1,146 @@ +:root { + --card: #fbfaf7; + --ring: #2e2e2e; + --input: #d6d6d6; + --muted: #ebeae6; + --accent: #e5e4df; + --border: #dedede; + --radius: 0.5rem; + --chart-1: #f26a4b; + --chart-2: #1e1e1e; + --chart-3: #626262; + --chart-4: #9b9b9b; + --chart-5: #c9c9c9; + --popover: #fbfaf7; + --primary: #2e2e2e; + --sidebar: #f3f2ee; + --spacing: 0.25rem; + --font-mono: "IBM Plex Mono", monospace; + --font-sans: "Instrument Sans", sans-serif; + --secondary: #f1f0ec; + --background: #f7f6f2; + --app-shell: #f3f2ee; + --font-serif: "Newsreader", serif; + --foreground: #1e1e1e; + --destructive: #dc2626; + --notice: #3f74c8; + --shadow-blur: 10px; + --shadow-color: #000000; + --sidebar-ring: #2e2e2e; + --shadow-spread: 0px; + --letter-spacing: 0.01em; + --shadow-opacity: 0.1; + --sidebar-accent: #e5e4df; + --sidebar-border: #dedede; + --card-foreground: #1e1e1e; + --shadow-offset-x: 0px; + --shadow-offset-y: 4px; + --sidebar-primary: #2e2e2e; + --muted-foreground: #626262; + --accent-foreground: #2e2e2e; + --popover-foreground: #1e1e1e; + --primary-foreground: #f7f7f7; + --sidebar-foreground: #1e1e1e; + --secondary-foreground: #2e2e2e; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #2e2e2e; + --sidebar-primary-foreground: #f7f7f7; +} + +.dark { + --card: #1c1c1c; + --ring: #d1cfc0; + --input: #2c2c2c; + --muted: #2a2a2a; + --accent: #363636; + --border: #2c2c2c; + --radius: 0.5rem; + --chart-1: #f26a4b; + --chart-2: #d9cfc2; + --chart-3: #8e8a83; + --chart-4: #5c5a56; + --chart-5: #3b3b3b; + --popover: #1c1c1c; + --primary: #d9aa90; + --sidebar: #141414; + --spacing: 0.25rem; + --font-mono: "IBM Plex Mono", monospace; + --font-sans: "Instrument Sans", sans-serif; + --secondary: #222222; + --background: #141414; + --app-shell: #101010; + --font-serif: "Newsreader", serif; + --foreground: #e8e3da; + --destructive: #ef4444; + --notice: #3f74c8; + --shadow-blur: 15px; + --shadow-color: #000000; + --sidebar-ring: #d1cfc0; + --shadow-spread: 0px; + --letter-spacing: 0.01em; + --shadow-opacity: 0.3; + --sidebar-accent: #363636; + --sidebar-border: #2c2c2c; + --card-foreground: #e8e3da; + --shadow-offset-x: 0px; + --shadow-offset-y: 6px; + --sidebar-primary: #d1cfc0; + --muted-foreground: #8e8a83; + --accent-foreground: #d1cfc0; + --popover-foreground: #e8e3da; + --primary-foreground: #363636; + --sidebar-foreground: #e8e3da; + --secondary-foreground: #d1cfc0; + --destructive-foreground: #ffffff; + --sidebar-accent-foreground: #d1cfc0; + --sidebar-primary-foreground: #363636; +} + +@theme inline { + --color-card: var(--card); + --color-ring: var(--ring); + --color-input: var(--input); + --color-muted: var(--muted); + --color-accent: var(--accent); + --color-border: var(--border); + --color-radius: var(--radius); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-popover: var(--popover); + --color-primary: var(--primary); + --color-sidebar: var(--sidebar); + --color-spacing: var(--spacing); + --color-font-mono: var(--font-mono); + --color-font-sans: var(--font-sans); + --color-secondary: var(--secondary); + --color-background: var(--background); + --color-app-shell: var(--app-shell); + --color-font-serif: var(--font-serif); + --color-foreground: var(--foreground); + --color-destructive: var(--destructive); + --color-notice: var(--notice); + --color-shadow-blur: var(--shadow-blur); + --color-shadow-color: var(--shadow-color); + --color-sidebar-ring: var(--sidebar-ring); + --color-shadow-spread: var(--shadow-spread); + --color-letter-spacing: var(--letter-spacing); + --color-shadow-opacity: var(--shadow-opacity); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-border: var(--sidebar-border); + --color-card-foreground: var(--card-foreground); + --color-shadow-offset-x: var(--shadow-offset-x); + --color-shadow-offset-y: var(--shadow-offset-y); + --color-sidebar-primary: var(--sidebar-primary); + --color-muted-foreground: var(--muted-foreground); + --color-accent-foreground: var(--accent-foreground); + --color-popover-foreground: var(--popover-foreground); + --color-primary-foreground: var(--primary-foreground); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-secondary-foreground: var(--secondary-foreground); + --color-destructive-foreground: var(--destructive-foreground); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); +} diff --git a/.agents/skills/frontend-workflow/color/SKILL.md b/.agents/skills/frontend-workflow/color/SKILL.md new file mode 100644 index 0000000000..6ed259a257 --- /dev/null +++ b/.agents/skills/frontend-workflow/color/SKILL.md @@ -0,0 +1,86 @@ + +--- +name: color-system +description: Applies and reviews SurfSense's canonical light and dark color palette, semantic design tokens, typography, charts, borders, shadows, and theme mappings. Use when creating or changing frontend colors, themes, component styling, data visualizations, or design tokens. +--- + +# SurfSense Color System + +Use [PALETTE.css](PALETTE.css) as the canonical palette contract. Preserve its +token names and values unless the user explicitly requests a palette change. + +## Principles + +1. Use semantic tokens such as `background`, `foreground`, `primary`, + `muted`, `accent`, `destructive`, `border`, and their foreground pairs. + Do not use raw hex values in components. +2. Use the matching foreground token for text and icons placed on a semantic + surface: `primary-foreground` on `primary`, `card-foreground` on `card`, + and so on. +3. Use `chart-1` through `chart-5` for data series. Do not repurpose chart + colors as component state colors. +4. Use `ring` for focus indicators and `border` or `input` for boundaries. + Never remove a visible keyboard focus indicator. +5. Use `muted-foreground` only for secondary text. Do not use it for small or + essential text unless its contrast passes WCAG. +6. Use `destructive` only for destructive actions, errors, or dangerous + states. Do not use chart colors to communicate errors. +7. Support both `:root` and `.dark`; never add a light-only semantic token. +8. Prefer existing semantic tokens over creating new aliases. Add a token only + when it represents a reusable semantic role that the palette does not cover. + +## Workflow + +1. Locate the active global CSS file from the target app's `components.json`; + this repository contains more than one frontend. +2. Compare the active theme with [PALETTE.css](PALETTE.css). Do not overwrite + unrelated CSS, animation, layout, or framework directives. +3. Apply palette values at the global token layer, not inside individual + components. +4. In components, use the project's semantic utility classes or CSS variables, + for example `bg-background text-foreground`, `bg-card + text-card-foreground`, and `border-border`. +5. Check affected foreground/background pairs in both themes. WCAG targets: + 4.5:1 for normal text, 3:1 for large text and meaningful UI boundaries. +6. Verify focus, hover, active, selected, disabled, destructive, chart, and + sidebar states when affected. + +## Guardrails + +- Do not invent intermediate shades to make one component look better. +- Do not use opacity to compensate for an incorrect semantic token when it + reduces text contrast. +- Do not replace the palette wholesale when the requested change concerns one + component. +- Report a contrast failure instead of silently changing canonical values. +- Treat the palette's `@theme inline` block as Tailwind v4 configuration. + Before applying it, verify the target app uses Tailwind v4 and preserve any + required non-color namespaces already present in its global stylesheet. + +## Known Accessibility Constraint + +The canonical dark `muted-foreground` (`#7a706a`) does not reach 4.5:1 for +normal text on dark `background` (4.11:1), `card` (3.90:1), or `muted` +(3.64:1). Preserve the palette, but do not use this token for essential or +small normal-weight dark-mode text. Report the conflict and request a palette +decision when no existing foreground token fits. + +## Coordination + +- For shadcn/ui composition and styling, also use `../../shadcn/SKILL.md`. +- For React or Next.js implementation, also use + `../react-performance/SKILL.md`. +- For interface polish, color transitions, or reduced-motion behavior, use + `../polish/SKILL.md` after implementation. + +## Output + +For implementation tasks, report: + +- which app and global stylesheet received the palette; +- whether both themes were updated; +- contrast or state risks that remain; +- validation performed. + +For audits, cite each issue by token pair and usage location, then recommend a +semantic-token correction before proposing a new color. diff --git a/.agents/skills/frontend-workflow/motion/apple-design.md b/.agents/skills/frontend-workflow/motion/apple-design.md new file mode 100644 index 0000000000..3dbfbbd5cd --- /dev/null +++ b/.agents/skills/frontend-workflow/motion/apple-design.md @@ -0,0 +1,283 @@ + +--- +name: apple-design +description: Apple's approach to interface design and fluid, physical motion, translated for the web. Use when building or reviewing gesture-driven UI, spring animations, drag/swipe/sheet interactions, momentum and interruptible transitions, translucent materials and depth, typography (optical sizing, tracking, leading), reduced-motion, or the design foundations (feedback, spatial consistency, restraint) behind Apple-style interfaces. +--- + +# Apple Design + +How Apple builds interfaces that stop feeling like a computer and start feeling like an extension of you. This knowledge comes from Apple's WWDC design talks — chiefly *Designing Fluid Interfaces* (WWDC 2018) — distilled and translated into the web platform (CSS, Pointer Events, `requestAnimationFrame`, spring libraries like Motion/Framer Motion). + +The through-line: **an interface feels alive when motion starts from the current on-screen value, inherits the user's velocity, projects momentum forward, and can be grabbed and reversed at any instant.** Springs are the tool that makes all of this natural, because they are inherently interruptible and velocity-aware. + +## The Core Idea + +> "When we align the interface to the way we think and move, something magical happens — it stops feeling like a computer and starts feeling like a seamless extension of us." + +An interface is fluid when it behaves like the physical world: things respond instantly, move continuously, carry momentum, resist at boundaries, and can be redirected mid-motion. Everything below is a way to get closer to that. + +Apple frames design as serving four human needs: **safety/predictability, understanding, achievement, and joy.** Every rule here serves one of them. + +## 1. Response — kill latency + +The moment lag appears, the feeling of directness "falls off a cliff." Response is the foundation everything else is built on. + +- **Respond on pointer-down, not on release.** Highlight a button the instant it's pressed. Waiting for `click`/touch-up to show feedback feels dead. +- **Be vigilant about every latency.** Audit debounces, artificial timers, transition waits, and the ~300ms tap delay. Anything on the input path that isn't essential is a regression. +- **Feedback must be continuous *during* the interaction, not just at the end.** For a drag, slider, or drawer, update the UI 1:1 with the pointer the whole way through — never animate only when the gesture completes. + +```css +/* Feedback lives on the press, and it's instant */ +.button:active { + transform: scale(0.97); + transition: transform 100ms ease-out; +} +``` + +## 2. Direct manipulation — 1:1 tracking + +> "Touch and content should move together." + +When the user drags something, it must stay glued to the finger — and respect the offset from *where they grabbed it*. Snapping to the element's center on grab breaks the illusion immediately. + +- Use Pointer Events with `setPointerCapture` so tracking continues even when the pointer leaves the element's bounds. +- Track a short **velocity/position history** (last few `pointermove` events), not just the current point — you'll need velocity at release. + +```js +el.addEventListener('pointerdown', (e) => { + el.setPointerCapture(e.pointerId); + const grabOffset = e.clientY - el.getBoundingClientRect().top; // respect where they grabbed + // ...track position + timestamp history for velocity +}); +``` + +## 3. Interruptibility — the single most important principle + +> "The thought and the gesture happen in parallel." + +Every animation must be interruptible and redirectable at any moment. A user must be able to grab a moving element mid-flight and reverse it without waiting for the animation to finish. A closing modal the user grabs again should follow the finger — not finish closing first, then reopen. + +- **Never lock out input during a transition.** +- **Always animate from the *presentation* (current) value, never the target value.** On interrupt, read the element's live on-screen transform and start the new animation from there. Starting from the logical/target value causes a visible jump. +- **Avoid CSS transitions and `@keyframes` for anything gesture-driven** — they can't be smoothly grabbed and reversed mid-flight. Springs animate from the current value by default, which is exactly what interruption needs. +- **When a gesture reverses, blend velocity — don't hard-cut it.** Replacing one animation with another at a reversal creates a velocity discontinuity, a "brick wall." Spring libraries that carry velocity through a re-target avoid it. (This is what iOS's *additive animations* do natively; on the web, choose a spring library that re-targets from the current velocity.) +- **Decompose 2D motion into independent X and Y springs.** A single spring on a 2D distance desyncs when X and Y have different velocities. + +## 4. Behavior over animation — use springs + +> "Think of animation as a conversation between you and the object, not something prescribed by the interface." + +A pre-scripted, fixed-duration animation can't respond to new input. A spring can — new input just changes the target, and the motion stays continuous. Reach for springs for anything a user can touch. + +Apple deliberately replaced the physics triplet (mass/stiffness/damping) with two designer-friendly parameters. Think in these: + +- **Damping ratio** — controls overshoot. `1.0` = critically damped, no bounce, smooth settle. `< 1.0` = overshoots and oscillates. Lower = bouncier. +- **Response** — how quickly the value reaches the target, in seconds. Lower = snappier. **This is not "duration"** — a spring has no fixed duration; its settle time emerges from the parameters. + +**Defaults:** +- Start most UI at **damping `1.0`** (critically damped) — graceful and non-distracting. +- Add bounce (**damping ~`0.8`**) **only when the gesture itself carried momentum** (a flick, a throw, a drag release). Overshoot on a menu that just faded in feels wrong; overshoot on a card you flicked feels right. + +**Concrete values Apple ships:** + +| Interaction | Damping | Response | +| --- | --- | --- | +| Move / reposition (e.g. PiP) | `1.0` | `0.4` | +| Rotation | `0.8` | `0.4` | +| Drawer / sheet | `0.8` | `0.3` | + +**Web mapping (Motion / Framer Motion):** the `bounce` + `duration` spring API maps closely to Apple's damping + response. A safe house style is `damping: 1.0` springs everywhere by default; reserve bounce for momentum-driven, physical interactions. + +```js +import { animate } from 'motion'; + +// Critically damped default (no overshoot) +animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 }); + +// Momentum interaction — a little bounce, only because a flick preceded it +animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 }); +``` + +## 5. Velocity handoff — the seam between drag and animation + +When a gesture ends, the animation must **continue at the finger's exact velocity**, so there's no visible seam between dragging and animating. This is the detail that most separates "fluid" from "fine." + +Pass the pointer's release velocity as the spring's initial velocity. Some spring APIs want **relative** velocity — normalize it by the remaining distance to the target: + +``` +relativeVelocity = gestureVelocity / (targetValue − currentValue) +``` + +Example: element at `y=50`, target `y=150` (100px to go), finger moving 50px/s → initial spring velocity = `50 / 100 = 0.5`. Framer Motion / Motion take absolute px/s velocity directly (`velocity` option), so you usually hand it the raw value. + +## 6. Momentum projection — animate to where the gesture is *going* + +> "Take a small input and make a big output." + +Don't snap to the nearest boundary from the *release point*. Use velocity to **project the resting position** — exactly like scroll deceleration — then snap to the target nearest that projected point. This is what makes a flick feel like it throws the element. + +Apple's exact projection function (from the *Designing Fluid Interfaces* sample code): + +```js +// decelerationRate ≈ 0.998 for normal scroll feel; 0.99 for snappier +function project(initialVelocity /* px/s */, decelerationRate = 0.998) { + return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate); +} + +const projectedEndpoint = currentPosition + project(releaseVelocity); +const target = nearestSnapPoint(projectedEndpoint); // choose target from the projection +animateSpringTo(target, { velocity: releaseVelocity }); // then hand off velocity (§5) +``` + +Note: the physics-textbook `v²/(2·decel)` is *not* what Apple ships — use the exponential-decay form above. This is the standard behavior in good bottom-sheets and carousels (Vaul, Embla). + +## 7. Spatial consistency — symmetric paths, anchored origins + +> "If something disappears one way, we expect it to emerge from where it came." + +- **Enter and exit along the same path.** A panel that slides in from the right must dismiss to the right. In-from-right / out-the-bottom feels disconnected and confusing. +- **Anchor interactions to their source.** A menu, popover, or sheet should originate from the element that triggered it — set `transform-origin` to the trigger, so the spatial relationship between button and content is obvious. (This is the same origin-awareness point as popovers scaling from their trigger, not their center.) +- **Mirror the easing on reversible transitions** so the outbound path matches the return path (use inverse cubic-bézier control points for the two directions). + +## 8. Hint in the direction of the gesture + +Humans predict a final state from a trajectory. Intermediate motion should telegraph where things are going — Control Center modules "grow up and out toward your finger." Make the in-between frames point at the outcome, not just interpolate blindly to it. + +## 9. Rubber-banding — soft boundaries + +At an edge, resist progressively instead of stopping hard. A hard stop reads as "frozen"; continuous resistance reads as "responsive, but there's nothing more here." Apply damping that increases the further past the boundary the user drags. + +```js +// The further past the bound, the less the element follows — real things slow before they stop +function rubberband(overshoot, dimension, constant = 0.55) { + return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot)); +} +``` + +## 10. Gesture design details (the "feel" checklist) + +- **Tap:** highlight on touch-*down* (instant), commit on touch-*up*. Add ~10px of hysteresis/hit padding around the target, and allow cancel-by-dragging-away and back. +- **Drag/swipe:** require a small movement threshold (hysteresis, ~10px) before committing to a direction, then track 1:1. +- **Detect all plausible gestures in parallel from the first move**, then confidently cancel the losers once intent is clear. Avoid recognizers that only report a *final* state (`swipeleft`-type events) — they throw away the continuous tracking you need for feedback. +- **Minimize disambiguation delays.** Double-tap detection unavoidably delays single taps; only pay that cost where double-tap truly exists. + +## 11. Frame-level smoothness + +Smoothness is about *what's in the frames*, not just the frame rate. + +- Keep the per-frame positional change below the perception threshold to avoid strobing. +- For very fast motion, a subtle **motion blur / stretch** encodes speed and reads better than a hard sharp streak. +- `requestAnimationFrame` is the web's display-synced clock (Apple uses `CADisplayLink`). Animate only compositor-friendly properties — `transform` and `opacity` — and hint with `will-change` where motion is imminent. + +## 12. Materials & depth — translucency conveys hierarchy + +Apple uses translucent materials as a floating functional layer that brings structure without stealing focus. On the web, approximate with `backdrop-filter`. + +- **Build nav/toolbars/sheets as translucent layers** (`backdrop-filter: blur()` + a semi-transparent background) with content scrolling underneath — not opaque bars that consume a fixed strip. +- **Material weight encodes hierarchy:** darker/heavier materials separate structural regions (sidebars); lighter materials draw attention to interactive elements (buttons). **Never stack a light translucent surface on another** — legibility collapses. +- **Bigger surfaces should read as thicker:** stronger blur + a deeper shadow than small chips. Consider context-aware shadow — heavier over busy/text content for separation, lighter over plain backgrounds. +- **Dim to focus, separate to keep flow.** A modal task pairs the surface with a dimming scrim and pushes the background back/down. A parallel, non-blocking panel uses translucency and offset *without* a scrim so the flow isn't broken. For stacked sheets, progressively dim and push back each parent layer. +- **Vibrancy keeps text legible over changing backgrounds.** Over blurred/translucent surfaces, don't use flat gray text — use higher-contrast, slightly heavier weight, and a small letter-spacing bump. Put color on a solid layer, not the translucent foreground. +- **Scroll edge effects, not hard dividers.** Instead of a 1px border under a sticky header, fade a small blur/gradient mask where content meets floating chrome — only where floating UI actually overlaps content. +- **Materialize, don't just fade.** For glass/blur surfaces, animate blur radius and scale together on enter/exit, so the surface reads as a real material arriving rather than a plain opacity fade. + +```css +.toolbar { + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(20px) saturate(180%); + border-top: 1px solid rgba(255, 255, 255, 0.4); /* bright top edge = light catching the material */ +} +``` + +## 13. Multimodal feedback — motion + sound + haptics + +Three rules for combining senses (from *Designing Audio-Haptic Experiences*): + +1. **Causality** — it must be obvious what caused the feedback. Trigger it on the actual causal event (the toggle flipping, the item snapping home), and match its character to the action's physicality. +2. **Harmony** — the visual, the sound, and the haptic must fire on the **same frame**. Latency between them destroys the illusion. Don't let a CSS transition lag the audio/haptic (Vibration API). +3. **Utility** — add feedback only where it earns its place. Reserve haptics/sound for meaningful moments (success, error, commit, snap). Over-feedback trains users to ignore all of it. + +## 14. Reduced motion & accessibility + +Reduced motion doesn't mean *no* feedback — it means a gentler, non-vestibular equivalent. Respond to three independent signals and bake them into your components: + +- **`prefers-reduced-motion: reduce`** — replace slides/springs/parallax with short opacity **cross-fades or static transitions**. Drop elastic/overshoot. Keep opacity/color changes that aid comprehension. +- **`prefers-reduced-transparency: reduce`** — make translucent surfaces frostier/solid: raise background opacity, drop the blur. +- **`prefers-contrast: more`** — near-solid backgrounds with a defined, contrasting border. + +Also: avoid full-viewport moving backgrounds, slow looping oscillations (near 0.2 Hz / one cycle per 5s), and abrupt brightness jumps (ease dark↔light theme changes). Make large moving objects semi-transparent while they travel, and fade big surfaces out during a large reposition and back in once settled. + +```css +@media (prefers-reduced-motion: reduce) { + .sheet { transition: opacity 200ms ease; transform: none !important; } +} +@media (prefers-reduced-transparency: reduce) { + .toolbar { background: white; backdrop-filter: none; } +} +``` + +## 15. Typography — optical sizing, tracking, leading + +Apple designs type to change shape with size; the same discipline applies on the web. (From *The Details of UI Typography*, WWDC 2020.) + +- **Tracking (letter-spacing) is size-specific — never one value for all sizes.** Large display text wants *negative* tracking (letters read too far apart as they grow); small text wants slightly *positive* tracking for legibility. A fixed `letter-spacing` is wrong somewhere. Tighten headings, leave body near `0`. +- **Leading (line-height) tracks size inversely.** Tight on large headings, looser on body copy. Increase it for scripts with tall ascenders/descenders; tighten it for dense, information-heavy UI. +- **Build hierarchy from weight + size + leading as a set,** not size alone. Emphasize with weight — it adds presence without taking more space. +- **Respect the user's text-size setting** (Dynamic Type). Scale layout *with* the text — spacing in `rem`/`em`, not fixed px — so a larger font doesn't break the layout. +- **Default to the platform's system font** before a custom face; it already ships optical sizing, tracking tables, and legibility tuning. Override only with a reason. + +```css +:root { font: 100%/1.5 system-ui, sans-serif; } /* body: system font, comfortable leading */ + +.display { + font-size: clamp(2rem, 5vw, 4rem); + line-height: 1.05; /* tight leading for large text */ + letter-spacing: -0.02em; /* negative tracking as it grows */ + font-optical-sizing: auto; +} +``` + +## 16. Design foundations — the eight principles + +The motion and craft above serve Apple's eight design principles (*Principles of Great Design*, WWDC 2026). Use these as the names you reason with: + +1. **Purpose.** Make with intention; decide what *not* to build. Every feature asks for the user's time, attention, and trust — spend that budget only where it pays off. +2. **Agency.** Keep people in control: offer choices, don't force a single path. Back it with forgiveness — easy undo for slips, a confirmation dialog only for genuinely destructive, irreversible actions (use sparingly; overusing it trains people to click through). +3. **Responsibility.** Act in the user's interest. Privacy: ask at the right moment, only for what's needed, transparently. Safety: anticipate misuse and harm — especially with AI (an allergy-aware recipe app must not suggest a harmful ingredient). Add previews, confirmations, disclaimers; cut a feature whose risk outweighs its value. +4. **Familiarity.** Build on what people already know. Use metaphors that are neither too literal nor too abstract (a trash can means delete), and honor their physics. Be consistent: things that look the same must behave the same and live in the same place (close is always top-left on macOS) so people can predict what happens next. Only break a familiar pattern if you can prove it's better — then test it, don't assume. +5. **Flexibility.** Design for different contexts, devices, and the full range of abilities. Adapt to the platform (iPhone = quick touch; desktop = deep workflows with precise pointer control) and to the situation. Design inclusively (age, language, expertise, accessibility). When no single layout fits everyone, let people personalize — rearrange controls, hide what they don't use. +6. **Simplicity — not minimalism.** Strip the unnecessary so the core purpose shines; burying everything in one place looks minimal but isn't simple. Be concise (plain language, no jargon, fewer steps) and clear (use hierarchy — order, spacing, contrast — so the most important thing is the most obvious). Every element earns its place; sometimes *adding* context simplifies (a video scrubber that shows time remaining). Show the common path first, advanced options one level deeper. +7. **Craft.** Uncompromising attention to detail builds trust. Beautiful typography, colors that adapt to light/dark, clear iconography, and responsive animations that give immediate, natural feedback. Nothing is random — every spacing, timing, and alignment value is a deliberate choice you can defend. Jittery scroll, misaligned icons, and layouts that break on rotation read as carelessness. Craft needs iteration and longevity — keep evolving the design as features and hardware change. +8. **Delight.** The result of getting the other seven right, not confetti tacked on top. Decide the emotion you want people to feel (calm, confident, excited) and reinforce it in every decision. + +Tactical rules that serve these: + +- **Feedback comes in four kinds:** status, completion, warning, error. Confirm meaningful actions, expose ongoing status, warn before problems, validate inline (not on submit). +- **Wayfinding.** Every screen should answer: Where am I? Where can I go? What's there? How do I get out? Never trap the user. +- **Grouping & mapping.** Proximity implies relationship; place a control near what it affects and arrange controls to mirror what they change. If you need a label to explain a control, the mapping is weak. +- **Direct, specific labels beat safe generic ones.** Name nav items for their contents ("Progress", "Library"), not vague umbrellas ("Home"). Specificity creates predictability. + +## 17. Process + +- **Prototype interactively — an interactive demo is worth "a million static designs."** You discover the interface by building and playing with it; a working prototype also sets a concrete bar that prevents a mediocre final implementation. +- **Design interaction and visuals together.** "You shouldn't be able to tell where one ends and the other begins." Motion is not a layer added after the pixels. +- **Test with real people in real context**, and review motion with fresh eyes — play it in slow motion / frame-by-frame to catch what's invisible at full speed. + +## Quick Reference + +| Need | Technique | Concrete value | +| --- | --- | --- | +| Default UI spring | Critically damped, no overshoot | `damping 1.0`, `response 0.3–0.4` | +| Momentum / flick spring | Under-damped, slight bounce | `damping ~0.8`, `response 0.3–0.4` | +| Gesture → spring velocity | Hand off release velocity | `gestureVelocity / (target − current)` if normalized | +| Flick landing point | Project momentum | `current + (v/1000)·d/(1−d)`, `d ≈ 0.998` | +| Interrupt cleanly | Start from presentation (live) value | read the on-screen transform | +| Avoid reversal "brick wall" | Carry velocity through re-target | spring that blends velocity | +| Reversible transition | Mirror the easing curve | inverse cubic-bézier | +| Decide reverse vs. commit | Use velocity **sign**, not position | at release | +| 1:1 drag | Pointer Events + capture | respect the grab offset | +| Feedback | On pointer-down, continuous | never only at the end | +| Boundary | Rubber-band, don't hard-stop | progressive resistance | +| Translucent chrome | `backdrop-filter` layer | content scrolls under | +| Type tracking | Size-specific, never fixed | tighten large text (`-0.02em`), body near `0` | +| Reduced motion | Cross-fade, not slide/spring | `@media (prefers-reduced-motion)` | diff --git a/.agents/skills/frontend-workflow/polish/SKILL.md b/.agents/skills/frontend-workflow/polish/SKILL.md new file mode 100644 index 0000000000..f5afbede88 --- /dev/null +++ b/.agents/skills/frontend-workflow/polish/SKILL.md @@ -0,0 +1,188 @@ + +--- +name: make-interfaces-feel-better +description: >- + Design engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, typography, icons, micro-interactions, enter/exit animations, or any visual detail work. Supports quick and full review modes. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, font smoothing, tabular numbers, image outlines, box shadows, icons, icon stroke weight, icon states, motion restraint. +--- + +# Details that make interfaces feel better + +Great interfaces rarely come from a single thing. It's usually a collection of small details that compound into a great experience. Apply these principles when building or reviewing UI code. Before suggesting or writing a fix, identify the project's existing styling system and express the change in that system: Tailwind in a Tailwind project, plain CSS in a CSS project, or the established CSS-in-JS approach. Never introduce a second styling system just to apply a polish fix. + +When reviewing, slow the interface down: replay motion at 10% speed in the browser's Animations panel and walk every state: hover, focus, active, loading, empty. What feels off at 10% speed is what's subtly wrong at full speed. + +## Quick Reference + +| Category | When to Use | +| --- | --- | +| [Typography](typography.md) | Text wrapping, font smoothing, tabular numbers | +| [Surfaces](surfaces.md) | Border radius, optical alignment, shadows, image outlines, hit areas | +| [Animations](animations.md) | Interruptible animations, enter/exit transitions, icon animations, scale on press, motion restraint | +| [Icons](icons.md) | Icon stroke weight, states via `currentColor`, outline vs fill, sizing, RTL flipping | +| [Performance](performance.md) | Transition specificity, `will-change` usage | + +## Core Principles + +### 1. Concentric Border Radius + +Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes interfaces feel off. + +### 2. Optical Over Geometric Alignment + +When geometric centering looks off, align optically. Buttons with icons, play triangles, and asymmetric icons all need manual adjustment. + +### 3. Shadows for Elevation, Borders for Structure + +For buttons, cards, and containers whose border exists only to create depth, prefer layered transparent `box-shadow` values. Keep borders that communicate structure or state: dividers, layout separators, and selected or focus states. + +### 4. Interruptible Animations + +Use CSS transitions for interactive state changes — they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once. + +### 5. Split and Stagger Enter Animations + +For an infrequent staged entrance where sequence helps communicate hierarchy, break content into semantic chunks and stagger them by ~100ms instead of animating one container. Do not stagger routine, high-frequency interactions. + +### 6. Subtle Exit Animations + +Use a small fixed `translateY` instead of full height. Exits should be softer than enters. Use `ease-out` for both enter and exit transitions. + +### 7. Contextual Icon Animations + +Animate icons with `opacity`, `scale`, and `blur` instead of toggling visibility. Use exactly these values: scale from `0.25` to `1`, opacity from `0` to `1`, blur from `4px` to `0px`. If the project has `motion` or `framer-motion` in `package.json`, match that package's import path (or the established nearby imports when both exist) and use `transition: { type: "spring", duration: 0.3, bounce: 0 }` — bounce must always be `0`. If no motion library is installed, keep both icons in the DOM (one absolute-positioned) and cross-fade with CSS transitions using `cubic-bezier(0.2, 0, 0, 1)` — this gives both enter and exit animations without any dependency. + +### 8. Font Smoothing + +Apply `-webkit-font-smoothing: antialiased` to the root layout on macOS for crisper text. + +### 9. Tabular Numbers + +Use `font-variant-numeric: tabular-nums` for any dynamically updating numbers to prevent layout shift. + +### 10. Text Wrapping + +Use `text-wrap: balance` on headings. Use `text-wrap: pretty` for body text to avoid orphans. + +### 11. Image Outlines + +Add a subtle `1px` outline with low opacity to images for consistent depth. The color must be pure black in light mode (`oklch(0 0 0 / 0.1)`) and pure white in dark mode (`oklch(1 0 0 / 0.1)`), never a near-black like slate, zinc, or any tinted neutral. A tinted outline picks up the surface color underneath it and reads as dirt on the image edge. + +### 12. Scale on Press + +A subtle `scale(0.96)` on click gives buttons tactile feedback. Always use `0.96`. Never use a value smaller than `0.95` — anything below feels exaggerated. Add a `static` prop to disable it when motion would be distracting. + +### 13. Skip Animation on Page Load + +Use `initial={false}` on `AnimatePresence` to prevent enter animations on first render. Verify it doesn't break intentional entrance animations. + +### 14. Never Use `transition: all` + +Always specify exact properties: `transition-property: scale, opacity`. Tailwind's `transition-transform` covers `transform, translate, scale, rotate`. + +### 15. Use `will-change` Sparingly + +Only for `transform`, `opacity`, `filter` — properties the GPU can composite. Never use `will-change: all`. Only add when you notice first-frame stutter. + +### 16. Minimum Hit Area + +Interactive elements should prefer a 44×44px hit area for touch or mobile contexts. In dense desktop interfaces, use at least 40×40px. Extend with a pseudo-element if the visible element is smaller. Never let hit areas of two elements overlap. + +### 17. Match Icon Stroke to Text Weight + +An icon next to text carries the text's optical weight: `1.5px` stroke beside regular (400) text, `2px` beside semibold (600). One stroke weight per icon set; never mix libraries on one surface. + +### 18. One SVG, Recolored per State + +Icons use `currentColor` and get their states (hover, selected, disabled) from CSS color and opacity, never from separate assets. Outline variant is the default; fill variant marks the active state. + +### 19. Motion Restraint + +No custom animation on high-frequency interactions: the attention cost repeats on every trigger. Motion is never the only feedback channel; every animated state change also needs a static cue such as color, icon, or label. + +## Common Mistakes + +| Mistake | Fix | +| --- | --- | +| Same border radius on parent and child | Calculate `outerRadius = innerRadius + padding` | +| Icons look off-center | Adjust optically with padding or fix SVG directly | +| Border used only to fake elevation | Use layered `box-shadow` with transparency; keep structural and state borders | +| Jarring staged entrance or contextual exit | Stagger infrequent entrances and keep context-preserving exits subtle | +| Numbers cause layout shift | Apply `tabular-nums` | +| Heavy text on macOS | Apply `antialiased` to root | +| Animation plays on page load | Add `initial={false}` to `AnimatePresence` | +| `transition: all` on elements | Specify exact properties | +| First-frame animation stutter | Add `will-change: transform` (sparingly) | +| Tiny hit areas on small controls | Extend with a pseudo-element to 44×44px for touch/mobile, or at least 40×40px in dense desktop UI | +| Hairline icon beside bold text | Match the stroke width to the text weight | +| Separate icon assets per state | One `currentColor` SVG, states via CSS | +| Filled icons everywhere | Outline as default, fill only for the active state | +| Entrance animation on every hover or keystroke | Instant feedback or ≤150ms opacity/color transition | + +## Review Output Format + +Use `full` when no review mode is supplied. + +| Mode | Coverage | Finding cap | +| --- | --- | --- | +| `quick` | Primary user path and highest-traffic states; report only `HIGH` and `MEDIUM` issues | 5 | +| `full` | Entire requested scope across typography, surfaces, animations, icons, and performance | 15 | + +### Scope and Coverage + +State the mode, exact scope, framework, styling conventions, and any review boundary. Show what was actually inspected: + +| Category | Evidence inspected | Result | +| --- | --- | --- | +| Typography | Files, components, states, or checks | Findings count, `Clear`, or `Not reviewed` with a reason | + +Include all five Quick Reference categories. Never imply an uninspected surface was reviewed. + +### Findings + +Group findings by principle. Use a markdown table with **Severity**, **Location**, **Before**, **After**, and **Why** columns. Include every change made or proposed, not a subset. Never use separate "Before:" / "After:" lines. + +- **Severity**: `HIGH` makes an interaction inaccessible, misleading, unreadable, or repeatedly disruptive; `MEDIUM` creates a noticeable usability or consistency problem; `LOW` is isolated polish and appears only in `full` mode. +- **Location**: cite `path/to/file:line`. If the artifact has no source files, cite the exact screen and component instead. +- **Before / After**: show the current implementation and an actionable replacement. +- **Why**: name the violated principle and explain its user impact. + +Consolidate a repeated systemic issue into one row and list every affected location. Omit principles with no findings and never pad the report to reach the cap. + +### Example + +#### Concentric border radius +| Severity | Location | Before | After | Why | +| --- | --- | --- | --- | --- | +| LOW | `src/Card.tsx:28` | `rounded-xl` on card + `rounded-xl` on inner button (`p-2`) | `rounded-2xl` on card (`8 + 8 = 16`), `rounded-lg` on inner button | Nested corners should be concentric | +| LOW | `src/card.css:11` | `border-radius: 16px` on both nested surfaces | Outer `24px`, inner `16px` with `8px` padding | Equal nested radii make the inner surface look pinched | + +#### Tabular numbers +| Severity | Location | Before | After | Why | +| --- | --- | --- | --- | --- | +| MEDIUM | `src/Counter.tsx:17` | `{count}` | `{count}` | Proportional digits cause changing values to shift | +| LOW | `src/timer.css:8` | Default numerals on a timer | Add `font-variant-numeric: tabular-nums` to the timer | Equal-width digits keep the timer stable | + +#### Scale on press +| Severity | Location | Before | After | Why | +| --- | --- | --- | --- | --- | +| LOW | `src/Button.tsx:19` | ` + + + ); +} +``` + +### CSS-Only Stagger + +```css +.stagger-item { + opacity: 0; + transform: translateY(12px); + filter: blur(4px); + animation: fadeInUp 400ms ease-out forwards; +} + +.stagger-item:nth-child(1) { animation-delay: 0ms; } +.stagger-item:nth-child(2) { animation-delay: 100ms; } +.stagger-item:nth-child(3) { animation-delay: 200ms; } + +@keyframes fadeInUp { + to { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} +``` + +## Exit Animations + +Exit animations should be softer and less attention-grabbing than enter animations. The user's focus is moving to the next thing — don't fight for attention. + +### Subtle Exit (Recommended) + +```tsx +// Small fixed translateY — indicates direction without drama + + {content} + +``` + +### Full Exit (When Context Matters) + +```tsx +// Slide fully out — use when spatial context is important +// (e.g., a card returning to a list, a drawer closing) + + {content} + +``` + +### Good vs. Bad + +```css +/* Good — subtle exit */ +.item-exit { + opacity: 0; + transform: translateY(-12px); + transition: opacity 150ms ease-out, transform 150ms ease-out; +} + +/* Bad — dramatic exit that steals focus */ +.item-exit { + opacity: 0; + transform: translateY(-100%) scale(0.5); + transition: all 400ms ease-out; +} + +/* Sometimes correct — remove immediately when motion adds no context */ +.item-exit { + display: none; +} +``` + +**Key points:** +- Use a small fixed `translateY` (e.g., `-12px`) instead of the full container height +- Keep some directional movement to indicate where the element went +- Exit duration should be shorter than enter duration (150ms vs 300ms) +- Use a subtle exit when it preserves spatial context. Remove immediately when motion adds no information, the interaction repeats frequently, or reduced motion is requested. + +## Contextual Icon Animations + +When icons appear or disappear contextually (on hover, on state change), animate them with `opacity`, `scale`, and `blur` rather than just toggling visibility. + +### Motion Example + +This example uses the `motion` package. If the project instead has `framer-motion`, import the same APIs from `"framer-motion"`; never mix an installed package with the other package's import path. + +```tsx +import { AnimatePresence, motion } from "motion/react"; + +function IconButton({ isActive, icon: Icon }) { + return ( + + ); +} +``` + +### CSS Transition Approach (No Motion) + +If the project doesn't use Motion (Framer Motion), keep both icons in the DOM and cross-fade them with CSS transitions. Because neither icon unmounts, both enter and exit animate smoothly. + +The trick: one icon is absolutely positioned on top of the other. Toggling state cross-fades them — the entering icon scales up from `0.25` while the exiting icon scales down to `0.25`, both with opacity and blur. + +```tsx +function IconButton({ isActive, ActiveIcon, InactiveIcon }) { + return ( + + ); +} +``` + +The non-absolute icon (InactiveIcon) defines the layout size. The absolute icon (ActiveIcon) overlays it without affecting flow. + +### Choosing Between Motion and CSS + +| | Motion (Framer Motion) | CSS transitions (both icons in DOM) | +| --- | --- | --- | +| **Enter animation** | Yes | Yes | +| **Exit animation** | Yes (via `AnimatePresence`) | Yes (cross-fade — icon never unmounts) | +| **Spring physics** | Yes | No — use `cubic-bezier(0.2, 0, 0, 1)` as approximation | +| **When to use** | Project already uses `motion` or `framer-motion` | No motion dependency, or keeping bundle small | + +**Rule:** Check the project's `package.json`. Import from `"motion/react"` when `motion` is installed, or from `"framer-motion"` when `framer-motion` is installed. If both exist, follow the imports already used by the component or its nearest peers. If neither is present, use the CSS cross-fade pattern — don't add a dependency just for icon transitions. + +### When to Animate Icons + +| Animate | Don't animate | +| --- | --- | +| Icons that appear on hover (action buttons) | Static navigation icons | +| State change icons (play → pause, like → liked) | Decorative icons | +| Icons in contextual toolbars | Icons that are always visible | +| Loading/success state indicators | Icon labels (text next to icon) | + +**Important:** Always use exactly these values for contextual icon animations — do not deviate: +- `scale`: `0.25` → `1` (never use `0.5` or `0.6`) +- `opacity`: `0` → `1` +- `filter`: `"blur(4px)"` → `"blur(0px)"` +- `transition`: `{ type: "spring", duration: 0.3, bounce: 0 }` — **bounce must always be `0`**, never `0.1` or any other value + +## Scale on Press + +A subtle scale-down on click gives buttons tactile feedback. Always use `scale(0.96)`. Never use a value smaller than `0.95` — anything below feels exaggerated. Use CSS transitions for interruptibility — if the user releases mid-press, it should smoothly return. + +Not every button needs this. Add a `static` prop to your button component that disables the scale effect when the motion would be distracting. + +### CSS Example + +```css +.button { + transition-property: scale; + transition-duration: 150ms; + transition-timing-function: ease-out; +} + +.button:active { + scale: 0.96; +} +``` + +### Tailwind Example + +```tsx + +``` + +### Motion Example + +```tsx + + Click me + +``` + +### Static Prop Pattern + +Extract the scale class into a variable and conditionally apply it based on a `static` prop: + +```tsx +const tapScale = "active:not-disabled:scale-[0.96]"; + +function Button({ static: isStatic, className, children, ...props }) { + return ( + + ); +} + +// Usage + {/* scales on press */} + {/* no scale */} +``` + +## Skip Animation on Page Load + +Use `initial={false}` on `AnimatePresence` to prevent enter animations from firing on first render. Elements that are already in their default state shouldn't animate in on page load — only on subsequent state changes. + +### When It Works + +```tsx +// Good — icon doesn't animate in on mount, only on state change + + + + + +``` + +Works well for: icon swaps, toggles, tabs, segmented controls — anything that has a default state on page load. + +### When It Breaks + +Don't use `initial={false}` when the component relies on its `initial` prop to set up a first-time enter animation, like a staggered page hero or a loading state. In those cases, removing the initial animation skips the entire entrance. + +```tsx +// Bad — initial={false} would skip the staggered page enter entirely + + + ... + + +``` + +Verify the component still looks right on a full page refresh before applying this. + +## Motion Restraint + +Motion is a budget, not a garnish: + +- **No custom animation on high-frequency interactions.** Repeated interactions get instant feedback or a minimal `opacity` or `background-color` transition at ≤150ms. +- **Motion is never the only feedback channel.** Every animated state change also needs a static cue such as color, icon, or label. +- **Brief and precise beats prominent.** If a shorter, smaller animation communicates the same thing, use it. +- **Honor reduced-motion preferences.** Preserve the static cue and remove unnecessary movement. + +```css +/* Good: high-frequency hover gets a minimal transition */ +.row:hover { + background-color: var(--surface-hover); + transition: background-color 100ms ease-out; +} + +/* Bad: every hover replays a full entrance */ +.row:hover .row-icon { + animation: bounceIn 500ms; +} +``` diff --git a/.agents/skills/frontend-workflow/polish/icons.md b/.agents/skills/frontend-workflow/polish/icons.md new file mode 100644 index 0000000000..6bdc0078c0 --- /dev/null +++ b/.agents/skills/frontend-workflow/polish/icons.md @@ -0,0 +1,63 @@ +# Icons + +Icon weight, states, sizing, and direction: the details that make icons sit naturally in an interface. + +## Match Icon Stroke to Text Weight + +An icon next to text should carry the same optical weight as the text. + +| Adjacent text | Icon stroke width (24px grid) | +| --- | --- | +| Regular (400), 14–16px | `1.5px` | +| Medium/Semibold (500–600) | `2px` | +| Bold (700), or emphasized standalone | `2.5px` | + +Use one stroke weight per icon set on a surface. Size inline icons relative to the text's cap height, typically `1em`–`1.25em`. + +## One SVG, Recolored per State + +Use one SVG drawn with `currentColor`; let CSS drive hover, selected, and disabled states. Strip hardcoded `fill` and `stroke` colors when importing icons. + +```html +… +``` + +```css +.icon-button { color: oklch(0.552 0.016 285.938); } +.icon-button:hover { color: oklch(0.21 0.006 285.885); } +.icon-button[aria-pressed="true"] { color: oklch(0.623 0.188 259.815); } +.icon-button:disabled { opacity: 0.4; } +``` + +## Outline Default, Fill Active + +| Variant | Use for | +| --- | --- | +| Outline | Default state: toolbars, list rows, inline with text | +| Fill | Selected or active state: active tab, toggled bookmark, liked heart | + +The swap between variants is a contextual icon animation; use the exact cross-fade values in [animations.md](animations.md). + +## Design at Render Size + +- Test every icon at the smallest size it will render, often `16px`. +- Prefer simplified glyphs for small contexts over scaled-down detailed artwork. +- Use the icon set's native grid sizes (`16`, `20`, `24`) rather than arbitrary fractional scales. +- Use SVG rather than raster assets. + +## Icons in RTL + +| Flip | Don't flip | +| --- | --- | +| Back/forward arrows, navigation chevrons | Logos and brand marks | +| Text alignment, lists, indent | Checkmarks | +| Directional send glyphs | Clocks, cups, pencils | +| Speaker waves tied to reading direction | Media playback controls | + +```css +[dir="rtl"] .icon-directional { + scale: -1 1; +} +``` + +Analyze composite icons part by part: an overlay may keep its position even when the base glyph flips. Give every icon-only control an accessible name and mark purely decorative icons hidden from assistive technology. diff --git a/.agents/skills/frontend-workflow/polish/performance.md b/.agents/skills/frontend-workflow/polish/performance.md new file mode 100644 index 0000000000..c12257a2ba --- /dev/null +++ b/.agents/skills/frontend-workflow/polish/performance.md @@ -0,0 +1,88 @@ +# Performance + +Transition specificity and GPU compositing hints. + +## Transition Only What Changes + +Never use `transition: all` or Tailwind's `transition-all`. Always specify the exact properties that change. Tailwind's bare `transition` maps to a curated default list of colors, opacity, shadow, and transforms, not to `all`; still prefer naming exactly what changes. + +### Why + +- `transition: all` forces the browser to watch every property for changes +- Causes unexpected transitions on properties you didn't intend to animate (colors, padding, shadows) +- Prevents browser optimizations + +### CSS Example + +```css +/* Good — only transition what changes */ +.button { + transition-property: scale, background-color; + transition-duration: 150ms; + transition-timing-function: ease-out; +} + +/* Bad — transition everything */ +.button { + transition: all 150ms ease-out; +} +``` + +### Tailwind + +```tsx +// Good — explicit properties + +``` + +### Play Button Triangles + +Play icons are triangular and their geometric center is not their visual center. Shift slightly right: + +```css +/* Good — optically centered */ +.play-button svg { + margin-left: 2px; /* shift right to account for triangle shape */ +} + +/* Bad — geometrically centered but looks off */ +.play-button svg { + /* no adjustment */ +} +``` + +### Asymmetric Icons (Stars, Arrows, Carets) + +Some icons have uneven visual weight. The best fix is adjusting the SVG directly so no extra margin/padding is needed in the component code. + +```tsx +// Best — fix in the SVG itself +// Adjust the viewBox or path to visually center the icon + +// Fallback — adjust with margin + + + +``` + +## Shadows Instead of Borders + +For **buttons, cards, and containers** that use a border for depth or elevation, prefer replacing it with a subtle `box-shadow`. Shadows adapt to any background since they use transparency; solid borders don't. This also helps when using images or multiple colors as backgrounds — solid border colors don't work well on backgrounds other than the ones they were designed for. + +**Do not apply this to dividers** (`border-b`, `border-t`, side borders) or any border whose purpose is layout separation rather than element depth. Those should stay as borders. + +### Shadow as Border (Light Mode) + +The shadow is comprised of three layers. The first acts as a 1px border ring, the second adds subtle lift, and the third provides ambient depth: + +```css +:root { + --shadow-border: + 0px 0px 0px 1px oklch(0 0 0 / 0.06), + 0px 1px 2px -1px oklch(0 0 0 / 0.06), + 0px 2px 4px 0px oklch(0 0 0 / 0.04); + --shadow-border-hover: + 0px 0px 0px 1px oklch(0 0 0 / 0.08), + 0px 1px 2px -1px oklch(0 0 0 / 0.08), + 0px 2px 4px 0px oklch(0 0 0 / 0.06); +} +``` + +### Shadow as Border (Dark Mode) + +In dark mode, simplify to a single white ring — layered depth shadows aren't visible on dark backgrounds: + +```css +/* Dark mode — adapt to whatever setup the project uses + (prefers-color-scheme, class, data attribute, etc.) */ +--shadow-border: 0 0 0 1px oklch(1 0 0 / 0.08); +--shadow-border-hover: 0 0 0 1px oklch(1 0 0 / 0.13); +``` + +### Usage with Hover Transition + +Apply the variable and add `transition-[box-shadow]` for a smooth hover: + +```css +.card { + box-shadow: var(--shadow-border); + transition-property: box-shadow; + transition-duration: 150ms; + transition-timing-function: ease-out; +} + +.card:hover { + box-shadow: var(--shadow-border-hover); +} +``` + +### When to Use Shadows vs. Borders + +| Use shadows | Use borders | +| --- | --- | +| Cards, containers with depth | Dividers between list items | +| Buttons with bordered styles | Table cell boundaries | +| Elevated elements (dropdowns, modals) | Form input outlines (for accessibility) | +| Elements on varied backgrounds | Hairline separators in dense UI | +| Hover/focus states for lift effect | | + +## Image Outlines + +Add a subtle `1px` outline with low opacity to images. This creates consistent depth, especially in design systems where other elements use borders or shadows. + +### Color rules (non-negotiable) + +- **Light mode**: pure black, `oklch(0 0 0 / 0.1)`. +- **Dark mode**: pure white, `oklch(1 0 0 / 0.1)`. +- Never use a near-black or near-white from the project palette (e.g. slate-900, zinc-900, `#0a0a0a`, `#111827`, `#f5f5f7`). Tinted outlines pick up the surrounding surface color and read as dirt on the image edge. +- Never match the outline to the project's accent or ink color. The outline is a neutral separator, not a themed element. + +### Light Mode + +```css +img { + outline: 1px solid oklch(0 0 0 / 0.1); + outline-offset: -1px; /* inset so it doesn't add to layout */ +} +``` + +### Dark Mode + +```css +img { + outline: 1px solid oklch(1 0 0 / 0.1); + outline-offset: -1px; +} +``` + +### Tailwind with Dark Mode + +```tsx +{alt} +``` + +Use `outline-black/10` and `outline-white/10` specifically — not `outline-slate-*`, `outline-zinc-*`, `outline-neutral-*`, or any tinted scale. + +**Why outline instead of border?** `outline` doesn't affect layout (no added width/height), and `outline-offset: -1px` keeps it inset so images stay their intended size. + +## Minimum Hit Area + +Interactive elements should prefer a 44×44px hit area for touch or mobile contexts. In dense desktop interfaces, use at least 40×40px. If the visible element is smaller (e.g., a 20×20 checkbox), extend the hit area with a pseudo-element. + +### CSS Example + +```css +/* Small checkbox with expanded 44px hit area */ +.checkbox { + position: relative; + width: 20px; + height: 20px; +} + +.checkbox::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 44px; + height: 44px; +} +``` + +### Tailwind Example + +```tsx + +``` + +### Collision Rule + +If the extended hit area overlaps another interactive element, shrink the pseudo-element — but make it as large as possible without colliding. Two interactive elements should never have overlapping hit areas. diff --git a/.agents/skills/frontend-workflow/polish/typography.md b/.agents/skills/frontend-workflow/polish/typography.md new file mode 100644 index 0000000000..a950535942 --- /dev/null +++ b/.agents/skills/frontend-workflow/polish/typography.md @@ -0,0 +1,157 @@ +# Typography + +Typography rendering details that make interfaces feel better. + +## Text Wrapping + +### text-wrap: balance + +Distributes text evenly across lines, preventing orphaned words on headings and short text blocks. **Only works on blocks of 6 lines or fewer** (Chromium) or 10 lines or fewer (Firefox) — the balancing algorithm is computationally expensive, so browsers limit it to short text. + +```css +/* Good — even line lengths on short text */ +h1, h2, h3 { + text-wrap: balance; +} +``` + +```css +/* Bad — default wrapping leaves orphans */ +h1 { + /* no text-wrap rule → "Read our + blog" instead of balanced lines */ +} +``` + +```css +/* Bad — balance on long paragraphs (silently ignored, wastes intent) */ +.article-body p { + text-wrap: balance; +} +``` + +**Tailwind:** `text-balance` + +### text-wrap: pretty + +Prevents orphaned words (a single word dangling on the last line) by adjusting line breaks throughout the paragraph. Unlike `balance`, it doesn't try to equalize line lengths — it just ensures the last line isn't embarrassingly short. Works on text of any length with no line-count limit. + +This should be your **default for short-to-medium text** — paragraphs, descriptions, captions, list items, card text. For very long text (10+ lines), skip both `pretty` and `balance` — the browser's default wrapping is fine and you avoid unnecessary layout cost. + +```css +/* Good — descriptions, captions, short paragraphs */ +p, li, figcaption, blockquote { + text-wrap: pretty; +} +``` + +```tsx +// Tailwind +

+ A short paragraph that won't leave an orphan on the last line. +

+``` + +**Tailwind:** `text-pretty` + +### When to Use Which + +| Scenario | Use | +| --- | --- | +| Headings, titles where even distribution matters | `text-wrap: balance` | +| Short-to-medium text — paragraphs, descriptions, captions, UI text | `text-wrap: pretty` | +| Long text (10+ lines), code blocks, pre-formatted text | Neither — leave default | + +## Font Smoothing (macOS) + +On macOS, text renders heavier than intended by default. Apply antialiased smoothing to the root layout so all text renders crisper and thinner. + +```css +/* CSS */ +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +``` + +```tsx +// Tailwind — apply to root layout + +``` + +### Good vs. Bad + +```css +/* Good — applied once at the root */ +html { + -webkit-font-smoothing: antialiased; +} + +/* Bad — applied per-element, inconsistent */ +.heading { + -webkit-font-smoothing: antialiased; +} +.body { + /* no smoothing → heavier than heading */ +} +``` + +**Note:** This only affects macOS rendering. Other platforms ignore these properties, so it's safe to apply universally. + +## Font Family Scope + +This skill does not require a specific font family. Do not introduce a paid or proprietary typeface just to satisfy the polish checklist. + +Use the product's existing type system unless the task explicitly asks for a type change. If the design calls for a system-native macOS feel, use the system font stack. If the design calls for a commercial face such as Helvetica Now, treat it as an optional brand decision and keep a practical fallback stack. + +```css +/* System-native macOS/iOS feel */ +html { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} +``` + +```css +/* Commercial brand face with safe fallbacks */ +html { + font-family: "Helvetica Now", "Helvetica Neue", Arial, sans-serif; +} +``` + +**Rule:** font smoothing, text wrapping, and tabular numbers are rendering details. They do not override the project's chosen font family. + +## Tabular Numbers + +When numbers update dynamically (counters, prices, timers, table columns), use tabular-nums to make all digits equal width. This prevents layout shift as values change. + +```css +/* CSS */ +.counter { + font-variant-numeric: tabular-nums; +} +``` + +```tsx +// Tailwind +{count} +``` + +### When to Use + +| Use tabular-nums | Don't use tabular-nums | +| --- | --- | +| Counters and timers | Static display numbers | +| Prices that update | Decorative large numbers | +| Table columns with numbers | Phone numbers, zip codes | +| Animated number transitions | Version numbers (v2.1.0) | +| Scoreboards, dashboards | | + +### Caveat + +Some fonts (like Inter) change the visual appearance of numerals with this property — specifically, the digit `1` becomes wider and centered. This is expected behavior and usually desirable for alignment, but verify it looks right in your specific font. + +```css +/* With Inter font: + Default: 1234 → proportional, "1" is narrow + Tabular: 1234 → all digits equal width, "1" centered */ +``` diff --git a/.agents/skills/vercel-react-best-practices/SKILL.md b/.agents/skills/frontend-workflow/react-performance/SKILL.md similarity index 97% rename from .agents/skills/vercel-react-best-practices/SKILL.md rename to .agents/skills/frontend-workflow/react-performance/SKILL.md index 237988de4a..3b810ec03f 100644 --- a/.agents/skills/vercel-react-best-practices/SKILL.md +++ b/.agents/skills/frontend-workflow/react-performance/SKILL.md @@ -1,3 +1,4 @@ + --- name: vercel-react-best-practices description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements. @@ -146,4 +147,4 @@ Each rule file contains: ## Full Compiled Document -For the complete guide with all rules expanded: `AGENTS.md` +For the complete guide with all rules expanded: `rules-compiled.md` diff --git a/.agents/skills/vercel-react-best-practices/AGENTS.md b/.agents/skills/frontend-workflow/react-performance/rules-compiled.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/AGENTS.md rename to .agents/skills/frontend-workflow/react-performance/rules-compiled.md diff --git a/.agents/skills/vercel-react-best-practices/rules/_sections.md b/.agents/skills/frontend-workflow/react-performance/rules/_sections.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/_sections.md rename to .agents/skills/frontend-workflow/react-performance/rules/_sections.md diff --git a/.agents/skills/vercel-react-best-practices/rules/_template.md b/.agents/skills/frontend-workflow/react-performance/rules/_template.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/_template.md rename to .agents/skills/frontend-workflow/react-performance/rules/_template.md diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md b/.agents/skills/frontend-workflow/react-performance/rules/advanced-effect-event-deps.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md rename to .agents/skills/frontend-workflow/react-performance/rules/advanced-effect-event-deps.md diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md b/.agents/skills/frontend-workflow/react-performance/rules/advanced-event-handler-refs.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md rename to .agents/skills/frontend-workflow/react-performance/rules/advanced-event-handler-refs.md diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md b/.agents/skills/frontend-workflow/react-performance/rules/advanced-init-once.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/advanced-init-once.md rename to .agents/skills/frontend-workflow/react-performance/rules/advanced-init-once.md diff --git a/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md b/.agents/skills/frontend-workflow/react-performance/rules/advanced-use-latest.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md rename to .agents/skills/frontend-workflow/react-performance/rules/advanced-use-latest.md diff --git a/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md b/.agents/skills/frontend-workflow/react-performance/rules/async-api-routes.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/async-api-routes.md rename to .agents/skills/frontend-workflow/react-performance/rules/async-api-routes.md diff --git a/.agents/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md b/.agents/skills/frontend-workflow/react-performance/rules/async-cheap-condition-before-await.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md rename to .agents/skills/frontend-workflow/react-performance/rules/async-cheap-condition-before-await.md diff --git a/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md b/.agents/skills/frontend-workflow/react-performance/rules/async-defer-await.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/async-defer-await.md rename to .agents/skills/frontend-workflow/react-performance/rules/async-defer-await.md diff --git a/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md b/.agents/skills/frontend-workflow/react-performance/rules/async-dependencies.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/async-dependencies.md rename to .agents/skills/frontend-workflow/react-performance/rules/async-dependencies.md diff --git a/.agents/skills/vercel-react-best-practices/rules/async-parallel.md b/.agents/skills/frontend-workflow/react-performance/rules/async-parallel.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/async-parallel.md rename to .agents/skills/frontend-workflow/react-performance/rules/async-parallel.md diff --git a/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md b/.agents/skills/frontend-workflow/react-performance/rules/async-suspense-boundaries.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md rename to .agents/skills/frontend-workflow/react-performance/rules/async-suspense-boundaries.md diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md b/.agents/skills/frontend-workflow/react-performance/rules/bundle-analyzable-paths.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md rename to .agents/skills/frontend-workflow/react-performance/rules/bundle-analyzable-paths.md diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md b/.agents/skills/frontend-workflow/react-performance/rules/bundle-barrel-imports.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md rename to .agents/skills/frontend-workflow/react-performance/rules/bundle-barrel-imports.md diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md b/.agents/skills/frontend-workflow/react-performance/rules/bundle-conditional.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md rename to .agents/skills/frontend-workflow/react-performance/rules/bundle-conditional.md diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md b/.agents/skills/frontend-workflow/react-performance/rules/bundle-defer-third-party.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md rename to .agents/skills/frontend-workflow/react-performance/rules/bundle-defer-third-party.md diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md b/.agents/skills/frontend-workflow/react-performance/rules/bundle-dynamic-imports.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md rename to .agents/skills/frontend-workflow/react-performance/rules/bundle-dynamic-imports.md diff --git a/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md b/.agents/skills/frontend-workflow/react-performance/rules/bundle-preload.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/bundle-preload.md rename to .agents/skills/frontend-workflow/react-performance/rules/bundle-preload.md diff --git a/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md b/.agents/skills/frontend-workflow/react-performance/rules/client-event-listeners.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md rename to .agents/skills/frontend-workflow/react-performance/rules/client-event-listeners.md diff --git a/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md b/.agents/skills/frontend-workflow/react-performance/rules/client-localstorage-schema.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md rename to .agents/skills/frontend-workflow/react-performance/rules/client-localstorage-schema.md diff --git a/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md b/.agents/skills/frontend-workflow/react-performance/rules/client-passive-event-listeners.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md rename to .agents/skills/frontend-workflow/react-performance/rules/client-passive-event-listeners.md diff --git a/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md b/.agents/skills/frontend-workflow/react-performance/rules/client-swr-dedup.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md rename to .agents/skills/frontend-workflow/react-performance/rules/client-swr-dedup.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md b/.agents/skills/frontend-workflow/react-performance/rules/js-batch-dom-css.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-batch-dom-css.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md b/.agents/skills/frontend-workflow/react-performance/rules/js-cache-function-results.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-cache-function-results.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md b/.agents/skills/frontend-workflow/react-performance/rules/js-cache-property-access.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-cache-property-access.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md b/.agents/skills/frontend-workflow/react-performance/rules/js-cache-storage.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-cache-storage.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md b/.agents/skills/frontend-workflow/react-performance/rules/js-combine-iterations.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-combine-iterations.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md b/.agents/skills/frontend-workflow/react-performance/rules/js-early-exit.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-early-exit.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-early-exit.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md b/.agents/skills/frontend-workflow/react-performance/rules/js-flatmap-filter.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-flatmap-filter.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md b/.agents/skills/frontend-workflow/react-performance/rules/js-hoist-regexp.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-hoist-regexp.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md b/.agents/skills/frontend-workflow/react-performance/rules/js-index-maps.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-index-maps.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-index-maps.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md b/.agents/skills/frontend-workflow/react-performance/rules/js-length-check-first.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-length-check-first.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md b/.agents/skills/frontend-workflow/react-performance/rules/js-min-max-loop.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-min-max-loop.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-request-idle-callback.md b/.agents/skills/frontend-workflow/react-performance/rules/js-request-idle-callback.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-request-idle-callback.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-request-idle-callback.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md b/.agents/skills/frontend-workflow/react-performance/rules/js-set-map-lookups.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-set-map-lookups.md diff --git a/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md b/.agents/skills/frontend-workflow/react-performance/rules/js-tosorted-immutable.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md rename to .agents/skills/frontend-workflow/react-performance/rules/js-tosorted-immutable.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-activity.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-activity.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-activity.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-animate-svg-wrapper.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-animate-svg-wrapper.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-conditional-render.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-conditional-render.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-content-visibility.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-content-visibility.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-hoist-jsx.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-hoist-jsx.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-hydration-no-flicker.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-hydration-no-flicker.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-hydration-suppress-warning.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-hydration-suppress-warning.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-resource-hints.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-resource-hints.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-script-defer-async.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-script-defer-async.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-svg-precision.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-svg-precision.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md b/.agents/skills/frontend-workflow/react-performance/rules/rendering-usetransition-loading.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md rename to .agents/skills/frontend-workflow/react-performance/rules/rendering-usetransition-loading.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-defer-reads.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-defer-reads.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-dependencies.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-dependencies.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-derived-state-no-effect.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-derived-state-no-effect.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-derived-state.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-derived-state.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-functional-setstate.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-functional-setstate.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-lazy-state-init.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-lazy-state-init.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-memo-with-default-value.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-memo-with-default-value.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-memo.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-memo.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-memo.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-move-effect-to-event.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-move-effect-to-event.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-no-inline-components.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-no-inline-components.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-simple-expression-in-memo.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-simple-expression-in-memo.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-split-combined-hooks.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-split-combined-hooks.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-transitions.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-transitions.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-use-deferred-value.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-use-deferred-value.md diff --git a/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md b/.agents/skills/frontend-workflow/react-performance/rules/rerender-use-ref-transient-values.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md rename to .agents/skills/frontend-workflow/react-performance/rules/rerender-use-ref-transient-values.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md b/.agents/skills/frontend-workflow/react-performance/rules/server-after-nonblocking.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-after-nonblocking.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md b/.agents/skills/frontend-workflow/react-performance/rules/server-auth-actions.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-auth-actions.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-auth-actions.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md b/.agents/skills/frontend-workflow/react-performance/rules/server-cache-lru.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-cache-lru.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md b/.agents/skills/frontend-workflow/react-performance/rules/server-cache-react.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-cache-react.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-cache-react.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md b/.agents/skills/frontend-workflow/react-performance/rules/server-dedup-props.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-dedup-props.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-dedup-props.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md b/.agents/skills/frontend-workflow/react-performance/rules/server-hoist-static-io.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-hoist-static-io.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md b/.agents/skills/frontend-workflow/react-performance/rules/server-no-shared-module-state.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-no-shared-module-state.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md b/.agents/skills/frontend-workflow/react-performance/rules/server-parallel-fetching.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-parallel-fetching.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md b/.agents/skills/frontend-workflow/react-performance/rules/server-parallel-nested-fetching.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-parallel-nested-fetching.md diff --git a/.agents/skills/vercel-react-best-practices/rules/server-serialization.md b/.agents/skills/frontend-workflow/react-performance/rules/server-serialization.md similarity index 100% rename from .agents/skills/vercel-react-best-practices/rules/server-serialization.md rename to .agents/skills/frontend-workflow/react-performance/rules/server-serialization.md diff --git a/.agents/skills/migrate-radix-to-base/SKILL.md b/.agents/skills/migrate-radix-to-base/SKILL.md new file mode 100644 index 0000000000..5eb5dc5700 --- /dev/null +++ b/.agents/skills/migrate-radix-to-base/SKILL.md @@ -0,0 +1,173 @@ +--- +name: migrate-radix-to-base +description: Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert radix primitives, or switch a shadcn project's base library. Handles single components ("migrate accordion") and whole projects. +--- + +# Radix UI -> Base UI migration + +You migrate shadcn wrappers, hand-rolled radix compositions, and their +consumers to `@base-ui/react`, keeping the project buildable at every step. +Be precise; never guess a mapping. When a prop or part is not in these +reference files, check `node_modules/@base-ui/react/**/*.d.ts` before +transforming, and record gaps in the report. + +## Preflight (always) + +1. `npx shadcn@latest info --json` (or the project's runner): gives the + current base, STYLE (e.g. `radix-lyra`), tailwind version, aliases, + installed components, and package manager. Trust it over inference. +2. Detect the package manager (packageManager field / lockfile: + pnpm-lock.yaml, bun.lock, yarn.lock, package-lock.json) and use IT for + every install. Never leave a stale lockfile. +3. Require a clean git tree; work on a branch; one commit per component. +4. Baseline check BEFORE touching dependencies: run the project's + typecheck/build so pre-existing failures are never attributed to you. +5. Install `@base-ui/react` alongside radix. Radix packages are removed only + after the LAST component is migrated (both coexist fine). + +## Strategy: golden pair first, transformation engine second + +- **Golden pair via the CLI (preferred).** If the project is shadcn with a + known style (`radix-