made system changes

This commit is contained in:
SinachPat
2026-04-23 13:46:15 +01:00
parent eb9c6c1f2c
commit dbe7c8cbb0
12 changed files with 644 additions and 1 deletions
+1
View File
@@ -14,6 +14,7 @@
"@originmain/origin-graph": "workspace:*",
"@originmain/renderer": "workspace:*",
"@fluentui/react-components": "^9.54.0",
"@fluentui/react-icons": "^2.0.0",
"next": "^15.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+7
View File
@@ -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' }} />;
}
+17
View File
@@ -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 }),
}));
+3
View File
@@ -8,6 +8,9 @@
],
"noEmit": true,
"allowJs": true,
"paths": {
"@/*": ["./src/*"]
},
"incremental": true,
"esModuleInterop": true,
"resolveJsonModule": true,