-
Notifications
You must be signed in to change notification settings - Fork 0
Add Canvas Theme Selector and Styles #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| $ vite --host 0.0.0.0 --port 3000 | ||
|
|
||
| VITE v5.4.21 ready in 312 ms | ||
| VITE v5.4.21 ready in 298 ms | ||
|
|
||
| ➜ Local: http://localhost:3000/ | ||
| ➜ Network: http://192.168.0.2:3000/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,6 +175,26 @@ function Canvas() { | |
| const rf = useReactFlow(); | ||
| const isMobile = useIsMobile(); | ||
|
|
||
| // ---- Canvas Theme Selector State ---- | ||
| const [canvasTheme, setCanvasTheme] = useState<string>(() => { | ||
| try { | ||
| return localStorage.getItem("agent_flow.canvas_theme") || "theme-ice"; | ||
| } catch { | ||
| return "theme-ice"; | ||
| } | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| const root = document.documentElement; | ||
| root.classList.remove("theme-ice", "theme-amber", "theme-blueprint", "theme-ink"); | ||
| root.classList.add(canvasTheme); | ||
| try { | ||
| localStorage.setItem("agent_flow.canvas_theme", canvasTheme); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }, [canvasTheme]); | ||
|
Comment on lines
+178
to
+196
|
||
|
|
||
| // ---- Workflows Library State ---- | ||
| const [workflows, setWorkflows] = useState<Workflow[]>(() => loadWorkflows()); | ||
| const [activeWorkflowId, setActiveWorkflowId] = useState<string | null>(() => loadActiveWorkflowId()); | ||
|
|
@@ -1417,6 +1437,20 @@ function Canvas() { | |
| <span className="hidden md:inline font-mono text-[10px] text-[hsl(var(--ink-faint))]"> | ||
| {nodes.length} nodes · {edges.length} edges | ||
| </span> | ||
| <select | ||
| value={canvasTheme} | ||
| onChange={(e) => { | ||
| setCanvasTheme(e.target.value); | ||
| toast.success(`Theme changed to ${e.target.selectedOptions[0].text}`); | ||
| }} | ||
| className="font-mono text-[10px] sm:text-[11px] bg-transparent px-2 py-1 border border-dashed border-[hsl(var(--ink))] text-[hsl(var(--ink))] outline-none focus:bg-[hsl(var(--paper))] transition-colors cursor-pointer" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On sub- Useful? React with 👍 / 👎. |
||
| title="Choose canvas workspace theme" | ||
| > | ||
|
Comment on lines
+1446
to
+1448
|
||
| <option value="theme-ice" className="bg-[hsl(var(--paper))] text-[hsl(var(--ink))]">🎨 Default Ice</option> | ||
| <option value="theme-amber" className="bg-[hsl(var(--paper))] text-[hsl(var(--ink))]">🎨 Retro Amber</option> | ||
| <option value="theme-blueprint" className="bg-[hsl(var(--paper))] text-[hsl(var(--ink))]">🎨 Blueprint Grid</option> | ||
| <option value="theme-ink" className="bg-[hsl(var(--paper))] text-[hsl(var(--ink))]">🎨 Minimal Ink</option> | ||
| </select> | ||
| <button | ||
| onClick={runValidate} | ||
| className="font-mono text-[10px] sm:text-[11px] px-2 sm:px-3 py-1 border border-dashed border-[hsl(var(--ink))] hover:bg-[hsl(var(--ink))] hover:text-[hsl(var(--paper))] transition-colors" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import React from "react"; | ||
| import { render, screen, fireEvent } from "@testing-library/react"; | ||
| import "@testing-library/jest-dom"; | ||
| import Index from "../pages/Index"; | ||
|
|
||
| // Mock ResizeObserver for ReactFlow in JSDOM | ||
| class MockResizeObserver { | ||
| observe = vi.fn(); | ||
| unobserve = vi.fn(); | ||
| disconnect = vi.fn(); | ||
| } | ||
| global.ResizeObserver = MockResizeObserver; | ||
|
|
||
| describe("Canvas Themes Feature", () => { | ||
| beforeEach(() => { | ||
| // Clean documentElement classes | ||
| document.documentElement.className = ""; | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| it("should apply default theme-ice to document.documentElement on initial mount", () => { | ||
| render(<Index />); | ||
|
|
||
| // Default theme should be theme-ice | ||
| expect(document.documentElement.classList.contains("theme-ice")).toBe(true); | ||
| expect(localStorage.getItem("agent_flow.canvas_theme")).toBe("theme-ice"); | ||
| }); | ||
|
|
||
| it("should restore theme from localStorage on mount if saved", () => { | ||
| localStorage.setItem("agent_flow.canvas_theme", "theme-blueprint"); | ||
|
|
||
| render(<Index />); | ||
|
|
||
| // Restored theme should be theme-blueprint | ||
| expect(document.documentElement.classList.contains("theme-blueprint")).toBe(true); | ||
| expect(document.documentElement.classList.contains("theme-ice")).toBe(false); | ||
| }); | ||
|
|
||
| it("should change theme and update document element class and localStorage on selection change", () => { | ||
| render(<Index />); | ||
|
|
||
| // Initially theme-ice | ||
| expect(document.documentElement.classList.contains("theme-ice")).toBe(true); | ||
|
|
||
| const themeSelect = screen.getByTitle("Choose canvas workspace theme"); | ||
| expect(themeSelect).toBeInTheDocument(); | ||
|
|
||
| // Select Retro Amber | ||
| fireEvent.change(themeSelect, { target: { value: "theme-amber" } }); | ||
|
|
||
| // Class list should update | ||
| expect(document.documentElement.classList.contains("theme-amber")).toBe(true); | ||
| expect(document.documentElement.classList.contains("theme-ice")).toBe(false); | ||
|
|
||
| // LocalStorage should update | ||
| expect(localStorage.getItem("agent_flow.canvas_theme")).toBe("theme-amber"); | ||
|
|
||
| // Select Blueprint Grid | ||
| fireEvent.change(themeSelect, { target: { value: "theme-blueprint" } }); | ||
| expect(document.documentElement.classList.contains("theme-blueprint")).toBe(true); | ||
| expect(document.documentElement.classList.contains("theme-amber")).toBe(false); | ||
| expect(localStorage.getItem("agent_flow.canvas_theme")).toBe("theme-blueprint"); | ||
| }); | ||
| }); | ||
|
Comment on lines
+64
to
+65
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Validate the persisted theme before applying it.
localStoragecan contain any string. An unknown value creates a class with no matching theme. A value containing whitespace can makeroot.classList.add(canvasTheme)throw aDOMException. The controlled<select>can also have no matching option.Restrict the value to the four supported theme names and fall back to
"theme-ice". Use the same validation in the change handler. Add a test for invalid stored values.🤖 Prompt for AI Agents