made system changes
This commit is contained in:
@@ -12,7 +12,9 @@
|
|||||||
"Bash(curl -s \"https://www.figma.com/draw/\")",
|
"Bash(curl -s \"https://www.figma.com/draw/\")",
|
||||||
"Bash(curl -s \"https://www.figma.com/make/\")",
|
"Bash(curl -s \"https://www.figma.com/make/\")",
|
||||||
"Bash(curl -s \"https://github.githubassets.com/assets/primer-primitives-0f570a01cbe448fb.css\")",
|
"Bash(curl -s \"https://github.githubassets.com/assets/primer-primitives-0f570a01cbe448fb.css\")",
|
||||||
"Bash(pnpm --filter @originmain/app run build)"
|
"Bash(pnpm --filter @originmain/app run build)",
|
||||||
|
"Bash(pnpm --filter @originmain/app install)",
|
||||||
|
"Bash(pnpm add *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@originmain/origin-graph": "workspace:*",
|
"@originmain/origin-graph": "workspace:*",
|
||||||
"@originmain/renderer": "workspace:*",
|
"@originmain/renderer": "workspace:*",
|
||||||
"@fluentui/react-components": "^9.54.0",
|
"@fluentui/react-components": "^9.54.0",
|
||||||
|
"@fluentui/react-icons": "^2.0.0",
|
||||||
"next": "^15.1.0",
|
"next": "^15.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { AppChrome } from '@/components/chrome/AppChrome';
|
||||||
|
|
||||||
|
export const metadata = { title: 'Canvas — Originmain' };
|
||||||
|
|
||||||
|
export default function CanvasPage() {
|
||||||
|
return <AppChrome />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useCanvas } from '@/store/canvas';
|
||||||
|
|
||||||
|
interface ArtboardProps {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Artboard({ id, label, x, y, width, height }: ArtboardProps) {
|
||||||
|
const { selectedArtboardId, selectArtboard } = useCanvas();
|
||||||
|
const selected = selectedArtboardId === id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ position: 'absolute', top: y, left: x }}
|
||||||
|
onClick={(e) => { e.stopPropagation(); selectArtboard(id); }}
|
||||||
|
>
|
||||||
|
{/* Label above artboard */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: -22,
|
||||||
|
left: 0,
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: selected ? '#0F52BA' : 'rgba(255,255,255,0.4)',
|
||||||
|
userSelect: 'none',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Artboard frame */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
background: '#fff',
|
||||||
|
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)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
cursor: 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 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 }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Placeholder content */}
|
||||||
|
<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>
|
||||||
|
<div style={{ fontSize: 9, background: '#ecfdf5', color: '#059669', padding: '2px 6px', borderRadius: 99, fontWeight: 600, fontFamily: 'monospace' }}>
|
||||||
|
Live
|
||||||
|
</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>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<Chip>Q4 2024</Chip>
|
||||||
|
<Chip>MRR</Chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Handle({ pos }: { pos: React.CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
background: '#fff',
|
||||||
|
border: '2px solid #0F52BA',
|
||||||
|
borderRadius: 2,
|
||||||
|
zIndex: 10,
|
||||||
|
...pos,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Chip({ children }: { children: string }) {
|
||||||
|
return (
|
||||||
|
<span style={{ fontSize: 9, background: '#f5f5f5', color: '#666', padding: '3px 7px', borderRadius: 99 }}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRef, useEffect, useCallback } from 'react';
|
||||||
|
import { useViewport } from '@/store/viewport';
|
||||||
|
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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Canvas() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const panX = useViewport((s) => s.panX);
|
||||||
|
const panY = useViewport((s) => s.panY);
|
||||||
|
const zoom = useViewport((s) => s.zoom);
|
||||||
|
const { activeTool, selectArtboard } = useCanvas();
|
||||||
|
|
||||||
|
const isPanning = useRef(false);
|
||||||
|
const lastPos = useRef({ x: 0, y: 0 });
|
||||||
|
const spaceDown = useRef(false);
|
||||||
|
|
||||||
|
// Wheel: pan or zoom
|
||||||
|
useEffect(() => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const onWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
setPan(panX - e.deltaX, panY - e.deltaY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener('wheel', onWheel, { passive: false });
|
||||||
|
return () => el.removeEventListener('wheel', onWheel);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Space key: temporary pan mode
|
||||||
|
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; };
|
||||||
|
window.addEventListener('keydown', down);
|
||||||
|
window.addEventListener('keyup', up);
|
||||||
|
return () => { window.removeEventListener('keydown', down); window.removeEventListener('keyup', up); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseDown = useCallback((e: React.MouseEvent) => {
|
||||||
|
const startPan = e.button === 1 || spaceDown.current || activeTool === 'pan';
|
||||||
|
if (startPan) {
|
||||||
|
e.preventDefault();
|
||||||
|
isPanning.current = true;
|
||||||
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||||
|
} else if (e.target === e.currentTarget) {
|
||||||
|
selectArtboard(null);
|
||||||
|
}
|
||||||
|
}, [activeTool, selectArtboard]);
|
||||||
|
|
||||||
|
const onMouseMove = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!isPanning.current) return;
|
||||||
|
const dx = e.clientX - lastPos.current.x;
|
||||||
|
const dy = e.clientY - lastPos.current.y;
|
||||||
|
lastPos.current = { x: e.clientX, y: e.clientY };
|
||||||
|
const { panX, panY, setPan } = useViewport.getState();
|
||||||
|
setPan(panX + dx, panY + dy);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseUp = useCallback(() => { isPanning.current = false; }, []);
|
||||||
|
|
||||||
|
const gridSize = Math.max(8, 22 * 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 }}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
>
|
||||||
|
{/* Dot grid — shifts with pan, scales with zoom */}
|
||||||
|
<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`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Transform layer */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
transform: `matrix(${zoom},0,0,${zoom},${panX},${panY})`,
|
||||||
|
transformOrigin: '0 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ARTBOARDS.map((ab) => (
|
||||||
|
<Artboard key={ab.id} {...ab} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Toolbar } from './Toolbar';
|
||||||
|
import { ArtboardNavigator } from '../navigator/ArtboardNavigator';
|
||||||
|
import { Canvas } from '../canvas/Canvas';
|
||||||
|
import { Inspector } from '../inspector/Inspector';
|
||||||
|
|
||||||
|
export function AppChrome() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateRows: '40px 1fr',
|
||||||
|
gridTemplateColumns: '240px 1fr 320px',
|
||||||
|
height: '100dvh',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar />
|
||||||
|
<ArtboardNavigator />
|
||||||
|
<Canvas />
|
||||||
|
<Inspector />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ToolbarButton,
|
||||||
|
ToolbarDivider,
|
||||||
|
Tooltip,
|
||||||
|
Badge,
|
||||||
|
} from '@fluentui/react-components';
|
||||||
|
import {
|
||||||
|
CursorClickRegular,
|
||||||
|
HandLeftRegular,
|
||||||
|
SquareRegular,
|
||||||
|
SelectObjectRegular,
|
||||||
|
DataTrendingRegular,
|
||||||
|
CircleHintHalfVerticalRegular,
|
||||||
|
ZoomInRegular,
|
||||||
|
ZoomOutRegular,
|
||||||
|
} from '@fluentui/react-icons';
|
||||||
|
import { useCanvas, type Tool } from '@/store/canvas';
|
||||||
|
import { useViewport } from '@/store/viewport';
|
||||||
|
|
||||||
|
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={{
|
||||||
|
gridColumn: '1 / -1',
|
||||||
|
gridRow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderBottom: '1px solid rgba(0,0,0,0.08)',
|
||||||
|
background: '#fff',
|
||||||
|
paddingInline: 8,
|
||||||
|
gap: 2,
|
||||||
|
height: 40,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Wordmark */}
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: 'system-ui, sans-serif',
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: '0.9375rem',
|
||||||
|
color: '#0F52BA',
|
||||||
|
letterSpacing: '-0.04em',
|
||||||
|
padding: '0 8px',
|
||||||
|
marginRight: 4,
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Om
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ToolbarDivider />
|
||||||
|
|
||||||
|
{/* Select */}
|
||||||
|
<Tooltip content="Select V" relationship="label">
|
||||||
|
<ToolbarButton appearance={ap('select')} icon={<CursorClickRegular />} onClick={() => setActiveTool('select')} />
|
||||||
|
</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')} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<ToolbarDivider />
|
||||||
|
|
||||||
|
{/* View tools */}
|
||||||
|
<Tooltip content="Intent Diff" relationship="label">
|
||||||
|
<ToolbarButton appearance="subtle" icon={<DataTrendingRegular />} />
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Origin Graph" relationship="label">
|
||||||
|
<ToolbarButton appearance="subtle" icon={<CircleHintHalfVerticalRegular />} />
|
||||||
|
</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',
|
||||||
|
fontSize: '0.6875rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
color: 'rgba(0,0,0,0.5)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
minWidth: 46,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(zoom * 100)}%
|
||||||
|
</button>
|
||||||
|
<Tooltip content="Zoom in" relationship="label">
|
||||||
|
<ToolbarButton appearance="subtle" icon={<ZoomInRegular />} onClick={() => setZoom(zoom * 1.25)} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<ToolbarDivider />
|
||||||
|
|
||||||
|
<Badge color="success" size="extra-small" />
|
||||||
|
<span style={{ fontSize: '0.6875rem', fontFamily: 'monospace', color: 'rgba(0,0,0,0.4)', paddingRight: 4 }}>
|
||||||
|
Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TabList, Tab, Divider } from '@fluentui/react-components';
|
||||||
|
import { useCanvas } from '@/store/canvas';
|
||||||
|
|
||||||
|
const PROPS = [
|
||||||
|
{ key: 'title', val: '"Revenue Overview"', type: 's' },
|
||||||
|
{ key: 'value', val: '"$12,450"', type: 's' },
|
||||||
|
{ key: 'delta', val: '+2.4', type: 'n' },
|
||||||
|
{ key: 'period', val: '"monthly"', type: 's' },
|
||||||
|
{ key: 'loading', val: 'false', type: 'b' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DIFF = [
|
||||||
|
{ op: 'del', text: '− borderRadius: 8px' },
|
||||||
|
{ op: 'add', text: '+ borderRadius: 12px' },
|
||||||
|
{ op: 'del', text: '− accentColor: #2A6CD4' },
|
||||||
|
{ op: 'add', text: '+ accentColor: #0F52BA' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TYPE_COLORS: Record<string, string> = {
|
||||||
|
s: '#16a34a',
|
||||||
|
n: '#2563eb',
|
||||||
|
b: '#d97706',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Inspector() {
|
||||||
|
const { selectedArtboardId } = useCanvas();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gridColumn: 3,
|
||||||
|
gridRow: 2,
|
||||||
|
background: '#fafafa',
|
||||||
|
borderLeft: '1px solid rgba(0,0,0,0.08)',
|
||||||
|
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>
|
||||||
|
|
||||||
|
{selectedArtboardId ? (
|
||||||
|
<>
|
||||||
|
<Section label="Component Props">
|
||||||
|
{PROPS.map(({ key, val, type }) => (
|
||||||
|
<PropRow key={key} label={key} value={val} color={TYPE_COLORS[type] ?? '#555'} />
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
|
||||||
|
<Section label="Intent Diff">
|
||||||
|
{DIFF.map((d, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '3px 8px',
|
||||||
|
borderRadius: 3,
|
||||||
|
marginBottom: 3,
|
||||||
|
background: d.op === 'del' ? 'rgba(239,68,68,0.08)' : 'rgba(34,197,94,0.08)',
|
||||||
|
color: d.op === 'del' ? '#dc2626' : '#16a34a',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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
|
||||||
|
</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 }}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tree,
|
||||||
|
TreeItem,
|
||||||
|
TreeItemLayout,
|
||||||
|
Text,
|
||||||
|
} from '@fluentui/react-components';
|
||||||
|
import {
|
||||||
|
SquareRegular,
|
||||||
|
DocumentRegular,
|
||||||
|
FolderRegular,
|
||||||
|
FolderOpenRegular,
|
||||||
|
} from '@fluentui/react-icons';
|
||||||
|
import { useCanvas } from '@/store/canvas';
|
||||||
|
|
||||||
|
const ARTBOARDS = [
|
||||||
|
{ id: 'dashboard-card', label: 'DashboardCard' },
|
||||||
|
{ id: 'user-profile', label: 'UserProfile' },
|
||||||
|
{ id: 'nav-sidebar', label: 'NavSidebar' },
|
||||||
|
{ id: 'data-table', label: 'DataTable' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ArtboardNavigator() {
|
||||||
|
const { selectedArtboardId, selectArtboard } = useCanvas();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
gridColumn: 1,
|
||||||
|
gridRow: 2,
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRight: '1px solid rgba(0,0,0,0.08)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Artboards section */}
|
||||||
|
<SectionLabel>Artboards</SectionLabel>
|
||||||
|
<Tree aria-label="Artboards" size="small" style={{ padding: '0 4px' }}>
|
||||||
|
{ARTBOARDS.map((ab) => (
|
||||||
|
<TreeItem
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
<TreeItemLayout iconBefore={<SquareRegular style={{ color: '#0F52BA', fontSize: 12 }} />}>
|
||||||
|
<Text size={200}>{ab.label}</Text>
|
||||||
|
</TreeItemLayout>
|
||||||
|
</TreeItem>
|
||||||
|
))}
|
||||||
|
</Tree>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</TreeItemLayout>
|
||||||
|
<Tree>
|
||||||
|
<TreeItem itemType="branch" value="components">
|
||||||
|
<TreeItemLayout iconBefore={<FolderRegular style={{ fontSize: 12 }} />}>
|
||||||
|
<Text size={200}>components</Text>
|
||||||
|
</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)' }}>
|
||||||
|
{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>
|
||||||
|
</TreeItem>
|
||||||
|
</Tree>
|
||||||
|
</TreeItem>
|
||||||
|
</Tree>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionLabel({ children }: { children: string }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '10px 12px 4px',
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: 'rgba(0,0,0,0.35)',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Divider() {
|
||||||
|
return <div style={{ height: 1, background: 'rgba(0,0,0,0.06)', margin: '6px 0' }} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export type Tool = 'select' | 'pan' | 'artboard' | 'zone';
|
||||||
|
|
||||||
|
interface CanvasStore {
|
||||||
|
activeTool: Tool;
|
||||||
|
selectedArtboardId: string | null;
|
||||||
|
setActiveTool: (tool: Tool) => void;
|
||||||
|
selectArtboard: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCanvas = create<CanvasStore>((set) => ({
|
||||||
|
activeTool: 'select',
|
||||||
|
selectedArtboardId: 'dashboard-card',
|
||||||
|
setActiveTool: (tool) => set({ activeTool: tool }),
|
||||||
|
selectArtboard: (id) => set({ selectedArtboardId: id }),
|
||||||
|
}));
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
],
|
],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
|||||||
Generated
+3
@@ -64,6 +64,9 @@ importers:
|
|||||||
'@fluentui/react-components':
|
'@fluentui/react-components':
|
||||||
specifier: ^9.54.0
|
specifier: ^9.54.0
|
||||||
version: 9.73.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(scheduler@0.27.0)
|
version: 9.73.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(scheduler@0.27.0)
|
||||||
|
'@fluentui/react-icons':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.324(react@19.2.5)
|
||||||
'@originmain/diff-engine':
|
'@originmain/diff-engine':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../diff-engine
|
version: link:../diff-engine
|
||||||
|
|||||||
Reference in New Issue
Block a user