Skip to content

Conversation

jaysin586
Copy link
Contributor

@jaysin586 jaysin586 commented Sep 25, 2025

Summary by CodeRabbit

  • New Features
    • Added an Examples section with live Rotate and Animated Button demos, new examples pages, layout with breadcrumbs, and a hero link.
  • Bug Fixes
    • Improved exit animation handling and more consistent hover opacity transitions.
  • Documentation
    • Refreshed docs styling, code block/link contrast, Example wrapper, and example wording.
  • Tests
    • Added end-to-end hover-opacity test and new unit tests for hover, spring, and transform utilities.
  • Chores
    • Bumped multiple dependencies and adjusted lint configuration.

@jaysin586 jaysin586 self-assigned this Sep 25, 2025
@jaysin586 jaysin586 added bug Something isn't working documentation Improvements or additions to documentation labels Sep 25, 2025
Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This PR updates many dependencies, enhances docs (styles, examples, routes), adds example components and demo pages, introduces hover demo + E2E test, expands unit tests, adds presence exit-clone handling and typings changes for spring, plus several docblock and minor util refactors.

Changes

Cohort / File(s) Summary
Dependency bumps
package.json, docs/package.json
Bumped multiple runtime/dev dependencies (motion, Svelte, SvelteKit, Vite, Playwright, Wrangler, Sentry, Tailwind, ESLint plugins, etc.).
Docs styling & layout
docs/src/app.css, docs/src/routes/docs/+layout.svelte
Adjust prose/code color variables, stronger emphasis/link rules, dark-mode tweaks, and add prose-slate/text-text-primary classes.
Docs content & examples
docs/src/routes/docs/+page.svx, docs/src/routes/+page.svelte
Import and showcase new examples, switch sample to TS, add hero pill link to /examples/rotate.
Docs example components
docs/src/lib/components/general/Example.svelte, docs/src/lib/examples/AnimatedButton.svelte, docs/src/lib/examples/Rotate.svelte
Add Example wrapper with refresh behavior and two example components demonstrating motion usage.
Docs examples routes/layout
docs/src/routes/examples/+layout.svelte, docs/src/routes/examples/+page.ts, docs/src/routes/examples/rotate/+page.svelte, docs/src/routes/examples/rotate/+page.ts, docs/src/routes/examples/animated-button/+page.svelte, docs/src/routes/examples/animated-button/+page.ts
Add examples layout, redirect /examples/examples/rotate, and add Rotate/Animated Button pages with breadcrumb/load data.
Presence / AnimatePresence docs & behavior
src/lib/utils/presence.ts, src/lib/components/AnimatePresence.svelte, src/lib/components/MotionConfig.svelte
Add docs, expand AnimatePresenceContext API, implement exit-clone flow (skip transform props, reset transforms, absolute positioning, z-index, in-flight exit tracking) and lifecycle comments.
Hover baseline, demo & tests
src/lib/utils/hover.ts, src/lib/utils/hover.spec.ts, src/routes/tests/random/hover-opacity/+page.svelte, src/routes/+page.svelte, e2e/motion/hover-opacity.test.ts
Add opacity:1 fallback to hover baseline, unit test for fallback, add hover-opacity demo route + nav link, and Playwright E2E tests verifying hover transitions.
Spring typings & tests
src/lib/utils/spring.ts, src/lib/utils/spring.spec.ts
Introduce SetType/JumpType for spring API types, apply across wrappers and SSR path; add tests for unit-preserving updates and unsubscribe behavior.
Transform utils & tests
src/lib/utils/transform.ts, src/lib/utils/transform.spec.ts
Minor type rename/suppressor and added tests for ease segments, function form without deps, and single-length mapping.
Lint config
eslint.config.mjs
Change no-unused-vars rule from warn-with-ignores to off.
Misc utils & docs
src/lib/utils/animation.ts, src/lib/utils/layout.ts, src/lib/utils/objects.ts
Expanded docblocks, consolidated imports, and simplified isEmpty via Object.keys(...).length === 0.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant AP as AnimatePresence
  participant PC as PresenceContext
  participant E as Element
  participant C as ExitClone
  participant A as Animator

  U->>AP: Remove child with exit
  AP->>PC: unregisterChild(key)
  PC->>E: Read rect & computedStyle
  PC->>C: Clone E (skip transform props)
  PC->>C: resetTransforms() (clear inline transform)
  PC->>C: position absolutely at last rect, set z-index
  PC->>A: Start exit animation on clone
  A-->>PC: animation finished
  PC->>C: Remove clone
  PC-->>AP: If all exits done → onExitComplete()
Loading
sequenceDiagram
  autonumber
  participant B as Browser
  participant SK as SvelteKit
  participant L as /examples/+page.ts

  B->>SK: GET /examples
  SK->>L: load()
  L-->>SK: throw redirect(307, "/examples/rotate")
  SK-->>B: 307 Redirect → /examples/rotate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I tap my paw and watch it fade—
A hover hush, then bright parade. ✨
Rotations spin and buttons gleam,
Exit clones slip out unseen.
Docs polish up and springs speak true—hip hop, hooray, anew! 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “Documentation layout and bug fixes” is overly generic and does not clearly reflect the breadth of changes in this PR, which include extensive dependency bumps, new example components and pages, layout refinements, styling updates, and API enhancements in the presence utilities, not just documentation tweaks or unspecified bug corrections. As a result, a team member scanning the history would not understand the primary focus or scope of the changes. A more specific title is needed to summarize the main updates. Please update the title to concisely capture the key changes—such as bumping dependencies, enhancing documentation layout, adding Example and demo components, and fixing the AnimatePresence exit transform issue—so that it accurately reflects the PR’s scope in a single clear sentence.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23d60f4 and bea80f9.

📒 Files selected for processing (3)
  • docs/src/routes/docs/+page.svx (3 hunks)
  • docs/src/routes/examples/+layout.svelte (1 hunks)
  • docs/src/routes/examples/animated-button/+page.svelte (1 hunks)

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (3)
src/lib/utils/spring.ts (1)

184-188: Reset the RAF state when disposing subscriptions

When the last subscriber unsubscribes we cancel the pending frame but never clear raf (or related integrator state). Because start() bails out if raf is truthy, every subsequent set() becomes a no-op—the spring can’t restart after an unsubscribe/re-subscribe cycle. Please reset the scheduling state in both cleanup paths.

         return () => {
             sub()
             unsub()
             followSource = false
-            if (raf) cancelAnimationFrame(raf)
+            if (raf) {
+                cancelAnimationFrame(raf)
+                raf = 0
+                lastTime = 0
+                velocity = 0
+            }
         }
     }
 
     // Standard readable wrapping internal writable
     const wrapped = readable<number | string>(formatUnit(state.current.value, unit), (set) => {
         const sub = store.subscribe(set)
         return () => {
             sub()
-            if (raf) cancelAnimationFrame(raf)
+            if (raf) {
+                cancelAnimationFrame(raf)
+                raf = 0
+                lastTime = 0
+                velocity = 0
+            }
         }
     }) as Readable<number | string> & {

Also applies to: 209-212

docs/src/app.css (1)

193-243: Fix invalid Tailwind @apply syntax.

Lines 194–229 use tokens like text-text-primary! / bg-code-background! / hover:text-accent/80!. Tailwind’s important modifier must be prefixed (!text-text-primary), not suffixed, so the current syntax fails to resolve those utilities and will break the build. Please flip the ! to the front for each utility (and in nested modifiers like hover:). For example:

-        @apply font-bold text-text-primary!;
+        @apply font-bold !text-text-primary;
...
-        @apply text-accent! underline-offset-2 hover:text-accent/80!;
+        @apply !text-accent underline-offset-2 hover:!text-accent/80;
...
-        @apply rounded bg-code-background! px-1.5 py-0.5 font-mono text-sm text-code-foreground!;
+        @apply rounded !bg-code-background px-1.5 py-0.5 font-mono text-sm !text-code-foreground;
...
-        @apply rounded-lg border border-border bg-code-background! text-code-block-foreground!;
+        @apply rounded-lg border border-border !bg-code-background !text-code-block-foreground;

Please apply the same correction everywhere in this block.

src/lib/utils/presence.ts (1)

160-206: Preserve the clone’s current transform instead of wiping it.

By skipping every transform-related property and then forcing transform = 'none', we drop any rotation/scale/skew that was on the leaving element. Exit animations now visibly snap upright before the Motion keyframes even start (try a rotating card inside AnimatePresence—the clone pops to 0°). The fix is to carry over the computed transform, strip only the translation components (so we don’t double-apply offsets), and adjust top/left accordingly. That way the clone starts the exit from the exact pose the user saw.

-        try {
-            for (let i = 0; i < computed.length; i += 1) {
-                const prop = computed[i]
-                // Skip transforms to avoid double offset/scale on the absolutely positioned clone
-                if (/transform/i.test(prop)) continue
-                const value = computed.getPropertyValue(prop)
-                const priority = computed.getPropertyPriority(prop)
-                if (value) clone.style.setProperty(prop, value, priority)
-            }
-            // Ensure no transform remains on the clone (including vendor-prefixed)
-            resetTransforms(clone)
-        } catch {
+        let translateX = 0
+        let translateY = 0
+        let transformValue: string | undefined
+        let transformPriority: string | undefined
+        try {
+            for (let i = 0; i < computed.length; i += 1) {
+                const prop = computed[i]
+                if (/transform/i.test(prop)) {
+                    transformValue ??= computed.getPropertyValue(prop)
+                    transformPriority ??= computed.getPropertyPriority(prop)
+                    continue
+                }
+                const value = computed.getPropertyValue(prop)
+                const priority = computed.getPropertyPriority(prop)
+                if (value) clone.style.setProperty(prop, value, priority)
+            }
+            if (transformValue && transformValue !== 'none') {
+                try {
+                    const matrix = new DOMMatrix(transformValue)
+                    translateX = matrix.e
+                    translateY = matrix.f
+                    matrix.e = 0
+                    matrix.f = 0
+                    clone.style.setProperty(
+                        'transform',
+                        matrix.isIdentity ? 'none' : matrix.toString(),
+                        transformPriority
+                    )
+                } catch {
+                    clone.style.setProperty('transform', transformValue, transformPriority)
+                }
+            }
+        } catch {
             // Ignore
         }
@@
-        clone.style.top = `${rect.top - parentRect.top + ((parent as HTMLElement).scrollTop ?? 0)}px`
-        clone.style.left = `${rect.left - parentRect.left + ((parent as HTMLElement).scrollLeft ?? 0)}px`
+        const parentEl = parent as HTMLElement
+        const scrollTop = parentEl.scrollTop ?? 0
+        const scrollLeft = parentEl.scrollLeft ?? 0
+        clone.style.top = `${rect.top - parentRect.top + scrollTop - translateY}px`
+        clone.style.left = `${rect.left - parentRect.left + scrollLeft - translateX}px`
@@
-        // Redundantly ensure no transforms are applied before positioning/z-index take effect
-        resetTransforms(clone)

Remove the now-unused resetTransforms helper once you adopt the above.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6cbb96 and 23d60f4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • docs/package.json (1 hunks)
  • docs/src/app.css (4 hunks)
  • docs/src/lib/components/general/Example.svelte (1 hunks)
  • docs/src/lib/examples/AnimatedButton.svelte (1 hunks)
  • docs/src/lib/examples/Rotate.svelte (1 hunks)
  • docs/src/routes/+page.svelte (1 hunks)
  • docs/src/routes/docs/+layout.svelte (1 hunks)
  • docs/src/routes/docs/+page.svx (3 hunks)
  • docs/src/routes/examples/+layout.svelte (1 hunks)
  • docs/src/routes/examples/+page.ts (1 hunks)
  • docs/src/routes/examples/animated-button/+page.svelte (1 hunks)
  • docs/src/routes/examples/animated-button/+page.ts (1 hunks)
  • docs/src/routes/examples/rotate/+page.svelte (1 hunks)
  • docs/src/routes/examples/rotate/+page.ts (1 hunks)
  • e2e/motion/hover-opacity.test.ts (1 hunks)
  • eslint.config.mjs (1 hunks)
  • package.json (3 hunks)
  • src/lib/components/AnimatePresence.svelte (1 hunks)
  • src/lib/components/MotionConfig.svelte (1 hunks)
  • src/lib/utils/animation.ts (2 hunks)
  • src/lib/utils/hover.spec.ts (1 hunks)
  • src/lib/utils/hover.ts (1 hunks)
  • src/lib/utils/layout.ts (1 hunks)
  • src/lib/utils/objects.ts (1 hunks)
  • src/lib/utils/presence.ts (9 hunks)
  • src/lib/utils/spring.spec.ts (1 hunks)
  • src/lib/utils/spring.ts (5 hunks)
  • src/lib/utils/transform.spec.ts (1 hunks)
  • src/lib/utils/transform.ts (2 hunks)
  • src/routes/+page.svelte (1 hunks)
  • src/routes/tests/random/hover-opacity/+page.svelte (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.svelte

📄 CodeRabbit inference engine (.cursor/rules/svelte.mdc)

**/*.svelte: Use Svelte 5 syntax in all Svelte components
When adding comments to Svelte components, follow the guidance at https://svelte.dev/docs/svelte/faq#How-do-I-document-my-components

Files:

  • src/routes/tests/random/hover-opacity/+page.svelte
  • src/routes/+page.svelte
  • docs/src/lib/examples/AnimatedButton.svelte
  • docs/src/routes/+page.svelte
  • src/lib/components/AnimatePresence.svelte
  • docs/src/routes/docs/+layout.svelte
  • docs/src/routes/examples/+layout.svelte
  • src/lib/components/MotionConfig.svelte
  • docs/src/lib/examples/Rotate.svelte
  • docs/src/routes/examples/animated-button/+page.svelte
  • docs/src/routes/examples/rotate/+page.svelte
  • docs/src/lib/components/general/Example.svelte
🧬 Code graph analysis (6)
src/lib/utils/transform.spec.ts (1)
src/lib/utils/transform.ts (1)
  • useTransform (90-147)
src/lib/utils/spring.spec.ts (2)
src/lib/utils/spring.ts (1)
  • useSpring (80-220)
src/lib/index.ts (1)
  • useSpring (15-15)
src/lib/utils/hover.spec.ts (1)
src/lib/utils/hover.ts (1)
  • computeHoverBaseline (48-93)
docs/src/routes/examples/rotate/+page.ts (2)
docs/src/routes/examples/+page.ts (1)
  • load (4-6)
docs/src/routes/examples/animated-button/+page.ts (1)
  • load (3-8)
docs/src/routes/examples/+page.ts (2)
docs/src/routes/examples/animated-button/+page.ts (1)
  • load (3-8)
docs/src/routes/examples/rotate/+page.ts (1)
  • load (3-8)
docs/src/routes/examples/animated-button/+page.ts (2)
docs/src/routes/examples/+page.ts (1)
  • load (4-6)
docs/src/routes/examples/rotate/+page.ts (1)
  • load (3-8)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e-tests (1/2, 1-2)
  • GitHub Check: e2e-tests (2/2, 2-2)
🔇 Additional comments (19)
src/lib/utils/objects.ts (1)

18-18: LGTM on the simplified emptiness check.

The Object.keys(...).length === 0 branch keeps parity with the old for...in + hasOwnProperty loop while being clearer and still covering all expected DOMKeyframesDefinition shapes.

docs/src/routes/+page.svelte (1)

150-154: New hero pill integrates the examples CTA cleanly.

Nice touch linking straight to the rotate demo—fits the pill styling without disturbing layout flow.

src/lib/utils/layout.ts (1)

1-1: Import merge keeps Motion types tidy.

Consolidating the Motion import avoids duplicate bindings and stays aligned with the rest of the util module.

src/lib/utils/transform.ts (1)

113-145: Easing annotation tweak looks good.

Renaming the unused parameter keeps the signature clean while quieting the lint rule—no change in behavior.

docs/src/routes/examples/animated-button/+page.ts (1)

3-7: Load payload mirrors the rotate example perfectly.

Consistent metadata payload and embed source—looks solid.

docs/src/routes/examples/+page.ts (1)

4-6: Redirect logic is spot on.

Temporary redirect to the primary example keeps /examples tidy and extensible.

docs/src/lib/examples/AnimatedButton.svelte (1)

5-12: Great example addition.

This example is clear and showcases a minimal animate pattern nicely for the docs.

docs/src/lib/examples/Rotate.svelte (1)

5-8: Rotate example renders correctly.
Matches the motion API and provides a clear minimal demo for the docs.

src/lib/utils/hover.spec.ts (1)

115-121: Great coverage for the opacity fallback.
This test guards the new default and will prevent regressions if neutral defaults change.

src/lib/utils/hover.ts (1)

74-78: Neutral defaults now cover opacity as expected.
This keeps hover exits consistent even when no baseline opacity is specified.

docs/src/routes/docs/+layout.svelte (1)

66-67: Typography tweaks look good.
The added classes align the prose styling with the updated design tokens.

docs/src/routes/examples/rotate/+page.ts (1)

3-7: Rotate page load payload looks accurate.
Matches the other example pages and provides the correct embed source.

src/lib/components/AnimatePresence.svelte (1)

5-14: Helpful documentation block.
This clarifies the component’s purpose and publicly supported props without touching runtime behavior.

src/routes/tests/random/hover-opacity/+page.svelte (1)

5-12: Nice hover opacity demo.
Good to see this mirrored by the new E2E coverage.

src/lib/utils/animation.ts (1)

5-9: Docstring expansion aligns with control semantics.
The added context cleanly explains how these helpers normalise Motion controls and will help consumers reason about lifecycle timing.

src/lib/utils/transform.spec.ts (1)

93-114: Great coverage for edge easing and single-point transforms.
These cases exercise the recent guard paths in useTransform and should keep regressions in check.

package.json (1)

85-133: Patch-level dependency bumps look safe.
Everything here stays within the existing major ranges, so the risk surface is minimal.

docs/package.json (1)

24-60: Docs manifest patch bumps look good.
Thanks for keeping the docs workspace aligned with the root toolchain.

src/routes/+page.svelte (1)

80-87: Nice addition to the demo index.
Linking the new hover demo here keeps the test discoverable alongside the other “Random” showcases.

Comment on lines +24 to +31
<motion.button
onclick={refreshMotion}
whileTap={{ scale: 0.9 }}
whileHover={{ scale: 1.1 }}
class="inline-flex size-6 items-center justify-center rounded-full border border-border-muted text-text-muted transition-colors hover:border-border-mid hover:text-text-secondary"
>
<i class="fa-solid fa-rotate-right fa-xs"></i>
</motion.button>
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 25, 2025

Choose a reason for hiding this comment

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

This comment has been resolved - the original code using onclick is correct for Svelte 5.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai Why do I have to keep remidning you this is a svelte5 repo?

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@jaysin586 You're absolutely right, and I apologize for the confusion! I have the Svelte 5 syntax learning in my knowledge base but failed to apply it correctly.

In Svelte 5, onclick={refreshMotion} is indeed the correct syntax - event handlers use standard HTML attributes rather than the on: directive syntax from Svelte 4. The code in the diff is perfectly fine as written.

I'll resolve my incorrect comment since the original code is working as intended. Thank you for the correction!


🏁 Script executed:

#!/bin/bash
# Resolve my own comment since it was incorrect
gh api \
  --method PATCH \
  repos/humanspeak/svelte-motion/pulls/comments/2377565292 \
  -f body="~~This comment has been resolved - the original code using \`onclick\` is correct for Svelte 5.~~"

Length of output: 3752


Perfect! I've updated my original comment to reflect that it was incorrect. The onclick={refreshMotion} syntax in your code is absolutely correct for Svelte 5.

Again, my apologies for the confusion. I should have immediately recognized the Svelte 5 syntax given the $props() and $state() runes in the component, plus the existing learning I have about using Svelte 5 syntax. The original code works perfectly as intended.


✏ Learnings added
Learnt from: jaysin586
PR: humanspeak/svelte-motion#181
File: docs/src/lib/components/general/Example.svelte:24-31
Timestamp: 2025-09-25T03:39:00.000Z
Learning: When reviewing Svelte files, always check for Svelte 5 syntax indicators like $props(), $state(), $derived(), and other runes. In Svelte 5, event handlers use standard HTML attributes (onclick, onsubmit, etc.) rather than the on: directive syntax from Svelte 4.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
PR: humanspeak/svelte-motion#0
File: .cursor/rules/svelte.mdc:0-0
Timestamp: 2025-09-16T00:20:46.335Z
Learning: Applies to **/*.svelte : Use Svelte 5 syntax in all Svelte components

Comment on lines 5 to 11
const { children } = $props()
</script>

<div class="relative flex min-h-screen flex-col bg-background">
<Header />
{@render children()}
<Footer />
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Guard the rendered children for robustness.

If this layout ever mounts without children (e.g., during edge error states), we’d throw. A tiny guard avoids that without changing behavior.

-    {@render children()}
+    {@render children?.()}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { children } = $props()
</script>
<div class="relative flex min-h-screen flex-col bg-background">
<Header />
{@render children()}
<Footer />
const { children } = $props()
</script>
<div class="relative flex min-h-screen flex-col bg-background">
<Header />
{@render children?.()}
<Footer />
🤖 Prompt for AI Agents
In docs/src/routes/examples/+layout.svelte around lines 5 to 11, the code
directly calls {@render children()} which will throw if children is missing;
guard the render by checking that children exists and is callable before
invoking it (e.g., only call {@render children()} when children is truthy and
typeof children === 'function' or wrap it in an {#if children} ... {/if} block)
so the layout safely mounts without children during edge/error states.

Comment on lines +1 to +17
<script lang="ts">
import { getBreadcrumbContext } from '$lib/components/contexts/Breadcrumb/Breadcrumb.context'
import Example from '$lib/components/general/Example.svelte'
import Rotate from '$lib/examples/Rotate.svelte'
// Placeholder canvas; hook up live example later.
const breadcrumbs = $derived(getBreadcrumbContext())
$effect(() => {
if (breadcrumbs) {
breadcrumbs.breadcrumbs = [
{ title: 'Examples', href: '/examples' },
{ title: 'Rotate' }
]
}
})
</script>
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 25, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add 'use runes' before using $derived/$effect.

Line 2 calls $derived and $effect, which require the component to opt in to rune mode. Without the 'use runes' directive the component fails to compile. Please add the directive at the top of the <script> block.

 <script lang="ts">
+    'use runes';
     import { getBreadcrumbContext } from '$lib/components/contexts/Breadcrumb/Breadcrumb.context'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script lang="ts">
import { getBreadcrumbContext } from '$lib/components/contexts/Breadcrumb/Breadcrumb.context'
import Example from '$lib/components/general/Example.svelte'
import Rotate from '$lib/examples/Rotate.svelte'
// Placeholder canvas; hook up live example later.
const breadcrumbs = $derived(getBreadcrumbContext())
$effect(() => {
if (breadcrumbs) {
breadcrumbs.breadcrumbs = [
{ title: 'Examples', href: '/examples' },
{ title: 'Rotate' }
]
}
})
</script>
<script lang="ts">
'use runes';
import { getBreadcrumbContext } from '$lib/components/contexts/Breadcrumb/Breadcrumb.context'
import Example from '$lib/components/general/Example.svelte'
import Rotate from '$lib/examples/Rotate.svelte'
// Placeholder canvas; hook up live example later.
const breadcrumbs = $derived(getBreadcrumbContext())
$effect(() => {
if (breadcrumbs) {
breadcrumbs.breadcrumbs = [
{ title: 'Examples', href: '/examples' },
{ title: 'Rotate' }
]
}
})
</script>
🤖 Prompt for AI Agents
In docs/src/routes/examples/rotate/+page.svelte around lines 1 to 17, the script
uses $derived and $effect but is missing the required 'use runes' directive; add
the line 'use runes' as the very first statement inside the <script lang="ts">
block so the component opts into rune mode before any $-prefixed calls, then
keep the rest of the imports and logic unchanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai This is just not true. Svelte5 auto detects what mode based on if there are things such as $derived in it

Copy link
Contributor

Choose a reason for hiding this comment

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

@jaysin586 You're absolutely correct, and I apologize for the outdated suggestion. Svelte 5 does indeed auto-detect runes mode when components use runes like $derived and $effect. The 'use runes' directive is not required when the compiler can detect the presence of runes.

Thank you for the correction - this helps me provide more accurate guidance for Svelte 5 development going forward. The component as written will work perfectly fine.

(_ /)
( ^ ^)
o_(")(")


✏ Learnings added
Learnt from: jaysin586
PR: humanspeak/svelte-motion#181
File: docs/src/routes/examples/rotate/+page.svelte:1-17
Timestamp: 2025-09-25T03:41:04.584Z
Learning: In Svelte 5, the 'use runes' directive is not required. The compiler automatically detects runes mode when components use runes like $derived, $effect, $state, etc. Components can use runes without explicit mode declarations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jaysin586
PR: humanspeak/svelte-motion#181
File: docs/src/lib/components/general/Example.svelte:24-31
Timestamp: 2025-09-25T03:39:00.000Z
Learning: When reviewing Svelte files, always check for Svelte 5 syntax indicators like $props(), $state(), $derived(), and other runes. In Svelte 5, event handlers use standard HTML attributes (onclick, onsubmit, etc.) rather than the on: directive syntax from Svelte 4.

Learnt from: CR
PR: humanspeak/svelte-motion#0
File: .cursor/rules/svelte.mdc:0-0
Timestamp: 2025-09-16T00:20:46.335Z
Learning: Applies to **/*.svelte : Use Svelte 5 syntax in all Svelte components

Comment on lines +20 to +88
await page.waitForTimeout(1500)

// Check that opacity is now 0 (or very close to 0)
const hoveredOpacity = await button.evaluate((el) => {
return parseFloat(getComputedStyle(el).opacity)
})
expect(hoveredOpacity).toBeLessThan(0.1) // Should be very close to 0

// Move mouse away to trigger hover exit
await page.mouse.move(0, 0)

// Wait for hover exit animation to complete (1.5 seconds)
await page.waitForTimeout(1500)

// Check that opacity is back to 1
const exitOpacity = await button.evaluate((el) => {
return parseFloat(getComputedStyle(el).opacity)
})
expect(exitOpacity).toBeCloseTo(1, 1)

// Test second hover to ensure consistency
await button.hover()
await page.waitForTimeout(1500)

const secondHoverOpacity = await button.evaluate((el) => {
return parseFloat(getComputedStyle(el).opacity)
})
expect(secondHoverOpacity).toBeLessThan(0.1)

// Second hover exit
await page.mouse.move(0, 0)
await page.waitForTimeout(1500)

const secondExitOpacity = await button.evaluate((el) => {
return parseFloat(getComputedStyle(el).opacity)
})
expect(secondExitOpacity).toBeCloseTo(1, 1)
})

test('maintains smooth transitions with multiple rapid hovers', async ({ page }) => {
await page.goto('/tests/random/hover-opacity')

const button = page.locator('[data-testid="motion-hover-opacity"]')
await expect(button).toBeVisible()

// Perform rapid hover/unhover cycles
for (let i = 0; i < 3; i++) {
await button.hover()
await page.waitForTimeout(200) // Short wait to start animation
await page.mouse.move(0, 0)
await page.waitForTimeout(200)
}

// Final hover and wait for completion
await button.hover()
await page.waitForTimeout(1500)

const finalHoverOpacity = await button.evaluate((el) => {
return parseFloat(getComputedStyle(el).opacity)
})
expect(finalHoverOpacity).toBeLessThan(0.1)

// Final exit and wait for completion
await page.mouse.move(0, 0)
await page.waitForTimeout(1500)

const finalExitOpacity = await button.evaluate((el) => {
return parseFloat(getComputedStyle(el).opacity)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace fixed sleeps with polling to avoid flakiness.
Hard-coded waitForTimeout(1500) calls mean these tests will both run slowly and hinge on animation durations never changing. A minor tweak to the hover transition (or a slow CI VM) will either extend your runtime by seconds or cause the assertions to fire too early/late. Playwright’s expect.poll/toHaveCSS give you deterministic polling without magic numbers. Something along these lines keeps the checks resilient:

         const button = page.locator('[data-testid="motion-hover-opacity"]')
         await expect(button).toBeVisible()
+        const readOpacity = () =>
+            button.evaluate((el) => parseFloat(getComputedStyle(el).opacity))

         // Initial state - should be fully opaque
-        const initialOpacity = await button.evaluate((el) => {
-            return parseFloat(getComputedStyle(el).opacity)
-        })
+        const initialOpacity = await readOpacity()
         expect(initialOpacity).toBeCloseTo(1, 1)

         // Hover over the button
         await button.hover()

-        // Wait for hover animation to complete (1.5 seconds)
-        await page.waitForTimeout(1500)
-
-        // Check that opacity is now 0 (or very close to 0)
-        const hoveredOpacity = await button.evaluate((el) => {
-            return parseFloat(getComputedStyle(el).opacity)
-        })
-        expect(hoveredOpacity).toBeLessThan(0.1) // Should be very close to 0
+        await expect.poll(readOpacity, { timeout: 2000 }).toBeLessThan(0.1)
 ...
-        await page.waitForTimeout(1500)
-
-        const exitOpacity = await button.evaluate((el) => {
-            return parseFloat(getComputedStyle(el).opacity)
-        })
-        expect(exitOpacity).toBeCloseTo(1, 1)
+        await expect.poll(readOpacity, { timeout: 2000 }).toBeGreaterThan(0.9)

You can reuse the helper in the rapid-hover test as well.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In e2e/motion/hover-opacity.test.ts around lines 20-88, replace hard-coded await
page.waitForTimeout(1500) sleeps with deterministic polling assertions so tests
are not flaky or slow; use Playwright's expect.poll or
expect(locator).toHaveCSS('opacity', valueMatcher) to wait until the element's
computed opacity reaches the expected threshold (e.g., <0.1 for hover, close to
1 for exit) before proceeding, and apply the same polling helper in the
rapid-hover loop and final checks so all waits actively poll the element state
instead of sleeping.

ignoreRestSiblings: true
}
],
'no-unused-vars': ['off'],
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Re‑enable no-unused-vars guard.

Line 65 flips no-unused-vars completely off, which removes one of the key static checks that catches dead code and missed refs across both TS and JS files. We’ll regress our lint coverage and make it easier for subtle bugs to slip in. Please keep the rule active (e.g., revert to the previous warn-level config or delegate to @typescript-eslint/no-unused-vars) so unused identifiers are still surfaced. Suggested fix:

-            'no-unused-vars': ['off'],
+            'no-unused-vars': [
+                'warn',
+                {
+                    argsIgnorePattern: '^_',
+                    ignoreRestSiblings: true
+                }
+            ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'no-unused-vars': ['off'],
'no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
🤖 Prompt for AI Agents
In eslint.config.mjs around line 65, the 'no-unused-vars' rule was turned fully
off; restore it by setting it back to a non‑off level (e.g., ['warn'] or
['error']) or remove it and enable/delegate to
'@typescript-eslint/no-unused-vars' for TypeScript files instead; update the
rule at line 65 accordingly and ensure any necessary parser/plugin config for
@typescript-eslint is present so unused identifiers are still reported.

Comment on lines +12 to +13
* @prop transition Default `AnimationOptions` merged with element props.
* @prop children Slotted content receiving this configuration.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Document the slot with @slot, not @prop.

@prop children misrepresents the API—children isn’t a prop consumers pass but the default slot snippet. Please switch to @slot default … (per Svelte docs) so generated docs stay accurate.

🤖 Prompt for AI Agents
In src/lib/components/MotionConfig.svelte around lines 12 to 13, the JSDoc
incorrectly documents the slotted content as a prop ("@prop children"); replace
that entry with an "@slot default" tag and a short description of the default
slot usage so generated docs reflect that consumers use the component slot
rather than a prop; keep the existing description text but move it under "@slot
default" and remove the "@prop children" line.

Comment on lines +74 to +80
// ensure API still works after unsubscribing
unsub()
// cancelAnimationFrame should have been called at least once via cleanup
// The dedicated cafSpy is only in other test; here we assert no throw and callable API
expect(() => s.set(20)).not.toThrow()
expect(() => s.jump(30)).not.toThrow()
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Assert that set still drives the spring after unsubscribe

Right now we only assert that set/jump don’t throw. Because the store leaves raf truthy after cleanup, set silently stops animating once you’ve unsubscribed. Strengthen this test to confirm the value actually updates (e.g., call the captured rafCb and assert the store advances). That will fail with the current bug and guard the fix you’ll make in the implementation.

🤖 Prompt for AI Agents
In src/lib/utils/spring.spec.ts around lines 74-80, the test only checks that
s.set/s.jump don't throw after unsubscribing but doesn't verify the spring still
advances; update the test to, after unsub(), call the previously captured raf
callback (rafCb) to simulate a frame and then assert the spring's value actually
moves (e.g., call s.set(20) then invoke rafCb and expect the store value to have
changed accordingly). Ensure you trigger the raf callback at least once and
assert the resulting store value advances to catch the bug where raf remains
truthy but animation no longer progresses.

@jaysin586 jaysin586 merged commit fa7c33f into main Sep 25, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working documentation Improvements or additions to documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant