improved a lot of things

This commit is contained in:
SinachPat
2026-04-29 04:43:29 +01:00
parent 8d07d97cdc
commit 957bb9e0e7
9 changed files with 362 additions and 166 deletions
+2 -1
View File
@@ -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\\(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\\('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\\); 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)"
] ]
} }
} }
+104 -5
View File
@@ -69,6 +69,45 @@
--mono: 'JetBrains Mono', 'SF Mono', ui-monospace, 'Cascadia Code', monospace; --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 RESET
════════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════════ */
@@ -276,6 +315,22 @@ button { font-family: inherit; cursor: pointer; border: none; background: none;
} }
.nav-links a:hover { color: var(--fg); } .nav-links a:hover { color: var(--fg); }
.nav-actions { display: flex; align-items: center; gap: 8px; } .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 ── */ /* ── Hamburger ── */
.nav-burger { .nav-burger {
@@ -1555,6 +1610,7 @@ footer {
} }
.nav-links { display: none; } .nav-links { display: none; }
.nav-actions .btn { display: none; } .nav-actions .btn { display: none; }
.nav-actions .nav-theme-btn { display: none; }
.nav-burger { display: flex; } .nav-burger { display: flex; }
.section { padding: 80px 0; } .section { padding: 80px 0; }
.feat-block { padding: 80px 24px; } .feat-block { padding: 80px 24px; }
@@ -1625,6 +1681,9 @@ footer {
<li><a href="#">Docs</a></li> <li><a href="#">Docs</a></li>
</ul> </ul>
<div class="nav-actions"> <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-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> <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"> <button class="nav-burger" id="nav-burger" aria-label="Toggle navigation" aria-expanded="false">
@@ -1657,6 +1716,9 @@ footer {
<div class="nav-mobile-ctas"> <div class="nav-mobile-ctas">
<a href="/sign-in" class="btn btn-secondary">Log in</a> <a href="/sign-in" class="btn btn-secondary">Log in</a>
<a href="/sign-up" class="btn btn-primary">Get Started</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>
</div> </div>
</nav> </nav>
@@ -2609,21 +2671,58 @@ annualBtn.addEventListener('click', () => {
function safeLS(op, key, val) { function safeLS(op, key, val) {
try { return op === 'get' ? localStorage.getItem(key) : localStorage.setItem(key, val); } catch(e) {} 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; const root = document.documentElement;
let theme = safeLS('get', 'om-theme') || 'light'; let theme = readStoredTheme();
const themeBtn = document.getElementById('themeToggle'); const themeBtn = document.getElementById('themeToggle');
const themeIcon = document.getElementById('themeIcon'); const themeIcon = document.getElementById('themeIcon');
const themeLabel = document.getElementById('themeLabel'); 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) { function applyTheme(t) {
theme = t; theme = t;
root.setAttribute('data-theme', t); root.setAttribute('data-theme', t);
safeLS('set', 'om-theme', t); writeTheme(t);
themeIcon.textContent = t === 'dark' ? '☀' : '☾'; const isDark = t === 'dark';
themeLabel.textContent = t === 'dark' ? 'Light mode' : 'Dark mode'; // 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); 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) ───── // ── Feat cell reveal highlight (Fluent reveal effect) ─────
document.querySelectorAll('.feat-cell').forEach(cell => { document.querySelectorAll('.feat-cell').forEach(cell => {
@@ -5,9 +5,11 @@ import { useQueryClient } from '@tanstack/react-query';
import { useViewport } from '@/store/viewport'; import { useViewport } from '@/store/viewport';
import { useCanvas } from '@/store/canvas'; import { useCanvas } from '@/store/canvas';
import { useArtboards, createArtboardMutation } from '@/hooks/useArtboards'; import { useArtboards, createArtboardMutation } from '@/hooks/useArtboards';
import { useCanvasTheme } from '@/store/canvasTheme';
import { Artboard } from './Artboard'; import { Artboard } from './Artboard';
export function Canvas() { export function Canvas() {
const T = useCanvasTheme();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const panX = useViewport((s) => s.panX); const panX = useViewport((s) => s.panX);
const panY = useViewport((s) => s.panY); const panY = useViewport((s) => s.panY);
@@ -151,7 +153,8 @@ export function Canvas() {
gridRow: 2, gridRow: 2,
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
background: '#0C0C10', background: T.canvasBg,
transition: 'background 0.2s',
cursor, cursor,
}} }}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
@@ -163,7 +166,7 @@ export function Canvas() {
style={{ style={{
position: 'absolute', position: 'absolute',
inset: 0, 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`, backgroundSize: `${gridSpacing}px ${gridSpacing}px`,
backgroundPosition: `${panX % gridSpacing}px ${panY % gridSpacing}px`, backgroundPosition: `${panX % gridSpacing}px ${panY % gridSpacing}px`,
pointerEvents: 'none', pointerEvents: 'none',
@@ -11,6 +11,7 @@ import { useHistory } from '@/store/history';
import { useCanvas, type Tool } from '@/store/canvas'; import { useCanvas, type Tool } from '@/store/canvas';
import { useViewport } from '@/store/viewport'; import { useViewport } from '@/store/viewport';
import { useTheme } from '@/store/theme'; import { useTheme } from '@/store/theme';
import { useCanvasTheme } from '@/store/canvasTheme';
interface AppChromeProps { interface AppChromeProps {
workspaceId?: string; workspaceId?: string;
@@ -24,6 +25,7 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
const setContext = useCanvas((s) => s.setContext); const setContext = useCanvas((s) => s.setContext);
const setActiveTool = useCanvas((s) => s.setActiveTool); const setActiveTool = useCanvas((s) => s.setActiveTool);
const { mode: themeMode, toggle: toggleTheme } = useTheme(); const { mode: themeMode, toggle: toggleTheme } = useTheme();
const CT = useCanvasTheme();
// Push workspace/project IDs into the store so Canvas and Navigator can read them. // Push workspace/project IDs into the store so Canvas and Navigator can read them.
useEffect(() => { useEffect(() => {
@@ -99,7 +101,8 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
flexDirection: 'column', flexDirection: 'column',
height: '100dvh', height: '100dvh',
overflow: 'hidden', overflow: 'hidden',
background: '#0C0C10', background: CT.canvasBg,
transition: 'background 0.2s',
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif", fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
}} }}
> >
@@ -108,8 +111,8 @@ export function AppChrome({ workspaceId, projectId, workspaceName, projectName }
<div style={{ <div style={{
height: 36, height: 36,
flexShrink: 0, flexShrink: 0,
background: '#0A0A0E', background: CT.bgDeep,
borderBottom: '1px solid rgba(255,255,255,0.06)', borderBottom: `1px solid ${CT.border}`,
display: 'flex', alignItems: 'center', display: 'flex', alignItems: 'center',
padding: '0 14px', gap: 0, padding: '0 14px', gap: 0,
zIndex: 10, zIndex: 10,
+36 -57
View File
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { import {
ToolbarButton,
ToolbarDivider, ToolbarDivider,
Tooltip, Tooltip,
} from '@fluentui/react-components'; } from '@fluentui/react-components';
@@ -17,24 +16,10 @@ import {
} from '@fluentui/react-icons'; } from '@fluentui/react-icons';
import { useCanvas, type Tool } from '@/store/canvas'; import { useCanvas, type Tool } from '@/store/canvas';
import { useViewport } from '@/store/viewport'; import { useViewport } from '@/store/viewport';
import { useCanvasTheme } from '@/store/canvasTheme';
// 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)',
};
export function Toolbar() { export function Toolbar() {
const T = useCanvasTheme();
const { activeTool, setActiveTool } = useCanvas(); const { activeTool, setActiveTool } = useCanvas();
const zoom = useViewport((s) => s.zoom); const zoom = useViewport((s) => s.zoom);
const setZoom = useViewport((s) => s.setZoom); const setZoom = useViewport((s) => s.setZoom);
@@ -53,6 +38,7 @@ export function Toolbar() {
gap: 2, gap: 2,
height: 44, height: 44,
userSelect: 'none', userSelect: 'none',
transition: 'background 0.2s, border-color 0.2s',
}} }}
> >
{/* Wordmark */} {/* Wordmark */}
@@ -66,54 +52,55 @@ export function Toolbar() {
padding: '0 10px 0 4px', padding: '0 10px 0 4px',
marginRight: 2, marginRight: 2,
flexShrink: 0, flexShrink: 0,
transition: 'color 0.2s',
}} }}
> >
Om<span style={{ color: T.accent }}></span> Om<span style={{ color: T.accent }}></span>
</span> </span>
<Sep /> <Sep T={T} />
{/* Selection tools */} {/* Selection tools */}
<ToolGroup> <ToolGroup>
<Tooltip content="Select V" relationship="label"> <Tooltip content="Select V" relationship="label">
<TBtn active={activeTool === 'select'} onClick={() => setActiveTool('select')}> <TBtn T={T} active={activeTool === 'select'} onClick={() => setActiveTool('select')}>
<CursorClickRegular /> <CursorClickRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
<Tooltip content="Pan H" relationship="label"> <Tooltip content="Pan H" relationship="label">
<TBtn active={activeTool === 'pan'} onClick={() => setActiveTool('pan')}> <TBtn T={T} active={activeTool === 'pan'} onClick={() => setActiveTool('pan')}>
<HandLeftRegular /> <HandLeftRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
</ToolGroup> </ToolGroup>
<Sep /> <Sep T={T} />
{/* Create tools */} {/* Create tools */}
<ToolGroup> <ToolGroup>
<Tooltip content="New Artboard A" relationship="label"> <Tooltip content="New Artboard A" relationship="label">
<TBtn active={activeTool === 'artboard'} onClick={() => setActiveTool('artboard')}> <TBtn T={T} active={activeTool === 'artboard'} onClick={() => setActiveTool('artboard')}>
<SquareRegular /> <SquareRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
<Tooltip content="Completion Zone Z" relationship="label"> <Tooltip content="Completion Zone Z" relationship="label">
<TBtn active={activeTool === 'zone'} onClick={() => setActiveTool('zone')}> <TBtn T={T} active={activeTool === 'zone'} onClick={() => setActiveTool('zone')}>
<SelectObjectRegular /> <SelectObjectRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
</ToolGroup> </ToolGroup>
<Sep /> <Sep T={T} />
{/* View tools */} {/* View tools */}
<ToolGroup> <ToolGroup>
<Tooltip content="Intent Diff" relationship="label"> <Tooltip content="Intent Diff" relationship="label">
<TBtn active={false} onClick={() => {}}> <TBtn T={T} active={false} onClick={() => {}}>
<DataTrendingRegular /> <DataTrendingRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
<Tooltip content="Origin Graph" relationship="label"> <Tooltip content="Origin Graph" relationship="label">
<TBtn active={false} onClick={() => {}}> <TBtn T={T} active={false} onClick={() => {}}>
<CircleHintHalfVerticalRegular /> <CircleHintHalfVerticalRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
@@ -122,7 +109,7 @@ export function Toolbar() {
{/* Right side */} {/* Right side */}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}> <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4 }}>
<Tooltip content="Zoom out" relationship="label"> <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 /> <ZoomOutRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
@@ -130,7 +117,7 @@ export function Toolbar() {
<button <button
onClick={reset} onClick={reset}
style={{ style={{
background: 'rgba(255,255,255,0.05)', background: T.activeBg,
border: `1px solid ${T.sep}`, border: `1px solid ${T.sep}`,
borderRadius: 5, borderRadius: 5,
padding: '3px 9px', padding: '3px 9px',
@@ -144,11 +131,11 @@ export function Toolbar() {
letterSpacing: '-0.01em', letterSpacing: '-0.01em',
}} }}
onMouseEnter={e => { 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; (e.currentTarget as HTMLButtonElement).style.color = T.fg;
}} }}
onMouseLeave={e => { 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; (e.currentTarget as HTMLButtonElement).style.color = T.fgMuted;
}} }}
> >
@@ -156,12 +143,12 @@ export function Toolbar() {
</button> </button>
<Tooltip content="Zoom in" relationship="label"> <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 /> <ZoomInRegular />
</TBtn> </TBtn>
</Tooltip> </Tooltip>
<Sep /> <Sep T={T} />
{/* Live status pill */} {/* Live status pill */}
<div <div
@@ -175,17 +162,10 @@ export function Toolbar() {
border: `1px solid ${T.liveBorder}`, border: `1px solid ${T.liveBorder}`,
marginLeft: 2, marginLeft: 2,
marginRight: 4, marginRight: 4,
transition: 'background 0.2s, border-color 0.2s',
}} }}
> >
<div <div style={{ width: 6, height: 6, borderRadius: '50%', background: T.live, flexShrink: 0 }} />
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: T.live,
flexShrink: 0,
}}
/>
<span <span
style={{ style={{
fontFamily: "'JetBrains Mono', ui-monospace, monospace", 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 ( return (
<div <div
style={{ style={{
width: 1, width: 1, height: 20,
height: 20, background: T.sep,
background: 'rgba(255,255,255,0.07)',
margin: '0 5px', margin: '0 5px',
flexShrink: 0, flexShrink: 0,
transition: 'background 0.2s',
}} }}
/> />
); );
} }
function ToolGroup({ children }: { children: React.ReactNode }) { function ToolGroup({ children }: { children: React.ReactNode }) {
return ( return <div style={{ display: 'flex', alignItems: 'center', gap: 1 }}>{children}</div>;
<div style={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{children}
</div>
);
} }
function TBtn({ function TBtn({
T,
active, active,
onClick, onClick,
children, children,
}: { }: {
T: CanvasTokens;
active: boolean; active: boolean;
onClick: () => void; onClick: () => void;
children: React.ReactNode; children: React.ReactNode;
@@ -237,12 +217,11 @@ function TBtn({
<button <button
onClick={onClick} onClick={onClick}
style={{ style={{
width: 32, width: 32, height: 32,
height: 32,
borderRadius: 6, borderRadius: 6,
border: 'none', border: 'none',
background: active ? 'rgba(51,133,255,0.15)' : 'transparent', background: active ? T.accentBg : 'transparent',
color: active ? '#3385FF' : 'rgba(255,255,255,0.42)', color: active ? T.accent : T.item,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -253,14 +232,14 @@ function TBtn({
}} }}
onMouseEnter={e => { onMouseEnter={e => {
if (!active) { if (!active) {
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.07)'; (e.currentTarget as HTMLButtonElement).style.background = T.activeBg;
(e.currentTarget as HTMLButtonElement).style.color = 'rgba(255,255,255,0.75)'; (e.currentTarget as HTMLButtonElement).style.color = T.itemHov;
} }
}} }}
onMouseLeave={e => { onMouseLeave={e => {
if (!active) { if (!active) {
(e.currentTarget as HTMLButtonElement).style.background = 'transparent'; (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 { useArtboards, patchArtboard } from '@/hooks/useArtboards';
import { useDiffs } from '@/hooks/useDiffs'; import { useDiffs } from '@/hooks/useDiffs';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useCanvasTheme } from '@/store/canvasTheme';
import type { PropChange } from '@originmain/diff-engine'; import type { PropChange } from '@originmain/diff-engine';
import type { FiberNode } from '@originmain/renderer'; import type { FiberNode } from '@originmain/renderer';
import type { Artboard, IntentDiff } from '@originmain/origin-graph'; import type { Artboard, IntentDiff } from '@originmain/origin-graph';
@@ -18,20 +19,8 @@ const TYPE_COLORS: Record<string, string> = {
type TabId = 'props' | 'diff' | 'graph'; 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() { export function Inspector() {
const T = useCanvasTheme();
const { selectedArtboardId, liveArtboardIds, artboardFiberRoots, selectedComponentId, selectedComponentData, workspaceId, projectId } = useCanvas(); const { selectedArtboardId, liveArtboardIds, artboardFiberRoots, selectedComponentId, selectedComponentData, workspaceId, projectId } = useCanvas();
const [tab, setTab] = useState<TabId>('props'); const [tab, setTab] = useState<TabId>('props');
const { rawArtboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined); const { rawArtboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined);
@@ -98,7 +87,7 @@ export function Inspector() {
style={{ style={{
fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontFamily: "'JetBrains Mono', ui-monospace, monospace",
fontSize: '0.625rem', fontSize: '0.625rem',
color: 'rgba(255,255,255,0.22)', color: T.dim,
letterSpacing: '0.06em', letterSpacing: '0.06em',
}} }}
> >
@@ -123,7 +112,7 @@ export function Inspector() {
<div <div
style={{ style={{
height: 27, height: 27,
background: '#0D0D11', background: T.bgDeep,
borderTop: `1px solid ${T.sep}`, borderTop: `1px solid ${T.sep}`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -134,14 +123,14 @@ export function Inspector() {
> >
<div style={{ <div style={{
width: 6, height: 6, borderRadius: '50%', flexShrink: 0, width: 6, height: 6, borderRadius: '50%', flexShrink: 0,
background: isLive ? '#10B981' : 'rgba(255,255,255,0.15)', background: isLive ? T.live : T.activeBg,
boxShadow: isLive ? '0 0 6px rgba(16,185,129,0.6)' : 'none', boxShadow: isLive ? `0 0 6px ${T.liveBorder}` : 'none',
transition: 'background 0.3s, box-shadow 0.3s', 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'} {isLive ? 'Live render connected' : selectedArtboardId ? 'No render — set URL in Props' : 'No artboard selected'}
</span> </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
? `${selectedArtboard.metadata_jsonb['width'] ?? '?'} × ${selectedArtboard.metadata_jsonb['height'] ?? '?'}` ? `${selectedArtboard.metadata_jsonb['width'] ?? '?'} × ${selectedArtboard.metadata_jsonb['height'] ?? '?'}`
: '—'} : '—'}
@@ -163,6 +152,7 @@ function PropsTab({
workspaceId: string | null; workspaceId: string | null;
projectId: string | null; projectId: string | null;
}) { }) {
const T = useCanvasTheme();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [editingUrl, setEditingUrl] = useState(false); const [editingUrl, setEditingUrl] = useState(false);
const [urlDraft, setUrlDraft] = useState(''); const [urlDraft, setUrlDraft] = useState('');
@@ -244,7 +234,7 @@ function PropsTab({
return <PropRow key={k} label={k} value={display} color={color} />; return <PropRow key={k} label={k} value={display} color={color} />;
})} })}
{Object.keys(selectedComponentData.props ?? {}).length === 0 && ( {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 No props
</span> </span>
)} )}
@@ -273,7 +263,7 @@ function PropsTab({
<Section label="Render Target"> <Section label="Render Target">
<PropRow label="name" value={artboard.name} color="#7EB8FF" /> <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 */} {/* renderUrl — inline editable */}
@@ -308,8 +298,8 @@ function PropsTab({
placeholder="http://localhost:3000" placeholder="http://localhost:3000"
style={{ style={{
flex: 1, fontSize: '0.5875rem', fontFamily: "'JetBrains Mono', monospace", 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)', background: T.bgDeep, border: `1px solid ${T.border}`,
borderRadius: 5, padding: '4px 8px', color: 'rgba(255,255,255,0.85)', borderRadius: 5, padding: '4px 8px', color: T.fg,
outline: 'none', outline: 'none',
}} }}
/> />
@@ -333,7 +323,7 @@ function PropsTab({
{renderUrl} {renderUrl}
</span> </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 not connected
</span> </span>
)} )}
@@ -351,10 +341,10 @@ function PropsTab({
width: '100%', width: '100%',
fontFamily: "'JetBrains Mono', monospace", fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.5875rem', fontSize: '0.5875rem',
background: driftStatus === 'loading' ? 'rgba(255,255,255,0.06)' : 'rgba(51,133,255,0.12)', background: driftStatus === 'loading' ? T.activeBg : T.accentBg,
border: `1px solid ${driftStatus === 'loading' ? 'rgba(255,255,255,0.08)' : 'rgba(51,133,255,0.25)'}`, border: `1px solid ${driftStatus === 'loading' ? T.border : T.accent}`,
borderRadius: 6, padding: '6px 0', 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', cursor: driftStatus === 'loading' ? 'wait' : 'pointer',
letterSpacing: '0.04em', letterSpacing: '0.04em',
transition: 'background 0.15s, border-color 0.15s, color 0.15s', transition: 'background 0.15s, border-color 0.15s, color 0.15s',
@@ -373,19 +363,19 @@ function PropsTab({
<div style={{ <div style={{
marginTop: 8, marginTop: 8,
padding: '8px 10px', padding: '8px 10px',
background: 'rgba(0,0,0,0.35)', background: T.bgDeep,
border: '1px solid rgba(255,255,255,0.08)', border: `1px solid ${T.border}`,
borderRadius: 6, borderRadius: 6,
maxHeight: 220, maxHeight: 220,
overflowY: 'auto', overflowY: 'auto',
fontSize: '0.5875rem', fontSize: '0.5875rem',
fontFamily: "'Inter', sans-serif", fontFamily: "'Inter', sans-serif",
color: 'rgba(255,255,255,0.62)', color: T.fgMuted,
lineHeight: 1.65, lineHeight: 1.65,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
wordBreak: 'break-word', wordBreak: 'break-word',
scrollbarWidth: 'thin', scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.18) transparent', scrollbarColor: `${T.dim} transparent`,
} as React.CSSProperties}> } as React.CSSProperties}>
{driftReport} {driftReport}
</div> </div>
@@ -396,6 +386,7 @@ function PropsTab({
} }
function Section({ label, children }: { label: string; children: React.ReactNode }) { function Section({ label, children }: { label: string; children: React.ReactNode }) {
const T = useCanvasTheme();
return ( return (
<div style={{ padding: '12px 14px' }}> <div style={{ padding: '12px 14px' }}>
<div <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 }) { function PropRow({ label, value, color }: { label: string; value: string; color: string }) {
const T = useCanvasTheme();
return ( return (
<div <div
style={{ 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 }) { function GraphNode({ label, depth, isRoot }: { label: string; depth: number; isRoot?: boolean }) {
const T = useCanvasTheme();
return ( return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, paddingLeft: depth * 14, marginBottom: 6 }}> <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 }} /> <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 : 'rgba(255,255,255,0.5)', letterSpacing: '-0.01em' }}> <span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: isRoot ? T.accent : T.fgMuted, letterSpacing: '-0.01em' }}>
{label} {label}
</span> </span>
</div> </div>
@@ -461,10 +454,11 @@ function GraphNode({ label, depth, isRoot }: { label: string; depth: number; isR
/* ── Graph tab ────────────────────────────────────────────── */ /* ── Graph tab ────────────────────────────────────────────── */
function GraphTab({ fiberRoot }: { fiberRoot: FiberNode | undefined }) { function GraphTab({ fiberRoot }: { fiberRoot: FiberNode | undefined }) {
const T = useCanvasTheme();
if (!fiberRoot) { if (!fiberRoot) {
return ( return (
<Section label="Origin Graph"> <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 No live render connect a URL in Props to see the fiber tree
</span> </span>
</Section> </Section>
@@ -487,6 +481,7 @@ function GraphTab({ fiberRoot }: { fiberRoot: FiberNode | undefined }) {
} }
function FiberTreeView({ node, depth }: { node: FiberNode; depth: number }) { function FiberTreeView({ node, depth }: { node: FiberNode; depth: number }) {
const T = useCanvasTheme();
const [collapsed, setCollapsed] = useState(depth > 2); const [collapsed, setCollapsed] = useState(depth > 2);
const hasChildren = node.children && node.children.length > 0; const hasChildren = node.children && node.children.length > 0;
@@ -502,17 +497,17 @@ function FiberTreeView({ node, depth }: { node: FiberNode; depth: number }) {
> >
<div style={{ <div style={{
width: 5, height: 5, borderRadius: '50%', flexShrink: 0, 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 && ( {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 ? '▶' : '▼'} {collapsed ? '▶' : '▼'}
</span> </span>
)} )}
<span style={{ <span style={{
fontFamily: "'JetBrains Mono', monospace", fontFamily: "'JetBrains Mono', monospace",
fontSize: '0.625rem', fontSize: '0.625rem',
color: depth === 0 ? T.accent : 'rgba(255,255,255,0.55)', color: depth === 0 ? T.accent : T.fgMuted,
letterSpacing: '-0.01em', letterSpacing: '-0.01em',
}}> }}>
{node.name} {node.name}
@@ -535,11 +530,13 @@ function measureFiberDepth(node: FiberNode, d = 0): number {
} }
function HSep() { 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 ─────────────────────────────────────────────── */ /* ── Diff tab ─────────────────────────────────────────────── */
function DiffTab({ artboardId }: { artboardId: string | null }) { function DiffTab({ artboardId }: { artboardId: string | null }) {
const T = useCanvasTheme();
const { stacks } = useHistory(); const { stacks } = useHistory();
const { diffs, createDiff, isLoading } = useDiffs(artboardId); const { diffs, createDiff, isLoading } = useDiffs(artboardId);
const [summaryStatus, setSummaryStatus] = useState<'idle' | 'summarising' | 'exporting'>('idle'); const [summaryStatus, setSummaryStatus] = useState<'idle' | 'summarising' | 'exporting'>('idle');
@@ -588,7 +585,7 @@ function DiffTab({ artboardId }: { artboardId: string | null }) {
if (!artboardId) { if (!artboardId) {
return ( return (
<Section label="Intent Diff"> <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 No artboard selected
</span> </span>
</Section> </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 No pending changes
</span> </span>
)} )}
@@ -642,11 +639,11 @@ function DiffTab({ artboardId }: { artboardId: string | null }) {
{/* Saved diffs from DB */} {/* Saved diffs from DB */}
<Section label="Exported"> <Section label="Exported">
{isLoading ? ( {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 Loading
</span> </span>
) : diffs.length === 0 ? ( ) : 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 No exported diffs yet
</span> </span>
) : ( ) : (
@@ -665,21 +662,22 @@ const STATUS_COLOR: Record<string, string> = {
}; };
function SavedDiffRow({ diff }: { diff: IntentDiff }) { function SavedDiffRow({ diff }: { diff: IntentDiff }) {
const T = useCanvasTheme();
const changes = diff.changes_jsonb as { propChanges?: PropChange[]; styleChanges?: PropChange[] } | null; const changes = diff.changes_jsonb as { propChanges?: PropChange[]; styleChanges?: PropChange[] } | null;
const count = (changes?.propChanges?.length ?? 0) + (changes?.styleChanges?.length ?? 0); const count = (changes?.propChanges?.length ?? 0) + (changes?.styleChanges?.length ?? 0);
const color = STATUS_COLOR[diff.status] ?? T.dim; const color = STATUS_COLOR[diff.status] ?? T.dim;
return ( 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 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 2 }}>
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color }}> <span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color }}>
{diff.status} {diff.status}
</span> </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' : ''} {count} change{count !== 1 ? 's' : ''}
</span> </span>
</div> </div>
{diff.summary && ( {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} {diff.summary}
</span> </span>
)} )}
@@ -7,39 +7,43 @@ import { SquareRegular } from '@fluentui/react-icons';
import { useCanvas } from '@/store/canvas'; import { useCanvas } from '@/store/canvas';
import { useArtboards, patchArtboard, createArtboardMutation } from '@/hooks/useArtboards'; import { useArtboards, patchArtboard, createArtboardMutation } from '@/hooks/useArtboards';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useCanvasTheme } from '@/store/canvasTheme';
const T = { import { useTheme } from '@/store/theme';
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)',
},
});
export function ArtboardNavigator() { export function ArtboardNavigator() {
const T = useCanvasTheme();
const mode = useTheme((s) => s.mode);
const { selectedArtboardId, selectArtboard, workspaceId, projectId, liveArtboardIds, artboardFiberRoots } = useCanvas(); const { selectedArtboardId, selectArtboard, workspaceId, projectId, liveArtboardIds, artboardFiberRoots } = useCanvas();
const { artboards, rawArtboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined); const { artboards, rawArtboards } = useArtboards(workspaceId ?? undefined, projectId ?? undefined);
const queryClient = useQueryClient(); 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) => { const deleteArtboard = useCallback(async (id: string, name: string) => {
if (!window.confirm(`Delete "${name}"?`)) return; if (!window.confirm(`Delete "${name}"?`)) return;
try { try {
@@ -126,12 +130,13 @@ export function ArtboardNavigator() {
return ( return (
<NavRow <NavRow
key={ab.id} key={ab.id}
T={T}
selected={sel} selected={sel}
live={live} live={live}
onClick={() => selectArtboard(ab.id)} onClick={() => selectArtboard(ab.id)}
icon={ icon={
<SquareRegular <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} label={ab.label}
@@ -167,8 +172,8 @@ export function ArtboardNavigator() {
<SectionLabel>Graph</SectionLabel> <SectionLabel>Graph</SectionLabel>
<div style={{ padding: '4px 14px 8px', display: 'flex', flexDirection: 'column', gap: 6, flexShrink: 0 }}> <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="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="live" value={liveCount > 0 ? String(liveCount) : '—'} color={liveCount > 0 ? T.live : T.fgDim} />
<GraphStat label="components" value={totalComponents > 0 ? String(totalComponents) : '—'} color="rgba(255,255,255,0.45)" /> <GraphStat label="components" value={totalComponents > 0 ? String(totalComponents) : '—'} color={T.fgMuted} />
</div> </div>
{/* ── Cross-artboard query ── */} {/* ── Cross-artboard query ── */}
@@ -178,7 +183,10 @@ export function ArtboardNavigator() {
} }
/* ── Artboard row ─────────────────────────────────────────── */ /* ── Artboard row ─────────────────────────────────────────── */
import type { CanvasTokens } from '@/store/canvasTheme';
function NavRow({ function NavRow({
T,
selected = false, selected = false,
live = false, live = false,
onClick, onClick,
@@ -188,6 +196,7 @@ function NavRow({
onFork, onFork,
onDelete, onDelete,
}: { }: {
T: CanvasTokens;
selected?: boolean; selected?: boolean;
live?: boolean; live?: boolean;
onClick?: () => void; onClick?: () => void;
@@ -211,7 +220,7 @@ function NavRow({
padding: '4px 6px 4px 10px', padding: '4px 6px 4px 10px',
borderRadius: 5, borderRadius: 5,
cursor: 'pointer', 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, color: selected ? T.selFg : hov ? T.itemHov : T.item,
fontSize: '0.75rem', fontSize: '0.75rem',
letterSpacing: '-0.01em', letterSpacing: '-0.01em',
@@ -228,8 +237,8 @@ function NavRow({
{live && !hov && ( {live && !hov && (
<span style={{ <span style={{
width: 5, height: 5, borderRadius: '50%', width: 5, height: 5, borderRadius: '50%',
background: '#10B981', flexShrink: 0, display: 'block', background: T.live, flexShrink: 0, display: 'block',
boxShadow: '0 0 4px rgba(16,185,129,0.8)', boxShadow: `0 0 4px ${T.liveBorder}`,
}} /> }} />
)} )}
@@ -237,16 +246,14 @@ function NavRow({
{(hov || selected) && ( {(hov || selected) && (
<div style={{ display: 'flex', gap: 2, flexShrink: 0 }} onClick={e => e.stopPropagation()}> <div style={{ display: 'flex', gap: 2, flexShrink: 0 }} onClick={e => e.stopPropagation()}>
{onRename && ( {onRename && (
<IconBtn title="Rename" onClick={onRename}> <IconBtn T={T} title="Rename" onClick={onRename}>
{/* Pencil */}
<svg width="10" height="10" viewBox="0 0 10 10" fill="none"> <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"/> <path d="M1 7.5L7 1.5l1.5 1.5-6 6H1V7.5z" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</IconBtn> </IconBtn>
)} )}
{onFork && ( {onFork && (
<IconBtn title="Fork" onClick={onFork}> <IconBtn T={T} title="Fork" onClick={onFork}>
{/* Branch / fork icon */}
<svg width="10" height="10" viewBox="0 0 10 10" fill="none"> <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="2" cy="2" r="1.2" stroke="currentColor" strokeWidth="1"/>
<circle cx="8" 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> </IconBtn>
)} )}
{onDelete && ( {onDelete && (
<IconBtn title="Delete" onClick={onDelete} danger> <IconBtn T={T} title="Delete" onClick={onDelete} danger>
{/* Trash */}
<svg width="10" height="10" viewBox="0 0 10 10" fill="none"> <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"/> <path d="M2 2.5h6M4 2.5V1.5h2V2.5M3 2.5v6h4v-6" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </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); const [hov, setHov] = useState(false);
return ( return (
<button <button
@@ -278,10 +284,10 @@ function IconBtn({ children, title, onClick, danger }: { children: React.ReactNo
onMouseEnter={() => setHov(true)} onMouseEnter={() => setHov(true)}
onMouseLeave={() => setHov(false)} onMouseLeave={() => setHov(false)}
style={{ 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', border: 'none', borderRadius: 3, padding: '2px 3px',
cursor: 'pointer', 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', transition: 'background 0.1s, color 0.1s',
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
}} }}
@@ -293,6 +299,7 @@ function IconBtn({ children, title, onClick, danger }: { children: React.ReactNo
/* ── Helpers ──────────────────────────────────────────────── */ /* ── Helpers ──────────────────────────────────────────────── */
function SectionLabel({ children }: { children: string }) { function SectionLabel({ children }: { children: string }) {
const T = useCanvasTheme();
return ( return (
<div style={{ <div style={{
padding: '12px 12px 5px', padding: '12px 12px 5px',
@@ -311,27 +318,28 @@ function SectionLabel({ children }: { children: string }) {
} }
function HSep() { function HSep() {
const T = useCanvasTheme();
return <div style={{ height: 1, background: T.sep, margin: '6px 0', flexShrink: 0 }} />; return <div style={{ height: 1, background: T.sep, margin: '6px 0', flexShrink: 0 }} />;
} }
function ActiveDot() { function ActiveDot() {
const T = useCanvasTheme();
return ( return (
<span style={{ <span style={{
marginLeft: 'auto', marginLeft: 'auto',
width: 5, width: 5, height: 5,
height: 5,
borderRadius: '50%', borderRadius: '50%',
background: T.accent, background: T.accent,
flexShrink: 0, flexShrink: 0, display: 'block',
display: 'block',
}} /> }} />
); );
} }
function GraphStat({ label, value, color }: { label: string; value: string; color: string }) { function GraphStat({ label, value, color }: { label: string; value: string; color: string }) {
const T = useCanvasTheme();
return ( return (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}> <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} {label}
</span> </span>
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color, fontWeight: 600 }}> <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 ─────────────────────────────────── */ /* ── Cross-artboard query ─────────────────────────────────── */
function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) { function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) {
const T = useCanvasTheme();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle'); const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle');
const [answer, setAnswer] = useState(''); const [answer, setAnswer] = useState('');
@@ -392,11 +401,11 @@ function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) {
}} }}
placeholder="Ask across artboards…" placeholder="Ask across artboards…"
style={{ style={{
flex: 1, background: 'rgba(255,255,255,0.05)', flex: 1, background: T.bgDeep,
border: '1px solid rgba(255,255,255,0.08)', border: `1px solid ${T.border}`,
borderRadius: 5, padding: '5px 8px', borderRadius: 5, padding: '5px 8px',
fontSize: '0.5875rem', fontFamily: 'inherit', fontSize: '0.5875rem', fontFamily: 'inherit',
color: 'rgba(255,255,255,0.75)', outline: 'none', color: T.fg, outline: 'none',
}} }}
/> />
<button <button
@@ -415,9 +424,9 @@ function CrossArtboardQuery({ workspaceId }: { workspaceId: string | null }) {
{status === 'done' && answer && ( {status === 'done' && answer && (
<div style={{ <div style={{
marginTop: 6, padding: '6px 8px', marginTop: 6, padding: '6px 8px',
background: 'rgba(255,255,255,0.04)', background: T.bgDeep,
borderRadius: 5, fontSize: '0.5875rem', 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", fontFamily: "'Inter', sans-serif",
maxHeight: 120, overflow: 'auto', maxHeight: 120, overflow: 'auto',
}}> }}>
+104
View File
@@ -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