diff --git a/frontend/public/getting-started.md b/frontend/public/getting-started.md index 6268515..f5006a0 100644 --- a/frontend/public/getting-started.md +++ b/frontend/public/getting-started.md @@ -6,6 +6,8 @@ Welcome to tono, a node-based SPM image analysis tool. This guide covers the bas The main area is an infinite canvas where you build workflows by placing and connecting nodes. You can pan by holding middle click and dragging (two-finger drag on a trackpad), and zoom by holding right click and dragging (pinching on a trackpad). You can make a box selection by left clicking and dragging. +You can open a menu by clicking the blue cantilever bubble in the top left. + ### Adding Nodes Right-click anywhere on the canvas to open the **Add Node** menu. Nodes are organized into categories — hover a category to see the available nodes, then click one to place it. The same nodes can be found in different categories, if they share similar features. @@ -101,3 +103,10 @@ Use **Ctrl+Z** / **Cmd+Z** to undo and **Ctrl+Shift+Z** / **Cmd+Shift+Z** to red | Ctrl+V | Paste nodes | | Right-click canvas | Add node menu | | Escape | Close help panel | + +## Plugins + +It's possible to write your own nodes and load them as plugins in tono. Please refer to the documentation in the [GitHub](https://github.com/vipqualitypost/tono) for how to do this. + +Plugins are disabled by default on webapp for security reasons. You can install and run the native app if you want to develop and use your own node plugins. + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 20c6f6f..9bfee39 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -898,6 +898,7 @@ function Flow() { const [helpTabs, setHelpTabs] = useState<{ label: string; type?: string; content: string | null }[]>([]); const [activeHelpTab, setActiveHelpTab] = useState(null); const [updateInfo, setUpdateInfo] = useState<{ latest: string; url: string } | null>(null); + const [menuOpen, setMenuOpen] = useState(false); const flowContainerRef = useRef(null); const panTimerRef = useRef | null>(null); @@ -2145,6 +2146,9 @@ function Flow() { if (defaultWorkflowLoadAttemptedRef.current) return; defaultWorkflowLoadAttemptedRef.current = true; + // Only auto-load the example workflow on first visit + if (localStorage.getItem('tono_visited')) return; + const graphHasContent = () => { const currentNodes = (reactFlow.getNodes() as TonoNode[]); const currentEdges = (reactFlow.getEdges() as TonoEdge[]); @@ -2167,6 +2171,20 @@ function Flow() { } }, [applyMaybePackedWorkflow, reactFlow, scheduleAutoRun]); + const loadExampleWorkflow = useCallback(async () => { + try { + const loaded = await loadDefaultWorkflowAsset(); + if (!loaded) { + setStatus({ text: 'No example workflow found.', level: 'error' }); + return; + } + await applyMaybePackedWorkflow(loaded.workflow); + setStatus({ text: 'Loaded example workflow.', level: 'info' }); + } catch (err: any) { + setStatus({ text: 'Failed to load example workflow: ' + err.message, level: 'error' }); + } + }, [applyMaybePackedWorkflow]); + // ── Load node definitions ─────────────────────────────────────────── useEffect(() => { @@ -2970,6 +2988,19 @@ function Flow() { // ── Keyboard shortcut ─────────────────────────────────────────────── + // Close floating menu on outside click + const floatingMenuRef = useRef(null); + useEffect(() => { + if (!menuOpen) return; + const handler = (e: MouseEvent) => { + if (floatingMenuRef.current && !floatingMenuRef.current.contains(e.target as HTMLElement)) { + setMenuOpen(false); + } + }; + document.addEventListener('pointerdown', handler); + return () => document.removeEventListener('pointerdown', handler); + }, [menuOpen]); + useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { @@ -3172,58 +3203,62 @@ function Flow() { return (
- {/* Toolbar */} -
- tono - -
- - -
- -
- - - - - {window.pywebview && ( - + {menuOpen && ( +
+ - )} -
- -
- - -
- -
{status.text}
+ +
+ + + + + {window.pywebview && ( + + )} +
+ + + + {updateInfo && ( + <> +
+ + ↑ Update to {updateInfo.latest} + + + )} +
+ )}
- {updateInfo && ( -
- tono {updateInfo.latest} is available. - {' '} - Download - -
+ {/* Status toast */} + {status.text && ( +
{status.text}
)} {/* React Flow canvas */} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 05b5b9a..5a5ae56 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -142,33 +142,70 @@ html, body, #root { flex-direction: column; } -/* ── Toolbar ───────────────────────────────────────────────────────── */ -#toolbar { - height: 44px; +/* ── Floating menu ─────────────────────────────────────────────────── */ +.floating-menu { + position: fixed; + top: 12px; + left: 12px; + z-index: 200; + user-select: none; +} + +.floating-menu-toggle { + width: 40px; + height: 40px; + border-radius: 10px; + border: 1px solid var(--border-toolbar); background: var(--bg-toolbar); - border-bottom: 1px solid var(--border-toolbar); + cursor: pointer; display: flex; align-items: center; - padding: 0 12px; - gap: 10px; - z-index: 100; - user-select: none; - flex-shrink: 0; + justify-content: center; + padding: 0; + transition: border-color 0.15s, box-shadow 0.15s; +} +.floating-menu-toggle:hover { + border-color: var(--accent); + box-shadow: 0 0 8px rgba(85, 126, 255, 0.3); } -#app-title { - font-size: 15px; - font-weight: 700; - letter-spacing: 0.5px; - color: var(--text-heading); - margin-right: 8px; - flex-shrink: 0; +.floating-menu-logo { + width: 28px; + height: 28px; + border-radius: 6px; } -.toolbar-group { +.floating-menu-dropdown { + position: absolute; + top: 48px; + left: 0; + background: var(--bg-toolbar); + border: 1px solid var(--border-toolbar); + border-radius: 8px; + padding: 6px; display: flex; - gap: 6px; - flex-shrink: 0; + flex-direction: column; + gap: 3px; + min-width: 150px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); +} + +.floating-menu-dropdown .btn { + width: 100%; + text-align: left; + box-sizing: border-box; +} + +.floating-menu-divider { + border: none; + border-top: 1px solid var(--border-toolbar); + margin: 3px 0; +} + +.floating-menu-update { + text-decoration: none; + display: block; + text-align: left; } /* ── Buttons ───────────────────────────────────────────────────────── */ @@ -200,45 +237,24 @@ html, body, #root { border-color: var(--danger-hover); } -/* ── Status bar ────────────────────────────────────────────────────── */ -.status-bar { - margin-left: auto; - padding: 4px 10px; - border-radius: 4px; +/* ── Status toast ─────────────────────────────────────────────────── */ +.status-toast { + position: fixed; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + padding: 6px 16px; + border-radius: 6px; font-size: 11px; + z-index: 200; + pointer-events: none; + background: var(--bg-toolbar); + border: 1px solid var(--border-toolbar); max-width: 60%; - flex-shrink: 1; -} -.status-bar.info { color: var(--accent-light); } -.status-bar.error { color: var(--error-text); background: var(--error-bg); } - -.update-banner { - background: var(--accent); - color: #fff; text-align: center; - padding: 4px 12px; - font-size: 12px; - position: relative; } -.update-banner a { - color: #fff; - font-weight: 600; - text-decoration: underline; -} -.update-banner-dismiss { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - color: #fff; - cursor: pointer; - font-size: 13px; - padding: 2px 4px; - opacity: 0.7; -} -.update-banner-dismiss:hover { opacity: 1; } +.status-toast.info { color: var(--accent-light); } +.status-toast.error { color: var(--error-text); background: var(--error-bg); } /* ── React Flow container ──────────────────────────────────────────── */ .flow-container {