Skip to content

Comments

ai impr#26

Merged
epavanello merged 2 commits intomainfrom
feat/ai-impr
Feb 17, 2026
Merged

ai impr#26
epavanello merged 2 commits intomainfrom
feat/ai-impr

Conversation

@epavanello
Copy link
Owner

@epavanello epavanello commented Feb 17, 2026

Summary by CodeRabbit

  • New Features

    • Color interpolation added to continuous animations; docs include expanded interpolation options and typewriter text examples
    • Radial gradient sizing can be overridden for background rendering
  • UI/UX Improvements

    • Richer field labels and expanded descriptions across many layer/property editors
    • Status messages clarified for tool execution; color picker input styling improved
  • Bug Fixes

    • Chat auto-scroll behavior fixed
  • Breaking Changes

    • Tool input shape and default model selection behavior updated (may affect integrations)

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Restructures AI layer-creation inputs to a discriminated union, updates AI model defaults, enriches field metadata (labels/descriptions) across layer components, adds color support to continuous interpolation, and replaces helper-based tool dispatch with explicit switch handling in chat and MCP flows.

Changes

Cohort / File(s) Summary
Repo config & AI models
/.gitignore, src/lib/ai/models.ts
Adds tools.json to .gitignore; flips recommended flags for two models, removes two model entries, and makes DEFAULT_MODEL_ID computed from recommended models with a fallback.
AI layer input & mutation
src/lib/ai/schemas.ts, src/lib/ai/mutations.ts
CreateLayer input reworked to a top-level layer: { type, props } discriminated-union; schema generation replaced with LayerTypePropsUnion; mutateCreateLayer updated to use input.layer.type/props. Legacy helpers removed.
AI chat & MCP dispatch
src/lib/components/ai/ai-chat.svelte, src/routes/mcp/+server.ts
Replaces helper-based tool dispatch with explicit switch-case handling for tool names; adds async scrolling (tick + scrollToBottom) and updates chat watchers.
Interpolation engine
src/lib/engine/interpolation.ts, src/lib/ai/system-prompt.ts
Continuous interpolation now supports color (RGB) interpolation and returns hex strings for colors; system-prompt docs expanded with interpolation/easing/color and typewriter examples.
Animation schemas & types
src/lib/schemas/animation.ts, src/lib/types/animation.ts
Adds descriptive .describe() metadata to interpolation and keyframe schemas; removes re-exports of AnchorPoint/Transform/LayerStyle schemas and types.
Field registry & metadata propagation
src/lib/layers/properties/field-registry.ts, src/lib/layers/base.ts
Adds optional label?: string to FieldMeta and propagates label into property descriptions during metadata extraction.
Layer component metadata updates
src/lib/layers/components/*
Extensive expansion of field descriptions, addition of label and grouping/interpolation metadata for many layers; some layers gain new public fields (e.g., terminal/textColor) and unified registry usage.
UI component tweaks
src/lib/components/ui/color-picker/color-picker.svelte, src/lib/components/ai/tool-part.svelte
Color-picker trigger refactored to an Input snippet with dynamic class merging; tool-part status display expanded to handle additional tool states.
Background styling API
src/lib/schemas/background.ts
backgroundValueToCSS, getStyleProperties, and getBackgroundImage accept an options param to control radial gradient sizing (radialSize).
Route reactive refactor
src/routes/(app)/p/[id]/+page.svelte
Replaces a reactive $effect with an explicit watch to reload project data and update editor state when input data changes.

Sequence Diagram(s)

sequenceDiagram
    participant User as User / AI
    participant Chat as AI Chat (ai-chat.svelte)
    participant MCP as MCP Server (+server.ts)
    participant Mutation as Mutations (mutations.ts)
    participant Engine as Canvas Engine
    participant Interp as Interpolation Engine

    User->>Chat: send tool call (create_layer with layer:{type,props})
    Chat->>Chat: parse tool call, switch-case dispatch
    Chat->>MCP: forward tool execution
    MCP->>Mutation: invoke mutateCreateLayer(input)
    Mutation->>Engine: create layer from input.layer.type & input.layer.props
    Engine->>Engine: apply transforms, timing, and keyframes
    Engine->>Interp: request interpolation for keyframes
    alt continuous + color values
        Interp->>Interp: interpolate in RGB space, return hex color (rgba(..., 0.5) used for visuals)
        Interp->>Engine: return color string
    else continuous + numeric values
        Interp->>Interp: interpolate numeric values
        Interp->>Engine: return number
    end
    Engine->>Chat: return operation result
    Chat->>Chat: update UI and await tick to scrollToBottom
    Chat->>User: show result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 With discriminated types tucked in my paws so neat,

Labels that sing and keyframes that beat,
Colors that blend from hex bright and bold,
Layers spring to life in stories retold,
Hooray — hop forward, metadata's sweet! 🎨✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'ai impr' is vague and does not clearly convey the scope of changes, using an abbreviation ('impr') that could mean multiple things without specific context. Use a more descriptive title that expands the abbreviation and clarifies the main objective, such as 'Improve AI layer creation schema and model handling' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ai-impr

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: 6

Caution

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

⚠️ Outside diff range comments (2)
src/lib/ai/schemas.ts (2)

345-353: ⚠️ Potential issue | 🟡 Minor

keyframeId is .optional() but the .refine() unconditionally requires it.

The field is typed as optional (z.string().optional()), but the refine rejects any payload where keyframeId is undefined, making .optional() misleading and dead code. mutateRemoveKeyframe in mutations.ts also guards with if (!input.keyframeId), confirming it is always expected. The error message "Either keyframeId must be provided" also implies a now-absent second alternative.

🐛 Proposed fix
 export const RemoveKeyframeInputSchema = z
   .object({
     layerId: z.string().describe('Layer ID or reference'),
-    keyframeId: z.string().optional().describe('Specific keyframe ID to remove')
+    keyframeId: z.string().describe('Keyframe ID to remove')
   })
-  .refine((data) => data.keyframeId !== undefined, {
-    message: 'Either keyframeId must be provided',
-    path: ['keyframeId']
-  });
+  ;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/schemas.ts` around lines 345 - 353, RemoveKeyframeInputSchema
declares keyframeId as optional but then .refine() rejects undefined, and
mutateRemoveKeyframe expects keyframeId always; change the schema to reflect
that keyframeId is required by replacing z.string().optional() with
z.string().describe('Specific keyframe ID to remove') (or remove the refine and
make keyframeId required), and update the refine message (or remove the refine
entirely) so it no longer misleadingly says "Either keyframeId must be provided"
— adjust the schema for RemoveKeyframeInputSchema to accurately require
keyframeId to match mutateRemoveKeyframe.

345-353: ⚠️ Potential issue | 🟡 Minor

keyframeId is typed .optional() but the .refine() unconditionally requires it — contradictory.

The refine message "Either keyframeId must be provided" implies an "or" condition that no longer exists. As-is, the field is always required, so .optional() is misleading and the schema should reflect that directly. mutateRemoveKeyframe in mutations.ts also guards with if (!input.keyframeId), confirming it's always expected.

🐛 Proposed fix
 export const RemoveKeyframeInputSchema = z
   .object({
     layerId: z.string().describe('Layer ID or reference'),
-    keyframeId: z.string().optional().describe('Specific keyframe ID to remove')
+    keyframeId: z.string().describe('Keyframe ID to remove')
   })
-  .refine((data) => data.keyframeId !== undefined, {
-    message: 'Either keyframeId must be provided',
-    path: ['keyframeId']
-  });
+  ;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/schemas.ts` around lines 345 - 353, The schema currently marks
keyframeId as optional but then unconditionally refines to require it; update
RemoveKeyframeInputSchema so keyframeId is a required string (remove
.optional()) and eliminate the contradictory .refine() (or adjust it to a
meaningful conditional) and update the validation message accordingly; sync this
change with the consumer mutateRemoveKeyframe which already expects
input.keyframeId to exist.
🧹 Nitpick comments (7)
src/lib/ai/models.ts (2)

170-174: DEFAULT_MODEL_ID — redundant .find() and placement before its dependency.

getRecommendedModels() already filters to recommended: true models, so .find((m) => m.recommended) always returns the first element — it's equivalent to [0]. Additionally, DEFAULT_MODEL_ID is declared before getRecommendedModels() (line 179); while this works due to function hoisting, it reads as if the constant depends on something defined later.

♻️ Suggested cleanup
-/**
- * Default model to use
- */
-export const DEFAULT_MODEL_ID: ModelId =
-  (getRecommendedModels().find((m) => m.recommended)?.id as ModelId) || 'x-ai/grok-4.1-fast';
-
 /**
  * Get recommended models
  */
 export function getRecommendedModels(): AIModel[] {
   return Object.values(AI_MODELS).filter((m) => m.recommended);
 }
+
+/**
+ * Default model to use
+ */
+export const DEFAULT_MODEL_ID: ModelId =
+  (getRecommendedModels()[0]?.id as ModelId) ?? 'x-ai/grok-4.1-fast';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/models.ts` around lines 170 - 174, DEFAULT_MODEL_ID currently uses
getRecommendedModels().find((m) => m.recommended)?.id which is redundant and
declared before getRecommendedModels(); change it to pull the first recommended
model directly (e.g., getRecommendedModels()[0]?.id) and/or move the
DEFAULT_MODEL_ID declaration to after the getRecommendedModels() function so it
reads clearly; ensure you keep the fallback 'x-ai/grok-4.1-fast' and reference
DEFAULT_MODEL_ID and getRecommendedModels() when making the change.

170-174: DEFAULT_MODEL_ID — redundant .find() and defined before its dependency.

getRecommendedModels() already filters to recommended: true models, so .find((m) => m.recommended) is always truthy for every element — it simply returns [0]. Also, DEFAULT_MODEL_ID is declared at line 173 before getRecommendedModels() is defined at line 179; this works via function hoisting but hurts readability.

♻️ Suggested cleanup
-/**
- * Default model to use
- */
-export const DEFAULT_MODEL_ID: ModelId =
-  (getRecommendedModels().find((m) => m.recommended)?.id as ModelId) || 'x-ai/grok-4.1-fast';
-
 /**
  * Get recommended models
  */
 export function getRecommendedModels(): AIModel[] {
   return Object.values(AI_MODELS).filter((m) => m.recommended);
 }
+
+/**
+ * Default model to use
+ */
+export const DEFAULT_MODEL_ID: ModelId =
+  (getRecommendedModels()[0]?.id as ModelId) ?? 'x-ai/grok-4.1-fast';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/models.ts` around lines 170 - 174, DEFAULT_MODEL_ID uses a
redundant .find and is declared before its dependency getRecommendedModels();
change it to directly take the first recommended model id (e.g., use
getRecommendedModels()[0]?.id) and fall back to 'x-ai/grok-4.1-fast', and
relocate the DEFAULT_MODEL_ID declaration to after the getRecommendedModels()
function definition (or at least remove the unnecessary .find((m) =>
m.recommended) and use optional chaining to avoid the cast) so the code is
clearer and not reliant on hoisting.
src/lib/ai/schemas.ts (2)

88-116: Unsafe type cast in generateLayerTypePropsUnion can hide an empty-array runtime error.

[] as unknown as [z.ZodObject<z.ZodRawShape>] bypasses TypeScript's non-empty tuple requirement for z.discriminatedUnion. If the registry is empty or all non-excluded types disappear, z.discriminatedUnion('type', layerSchemas) will throw at module initialization. A runtime guard makes the failure explicit:

🛡️ Suggested fix
-  const layerSchemas = [] as unknown as [z.ZodObject<z.ZodRawShape>];
+  const layerSchemas: z.ZodObject<z.ZodRawShape>[] = [];

   // ... (loop body unchanged) ...

-  return z.discriminatedUnion('type', layerSchemas);
+  if (layerSchemas.length === 0) {
+    throw new Error('generateLayerTypePropsUnion: no layer types available for discriminated union');
+  }
+  return z.discriminatedUnion('type', layerSchemas as unknown as [z.ZodObject<z.ZodRawShape>, ...z.ZodObject<z.ZodRawShape>[]]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/schemas.ts` around lines 88 - 116, The function
generateLayerTypePropsUnion currently constructs layerSchemas with an unsafe
non-empty-tuple cast which can hide the case where no schemas are added and
z.discriminatedUnion throws at module init; change layerSchemas to a normal
array of Zod objects (e.g., z.ZodObject<z.ZodRawShape>[]), after the loop check
if layerSchemas.length === 0 and throw a clear, descriptive Error (or return a
safe fallback schema) to make the failure explicit, then call
z.discriminatedUnion('type', layerSchemas) only when the array is non-empty;
reference generateLayerTypePropsUnion, layerSchemas, and z.discriminatedUnion in
your change.

88-116: Unsafe double-cast in generateLayerTypePropsUnion hides an empty-array runtime risk.

[] as unknown as [z.ZodObject<z.ZodRawShape>] bypasses TypeScript's non-empty tuple requirement for z.discriminatedUnion. If the layer registry is empty or all non-excluded types vanish, z.discriminatedUnion('type', layerSchemas) will throw at module initialisation time — silently from TypeScript's perspective.

🛡️ Suggested fix
-  const layerSchemas = [] as unknown as [z.ZodObject<z.ZodRawShape>];
+  const layerSchemas: z.ZodObject<z.ZodRawShape>[] = [];

   // ... loop body unchanged ...

-  return z.discriminatedUnion('type', layerSchemas);
+  if (layerSchemas.length === 0) {
+    throw new Error(
+      'generateLayerTypePropsUnion: no layer types available for discriminated union'
+    );
+  }
+  return z.discriminatedUnion(
+    'type',
+    layerSchemas as unknown as [z.ZodObject<z.ZodRawShape>, ...z.ZodObject<z.ZodRawShape>[]]
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/schemas.ts` around lines 88 - 116, The generateLayerTypePropsUnion
function uses an unsafe cast for layerSchemas which can be empty and will cause
z.discriminatedUnion to throw at module init; change layerSchemas to a proper
array type (e.g., const layerSchemas: z.ZodObject<z.ZodRawShape>[] = []) and
after populating it check if layerSchemas.length === 0 and return a safe empty
schema (for example z.never() or another appropriate fallback) instead of
calling z.discriminatedUnion when empty; keep using layerRegistry and
getAvailableLayerTypes to populate the array and call
z.discriminatedUnion('type', layerSchemas) only when the array is non-empty.
src/routes/(app)/p/[id]/+page.svelte (1)

10-10: Import order violates coding guidelines.

import { watch } from 'runed' (external package) should be grouped at the top of the import block, before SvelteKit and internal/relative imports. As per coding guidelines, the required order is: External packages → SvelteKit → Internal lib → Relative imports.

🔧 Suggested reordering
+import { toast } from 'svelte-sonner';
+import { watch } from 'runed';
+
 import SeoHead from '$lib/components/seo-head.svelte';
 import JsonLd from '$lib/components/json-ld.svelte';
 import EditorLayout from '$lib/components/editor/editor-layout.svelte';
+
 import { PUBLIC_BASE_URL } from '$env/static/public';
 import type { PageData } from './$types';
+
 import { getEditorState } from '$lib/contexts/editor.svelte';
 import { ProjectSchema } from '$lib/types/animation';
-import { toast } from 'svelte-sonner';
-import { watch } from 'runed';

As per coding guidelines: "Organize imports in order: External packages → SvelteKit → Internal lib → Relative imports".

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

In `@src/routes/`(app)/p/[id]/+page.svelte at line 10, The import for the external
package "watch" is out of order; move the line importing { watch } from 'runed'
to the top of the import block so external packages appear before SvelteKit
imports and internal/relative imports (follow External → SvelteKit → Internal
lib → Relative imports ordering), ensuring the existing SvelteKit imports and
any local imports remain below the runed import.
src/lib/layers/base.ts (1)

222-224: Redundant optional chaining and consider preserving .describe() text alongside the label.

Two minor points:

  1. Redundant ?. inside the if body. TypeScript narrows fieldMeta to non-nullable within the if (fieldMeta?.label) guard, so the inner access can be non-optional:
🔧 Nitpick
  propertiesMeta.meta = fieldMeta;
  if (fieldMeta?.label) {
-   propertiesMeta.description = fieldMeta?.label;
+   propertiesMeta.description = fieldMeta.label;
  }
  1. description now holds the short label, not the Zod .describe() text. Any UI consumer (tooltips, help text) that previously read PropertyMetadata.description for the verbose description will now receive "Width", "Height", etc. If you want to keep both, consider storing the long text in a separate field (e.g. hint) instead of overwriting description.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/layers/base.ts` around lines 222 - 224, The guard if
(fieldMeta?.label) redundantly uses optional chaining inside the block—change
inner access to fieldMeta.label (TypeScript already narrows fieldMeta). Also
avoid overwriting the Zod .describe() text in PropertyMetadata.description:
preserve the existing description value and store the short label in a new field
(e.g. propertiesMeta.hint or propertiesMeta.shortLabel) so consumers can access
both the verbose .describe() content and the human-friendly label; update code
paths that read description to use hint/shortLabel when appropriate and adjust
the PropertyMetadata shape accordingly.
src/lib/components/ai/ai-chat.svelte (1)

100-100: Remove debug console.log or guard it behind a dev flag.

This console.log('Tool call:', toolCall) will fire in production for every tool invocation, potentially logging large input objects. Consider removing it or wrapping it in a development-only check.

Proposed fix
-      console.log('Tool call:', toolCall);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/ai/ai-chat.svelte` at line 100, Remove the unconditional
debug console.log in the ai-chat.svelte component that prints toolCall
(console.log('Tool call:', toolCall)); either delete it or wrap it in a
development-only guard (e.g., check process.env.NODE_ENV === 'development' or a
runtime isDev flag) so it does not log toolCall in production; update any
initialization of isDev or environment checks near the component/script block
and ensure the log references toolCall only inside that guarded branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/components/ai/tool-part.svelte`:
- Around line 35-41: The UI branch for tool.state is missing a case for
'input-available', leaving the paragraph empty; update the conditional in
ToolUIPart (the {`#if` tool.state === ...} block in tool-part.svelte) to include
an {:else if tool.state === 'input-available'} branch that renders a fallback
message like "Ready" or "Input received — awaiting execution" so the UI shows a
clear state for 'input-available' instead of blank.
- Around line 35-41: The template conditional for tool.state in tool-part.svelte
only covers 'output-available', 'input-streaming', and 'output-error', leaving
the <p> empty for other states; add an {:else} clause after the existing
branches to render a sensible label for remaining states (e.g., show "Ready" for
'input-available', "Approval requested" for 'approval-requested', "Approval
responded" for 'approval-responded', "Denied" for 'output-denied', or a generic
"Unknown state" fallback) so the <p> always contains text when tool.state is
anything other than the three handled values. Ensure you modify the same {`#if`
tool.state === ...} block that references tool.state to include this {:else}
fallback.

In `@src/lib/components/ui/color-picker/color-picker.svelte`:
- Around line 40-41: The inline style for the color indicator is placed before
the props spread so any style in {...props} can overwrite it; move the style
attribute ("background-color: {value}; color: {textColor};") to after the
{...props} spread and keep the class attribute after the spread as well so
explicit color styling always wins; update the element in the ColorPicker
component (the element using style, {...props}, and class) to place {...props}
first, then style, then class.
- Around line 36-44: Props declares inputClass (and showInput) but inputClass is
never destructured and thus not applied to the trigger Input; update the
ColorPicker component to destructure inputClass (and showInput if needed) out of
restProps where Props are handled, then merge inputClass into the trigger
Input's class list (the cn(...) call inside the {`#snippet` child({ props })}
block) so the trigger uses class={cn('w-auto min-w-26', props.class as string,
inputClass)}; ensure restProps no longer swallows inputClass and that
ColorPicker still forwards the remaining props unchanged.

In `@src/lib/layers/components/CodeLayer.svelte`:
- Around line 20-22: The description claiming "text" yields plain text with no
highlighting is inaccurate because highlightLine() still applies string,
comment, and number regexes even when the keyword list is empty; update the
.describe(...) text in CodeLayer.svelte (the language option description that
mentions "text") to state that "text" disables keyword-based highlighting but
still applies string, comment, and number patterns (so quoted strings, //, #, /*
*/ comments and numeric literals may still be colored), and reference
highlightLine() behavior in the comment if helpful for future maintainers.

In `@src/lib/layers/components/IconLayer.svelte`:
- Around line 155-176: The fill field in IconLayer.svelte currently defaults to
'none' but registers with interpolationFamily: 'continuous', which causes
interpolateContinuous() to throw for non-color keywords; update the registration
for the fill field (the fill z.string().default('none') .register(...)) to
either set interpolationFamily: 'discrete' or change the default to a valid
color (e.g., '#000000') so continuous interpolation only receives valid color
values; ensure you modify the fill registration (not backgroundColor) in the
same block that references fieldRegistry and label 'Fill'.

---

Outside diff comments:
In `@src/lib/ai/schemas.ts`:
- Around line 345-353: RemoveKeyframeInputSchema declares keyframeId as optional
but then .refine() rejects undefined, and mutateRemoveKeyframe expects
keyframeId always; change the schema to reflect that keyframeId is required by
replacing z.string().optional() with z.string().describe('Specific keyframe ID
to remove') (or remove the refine and make keyframeId required), and update the
refine message (or remove the refine entirely) so it no longer misleadingly says
"Either keyframeId must be provided" — adjust the schema for
RemoveKeyframeInputSchema to accurately require keyframeId to match
mutateRemoveKeyframe.
- Around line 345-353: The schema currently marks keyframeId as optional but
then unconditionally refines to require it; update RemoveKeyframeInputSchema so
keyframeId is a required string (remove .optional()) and eliminate the
contradictory .refine() (or adjust it to a meaningful conditional) and update
the validation message accordingly; sync this change with the consumer
mutateRemoveKeyframe which already expects input.keyframeId to exist.

---

Nitpick comments:
In `@src/lib/ai/models.ts`:
- Around line 170-174: DEFAULT_MODEL_ID currently uses
getRecommendedModels().find((m) => m.recommended)?.id which is redundant and
declared before getRecommendedModels(); change it to pull the first recommended
model directly (e.g., getRecommendedModels()[0]?.id) and/or move the
DEFAULT_MODEL_ID declaration to after the getRecommendedModels() function so it
reads clearly; ensure you keep the fallback 'x-ai/grok-4.1-fast' and reference
DEFAULT_MODEL_ID and getRecommendedModels() when making the change.
- Around line 170-174: DEFAULT_MODEL_ID uses a redundant .find and is declared
before its dependency getRecommendedModels(); change it to directly take the
first recommended model id (e.g., use getRecommendedModels()[0]?.id) and fall
back to 'x-ai/grok-4.1-fast', and relocate the DEFAULT_MODEL_ID declaration to
after the getRecommendedModels() function definition (or at least remove the
unnecessary .find((m) => m.recommended) and use optional chaining to avoid the
cast) so the code is clearer and not reliant on hoisting.

In `@src/lib/ai/schemas.ts`:
- Around line 88-116: The function generateLayerTypePropsUnion currently
constructs layerSchemas with an unsafe non-empty-tuple cast which can hide the
case where no schemas are added and z.discriminatedUnion throws at module init;
change layerSchemas to a normal array of Zod objects (e.g.,
z.ZodObject<z.ZodRawShape>[]), after the loop check if layerSchemas.length === 0
and throw a clear, descriptive Error (or return a safe fallback schema) to make
the failure explicit, then call z.discriminatedUnion('type', layerSchemas) only
when the array is non-empty; reference generateLayerTypePropsUnion,
layerSchemas, and z.discriminatedUnion in your change.
- Around line 88-116: The generateLayerTypePropsUnion function uses an unsafe
cast for layerSchemas which can be empty and will cause z.discriminatedUnion to
throw at module init; change layerSchemas to a proper array type (e.g., const
layerSchemas: z.ZodObject<z.ZodRawShape>[] = []) and after populating it check
if layerSchemas.length === 0 and return a safe empty schema (for example
z.never() or another appropriate fallback) instead of calling
z.discriminatedUnion when empty; keep using layerRegistry and
getAvailableLayerTypes to populate the array and call
z.discriminatedUnion('type', layerSchemas) only when the array is non-empty.

In `@src/lib/components/ai/ai-chat.svelte`:
- Line 100: Remove the unconditional debug console.log in the ai-chat.svelte
component that prints toolCall (console.log('Tool call:', toolCall)); either
delete it or wrap it in a development-only guard (e.g., check
process.env.NODE_ENV === 'development' or a runtime isDev flag) so it does not
log toolCall in production; update any initialization of isDev or environment
checks near the component/script block and ensure the log references toolCall
only inside that guarded branch.

In `@src/lib/layers/base.ts`:
- Around line 222-224: The guard if (fieldMeta?.label) redundantly uses optional
chaining inside the block—change inner access to fieldMeta.label (TypeScript
already narrows fieldMeta). Also avoid overwriting the Zod .describe() text in
PropertyMetadata.description: preserve the existing description value and store
the short label in a new field (e.g. propertiesMeta.hint or
propertiesMeta.shortLabel) so consumers can access both the verbose .describe()
content and the human-friendly label; update code paths that read description to
use hint/shortLabel when appropriate and adjust the PropertyMetadata shape
accordingly.

In `@src/routes/`(app)/p/[id]/+page.svelte:
- Line 10: The import for the external package "watch" is out of order; move the
line importing { watch } from 'runed' to the top of the import block so external
packages appear before SvelteKit imports and internal/relative imports (follow
External → SvelteKit → Internal lib → Relative imports ordering), ensuring the
existing SvelteKit imports and any local imports remain below the runed import.

Comment on lines +35 to +41
{#if tool.state === 'output-available'}
Completed
{:else if tool.state === 'input-streaming'}
Processing...
{:else if tool.state === 'output-error'}
Error
{/if}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

'input-available' state renders an empty <p> — add a fallback.

The four ToolUIPart states are "input-streaming" | "input-available" | "output-available" | "output-error". The 'input-available' state (input fully received, tool about to execute) has no branch, leaving the paragraph empty.

✨ Suggested fix
  {`#if` tool.state === 'output-available'}
    Completed
  {:else if tool.state === 'input-streaming'}
    Processing...
  {:else if tool.state === 'output-error'}
    Error
+ {:else}
+   Pending...
  {/if}
📝 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
{#if tool.state === 'output-available'}
Completed
{:else if tool.state === 'input-streaming'}
Processing...
{:else if tool.state === 'output-error'}
Error
{/if}
{`#if` tool.state === 'output-available'}
Completed
{:else if tool.state === 'input-streaming'}
Processing...
{:else if tool.state === 'output-error'}
Error
{:else}
Pending...
{/if}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/ai/tool-part.svelte` around lines 35 - 41, The UI branch
for tool.state is missing a case for 'input-available', leaving the paragraph
empty; update the conditional in ToolUIPart (the {`#if` tool.state === ...} block
in tool-part.svelte) to include an {:else if tool.state === 'input-available'}
branch that renders a fallback message like "Ready" or "Input received —
awaiting execution" so the UI shows a clear state for 'input-available' instead
of blank.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the tool-part.svelte file and examine its content
find . -name "tool-part.svelte" -type f

Repository: epavanello/devmotion

Length of output: 104


🏁 Script executed:

# Search for tool state type definitions
rg -t svelte -t ts "tool\.state|ToolUIPart|DynamicTool" --max-count 20

Repository: epavanello/devmotion

Length of output: 94


🌐 Web query:

Vercel AI SDK ToolUIPart state values documentation

💡 Result:

In the Vercel AI SDK (v6), a ToolUIPart is a UI message part named tool-{toolName} whose state comes from the UIToolInvocation union type. The possible state values are: input-streaming, input-available, approval-requested, approval-responded, output-available, output-error, output-denied. In output-available, the optional preliminary: true flag indicates streaming/in-progress output vs a final completed output. [1] [2]

Where this is documented:

  • Type reference (most complete): AI SDK TypeScript definitions (UIToolInvocation / ToolUIPart) in the ai package. [1]
  • Docs pages (partial + UI guidance): UIMessage reference (shows core tool states) and the “Subagents” guide (explains tool-part states + preliminary). [2] [3]

Sources: [1] https://app.unpkg.com/ai@6.0.41/files/dist/index.d.ts, [2] https://ai-sdk.dev/docs/agents/subagents, [3] https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message


🏁 Script executed:

cat -n ./src/lib/components/ai/tool-part.svelte

Repository: epavanello/devmotion

Length of output: 3383


Add a fallback for unhandled tool states.

The conditional at lines 35-41 only handles 'output-available', 'input-streaming', and 'output-error', leaving the <p> tag empty for other states ('input-available', 'approval-requested', 'approval-responded', 'output-denied'). Add an {:else} clause to provide a label:

Suggested fix
  {`#if` tool.state === 'output-available'}
    Completed
  {:else if tool.state === 'input-streaming'}
    Processing...
  {:else if tool.state === 'output-error'}
    Error
+ {:else}
+   Pending...
  {/if}
📝 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
{#if tool.state === 'output-available'}
Completed
{:else if tool.state === 'input-streaming'}
Processing...
{:else if tool.state === 'output-error'}
Error
{/if}
{`#if` tool.state === 'output-available'}
Completed
{:else if tool.state === 'input-streaming'}
Processing...
{:else if tool.state === 'output-error'}
Error
{:else}
Pending...
{/if}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/ai/tool-part.svelte` around lines 35 - 41, The template
conditional for tool.state in tool-part.svelte only covers 'output-available',
'input-streaming', and 'output-error', leaving the <p> empty for other states;
add an {:else} clause after the existing branches to render a sensible label for
remaining states (e.g., show "Ready" for 'input-available', "Approval requested"
for 'approval-requested', "Approval responded" for 'approval-responded',
"Denied" for 'output-denied', or a generic "Unknown state" fallback) so the <p>
always contains text when tool.state is anything other than the three handled
values. Ensure you modify the same {`#if` tool.state === ...} block that
references tool.state to include this {:else} fallback.

Comment on lines +36 to +44
{#snippet child({ props })}
<Input
{value}
readonly
style="background-color: {value}; color: {textColor};"
{...props}
class={cn('w-auto min-w-26', props.class as string)}
/>
{/snippet}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -t f "color-picker.svelte" --exec wc -l {}

Repository: epavanello/devmotion

Length of output: 123


🏁 Script executed:

cat -n src/lib/components/ui/color-picker/color-picker.svelte

Repository: epavanello/devmotion

Length of output: 2224


🏁 Script executed:

rg -n --type=svelte 'inputClass' --glob '*.svelte'

Repository: epavanello/devmotion

Length of output: 94


🏁 Script executed:

rg -n 'inputClass' --glob '*.svelte'

Repository: epavanello/devmotion

Length of output: 146


🏁 Script executed:

rg -n 'showInput' --glob '*.svelte'

Repository: epavanello/devmotion

Length of output: 248


inputClass prop is declared in Props but never applied to the trigger Input

inputClass (and showInput) are part of the public Props type but are never destructured, so they fall into restProps and are forwarded to <ColorPicker> (line 57), which ignores them. A caller supplying inputClass to customize the trigger's appearance will silently see no effect. Evidence shows showInput is already being used by callers (e.g., background-picker.svelte line 326), confirming this is a real issue.

If inputClass is intended to style the trigger Input, it should be pulled out of restProps and merged in the cn() call:

♻️ Proposed fix — wire `inputClass` into the trigger
-  let { value = $bindable(), optional, onchange, ...restProps }: Props = $props();
+  let { value = $bindable(), optional, onchange, inputClass, showInput: _showInput, ...restProps }: Props = $props();

Then in the snippet:

-        class={cn('w-auto min-w-26', props.class as string)}
+        class={cn('w-auto min-w-26', inputClass, props.class as string)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/ui/color-picker/color-picker.svelte` around lines 36 - 44,
Props declares inputClass (and showInput) but inputClass is never destructured
and thus not applied to the trigger Input; update the ColorPicker component to
destructure inputClass (and showInput if needed) out of restProps where Props
are handled, then merge inputClass into the trigger Input's class list (the
cn(...) call inside the {`#snippet` child({ props })} block) so the trigger uses
class={cn('w-auto min-w-26', props.class as string, inputClass)}; ensure
restProps no longer swallows inputClass and that ColorPicker still forwards the
remaining props unchanged.

Comment on lines +40 to +41
style="background-color: {value}; color: {textColor};"
{...props}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

style placement before {...props} spread risks overriding the color-indicator styling

In Svelte, when the same attribute appears both explicitly and via a spread, the last one wins. style="background-color: {value}; color: {textColor};" (line 40) is set before {...props} (line 41), so any style key inside props (e.g. from a future Popover.Trigger version adding inline positioning) will silently overwrite the colour-indicator styling. class is already placed correctly after the spread on line 42 — style should follow the same pattern.

🐛 Proposed fix — move `style` after the spread
       <Input
         {value}
         readonly
-        style="background-color: {value}; color: {textColor};"
         {...props}
+        style="background-color: {value}; color: {textColor};"
         class={cn('w-auto min-w-26', props.class as string)}
       />
📝 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
style="background-color: {value}; color: {textColor};"
{...props}
<Input
{value}
readonly
{...props}
style="background-color: {value}; color: {textColor};"
class={cn('w-auto min-w-26', props.class as string)}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/components/ui/color-picker/color-picker.svelte` around lines 40 - 41,
The inline style for the color indicator is placed before the props spread so
any style in {...props} can overwrite it; move the style attribute
("background-color: {value}; color: {textColor};") to after the {...props}
spread and keep the class attribute after the spread as well so explicit color
styling always wins; update the element in the ColorPicker component (the
element using style, {...props}, and class) to place {...props} first, then
style, then class.

Comment on lines +20 to +22
.describe(
'The programming language for syntax highlighting. Determines which keywords, strings, and comments are colored. Use "text" for plain text with no highlighting.'
),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Description for "text" language is misleading.

The description claims "text" provides "plain text with no highlighting," but highlightLine() unconditionally applies string, comment, and number regex patterns to every language — only the keyword list is empty for "text". This means quoted strings, // / # / /* */ patterns, and numeric literals will still be colourised even in "text" mode.

Since these descriptions are specifically intended to guide AI tooling, an inaccurate claim here can cause incorrect behaviour.

📝 Suggested description fix
-        'The programming language for syntax highlighting. Determines which keywords, strings, and comments are colored. Use "text" for plain text with no highlighting.'
+        'The programming language for syntax highlighting. Determines which keywords are colored. Use "text" to suppress keyword highlighting; strings, comments, and numbers are still highlighted in all modes.'
📝 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
.describe(
'The programming language for syntax highlighting. Determines which keywords, strings, and comments are colored. Use "text" for plain text with no highlighting.'
),
.describe(
'The programming language for syntax highlighting. Determines which keywords are colored. Use "text" to suppress keyword highlighting; strings, comments, and numbers are still highlighted in all modes.'
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/layers/components/CodeLayer.svelte` around lines 20 - 22, The
description claiming "text" yields plain text with no highlighting is inaccurate
because highlightLine() still applies string, comment, and number regexes even
when the keyword list is empty; update the .describe(...) text in
CodeLayer.svelte (the language option description that mentions "text") to state
that "text" disables keyword-based highlighting but still applies string,
comment, and number patterns (so quoted strings, //, #, /* */ comments and
numeric literals may still be colored), and reference highlightLine() behavior
in the comment if helpful for future maintainers.

Comment on lines +155 to +176
fill: z
.string()
.default('none')
.describe(
'The fill color inside the icon shapes in hexadecimal format. Use "none" for outline-only icons, or a color hex (e.g., #ff0000) to fill the icon interior. Smoothly animatable between colors.'
)
.register(fieldRegistry, {
interpolationFamily: 'continuous',
widget: 'color',
label: 'Fill'
}),
backgroundColor: z
.string()
.default('transparent')
.describe(
'The background color behind the icon in hexadecimal format. Use "transparent" for no background, or a color (e.g., #000000) to add a background shape. Smoothly animatable.'
)
.register(fieldRegistry, {
interpolationFamily: 'continuous',
widget: 'color',
label: 'Background'
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*interpolat*" | head -20

Repository: epavanello/devmotion

Length of output: 228


🏁 Script executed:

rg -l "interpolationFamily|interpolate" --type=ts --type=js --type=svelte | head -15

Repository: epavanello/devmotion

Length of output: 94


🏁 Script executed:

rg -nP "parseColor|interpolateColor|isColor|interpolation" --type=ts -C3 | head -100

Repository: epavanello/devmotion

Length of output: 5807


🏁 Script executed:

cat -n src/lib/engine/interpolation.ts

Repository: epavanello/devmotion

Length of output: 17931


🏁 Script executed:

grep -n "none\|transparent" src/lib/engine/interpolation.ts | head -20

Repository: epavanello/devmotion

Length of output: 46


🏁 Script executed:

rg -n "color|Color" src/lib/engine/interpolation.ts -C3

Repository: epavanello/devmotion

Length of output: 2199


🏁 Script executed:

# Verify colord behavior with 'none' and 'transparent'
cat > /tmp/test_colord.js << 'EOF'
// Quick check - colord library behavior
const colord = require('colord');

console.log("Testing 'none':");
const noneColor = colord('none');
console.log("  isValid():", noneColor.isValid());

console.log("Testing 'transparent':");
const transparentColor = colord('transparent');
console.log("  isValid():", transparentColor.isValid());

console.log("Testing '#ff0000':");
const hexColor = colord('#ff0000');
console.log("  isValid():", hexColor.isValid());
EOF
cd /tmp && npm init -y > /dev/null 2>&1 && npm install colord > /dev/null 2>&1 && node test_colord.js 2>/dev/null || echo "Library test setup skipped"

Repository: epavanello/devmotion

Length of output: 106


🏁 Script executed:

# Check IconLayer.svelte to see if there's any special handling for 'none' or 'transparent'
grep -n "none\|transparent" src/lib/layers/components/IconLayer.svelte | head -20

Repository: epavanello/devmotion

Length of output: 514


🏁 Script executed:

# Check if there's any animation being performed in tests or examples with fill='none'
rg -n "fill.*none\|animate.*fill" --type=ts --type=js src/lib/layers/components/IconLayer.svelte -C2

Repository: epavanello/devmotion

Length of output: 46


🏁 Script executed:

# Check if there's any error handling for interpolation failures in the animation system
rg -n "try\|catch" src/lib/engine/interpolation.ts -C2

Repository: epavanello/devmotion

Length of output: 46


fill: 'none' with interpolationFamily: 'continuous' will cause runtime errors during animation.

The fill field defaults to 'none', which is not a valid CSS color that the colord library recognizes. When animating from fill='none' to a hex color, interpolateContinuous() in src/lib/engine/interpolation.ts will throw an error: "continuous interpolation requires numeric or color values" (line 70).

In contrast, backgroundColor='transparent' works fine—'transparent' is a valid CSS color keyword that colord handles correctly.

Change fill to use interpolationFamily: 'discrete' or ensure the default is a valid hex color (e.g., #000000) so continuous interpolation doesn't fail.

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

In `@src/lib/layers/components/IconLayer.svelte` around lines 155 - 176, The fill
field in IconLayer.svelte currently defaults to 'none' but registers with
interpolationFamily: 'continuous', which causes interpolateContinuous() to throw
for non-color keywords; update the registration for the fill field (the fill
z.string().default('none') .register(...)) to either set interpolationFamily:
'discrete' or change the default to a valid color (e.g., '#000000') so
continuous interpolation only receives valid color values; ensure you modify the
fill registration (not backgroundColor) in the same block that references
fieldRegistry and label 'Fill'.

@epavanello epavanello merged commit 60dc5f2 into main Feb 17, 2026
1 check was pending
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

🧹 Nitpick comments (2)
src/lib/layers/components/ShapeLayer.svelte (2)

38-40: stroke schema accepts any string — consider adding hex color validation

z.string() is unconstrained. The description states "hexadecimal format" and the widget: 'color' enforces this in the UI, but the schema itself will accept any arbitrary string, potentially reaching getStyleProperties/CSS with an invalid value.

🔧 Proposed fix – add regex validation
-    stroke: z
-      .string()
-      .default('#000000')
+    stroke: z
+      .string()
+      .regex(/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, 'Must be a valid hex color')
+      .default('#000000')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/layers/components/ShapeLayer.svelte` around lines 38 - 40, The stroke
property currently uses an unconstrained z.string() in the ShapeLayer schema;
update the schema to validate hex color format by replacing z.string() with
z.string().regex(/^(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$)/) (or equivalent) so
only valid hex colors pass through to getStyleProperties/CSS; locate the stroke
declaration in ShapeLayer.svelte and apply the regex validation while keeping
the .default('#000000') fallback.

156-158: isRound excludes polygon, which also uses radius × 2 square sizing

The polygon case produces a square element identical in aspect ratio to circle (radius * 2 × radius * 2). Not including polygon in isRound means radial-gradient backgrounds on polygons won't receive radialSize: 'closest-side', so they'll default to ellipse farthest-corner sizing — visually inconsistent with how radial gradients render on circle. If this is intentional (letting the gradient extend beyond the polygon's clip-path), a comment would clarify the intent.

🔧 Proposed fix — include polygon in isRound (if desired)
-    const isRound = shapeType === 'circle' || shapeType === 'ellipse';
+    const isRound = shapeType === 'circle' || shapeType === 'ellipse' || shapeType === 'polygon';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/layers/components/ShapeLayer.svelte` around lines 156 - 158, The
isRound check currently treats only 'circle' and 'ellipse' as round, causing
polygon shapes to miss radialSize: 'closest-side' and render radial gradients
with ellipse farthest-corner sizing; update the isRound expression used when
building base (the const isRound = ... used with getStyleProperties in base) to
also include 'polygon' (or add a clear comment if leaving behavior intentional)
so that getStyleProperties(background, isRound ? { radialSize: 'closest-side' }
: undefined) applies closest-side to polygons as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/schemas/background.ts`:
- Around line 142-145: Add a single shared TypeScript type alias for the
radial-size union by inferring it from the existing RadialGradientSchema (e.g.
create a type like RadialSize = z.infer<typeof RadialGradientSchema>['size'] in
the Types section) and then replace the repeated inline union in the three
function signatures (including backgroundValueToCSS and the two other functions
that currently declare the same options type) with this new RadialSize-based
options type so all references derive from RadialGradientSchema.

---

Nitpick comments:
In `@src/lib/layers/components/ShapeLayer.svelte`:
- Around line 38-40: The stroke property currently uses an unconstrained
z.string() in the ShapeLayer schema; update the schema to validate hex color
format by replacing z.string() with
z.string().regex(/^(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$)/) (or equivalent) so
only valid hex colors pass through to getStyleProperties/CSS; locate the stroke
declaration in ShapeLayer.svelte and apply the regex validation while keeping
the .default('#000000') fallback.
- Around line 156-158: The isRound check currently treats only 'circle' and
'ellipse' as round, causing polygon shapes to miss radialSize: 'closest-side'
and render radial gradients with ellipse farthest-corner sizing; update the
isRound expression used when building base (the const isRound = ... used with
getStyleProperties in base) to also include 'polygon' (or add a clear comment if
leaving behavior intentional) so that getStyleProperties(background, isRound ? {
radialSize: 'closest-side' } : undefined) applies closest-side to polygons as
well.

Comment on lines +142 to +145
export function backgroundValueToCSS(
value: BackgroundValue,
options?: { radialSize?: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner' }
): string {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract the duplicated options type using z.infer from the existing schema

The inline literal union 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner' is repeated verbatim in all three changed signatures (lines 144, 176, 198). The identical four values are already the source of truth in RadialGradientSchema.size (line 71). This should be derived once via z.infer and shared.

♻️ Proposed fix — extract a shared type from the schema

Add a named type in the Types section (after line 123):

 export type GradientBackground = LinearGradient | RadialGradient | ConicGradient;
 export type BackgroundValue = SolidBackground | GradientBackground;
+
+/** Render-time overrides for background CSS generation */
+export type BackgroundRenderOptions = {
+  radialSize?: z.infer<typeof RadialGradientSchema>['size'];
+};

Then replace all three inline option types with the new alias:

-export function backgroundValueToCSS(
-  value: BackgroundValue,
-  options?: { radialSize?: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner' }
-): string {
+export function backgroundValueToCSS(value: BackgroundValue, options?: BackgroundRenderOptions): string {
-export function getStyleProperties(
-  value: BackgroundValue,
-  options?: { radialSize?: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner' }
-) {
+export function getStyleProperties(value: BackgroundValue, options?: BackgroundRenderOptions) {
-export function getBackgroundImage(
-  value?: BackgroundValue,
-  options?: { radialSize?: 'closest-side' | 'closest-corner' | 'farthest-side' | 'farthest-corner' }
-) {
+export function getBackgroundImage(value?: BackgroundValue, options?: BackgroundRenderOptions) {

As per coding guidelines, src/lib/schemas/**/*.ts requires inferring TypeScript types via z.infer instead of duplicating type definitions.

Also applies to: 174-177, 196-199

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

In `@src/lib/schemas/background.ts` around lines 142 - 145, Add a single shared
TypeScript type alias for the radial-size union by inferring it from the
existing RadialGradientSchema (e.g. create a type like RadialSize =
z.infer<typeof RadialGradientSchema>['size'] in the Types section) and then
replace the repeated inline union in the three function signatures (including
backgroundValueToCSS and the two other functions that currently declare the same
options type) with this new RadialSize-based options type so all references
derive from RadialGradientSchema.

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