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
@@ -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,
+36 -57
View File
@@ -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',
}}>