diff --git a/frontend/src/components/editor/EditorToolbar.css b/frontend/src/components/editor/EditorToolbar.css index 686eaf09..5b72daa6 100644 --- a/frontend/src/components/editor/EditorToolbar.css +++ b/frontend/src/components/editor/EditorToolbar.css @@ -188,6 +188,38 @@ } } +/* ── Progressive overflow ────────────────────────────────────────────── + The overflow menu starts with three items (BOM, Schematic, Firmware) + plus two hidden ones (Import, Export) declared up top. As the toolbar + narrows we drop inline buttons into the menu instead, so mobile users + always reach every action — they just open the "..." menu to find it. + + Selectors are intentionally compound (.tb-overflow-menu .tb-…) to beat + the later-defined `.tb-overflow-item { display: flex }` baseline. + Container width thresholds reflect the editor pane width (toolbar lives + inside it), not the viewport — at a 50/50 desktop split a 1280px + viewport gives the toolbar ~376px to work with. */ +.tb-overflow-menu .tb-overflow-import, +.tb-overflow-menu .tb-overflow-export { + display: none; +} +@container editor-toolbar (max-width: 320px) { + .tb-btn-import-inline { + display: none; + } + .tb-overflow-menu .tb-overflow-import { + display: flex; + } +} +@container editor-toolbar (max-width: 280px) { + .tb-btn-export-inline { + display: none; + } + .tb-overflow-menu .tb-overflow-export { + display: flex; + } +} + /* Legacy lib icon-only button (for overflow items) */ .tb-btn-lib { color: #00b8d4; @@ -427,6 +459,25 @@ color: #00b8d4; } +.tb-overflow-label { + flex: 1; + text-align: left; +} + +/* Pro pill — signals premium items in the overflow menu BEFORE the user + clicks. Used in place of a hidden gate so free users aren't surprised + by an upgrade prompt on what looked like a free button. */ +.tb-overflow-pro { + font-size: 9.5px; + font-weight: 700; + letter-spacing: 0.5px; + padding: 2px 6px; + border-radius: 3px; + background: linear-gradient(135deg, #ffd54f 0%, #ffb300 100%); + color: #1a1a1a; + flex-shrink: 0; +} + /* ── Error detail bar (for long compiler errors) ── */ .toolbar-error-detail { background: #1a0000; diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 586f9be5..21204d4f 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -148,6 +148,26 @@ export const EditorToolbar = ({ const firmwareInputRef = useRef(null); const toolbarRef = useRef(null); const [missingLibHint, setMissingLibHint] = useState(false); + const [moreMenuOpen, setMoreMenuOpen] = useState(false); + const moreMenuRef = useRef(null); + + useEffect(() => { + if (!moreMenuOpen) return; + const onClickOutside = (e: MouseEvent) => { + if (moreMenuRef.current && !moreMenuRef.current.contains(e.target as Node)) { + setMoreMenuOpen(false); + } + }; + const onEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') setMoreMenuOpen(false); + }; + document.addEventListener('mousedown', onClickOutside); + document.addEventListener('keydown', onEsc); + return () => { + document.removeEventListener('mousedown', onClickOutside); + document.removeEventListener('keydown', onEsc); + }; + }, [moreMenuOpen]); // Compile All / Run All — runs sequentially, logs to console (no dialog) const [compileAllRunning, setCompileAllRunning] = useState(false); @@ -897,7 +917,12 @@ export const EditorToolbar = ({ credentials: 'include', }); if (resp.status === 402) { - window.location.href = '/pricing?from=screenshot_export'; + // Fire the in-place upgrade modal instead of bouncing to /pricing — + // keeps the user in the editor with full context. The pro overlay's + // UpgradeGate listens for this event and opens UpgradePromptModal. + window.dispatchEvent(new CustomEvent('velxio-pro-upgrade-prompt', { + detail: { componentName: 'Schematic screenshot export' }, + })); return; } if (resp.status === 401) { @@ -944,11 +969,12 @@ export const EditorToolbar = ({ credentials: 'include', }); if (resp.status === 402) { - // Pro-required. Bounce to /pricing with a hint so the page can - // surface the right upgrade message. (The backend returned a - // structured `detail.error = 'pro_required'` payload but for the - // MVP we just route to pricing.) - window.location.href = '/pricing?from=bom_export'; + // Fire the in-place upgrade modal instead of bouncing to /pricing — + // keeps the user in the editor with full context. The pro overlay's + // UpgradeGate listens for this event and opens UpgradePromptModal. + window.dispatchEvent(new CustomEvent('velxio-pro-upgrade-prompt', { + detail: { componentName: 'BOM export' }, + })); return; } if (resp.status === 401) { @@ -1282,12 +1308,11 @@ export const EditorToolbar = ({ {t('editor.toolbar.libraries.label')} - {/* Import zip — was previously hidden in a 3-dot overflow menu; - inlined since there's space and the discoverability cost - outweighed the toolbar savings. */} + {/* Import zip — inline by default; container query at narrow + widths swaps this for the corresponding overflow-menu item. */} - - - + {/* Overflow "More" menu — collects the secondary actions + (BOM, Schematic image, Upload firmware) so the toolbar no + longer overflows on narrow widths. The two Pro items show + a small "PRO" pill in the menu so users know they're + premium BEFORE clicking, instead of being surprised by an + upgrade prompt. */} +
+ + {moreMenuOpen && ( +
+ {/* Responsive items — hidden by default, shown via + container query when the toolbar is too narrow to + keep their inline twins. Keeps mobile users from + losing access to Import / Export entirely. */} + + + + + +
+ )} +