Marker color
diff --git a/configurator/src/components/panels/MotionPanel.svelte b/configurator/src/components/panels/MotionPanel.svelte
index ccdffd5a..b24297da 100644
--- a/configurator/src/components/panels/MotionPanel.svelte
+++ b/configurator/src/components/panels/MotionPanel.svelte
@@ -19,54 +19,59 @@
];
const EASINGS = [
- { label: "Linear", token: "--sf-ease-linear", value: "linear", preview: "M0,40 L40,0" },
- { label: "Ease out", token: "--sf-ease-out", value: "cubic-bezier(0.25, 0, 0.15, 1)", preview: "M0,40 C8,40 28,0 40,0" },
- { label: "Ease in", token: "--sf-ease-in", value: "cubic-bezier(0.5, 0, 0.75, 0.25)", preview: "M0,40 C12,40 32,32 40,0" },
- { label: "Ease in-out",token: "--sf-ease-in-out", value: "cubic-bezier(0.4, 0, 0.2, 1)", preview: "M0,40 C8,40 32,0 40,0" },
- { label: "Spring", token: "--sf-ease-spring", value: "linear(0, 0.5, 1.1, 0.95, 1.02, 1)", preview: "M0,40 C10,20 20,-4 28,-2 S38,2 40,0" },
+ { label: "Linear", token: "--sf-ease-linear", value: "linear", preview: "M0,40 L40,0" },
+ { label: "Ease out", token: "--sf-ease-out", value: "cubic-bezier(0.25, 0, 0.15, 1)", preview: "M0,40 C8,40 28,0 40,0" },
+ { label: "Ease in", token: "--sf-ease-in", value: "cubic-bezier(0.5, 0, 0.75, 0.25)", preview: "M0,40 C12,40 32,32 40,0" },
+ { label: "Ease in-out",token: "--sf-ease-in-out", value: "cubic-bezier(0.4, 0, 0.2, 1)", preview: "M0,40 C8,40 32,0 40,0" },
+ { label: "Spring", token: "--sf-ease-spring", value: "linear(0, 0.5, 1.1, 0.95, 1.02, 1)", preview: "M0,40 C10,20 20,-4 28,-2 S38,2 40,0" },
{ label: "Bounce", token: "--sf-ease-bounce", value: "linear(0, 0.35 18%, 1 32%, 0.86 42%, 1.02 56%, 0.98 72%, 1)", preview: "M0,40 L12,16 L20,0 L26,6 L32,-1 L36,1 L40,0" },
- { label: "Elastic", token: "--sf-ease-elastic", value: "linear(0, 0.3, 1.2, 0.9, 1.05, 1)", preview: "M0,40 C8,40 16,-10 24,-2 S36,2 40,0" },
- { label: "Overshoot", token: "--sf-ease-overshoot", value: "cubic-bezier(0.34, 1.56, 0.64, 1)", preview: "M0,40 C8,40 20,-10 40,0" },
+ { label: "Elastic", token: "--sf-ease-elastic", value: "linear(0, 0.3, 1.2, 0.9, 1.05, 1)", preview: "M0,40 C8,40 16,-10 24,-2 S36,2 40,0" },
+ { label: "Overshoot", token: "--sf-ease-overshoot", value: "linear(0, 0.6 30%, 1.08 55%, 0.98 75%, 1)", preview: "M0,40 C8,40 20,-10 40,0" },
];
- // animation-delay-N base is 75ms * N; stagger base is 75ms
const STAGGER_TOKENS = ["--sf-animation-delay-1","--sf-animation-delay-2","--sf-animation-delay-3","--sf-animation-delay-4","--sf-animation-delay-5"];
const knobs = KNOBS_BY_DOMAIN["motion"] ?? [];
- let scale = $derived(parseFloat(overrides["--sf-motion-scale"] ?? "1"));
+ let scale = $derived((() => { const v = parseFloat(overrides["--sf-motion-scale"] ?? "1"); return isFinite(v) ? v : 1; })());
let motionDisabled = $derived(overrides["--sf-motion-scale"] === "0");
- let themeTransition = $derived(parseFloat(overrides["--sf-theme-transition-duration"]?.replace("ms","") ?? String(300 * scale)));
+ let themeTransition = $derived((() => { const v = parseFloat(overrides["--sf-theme-transition-duration"]?.replace("ms","") ?? String(300 * scale)); return isFinite(v) ? v : Math.round(300 * scale); })());
let staggerBase = $derived(() => {
const raw = overrides[STAGGER_TOKENS[0]];
if (raw) return parseFloat(raw.replace("ms",""));
return 75 * scale;
});
- // Animation demo state
+ // Animation demo — user picks which easing + duration to feel.
let animating = $state(false);
let animOffsetX = $state(0);
+ let demoEase = $state("--sf-ease-out");
+ let demoDuration = $state("--sf-duration-normal");
- let showGlobalScale = $state(false);
- let showThemeTransition = $state(false);
- let showDurationOverrides = $state(false);
- let showStaggerBase = $state(false);
- let showEasingCurves = $state(false);
- let showAnimationDemo = $state(false);
- let showDurationPreview = $state(false);
- let showScrollTimeline = $state(false);
+ let showDurations = $state(false);
+ let showEasing = $state(false);
+ let showStagger = $state(false);
+ let showAdvanced = $state(false);
function getDuration(token: string, base: number): number {
const raw = overrides[token];
- if (raw) return parseFloat(raw);
+ if (raw) {
+ const parsed = parseFloat(raw);
+ if (isFinite(parsed)) return parsed;
+ }
return Math.round(base * scale);
}
+ function easeValue(token: string): string {
+ return overrides[token] ?? EASINGS.find((e) => e.token === token)?.value ?? "ease";
+ }
+
function playDemo() {
if (animating) return;
+ const base = DURATIONS.find((d) => d.token === demoDuration)?.base ?? 250;
+ const dur = getDuration(demoDuration, base);
animating = true;
animOffsetX = 180;
- const dur = getDuration("--sf-duration-normal", 250);
setTimeout(() => {
animOffsetX = 0;
setTimeout(() => { animating = false; }, dur);
@@ -75,9 +80,7 @@
function setStaggerBase(baseMs: number) {
const patch: Record
= {};
- for (let i = 1; i <= 5; i++) {
- patch[`--sf-animation-delay-${i}`] = `${Math.round(baseMs * i)}ms`;
- }
+ for (let i = 1; i <= 5; i++) patch[`--sf-animation-delay-${i}`] = `${Math.round(baseMs * i)}ms`;
onBulkChange(patch);
}
@@ -114,71 +117,66 @@
{#if motionDisabled}
-
Motion is disabled — duration sliders have no effect while scale is 0.
+
Motion is disabled — duration & easing have no effect while scale is 0.
{/if}
-
-
-
{ showGlobalScale = !showGlobalScale; }}
- aria-expanded={showGlobalScale}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Global scale
- {showGlobalScale ? "▲" : "▼"}
-
- {#if showGlobalScale}
-
- {#each knobs as k (k.name)}
-
val === null ? onReset(name) : onSet(name, val)}
- />
- {/each}
-
- {/if}
-
-
-
-
-
+
- { showThemeTransition = !showThemeTransition; }}
- aria-expanded={showThemeTransition}
- class="w-full flex items-center justify-between cursor-pointer"
- >
- Theme transition
- {showThemeTransition ? "▲" : "▼"}
-
- {#if showThemeTransition}
- onSet("--sf-theme-transition-duration", `${v}ms`)}
- onReset={() => onReset("--sf-theme-transition-duration")}
- />
- {/if}
+ Live preview
+
+
+
d.token === demoDuration)?.base ?? 250)}ms ${easeValue(demoEase)}`}
+ >
+
+
+ { demoEase = (e.target as HTMLSelectElement).value; }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-[10px] text-slate-200 focus:outline-none focus:border-indigo-500 cursor-pointer"
+ >
+ {#each EASINGS as e (e.token)}
+ {e.label}
+ {/each}
+
+ { demoDuration = (e.target as HTMLSelectElement).value; }}
+ class="flex-1 min-w-0 bg-white/5 border border-white/10 rounded-lg px-2 py-1.5 text-[10px] text-slate-200 focus:outline-none focus:border-indigo-500 cursor-pointer"
+ >
+ {#each DURATIONS as d (d.token)}
+ {d.label}
+ {/each}
+
+ {animating ? "…" : "▶ Play"}
+
+
-
-
+
+
{ showDurationOverrides = !showDurationOverrides; }}
- aria-expanded={showDurationOverrides}
+ onclick={() => { showDurations = !showDurations; }}
+ aria-expanded={showDurations}
class="w-full flex items-center justify-between cursor-pointer"
>
- Duration overrides
- {showDurationOverrides ? "▲" : "▼"}
+ Durations
+ {showDurations ? "▲" : "▼"}
- {#if showDurationOverrides}
+ {#if showDurations}
- Set absolute ms values to override the fluid scale calculation.
+ Named duration tokens. Drag to set an absolute ms value, overriding the global scale.
{#if motionDisabled} (No effect while motion is disabled.) {/if}
{#each DURATIONS as d (d.token)}
@@ -195,22 +193,94 @@
onReset={() => onReset(d.token)}
/>
{/each}
+
+
+ {#each DURATIONS as d (d.label)}
+ {@const actual = getDuration(d.token, d.base)}
+
+
{d.label}
+
+
{actual}ms
+
+ {/each}
+
+ {/if}
+
+
+
+
+
+
+ { showEasing = !showEasing; }}
+ aria-expanded={showEasing}
+ class="w-full flex items-center justify-between cursor-pointer"
+ >
+ Easing curves
+ {showEasing ? "▲" : "▼"}
+
+ {#if showEasing}
+
+ Edit the named easing tokens. Accepts any CSS timing function —
+ cubic-bezier(…) or
+ linear(…) . Use “Live preview” above to feel a curve.
+
+
+ {#each EASINGS as e (e.token)}
+ {@const isOverridden = e.token in overrides}
+
+
+
+
+
+
+
+
{e.label}
+
+ { demoEase = e.token; playDemo(); }}
+ class="text-[8px] text-slate-500 hover:text-indigo-400 cursor-pointer"
+ >preview
+ {#if isOverridden}
+ onReset(e.token)} class="text-[8px] text-slate-500 hover:text-rose-400 cursor-pointer">reset
+ {/if}
+
+
+
{
+ const v = (ev.target as HTMLInputElement).value.trim();
+ v ? onSet(e.token, v) : onReset(e.token);
+ }}
+ class="w-full bg-white/5 border border-white/10 rounded px-1.5 py-1 text-[9px] font-mono text-slate-300 placeholder:text-slate-600 focus:outline-none focus:border-indigo-500"
+ />
+
+
+
+ {/each}
+
{/if}
-
+
{ showStaggerBase = !showStaggerBase; }}
- aria-expanded={showStaggerBase}
+ onclick={() => { showStagger = !showStagger; }}
+ aria-expanded={showStagger}
class="w-full flex items-center justify-between cursor-pointer"
>
- Stagger base
- {showStaggerBase ? "▲" : "▼"}
+ Stagger
+ {showStagger ? "▲" : "▼"}
- {#if showStaggerBase}
+ {#if showStagger}
One slider sets all five stagger delays (delay-1 through delay-5 are multiples of this base).
@@ -229,7 +299,6 @@
onReset={resetStagger}
/>
-