Skip to content

Comments

Feat/props#23

Merged
epavanello merged 5 commits intomainfrom
feat/props
Feb 15, 2026
Merged

Feat/props#23
epavanello merged 5 commits intomainfrom
feat/props

Conversation

@epavanello
Copy link
Owner

@epavanello epavanello commented Feb 15, 2026

Summary by CodeRabbit

  • New Features

    • Added jump-to-property navigation from keyframe cards to quickly navigate to corresponding layer properties.
    • Introduced interpolation family selector for advanced keyframe interpolation control with multiple strategy options.
  • Improvements

    • Reorganized transform properties with separated position, rotation, and scale groups for clearer hierarchy.
    • Enhanced keyframe card UI with dedicated controls for interpolation families and strategies.
  • Bug Fixes

    • Corrected layer transform initialization to use proper property structure.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Warning

Rate limit exceeded

@epavanello has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR restructures the transform data model from flat fields (x, y, z, rotationX/Y/Z, scaleX/Y) to a nested hierarchy (position, rotation, scale objects), updates the interpolation system to support optional interpolation and family-based strategies, and refactors UI components and AI mutations to align with the new structure. New utility modules provide property categorization and interpolation family management.

Changes

Cohort / File(s) Summary
Transform Schema & Core Types
src/lib/schemas/base.ts, src/lib/schemas/animation.ts
Introduced nested TransformSchema supporting both legacy flat fields and new structure (position, rotation, scale) with validation and auto-conversion. Updated AnimatableProperty and PropsAnimatableProperty types to use template literals and LiteralUnion for flexibility. Made KeyframeSchema.interpolation optional and added ContinuousInterpolationStrategy types.
AI System & Mutations
src/lib/ai/mutations.ts, src/lib/ai/schemas.ts, src/lib/ai/system-prompt.ts, src/lib/components/ai/ai-chat.svelte
Updated CreateLayerInputSchema and EditLayerInputSchema to use TransformSchema instead of flat position fields. Fixed field mapping in mutateCreateLayer (trasform → transform) and restructured mutateEditLayer, mutateGroupLayers, and mutateUngroupLayers to operate on nested transform objects. System prompt updated to reference nested transform paths (position.x/y, rotation.z, scale.x/y) and use optional chaining for interpolation strategy.
Interpolation & Strategy Management
src/lib/engine/interpolation.ts, src/lib/utils/interpolation-utils.ts
Made interpolation parameter optional in interpolateValue and added missing interpolation guard. Strengthened type safety with ContinuousInterpolationStrategy. New interpolation-utils module introduces getSupportedInterpolationFamilies, getStrategyOptionsForFamilies, getDefaultInterpolationForProperty, and isInterpolationValid for family-based interpolation handling.
Layer Creation & Rendering
src/lib/engine/layer-factory.ts, src/lib/engine/layer-rendering.ts, src/lib/layers/base.ts, src/lib/layers/LayerWrapper.svelte
Updated createLayer to use transform override with nested structure. getLayerTransform now returns nested position/rotation/scale objects. generateTransformCSS refactored to apply CSS transforms using nested fields. LayerWrapper accessors updated to reference position.x/y instead of flat x/y.
Properties & Input Panel
src/lib/components/editor/panels/properties-panel.svelte, src/lib/components/editor/panels/add-layer.svelte, src/lib/components/editor/panels/input-wrapper.svelte, src/lib/components/editor/panels/inputs-wrapper.svelte, src/lib/components/editor/panels/input-property.svelte
Major restructuring of properties-panel to use nested transform paths (position.x/y/z, rotation.x/y/z, scale.x/y) in bindings and UI. Added setDeep helper for deep property updates. Renamed input-wrapper prop from id to for (htmlFor). Updated input-property to use props. namespace for IDs. Replaced id references with for attributes throughout.
Keyframe & Animation UI
src/lib/components/editor/keyframe-card.svelte, src/lib/components/editor/panels/layer-keyframes.svelte, src/lib/components/editor/timeline/timeline-keyframe.svelte
Extended keyframe-card with layerType prop and family-based interpolation UI. Added Family selector with getSupportedInterpolationFamilies and getStrategyOptionsForFamilies. Introduced Jump to property button with onGoToPropertyClick callback. Updated layer-keyframes to pass layerType prop. timeline-keyframe now passes layerType and manages popover close on property navigation.
Store & Project Management
src/lib/stores/project.svelte.ts, src/routes/(app)/p/[id]/+page.svelte
Updated createGroup to use nested transform structure for groups and preserve TypedLayer<'group'> typing. Reworked ungroup and group-related operations to bake offsets using nested position/rotation/scale. Added project data parsing with ProjectSchema.omit and error handling with toast feedback in route loader.
Supporting Utilities & UI
src/lib/utils/property-names.ts, src/lib/schemas/size.ts, src/lib/components/ui/select/select.svelte
New property-names module provides property categorization and input ID generation (getTransformInputId, getStyleInputId, getPropertyCategory, isTransformProperty, isStyleProperty). Updated scaleMiddleware to support nested scale.x/scale.y locking. Made Select component value prop optional with placeholder fallback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #18: Overlaps with nested transform refactor and group layer mutations; same core changes to layer structure, grouping operations, and transform initialization.
  • PR #15: Modifies AI mutation and schema files (mutateEditLayer, tool-generation logic) with similar pattern changes to layer creation/edit flows.
  • PR #20: Concurrent system-prompt changes for rotation units (radians) and keyframe interpolation optional chaining; directly related on prompt updates.

Poem

🐰 Transforms now nest like burrows deep,
Position, rotation, scale in heaps,
No more flat fields to make us weep,
Interpolation guards the keep,
A prettier structure for us to reap!

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "Feat/props" is vague and does not clearly convey the main purpose of this comprehensive refactoring that restructures transform properties from flat to nested objects and introduces interpolation utilities. Consider a more descriptive title such as 'Refactor transform schema to nested structure with position/rotation/scale' or 'Add interpolation utilities and transform property restructuring' to better communicate the scope and intent.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/props

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

❤️ Share

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

Copy link

@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: 11

Caution

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

⚠️ Outside diff range comments (1)
src/lib/ai/mutations.ts (1)

303-310: ⚠️ Potential issue | 🟠 Major

Merge transform updates instead of replacing to preserve existing anchor.

When input.updates.transform is provided (line 304), the TransformSchema applies defaults during parsing, including anchor: 'center' if not explicitly provided. This replaces the entire layer.transform object, silently losing any previously-set custom anchor—even if the AI only intended to update position or rotation. The separate anchor check (line 308) only helps if the caller explicitly provides anchor alongside the transform.

Proposed fix
     if (input.updates.transform) {
-      layer.transform = input.updates.transform;
+      layer.transform = { ...layer.transform, ...input.updates.transform };
     }
🤖 Fix all issues with AI agents
In `@src/lib/ai/system-prompt.ts`:
- Line 126: The system prompt says rotations are in degrees but the canvas state
string uses "rad" for layer.transform.rotation.z; update the canvas-state
template in src/lib/ai/system-prompt.ts so the rotation is presented in degrees
(convert layer.transform.rotation.z from radians to degrees using * 180 /
Math.PI) and change the unit label from "rad" to "deg" (or "degrees") to make
units consistent with the prompt.

In `@src/lib/components/editor/keyframe-card.svelte`:
- Around line 20-24: Remove the unused import getDefaultInterpolationForProperty
from the import list in keyframe-card.svelte; update the import statement that
currently imports getStrategyOptionsForFamilies,
getSupportedInterpolationFamilies, and getDefaultInterpolationForProperty so it
only imports getStrategyOptionsForFamilies and
getSupportedInterpolationFamilies, and run a quick build/lint to confirm no
references to getDefaultInterpolationForProperty remain.
- Around line 90-97: The getPropertyIcon function currently checks
isTransformProperty first which catches rotation.* and scale.* cases and returns
Move; change the order so specific checks for property.startsWith('rotation.')
and property.startsWith('scale.') come before the isTransformProperty(property)
check, keeping the remaining checks for isStyleProperty(property), property ===
'color' (Palette) and defaulting to Move; update the function that references
getPropertyIcon, RotateCw, Scale, isTransformProperty, isStyleProperty, Palette,
and Move accordingly.

In `@src/lib/components/editor/panels/input-propery.svelte`:
- Line 69: The file name is misspelled: rename the component file
input-propery.svelte to input-property.svelte and update all imports/usages to
the new name (look for imports or references to input-propery.svelte and the
component where it's used in editor/panels); ensure any export/default component
name or path strings inside code are consistent with the new filename so nothing
breaks at compile-time.

In `@src/lib/components/editor/panels/properties-panel.svelte`:
- Line 257: Remove the debug console.log call in the properties-panel update
flow: delete the console.log('updateProperty', propertyName, value, target)
statement inside the updateProperty handler (or wherever updateProperty is
defined/used) in the properties-panel.svelte component so the function no longer
emits debug output to the console before returning or dispatching updates.
- Around line 117-131: currentValues.transform currently mixes flat position
fields with nested rotation/scale; change it to the canonical nested shape so
position is under position: { x,y,z } (i.e.
currentValues.transform.position.x/y/z) while keeping rotation and scale nested,
and ensure the object you return uses animatedTransform.position.{x,y,z} ??
selectedLayer.transform.position.{x,y,z} for position and
animatedTransform.rotation/scale for their nested fields; then update all
callers and spreads that assume the canonical Transform (references to
currentValues.transform, animatedTransform, and selectedLayer.transform) to use
.position.* instead of .x/.y/.z so merges/spreads into selectedLayer.transform
remain type-consistent.

In `@src/lib/engine/interpolation.ts`:
- Around line 19-24: The current code in the function handling optional
interpolation (parameter interpolation?: Interpolation) warns on every frame
when interpolation is missing, which is noisy; instead silently default missing
interpolation to a sensible value (e.g. { family: 'continuous', strategy:
'ease-in-out' }) and proceed, or at minimum log once per keyframe creation—so
replace the console.warn and early return with code that sets interpolation =
interpolation ?? { family: 'continuous', strategy: 'ease-in-out' } (or use a
one-time logger keyed by the KeyframeSchema id) and continue using interpolation
to compute the result.

In `@src/lib/layers/LayerWrapper.svelte`:
- Around line 194-200: The code currently builds newTransform using a shallow
spread that adds top-level x/y keys (const newTransform = { ...layer.transform,
x: ..., y: ... }) which leaves layer.transform.position unchanged; change the
update to set the nested position object instead — construct newTransform by
copying layer.transform but replacing position with a new object that sets
position.x and position.y (using hasXKeyframes/hasYKeyframes to decide whether
to use existing position.x/position.y or add movementX/movementY), then call
projectStore.updateLayer(id, { transform: newTransform }) so the updated values
live at transform.position.x and transform.position.y.

In `@src/lib/schemas/base.ts`:
- Around line 93-101: The validator currently rejects inputs when both
hasNewFormat and hasOldFormat are false, which causes valid anchor-only objects
like { anchor: 'top-left' } to be refused; update the check in the Transform
validation to treat an explicit anchor as acceptable by including a condition
for the presence of the anchor field (e.g., check for input.anchor or similar)
before calling ctx.addIssue, so that only inputs lacking new format, old format,
and anchor are rejected; adjust the conditional using the existing hasNewFormat,
hasOldFormat, and the anchor presence check and keep the ctx.addIssue call for
the true-invalid case.
- Around line 103-109: The superRefine block in src/lib/schemas/base.ts
currently calls ctx.addIssue when both hasNewFormat and hasOldFormat are true,
which creates a failing Zod validation; change this to a non-failing behavior:
either remove the ctx.addIssue call entirely or replace it with a non-blocking
notification (e.g., console.warn) and ensure the schema's transform logic (in
the same schema's .transform or the function handling transform) prefers the new
nested fields and silently drops/ignores the old flat fields; reference the
superRefine callback and ctx.addIssue so you update that exact spot and make
sure downstream code (the transform) reconciles/normalizes inputs accordingly.

In `@src/lib/stores/project.svelte.ts`:
- Around line 326-329: In removeLayerFromGroup, the child layer's scale is
incorrectly being added to the group transform scale; change the combination
from addition to multiplication so scale becomes l.transform.scale.x *
gt.scale.x and l.transform.scale.y * gt.scale.y (match the logic used in
ungroupLayers); update the scale computation where l.transform.scale and
gt.scale are combined to use multiplication instead of +.
🧹 Nitpick comments (9)
src/lib/components/ui/select/select.svelte (1)

39-39: Consider ?? instead of || for the fallback.

|| treats empty string "" as falsy, so a value of "" would incorrectly show the placeholder. If that's not intended, prefer nullish coalescing:

Suggested fix
-    {value || placeholder}
+    {value ?? placeholder}
src/lib/schemas/animation.ts (1)

128-128: Making interpolation optional has a runtime consequence worth noting.

With interpolation now optional on KeyframeSchema, the engine's interpolateValue falls back to returning endValue and emitting console.warn on every frame when interpolation is missing. This could be noisy during playback for any keyframe that was created without an explicit interpolation. Consider whether getDefaultInterpolationForProperty (from interpolation-utils.ts) should be used at keyframe creation time to always populate a default, or alternatively suppress the warning for expected cases.

src/lib/utils/property-names.ts (2)

48-60: isTransformProperty duplicates logic already in getPropertyCategory.

Consider implementing isTransformProperty in terms of getPropertyCategory to avoid maintaining the same prefix checks in two places:

♻️ DRY suggestion
 export function isTransformProperty(property: AnimatableProperty): boolean {
-  return (
-    property.startsWith('position.') ||
-    property.startsWith('rotation.') ||
-    property.startsWith('scale.')
-  );
+  return getPropertyCategory(property) === 'transform';
 }

Also applies to: 65-71


76-78: isStyleProperty is narrower than the getPropertyCategory 'style' catch-all.

getPropertyCategory returns 'style' for any property that isn't transform or props (catch-all at line 59), but isStyleProperty only returns true for 'opacity' and 'color'. If a new style property is introduced, getPropertyCategory would classify it as style, but isStyleProperty would miss it. Consider deriving from the category helper for consistency, or documenting that isStyleProperty is intentionally restrictive.

src/lib/utils/interpolation-utils.ts (1)

43-44: Property name extraction can collide for built-in vs. props properties.

property.split('.').pop()! extracts the last segment, so both "position.x" and "props.x" would yield "x". If a layer schema happens to define a prop named x, the built-in property would incorrectly pick up that prop's interpolation metadata instead of defaulting to 'continuous'.

Consider checking the prefix before extracting:

♻️ Proposed fix
-  // Extract property name from animatable property (e.g., "props.fontSize" -> "fontSize")
-  const propertyName = property.includes('.') ? property.split('.').pop()! : property;
+  // Only extract from layer-specific props; built-in properties use defaults
+  if (!property.startsWith('props.')) {
+    // Built-in properties (position.x, scale.y, opacity, etc.) always use continuous
+    return ['continuous'];
+  }
+
+  const propertyName = property.slice('props.'.length);
src/routes/(app)/p/[id]/+page.svelte (1)

28-38: Manual field enumeration is redundant — projectData already has the correct shape.

After Zod parsing, projectData conforms to the schema. You can spread it directly:

♻️ Simplification
       editorState.project.loadProject({
         id: data.project.id,
-        name: projectData.name,
-        width: projectData.width,
-        height: projectData.height,
-        duration: projectData.duration,
-        fps: projectData.fps,
-        background: projectData.background,
-        layers: projectData.layers,
-        fontFamily: projectData.fontFamily
+        ...projectData
       });
src/lib/components/editor/panels/add-layer.svelte (1)

18-29: Redundant transform override — all values match createLayer defaults.

The transform passed here is identical to what createLayer uses internally as defaults. You could simplify this to just createLayer(type, { projectDimensions: { ... } }). Not blocking, just a simplification opportunity.

src/lib/components/editor/timeline/timeline-keyframe.svelte (1)

20-21: Hardcoded fallback type 'rectangle' may mask bugs.

If the layer isn't found (e.g., deleted while the popover is open), silently falling back to 'rectangle' could produce confusing UI behavior. Consider whether you'd rather guard against rendering the keyframe card at all when the layer is missing.

src/lib/ai/schemas.ts (1)

57-63: Remove unused PositionSchema.

PositionSchema (lines 57–63) is not referenced anywhere in the codebase. It was replaced by TransformSchema in CreateLayerInputSchema and should be deleted.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/lib/ai/system-prompt.ts`:
- Line 33: The system prompt's property examples use flat names (rotationX,
rotationY, etc.) that no longer match the nested animatable paths; update the
prompt text in src/lib/ai/system-prompt.ts (the prompt string that contains the
"Rotation", "Position" and "Scale" descriptions) to use dot-notation paths:
rotation.x, rotation.y, rotation.z and likewise position.x/position.y/position.z
and scale.x/scale.y so the AI generates mutations against the actual property
names.
🧹 Nitpick comments (4)
src/lib/components/editor/keyframe-card.svelte (3)

216-230: Hardcoded 500ms delay is fragile for scroll-then-focus.

The setTimeout(500) assumes smooth scrolling completes within 500ms, which varies by browser and scroll distance. Consider listening for the scrollend event on the scrollable container instead, or at minimum extracting the magic number into a named constant.

Sketch using scrollend
-    if (input) {
-      input.scrollIntoView({ behavior: 'smooth', block: 'center' });
-      setTimeout(() => {
-        (input as HTMLInputElement)?.focus?.();
-        (input as HTMLInputElement)?.select?.();
-      }, 500);
-    }
+    if (input) {
+      const scrollContainer = input.closest('[data-scroll-container]') ?? document.documentElement;
+      const onScrollEnd = () => {
+        scrollContainer.removeEventListener('scrollend', onScrollEnd);
+        (input as HTMLInputElement)?.focus?.();
+        (input as HTMLInputElement)?.select?.();
+      };
+      scrollContainer.addEventListener('scrollend', onScrollEnd, { once: true });
+      input.scrollIntoView({ behavior: 'smooth', block: 'center' });
+    }

121-141: Consider adding exhaustiveness check for the family switch.

If InterpolationFamily is extended with a new variant, this switch will silently leave newInterpolation uninitialized. A default: never guard makes this a compile-time error:

default: {
  const _exhaustive: never = family;
  throw new Error(`Unknown family: ${_exhaustive}`);
}

Same applies to the switch in handleStrategyChange (line 147).


38-56: When interpolation is undefined, the strategy Select shows a placeholder but may confuse users.

activeFamily defaults to supportedFamilies[0] when there's no interpolation (line 47), but the strategy Select at line 353 will show "Select Strategy" placeholder since interpolation?.strategy is undefined. This creates a state where a family appears selected but no strategy is visually chosen.

Consider defaulting the displayed strategy to the first option in currentFamilyOptions for visual consistency, or making the placeholder text clearer (e.g., "Default" or "Auto").

src/lib/components/editor/panels/properties-panel.svelte (1)

229-246: setDeep assumes max two levels of nesting — document or guard this.

The helper works correctly for the current use case (position.x, scale.y, etc.) but will silently produce incorrect results if called with a single key (no dot) since it would still work, or with 3+ levels since it only shallow-copies each level. This is fine for now but consider adding a brief JSDoc note about the expected depth.

@epavanello epavanello merged commit 73bfca9 into main Feb 15, 2026
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 16, 2026
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant