From 4272e157b8eb046104620a5f724d83d3fd808725 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:33:00 -0300 Subject: [PATCH 01/60] Align preview toolbar chrome --- renderer/components/appearance-toggle.tsx | 4 ++-- renderer/components/preview-stage.tsx | 6 +++--- renderer/components/treatment-stage.tsx | 6 +++--- renderer/shims/components.tsx | 4 +++- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/renderer/components/appearance-toggle.tsx b/renderer/components/appearance-toggle.tsx index b65dede..afe92be 100644 --- a/renderer/components/appearance-toggle.tsx +++ b/renderer/components/appearance-toggle.tsx @@ -33,12 +33,12 @@ export function AppearanceToggle() { ); } diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index 4a71f43..ca15e83 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -33,10 +33,10 @@ export function PreviewStage({ effect, params, replayToken, onReplay, onExport } {effect.name} + + + - - -
{/* Give the preview a definite, centered width so panel-style effects diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index 48fdce8..61714cc 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -182,10 +182,10 @@ export function TreatmentStage({ {treatment.name} + + + - - -
{ variant?: "glass" | "accent" | "muted" | "filled" | "default"; size?: "small" | "medium" | "large"; + iconOnly?: boolean; children?: React.ReactNode; } -export function Button({ variant = "default", size = "medium", className, children, ...props }: ButtonProps) { +export function Button({ variant = "default", size = "medium", iconOnly = false, className, children, ...props }: ButtonProps) { const variantClasses = { glass: "bg-white/10 hover:bg-white/20 dark:bg-white/5 dark:hover:bg-white/10 border border-white/20 text-foreground backdrop-blur-sm", @@ -82,6 +83,7 @@ export function Button({ variant = "default", size = "medium", className, childr "disabled:pointer-events-none disabled:opacity-50 cursor-default", variantClasses[variant], sizeClasses[size], + iconOnly && (size === "small" ? "w-6 px-0" : size === "medium" ? "w-8 px-0" : "w-10 px-0"), className, )} {...props} From 43a53dad110128265362f5eeb7bf91fe0a238120 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:33:55 -0300 Subject: [PATCH 02/60] Tighten inspector top spacing --- renderer/components/control-panel.tsx | 4 ++-- renderer/components/treatment-panel.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/renderer/components/control-panel.tsx b/renderer/components/control-panel.tsx index 1159a65..f1bd87c 100644 --- a/renderer/components/control-panel.tsx +++ b/renderer/components/control-panel.tsx @@ -13,7 +13,7 @@ interface ControlPanelProps { export function ControlPanel({ effect, params, onChange, onReset }: ControlPanelProps) { return (
-
+

{effect.name}

{effect.description}

@@ -31,7 +31,7 @@ export function ControlPanel({ effect, params, onChange, onReset }: ControlPanel
-
+
{effect.controls.map((control) => ( ))} diff --git a/renderer/components/treatment-panel.tsx b/renderer/components/treatment-panel.tsx index 2abd858..7d10433 100644 --- a/renderer/components/treatment-panel.tsx +++ b/renderer/components/treatment-panel.tsx @@ -173,7 +173,7 @@ export function TreatmentPanel({ const supportsMotion = Boolean(treatment.animate || treatment.animated); return (
-
+

{treatment.name}

{treatment.description}

@@ -191,7 +191,7 @@ export function TreatmentPanel({
-
+
{treatment.needsSource && ( <> From 96aebbc5d32f2e38c3af3c21816e25412dd87687 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:36:24 -0300 Subject: [PATCH 03/60] Fix web theme toggle and sidebar scrollbar --- index.html | 3 ++- renderer/shims/browser-glazeapi.ts | 37 +++++++++++++++++++++++++----- renderer/shims/components.tsx | 2 +- renderer/shims/hooks.ts | 9 ++++---- renderer/web-styles.css | 19 +++++++++++++++ 5 files changed, 58 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index e1abf7f..f8a1980 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,8 @@ Motion Studio diff --git a/renderer/shims/browser-glazeapi.ts b/renderer/shims/browser-glazeapi.ts index 8c6f5a1..2b9e16a 100644 --- a/renderer/shims/browser-glazeapi.ts +++ b/renderer/shims/browser-glazeapi.ts @@ -3,7 +3,29 @@ * checks `window.glazeAPI?.glaze?.ipc?.disconnect()` doesn't throw. */ +type ThemeSource = "system" | "light" | "dark"; + +const THEME_STORAGE_KEY = "motion-studio-theme"; + +function storedThemeSource(): ThemeSource { + const stored = window.localStorage.getItem(THEME_STORAGE_KEY); + return stored === "light" || stored === "dark" ? stored : "system"; +} + +function shouldUseDarkColors(source: ThemeSource): boolean { + return source === "dark" || (source === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); +} + +function applyThemeSource(source: ThemeSource) { + document.documentElement.classList.toggle("dark", shouldUseDarkColors(source)); + if (source === "system") window.localStorage.removeItem(THEME_STORAGE_KEY); + else window.localStorage.setItem(THEME_STORAGE_KEY, source); +} + if (typeof window !== "undefined" && !("glazeAPI" in window)) { + let themeSource = storedThemeSource(); + applyThemeSource(themeSource); + (window as Record).glazeAPI = { glaze: { ipc: { @@ -27,13 +49,16 @@ if (typeof window !== "undefined" && !("glazeAPI" in window)) { nativeTheme: { getInfo: () => Promise.resolve({ - shouldUseDarkColors: window.matchMedia("(prefers-color-scheme: dark)").matches, - themeSource: "system" as const, + shouldUseDarkColors: shouldUseDarkColors(themeSource), + themeSource, }), - setThemeSource: () => Promise.resolve(true), - getShouldUseDarkColors: () => - Promise.resolve(window.matchMedia("(prefers-color-scheme: dark)").matches), - getThemeSource: () => Promise.resolve("system" as const), + setThemeSource: (source: ThemeSource) => { + themeSource = source; + applyThemeSource(source); + return Promise.resolve(true); + }, + getShouldUseDarkColors: () => Promise.resolve(shouldUseDarkColors(themeSource)), + getThemeSource: () => Promise.resolve(themeSource), }, shell: { beep: () => {}, beepAsync: () => Promise.resolve() }, systemPreferences: { diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 05754fb..fa3306e 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -131,7 +131,7 @@ export function Sidebar({ children, className }: { children?: React.ReactNode; c export function SidebarList({ children, className }: { children?: React.ReactNode; className?: string }) { return ( -
+
{children}
); diff --git a/renderer/shims/hooks.ts b/renderer/shims/hooks.ts index 22485cc..a255b4e 100644 --- a/renderer/shims/hooks.ts +++ b/renderer/shims/hooks.ts @@ -6,11 +6,12 @@ import * as React from "react"; export function useTheme() { React.useEffect(() => { const mq = window.matchMedia("(prefers-color-scheme: dark)"); - const apply = (dark: boolean) => { - document.documentElement.classList.toggle("dark", dark); + const applySystemTheme = async () => { + const source = await window.glazeAPI.nativeTheme.getThemeSource(); + if (source === "system") document.documentElement.classList.toggle("dark", mq.matches); }; - apply(mq.matches); - const handler = (e: MediaQueryListEvent) => apply(e.matches); + void applySystemTheme(); + const handler = () => void applySystemTheme(); mq.addEventListener("change", handler); return () => mq.removeEventListener("change", handler); }, []); diff --git a/renderer/web-styles.css b/renderer/web-styles.css index a719153..6730053 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -84,6 +84,25 @@ body { app-region: drag; } +.sidebar-scroll { + scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; +} + +.sidebar-scroll::-webkit-scrollbar { + width: 10px; +} + +.sidebar-scroll::-webkit-scrollbar-track { + background: transparent; +} + +.sidebar-scroll::-webkit-scrollbar-thumb { + background: color-mix(in oklch, var(--color-foreground) 32%, transparent); + background-clip: content-box; + border: 3px solid transparent; + border-radius: 999px; +} + /* Button press feedback — scale-down on active, matching Emil's 0.97 rule */ button:active:not(:disabled) { transform: scale(0.97); From 8f3c17b58e872828e949d40c058182577186a4f6 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:39:22 -0300 Subject: [PATCH 04/60] Use neutral border for download action --- renderer/components/export-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/components/export-dialog.tsx b/renderer/components/export-dialog.tsx index 7bbce31..cf67dd4 100644 --- a/renderer/components/export-dialog.tsx +++ b/renderer/components/export-dialog.tsx @@ -89,7 +89,7 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo - From fd0cbef89f33c8d335076b49ae10dcfc78ecbf40 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:40:59 -0300 Subject: [PATCH 05/60] Add code wrap control to export dialog --- renderer/components/export-dialog.tsx | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/renderer/components/export-dialog.tsx b/renderer/components/export-dialog.tsx index cf67dd4..0a7eab8 100644 --- a/renderer/components/export-dialog.tsx +++ b/renderer/components/export-dialog.tsx @@ -11,7 +11,7 @@ import { Tabs, TabsTrigger, } from "@glaze/core/components"; -import { Copy, Check, Download } from "lucide-react"; +import { Copy, Check, Download, WrapText } from "lucide-react"; import type { Effect, EffectParams } from "./effects/types"; import { copyToClipboard, downloadText } from "../lib/export-utils"; @@ -36,6 +36,7 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo const hasCss = typeof effect.exports.css === "function"; const [format, setFormat] = React.useState("react"); const [copied, setCopied] = React.useState(false); + const [wrapText, setWrapText] = React.useState(false); // Fall back to React when switching to an effect that lacks the active format. React.useEffect(() => { @@ -84,9 +85,27 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo {hasCss && {FORMAT_META.css.label}} -
-            {content}
-          
+
+ +
+              {content}
+            
+

From 6a7783fecf8a4753846f2502da668e55232ecae0 Mon Sep 17 00:00:00 2001
From: vyctorbrzezowski 
Date: Sat, 27 Jun 2026 16:51:40 -0300
Subject: [PATCH 07/60] Replay preview when resetting controls

---
 renderer/main/home-view.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx
index f06708e..6c6b3d4 100644
--- a/renderer/main/home-view.tsx
+++ b/renderer/main/home-view.tsx
@@ -64,6 +64,7 @@ export function HomeView() {
 
   const handleReset = () => {
     setParamsMap((prev) => ({ ...prev, [activeId]: defaultParams(controls) }));
+    setReplayToken((token) => token + 1);
   };
 
   const handlePickSample = (sample: SampleImage) => {

From b6b21b287599a4b0c794e0f5ee0e882358e68e2f Mon Sep 17 00:00:00 2001
From: vyctorbrzezowski 
Date: Sat, 27 Jun 2026 16:51:40 -0300
Subject: [PATCH 08/60] Restyle inspector sliders

---
 renderer/components/control-row.tsx     | 68 ++++++++++++++++++-------
 renderer/components/treatment-panel.tsx | 27 ++++------
 renderer/shims/components.tsx           |  8 ++-
 renderer/web-styles.css                 | 31 +++++++++++
 4 files changed, 97 insertions(+), 37 deletions(-)

diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx
index 8ccc00f..06d34b6 100644
--- a/renderer/components/control-row.tsx
+++ b/renderer/components/control-row.tsx
@@ -12,6 +12,47 @@ import {
 import { CurveEditor } from "./curve-editor";
 import type { ControlDef, EffectParams, ParamValue } from "./effects/types";
 
+export function LabeledSlider({
+  label,
+  value,
+  unit = "",
+  min,
+  max,
+  step,
+  onValueChange,
+}: {
+  label: string;
+  value: number;
+  unit?: string;
+  min: number;
+  max: number;
+  step: number;
+  onValueChange: (value: number) => void;
+}) {
+  return (
+    
+
+ onValueChange(nextValue)} + /> + + {label} + +
+ + {value} + {unit} + +
+ ); +} + function ColorInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { return (
-
-
- Duration - {anim.duration}s -
- onAnimChange({ duration: v })} - /> -
+ onAnimChange({ duration })} + />
Loop - + ); } diff --git a/renderer/web-styles.css b/renderer/web-styles.css index 6730053..c5265ad 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -103,6 +103,37 @@ body { border-radius: 999px; } +.inspector-slider { + height: 36px; + overflow: hidden; + border: 1px solid var(--color-separator); + border-radius: 6px; + background: color-mix(in oklch, var(--color-foreground) 5%, transparent); +} + +.inspector-slider > span:first-child { + position: absolute; + inset: 0; + height: 100%; + border-radius: 0; + background: transparent; +} + +.inspector-slider > span:first-child > span { + height: 100%; + border-radius: 0; + background: color-mix(in oklch, var(--color-foreground) 10%, transparent); +} + +.inspector-slider > span:last-child > [role="slider"] { + width: 2px; + height: 24px; + border: 0; + border-radius: 999px; + background: color-mix(in oklch, var(--color-foreground) 22%, transparent); + box-shadow: none; +} + /* Button press feedback — scale-down on active, matching Emil's 0.97 rule */ button:active:not(:disabled) { transform: scale(0.97); From b787515ad2e63076b9cf4ca8c150df6d946f2a78 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:52:12 -0300 Subject: [PATCH 09/60] Space sidebar section headings --- renderer/shims/components.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index b717b18..10969ad 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -145,7 +145,7 @@ interface SidebarListGroupProps { export function SidebarListGroup({ title, children, className }: SidebarListGroupProps) { return ( -
+
{title}
From 152c2361d242e7eeb553fb5acbff803fdd7ddd0b Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:52:36 -0300 Subject: [PATCH 10/60] Offset export wrap control from scrollbar --- renderer/components/export-dialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renderer/components/export-dialog.tsx b/renderer/components/export-dialog.tsx index 5f5b6f8..0780731 100644 --- a/renderer/components/export-dialog.tsx +++ b/renderer/components/export-dialog.tsx @@ -90,7 +90,7 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo variant="default" size="small" iconOnly - className="absolute top-2 right-2 z-10 backdrop-blur-sm" + className="absolute top-3 right-7 z-10 backdrop-blur-sm" aria-label={wrapText ? "Disable text wrapping" : "Wrap text"} aria-pressed={wrapText} title={wrapText ? "Disable text wrapping" : "Wrap text"} @@ -99,7 +99,7 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo

From 61e89bc96bdd871512d89a722f1ec939a15a8853 Mon Sep 17 00:00:00 2001
From: vyctorbrzezowski 
Date: Sat, 27 Jun 2026 16:53:08 -0300
Subject: [PATCH 11/60] Show wrap control only for horizontal overflow

---
 renderer/components/export-dialog.tsx | 61 +++++++++++++++++++++------
 1 file changed, 49 insertions(+), 12 deletions(-)

diff --git a/renderer/components/export-dialog.tsx b/renderer/components/export-dialog.tsx
index 0780731..177544a 100644
--- a/renderer/components/export-dialog.tsx
+++ b/renderer/components/export-dialog.tsx
@@ -37,6 +37,9 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo
   const [format, setFormat] = React.useState("react");
   const [copied, setCopied] = React.useState(false);
   const [wrapText, setWrapText] = React.useState(false);
+  const [canWrap, setCanWrap] = React.useState(false);
+  const preRef = React.useRef(null);
+  const measureRef = React.useRef(null);
 
   // Fall back to React when switching to an effect that lacks the active format.
   React.useEffect(() => {
@@ -57,6 +60,30 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo
     return effect.exports.react(params);
   }, [effect, params, format]);
 
+  React.useLayoutEffect(() => {
+    const updateWrapAvailability = () => {
+      const pre = preRef.current;
+      const measure = measureRef.current;
+      if (!pre || !measure) return;
+
+      const nextCanWrap = measure.scrollWidth > pre.clientWidth + 1;
+      setCanWrap(nextCanWrap);
+      if (!nextCanWrap) setWrapText(false);
+    };
+
+    updateWrapAvailability();
+
+    const observer = new ResizeObserver(updateWrapAvailability);
+    if (preRef.current) observer.observe(preRef.current);
+    if (measureRef.current) observer.observe(measureRef.current);
+    window.addEventListener("resize", updateWrapAvailability);
+
+    return () => {
+      observer.disconnect();
+      window.removeEventListener("resize", updateWrapAvailability);
+    };
+  }, [content]);
+
   const handleCopy = async () => {
     const ok = await copyToClipboard(content);
     if (ok) {
@@ -86,25 +113,35 @@ export function ExportDialog({ effect, params, open, onOpenChange }: ExportDialo
             
           
           
- + {canWrap && ( + + )}
               {content}
             
+
+              {content}
+            
From ce255814a26827feb3db1e79f0380a4ee8512291 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:55:41 -0300 Subject: [PATCH 12/60] Add centered effect navigation controls --- renderer/components/preview-stage.tsx | 46 ++++++++++++++++++++++--- renderer/components/treatment-stage.tsx | 38 +++++++++++++++++--- renderer/main/home-view.tsx | 18 +++++++++- 3 files changed, 92 insertions(+), 10 deletions(-) diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index ca15e83..6e4f983 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -1,5 +1,5 @@ import { Toolbar, ToolbarRow, ToolbarTitle, ToolbarActions, Button } from "@glaze/core/components"; -import { RotateCcw, Share } from "lucide-react"; +import { ChevronLeft, ChevronRight, RotateCcw, Share } from "lucide-react"; import type { Effect, EffectParams } from "./effects/types"; import { AppearanceToggle } from "./appearance-toggle"; @@ -7,6 +7,10 @@ interface PreviewStageProps { effect: Effect; params: EffectParams; replayToken: number; + previousLabel: string; + nextLabel: string; + onPrevious: () => void; + onNext: () => void; onReplay: () => void; onExport: () => void; } @@ -26,14 +30,46 @@ function timingSignature(effect: Effect, params: EffectParams): string { .join("&"); } -export function PreviewStage({ effect, params, replayToken, onReplay, onExport }: PreviewStageProps) { +export function PreviewStage({ + effect, + params, + replayToken, + previousLabel, + nextLabel, + onPrevious, + onNext, + onReplay, + onExport, +}: PreviewStageProps) { const Preview = effect.Preview; return (
- - {effect.name} - + +
+ + {effect.name} + +
+
diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index 61714cc..c8e9e6e 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -11,7 +11,7 @@ import { EmptyStateTitle, EmptyStateDescription, } from "@glaze/core/components"; -import { RotateCcw, Download, Check, ImageUp, Loader2 } from "lucide-react"; +import { ChevronLeft, ChevronRight, RotateCcw, Download, Check, ImageUp, Loader2 } from "lucide-react"; import type { Treatment } from "./treatments/types"; import type { EffectParams } from "./effects/types"; import { AppearanceToggle } from "./appearance-toggle"; @@ -36,6 +36,10 @@ interface TreatmentStageProps { source: HTMLImageElement | null; anim: AnimationSettings; replayToken: number; + previousLabel: string; + nextLabel: string; + onPrevious: () => void; + onNext: () => void; onReplay: () => void; onDropFile: (file: File) => void; } @@ -63,6 +67,10 @@ export function TreatmentStage({ source, anim, replayToken, + previousLabel, + nextLabel, + onPrevious, + onNext, onReplay, onDropFile, }: TreatmentStageProps) { @@ -180,9 +188,31 @@ export function TreatmentStage({ return (
- - {treatment.name} - + +
+ + {treatment.name} + +
+
diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx index 6c6b3d4..dd1253f 100644 --- a/renderer/main/home-view.tsx +++ b/renderer/main/home-view.tsx @@ -3,7 +3,7 @@ import { SplitView } from "@glaze/core/components"; import { effects } from "../components/effects/registry"; import { treatments } from "../components/treatments/registry"; import { defaultParams, type EffectParams, type ParamValue } from "../components/effects/types"; -import { resolveItem, defaultItemId } from "../lib/library"; +import { resolveItem, defaultItemId, libraryGroups } from "../lib/library"; import { EffectSidebar } from "../components/effect-sidebar"; import { ControlPanel } from "../components/control-panel"; import { PreviewStage } from "../components/preview-stage"; @@ -47,6 +47,10 @@ export function HomeView() { const item = resolveItem(selectedId) ?? { kind: "effect" as const, effect: effects[0] }; const activeId = item.kind === "effect" ? item.effect.id : item.treatment.id; + const libraryItems = React.useMemo(() => libraryGroups().flatMap((group) => group.items), []); + const activeIndex = libraryItems.findIndex((entry) => entry.id === activeId); + const previousItem = libraryItems[(activeIndex - 1 + libraryItems.length) % libraryItems.length]; + const nextItem = libraryItems[(activeIndex + 1) % libraryItems.length]; const controls = item.kind === "effect" ? item.effect.controls : item.treatment.controls; const params = paramsMap[activeId]; @@ -55,6 +59,10 @@ export function HomeView() { setReplayToken(0); }; + const handleNavigate = (id: string) => { + handleSelect(id); + }; + const handleChange = (id: string, value: ParamValue) => { setParamsMap((prev) => ({ ...prev, @@ -119,6 +127,10 @@ export function HomeView() { effect={item.effect} params={params} replayToken={replayToken} + previousLabel={previousItem.name} + nextLabel={nextItem.name} + onPrevious={() => handleNavigate(previousItem.id)} + onNext={() => handleNavigate(nextItem.id)} onReplay={() => setReplayToken((t) => t + 1)} onExport={() => setExportOpen(true)} /> @@ -129,6 +141,10 @@ export function HomeView() { source={source} anim={anim} replayToken={replayToken} + previousLabel={previousItem.name} + nextLabel={nextItem.name} + onPrevious={() => handleNavigate(previousItem.id)} + onNext={() => handleNavigate(nextItem.id)} onReplay={() => setReplayToken((t) => t + 1)} onDropFile={handlePickFile} /> From ace8a78844b88bef31e228f75d837675300d9f8e Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:56:46 -0300 Subject: [PATCH 13/60] Allow manual slider value entry --- renderer/components/control-row.tsx | 54 ++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx index 06d34b6..2b08419 100644 --- a/renderer/components/control-row.tsx +++ b/renderer/components/control-row.tsx @@ -1,3 +1,4 @@ +import * as React from "react"; import { Slider, Switch, @@ -12,6 +13,17 @@ import { import { CurveEditor } from "./curve-editor"; import type { ControlDef, EffectParams, ParamValue } from "./effects/types"; +function decimalsFor(step: number) { + const [, decimals = ""] = String(step).split("."); + return decimals.length; +} + +function snapToStep(value: number, min: number, max: number, step: number) { + const clamped = Math.min(max, Math.max(min, value)); + const snapped = Math.round((clamped - min) / step) * step + min; + return Number(snapped.toFixed(decimalsFor(step))); +} + export function LabeledSlider({ label, value, @@ -29,6 +41,23 @@ export function LabeledSlider({ step: number; onValueChange: (value: number) => void; }) { + const [draft, setDraft] = React.useState(String(value)); + + React.useEffect(() => { + setDraft(String(value)); + }, [value]); + + const commitDraft = () => { + const parsed = Number(draft); + if (!Number.isFinite(parsed)) { + setDraft(String(value)); + return; + } + const nextValue = snapToStep(parsed, min, max, step); + setDraft(String(nextValue)); + onValueChange(nextValue); + }; + return (
@@ -45,10 +74,27 @@ export function LabeledSlider({ {label}
- - {value} - {unit} - +
); } From 6ce78ade48c177a8b91ad1452a987fd181c9e6dc Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:57:24 -0300 Subject: [PATCH 14/60] Match switch controls to inspector sliders --- renderer/components/control-row.tsx | 4 ++-- renderer/shims/components.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx index 2b08419..3de34d5 100644 --- a/renderer/components/control-row.tsx +++ b/renderer/components/control-row.tsx @@ -147,8 +147,8 @@ export function ControlRow({ } case "switch": return ( -
- {control.label} +
+ {control.label} onChange(control.id, checked)} diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 10969ad..aab5dc9 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -371,16 +371,16 @@ export function Switch({ checked, onCheckedChange, className }: SwitchProps) { checked={checked} onCheckedChange={onCheckedChange} className={cn( - "inline-flex h-5 w-9 shrink-0 cursor-default rounded-full border-2 border-transparent", + "inline-flex h-6 w-11 shrink-0 cursor-default items-center rounded-md border border-separator p-0.5", "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500", - checked ? "bg-blue-500" : "bg-black/15 dark:bg-white/15", + checked ? "bg-blue-500" : "bg-black/5 dark:bg-white/10", className, )} > From 1676f073063129f012b6fc0e2aafc68987a0fbb9 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 16:59:15 -0300 Subject: [PATCH 15/60] Add color preset controls --- renderer/components/control-panel.tsx | 23 ++++++-- renderer/components/control-row.tsx | 73 +++++++++++++++++++++++++ renderer/components/treatment-panel.tsx | 21 +++++-- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/renderer/components/control-panel.tsx b/renderer/components/control-panel.tsx index f1bd87c..5d52417 100644 --- a/renderer/components/control-panel.tsx +++ b/renderer/components/control-panel.tsx @@ -1,6 +1,7 @@ import { Button, Separator } from "@glaze/core/components"; import { RotateCcw } from "lucide-react"; -import { ControlRow } from "./control-row"; +import * as React from "react"; +import { ColorPresetGrid, ControlRow } from "./control-row"; import type { Effect, EffectParams, ParamValue } from "./effects/types"; interface ControlPanelProps { @@ -11,6 +12,8 @@ interface ControlPanelProps { } export function ControlPanel({ effect, params, onChange, onReset }: ControlPanelProps) { + let colorPresetsShown = false; + return (
@@ -32,9 +35,21 @@ export function ControlPanel({ effect, params, onChange, onReset }: ControlPanel
- {effect.controls.map((control) => ( - - ))} + {effect.controls.map((control) => { + const showColorPresets = + !colorPresetsShown && + control.type === "color" && + (!control.visibleWhen || control.visibleWhen(params)); + if (showColorPresets) colorPresetsShown = true; + return ( + + {showColorPresets && ( + + )} + + + ); + })}
); diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx index 3de34d5..c2e7711 100644 --- a/renderer/components/control-row.tsx +++ b/renderer/components/control-row.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { + Button, Slider, Switch, SegmentedControl, @@ -10,6 +11,7 @@ import { SelectContent, SelectItem, } from "@glaze/core/components"; +import { Shuffle } from "lucide-react"; import { CurveEditor } from "./curve-editor"; import type { ControlDef, EffectParams, ParamValue } from "./effects/types"; @@ -24,6 +26,77 @@ function snapToStep(value: number, min: number, max: number, step: number) { return Number(snapped.toFixed(decimalsFor(step))); } +const COLOR_PALETTES = [ + ["#ef4444", "#f97316", "#facc15", "#38bdf8", "#f8fafc"], + ["#22d3ee", "#ec4899", "#f97316", "#7c3aed", "#111827"], + ["#2563eb", "#8b5cf6", "#d946ef", "#fb7185", "#f8fafc"], + ["#84cc16", "#22c55e", "#0f766e", "#ecfccb", "#111827"], + ["#0f4c81", "#2f8ac4", "#bae6fd", "#e0f2fe", "#0f172a"], + ["#ff3b30", "#ff8a5b", "#ffd166", "#4c1d95", "#ffffff"], + ["#ffffff", "#a1a1aa", "#3f3f46", "#18181b", "#09090b"], + ["#fb923c", "#e5e7eb", "#111827", "#fdba74", "#fff7ed"], +]; + +export function ColorPresetGrid({ + controls, + params, + onChange, +}: { + controls: ControlDef[]; + params: EffectParams; + onChange: (id: string, value: ParamValue) => void; +}) { + const colorControls = controls.filter( + (control) => control.type === "color" && (!control.visibleWhen || control.visibleWhen(params)), + ); + + if (colorControls.length === 0) return null; + + const currentColors = colorControls.map((control) => String(params[control.id]).toLowerCase()); + const applyPalette = (palette: string[]) => { + colorControls.forEach((control, index) => { + onChange(control.id, palette[index % palette.length]); + }); + }; + + return ( +
+
+ {COLOR_PALETTES.map((palette) => { + const selected = currentColors.every( + (color, index) => color === palette[index % palette.length].toLowerCase(), + ); + return ( + + ); + })} +
+ +
+ ); +} + export function LabeledSlider({ label, value, diff --git a/renderer/components/treatment-panel.tsx b/renderer/components/treatment-panel.tsx index 34af419..76e4803 100644 --- a/renderer/components/treatment-panel.tsx +++ b/renderer/components/treatment-panel.tsx @@ -8,7 +8,7 @@ import { } from "@glaze/core/components"; import { RotateCcw, ImageUp, Check } from "lucide-react"; import { cn } from "@glaze/core/utils"; -import { ControlRow, LabeledSlider } from "./control-row"; +import { ColorPresetGrid, ControlRow, LabeledSlider } from "./control-row"; import { CurveEditor } from "./curve-editor"; import type { Treatment } from "./treatments/types"; import type { EffectParams, ParamValue } from "./effects/types"; @@ -164,6 +164,7 @@ export function TreatmentPanel({ onAnimChange, }: TreatmentPanelProps) { const supportsMotion = Boolean(treatment.animate || treatment.animated); + let colorPresetsShown = false; return (
@@ -191,9 +192,21 @@ export function TreatmentPanel({ )} - {treatment.controls.map((control) => ( - - ))} + {treatment.controls.map((control) => { + const showColorPresets = + !colorPresetsShown && + control.type === "color" && + (!control.visibleWhen || control.visibleWhen(params)); + if (showColorPresets) colorPresetsShown = true; + return ( + + {showColorPresets && ( + + )} + + + ); + })} {supportsMotion && ( <> From 972fc904c8872c91b6b864f584162df177f56d98 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:03:39 -0300 Subject: [PATCH 16/60] Improve light mode contrast --- renderer/shims/components.tsx | 22 +++++++++++----------- renderer/web-styles.css | 18 ++++++++++++++++-- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index aab5dc9..2392b46 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -63,7 +63,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes { export function Button({ variant = "default", size = "medium", iconOnly = false, className, children, ...props }: ButtonProps) { const variantClasses = { glass: - "bg-white/10 hover:bg-white/20 dark:bg-white/5 dark:hover:bg-white/10 border border-white/20 text-foreground backdrop-blur-sm", + "bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 border border-black/[0.12] dark:border-white/20 text-foreground backdrop-blur-sm", accent: "bg-blue-500 hover:bg-blue-600 text-white border-transparent", muted: "bg-black/5 hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 border border-black/10 dark:border-white/10 text-foreground", @@ -120,7 +120,7 @@ export function Sidebar({ children, className }: { children?: React.ReactNode; c return (
@@ -146,7 +146,7 @@ interface SidebarListGroupProps { export function SidebarListGroup({ title, children, className }: SidebarListGroupProps) { return (
-
+
{title}
{children}
@@ -169,8 +169,8 @@ export function SidebarListItem({ title, icon, selected, onClick, className }: S className={cn( "w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left cursor-default transition-colors rounded-md mx-1", selected - ? "bg-blue-500/15 text-blue-600 dark:text-blue-400 font-medium" - : "text-black/70 dark:text-white/70 hover:bg-black/5 dark:hover:bg-white/5 hover:text-foreground", + ? "bg-blue-500/14 text-blue-700 dark:text-blue-400 font-medium" + : "text-black/[0.78] dark:text-white/70 hover:bg-black/[0.07] dark:hover:bg-white/5 hover:text-foreground", className, )} > @@ -221,7 +221,7 @@ export function SplitView({ {inspector && (
{inspector}
@@ -244,7 +244,7 @@ export function Toolbar({ position = "top", children, className }: ToolbarProps)
-
+
{children}
@@ -567,7 +567,7 @@ export function DialogContent({ children, className }: { children?: React.ReactN return (
+
{children}
); @@ -595,7 +595,7 @@ export function DialogBody({ children, className }: { children?: React.ReactNode export function DialogFooter({ children, className }: { children?: React.ReactNode; className?: string }) { return ( -
+
{children}
); diff --git a/renderer/web-styles.css b/renderer/web-styles.css index c5265ad..4d58ef7 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -1,6 +1,8 @@ /* Web build: explicitly import Tailwind (Glaze normally injects this) */ @import "tailwindcss"; +@custom-variant dark (&:where(.dark, .dark *)); + /* Source Tailwind from all renderer files */ @source "./main/**/*.{ts,tsx}"; @source "./settings/**/*.{ts,tsx}"; @@ -19,12 +21,16 @@ /* Separator */ --color-separator: rgba(0, 0, 0, 0.12); + --color-control: rgba(0, 0, 0, 0.055); + --color-elevated: rgba(0, 0, 0, 0.08); } .dark { --color-foreground: oklch(0.985 0 0); --color-background: oklch(0.145 0 0); --color-separator: rgba(255, 255, 255, 0.12); + --color-control: rgba(255, 255, 255, 0.055); + --color-elevated: rgba(255, 255, 255, 0.085); } html, @@ -50,7 +56,7 @@ body { } .text-secondary { - color: color-mix(in oklch, var(--color-foreground) 55%, transparent); + color: color-mix(in oklch, var(--color-foreground) 68%, transparent); } .text-small { @@ -78,6 +84,14 @@ body { border-color: var(--color-separator); } +.bg-control { + background-color: var(--color-control); +} + +.bg-elevated { + background-color: var(--color-elevated); +} + /* Drag region (no-op in browser) */ .drag-region { -webkit-app-region: drag; @@ -168,7 +182,7 @@ button { /* Subtle dotted backdrop for the live preview stage */ .motion-stage { background-image: radial-gradient( - var(--color-separator) 1px, + color-mix(in oklch, var(--color-foreground) 14%, transparent) 1px, transparent 1px ); background-size: 16px 16px; From 8ec040d494a7f9b2b03fb619ce2677d246ed2c68 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:05:33 -0300 Subject: [PATCH 17/60] Render short selects as choice blocks --- renderer/components/control-row.tsx | 34 +++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx index c2e7711..c15266b 100644 --- a/renderer/components/control-row.tsx +++ b/renderer/components/control-row.tsx @@ -239,6 +239,40 @@ export function ControlRow({
); case "select": + if (control.options.length <= 4) { + const value = String(params[control.id]); + const gridClass = + control.options.length === 4 + ? "grid-cols-2" + : control.options.length === 3 + ? "grid-cols-3" + : "grid-cols-2"; + return ( +
+ {control.label} +
+ {control.options.map((opt) => { + const selected = opt.value === value; + return ( + + ); + })} +
+
+ ); + } return (
{control.label} From e9b6691fd07ba3afe8f834050870c6ce4616c8f9 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:07:14 -0300 Subject: [PATCH 18/60] Remove inspector reset action --- renderer/components/control-panel.tsx | 17 ++--------------- renderer/components/treatment-panel.tsx | 15 +-------------- renderer/main/home-view.tsx | 8 -------- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/renderer/components/control-panel.tsx b/renderer/components/control-panel.tsx index 5d52417..83b54a5 100644 --- a/renderer/components/control-panel.tsx +++ b/renderer/components/control-panel.tsx @@ -1,5 +1,4 @@ -import { Button, Separator } from "@glaze/core/components"; -import { RotateCcw } from "lucide-react"; +import { Separator } from "@glaze/core/components"; import * as React from "react"; import { ColorPresetGrid, ControlRow } from "./control-row"; import type { Effect, EffectParams, ParamValue } from "./effects/types"; @@ -8,10 +7,9 @@ interface ControlPanelProps { effect: Effect; params: EffectParams; onChange: (id: string, value: ParamValue) => void; - onReset: () => void; } -export function ControlPanel({ effect, params, onChange, onReset }: ControlPanelProps) { +export function ControlPanel({ effect, params, onChange }: ControlPanelProps) { let colorPresetsShown = false; return ( @@ -21,17 +19,6 @@ export function ControlPanel({ effect, params, onChange, onReset }: ControlPanel

{effect.name}

{effect.description}

-
diff --git a/renderer/components/treatment-panel.tsx b/renderer/components/treatment-panel.tsx index 76e4803..3ad9705 100644 --- a/renderer/components/treatment-panel.tsx +++ b/renderer/components/treatment-panel.tsx @@ -6,7 +6,7 @@ import { SegmentedControl, SegmentedControlItem, } from "@glaze/core/components"; -import { RotateCcw, ImageUp, Check } from "lucide-react"; +import { ImageUp, Check } from "lucide-react"; import { cn } from "@glaze/core/utils"; import { ColorPresetGrid, ControlRow, LabeledSlider } from "./control-row"; import { CurveEditor } from "./curve-editor"; @@ -19,7 +19,6 @@ interface TreatmentPanelProps { treatment: Treatment; params: EffectParams; onChange: (id: string, value: ParamValue) => void; - onReset: () => void; sourceId: string; onPickSample: (sample: SampleImage) => void; onPickFile: (file: File) => void; @@ -156,7 +155,6 @@ export function TreatmentPanel({ treatment, params, onChange, - onReset, sourceId, onPickSample, onPickFile, @@ -172,17 +170,6 @@ export function TreatmentPanel({

{treatment.name}

{treatment.description}

-
diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx index dd1253f..725df5f 100644 --- a/renderer/main/home-view.tsx +++ b/renderer/main/home-view.tsx @@ -51,7 +51,6 @@ export function HomeView() { const activeIndex = libraryItems.findIndex((entry) => entry.id === activeId); const previousItem = libraryItems[(activeIndex - 1 + libraryItems.length) % libraryItems.length]; const nextItem = libraryItems[(activeIndex + 1) % libraryItems.length]; - const controls = item.kind === "effect" ? item.effect.controls : item.treatment.controls; const params = paramsMap[activeId]; const handleSelect = (id: string) => { @@ -70,11 +69,6 @@ export function HomeView() { })); }; - const handleReset = () => { - setParamsMap((prev) => ({ ...prev, [activeId]: defaultParams(controls) })); - setReplayToken((token) => token + 1); - }; - const handlePickSample = (sample: SampleImage) => { setSourceId(sample.id); loadSample(sample).then(setSource); @@ -104,14 +98,12 @@ export function HomeView() { effect={item.effect} params={params} onChange={handleChange} - onReset={handleReset} /> ) : ( Date: Sat, 27 Jun 2026 17:07:45 -0300 Subject: [PATCH 19/60] Navigate effects with arrow keys --- renderer/main/home-view.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx index 725df5f..e6bbf79 100644 --- a/renderer/main/home-view.tsx +++ b/renderer/main/home-view.tsx @@ -62,6 +62,30 @@ export function HomeView() { handleSelect(id); }; + React.useEffect(() => { + if (exportOpen) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return; + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; + + const target = event.target instanceof HTMLElement ? event.target : null; + if ( + target?.closest( + 'input, textarea, select, [contenteditable="true"], [role="slider"], [role="spinbutton"]', + ) + ) { + return; + } + + event.preventDefault(); + handleNavigate(event.key === "ArrowLeft" ? previousItem.id : nextItem.id); + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [exportOpen, previousItem.id, nextItem.id]); + const handleChange = (id: string, value: ParamValue) => { setParamsMap((prev) => ({ ...prev, From 669babbe852515a4b59f10a615347b9ca558d041 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:08:53 -0300 Subject: [PATCH 20/60] Refine color preset layout --- renderer/components/control-row.tsx | 31 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx index c15266b..c1df0e0 100644 --- a/renderer/components/control-row.tsx +++ b/renderer/components/control-row.tsx @@ -53,6 +53,8 @@ export function ColorPresetGrid({ if (colorControls.length === 0) return null; const currentColors = colorControls.map((control) => String(params[control.id]).toLowerCase()); + const colorCount = colorControls.length; + const previewPalettes = COLOR_PALETTES.map((palette) => palette.slice(0, colorCount)); const applyPalette = (palette: string[]) => { colorControls.forEach((control, index) => { onChange(control.id, palette[index % palette.length]); @@ -60,9 +62,22 @@ export function ColorPresetGrid({ }; return ( -
-
- {COLOR_PALETTES.map((palette) => { +
+
+ Presets + +
+
+ {previewPalettes.map((palette) => { const selected = currentColors.every( (color, index) => color === palette[index % palette.length].toLowerCase(), ); @@ -83,16 +98,6 @@ export function ColorPresetGrid({ ); })}
-
); } From b296dc6b002bf7f01f74c6671d889d7341b83e86 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:09:47 -0300 Subject: [PATCH 21/60] Add sidebar brand header --- renderer/components/effect-sidebar.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/renderer/components/effect-sidebar.tsx b/renderer/components/effect-sidebar.tsx index aefe930..df201bc 100644 --- a/renderer/components/effect-sidebar.tsx +++ b/renderer/components/effect-sidebar.tsx @@ -1,4 +1,5 @@ import { Sidebar, SidebarList, SidebarListGroup, SidebarListItem } from "@glaze/core/components"; +import { Sparkles } from "lucide-react"; import { libraryGroups } from "../lib/library"; interface EffectSidebarProps { @@ -10,6 +11,12 @@ export function EffectSidebar({ selectedId, onSelect }: EffectSidebarProps) { const groups = libraryGroups(); return ( +
+ + + + Motion Studio +
{groups.map((group) => ( From f40f168fbef8abc75bec108e3ca731b90e0c6dfe Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:10:45 -0300 Subject: [PATCH 22/60] Allow collapsing sidebar groups --- renderer/components/effect-sidebar.tsx | 22 ++++++++++++++++++++-- renderer/shims/components.tsx | 24 ++++++++++++++++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/renderer/components/effect-sidebar.tsx b/renderer/components/effect-sidebar.tsx index df201bc..d80a602 100644 --- a/renderer/components/effect-sidebar.tsx +++ b/renderer/components/effect-sidebar.tsx @@ -1,3 +1,4 @@ +import * as React from "react"; import { Sidebar, SidebarList, SidebarListGroup, SidebarListItem } from "@glaze/core/components"; import { Sparkles } from "lucide-react"; import { libraryGroups } from "../lib/library"; @@ -8,7 +9,19 @@ interface EffectSidebarProps { } export function EffectSidebar({ selectedId, onSelect }: EffectSidebarProps) { - const groups = libraryGroups(); + const groups = React.useMemo(() => libraryGroups(), []); + const [collapsedGroups, setCollapsedGroups] = React.useState>({}); + + React.useEffect(() => { + const activeGroup = groups.find((group) => group.items.some((item) => item.id === selectedId)); + if (!activeGroup || !collapsedGroups[activeGroup.title]) return; + setCollapsedGroups((prev) => ({ ...prev, [activeGroup.title]: false })); + }, [selectedId, groups, collapsedGroups]); + + const toggleGroup = (title: string) => { + setCollapsedGroups((prev) => ({ ...prev, [title]: !prev[title] })); + }; + return (
@@ -19,7 +32,12 @@ export function EffectSidebar({ selectedId, onSelect }: EffectSidebarProps) {
{groups.map((group) => ( - + toggleGroup(group.title)} + > {group.items.map((item) => ( void; } -export function SidebarListGroup({ title, children, className }: SidebarListGroupProps) { +export function SidebarListGroup({ title, children, className, collapsed = false, onToggle }: SidebarListGroupProps) { return (
-
+
-
{children}
+ + {!collapsed &&
{children}
}
); } From 99c74392f2bdc0e5de384be66adb825b741499a1 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:13:08 -0300 Subject: [PATCH 23/60] Float stage navigation controls --- renderer/components/preview-stage.tsx | 22 ++++++------- renderer/components/treatment-stage.tsx | 44 +++++++++++-------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index 6e4f983..45985f7 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -1,4 +1,4 @@ -import { Toolbar, ToolbarRow, ToolbarTitle, ToolbarActions, Button } from "@glaze/core/components"; +import { Button } from "@glaze/core/components"; import { ChevronLeft, ChevronRight, RotateCcw, Share } from "lucide-react"; import type { Effect, EffectParams } from "./effects/types"; import { AppearanceToggle } from "./appearance-toggle"; @@ -44,24 +44,26 @@ export function PreviewStage({ const Preview = effect.Preview; return (
- - -
+
+
+
- {effect.name} + {effect.name}
- - - - - -
+
+
+ +
{/* Give the preview a definite, centered width so panel-style effects using `w-full max-w-*` (overlays, dialogs, transitions) expand to their intended size instead of collapsing to ~0 in a shrink-to-fit diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index c8e9e6e..3660970 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -1,9 +1,5 @@ import * as React from "react"; import { - Toolbar, - ToolbarRow, - ToolbarTitle, - ToolbarActions, Button, SegmentedControl, SegmentedControlItem, @@ -187,24 +183,36 @@ export function TreatmentStage({ return (
- - -
+
{ + if (treatment.needsSource) { + e.preventDefault(); + setDragOver(true); + } + }} + onDragLeave={() => setDragOver(false)} + onDrop={treatment.needsSource ? handleDrop : undefined} + > +
+
- {treatment.name} + {treatment.name}
- - - - - -
{ - if (treatment.needsSource) { - e.preventDefault(); - setDragOver(true); - } - }} - onDragLeave={() => setDragOver(false)} - onDrop={treatment.needsSource ? handleDrop : undefined} - > +
+
+ +
{missingSource ? ( No image yet From 223c66b45e1460a2162279a91cb2718b42bea823 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:14:10 -0300 Subject: [PATCH 24/60] Replay treatment animation on control changes --- renderer/main/home-view.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx index e6bbf79..99eb031 100644 --- a/renderer/main/home-view.tsx +++ b/renderer/main/home-view.tsx @@ -32,8 +32,10 @@ export function HomeView() { // Shared easing-driven animation settings for image treatments. const [anim, setAnim] = React.useState(defaultAnimation); - const handleAnimChange = (patch: Partial) => + const handleAnimChange = (patch: Partial) => { setAnim((prev) => ({ ...prev, ...patch })); + setReplayToken((token) => token + 1); + }; React.useEffect(() => { let alive = true; From 1b776b702115d491e04579738af132a5fd998d17 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:14:45 -0300 Subject: [PATCH 25/60] Add random easing control --- renderer/components/treatment-panel.tsx | 27 +++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/renderer/components/treatment-panel.tsx b/renderer/components/treatment-panel.tsx index 3ad9705..5f4b559 100644 --- a/renderer/components/treatment-panel.tsx +++ b/renderer/components/treatment-panel.tsx @@ -6,7 +6,7 @@ import { SegmentedControl, SegmentedControlItem, } from "@glaze/core/components"; -import { ImageUp, Check } from "lucide-react"; +import { Dice5, ImageUp, Check } from "lucide-react"; import { cn } from "@glaze/core/utils"; import { ColorPresetGrid, ControlRow, LabeledSlider } from "./control-row"; import { CurveEditor } from "./curve-editor"; @@ -15,6 +15,17 @@ import type { EffectParams, ParamValue } from "./effects/types"; import type { AnimationSettings } from "../lib/anim"; import { SAMPLE_IMAGES, type SampleImage } from "../lib/source-image"; +const EASING_PRESETS = [ + "0.25,0.1,0.25,1", + "0.32,0.72,0,1", + "0.45,0,0.55,1", + "0.2,0.8,0.2,1", + "0.16,1,0.3,1", + "0.7,0,0.84,0", + "0.34,1.56,0.64,1", + "0.87,0,0.13,1", +]; + interface TreatmentPanelProps { treatment: Treatment; params: EffectParams; @@ -81,7 +92,19 @@ function AnimationControls({ />
- Easing +
+ Easing + +
onAnimChange({ easing: v })} animate />
From 548ff8443b983227e9bc2ce1b9c29db64696fa4c Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:16:55 -0300 Subject: [PATCH 26/60] Add floating canvas background controls --- renderer/components/preview-stage.tsx | 27 ++++++- renderer/components/stage-canvas-controls.tsx | 70 +++++++++++++++++++ renderer/components/treatment-stage.tsx | 35 ++++++++-- renderer/main/home-view.tsx | 11 +++ renderer/web-styles.css | 15 +++- 5 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 renderer/components/stage-canvas-controls.tsx diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index 45985f7..ec08853 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -2,6 +2,7 @@ import { Button } from "@glaze/core/components"; import { ChevronLeft, ChevronRight, RotateCcw, Share } from "lucide-react"; import type { Effect, EffectParams } from "./effects/types"; import { AppearanceToggle } from "./appearance-toggle"; +import { StageCanvasControls, type StageBackgroundMode } from "./stage-canvas-controls"; interface PreviewStageProps { effect: Effect; @@ -9,6 +10,10 @@ interface PreviewStageProps { replayToken: number; previousLabel: string; nextLabel: string; + backgroundMode: StageBackgroundMode; + zoom: number; + onBackgroundModeChange: (mode: StageBackgroundMode) => void; + onZoomChange: (zoom: number) => void; onPrevious: () => void; onNext: () => void; onReplay: () => void; @@ -36,6 +41,10 @@ export function PreviewStage({ replayToken, previousLabel, nextLabel, + backgroundMode, + zoom, + onBackgroundModeChange, + onZoomChange, onPrevious, onNext, onReplay, @@ -44,7 +53,10 @@ export function PreviewStage({ const Preview = effect.Preview; return (
-
+
+
+ +
); diff --git a/renderer/components/stage-canvas-controls.tsx b/renderer/components/stage-canvas-controls.tsx new file mode 100644 index 0000000..2795a7d --- /dev/null +++ b/renderer/components/stage-canvas-controls.tsx @@ -0,0 +1,70 @@ +import { Button } from "@glaze/core/components"; +import { Circle, Grid2X2, Minus, Plus } from "lucide-react"; + +export type StageBackgroundMode = "dots" | "grid" | "solid"; + +interface StageCanvasControlsProps { + backgroundMode: StageBackgroundMode; + zoom: number; + onBackgroundModeChange: (mode: StageBackgroundMode) => void; + onZoomChange: (zoom: number) => void; +} + +const BACKGROUND_MODES: StageBackgroundMode[] = ["dots", "grid", "solid"]; + +export function StageCanvasControls({ + backgroundMode, + zoom, + onBackgroundModeChange, + onZoomChange, +}: StageCanvasControlsProps) { + const nextMode = BACKGROUND_MODES[(BACKGROUND_MODES.indexOf(backgroundMode) + 1) % BACKGROUND_MODES.length]; + const zoomOut = () => onZoomChange(Math.max(0.5, Number((zoom - 0.1).toFixed(2)))); + const zoomIn = () => onZoomChange(Math.min(2, Number((zoom + 0.1).toFixed(2)))); + + return ( +
+ +
+ + + +
+ ); +} diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index 3660970..510ac79 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -11,6 +11,7 @@ import { ChevronLeft, ChevronRight, RotateCcw, Download, Check, ImageUp, Loader2 import type { Treatment } from "./treatments/types"; import type { EffectParams } from "./effects/types"; import { AppearanceToggle } from "./appearance-toggle"; +import { StageCanvasControls, type StageBackgroundMode } from "./stage-canvas-controls"; import { downloadCanvasPng } from "../lib/image-export"; import { applyAnimation, @@ -34,6 +35,10 @@ interface TreatmentStageProps { replayToken: number; previousLabel: string; nextLabel: string; + backgroundMode: StageBackgroundMode; + zoom: number; + onBackgroundModeChange: (mode: StageBackgroundMode) => void; + onZoomChange: (zoom: number) => void; onPrevious: () => void; onNext: () => void; onReplay: () => void; @@ -65,6 +70,10 @@ export function TreatmentStage({ replayToken, previousLabel, nextLabel, + backgroundMode, + zoom, + onBackgroundModeChange, + onZoomChange, onPrevious, onNext, onReplay, @@ -185,6 +194,7 @@ export function TreatmentStage({
{ if (treatment.needsSource) { e.preventDefault(); @@ -225,16 +235,19 @@ export function TreatmentStage({
{missingSource ? ( - - No image yet - - Pick a sample or drop an image to start treating it. - - +
+ + No image yet + + Pick a sample or drop an image to start treating it. + + +
) : ( )} @@ -281,6 +294,14 @@ export function TreatmentStage({
+
+ +
); diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx index 99eb031..2c0e512 100644 --- a/renderer/main/home-view.tsx +++ b/renderer/main/home-view.tsx @@ -10,6 +10,7 @@ import { PreviewStage } from "../components/preview-stage"; import { ExportDialog } from "../components/export-dialog"; import { TreatmentPanel } from "../components/treatment-panel"; import { TreatmentStage } from "../components/treatment-stage"; +import type { StageBackgroundMode } from "../components/stage-canvas-controls"; import { SAMPLE_IMAGES, loadSample, loadFile, type SampleImage } from "../lib/source-image"; import { defaultAnimation, type AnimationSettings } from "../lib/anim"; @@ -25,6 +26,8 @@ export function HomeView() { const [paramsMap, setParamsMap] = React.useState>(buildInitialParams); const [replayToken, setReplayToken] = React.useState(0); const [exportOpen, setExportOpen] = React.useState(false); + const [stageBackgroundMode, setStageBackgroundMode] = React.useState("dots"); + const [stageZoom, setStageZoom] = React.useState(1); // Shared source image for the treatment lab. const [sourceId, setSourceId] = React.useState(SAMPLE_IMAGES[0].id); @@ -147,6 +150,10 @@ export function HomeView() { replayToken={replayToken} previousLabel={previousItem.name} nextLabel={nextItem.name} + backgroundMode={stageBackgroundMode} + zoom={stageZoom} + onBackgroundModeChange={setStageBackgroundMode} + onZoomChange={setStageZoom} onPrevious={() => handleNavigate(previousItem.id)} onNext={() => handleNavigate(nextItem.id)} onReplay={() => setReplayToken((t) => t + 1)} @@ -161,6 +168,10 @@ export function HomeView() { replayToken={replayToken} previousLabel={previousItem.name} nextLabel={nextItem.name} + backgroundMode={stageBackgroundMode} + zoom={stageZoom} + onBackgroundModeChange={setStageBackgroundMode} + onZoomChange={setStageZoom} onPrevious={() => handleNavigate(previousItem.id)} onNext={() => handleNavigate(nextItem.id)} onReplay={() => setReplayToken((t) => t + 1)} diff --git a/renderer/web-styles.css b/renderer/web-styles.css index 4d58ef7..976314e 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -179,11 +179,22 @@ button { } } -/* Subtle dotted backdrop for the live preview stage */ -.motion-stage { +.motion-stage[data-bg-mode="dots"], +.motion-stage:not([data-bg-mode]) { background-image: radial-gradient( color-mix(in oklch, var(--color-foreground) 14%, transparent) 1px, transparent 1px ); background-size: 16px 16px; } + +.motion-stage[data-bg-mode="grid"] { + background-image: + linear-gradient(color-mix(in oklch, var(--color-foreground) 10%, transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in oklch, var(--color-foreground) 10%, transparent) 1px, transparent 1px); + background-size: 32px 32px; +} + +.motion-stage[data-bg-mode="solid"] { + background-image: none; +} From 7ff1120c8291aa14ee3bdff01bbf590da133d3f3 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:18:01 -0300 Subject: [PATCH 27/60] Inset sidebar selection highlight --- renderer/shims/components.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index fabd192..d9bacfa 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -183,7 +183,7 @@ export function SidebarListItem({ title, icon, selected, onClick, className }: S {!collapsed &&
{children}
}
From 7be7da2d9c683e214b844718503f3dff67af77a6 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:22:31 -0300 Subject: [PATCH 29/60] Add resizable sidebar width --- renderer/main/home-view.tsx | 2 +- renderer/shims/components.tsx | 42 +++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/renderer/main/home-view.tsx b/renderer/main/home-view.tsx index 2c0e512..649869c 100644 --- a/renderer/main/home-view.tsx +++ b/renderer/main/home-view.tsx @@ -120,7 +120,7 @@ export function HomeView() { storageKey="motion-studio" className="h-full" sidebar={} - sidebarSize={{ default: 220, min: 180, max: 300 }} + sidebarSize={{ default: 240, min: 180, max: 350 }} inspector={ item.kind === "effect" ? ( { + if (!sidebarStorageKey) return sidebarSize.default; + const stored = window.localStorage.getItem(sidebarStorageKey); + const parsed = stored ? Number(stored) : NaN; + return Number.isFinite(parsed) ? parsed : sidebarSize.default; + }); + const sidebarMin = sidebarSize.min ?? 160; + const sidebarMax = sidebarSize.max ?? 350; + + const startSidebarResize = (event: React.PointerEvent) => { + event.preventDefault(); + const startX = event.clientX; + const startWidth = sidebarWidth; + + const onMove = (moveEvent: PointerEvent) => { + const nextWidth = Math.min(sidebarMax, Math.max(sidebarMin, startWidth + moveEvent.clientX - startX)); + setSidebarWidth(nextWidth); + if (sidebarStorageKey) window.localStorage.setItem(sidebarStorageKey, String(Math.round(nextWidth))); + }; + + const onUp = () => { + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + }; + + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp); + }; + return (
{sidebar && (
{sidebar} +
)}
{children}
From 01249247bccd32b709652fd91452a558b6a4ed29 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:23:04 -0300 Subject: [PATCH 30/60] Prevent sidebar horizontal scrolling --- renderer/shims/components.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index bf6faac..d290cc3 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -131,7 +131,7 @@ export function Sidebar({ children, className }: { children?: React.ReactNode; c export function SidebarList({ children, className }: { children?: React.ReactNode; className?: string }) { return ( -
+
{children}
); From 3f2791b74a0798aa7044c855dbc4a6d063012e56 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:23:58 -0300 Subject: [PATCH 31/60] Generate harmonic random colors --- renderer/components/control-row.tsx | 53 ++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/renderer/components/control-row.tsx b/renderer/components/control-row.tsx index c1df0e0..94fc6f5 100644 --- a/renderer/components/control-row.tsx +++ b/renderer/components/control-row.tsx @@ -37,6 +37,57 @@ const COLOR_PALETTES = [ ["#fb923c", "#e5e7eb", "#111827", "#fdba74", "#fff7ed"], ]; +const HARMONY_OFFSETS = [ + [0, 24, -24, 48, -48], + [0, 180, 30, 210, 150], + [0, 150, 210, 30, 330], + [0, 120, 240, 60, 300], + [0, 90, 180, 270, 45], +]; + +function randomBetween(min: number, max: number) { + return min + Math.random() * (max - min); +} + +function wrapHue(hue: number) { + return ((hue % 360) + 360) % 360; +} + +function hslToHex(hue: number, saturation: number, lightness: number) { + const s = saturation / 100; + const l = lightness / 100; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)); + const m = l - c / 2; + const [r, g, b] = + hue < 60 ? [c, x, 0] : + hue < 120 ? [x, c, 0] : + hue < 180 ? [0, c, x] : + hue < 240 ? [0, x, c] : + hue < 300 ? [x, 0, c] : + [c, 0, x]; + return [r, g, b] + .map((channel) => Math.round((channel + m) * 255).toString(16).padStart(2, "0")) + .join("") + .toUpperCase() + .padStart(6, "0") + .replace(/^/, "#"); +} + +function randomHarmonyPalette(count: number) { + const baseHue = Math.floor(Math.random() * 360); + const offsets = HARMONY_OFFSETS[Math.floor(Math.random() * HARMONY_OFFSETS.length)]; + const saturation = randomBetween(64, 88); + const lightness = randomBetween(48, 66); + + return Array.from({ length: count }, (_, index) => { + const hue = wrapHue(baseHue + offsets[index % offsets.length] + randomBetween(-5, 5)); + const sat = Math.min(92, Math.max(54, saturation + randomBetween(-8, 8))); + const light = Math.min(74, Math.max(38, lightness + randomBetween(-8, 8))); + return hslToHex(hue, sat, light); + }); +} + export function ColorPresetGrid({ controls, params, @@ -71,7 +122,7 @@ export function ColorPresetGrid({ iconOnly aria-label="Randomize colors" title="Randomize colors" - onClick={() => applyPalette(previewPalettes[Math.floor(Math.random() * previewPalettes.length)])} + onClick={() => applyPalette(randomHarmonyPalette(colorCount))} > From c73bae9708e2c6a4dad73721158bf96538209203 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:24:56 -0300 Subject: [PATCH 32/60] Hide horizontal stage overflow when zoomed --- renderer/components/preview-stage.tsx | 2 +- renderer/components/treatment-stage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index ec08853..59c761a 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -54,7 +54,7 @@ export function PreviewStage({ return (
diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index 510ac79..0395696 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -193,7 +193,7 @@ export function TreatmentStage({ return (
{ if (treatment.needsSource) { From caa1e9c3315d7c079bda0d7e55da2c24237f5475 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:26:33 -0300 Subject: [PATCH 33/60] Cycle global appearance modes --- renderer/components/appearance-toggle.tsx | 35 ++++++++++++----------- renderer/components/preview-stage.tsx | 2 +- renderer/components/treatment-stage.tsx | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/renderer/components/appearance-toggle.tsx b/renderer/components/appearance-toggle.tsx index afe92be..f72944d 100644 --- a/renderer/components/appearance-toggle.tsx +++ b/renderer/components/appearance-toggle.tsx @@ -1,44 +1,45 @@ import * as React from "react"; import { Button } from "@glaze/core/components"; -import { Sun, Moon } from "lucide-react"; +import { Laptop, Moon, Sun } from "lucide-react"; + +type ThemeSource = "system" | "dark" | "light"; + +const THEME_ORDER: ThemeSource[] = ["system", "dark", "light"]; -// Quick light/dark switch for the main window. The full Auto/Light/Dark control -// still lives in Settings; this just flips the effective appearance in place. export function AppearanceToggle() { - const [isDark, setIsDark] = React.useState(null); + const [themeSource, setThemeSource] = React.useState("system"); React.useEffect(() => { let active = true; - window.glazeAPI.nativeTheme.getShouldUseDarkColors().then((dark) => { - if (active) setIsDark(dark); + window.glazeAPI.nativeTheme.getThemeSource().then((source) => { + if (active) setThemeSource(source); }); - // Keep the icon in sync when the OS appearance changes while on "system". - const media = window.matchMedia("(prefers-color-scheme: dark)"); - const onChange = (e: MediaQueryListEvent) => setIsDark(e.matches); - media.addEventListener("change", onChange); return () => { active = false; - media.removeEventListener("change", onChange); }; }, []); const toggle = async () => { - const next = !isDark; - setIsDark(next); - await window.glazeAPI.nativeTheme.setThemeSource(next ? "dark" : "light"); + const next = THEME_ORDER[(THEME_ORDER.indexOf(themeSource) + 1) % THEME_ORDER.length]; + setThemeSource(next); + await window.glazeAPI.nativeTheme.setThemeSource(next); }; + const label = + themeSource === "system" ? "Use dark mode" : themeSource === "dark" ? "Use light mode" : "Use system theme"; + return ( ); } diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index 59c761a..63d9c29 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -84,7 +84,7 @@ export function PreviewStage({
-
+
{/* Give the preview a definite, centered width so panel-style effects diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index 0395696..5bc05bb 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -231,7 +231,7 @@ export function TreatmentStage({
-
+
{missingSource ? ( From 604dd97a03087691630683c728830edcd31d94a6 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:29:12 -0300 Subject: [PATCH 34/60] Refine canvas background modes --- renderer/components/preview-stage.tsx | 9 ++++++- renderer/components/stage-canvas-controls.tsx | 26 ++++++++++++++++--- renderer/components/treatment-stage.tsx | 9 ++++++- renderer/main/home-view.tsx | 7 ++++- renderer/web-styles.css | 25 +++++++++++++++--- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index 63d9c29..6472040 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -2,7 +2,7 @@ import { Button } from "@glaze/core/components"; import { ChevronLeft, ChevronRight, RotateCcw, Share } from "lucide-react"; import type { Effect, EffectParams } from "./effects/types"; import { AppearanceToggle } from "./appearance-toggle"; -import { StageCanvasControls, type StageBackgroundMode } from "./stage-canvas-controls"; +import { StageCanvasControls, type StageBackgroundMode, type StageCanvasTone } from "./stage-canvas-controls"; interface PreviewStageProps { effect: Effect; @@ -11,8 +11,10 @@ interface PreviewStageProps { previousLabel: string; nextLabel: string; backgroundMode: StageBackgroundMode; + canvasTone: StageCanvasTone; zoom: number; onBackgroundModeChange: (mode: StageBackgroundMode) => void; + onCanvasToneChange: (tone: StageCanvasTone) => void; onZoomChange: (zoom: number) => void; onPrevious: () => void; onNext: () => void; @@ -42,8 +44,10 @@ export function PreviewStage({ previousLabel, nextLabel, backgroundMode, + canvasTone, zoom, onBackgroundModeChange, + onCanvasToneChange, onZoomChange, onPrevious, onNext, @@ -56,6 +60,7 @@ export function PreviewStage({
@@ -116,8 +121,10 @@ export function PreviewStage({
diff --git a/renderer/components/stage-canvas-controls.tsx b/renderer/components/stage-canvas-controls.tsx index 2795a7d..686e13d 100644 --- a/renderer/components/stage-canvas-controls.tsx +++ b/renderer/components/stage-canvas-controls.tsx @@ -1,26 +1,35 @@ import { Button } from "@glaze/core/components"; -import { Circle, Grid2X2, Minus, Plus } from "lucide-react"; +import { CircleDot, Grid2X2, Laptop, Minus, Moon, Plus, Square, Sun } from "lucide-react"; export type StageBackgroundMode = "dots" | "grid" | "solid"; +export type StageCanvasTone = "system" | "light" | "dark"; interface StageCanvasControlsProps { backgroundMode: StageBackgroundMode; + canvasTone: StageCanvasTone; zoom: number; onBackgroundModeChange: (mode: StageBackgroundMode) => void; + onCanvasToneChange: (tone: StageCanvasTone) => void; onZoomChange: (zoom: number) => void; } const BACKGROUND_MODES: StageBackgroundMode[] = ["dots", "grid", "solid"]; +const CANVAS_TONES: StageCanvasTone[] = ["system", "dark", "light"]; export function StageCanvasControls({ backgroundMode, + canvasTone, zoom, onBackgroundModeChange, + onCanvasToneChange, onZoomChange, }: StageCanvasControlsProps) { const nextMode = BACKGROUND_MODES[(BACKGROUND_MODES.indexOf(backgroundMode) + 1) % BACKGROUND_MODES.length]; + const nextTone = CANVAS_TONES[(CANVAS_TONES.indexOf(canvasTone) + 1) % CANVAS_TONES.length]; const zoomOut = () => onZoomChange(Math.max(0.5, Number((zoom - 0.1).toFixed(2)))); const zoomIn = () => onZoomChange(Math.min(2, Number((zoom + 0.1).toFixed(2)))); + const PatternIcon = backgroundMode === "dots" ? CircleDot : backgroundMode === "grid" ? Grid2X2 : Square; + const ToneIcon = canvasTone === "system" ? Laptop : canvasTone === "dark" ? Moon : Sun; return (
@@ -30,10 +39,21 @@ export function StageCanvasControls({ iconOnly className="rounded-full" aria-label={`Switch canvas background to ${nextMode}`} - title={`Background: ${backgroundMode}`} + title={`Canvas background: ${backgroundMode}`} onClick={() => onBackgroundModeChange(nextMode)} > - {backgroundMode === "grid" ? : } + + +
+
+ + +
{previewPalettes.map((palette) => { From c5ef9284f81c9dedccf396fd40be0df6202ea63e Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:35:15 -0300 Subject: [PATCH 37/60] Improve sidebar collapse chevron visibility --- renderer/shims/components.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index d290cc3..96fa252 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -150,14 +150,14 @@ export function SidebarListGroup({ title, children, className, collapsed = false
-
- -
{/* Give the preview a definite, centered width so panel-style effects using `w-full max-w-*` (overlays, dialogs, transitions) expand to their intended size instead of collapsing to ~0 in a shrink-to-fit diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index beb773f..8149917 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -10,7 +10,6 @@ import { import { ChevronLeft, ChevronRight, RotateCcw, Download, Check, ImageUp, Loader2 } from "lucide-react"; import type { Treatment } from "./treatments/types"; import type { EffectParams } from "./effects/types"; -import { AppearanceToggle } from "./appearance-toggle"; import { StageCanvasControls, type StageBackgroundMode, type StageCanvasTone } from "./stage-canvas-controls"; import { downloadCanvasPng } from "../lib/image-export"; import { @@ -236,9 +235,6 @@ export function TreatmentStage({
-
- -
{missingSource ? (
From ef1ece0ec5296da5581e6ed4d61e2f738a4ca508 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:43:31 -0300 Subject: [PATCH 41/60] Fold canvas tone into background control --- renderer/components/stage-canvas-controls.tsx | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/renderer/components/stage-canvas-controls.tsx b/renderer/components/stage-canvas-controls.tsx index 686e13d..cef8937 100644 --- a/renderer/components/stage-canvas-controls.tsx +++ b/renderer/components/stage-canvas-controls.tsx @@ -1,5 +1,5 @@ import { Button } from "@glaze/core/components"; -import { CircleDot, Grid2X2, Laptop, Minus, Moon, Plus, Square, Sun } from "lucide-react"; +import { CircleDot, Grid2X2, Minus, Plus, Square } from "lucide-react"; export type StageBackgroundMode = "dots" | "grid" | "solid"; export type StageCanvasTone = "system" | "light" | "dark"; @@ -13,8 +13,17 @@ interface StageCanvasControlsProps { onZoomChange: (zoom: number) => void; } -const BACKGROUND_MODES: StageBackgroundMode[] = ["dots", "grid", "solid"]; -const CANVAS_TONES: StageCanvasTone[] = ["system", "dark", "light"]; +const CANVAS_PRESETS: Array<{ mode: StageBackgroundMode; tone: StageCanvasTone }> = [ + { mode: "dots", tone: "system" }, + { mode: "grid", tone: "system" }, + { mode: "solid", tone: "system" }, + { mode: "dots", tone: "dark" }, + { mode: "grid", tone: "dark" }, + { mode: "solid", tone: "dark" }, + { mode: "dots", tone: "light" }, + { mode: "grid", tone: "light" }, + { mode: "solid", tone: "light" }, +]; export function StageCanvasControls({ backgroundMode, @@ -24,12 +33,17 @@ export function StageCanvasControls({ onCanvasToneChange, onZoomChange, }: StageCanvasControlsProps) { - const nextMode = BACKGROUND_MODES[(BACKGROUND_MODES.indexOf(backgroundMode) + 1) % BACKGROUND_MODES.length]; - const nextTone = CANVAS_TONES[(CANVAS_TONES.indexOf(canvasTone) + 1) % CANVAS_TONES.length]; + const currentPresetIndex = CANVAS_PRESETS.findIndex( + (preset) => preset.mode === backgroundMode && preset.tone === canvasTone, + ); + const nextPreset = CANVAS_PRESETS[(Math.max(0, currentPresetIndex) + 1) % CANVAS_PRESETS.length]; const zoomOut = () => onZoomChange(Math.max(0.5, Number((zoom - 0.1).toFixed(2)))); const zoomIn = () => onZoomChange(Math.min(2, Number((zoom + 0.1).toFixed(2)))); const PatternIcon = backgroundMode === "dots" ? CircleDot : backgroundMode === "grid" ? Grid2X2 : Square; - const ToneIcon = canvasTone === "system" ? Laptop : canvasTone === "dark" ? Moon : Sun; + const switchCanvasPreset = () => { + onBackgroundModeChange(nextPreset.mode); + onCanvasToneChange(nextPreset.tone); + }; return (
@@ -38,23 +52,12 @@ export function StageCanvasControls({ size="small" iconOnly className="rounded-full" - aria-label={`Switch canvas background to ${nextMode}`} - title={`Canvas background: ${backgroundMode}`} - onClick={() => onBackgroundModeChange(nextMode)} + aria-label={`Switch canvas background to ${nextPreset.mode}`} + title={`Canvas: ${backgroundMode}, ${canvasTone}`} + onClick={switchCanvasPreset} > -
+ {colorsChanged && ( + + )}
+ {canReset && ( + + )} + )} {(treatment.animated || anim.enabled) && ( + ); } diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 6255c02..f606ae2 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -4,6 +4,7 @@ */ import * as React from "react"; import { Tooltip, Select as RadixSelect, Slider as RadixSlider, Switch as RadixSwitch } from "radix-ui"; +import { ChevronDown } from "lucide-react"; import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -150,20 +151,20 @@ export function SidebarListGroup({ title, children, className, collapsed = false
{!collapsed &&
{children}
}
From cdcd7a544a30aa45a24231e63cf1ac6f343e83fd Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 17:58:30 -0300 Subject: [PATCH 46/60] Fix canvas controls contrast for mixed theme and tone --- renderer/components/stage-canvas-controls.tsx | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/renderer/components/stage-canvas-controls.tsx b/renderer/components/stage-canvas-controls.tsx index cef8937..126c692 100644 --- a/renderer/components/stage-canvas-controls.tsx +++ b/renderer/components/stage-canvas-controls.tsx @@ -13,6 +13,33 @@ interface StageCanvasControlsProps { onZoomChange: (zoom: number) => void; } +function canvasControlToneClasses(canvasTone: StageCanvasTone) { + if (canvasTone === "light") { + return { + pill: "border-black/12 bg-white/90", + divider: "bg-black/12 dark:bg-black/12", + button: + "rounded-full bg-black/5 hover:bg-black/10 border-black/10 text-neutral-900 dark:bg-black/5 dark:hover:bg-black/10 dark:border-black/10 dark:text-neutral-900", + zoom: "text-neutral-600 hover:text-neutral-900 dark:text-neutral-600 dark:hover:text-neutral-900", + }; + } + if (canvasTone === "dark") { + return { + pill: "border-white/20 bg-neutral-950/90", + divider: "bg-white/12 dark:bg-white/12", + button: + "rounded-full bg-white/5 hover:bg-white/10 border-white/10 text-white dark:bg-white/5 dark:hover:bg-white/10 dark:border-white/10 dark:text-white", + zoom: "text-neutral-400 hover:text-white dark:text-neutral-400 dark:hover:text-white", + }; + } + return { + pill: "border-separator bg-background/85", + divider: "bg-black/12 dark:bg-white/12", + button: "rounded-full", + zoom: "text-secondary hover:text-foreground", + }; +} + const CANVAS_PRESETS: Array<{ mode: StageBackgroundMode; tone: StageCanvasTone }> = [ { mode: "dots", tone: "system" }, { mode: "grid", tone: "system" }, @@ -44,26 +71,30 @@ export function StageCanvasControls({ onBackgroundModeChange(nextPreset.mode); onCanvasToneChange(nextPreset.tone); }; + const toneClasses = canvasControlToneClasses(canvasTone); return ( -
+
-
+
+ )} + +
{groups.map((group) => ( diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 9507bd8..29ffe7f 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -4,7 +4,7 @@ */ import * as React from "react"; import { Tooltip, Select as RadixSelect, Slider as RadixSlider, Switch as RadixSwitch } from "radix-ui"; -import { ChevronDown } from "lucide-react"; +import { ChevronDown, PanelLeftOpen } from "lucide-react"; import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -117,13 +117,14 @@ export function Separator({ className, orientation = "horizontal" }: SeparatorPr // --------------------------------------------------------------------------- // Sidebar // --------------------------------------------------------------------------- -export function Sidebar({ children, className }: { children?: React.ReactNode; className?: string }) { +export function Sidebar({ children, className, ...props }: React.ComponentProps<"div">) { return (
{children}
@@ -233,6 +234,17 @@ export function SidebarListItem({ title, icon, selected, onClick, className }: S // --------------------------------------------------------------------------- // SplitView — 3-pane layout: sidebar | main content | inspector // --------------------------------------------------------------------------- +interface SidebarCollapseContextValue { + collapsed: boolean; + toggle: () => void; +} + +const SidebarCollapseContext = React.createContext(null); + +export function useSidebarCollapse() { + return React.useContext(SidebarCollapseContext); +} + interface PaneSize { default: number; min?: number; @@ -259,12 +271,18 @@ export function SplitView({ storageKey, }: SplitViewProps) { const sidebarStorageKey = storageKey ? `${storageKey}:sidebar-width` : null; + const collapseStorageKey = storageKey ? `${storageKey}:sidebar-collapsed` : null; const [sidebarWidth, setSidebarWidth] = React.useState(() => { if (!sidebarStorageKey) return sidebarSize.default; const stored = window.localStorage.getItem(sidebarStorageKey); const parsed = stored ? Number(stored) : NaN; return Number.isFinite(parsed) ? parsed : sidebarSize.default; }); + const [sidebarCollapsed, setSidebarCollapsed] = React.useState(() => { + if (!collapseStorageKey) return false; + return window.localStorage.getItem(collapseStorageKey) === "1"; + }); + const savedSidebarWidthRef = React.useRef(sidebarWidth); const sidebarMin = sidebarSize.min ?? 160; const sidebarMax = sidebarSize.max ?? 350; const setPersistedSidebarWidth = (width: number) => { @@ -272,6 +290,27 @@ export function SplitView({ if (sidebarStorageKey) window.localStorage.setItem(sidebarStorageKey, String(Math.round(width))); }; + React.useEffect(() => { + if (!sidebarCollapsed) savedSidebarWidthRef.current = sidebarWidth; + }, [sidebarWidth, sidebarCollapsed]); + + const toggleSidebarCollapsed = React.useCallback(() => { + setSidebarCollapsed((prev) => { + const next = !prev; + if (next) { + savedSidebarWidthRef.current = sidebarWidth; + } else { + setPersistedSidebarWidth(savedSidebarWidthRef.current || sidebarSize.default); + } + if (collapseStorageKey) window.localStorage.setItem(collapseStorageKey, next ? "1" : "0"); + return next; + }); + }, [sidebarWidth, sidebarSize.default, collapseStorageKey]); + + const effectiveSidebarWidth = sidebarCollapsed ? 0 : sidebarWidth; + const effectiveSidebarMin = sidebarCollapsed ? 0 : sidebarMin; + const effectiveSidebarMax = sidebarCollapsed ? 0 : sidebarMax; + const startSidebarResize = (event: React.PointerEvent) => { event.preventDefault(); if (event.detail > 1) return; @@ -294,33 +333,57 @@ export function SplitView({ const resetSidebarWidth = () => setPersistedSidebarWidth(sidebarSize.default); return ( -
- {sidebar && ( -
- {sidebar} + +
+ {sidebar && (
-
- )} -
{children}
- {inspector && ( -
- {inspector} -
- )} -
+ style={{ + width: effectiveSidebarWidth, + minWidth: effectiveSidebarMin, + maxWidth: effectiveSidebarMax, + }} + className={cn( + "relative h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out", + sidebarCollapsed && "border-r-0", + )} + > + {sidebar} + {!sidebarCollapsed && ( +
+ )} +
+ )} + {sidebarCollapsed && ( +
+ +
+ )} +
{children}
+ {inspector && ( +
+ {inspector} +
+ )} +
+ ); } From c1b595748d2c2f9849388f8feec08615ca6f05c0 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:05:58 -0300 Subject: [PATCH 51/60] Clean up branch diff Remove redundant dark-mode class duplicates in canvas controls and shorten drag-region comment. --- renderer/components/stage-canvas-controls.tsx | 14 ++++++-------- renderer/web-styles.css | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/renderer/components/stage-canvas-controls.tsx b/renderer/components/stage-canvas-controls.tsx index 126c692..9f786eb 100644 --- a/renderer/components/stage-canvas-controls.tsx +++ b/renderer/components/stage-canvas-controls.tsx @@ -17,19 +17,17 @@ function canvasControlToneClasses(canvasTone: StageCanvasTone) { if (canvasTone === "light") { return { pill: "border-black/12 bg-white/90", - divider: "bg-black/12 dark:bg-black/12", - button: - "rounded-full bg-black/5 hover:bg-black/10 border-black/10 text-neutral-900 dark:bg-black/5 dark:hover:bg-black/10 dark:border-black/10 dark:text-neutral-900", - zoom: "text-neutral-600 hover:text-neutral-900 dark:text-neutral-600 dark:hover:text-neutral-900", + divider: "bg-black/12", + button: "rounded-full bg-black/5 hover:bg-black/10 border-black/10 text-neutral-900", + zoom: "text-neutral-600 hover:text-neutral-900", }; } if (canvasTone === "dark") { return { pill: "border-white/20 bg-neutral-950/90", - divider: "bg-white/12 dark:bg-white/12", - button: - "rounded-full bg-white/5 hover:bg-white/10 border-white/10 text-white dark:bg-white/5 dark:hover:bg-white/10 dark:border-white/10 dark:text-white", - zoom: "text-neutral-400 hover:text-white dark:text-neutral-400 dark:hover:text-white", + divider: "bg-white/12", + button: "rounded-full bg-white/5 hover:bg-white/10 border-white/10 text-white", + zoom: "text-neutral-400 hover:text-white", }; } return { diff --git a/renderer/web-styles.css b/renderer/web-styles.css index 45d040c..ab558c0 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -92,7 +92,7 @@ body { background-color: var(--color-elevated); } -/* Drag region overlay: click-through so top controls stay interactive */ +/* Drag region — click-through overlay */ .drag-region { -webkit-app-region: drag; app-region: drag; From aab49e5a1c36485e915550d7c28cbaba7ed01997 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:06:40 -0300 Subject: [PATCH 52/60] Remove sidebar panel collapse --- renderer/components/effect-sidebar.tsx | 21 +---- renderer/shims/components.tsx | 116 ++++++------------------- 2 files changed, 30 insertions(+), 107 deletions(-) diff --git a/renderer/components/effect-sidebar.tsx b/renderer/components/effect-sidebar.tsx index 9932773..cb60438 100644 --- a/renderer/components/effect-sidebar.tsx +++ b/renderer/components/effect-sidebar.tsx @@ -1,6 +1,5 @@ import * as React from "react"; -import { PanelLeftClose } from "lucide-react"; -import { Sidebar, SidebarList, SidebarListGroup, SidebarListItem, useSidebarCollapse } from "@glaze/core/components"; +import { Sidebar, SidebarList, SidebarListGroup, SidebarListItem } from "@glaze/core/components"; import { AppearanceToggle } from "./appearance-toggle"; import { libraryGroups } from "../lib/library"; @@ -10,7 +9,6 @@ interface EffectSidebarProps { } export function EffectSidebar({ selectedId, onSelect }: EffectSidebarProps) { - const sidebarCollapse = useSidebarCollapse(); const groups = React.useMemo(() => libraryGroups(), []); const [collapsedGroups, setCollapsedGroups] = React.useState>({}); const previousSelectedId = React.useRef(selectedId); @@ -29,24 +27,11 @@ export function EffectSidebar({ selectedId, onSelect }: EffectSidebarProps) { }; return ( - +
Motion Studio -
- {sidebarCollapse && !sidebarCollapse.collapsed && ( - - )} - -
+
{groups.map((group) => ( diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 29ffe7f..171b152 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -4,7 +4,7 @@ */ import * as React from "react"; import { Tooltip, Select as RadixSelect, Slider as RadixSlider, Switch as RadixSwitch } from "radix-ui"; -import { ChevronDown, PanelLeftOpen } from "lucide-react"; +import { ChevronDown } from "lucide-react"; import { clsx } from "clsx"; import { twMerge } from "tailwind-merge"; @@ -234,17 +234,6 @@ export function SidebarListItem({ title, icon, selected, onClick, className }: S // --------------------------------------------------------------------------- // SplitView — 3-pane layout: sidebar | main content | inspector // --------------------------------------------------------------------------- -interface SidebarCollapseContextValue { - collapsed: boolean; - toggle: () => void; -} - -const SidebarCollapseContext = React.createContext(null); - -export function useSidebarCollapse() { - return React.useContext(SidebarCollapseContext); -} - interface PaneSize { default: number; min?: number; @@ -271,18 +260,12 @@ export function SplitView({ storageKey, }: SplitViewProps) { const sidebarStorageKey = storageKey ? `${storageKey}:sidebar-width` : null; - const collapseStorageKey = storageKey ? `${storageKey}:sidebar-collapsed` : null; const [sidebarWidth, setSidebarWidth] = React.useState(() => { if (!sidebarStorageKey) return sidebarSize.default; const stored = window.localStorage.getItem(sidebarStorageKey); const parsed = stored ? Number(stored) : NaN; return Number.isFinite(parsed) ? parsed : sidebarSize.default; }); - const [sidebarCollapsed, setSidebarCollapsed] = React.useState(() => { - if (!collapseStorageKey) return false; - return window.localStorage.getItem(collapseStorageKey) === "1"; - }); - const savedSidebarWidthRef = React.useRef(sidebarWidth); const sidebarMin = sidebarSize.min ?? 160; const sidebarMax = sidebarSize.max ?? 350; const setPersistedSidebarWidth = (width: number) => { @@ -290,27 +273,6 @@ export function SplitView({ if (sidebarStorageKey) window.localStorage.setItem(sidebarStorageKey, String(Math.round(width))); }; - React.useEffect(() => { - if (!sidebarCollapsed) savedSidebarWidthRef.current = sidebarWidth; - }, [sidebarWidth, sidebarCollapsed]); - - const toggleSidebarCollapsed = React.useCallback(() => { - setSidebarCollapsed((prev) => { - const next = !prev; - if (next) { - savedSidebarWidthRef.current = sidebarWidth; - } else { - setPersistedSidebarWidth(savedSidebarWidthRef.current || sidebarSize.default); - } - if (collapseStorageKey) window.localStorage.setItem(collapseStorageKey, next ? "1" : "0"); - return next; - }); - }, [sidebarWidth, sidebarSize.default, collapseStorageKey]); - - const effectiveSidebarWidth = sidebarCollapsed ? 0 : sidebarWidth; - const effectiveSidebarMin = sidebarCollapsed ? 0 : sidebarMin; - const effectiveSidebarMax = sidebarCollapsed ? 0 : sidebarMax; - const startSidebarResize = (event: React.PointerEvent) => { event.preventDefault(); if (event.detail > 1) return; @@ -333,57 +295,33 @@ export function SplitView({ const resetSidebarWidth = () => setPersistedSidebarWidth(sidebarSize.default); return ( - -
- {sidebar && ( -
- {sidebar} - {!sidebarCollapsed && ( -
- )} -
- )} - {sidebarCollapsed && ( -
- -
- )} -
{children}
- {inspector && ( +
+ {sidebar && ( +
+ {sidebar}
- {inspector} -
- )} -
- + role="separator" + aria-orientation="vertical" + aria-label="Resize sidebar" + className="absolute inset-y-0 right-0 z-20 w-1 cursor-col-resize bg-transparent hover:bg-blue-500/40" + onDoubleClick={resetSidebarWidth} + onPointerDown={startSidebarResize} + /> +
+ )} +
{children}
+ {inspector && ( +
+ {inspector} +
+ )} +
); } From 3931a1167ae7efb540f7130c02dd0b85112c6909 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:07:09 -0300 Subject: [PATCH 53/60] Use Minimize2 and Maximize2 for sidebar group toggle --- renderer/components/effect-sidebar.tsx | 28 ++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/renderer/components/effect-sidebar.tsx b/renderer/components/effect-sidebar.tsx index cb60438..fc19e57 100644 --- a/renderer/components/effect-sidebar.tsx +++ b/renderer/components/effect-sidebar.tsx @@ -1,4 +1,5 @@ import * as React from "react"; +import { Maximize2, Minimize2 } from "lucide-react"; import { Sidebar, SidebarList, SidebarListGroup, SidebarListItem } from "@glaze/core/components"; import { AppearanceToggle } from "./appearance-toggle"; import { libraryGroups } from "../lib/library"; @@ -26,12 +27,35 @@ export function EffectSidebar({ selectedId, onSelect }: EffectSidebarProps) { setCollapsedGroups((prev) => ({ ...prev, [title]: !prev[title] })); }; + const anyGroupExpanded = groups.some((group) => collapsedGroups[group.title] !== true); + + const toggleAllGroups = () => { + if (anyGroupExpanded) { + setCollapsedGroups(Object.fromEntries(groups.map((group) => [group.title, true]))); + return; + } + setCollapsedGroups({}); + }; + + const groupsToggleLabel = anyGroupExpanded ? "Collapse all groups" : "Expand all groups"; + return ( - +
Motion Studio - +
+ + +
{groups.map((group) => ( From 2174ad8b9d36cd2448dab3d8b06baac9833a7044 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:12:29 -0300 Subject: [PATCH 54/60] Fix sidebar bottom scroll fade --- renderer/shims/components.tsx | 13 ++++++------- renderer/web-styles.css | 5 +++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 171b152..7ae1c22 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -158,16 +158,15 @@ export function SidebarList({ children, className }: { children?: React.ReactNod return (
-
- {children} -
+ > + {children} +
); } diff --git a/renderer/web-styles.css b/renderer/web-styles.css index ab558c0..0a7794d 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -114,6 +114,11 @@ body { scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } +.sidebar-scroll-fade-bottom { + -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); + mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); +} + .sidebar-scroll::-webkit-scrollbar { width: 10px; } From ce480735c61d3da3156b1b022c05d388efc52b6b Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:12:56 -0300 Subject: [PATCH 55/60] Fix infinite canvas scroll --- renderer/components/preview-stage.tsx | 4 ++-- renderer/components/treatment-stage.tsx | 4 ++-- renderer/main/index.tsx | 5 ++++- renderer/settings/index.tsx | 3 +++ renderer/web-styles.css | 5 ----- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/renderer/components/preview-stage.tsx b/renderer/components/preview-stage.tsx index a389395..6dd26ea 100644 --- a/renderer/components/preview-stage.tsx +++ b/renderer/components/preview-stage.tsx @@ -59,9 +59,9 @@ export function PreviewStage({ }: PreviewStageProps) { const Preview = effect.Preview; return ( -
+
diff --git a/renderer/components/treatment-stage.tsx b/renderer/components/treatment-stage.tsx index 56d3255..b3fa80b 100644 --- a/renderer/components/treatment-stage.tsx +++ b/renderer/components/treatment-stage.tsx @@ -198,9 +198,9 @@ export function TreatmentStage({ }; return ( -
+
{ diff --git a/renderer/main/index.tsx b/renderer/main/index.tsx index bf96f09..064a9a5 100644 --- a/renderer/main/index.tsx +++ b/renderer/main/index.tsx @@ -32,7 +32,10 @@ root.render( , ); -// Hot Module Replacement (HMR) support +// HMR re-runs this module; unmount so we don't stack duplicate app roots in #root. if (import.meta.hot) { import.meta.hot.accept(); + import.meta.hot.dispose(() => { + root.unmount(); + }); } diff --git a/renderer/settings/index.tsx b/renderer/settings/index.tsx index b6f8158..4f9ae71 100644 --- a/renderer/settings/index.tsx +++ b/renderer/settings/index.tsx @@ -36,4 +36,7 @@ root.render( if (import.meta.hot) { import.meta.hot.accept(); + import.meta.hot.dispose(() => { + root.unmount(); + }); } diff --git a/renderer/web-styles.css b/renderer/web-styles.css index 0a7794d..ab558c0 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -114,11 +114,6 @@ body { scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } -.sidebar-scroll-fade-bottom { - -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); - mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); -} - .sidebar-scroll::-webkit-scrollbar { width: 10px; } From 361c39388553a248f328220b620394ea6239e3e9 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:13:27 -0300 Subject: [PATCH 56/60] Restore sidebar scroll fade styles --- renderer/web-styles.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/renderer/web-styles.css b/renderer/web-styles.css index ab558c0..0a7794d 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -114,6 +114,11 @@ body { scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } +.sidebar-scroll-fade-bottom { + -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); + mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); +} + .sidebar-scroll::-webkit-scrollbar { width: 10px; } From e72441f443e6f46a0c242728918149da18818d44 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:14:51 -0300 Subject: [PATCH 57/60] Fix sidebar scroll fade (top + bottom) Replace mask-image fade with gradient overlays that show at the top and bottom of the sidebar list when more content is available in that direction. --- renderer/shims/components.tsx | 34 ++++++++++++++++++++++------------ renderer/web-styles.css | 5 ----- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 7ae1c22..1c3010f 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -133,12 +133,14 @@ export function Sidebar({ children, className, ...props }: React.ComponentProps< export function SidebarList({ children, className }: { children?: React.ReactNode; className?: string }) { const scrollRef = React.useRef(null); + const [showTopFade, setShowTopFade] = React.useState(false); const [showBottomFade, setShowBottomFade] = React.useState(false); - const updateBottomFade = React.useCallback(() => { + const updateScrollFades = React.useCallback(() => { const el = scrollRef.current; if (!el) return; const canScroll = el.scrollHeight > el.clientHeight + 1; + setShowTopFade(canScroll && el.scrollTop > 0); const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 2; setShowBottomFade(canScroll && !atBottom); }, []); @@ -146,27 +148,35 @@ export function SidebarList({ children, className }: { children?: React.ReactNod React.useEffect(() => { const el = scrollRef.current; if (!el) return; - updateBottomFade(); - const observer = new ResizeObserver(updateBottomFade); + updateScrollFades(); + const observer = new ResizeObserver(updateScrollFades); observer.observe(el); - el.addEventListener("scroll", updateBottomFade, { passive: true }); + el.addEventListener("scroll", updateScrollFades, { passive: true }); return () => { observer.disconnect(); - el.removeEventListener("scroll", updateBottomFade); + el.removeEventListener("scroll", updateScrollFades); }; - }, [updateBottomFade, children]); + }, [updateScrollFades, children]); return (
+
+ {children} +
- {children} -
+ /> +
); } diff --git a/renderer/web-styles.css b/renderer/web-styles.css index 0a7794d..ab558c0 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -114,11 +114,6 @@ body { scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } -.sidebar-scroll-fade-bottom { - -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); - mask-image: linear-gradient(to bottom, #000 calc(100% - 2.5rem), transparent 100%); -} - .sidebar-scroll::-webkit-scrollbar { width: 10px; } From 58e9967e9913d5797f73e5746361f4d84004cbf5 Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:18:32 -0300 Subject: [PATCH 58/60] Refine sidebar scroll fade with mask-image Use conditional top/bottom mask classes on the scroll container, including a combined gradient when both edges need fade hints. --- renderer/shims/components.tsx | 21 +++++--------- renderer/web-styles.css | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 1c3010f..622eedb 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -160,23 +160,16 @@ export function SidebarList({ children, className }: { children?: React.ReactNod return (
-
- {children} -
-
+ > + {children} +
); } diff --git a/renderer/web-styles.css b/renderer/web-styles.css index ab558c0..9b31f89 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -111,9 +111,61 @@ body { } .sidebar-scroll { + --sidebar-fade-size: 4rem; scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } +.sidebar-scroll-fade-top { + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0, + #000 12%, + #000 var(--sidebar-fade-size) + ); + mask-image: linear-gradient( + to bottom, + transparent 0, + #000 12%, + #000 var(--sidebar-fade-size) + ); +} + +.sidebar-scroll-fade-bottom { + -webkit-mask-image: linear-gradient( + to top, + transparent 0, + #000 12%, + #000 var(--sidebar-fade-size) + ); + mask-image: linear-gradient( + to top, + transparent 0, + #000 12%, + #000 var(--sidebar-fade-size) + ); +} + +.sidebar-scroll-fade-top.sidebar-scroll-fade-bottom { + -webkit-mask-image: linear-gradient( + to bottom, + transparent 0, + #000 12%, + #000 var(--sidebar-fade-size), + #000 calc(100% - var(--sidebar-fade-size)), + #000 88%, + transparent 100% + ); + mask-image: linear-gradient( + to bottom, + transparent 0, + #000 12%, + #000 var(--sidebar-fade-size), + #000 calc(100% - var(--sidebar-fade-size)), + #000 88%, + transparent 100% + ); +} + .sidebar-scroll::-webkit-scrollbar { width: 10px; } From 8f5e5d0e13c2e8b4da3bd94401cf7b3922dfdbed Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:19:48 -0300 Subject: [PATCH 59/60] Use gradient overlays for sidebar scroll fade Replace mask-image fades with theme-aware top/bottom overlays that toggle visibility based on scroll position. --- renderer/shims/components.tsx | 17 ++++----- renderer/web-styles.css | 67 ++++++++++++++--------------------- 2 files changed, 35 insertions(+), 49 deletions(-) diff --git a/renderer/shims/components.tsx b/renderer/shims/components.tsx index 622eedb..cb5a703 100644 --- a/renderer/shims/components.tsx +++ b/renderer/shims/components.tsx @@ -160,16 +160,17 @@ export function SidebarList({ children, className }: { children?: React.ReactNod return (
-
+
{children}
+
+
); } diff --git a/renderer/web-styles.css b/renderer/web-styles.css index 9b31f89..b580c1a 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -111,57 +111,42 @@ body { } .sidebar-scroll { - --sidebar-fade-size: 4rem; + --sidebar-fade-size: 2.25rem; scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } -.sidebar-scroll-fade-top { - -webkit-mask-image: linear-gradient( - to bottom, - transparent 0, - #000 12%, - #000 var(--sidebar-fade-size) - ); - mask-image: linear-gradient( - to bottom, - transparent 0, - #000 12%, - #000 var(--sidebar-fade-size) - ); +.sidebar-fade { + pointer-events: none; + position: absolute; + inset-inline: 0; + z-index: 10; + height: var(--sidebar-fade-size); + opacity: 0; + transition: opacity 150ms ease; } -.sidebar-scroll-fade-bottom { - -webkit-mask-image: linear-gradient( - to top, - transparent 0, - #000 12%, - #000 var(--sidebar-fade-size) - ); - mask-image: linear-gradient( - to top, - transparent 0, - #000 12%, - #000 var(--sidebar-fade-size) - ); +.sidebar-fade.is-visible { + opacity: 1; } -.sidebar-scroll-fade-top.sidebar-scroll-fade-bottom { - -webkit-mask-image: linear-gradient( +.sidebar-fade-top { + top: 0; + background: linear-gradient( to bottom, - transparent 0, - #000 12%, - #000 var(--sidebar-fade-size), - #000 calc(100% - var(--sidebar-fade-size)), - #000 88%, + color-mix(in oklch, var(--color-background) 82%, transparent) 0%, + color-mix(in oklch, var(--color-background) 48%, transparent) 38%, + color-mix(in oklch, var(--color-background) 14%, transparent) 68%, transparent 100% ); - mask-image: linear-gradient( - to bottom, - transparent 0, - #000 12%, - #000 var(--sidebar-fade-size), - #000 calc(100% - var(--sidebar-fade-size)), - #000 88%, +} + +.sidebar-fade-bottom { + bottom: 0; + background: linear-gradient( + to top, + color-mix(in oklch, var(--color-background) 82%, transparent) 0%, + color-mix(in oklch, var(--color-background) 48%, transparent) 38%, + color-mix(in oklch, var(--color-background) 14%, transparent) 68%, transparent 100% ); } From e045de34d4fe4f3a8e8f7b4fe0bdf895430be08a Mon Sep 17 00:00:00 2001 From: vyctorbrzezowski Date: Sat, 27 Jun 2026 18:21:57 -0300 Subject: [PATCH 60/60] Tone down sidebar scroll fades --- renderer/web-styles.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/renderer/web-styles.css b/renderer/web-styles.css index b580c1a..8edb381 100644 --- a/renderer/web-styles.css +++ b/renderer/web-styles.css @@ -111,11 +111,11 @@ body { } .sidebar-scroll { - --sidebar-fade-size: 2.25rem; scrollbar-color: color-mix(in oklch, var(--color-foreground) 32%, transparent) transparent; } .sidebar-fade { + --sidebar-fade-size: 2rem; pointer-events: none; position: absolute; inset-inline: 0; @@ -133,9 +133,9 @@ body { top: 0; background: linear-gradient( to bottom, - color-mix(in oklch, var(--color-background) 82%, transparent) 0%, - color-mix(in oklch, var(--color-background) 48%, transparent) 38%, - color-mix(in oklch, var(--color-background) 14%, transparent) 68%, + color-mix(in oklch, var(--color-background) 68%, transparent) 0%, + color-mix(in oklch, var(--color-background) 34%, transparent) 42%, + color-mix(in oklch, var(--color-background) 10%, transparent) 72%, transparent 100% ); } @@ -144,9 +144,9 @@ body { bottom: 0; background: linear-gradient( to top, - color-mix(in oklch, var(--color-background) 82%, transparent) 0%, - color-mix(in oklch, var(--color-background) 48%, transparent) 38%, - color-mix(in oklch, var(--color-background) 14%, transparent) 68%, + color-mix(in oklch, var(--color-background) 68%, transparent) 0%, + color-mix(in oklch, var(--color-background) 34%, transparent) 42%, + color-mix(in oklch, var(--color-background) 10%, transparent) 72%, transparent 100% ); }