improved a lot of things
This commit is contained in:
@@ -150,7 +150,8 @@
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\('dependencies',{}\\), indent=2\\)\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('version:', d.get\\('version'\\)\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('type:', d.get\\('type'\\)\\); print\\('error:', d.get\\('error',{}\\).get\\('message','none'\\)[:200]\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); text=next\\(\\(b['text'] for b in d.get\\('content',[]\\) if b['type']=='text'\\),''\\); print\\('STATUS:', d.get\\('type','?'\\)\\); print\\('ERROR:', d.get\\('error',{}\\).get\\('message','none'\\)[:300]\\); print\\('TEXT:', text[:300]\\)\")"
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); text=next\\(\\(b['text'] for b in d.get\\('content',[]\\) if b['type']=='text'\\),''\\); print\\('STATUS:', d.get\\('type','?'\\)\\); print\\('ERROR:', d.get\\('error',{}\\).get\\('message','none'\\)[:300]\\); print\\('TEXT:', text[:300]\\)\")",
|
||||
"Bash(perl -pi -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.22\\\\\\)'\\\\''/color: T.dim/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.18\\\\\\)'\\\\''/color: T.dim/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.28\\\\\\)'\\\\''/color: T.key/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.32\\\\\\)'\\\\''/color: T.key/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.45\\\\\\)'\\\\''/color: T.fgMuted/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.5\\\\\\)'\\\\''/color: T.fgMuted/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.62\\\\\\)'\\\\''/color: T.fgMuted/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.85\\\\\\)'\\\\''/color: T.fg/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.88\\\\\\)'\\\\''/color: T.fg/g;' /Users/USER/Desktop/originmain/packages/app/src/components/inspector/Inspector.tsx)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,45 @@
|
||||
--mono: 'JetBrains Mono', 'SF Mono', ui-monospace, 'Cascadia Code', monospace;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
DARK MODE — flip every semantic token
|
||||
════════════════════════════════════════════════════════════ */
|
||||
[data-theme="dark"] {
|
||||
--bg: #0A0A0A;
|
||||
--bg-subtle: #0F0F11;
|
||||
--bg-muted: #161618;
|
||||
--bg-strong: #1C1C1E;
|
||||
--bg-inv: #F4F4F5;
|
||||
--bg-inv-2: #E4E4E7;
|
||||
--bg-inv-3: #D4D4D8;
|
||||
|
||||
--fg: rgba(255,255,255,0.92);
|
||||
--fg-2: rgba(255,255,255,0.58);
|
||||
--fg-3: rgba(255,255,255,0.34);
|
||||
--fg-4: rgba(255,255,255,0.16);
|
||||
--fg-inv: #09090B;
|
||||
--fg-inv-2: rgba(0,0,0,0.62);
|
||||
--fg-inv-3: rgba(0,0,0,0.35);
|
||||
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--border-2: rgba(255,255,255,0.12);
|
||||
--border-inv: rgba(0,0,0,0.08);
|
||||
--border-inv-2:rgba(0,0,0,0.14);
|
||||
|
||||
--sh-xs: 0 1px 2px rgba(0,0,0,0.3);
|
||||
--sh-sm: 0 1px 3px rgba(0,0,0,0.35), 0 1px 2px rgba(0,0,0,0.25);
|
||||
--sh-md: 0 4px 8px rgba(0,0,0,0.32), 0 2px 4px rgba(0,0,0,0.22);
|
||||
--sh-lg: 0 10px 24px rgba(0,0,0,0.38), 0 4px 8px rgba(0,0,0,0.26);
|
||||
--sh-xl: 0 20px 40px rgba(0,0,0,0.42), 0 8px 16px rgba(0,0,0,0.28);
|
||||
--sh-2xl:0 32px 64px rgba(0,0,0,0.48), 0 16px 32px rgba(0,0,0,0.32);
|
||||
--sh-product: 0 48px 96px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
/* Nav glass adapts in dark mode */
|
||||
[data-theme="dark"] #nav.scrolled {
|
||||
background: rgba(10,10,10,0.88);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
RESET
|
||||
════════════════════════════════════════════════════════════ */
|
||||
@@ -276,6 +315,22 @@ button { font-family: inherit; cursor: pointer; border: none; background: none;
|
||||
}
|
||||
.nav-links a:hover { color: var(--fg); }
|
||||
.nav-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.nav-theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--r-2);
|
||||
background: none;
|
||||
border: 1px solid var(--border-2);
|
||||
color: var(--fg-3);
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-theme-btn:hover { background: var(--bg-muted); color: var(--fg); border-color: var(--border-2); }
|
||||
|
||||
/* ── Hamburger ── */
|
||||
.nav-burger {
|
||||
@@ -1555,6 +1610,7 @@ footer {
|
||||
}
|
||||
.nav-links { display: none; }
|
||||
.nav-actions .btn { display: none; }
|
||||
.nav-actions .nav-theme-btn { display: none; }
|
||||
.nav-burger { display: flex; }
|
||||
.section { padding: 80px 0; }
|
||||
.feat-block { padding: 80px 24px; }
|
||||
@@ -1625,6 +1681,9 @@ footer {
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<button class="nav-theme-btn" id="navThemeBtn" aria-label="Toggle theme" title="Toggle dark mode">
|
||||
<span id="navThemeIcon">☾</span>
|
||||
</button>
|
||||
<a href="/sign-in" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Toggle navigation" aria-expanded="false">
|
||||
@@ -1657,6 +1716,9 @@ footer {
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="/sign-in" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
<button class="nav-theme-btn" id="mobThemeBtn" aria-label="Toggle theme" style="margin-left:auto;">
|
||||
<span id="mobThemeIcon">☾</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -2609,21 +2671,58 @@ annualBtn.addEventListener('click', () => {
|
||||
function safeLS(op, key, val) {
|
||||
try { return op === 'get' ? localStorage.getItem(key) : localStorage.setItem(key, val); } catch(e) {}
|
||||
}
|
||||
|
||||
// Read from either the app Zustand key (set by the React app) or the landing-page key.
|
||||
// Zustand stores { state: { mode: "dark" } } under 'originmain:theme'.
|
||||
function readStoredTheme() {
|
||||
try {
|
||||
const zustand = localStorage.getItem('originmain:theme');
|
||||
if (zustand) {
|
||||
const parsed = JSON.parse(zustand);
|
||||
if (parsed?.state?.mode) return parsed.state.mode;
|
||||
}
|
||||
} catch(e) {}
|
||||
return safeLS('get', 'om-theme') || 'light';
|
||||
}
|
||||
|
||||
// Write to both keys so switching on the landing page carries through to the app.
|
||||
function writeTheme(t) {
|
||||
safeLS('set', 'om-theme', t);
|
||||
try {
|
||||
localStorage.setItem('originmain:theme', JSON.stringify({ state: { mode: t }, version: 0 }));
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
let theme = safeLS('get', 'om-theme') || 'light';
|
||||
let theme = readStoredTheme();
|
||||
|
||||
const themeBtn = document.getElementById('themeToggle');
|
||||
const themeIcon = document.getElementById('themeIcon');
|
||||
const themeLabel = document.getElementById('themeLabel');
|
||||
const navThemeBtn = document.getElementById('navThemeBtn');
|
||||
const navThemeIcon = document.getElementById('navThemeIcon');
|
||||
const mobThemeBtn = document.getElementById('mobThemeBtn');
|
||||
const mobThemeIcon = document.getElementById('mobThemeIcon');
|
||||
|
||||
function applyTheme(t) {
|
||||
theme = t;
|
||||
root.setAttribute('data-theme', t);
|
||||
safeLS('set', 'om-theme', t);
|
||||
themeIcon.textContent = t === 'dark' ? '☀' : '☾';
|
||||
themeLabel.textContent = t === 'dark' ? 'Light mode' : 'Dark mode';
|
||||
writeTheme(t);
|
||||
const isDark = t === 'dark';
|
||||
// Footer toggle
|
||||
if (themeIcon) themeIcon.textContent = isDark ? '☀' : '☾';
|
||||
if (themeLabel) themeLabel.textContent = isDark ? 'Light mode' : 'Dark mode';
|
||||
// Nav toggle (desktop)
|
||||
if (navThemeIcon) navThemeIcon.textContent = isDark ? '☀' : '☾';
|
||||
// Nav toggle (mobile drawer)
|
||||
if (mobThemeIcon) mobThemeIcon.textContent = isDark ? '☀' : '☾';
|
||||
}
|
||||
|
||||
applyTheme(theme);
|
||||
themeBtn.addEventListener('click', () => applyTheme(theme === 'light' ? 'dark' : 'light'));
|
||||
|
||||
if (themeBtn) themeBtn.addEventListener('click', () => applyTheme(theme === 'light' ? 'dark' : 'light'));
|
||||
if (navThemeBtn) navThemeBtn.addEventListener('click', () => applyTheme(theme === 'light' ? 'dark' : 'light'));
|
||||
if (mobThemeBtn) mobThemeBtn.addEventListener('click', () => applyTheme(theme === 'light' ? 'dark' : 'light'));
|
||||
|
||||
// ── Feat cell reveal highlight (Fluent reveal effect) ─────
|
||||
document.querySelectorAll('.feat-cell').forEach(cell => {
|
||||
|
||||
@@ -5,9 +5,11 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useViewport } from '@/store/viewport';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import { useArtboards, createArtboardMutation } from '@/hooks/useArtboards';
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
import { Artboard } from './Artboard';
|
||||
|
||||
export function Canvas() {
|
||||
const T = useCanvasTheme();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const panX = useViewport((s) => s.panX);
|
||||
const panY = useViewport((s) => s.panY);
|
||||
@@ -151,7 +153,8 @@ export function Canvas() {
|
||||
gridRow: 2,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: '#0C0C10',
|
||||
background: T.canvasBg,
|
||||
transition: 'background 0.2s',
|
||||
cursor,
|
||||
}}
|
||||
onMouseDown={onMouseDown}
|
||||
@@ -163,7 +166,7 @@ export function Canvas() {
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.055) 1px, transparent 1px)',
|
||||
backgroundImage: `radial-gradient(circle, ${T.dotColor} 1px, transparent 1px)`,
|
||||
backgroundSize: `${gridSpacing}px ${gridSpacing}px`,
|
||||
backgroundPosition: `${panX % gridSpacing}px ${panY % gridSpacing}px`,
|
||||
pointerEvents: 'none',
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useHistory } from '@/store/history';
|
||||
import { useCanvas, type Tool } from '@/store/canvas';
|
||||
import { useViewport } from '@/store/viewport';
|
||||
import { useTheme } from '@/store/theme';
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
|
||||
interface AppChromeProps {
|
||||
workspaceId?: string;
|
||||
@@ -24,6 +25,7 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
|
||||
const setContext = useCanvas((s) => s.setContext);
|
||||
const setActiveTool = useCanvas((s) => s.setActiveTool);
|
||||
const { mode: themeMode, toggle: toggleTheme } = useTheme();
|
||||
const CT = useCanvasTheme();
|
||||
|
||||
// Push workspace/project IDs into the store so Canvas and Navigator can read them.
|
||||
useEffect(() => {
|
||||
@@ -99,7 +101,8 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
|
||||
flexDirection: 'column',
|
||||
height: '100dvh',
|
||||
overflow: 'hidden',
|
||||
background: '#0C0C10',
|
||||
background: CT.canvasBg,
|
||||
transition: 'background 0.2s',
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
@@ -108,8 +111,8 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
|
||||
<div style={{
|
||||
height: 36,
|
||||
flexShrink: 0,
|
||||
background: '#0A0A0E',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||
background: CT.bgDeep,
|
||||
borderBottom: `1px solid ${CT.border}`,
|
||||
display: 'flex', alignItems: 'center',
|
||||
padding: '0 14px', gap: 0,
|
||||
zIndex: 10,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
ToolbarButton,
|
||||
ToolbarDivider,
|
||||
Tooltip,
|
||||
} from '@fluentui/react-components';
|
||||
@@ -17,24 +16,10 @@ import {
|
||||
} from '@fluentui/react-icons';
|
||||
import { useCanvas, type Tool } from '@/store/canvas';
|
||||
import { useViewport } from '@/store/viewport';
|
||||
|
||||
// Design tokens — Fluent dark surface
|
||||
const T = {
|
||||
bg: '#141418',
|
||||
border: 'rgba(255,255,255,0.06)',
|
||||
sep: 'rgba(255,255,255,0.07)',
|
||||
fg: 'rgba(255,255,255,0.88)',
|
||||
fgMuted: 'rgba(255,255,255,0.38)',
|
||||
fgDim: 'rgba(255,255,255,0.22)',
|
||||
accent: '#3385FF',
|
||||
accentBg: 'rgba(51,133,255,0.14)',
|
||||
activeBg: 'rgba(255,255,255,0.07)',
|
||||
live: '#10B981',
|
||||
liveBg: 'rgba(16,185,129,0.1)',
|
||||
liveBorder:'rgba(16,185,129,0.2)',
|
||||
};
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
|
||||
export function Toolbar() {
|
||||
const T = useCanvasTheme();
|
||||
const { activeTool, setActiveTool } = useCanvas();
|
||||
const zoom = useViewport((s) => s.zoom);
|
||||
const setZoom = useViewport((s) => s.setZoom);
|
||||
@@ -53,6 +38,7 @@ export function Toolbar() {
|
||||
gap: 2,
|
||||
height: 44,
|
||||
userSelect: 'none',
|
||||
transition: 'background 0.2s, border-color 0.2s',
|
||||
}}
|
||||
>
|
||||
{/* Wordmark */}
|
||||
@@ -66,54 +52,55 @@ export function Toolbar() {
|
||||
padding: '0 10px 0 4px',
|
||||
marginRight: 2,
|
||||
flexShrink: 0,
|
||||
transition: 'color 0.2s',
|
||||
}}
|
||||
>
|
||||
Om<span style={{ color: T.accent }}>•</span>
|
||||
</span>
|
||||
|
||||
<Sep />
|
||||
<Sep T={T} />
|
||||
|
||||
{/* Selection tools */}
|
||||
<ToolGroup>
|
||||
<Tooltip content="Select V" relationship="label">
|
||||
<TBtn active={activeTool === 'select'} onClick={() => setActiveTool('select')}>
|
||||
<TBtn T={T} active={activeTool === 'select'} onClick={() => setActiveTool('select')}>
|
||||
<CursorClickRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
<Tooltip content="Pan H" relationship="label">
|
||||
<TBtn active={activeTool === 'pan'} onClick={() => setActiveTool('pan')}>
|
||||
<TBtn T={T} active={activeTool === 'pan'} onClick={() => setActiveTool('pan')}>
|
||||
<HandLeftRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
</ToolGroup>
|
||||
|
||||
<Sep />
|
||||
<Sep T={T} />
|
||||
|
||||
{/* Create tools */}
|
||||
<ToolGroup>
|
||||
<Tooltip content="New Artboard A" relationship="label">
|
||||
<TBtn active={activeTool === 'artboard'} onClick={() => setActiveTool('artboard')}>
|
||||
<TBtn T={T} active={activeTool === 'artboard'} onClick={() => setActiveTool('artboard')}>
|
||||
<SquareRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
<Tooltip content="Completion Zone Z" relationship="label">
|
||||
<TBtn active={activeTool === 'zone'} onClick={() => setActiveTool('zone')}>
|
||||
<TBtn T={T} active={activeTool === 'zone'} onClick={() => setActiveTool('zone')}>
|
||||
<SelectObjectRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
</ToolGroup>
|
||||
|
||||
<Sep />
|
||||
<Sep T={T} />
|
||||
|
||||
{/* View tools */}
|
||||
<ToolGroup>
|
||||
<Tooltip content="Intent Diff" relationship="label">
|
||||
<TBtn active={false} onClick={() => {}}>
|
||||
<TBtn T={T} active={false} onClick={() => {}}>
|
||||
<DataTrendingRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
<Tooltip content="Origin Graph" relationship="label">
|
||||
<TBtn active={false} onClick={() => {}}>
|
||||
<TBtn T={T} active={false} onClick={() => {}}>
|
||||
<CircleHintHalfVerticalRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
@@ -122,7 +109,7 @@ export function Toolbar() {
|
||||
{/* Right side */}
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Tooltip content="Zoom out" relationship="label">
|
||||
<TBtn active={false} onClick={() => setZoom(zoom * 0.8)}>
|
||||
<TBtn T={T} active={false} onClick={() => setZoom(zoom * 0.8)}>
|
||||
<ZoomOutRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
@@ -130,7 +117,7 @@ export function Toolbar() {
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
background: T.activeBg,
|
||||
border: `1px solid ${T.sep}`,
|
||||
borderRadius: 5,
|
||||
padding: '3px 9px',
|
||||
@@ -144,11 +131,11 @@ export function Toolbar() {
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.09)';
|
||||
(e.currentTarget as HTMLButtonElement).style.background = T.hoverBg;
|
||||
(e.currentTarget as HTMLButtonElement).style.color = T.fg;
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.05)';
|
||||
(e.currentTarget as HTMLButtonElement).style.background = T.activeBg;
|
||||
(e.currentTarget as HTMLButtonElement).style.color = T.fgMuted;
|
||||
}}
|
||||
>
|
||||
@@ -156,12 +143,12 @@ export function Toolbar() {
|
||||
</button>
|
||||
|
||||
<Tooltip content="Zoom in" relationship="label">
|
||||
<TBtn active={false} onClick={() => setZoom(zoom * 1.25)}>
|
||||
<TBtn T={T} active={false} onClick={() => setZoom(zoom * 1.25)}>
|
||||
<ZoomInRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
|
||||
<Sep />
|
||||
<Sep T={T} />
|
||||
|
||||
{/* Live status pill */}
|
||||
<div
|
||||
@@ -175,17 +162,10 @@ export function Toolbar() {
|
||||
border: `1px solid ${T.liveBorder}`,
|
||||
marginLeft: 2,
|
||||
marginRight: 4,
|
||||
transition: 'background 0.2s, border-color 0.2s',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: T.live,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: T.live, flexShrink: 0 }} />
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
@@ -202,33 +182,33 @@ export function Toolbar() {
|
||||
);
|
||||
}
|
||||
|
||||
function Sep() {
|
||||
import type { CanvasTokens } from '@/store/canvasTheme';
|
||||
|
||||
function Sep({ T }: { T: CanvasTokens }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 1,
|
||||
height: 20,
|
||||
background: 'rgba(255,255,255,0.07)',
|
||||
width: 1, height: 20,
|
||||
background: T.sep,
|
||||
margin: '0 5px',
|
||||
flexShrink: 0,
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolGroup({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div style={{ display: 'flex', alignItems: 'center', gap: 1 }}>{children}</div>;
|
||||
}
|
||||
|
||||
function TBtn({
|
||||
T,
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
T: CanvasTokens;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
@@ -237,12 +217,11 @@ function TBtn({
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
width: 32, height: 32,
|
||||
borderRadius: 6,
|
||||
border: 'none',
|
||||
background: active ? 'rgba(51,133,255,0.15)' : 'transparent',
|
||||
color: active ? '#3385FF' : 'rgba(255,255,255,0.42)',
|
||||
background: active ? T.accentBg : 'transparent',
|
||||
color: active ? T.accent : T.item,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -253,14 +232,14 @@ function TBtn({
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
if (!active) {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.07)';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'rgba(255,255,255,0.75)';
|
||||
(e.currentTarget as HTMLButtonElement).style.background = T.activeBg;
|
||||
(e.currentTarget as HTMLButtonElement).style.color = T.itemHov;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (!active) {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'rgba(255,255,255,0.42)';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = T.item;
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useHistory } from '@/store/history';
|
||||
import { useArtboards, patchArtboard } from '@/hooks/useArtboards';
|
||||
import { useDiffs } from '@/hooks/useDiffs';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
import type { PropChange } from '@originmain/diff-engine';
|
||||
import type { FiberNode } from '@originmain/renderer';
|
||||
import type { Artboard, IntentDiff } from '@originmain/origin-graph';
|
||||
@@ -18,20 +19,8 @@ const TYPE_COLORS: Record<string, string> = {
|
||||
|
||||
type TabId = 'props' | 'diff' | 'graph';
|
||||
|
||||
// Dark panel tokens
|
||||
const T = {
|
||||
bg: '#111115',
|
||||
border: 'rgba(255,255,255,0.055)',
|
||||
sep: 'rgba(255,255,255,0.04)',
|
||||
label: 'rgba(255,255,255,0.22)',
|
||||
key: 'rgba(255,255,255,0.32)',
|
||||
dim: 'rgba(255,255,255,0.18)',
|
||||
accent: '#3385FF',
|
||||
tabFg: 'rgba(255,255,255,0.28)',
|
||||
tabOn: 'rgba(255,255,255,0.88)',
|
||||
};
|
||||
|
||||
export function Inspector() {
|
||||
const T = useCanvasTheme();
|
||||
const { selectedArtboardId, liveArtboardIds, artboardFiberRoots, selectedComponentId, selectedComponentData, workspaceId, projectId } = useCanvas();
|
||||
const [tab, setTab] = useState<TabId>('props');
|
||||
const { rawArtboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined);
|
||||
@@ -98,7 +87,7 @@ export function Inspector() {
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.625rem',
|
||||
color: 'rgba(255,255,255,0.22)',
|
||||
color: T.dim,
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
>
|
||||
@@ -123,7 +112,7 @@ export function Inspector() {
|
||||
<div
|
||||
style={{
|
||||
height: 27,
|
||||
background: '#0D0D11',
|
||||
background: T.bgDeep,
|
||||
borderTop: `1px solid ${T.sep}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -134,14 +123,14 @@ export function Inspector() {
|
||||
>
|
||||
<div style={{
|
||||
width: 6, height: 6, borderRadius: '50%', flexShrink: 0,
|
||||
background: isLive ? '#10B981' : 'rgba(255,255,255,0.15)',
|
||||
boxShadow: isLive ? '0 0 6px rgba(16,185,129,0.6)' : 'none',
|
||||
background: isLive ? T.live : T.activeBg,
|
||||
boxShadow: isLive ? `0 0 6px ${T.liveBorder}` : 'none',
|
||||
transition: 'background 0.3s, box-shadow 0.3s',
|
||||
}} />
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.5625rem', color: 'rgba(255,255,255,0.45)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.5625rem', color: T.fgMuted }}>
|
||||
{isLive ? 'Live render connected' : selectedArtboardId ? 'No render — set URL in Props' : 'No artboard selected'}
|
||||
</span>
|
||||
<span style={{ marginLeft: 'auto', fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.5625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ marginLeft: 'auto', fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.5625rem', color: T.dim }}>
|
||||
{selectedArtboard
|
||||
? `${selectedArtboard.metadata_jsonb['width'] ?? '?'} × ${selectedArtboard.metadata_jsonb['height'] ?? '?'}`
|
||||
: '—'}
|
||||
@@ -163,6 +152,7 @@ function PropsTab({
|
||||
workspaceId: string | null;
|
||||
projectId: string | null;
|
||||
}) {
|
||||
const T = useCanvasTheme();
|
||||
const queryClient = useQueryClient();
|
||||
const [editingUrl, setEditingUrl] = useState(false);
|
||||
const [urlDraft, setUrlDraft] = useState('');
|
||||
@@ -244,7 +234,7 @@ function PropsTab({
|
||||
return <PropRow key={k} label={k} value={display} color={color} />;
|
||||
})}
|
||||
{Object.keys(selectedComponentData.props ?? {}).length === 0 && (
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
No props
|
||||
</span>
|
||||
)}
|
||||
@@ -273,7 +263,7 @@ function PropsTab({
|
||||
|
||||
<Section label="Render Target">
|
||||
<PropRow label="name" value={artboard.name} color="#7EB8FF" />
|
||||
<PropRow label="id" value={artboard.id.slice(0, 8) + '…'} color="rgba(255,255,255,0.28)" />
|
||||
<PropRow label="id" value={artboard.id.slice(0, 8) + '…'} color={T.key} />
|
||||
|
||||
{/* renderUrl — inline editable */}
|
||||
|
||||
@@ -308,8 +298,8 @@ function PropsTab({
|
||||
placeholder="http://localhost:3000"
|
||||
style={{
|
||||
flex: 1, fontSize: '0.5875rem', fontFamily: "'JetBrains Mono', monospace",
|
||||
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.12)',
|
||||
borderRadius: 5, padding: '4px 8px', color: 'rgba(255,255,255,0.85)',
|
||||
background: T.bgDeep, border: `1px solid ${T.border}`,
|
||||
borderRadius: 5, padding: '4px 8px', color: T.fg,
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
@@ -333,7 +323,7 @@ function PropsTab({
|
||||
{renderUrl}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.18)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
not connected
|
||||
</span>
|
||||
)}
|
||||
@@ -351,10 +341,10 @@ function PropsTab({
|
||||
width: '100%',
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.5875rem',
|
||||
background: driftStatus === 'loading' ? 'rgba(255,255,255,0.06)' : 'rgba(51,133,255,0.12)',
|
||||
border: `1px solid ${driftStatus === 'loading' ? 'rgba(255,255,255,0.08)' : 'rgba(51,133,255,0.25)'}`,
|
||||
background: driftStatus === 'loading' ? T.activeBg : T.accentBg,
|
||||
border: `1px solid ${driftStatus === 'loading' ? T.border : T.accent}`,
|
||||
borderRadius: 6, padding: '6px 0',
|
||||
color: driftStatus === 'loading' ? 'rgba(255,255,255,0.35)' : T.accent,
|
||||
color: driftStatus === 'loading' ? T.fgDim : T.accent,
|
||||
cursor: driftStatus === 'loading' ? 'wait' : 'pointer',
|
||||
letterSpacing: '0.04em',
|
||||
transition: 'background 0.15s, border-color 0.15s, color 0.15s',
|
||||
@@ -373,19 +363,19 @@ function PropsTab({
|
||||
<div style={{
|
||||
marginTop: 8,
|
||||
padding: '8px 10px',
|
||||
background: 'rgba(0,0,0,0.35)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
background: T.bgDeep,
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 6,
|
||||
maxHeight: 220,
|
||||
overflowY: 'auto',
|
||||
fontSize: '0.5875rem',
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
color: 'rgba(255,255,255,0.62)',
|
||||
color: T.fgMuted,
|
||||
lineHeight: 1.65,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(255,255,255,0.18) transparent',
|
||||
scrollbarColor: `${T.dim} transparent`,
|
||||
} as React.CSSProperties}>
|
||||
{driftReport}
|
||||
</div>
|
||||
@@ -396,6 +386,7 @@ function PropsTab({
|
||||
}
|
||||
|
||||
function Section({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
const T = useCanvasTheme();
|
||||
return (
|
||||
<div style={{ padding: '12px 14px' }}>
|
||||
<div
|
||||
@@ -417,6 +408,7 @@ function Section({ label, children }: { label: string; children: React.ReactNode
|
||||
}
|
||||
|
||||
function PropRow({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
const T = useCanvasTheme();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -449,10 +441,11 @@ function PropRow({ label, value, color }: { label: string; value: string; color:
|
||||
}
|
||||
|
||||
function GraphNode({ label, depth, isRoot }: { label: string; depth: number; isRoot?: boolean }) {
|
||||
const T = useCanvasTheme();
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, paddingLeft: depth * 14, marginBottom: 6 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: isRoot ? T.accent : 'rgba(255,255,255,0.18)', flexShrink: 0 }} />
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: isRoot ? T.accent : 'rgba(255,255,255,0.5)', letterSpacing: '-0.01em' }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: isRoot ? T.accent : T.dim, flexShrink: 0 }} />
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: isRoot ? T.accent : T.fgMuted, letterSpacing: '-0.01em' }}>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -461,10 +454,11 @@ function GraphNode({ label, depth, isRoot }: { label: string; depth: number; isR
|
||||
|
||||
/* ── Graph tab ────────────────────────────────────────────── */
|
||||
function GraphTab({ fiberRoot }: { fiberRoot: FiberNode | undefined }) {
|
||||
const T = useCanvasTheme();
|
||||
if (!fiberRoot) {
|
||||
return (
|
||||
<Section label="Origin Graph">
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
No live render — connect a URL in Props to see the fiber tree
|
||||
</span>
|
||||
</Section>
|
||||
@@ -487,6 +481,7 @@ function GraphTab({ fiberRoot }: { fiberRoot: FiberNode | undefined }) {
|
||||
}
|
||||
|
||||
function FiberTreeView({ node, depth }: { node: FiberNode; depth: number }) {
|
||||
const T = useCanvasTheme();
|
||||
const [collapsed, setCollapsed] = useState(depth > 2);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
@@ -502,17 +497,17 @@ function FiberTreeView({ node, depth }: { node: FiberNode; depth: number }) {
|
||||
>
|
||||
<div style={{
|
||||
width: 5, height: 5, borderRadius: '50%', flexShrink: 0,
|
||||
background: depth === 0 ? T.accent : 'rgba(255,255,255,0.2)',
|
||||
background: depth === 0 ? T.accent : T.dim,
|
||||
}} />
|
||||
{hasChildren && (
|
||||
<span style={{ fontSize: '0.5rem', color: 'rgba(255,255,255,0.3)', marginRight: -2 }}>
|
||||
<span style={{ fontSize: '0.5rem', color: T.fgDim, marginRight: -2 }}>
|
||||
{collapsed ? '▶' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.625rem',
|
||||
color: depth === 0 ? T.accent : 'rgba(255,255,255,0.55)',
|
||||
color: depth === 0 ? T.accent : T.fgMuted,
|
||||
letterSpacing: '-0.01em',
|
||||
}}>
|
||||
{node.name}
|
||||
@@ -535,11 +530,13 @@ function measureFiberDepth(node: FiberNode, d = 0): number {
|
||||
}
|
||||
|
||||
function HSep() {
|
||||
return <div style={{ height: 1, background: 'rgba(255,255,255,0.04)', margin: '2px 0' }} />;
|
||||
const T = useCanvasTheme();
|
||||
return <div style={{ height: 1, background: T.sep, margin: '2px 0' }} />;
|
||||
}
|
||||
|
||||
/* ── Diff tab ─────────────────────────────────────────────── */
|
||||
function DiffTab({ artboardId }: { artboardId: string | null }) {
|
||||
const T = useCanvasTheme();
|
||||
const { stacks } = useHistory();
|
||||
const { diffs, createDiff, isLoading } = useDiffs(artboardId);
|
||||
const [summaryStatus, setSummaryStatus] = useState<'idle' | 'summarising' | 'exporting'>('idle');
|
||||
@@ -588,7 +585,7 @@ function DiffTab({ artboardId }: { artboardId: string | null }) {
|
||||
if (!artboardId) {
|
||||
return (
|
||||
<Section label="Intent Diff">
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
No artboard selected
|
||||
</span>
|
||||
</Section>
|
||||
@@ -631,7 +628,7 @@ function DiffTab({ artboardId }: { artboardId: string | null }) {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
No pending changes
|
||||
</span>
|
||||
)}
|
||||
@@ -642,11 +639,11 @@ function DiffTab({ artboardId }: { artboardId: string | null }) {
|
||||
{/* Saved diffs from DB */}
|
||||
<Section label="Exported">
|
||||
{isLoading ? (
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
Loading…
|
||||
</span>
|
||||
) : diffs.length === 0 ? (
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
No exported diffs yet
|
||||
</span>
|
||||
) : (
|
||||
@@ -665,21 +662,22 @@ const STATUS_COLOR: Record<string, string> = {
|
||||
};
|
||||
|
||||
function SavedDiffRow({ diff }: { diff: IntentDiff }) {
|
||||
const T = useCanvasTheme();
|
||||
const changes = diff.changes_jsonb as { propChanges?: PropChange[]; styleChanges?: PropChange[] } | null;
|
||||
const count = (changes?.propChanges?.length ?? 0) + (changes?.styleChanges?.length ?? 0);
|
||||
const color = STATUS_COLOR[diff.status] ?? T.dim;
|
||||
return (
|
||||
<div style={{ marginBottom: 8, padding: '6px 8px', background: 'rgba(255,255,255,0.025)', borderRadius: 6 }}>
|
||||
<div style={{ marginBottom: 8, padding: '6px 8px', background: T.bgDeep, borderRadius: 6 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 2 }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color }}>
|
||||
{diff.status}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5rem', color: T.dim }}>
|
||||
{count} change{count !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{diff.summary && (
|
||||
<span style={{ fontFamily: 'sans-serif', fontSize: '0.625rem', color: 'rgba(255,255,255,0.45)', lineHeight: 1.4, display: 'block' }}>
|
||||
<span style={{ fontFamily: 'sans-serif', fontSize: '0.625rem', color: T.fgMuted, lineHeight: 1.4, display: 'block' }}>
|
||||
{diff.summary}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -7,39 +7,43 @@ import { SquareRegular } from '@fluentui/react-icons';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import { useArtboards, patchArtboard, createArtboardMutation } from '@/hooks/useArtboards';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
const T = {
|
||||
bg: '#111115',
|
||||
border: 'rgba(255,255,255,0.055)',
|
||||
item: 'rgba(255,255,255,0.42)',
|
||||
itemHov: 'rgba(255,255,255,0.72)',
|
||||
selBg: 'rgba(51,133,255,0.12)',
|
||||
selFg: 'rgba(255,255,255,0.88)',
|
||||
accent: '#3385FF',
|
||||
dim: 'rgba(255,255,255,0.22)',
|
||||
sep: 'rgba(255,255,255,0.04)',
|
||||
};
|
||||
|
||||
// Map the panel's dark theme into Trees' CSS custom properties
|
||||
const treeThemeStyles = themeToTreeStyles({
|
||||
type: 'dark',
|
||||
bg: '#111115',
|
||||
fg: 'rgba(255,255,255,0.42)',
|
||||
colors: {
|
||||
'editor.selectionBackground': 'rgba(51,133,255,0.14)',
|
||||
'list.activeSelectionBackground': 'rgba(51,133,255,0.14)',
|
||||
'list.inactiveSelectionBackground': 'rgba(51,133,255,0.08)',
|
||||
'list.hoverBackground': 'rgba(255,255,255,0.04)',
|
||||
'list.activeSelectionForeground': 'rgba(255,255,255,0.88)',
|
||||
'editorIndentGuide.background': 'rgba(255,255,255,0.04)',
|
||||
},
|
||||
});
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
import { useTheme } from '@/store/theme';
|
||||
|
||||
export function ArtboardNavigator() {
|
||||
const T = useCanvasTheme();
|
||||
const mode = useTheme((s) => s.mode);
|
||||
const { selectedArtboardId, selectArtboard, workspaceId, projectId, liveArtboardIds, artboardFiberRoots } = useCanvas();
|
||||
const { artboards, rawArtboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Map the panel theme into Trees' CSS custom properties — recomputed when mode changes
|
||||
const treeThemeStyles = themeToTreeStyles(mode === 'dark' ? {
|
||||
type: 'dark',
|
||||
bg: T.bg,
|
||||
fg: T.item,
|
||||
colors: {
|
||||
'editor.selectionBackground': T.accentBg,
|
||||
'list.activeSelectionBackground': T.accentBg,
|
||||
'list.inactiveSelectionBackground': T.selBg,
|
||||
'list.hoverBackground': T.hoverBg,
|
||||
'list.activeSelectionForeground': T.selFg,
|
||||
'editorIndentGuide.background': T.sep,
|
||||
},
|
||||
} : {
|
||||
type: 'light',
|
||||
bg: T.bg,
|
||||
fg: T.item,
|
||||
colors: {
|
||||
'editor.selectionBackground': T.accentBg,
|
||||
'list.activeSelectionBackground': T.accentBg,
|
||||
'list.inactiveSelectionBackground': T.selBg,
|
||||
'list.hoverBackground': T.hoverBg,
|
||||
'list.activeSelectionForeground': T.selFg,
|
||||
'editorIndentGuide.background': T.sep,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteArtboard = useCallback(async (id: string, name: string) => {
|
||||
if (!window.confirm(`Delete "${name}"?`)) return;
|
||||
try {
|
||||
@@ -126,12 +130,13 @@ export function ArtboardNavigator() {
|
||||
return (
|
||||
<NavRow
|
||||
key={ab.id}
|
||||
T={T}
|
||||
selected={sel}
|
||||
live={live}
|
||||
onClick={() => selectArtboard(ab.id)}
|
||||
icon={
|
||||
<SquareRegular
|
||||
style={{ fontSize: 11, color: sel ? T.accent : 'rgba(255,255,255,0.2)', flexShrink: 0 }}
|
||||
style={{ fontSize: 11, color: sel ? T.accent : T.dim, flexShrink: 0 }}
|
||||
/>
|
||||
}
|
||||
label={ab.label}
|
||||
@@ -167,8 +172,8 @@ export function ArtboardNavigator() {
|
||||
<SectionLabel>Graph</SectionLabel>
|
||||
<div style={{ padding: '4px 14px 8px', display: 'flex', flexDirection: 'column', gap: 6, flexShrink: 0 }}>
|
||||
<GraphStat label="artboards" value={String(rawArtboards.length)} color={T.accent} />
|
||||
<GraphStat label="live" value={liveCount > 0 ? String(liveCount) : '—'} color={liveCount > 0 ? '#10B981' : 'rgba(255,255,255,0.25)'} />
|
||||
<GraphStat label="components" value={totalComponents > 0 ? String(totalComponents) : '—'} color="rgba(255,255,255,0.45)" />
|
||||
<GraphStat label="live" value={liveCount > 0 ? String(liveCount) : '—'} color={liveCount > 0 ? T.live : T.fgDim} />
|
||||
<GraphStat label="components" value={totalComponents > 0 ? String(totalComponents) : '—'} color={T.fgMuted} />
|
||||
</div>
|
||||
|
||||
{/* ── Cross-artboard query ── */}
|
||||
@@ -178,7 +183,10 @@ export function ArtboardNavigator() {
|
||||
}
|
||||
|
||||
/* ── Artboard row ─────────────────────────────────────────── */
|
||||
import type { CanvasTokens } from '@/store/canvasTheme';
|
||||
|
||||
function NavRow({
|
||||
T,
|
||||
selected = false,
|
||||
live = false,
|
||||
onClick,
|
||||
@@ -188,6 +196,7 @@ function NavRow({
|
||||
onFork,
|
||||
onDelete,
|
||||
}: {
|
||||
T: CanvasTokens;
|
||||
selected?: boolean;
|
||||
live?: boolean;
|
||||
onClick?: () => void;
|
||||
@@ -211,7 +220,7 @@ function NavRow({
|
||||
padding: '4px 6px 4px 10px',
|
||||
borderRadius: 5,
|
||||
cursor: 'pointer',
|
||||
background: selected ? T.selBg : hov ? 'rgba(255,255,255,0.04)' : 'transparent',
|
||||
background: selected ? T.selBg : hov ? T.hoverBg : 'transparent',
|
||||
color: selected ? T.selFg : hov ? T.itemHov : T.item,
|
||||
fontSize: '0.75rem',
|
||||
letterSpacing: '-0.01em',
|
||||
@@ -228,8 +237,8 @@ function NavRow({
|
||||
{live && !hov && (
|
||||
<span style={{
|
||||
width: 5, height: 5, borderRadius: '50%',
|
||||
background: '#10B981', flexShrink: 0, display: 'block',
|
||||
boxShadow: '0 0 4px rgba(16,185,129,0.8)',
|
||||
background: T.live, flexShrink: 0, display: 'block',
|
||||
boxShadow: `0 0 4px ${T.liveBorder}`,
|
||||
}} />
|
||||
)}
|
||||
|
||||
@@ -237,16 +246,14 @@ function NavRow({
|
||||
{(hov || selected) && (
|
||||
<div style={{ display: 'flex', gap: 2, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
|
||||
{onRename && (
|
||||
<IconBtn title="Rename" onClick={onRename}>
|
||||
{/* Pencil */}
|
||||
<IconBtn T={T} title="Rename" onClick={onRename}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M1 7.5L7 1.5l1.5 1.5-6 6H1V7.5z" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</IconBtn>
|
||||
)}
|
||||
{onFork && (
|
||||
<IconBtn title="Fork" onClick={onFork}>
|
||||
{/* Branch / fork icon */}
|
||||
<IconBtn T={T} title="Fork" onClick={onFork}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<circle cx="2" cy="2" r="1.2" stroke="currentColor" strokeWidth="1"/>
|
||||
<circle cx="8" cy="2" r="1.2" stroke="currentColor" strokeWidth="1"/>
|
||||
@@ -256,8 +263,7 @@ function NavRow({
|
||||
</IconBtn>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconBtn title="Delete" onClick={onDelete} danger>
|
||||
{/* Trash */}
|
||||
<IconBtn T={T} title="Delete" onClick={onDelete} danger>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M2 2.5h6M4 2.5V1.5h2V2.5M3 2.5v6h4v-6" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
@@ -269,7 +275,7 @@ function NavRow({
|
||||
);
|
||||
}
|
||||
|
||||
function IconBtn({ children, title, onClick, danger }: { children: React.ReactNode; title: string; onClick: () => void; danger?: boolean }) {
|
||||
function IconBtn({ T, children, title, onClick, danger }: { T: CanvasTokens; children: React.ReactNode; title: string; onClick: () => void; danger?: boolean }) {
|
||||
const [hov, setHov] = useState(false);
|
||||
return (
|
||||
<button
|
||||
@@ -278,10 +284,10 @@ function IconBtn({ children, title, onClick, danger }: { children: React.ReactNo
|
||||
onMouseEnter={() => setHov(true)}
|
||||
onMouseLeave={() => setHov(false)}
|
||||
style={{
|
||||
background: hov ? (danger ? 'rgba(255,80,80,0.15)' : 'rgba(255,255,255,0.08)') : 'none',
|
||||
background: hov ? (danger ? 'rgba(255,80,80,0.15)' : T.activeBg) : 'none',
|
||||
border: 'none', borderRadius: 3, padding: '2px 3px',
|
||||
cursor: 'pointer',
|
||||
color: hov ? (danger ? '#FF6060' : 'rgba(255,255,255,0.8)') : 'rgba(255,255,255,0.3)',
|
||||
color: hov ? (danger ? '#FF6060' : T.fg) : T.dim,
|
||||
transition: 'background 0.1s, color 0.1s',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
@@ -293,6 +299,7 @@ function IconBtn({ children, title, onClick, danger }: { children: React.ReactNo
|
||||
|
||||
/* ── Helpers ──────────────────────────────────────────────── */
|
||||
function SectionLabel({ children }: { children: string }) {
|
||||
const T = useCanvasTheme();
|
||||
return (
|
||||
<div style={{
|
||||
padding: '12px 12px 5px',
|
||||
@@ -311,27 +318,28 @@ function SectionLabel({ children }: { children: string }) {
|
||||
}
|
||||
|
||||
function HSep() {
|
||||
const T = useCanvasTheme();
|
||||
return <div style={{ height: 1, background: T.sep, margin: '6px 0', flexShrink: 0 }} />;
|
||||
}
|
||||
|
||||
function ActiveDot() {
|
||||
const T = useCanvasTheme();
|
||||
return (
|
||||
<span style={{
|
||||
marginLeft: 'auto',
|
||||
width: 5,
|
||||
height: 5,
|
||||
width: 5, height: 5,
|
||||
borderRadius: '50%',
|
||||
background: T.accent,
|
||||
flexShrink: 0,
|
||||
display: 'block',
|
||||
flexShrink: 0, display: 'block',
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
function GraphStat({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
const T = useCanvasTheme();
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.28)' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: T.dim }}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color, fontWeight: 600 }}>
|
||||
@@ -350,6 +358,7 @@ function countFiberNodes(node: { children?: unknown[] }): number {
|
||||
|
||||
/* ── Cross-artboard query ─────────────────────────────────── */
|
||||
function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) {
|
||||
const T = useCanvasTheme();
|
||||
const [query, setQuery] = useState('');
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle');
|
||||
const [answer, setAnswer] = useState('');
|
||||
@@ -392,11 +401,11 @@ function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) {
|
||||
}}
|
||||
placeholder="Ask across artboards…"
|
||||
style={{
|
||||
flex: 1, background: 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
flex: 1, background: T.bgDeep,
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 5, padding: '5px 8px',
|
||||
fontSize: '0.5875rem', fontFamily: 'inherit',
|
||||
color: 'rgba(255,255,255,0.75)', outline: 'none',
|
||||
color: T.fg, outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@@ -415,9 +424,9 @@ function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) {
|
||||
{status === 'done' && answer && (
|
||||
<div style={{
|
||||
marginTop: 6, padding: '6px 8px',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
background: T.bgDeep,
|
||||
borderRadius: 5, fontSize: '0.5875rem',
|
||||
color: 'rgba(255,255,255,0.6)', lineHeight: 1.55,
|
||||
color: T.fgMuted, lineHeight: 1.55,
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
maxHeight: 120, overflow: 'auto',
|
||||
}}>
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { useTheme } from './theme';
|
||||
|
||||
/**
|
||||
* Shared design tokens for every canvas panel (Toolbar, ArtboardNavigator,
|
||||
* Inspector, Canvas). Toggling the theme store flips all tokens at once.
|
||||
*/
|
||||
export interface CanvasTokens {
|
||||
// Panel surfaces
|
||||
bg: string;
|
||||
bgDeep: string; // slightly deeper than bg (inner cards, inputs)
|
||||
border: string;
|
||||
sep: string;
|
||||
// Text
|
||||
fg: string;
|
||||
fgMuted: string;
|
||||
fgDim: string;
|
||||
label: string;
|
||||
key: string;
|
||||
dim: string;
|
||||
// Interactive
|
||||
accent: string;
|
||||
accentBg: string;
|
||||
activeBg: string;
|
||||
hoverBg: string;
|
||||
selBg: string;
|
||||
selFg: string;
|
||||
// List items
|
||||
item: string;
|
||||
itemHov: string;
|
||||
// Tabs
|
||||
tabFg: string;
|
||||
tabOn: string;
|
||||
// Live indicator
|
||||
live: string;
|
||||
liveBg: string;
|
||||
liveBorder: string;
|
||||
// Canvas drawing area
|
||||
canvasBg: string;
|
||||
dotColor: string; // grid dot colour
|
||||
}
|
||||
|
||||
export const DARK_TOKENS: CanvasTokens = {
|
||||
bg: '#111115',
|
||||
bgDeep: '#0D0D11',
|
||||
border: 'rgba(255,255,255,0.055)',
|
||||
sep: 'rgba(255,255,255,0.04)',
|
||||
fg: 'rgba(255,255,255,0.88)',
|
||||
fgMuted: 'rgba(255,255,255,0.38)',
|
||||
fgDim: 'rgba(255,255,255,0.22)',
|
||||
label: 'rgba(255,255,255,0.22)',
|
||||
key: 'rgba(255,255,255,0.32)',
|
||||
dim: 'rgba(255,255,255,0.22)',
|
||||
accent: '#3385FF',
|
||||
accentBg: 'rgba(51,133,255,0.14)',
|
||||
activeBg: 'rgba(255,255,255,0.07)',
|
||||
hoverBg: 'rgba(255,255,255,0.04)',
|
||||
selBg: 'rgba(51,133,255,0.12)',
|
||||
selFg: 'rgba(255,255,255,0.88)',
|
||||
item: 'rgba(255,255,255,0.42)',
|
||||
itemHov: 'rgba(255,255,255,0.72)',
|
||||
tabFg: 'rgba(255,255,255,0.28)',
|
||||
tabOn: 'rgba(255,255,255,0.88)',
|
||||
live: '#10B981',
|
||||
liveBg: 'rgba(16,185,129,0.10)',
|
||||
liveBorder: 'rgba(16,185,129,0.20)',
|
||||
canvasBg: '#0C0C10',
|
||||
dotColor: 'rgba(255,255,255,0.055)',
|
||||
};
|
||||
|
||||
export const LIGHT_TOKENS: CanvasTokens = {
|
||||
bg: '#F2F2F4',
|
||||
bgDeep: '#E8E8EB',
|
||||
border: 'rgba(0,0,0,0.08)',
|
||||
sep: 'rgba(0,0,0,0.06)',
|
||||
fg: '#1A1A2E',
|
||||
fgMuted: 'rgba(0,0,0,0.48)',
|
||||
fgDim: 'rgba(0,0,0,0.30)',
|
||||
label: 'rgba(0,0,0,0.38)',
|
||||
key: 'rgba(0,0,0,0.55)',
|
||||
dim: 'rgba(0,0,0,0.30)',
|
||||
accent: '#0066FF',
|
||||
accentBg: 'rgba(0,102,255,0.10)',
|
||||
activeBg: 'rgba(0,0,0,0.05)',
|
||||
hoverBg: 'rgba(0,0,0,0.04)',
|
||||
selBg: 'rgba(0,102,255,0.10)',
|
||||
selFg: '#0A0A0A',
|
||||
item: 'rgba(0,0,0,0.55)',
|
||||
itemHov: '#0A0A0A',
|
||||
tabFg: 'rgba(0,0,0,0.38)',
|
||||
tabOn: '#0A0A0A',
|
||||
live: '#059669',
|
||||
liveBg: 'rgba(5,150,105,0.08)',
|
||||
liveBorder: 'rgba(5,150,105,0.18)',
|
||||
canvasBg: '#DDDDE2',
|
||||
dotColor: 'rgba(0,0,0,0.10)',
|
||||
};
|
||||
|
||||
/** Returns the token set matching the current user theme preference. */
|
||||
export function useCanvasTheme(): CanvasTokens {
|
||||
const mode = useTheme((s) => s.mode);
|
||||
return mode === 'dark' ? DARK_TOKENS : LIGHT_TOKENS;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user