updated design
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
"Bash(curl -s \"https://github.githubassets.com/assets/primer-primitives-0f570a01cbe448fb.css\")",
|
||||
"Bash(pnpm --filter @originmain/app run build)",
|
||||
"Bash(pnpm --filter @originmain/app install)",
|
||||
"Bash(pnpm add *)"
|
||||
"Bash(pnpm add *)",
|
||||
"Bash(xargs ls *)",
|
||||
"Bash(grep -E \"\\\\.\\(ts|tsx\\)$\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1742
-560
File diff suppressed because it is too large
Load Diff
@@ -20,69 +20,156 @@ export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
|
||||
style={{ position: 'absolute', top: y, left: x }}
|
||||
onClick={(e) => { e.stopPropagation(); selectArtboard(id); }}
|
||||
>
|
||||
{/* Label above artboard */}
|
||||
{/* Label */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: -22,
|
||||
top: -24,
|
||||
left: 0,
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: selected ? '#0F52BA' : 'rgba(255,255,255,0.4)',
|
||||
fontFamily: "'JetBrains Mono', 'SF Mono', ui-monospace, monospace",
|
||||
fontWeight: selected ? 500 : 400,
|
||||
color: selected ? '#3385FF' : 'rgba(255,255,255,0.35)',
|
||||
userSelect: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
letterSpacing: '-0.01em',
|
||||
transition: 'color 0.15s',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{/* Artboard frame */}
|
||||
{/* Frame */}
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
background: '#fff',
|
||||
background: '#FFFFFF',
|
||||
borderRadius: 8,
|
||||
boxShadow: selected
|
||||
? '0 0 0 2px #0F52BA, 0 8px 40px rgba(0,0,0,0.5)'
|
||||
: '0 4px 24px rgba(0,0,0,0.4)',
|
||||
? '0 0 0 2px #3385FF, 0 12px 48px rgba(0,0,0,0.65)'
|
||||
: '0 4px 28px rgba(0,0,0,0.55)',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
cursor: 'default',
|
||||
cursor: selected ? 'default' : 'pointer',
|
||||
transition: 'box-shadow 0.15s',
|
||||
}}
|
||||
>
|
||||
{/* Selection handles */}
|
||||
{selected && (
|
||||
<>
|
||||
<Handle pos={{ top: -3, left: -3 }} />
|
||||
<Handle pos={{ top: -3, right: -3 }} />
|
||||
<Handle pos={{ bottom: -3, left: -3 }} />
|
||||
<Handle pos={{ bottom: -3, right: -3 }} />
|
||||
<Handle pos={{ top: -4, left: -4 }} />
|
||||
<Handle pos={{ top: -4, right: -4 }} />
|
||||
<Handle pos={{ bottom: -4, left: -4 }} />
|
||||
<Handle pos={{ bottom: -4, right: -4 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Placeholder content */}
|
||||
{/* Per-artboard content */}
|
||||
<ArtboardContent id={id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtboardContent({ id }: { id: string }) {
|
||||
switch (id) {
|
||||
case 'dashboard-card': return <DashboardCard />;
|
||||
case 'user-profile': return <UserProfile />;
|
||||
case 'nav-sidebar': return <NavSidebar />;
|
||||
case 'data-table': return <DataTable />;
|
||||
default: return <Placeholder label={id} />;
|
||||
}
|
||||
}
|
||||
|
||||
// ── DashboardCard ──────────────────────────────────────────
|
||||
function DashboardCard() {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: '#111', letterSpacing: '-0.01em' }}>
|
||||
Revenue Overview
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: '#111', letterSpacing: '-0.01em' }}>Revenue Overview</span>
|
||||
<span style={{ fontSize: 9, background: '#ECFDF5', color: '#059669', padding: '2px 7px', borderRadius: 99, fontWeight: 600, fontFamily: 'monospace' }}>Live</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 9, background: '#ecfdf5', color: '#059669', padding: '2px 6px', borderRadius: 99, fontWeight: 600, fontFamily: 'monospace' }}>
|
||||
Live
|
||||
<div style={{ fontSize: 24, fontWeight: 800, color: '#0A0A0A', letterSpacing: '-0.045em', lineHeight: 1, marginBottom: 4 }}>$12,450</div>
|
||||
<div style={{ fontSize: 10, color: '#059669', fontWeight: 500, marginBottom: 14, display: 'flex', alignItems: 'center', gap: 3 }}>
|
||||
<span>↑</span> +2.4% vs last month
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 800, color: '#0c0c10', letterSpacing: '-0.04em', lineHeight: 1, marginBottom: 5 }}>
|
||||
$12,450
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#16a34a', fontWeight: 500, marginBottom: 12 }}>
|
||||
↑ +2.4% vs last month
|
||||
</div>
|
||||
<div style={{ height: 3, background: '#f0f0f0', borderRadius: 99, overflow: 'hidden', marginBottom: 12 }}>
|
||||
<div style={{ height: '100%', width: '68%', background: 'linear-gradient(90deg,#0F52BA,#4d84e0)', borderRadius: 99 }} />
|
||||
<div style={{ height: 3, background: '#F0F0F0', borderRadius: 99, overflow: 'hidden', marginBottom: 14 }}>
|
||||
<div style={{ height: '100%', width: '68%', background: 'linear-gradient(90deg, #0066FF, #3385FF)', borderRadius: 99 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<Chip>Q4 2024</Chip>
|
||||
<Chip>MRR</Chip>
|
||||
<Tag>Q4 2024</Tag>
|
||||
<Tag>MRR</Tag>
|
||||
<Tag>SaaS</Tag>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── UserProfile ────────────────────────────────────────────
|
||||
function UserProfile() {
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%' }}>
|
||||
<div style={{ width: 52, height: 52, borderRadius: '50%', background: 'linear-gradient(135deg, #7C3AED, #0066FF)', marginBottom: 12 }} />
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#0A0A0A', letterSpacing: '-0.025em', marginBottom: 3 }}>Sarah Chen</div>
|
||||
<div style={{ fontSize: 10, color: '#A1A1AA', marginBottom: 16 }}>Design Engineer</div>
|
||||
<div style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
{[['Team', 'Acme Inc'], ['Role', 'Admin'], ['Plan', 'Team']].map(([k, v]) => (
|
||||
<div key={k} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 10 }}>
|
||||
<span style={{ color: '#A1A1AA' }}>{k}</span>
|
||||
<span style={{ fontWeight: 500, color: '#0A0A0A' }}>{v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button style={{ marginTop: 16, width: '100%', padding: '7px 0', borderRadius: 6, border: '1px solid #E4E4E7', background: '#FAFAFA', fontSize: 10, fontWeight: 600, color: '#3F3F46', cursor: 'default' }}>
|
||||
Edit profile
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── NavSidebar ─────────────────────────────────────────────
|
||||
function NavSidebar() {
|
||||
const items = [
|
||||
{ icon: '⊞', label: 'Dashboard', active: true },
|
||||
{ icon: '◉', label: 'Origin Graph', active: false },
|
||||
{ icon: '⬜', label: 'Artboards', active: false },
|
||||
{ icon: '△', label: 'Diffs', active: false },
|
||||
{ icon: '🔗', label: 'Integrations',active: false },
|
||||
];
|
||||
return (
|
||||
<div style={{ height: '100%', background: '#FAFAFA', display: 'flex', flexDirection: 'column', padding: '12px 0' }}>
|
||||
<div style={{ padding: '0 12px 12px', fontSize: 11, fontWeight: 800, letterSpacing: '-0.04em', color: '#0A0A0A' }}>
|
||||
Origin<span style={{ color: '#0066FF' }}>main</span>
|
||||
</div>
|
||||
<div style={{ height: 1, background: '#EBEBEB', margin: '0 0 8px' }} />
|
||||
{items.map(({ icon, label, active }) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '7px 12px',
|
||||
margin: '1px 6px',
|
||||
borderRadius: 5,
|
||||
background: active ? 'rgba(0,102,255,0.07)' : 'transparent',
|
||||
fontSize: 10,
|
||||
fontWeight: active ? 600 : 400,
|
||||
color: active ? '#0066FF' : '#52525B',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 11 }}>{icon}</span>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: 'auto', padding: '8px 12px 0', borderTop: '1px solid #EBEBEB' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ width: 24, height: 24, borderRadius: '50%', background: 'linear-gradient(135deg, #7C3AED, #0066FF)', flexShrink: 0 }} />
|
||||
<div>
|
||||
<div style={{ fontSize: 9, fontWeight: 600, color: '#0A0A0A' }}>Sarah Chen</div>
|
||||
<div style={{ fontSize: 8, color: '#A1A1AA' }}>Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,15 +177,75 @@ export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── DataTable ──────────────────────────────────────────────
|
||||
function DataTable() {
|
||||
const rows = [
|
||||
{ name: 'DashboardCard', status: 'Live', nodes: 12, tokens: 8 },
|
||||
{ name: 'UserProfile', status: 'Draft', nodes: 7, tokens: 3 },
|
||||
{ name: 'NavSidebar', status: 'Live', nodes: 19, tokens: 11 },
|
||||
{ name: 'DataTable', status: 'Review', nodes: 24, tokens: 14 },
|
||||
];
|
||||
return (
|
||||
<div style={{ padding: '16px 0', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ padding: '0 16px 12px', fontSize: 11, fontWeight: 700, color: '#0A0A0A', letterSpacing: '-0.02em' }}>
|
||||
Component Inventory
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 60px 50px 50px', padding: '0 16px 6px', gap: 4 }}>
|
||||
{['Name', 'Status', 'Nodes', 'Tokens'].map(h => (
|
||||
<span key={h} style={{ fontFamily: 'monospace', fontSize: 8, color: '#A1A1AA', letterSpacing: '0.05em', textTransform: 'uppercase' }}>{h}</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ height: 1, background: '#F0F0F0' }} />
|
||||
{rows.map((row, i) => (
|
||||
<div
|
||||
key={row.name}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 60px 50px 50px',
|
||||
padding: '8px 16px',
|
||||
gap: 4,
|
||||
background: i % 2 === 0 ? 'transparent' : '#FAFAFA',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 10, fontWeight: 500, color: '#0A0A0A', letterSpacing: '-0.01em' }}>{row.name}</span>
|
||||
<span style={{
|
||||
fontSize: 8,
|
||||
fontWeight: 600,
|
||||
color: row.status === 'Live' ? '#059669' : row.status === 'Draft' ? '#6B7280' : '#D97706',
|
||||
background: row.status === 'Live' ? '#ECFDF5' : row.status === 'Draft' ? '#F9FAFB' : '#FFFBEB',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 99,
|
||||
width: 'fit-content',
|
||||
fontFamily: 'monospace',
|
||||
}}>
|
||||
{row.status}
|
||||
</span>
|
||||
<span style={{ fontSize: 10, color: '#52525B', fontFamily: 'monospace' }}>{row.nodes}</span>
|
||||
<span style={{ fontSize: 10, color: '#52525B', fontFamily: 'monospace' }}>{row.tokens}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Placeholder({ label }: { label: string }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#A1A1AA', fontSize: 11 }}>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Handle({ pos }: { pos: React.CSSProperties }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 7,
|
||||
height: 7,
|
||||
width: 8,
|
||||
height: 8,
|
||||
background: '#fff',
|
||||
border: '2px solid #0F52BA',
|
||||
border: '2px solid #3385FF',
|
||||
borderRadius: 2,
|
||||
zIndex: 10,
|
||||
...pos,
|
||||
@@ -107,9 +254,9 @@ function Handle({ pos }: { pos: React.CSSProperties }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Chip({ children }: { children: string }) {
|
||||
function Tag({ children }: { children: string }) {
|
||||
return (
|
||||
<span style={{ fontSize: 9, background: '#f5f5f5', color: '#666', padding: '3px 7px', borderRadius: 99 }}>
|
||||
<span style={{ fontSize: 9, background: '#F4F4F5', color: '#71717A', padding: '3px 8px', borderRadius: 99, fontWeight: 500 }}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -6,10 +6,10 @@ import { useCanvas } from '@/store/canvas';
|
||||
import { Artboard } from './Artboard';
|
||||
|
||||
const ARTBOARDS = [
|
||||
{ id: 'dashboard-card', label: 'DashboardCard', x: 120, y: 120, width: 280, height: 200 },
|
||||
{ id: 'user-profile', label: 'UserProfile', x: 480, y: 120, width: 240, height: 280 },
|
||||
{ id: 'nav-sidebar', label: 'NavSidebar', x: 120, y: 400, width: 200, height: 360 },
|
||||
{ id: 'data-table', label: 'DataTable', x: 400, y: 420, width: 420, height: 300 },
|
||||
{ id: 'dashboard-card', label: 'DashboardCard', x: 120, y: 100, width: 280, height: 200 },
|
||||
{ id: 'user-profile', label: 'UserProfile', x: 460, y: 100, width: 200, height: 260 },
|
||||
{ id: 'nav-sidebar', label: 'NavSidebar', x: 120, y: 360, width: 200, height: 340 },
|
||||
{ id: 'data-table', label: 'DataTable', x: 380, y: 380, width: 420, height: 280 },
|
||||
];
|
||||
|
||||
export function Canvas() {
|
||||
@@ -23,7 +23,7 @@ export function Canvas() {
|
||||
const lastPos = useRef({ x: 0, y: 0 });
|
||||
const spaceDown = useRef(false);
|
||||
|
||||
// Wheel: pan or zoom
|
||||
// Wheel: pan or pinch-zoom
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
@@ -32,9 +32,7 @@ export function Canvas() {
|
||||
const { zoom, panX, panY, setPan, setZoom } = useViewport.getState();
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const ox = e.clientX - rect.left;
|
||||
const oy = e.clientY - rect.top;
|
||||
setZoom(zoom * (e.deltaY > 0 ? 0.92 : 1.09), ox, oy);
|
||||
setZoom(zoom * (e.deltaY > 0 ? 0.92 : 1.09), e.clientX - rect.left, e.clientY - rect.top);
|
||||
} else {
|
||||
setPan(panX - e.deltaX, panY - e.deltaY);
|
||||
}
|
||||
@@ -43,7 +41,7 @@ export function Canvas() {
|
||||
return () => el.removeEventListener('wheel', onWheel);
|
||||
}, []);
|
||||
|
||||
// Space key: temporary pan mode
|
||||
// Space bar temporary pan
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => { if (e.code === 'Space' && e.target === document.body) spaceDown.current = true; };
|
||||
const up = (e: KeyboardEvent) => { if (e.code === 'Space') spaceDown.current = false; };
|
||||
@@ -74,29 +72,48 @@ export function Canvas() {
|
||||
|
||||
const onMouseUp = useCallback(() => { isPanning.current = false; }, []);
|
||||
|
||||
const gridSize = Math.max(8, 22 * zoom);
|
||||
// Dot grid that shifts with pan and scales with zoom
|
||||
const gridSpacing = Math.max(6, 20 * zoom);
|
||||
const cursor = activeTool === 'pan' || isPanning.current ? 'grab' : 'default';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{ gridColumn: 2, gridRow: 2, position: 'relative', overflow: 'hidden', background: '#111115', cursor }}
|
||||
style={{
|
||||
gridColumn: 2,
|
||||
gridRow: 2,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: '#0C0C10',
|
||||
cursor,
|
||||
}}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onMouseUp}
|
||||
>
|
||||
{/* Dot grid — shifts with pan, scales with zoom */}
|
||||
{/* Dot grid — scales and shifts with viewport */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.07) 1px, transparent 1px)',
|
||||
backgroundSize: `${gridSize}px ${gridSize}px`,
|
||||
backgroundPosition: `${panX % gridSize}px ${panY % gridSize}px`,
|
||||
backgroundImage: 'radial-gradient(circle, rgba(255,255,255,0.055) 1px, transparent 1px)',
|
||||
backgroundSize: `${gridSpacing}px ${gridSpacing}px`,
|
||||
backgroundPosition: `${panX % gridSpacing}px ${panY % gridSpacing}px`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Subtle radial vignette */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'radial-gradient(ellipse 100% 100% at 50% 50%, transparent 40%, rgba(0,0,0,0.35) 100%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Transform layer */}
|
||||
<div
|
||||
style={{
|
||||
@@ -104,6 +121,7 @@ export function Canvas() {
|
||||
inset: 0,
|
||||
transform: `matrix(${zoom},0,0,${zoom},${panX},${panY})`,
|
||||
transformOrigin: '0 0',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{ARTBOARDS.map((ab) => (
|
||||
|
||||
@@ -10,10 +10,12 @@ export function AppChrome() {
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: '40px 1fr',
|
||||
gridTemplateColumns: '240px 1fr 320px',
|
||||
gridTemplateRows: '44px 1fr',
|
||||
gridTemplateColumns: '220px 1fr 272px',
|
||||
height: '100dvh',
|
||||
overflow: 'hidden',
|
||||
background: '#0C0C10',
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ToolbarButton,
|
||||
ToolbarDivider,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from '@fluentui/react-components';
|
||||
import {
|
||||
CursorClickRegular,
|
||||
@@ -19,14 +18,28 @@ import {
|
||||
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)',
|
||||
};
|
||||
|
||||
export function Toolbar() {
|
||||
const { activeTool, setActiveTool } = useCanvas();
|
||||
const zoom = useViewport((s) => s.zoom);
|
||||
const setZoom = useViewport((s) => s.setZoom);
|
||||
const reset = useViewport((s) => s.reset);
|
||||
|
||||
const ap = (id: Tool): 'primary' | 'subtle' => activeTool === id ? 'primary' : 'subtle';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -34,90 +47,224 @@ export function Toolbar() {
|
||||
gridRow: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderBottom: '1px solid rgba(0,0,0,0.08)',
|
||||
background: '#fff',
|
||||
paddingInline: 8,
|
||||
background: T.bg,
|
||||
borderBottom: `1px solid ${T.border}`,
|
||||
paddingInline: 12,
|
||||
gap: 2,
|
||||
height: 40,
|
||||
height: 44,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{/* Wordmark */}
|
||||
<span
|
||||
style={{
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
fontFamily: "'Inter', system-ui, sans-serif",
|
||||
fontWeight: 800,
|
||||
fontSize: '0.9375rem',
|
||||
color: '#0F52BA',
|
||||
fontSize: '0.875rem',
|
||||
letterSpacing: '-0.04em',
|
||||
padding: '0 8px',
|
||||
marginRight: 4,
|
||||
userSelect: 'none',
|
||||
color: T.fg,
|
||||
padding: '0 10px 0 4px',
|
||||
marginRight: 2,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
Om
|
||||
Om<span style={{ color: T.accent }}>•</span>
|
||||
</span>
|
||||
|
||||
<ToolbarDivider />
|
||||
<Sep />
|
||||
|
||||
{/* Select */}
|
||||
{/* Selection tools */}
|
||||
<ToolGroup>
|
||||
<Tooltip content="Select V" relationship="label">
|
||||
<ToolbarButton appearance={ap('select')} icon={<CursorClickRegular />} onClick={() => setActiveTool('select')} />
|
||||
<TBtn active={activeTool === 'select'} onClick={() => setActiveTool('select')}>
|
||||
<CursorClickRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
{/* Pan */}
|
||||
<Tooltip content="Pan H" relationship="label">
|
||||
<ToolbarButton appearance={ap('pan')} icon={<HandLeftRegular />} onClick={() => setActiveTool('pan')} />
|
||||
</Tooltip>
|
||||
{/* Artboard */}
|
||||
<Tooltip content="New Artboard A" relationship="label">
|
||||
<ToolbarButton appearance={ap('artboard')} icon={<SquareRegular />} onClick={() => setActiveTool('artboard')} />
|
||||
</Tooltip>
|
||||
{/* Zone */}
|
||||
<Tooltip content="Completion Zone Z" relationship="label">
|
||||
<ToolbarButton appearance={ap('zone')} icon={<SelectObjectRegular />} onClick={() => setActiveTool('zone')} />
|
||||
<TBtn active={activeTool === 'pan'} onClick={() => setActiveTool('pan')}>
|
||||
<HandLeftRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
</ToolGroup>
|
||||
|
||||
<ToolbarDivider />
|
||||
<Sep />
|
||||
|
||||
{/* Create tools */}
|
||||
<ToolGroup>
|
||||
<Tooltip content="New Artboard A" relationship="label">
|
||||
<TBtn active={activeTool === 'artboard'} onClick={() => setActiveTool('artboard')}>
|
||||
<SquareRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
<Tooltip content="Completion Zone Z" relationship="label">
|
||||
<TBtn active={activeTool === 'zone'} onClick={() => setActiveTool('zone')}>
|
||||
<SelectObjectRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
</ToolGroup>
|
||||
|
||||
<Sep />
|
||||
|
||||
{/* View tools */}
|
||||
<ToolGroup>
|
||||
<Tooltip content="Intent Diff" relationship="label">
|
||||
<ToolbarButton appearance="subtle" icon={<DataTrendingRegular />} />
|
||||
<TBtn active={false} onClick={() => {}}>
|
||||
<DataTrendingRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
<Tooltip content="Origin Graph" relationship="label">
|
||||
<ToolbarButton appearance="subtle" icon={<CircleHintHalfVerticalRegular />} />
|
||||
<TBtn active={false} onClick={() => {}}>
|
||||
<CircleHintHalfVerticalRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
</ToolGroup>
|
||||
|
||||
{/* 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)}>
|
||||
<ZoomOutRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
|
||||
{/* Right — zoom + live status */}
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4, paddingInline: 4 }}>
|
||||
<Tooltip content="Zoom out" relationship="label">
|
||||
<ToolbarButton appearance="subtle" icon={<ZoomOutRegular />} onClick={() => setZoom(zoom * 0.8)} />
|
||||
</Tooltip>
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
borderRadius: 4,
|
||||
padding: '2px 8px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
border: `1px solid ${T.sep}`,
|
||||
borderRadius: 5,
|
||||
padding: '3px 9px',
|
||||
fontSize: '0.6875rem',
|
||||
fontFamily: 'monospace',
|
||||
color: 'rgba(0,0,0,0.5)',
|
||||
fontFamily: "'JetBrains Mono', 'SF Mono', ui-monospace, monospace",
|
||||
color: T.fgMuted,
|
||||
cursor: 'pointer',
|
||||
minWidth: 46,
|
||||
minWidth: 48,
|
||||
textAlign: 'center',
|
||||
transition: 'background 0.12s, color 0.12s',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.09)';
|
||||
(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.color = T.fgMuted;
|
||||
}}
|
||||
>
|
||||
{Math.round(zoom * 100)}%
|
||||
</button>
|
||||
|
||||
<Tooltip content="Zoom in" relationship="label">
|
||||
<ToolbarButton appearance="subtle" icon={<ZoomInRegular />} onClick={() => setZoom(zoom * 1.25)} />
|
||||
<TBtn active={false} onClick={() => setZoom(zoom * 1.25)}>
|
||||
<ZoomInRegular />
|
||||
</TBtn>
|
||||
</Tooltip>
|
||||
|
||||
<ToolbarDivider />
|
||||
<Sep />
|
||||
|
||||
<Badge color="success" size="extra-small" />
|
||||
<span style={{ fontSize: '0.6875rem', fontFamily: 'monospace', color: 'rgba(0,0,0,0.4)', paddingRight: 4 }}>
|
||||
{/* Live status pill */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '4px 10px',
|
||||
borderRadius: 6,
|
||||
background: T.liveBg,
|
||||
border: `1px solid ${T.liveBorder}`,
|
||||
marginLeft: 2,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: '50%',
|
||||
background: T.live,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.625rem',
|
||||
color: T.live,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Sep() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 1,
|
||||
height: 20,
|
||||
background: 'rgba(255,255,255,0.07)',
|
||||
margin: '0 5px',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolGroup({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TBtn({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{
|
||||
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)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s, color 0.1s',
|
||||
fontSize: '1rem',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
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)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (!active) {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'transparent';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'rgba(255,255,255,0.42)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { TabList, Tab, Divider } from '@fluentui/react-components';
|
||||
import { useState } from 'react';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
|
||||
const PROPS = [
|
||||
@@ -15,87 +15,211 @@ const DIFF = [
|
||||
{ op: 'del', text: '− borderRadius: 8px' },
|
||||
{ op: 'add', text: '+ borderRadius: 12px' },
|
||||
{ op: 'del', text: '− accentColor: #2A6CD4' },
|
||||
{ op: 'add', text: '+ accentColor: #0F52BA' },
|
||||
{ op: 'add', text: '+ accentColor: #0066FF' },
|
||||
{ op: 'del', text: '− padding: 16' },
|
||||
{ op: 'add', text: '+ padding: 20' },
|
||||
];
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
s: '#16a34a',
|
||||
n: '#2563eb',
|
||||
b: '#d97706',
|
||||
s: '#7DD3A8',
|
||||
n: '#7EB8FF',
|
||||
b: '#FFBA7B',
|
||||
};
|
||||
|
||||
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 { selectedArtboardId } = useCanvas();
|
||||
const [tab, setTab] = useState<TabId>('props');
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
gridColumn: 3,
|
||||
gridRow: 2,
|
||||
background: '#fafafa',
|
||||
borderLeft: '1px solid rgba(0,0,0,0.08)',
|
||||
background: T.bg,
|
||||
borderLeft: `1px solid ${T.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<TabList defaultSelectedValue="props" size="small" style={{ padding: '0 8px', borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
|
||||
<Tab value="props">Props</Tab>
|
||||
<Tab value="diff">Diff</Tab>
|
||||
<Tab value="graph">Graph</Tab>
|
||||
</TabList>
|
||||
{/* Tab bar */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
borderBottom: `1px solid ${T.border}`,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{(['props', 'diff', 'graph'] as TabId[]).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setTab(t)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '11px 0',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.5875rem',
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
color: tab === t ? T.tabOn : T.tabFg,
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: tab === t ? `2px solid ${T.accent}` : '2px solid transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'color 0.12s',
|
||||
marginBottom: -1,
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedArtboardId ? (
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{!selectedArtboardId ? (
|
||||
<div
|
||||
style={{
|
||||
padding: '32px 16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '1.25rem', opacity: 0.3 }}>⬜</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.625rem',
|
||||
color: 'rgba(255,255,255,0.22)',
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
>
|
||||
Select an artboard
|
||||
</span>
|
||||
</div>
|
||||
) : tab === 'props' ? (
|
||||
<>
|
||||
<Section label="Component Props">
|
||||
{PROPS.map(({ key, val, type }) => (
|
||||
<PropRow key={key} label={key} value={val} color={TYPE_COLORS[type] ?? '#555'} />
|
||||
<PropRow key={key} label={key} value={val} color={TYPE_COLORS[type] ?? T.key} />
|
||||
))}
|
||||
</Section>
|
||||
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<HSep />
|
||||
<Section label="Render Target">
|
||||
<PropRow label="file" value="dashboard.tsx:42" color="#7EB8FF" />
|
||||
<PropRow label="status" value="connected" color="#7DD3A8" />
|
||||
<PropRow label="agent" value="claude-code" color="#7DD3A8" />
|
||||
</Section>
|
||||
</>
|
||||
) : tab === 'diff' ? (
|
||||
<Section label="Intent Diff">
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.5875rem',
|
||||
color: 'rgba(255,255,255,0.28)',
|
||||
marginBottom: 10,
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
DashboardCard.tsx · 3 hunks
|
||||
</div>
|
||||
{DIFF.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
padding: '3px 8px',
|
||||
borderRadius: 3,
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.625rem',
|
||||
padding: '4px 8px',
|
||||
borderRadius: 4,
|
||||
marginBottom: 3,
|
||||
background: d.op === 'del' ? 'rgba(239,68,68,0.08)' : 'rgba(34,197,94,0.08)',
|
||||
color: d.op === 'del' ? '#dc2626' : '#16a34a',
|
||||
lineHeight: 1.55,
|
||||
background: d.op === 'del' ? 'rgba(255,70,70,0.08)' : 'rgba(70,220,120,0.08)',
|
||||
color: d.op === 'del' ? '#FF8080' : '#7DDBA0',
|
||||
borderLeft: `2px solid ${d.op === 'del' ? 'rgba(255,80,80,0.3)' : 'rgba(70,220,120,0.3)'}`,
|
||||
}}
|
||||
>
|
||||
{d.text}
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
|
||||
<Divider style={{ margin: 0 }} />
|
||||
|
||||
<Section label="Render Target">
|
||||
<PropRow label="file" value="dashboard.tsx:42" color="#2563eb" />
|
||||
<PropRow label="status" value="connected" color="#16a34a" />
|
||||
<PropRow label="agent" value="claude-code" color="#16a34a" />
|
||||
</Section>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ padding: 24, color: 'rgba(0,0,0,0.3)', fontSize: 12, fontFamily: 'monospace', textAlign: 'center' }}>
|
||||
Select an artboard
|
||||
<Section label="Origin Graph">
|
||||
<GraphNode label="DashboardCard" depth={0} isRoot />
|
||||
<GraphNode label="StatsCard" depth={1} />
|
||||
<GraphNode label="ProgressBar" depth={2} />
|
||||
<GraphNode label="ValueDisplay" depth={2} />
|
||||
<GraphNode label="CardBase" depth={1} />
|
||||
<GraphNode label="Elevation" depth={2} />
|
||||
<HSep />
|
||||
<div style={{ padding: '4px 0 8px' }}>
|
||||
<PropRow label="nodes" value="284" color="#7EB8FF" />
|
||||
<PropRow label="depth" value="4" color="#7EB8FF" />
|
||||
<PropRow label="tokens used" value="12" color="#FFBA7B" />
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
<div
|
||||
style={{
|
||||
height: 27,
|
||||
background: '#0D0D11',
|
||||
borderTop: `1px solid ${T.sep}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 12px',
|
||||
gap: 12,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#10B981', flexShrink: 0 }} />
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.5625rem', color: 'rgba(255,255,255,0.45)' }}>
|
||||
Live render connected
|
||||
</span>
|
||||
<span style={{ marginLeft: 'auto', fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.5625rem', color: 'rgba(255,255,255,0.22)' }}>
|
||||
284 nodes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ padding: '10px 12px' }}>
|
||||
<div style={{ fontFamily: 'monospace', fontSize: 9, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.3)', marginBottom: 8 }}>
|
||||
<div style={{ padding: '12px 14px' }}>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.5rem',
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: T.label,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{children}
|
||||
@@ -105,9 +229,70 @@ function Section({ label, children }: { label: string; children: React.ReactNode
|
||||
|
||||
function PropRow({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 11, color: 'rgba(0,0,0,0.4)' }}>{label}</span>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: 11, color }}>{value}</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
marginBottom: 8,
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: T.key, flexShrink: 0 }}>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.625rem',
|
||||
color,
|
||||
textAlign: 'right',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '60%',
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GraphNode({ label, depth, isRoot }: { label: string; depth: number; isRoot?: boolean }) {
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HSep() {
|
||||
return <div style={{ height: 1, background: 'rgba(255,255,255,0.04)', margin: '2px 0' }} />;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Tree,
|
||||
TreeItem,
|
||||
TreeItemLayout,
|
||||
Text,
|
||||
} from '@fluentui/react-components';
|
||||
import {
|
||||
SquareRegular,
|
||||
@@ -21,6 +20,19 @@ const ARTBOARDS = [
|
||||
{ id: 'data-table', label: 'DataTable' },
|
||||
];
|
||||
|
||||
// Dark panel tokens
|
||||
const T = {
|
||||
bg: '#111115',
|
||||
border: 'rgba(255,255,255,0.055)',
|
||||
label: 'rgba(255,255,255,0.22)',
|
||||
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',
|
||||
sep: 'rgba(255,255,255,0.04)',
|
||||
};
|
||||
|
||||
export function ArtboardNavigator() {
|
||||
const { selectedArtboardId, selectArtboard } = useCanvas();
|
||||
|
||||
@@ -29,83 +41,162 @@ export function ArtboardNavigator() {
|
||||
style={{
|
||||
gridColumn: 1,
|
||||
gridRow: 2,
|
||||
background: '#fafafa',
|
||||
borderRight: '1px solid rgba(0,0,0,0.08)',
|
||||
background: T.bg,
|
||||
borderRight: `1px solid ${T.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{/* Artboards section */}
|
||||
<SectionLabel>Artboards</SectionLabel>
|
||||
<Tree aria-label="Artboards" size="small" style={{ padding: '0 4px' }}>
|
||||
<SectionLabel icon="⬜">Artboards</SectionLabel>
|
||||
<div style={{ padding: '2px 6px 0' }}>
|
||||
{ARTBOARDS.map((ab) => (
|
||||
<TreeItem
|
||||
<div
|
||||
key={ab.id}
|
||||
itemType="leaf"
|
||||
value={ab.id}
|
||||
style={{
|
||||
background: selectedArtboardId === ab.id ? 'rgba(15,82,186,0.08)' : undefined,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
onClick={() => selectArtboard(ab.id)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 5,
|
||||
cursor: 'pointer',
|
||||
background: selectedArtboardId === ab.id ? T.selBg : 'transparent',
|
||||
color: selectedArtboardId === ab.id ? T.selFg : T.item,
|
||||
fontSize: '0.75rem',
|
||||
letterSpacing: '-0.01em',
|
||||
fontWeight: selectedArtboardId === ab.id ? 500 : 400,
|
||||
transition: 'background 0.1s, color 0.1s',
|
||||
userSelect: 'none',
|
||||
marginBottom: 1,
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
if (selectedArtboardId !== ab.id) {
|
||||
(e.currentTarget as HTMLDivElement).style.background = 'rgba(255,255,255,0.04)';
|
||||
(e.currentTarget as HTMLDivElement).style.color = T.itemHov;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (selectedArtboardId !== ab.id) {
|
||||
(e.currentTarget as HTMLDivElement).style.background = 'transparent';
|
||||
(e.currentTarget as HTMLDivElement).style.color = T.item;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TreeItemLayout iconBefore={<SquareRegular style={{ color: '#0F52BA', fontSize: 12 }} />}>
|
||||
<Text size={200}>{ab.label}</Text>
|
||||
</TreeItemLayout>
|
||||
</TreeItem>
|
||||
<SquareRegular
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: selectedArtboardId === ab.id ? T.accent : 'rgba(255,255,255,0.25)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
{ab.label}
|
||||
{selectedArtboardId === ab.id && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
width: 5,
|
||||
height: 5,
|
||||
borderRadius: '50%',
|
||||
background: T.accent,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Tree>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<HSep />
|
||||
|
||||
{/* Codebase section */}
|
||||
<SectionLabel>Files</SectionLabel>
|
||||
<Tree aria-label="Codebase" size="small" style={{ padding: '0 4px' }}>
|
||||
<TreeItem itemType="branch" value="src">
|
||||
<TreeItemLayout iconBefore={<FolderOpenRegular style={{ fontSize: 12 }} />}>
|
||||
<Text size={200}>src</Text>
|
||||
{/* Files section */}
|
||||
<SectionLabel icon="📁">Files</SectionLabel>
|
||||
<Tree
|
||||
aria-label="Codebase"
|
||||
size="small"
|
||||
style={{ padding: '2px 6px' }}
|
||||
>
|
||||
<TreeItem
|
||||
itemType="branch"
|
||||
value="src"
|
||||
style={{ color: T.item }}
|
||||
>
|
||||
<TreeItemLayout
|
||||
iconBefore={<FolderOpenRegular style={{ fontSize: 11, color: 'rgba(255,255,255,0.3)' }} />}
|
||||
style={{ fontSize: '0.75rem', color: T.item, padding: '4px 4px' }}
|
||||
>
|
||||
src
|
||||
</TreeItemLayout>
|
||||
<Tree>
|
||||
<TreeItem itemType="branch" value="components">
|
||||
<TreeItemLayout iconBefore={<FolderRegular style={{ fontSize: 12 }} />}>
|
||||
<Text size={200}>components</Text>
|
||||
<TreeItemLayout
|
||||
iconBefore={<FolderRegular style={{ fontSize: 11, color: 'rgba(255,255,255,0.25)' }} />}
|
||||
style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.35)', padding: '4px 4px' }}
|
||||
>
|
||||
components
|
||||
</TreeItemLayout>
|
||||
<Tree>
|
||||
{ARTBOARDS.map((ab) => (
|
||||
<TreeItem key={ab.id} itemType="leaf" value={`file-${ab.id}`}>
|
||||
<TreeItemLayout iconBefore={<DocumentRegular style={{ fontSize: 12 }} />}>
|
||||
<Text size={200} style={{ color: 'rgba(0,0,0,0.55)' }}>
|
||||
<TreeItemLayout
|
||||
iconBefore={<DocumentRegular style={{ fontSize: 10, color: 'rgba(255,255,255,0.2)' }} />}
|
||||
style={{ fontSize: '0.6875rem', color: 'rgba(255,255,255,0.3)', padding: '3px 4px' }}
|
||||
>
|
||||
{ab.label}.tsx
|
||||
</Text>
|
||||
</TreeItemLayout>
|
||||
</TreeItem>
|
||||
))}
|
||||
</Tree>
|
||||
</TreeItem>
|
||||
<TreeItem itemType="leaf" value="app-page">
|
||||
<TreeItemLayout iconBefore={<DocumentRegular style={{ fontSize: 12 }} />}>
|
||||
<Text size={200} style={{ color: 'rgba(0,0,0,0.55)' }}>page.tsx</Text>
|
||||
<TreeItemLayout
|
||||
iconBefore={<DocumentRegular style={{ fontSize: 10, color: 'rgba(255,255,255,0.2)' }} />}
|
||||
style={{ fontSize: '0.6875rem', color: 'rgba(255,255,255,0.3)', padding: '3px 4px' }}
|
||||
>
|
||||
page.tsx
|
||||
</TreeItemLayout>
|
||||
</TreeItem>
|
||||
</Tree>
|
||||
</TreeItem>
|
||||
</Tree>
|
||||
|
||||
<HSep />
|
||||
|
||||
{/* Graph nodes indicator */}
|
||||
<SectionLabel icon="◉">Graph</SectionLabel>
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 16px 12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<GraphStat label="nodes" value="284" color="#3385FF" />
|
||||
<GraphStat label="components" value="47" color="rgba(255,255,255,0.45)" />
|
||||
<GraphStat label="tokens" value="112" color="rgba(255,255,255,0.45)" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: string }) {
|
||||
function SectionLabel({ children, icon }: { children: string; icon: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 12px 4px',
|
||||
fontSize: '0.625rem',
|
||||
fontFamily: 'monospace',
|
||||
padding: '12px 12px 5px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
fontSize: '0.5625rem',
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(0,0,0,0.35)',
|
||||
color: 'rgba(255,255,255,0.22)',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -113,6 +204,28 @@ function SectionLabel({ children }: { children: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
return <div style={{ height: 1, background: 'rgba(0,0,0,0.06)', margin: '6px 0' }} />;
|
||||
function HSep() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 1,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
margin: '8px 0',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function GraphStat({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
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)' }}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color, fontWeight: 600 }}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user