diff --git a/backend/apps/workflows/models.py b/backend/apps/workflows/models.py index 2c4238f1..93a6f98e 100644 --- a/backend/apps/workflows/models.py +++ b/backend/apps/workflows/models.py @@ -105,6 +105,8 @@ class Workflow(BaseModel): title: str = "Untitled workflow" description: str = "" icon: str = "" + # User-chosen swatch (hex). None falls back to the id-hash color in the UI. + color: Optional[str] = None system_prompt: Optional[str] = None use_synced_prompt: bool = True steps: list[WorkflowStep] = Field(default_factory=list) @@ -212,6 +214,7 @@ class WorkflowCreate(BaseModel): unsaved: bool = False description: str = "" icon: str = "" + color: Optional[str] = None system_prompt: Optional[str] = None use_synced_prompt: bool = True steps: list[WorkflowStep] = Field(default_factory=list) @@ -246,6 +249,7 @@ class WorkflowUpdate(BaseModel): auto_named: Optional[bool] = None description: Optional[str] = None icon: Optional[str] = None + color: Optional[str] = None system_prompt: Optional[str] = None use_synced_prompt: Optional[bool] = None steps: Optional[list[WorkflowStep]] = None diff --git a/backend/apps/workflows/workflows.py b/backend/apps/workflows/workflows.py index f5cfae7a..44c4e865 100644 --- a/backend/apps/workflows/workflows.py +++ b/backend/apps/workflows/workflows.py @@ -248,6 +248,7 @@ async def create_workflow(body: WorkflowCreate): title=body.title, description=body.description, icon=body.icon, + color=body.color, system_prompt=body.system_prompt, use_synced_prompt=body.use_synced_prompt, steps=body.steps, diff --git a/frontend/src/app/pages/Workflows/app/ColorSwatch.tsx b/frontend/src/app/pages/Workflows/app/ColorSwatch.tsx new file mode 100644 index 00000000..2eb9d3c2 --- /dev/null +++ b/frontend/src/app/pages/Workflows/app/ColorSwatch.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { useWC, WORKFLOW_PALETTE } from './uiKit'; + +// Small swatch button that opens a palette popover. Self-rendered (no portal) +// so it survives the canvas compositor, same reasoning as RepeatField. +const ColorSwatch: React.FC<{ value: string; onChange: (hex: string) => void; size?: number }> = ({ value, onChange, size = 14 }) => { + const WC = useWC(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!open) return; + const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; + document.addEventListener('mousedown', onDoc); + return () => document.removeEventListener('mousedown', onDoc); + }, [open]); + + return ( +
+
setOpen((o) => !o)} + title="Workflow color" + style={{ width: size, height: size, borderRadius: 4, cursor: 'pointer', background: value, boxShadow: `0 0 0 1px rgba(${WC.inkRGB},0.14)` }} + /> + {open && ( +
+ {WORKFLOW_PALETTE.map((hex) => { + const active = hex.toLowerCase() === value.toLowerCase(); + return ( +
{ onChange(hex); setOpen(false); }} + style={{ width: 18, height: 18, borderRadius: 5, cursor: 'pointer', background: hex, boxShadow: active ? `0 0 0 2px ${WC.paper}, 0 0 0 3.5px ${hex}` : `0 0 0 1px rgba(${WC.inkRGB},0.12)` }} + /> + ); + })} +
+ )} +
+ ); +}; + +export default ColorSwatch; diff --git a/frontend/src/shared/state/workflowsSlice.ts b/frontend/src/shared/state/workflowsSlice.ts index b1370e4c..da35f243 100644 --- a/frontend/src/shared/state/workflowsSlice.ts +++ b/frontend/src/shared/state/workflowsSlice.ts @@ -63,6 +63,8 @@ export interface Workflow { title: string; description: string; icon: string; + /** User-chosen swatch (hex). Null/undefined falls back to the id-hash color. */ + color?: string | null; system_prompt: string | null; use_synced_prompt: boolean; steps: WorkflowStep[];