updated design

This commit is contained in:
SinachPat
2026-04-25 22:52:30 +01:00
parent df79ae0bca
commit e836ac6d2b
22 changed files with 3480 additions and 781 deletions
@@ -2,6 +2,9 @@
import { useState } from 'react';
import { useCanvas } from '@/store/canvas';
import { useDiff } from '@/hooks/useDiff';
import { ARTBOARD_SNAPSHOTS } from '@/data/artboard-snapshots';
import type { DiffResult, PropChange } from '@originmain/diff-engine';
const PROPS = [
{ key: 'title', val: '"Revenue Overview"', type: 's' },
@@ -11,15 +14,6 @@ const PROPS = [
{ 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: #0066FF' },
{ op: 'del', text: ' padding: 16' },
{ op: 'add', text: '+ padding: 20' },
];
const TYPE_COLORS: Record<string, string> = {
s: '#7DD3A8',
n: '#7EB8FF',
@@ -44,6 +38,7 @@ const T = {
export function Inspector() {
const { selectedArtboardId } = useCanvas();
const [tab, setTab] = useState<TabId>('props');
const diffResult = useDiff(selectedArtboardId);
return (
<div
@@ -132,37 +127,7 @@ export function Inspector() {
</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: "'JetBrains Mono', ui-monospace, monospace",
fontSize: '0.625rem',
padding: '4px 8px',
borderRadius: 4,
marginBottom: 3,
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>
<DiffTab artboardId={selectedArtboardId} diffResult={diffResult} />
) : (
<Section label="Origin Graph">
<GraphNode label="DashboardCard" depth={0} isRoot />
@@ -296,3 +261,92 @@ function GraphNode({ label, depth, isRoot }: { label: string; depth: number; isR
function HSep() {
return <div style={{ height: 1, background: 'rgba(255,255,255,0.04)', margin: '2px 0' }} />;
}
/* ── Diff tab ─────────────────────────────────────────────── */
function DiffTab({
artboardId,
diffResult,
}: {
artboardId: string | null;
diffResult: DiffResult | null;
}) {
if (!artboardId || !diffResult) {
return (
<Section label="Intent Diff">
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
No diff available
</span>
</Section>
);
}
const { diff } = diffResult;
const snapshots = ARTBOARD_SNAPSHOTS[artboardId];
const filename = snapshots?.after.filePath ?? `${diff.name}.tsx`;
const allChanges = [...diff.propChanges, ...diff.styleChanges];
const hunkCount = allChanges.filter(c => c.changeType !== 'unchanged').length;
return (
<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',
}}
>
{filename} · {hunkCount} change{hunkCount !== 1 ? 's' : ''}
</div>
{allChanges.map((change, i) => (
<DiffChangeRow key={i} change={change} />
))}
{allChanges.length === 0 && (
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '0.625rem', color: 'rgba(255,255,255,0.22)' }}>
No changes detected
</span>
)}
</Section>
);
}
function DiffChangeRow({ change }: { change: PropChange }) {
const isRemoved = change.changeType === 'removed';
const isAdded = change.changeType === 'added';
const isModified = change.changeType === 'modified';
const rows: Array<{ op: 'del' | 'add'; text: string }> = [];
if (isModified) {
rows.push({ op: 'del', text: ` ${change.key}: ${change.before}` });
rows.push({ op: 'add', text: `+ ${change.key}: ${change.after}` });
} else if (isRemoved) {
rows.push({ op: 'del', text: ` ${change.key}: ${change.before}` });
} else if (isAdded) {
rows.push({ op: 'add', text: `+ ${change.key}: ${change.after}` });
}
return (
<>
{rows.map((r, i) => (
<div
key={i}
style={{
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
fontSize: '0.625rem',
padding: '4px 8px',
borderRadius: 4,
marginBottom: 3,
lineHeight: 1.55,
background: r.op === 'del' ? 'rgba(255,70,70,0.08)' : 'rgba(70,220,120,0.08)',
color: r.op === 'del' ? '#FF8080' : '#7DDBA0',
borderLeft: `2px solid ${r.op === 'del' ? 'rgba(255,80,80,0.3)' : 'rgba(70,220,120,0.3)'}`,
}}
>
{r.text}
</div>
))}
</>
);
}
@@ -1,16 +1,9 @@
'use client';
import {
Tree,
TreeItem,
TreeItemLayout,
} from '@fluentui/react-components';
import {
SquareRegular,
DocumentRegular,
FolderRegular,
FolderOpenRegular,
} from '@fluentui/react-icons';
import { useState } from 'react';
import { useFileTree, FileTree, useFileTreeSelection } from '@pierre/trees/react';
import { themeToTreeStyles } from '@pierre/trees';
import { SquareRegular } from '@fluentui/react-icons';
import { useCanvas } from '@/store/canvas';
const ARTBOARDS = [
@@ -20,22 +13,55 @@ const ARTBOARDS = [
{ id: 'data-table', label: 'DataTable' },
];
// Dark panel tokens
const FILE_PATHS = [
'src/components/DashboardCard.tsx',
'src/components/UserProfile.tsx',
'src/components/NavSidebar.tsx',
'src/components/DataTable.tsx',
'src/components/StatsCard.tsx',
'src/app/canvas/page.tsx',
'src/app/layout.tsx',
'src/store/canvas.ts',
'src/store/viewport.ts',
];
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)',
bg: '#111115',
border: 'rgba(255,255,255,0.055)',
item: 'rgba(255,255,255,0.42)',
itemHov: 'rgba(255,255,255,0.72)',
selBg: 'rgba(51,133,255,0.12)',
selFg: 'rgba(255,255,255,0.88)',
accent: '#3385FF',
dim: 'rgba(255,255,255,0.22)',
sep: 'rgba(255,255,255,0.04)',
};
// Map the panel's dark theme into Trees' CSS custom properties
const treeThemeStyles = themeToTreeStyles({
type: 'dark',
bg: '#111115',
fg: 'rgba(255,255,255,0.42)',
colors: {
'editor.selectionBackground': 'rgba(51,133,255,0.14)',
'list.activeSelectionBackground': 'rgba(51,133,255,0.14)',
'list.inactiveSelectionBackground': 'rgba(51,133,255,0.08)',
'list.hoverBackground': 'rgba(255,255,255,0.04)',
'list.activeSelectionForeground': 'rgba(255,255,255,0.88)',
'editorIndentGuide.background': 'rgba(255,255,255,0.04)',
},
});
export function ArtboardNavigator() {
const { selectedArtboardId, selectArtboard } = useCanvas();
const { model } = useFileTree({
paths: FILE_PATHS,
initialExpansion: 2,
density: 'compact',
icons: 'minimal',
});
return (
<div
style={{
@@ -49,171 +75,138 @@ export function ArtboardNavigator() {
fontSize: 12,
}}
>
{/* Artboards section */}
<SectionLabel icon="⬜">Artboards</SectionLabel>
{/* ── Artboards ── */}
<SectionLabel>Artboards</SectionLabel>
<div style={{ padding: '2px 6px 0' }}>
{ARTBOARDS.map((ab) => (
<div
key={ab.id}
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;
{ARTBOARDS.map((ab) => {
const sel = selectedArtboardId === ab.id;
return (
<NavRow
key={ab.id}
selected={sel}
onClick={() => selectArtboard(ab.id)}
icon={
<SquareRegular
style={{ fontSize: 11, color: sel ? T.accent : 'rgba(255,255,255,0.2)', flexShrink: 0 }}
/>
}
}}
onMouseLeave={e => {
if (selectedArtboardId !== ab.id) {
(e.currentTarget as HTMLDivElement).style.background = 'transparent';
(e.currentTarget as HTMLDivElement).style.color = T.item;
}
}}
>
<SquareRegular
style={{
fontSize: 11,
color: selectedArtboardId === ab.id ? T.accent : 'rgba(255,255,255,0.25)',
flexShrink: 0,
}}
label={ab.label}
after={sel && <ActiveDot />}
/>
{ab.label}
{selectedArtboardId === ab.id && (
<span
style={{
marginLeft: 'auto',
width: 5,
height: 5,
borderRadius: '50%',
background: T.accent,
flexShrink: 0,
}}
/>
)}
</div>
))}
);
})}
</div>
<HSep />
{/* 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: 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: 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
</TreeItemLayout>
</TreeItem>
))}
</Tree>
</TreeItem>
<TreeItem itemType="leaf" value="app-page">
<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>
{/* ── Files — @pierre/trees ── */}
<SectionLabel>Files</SectionLabel>
<div style={{ flex: 1, overflow: 'hidden', minHeight: 0 }}>
<FileTree
model={model}
style={{
...treeThemeStyles,
height: '100%',
width: '100%',
// Fine-tune item sizing to match our panel density
'--trees-item-height': '26px',
'--trees-indent-width': '14px',
} as React.CSSProperties}
/>
</div>
<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)" />
{/* ── Graph stats ── */}
<SectionLabel>Graph</SectionLabel>
<div style={{ padding: '4px 14px 14px', display: 'flex', flexDirection: 'column', gap: 6, flexShrink: 0 }}>
<GraphStat label="nodes" value="284" color={T.accent} />
<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, icon }: { children: string; icon: string }) {
/* ── Artboard row ─────────────────────────────────────────── */
function NavRow({
selected = false,
onClick,
icon,
label,
after,
}: {
selected?: boolean;
onClick?: () => void;
icon: React.ReactNode;
label: string;
after?: React.ReactNode;
}) {
const [hov, setHov] = useState(false);
return (
<div
onClick={onClick}
onMouseEnter={() => setHov(true)}
onMouseLeave={() => setHov(false)}
style={{
padding: '12px 12px 5px',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
fontSize: '0.5625rem',
fontWeight: 500,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: 'rgba(255,255,255,0.22)',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
gap: 6,
gap: 7,
padding: '6px 10px',
borderRadius: 5,
cursor: 'pointer',
background: selected ? T.selBg : hov ? 'rgba(255,255,255,0.04)' : 'transparent',
color: selected ? T.selFg : hov ? T.itemHov : T.item,
fontSize: '0.75rem',
letterSpacing: '-0.01em',
fontWeight: selected ? 500 : 400,
userSelect: 'none',
marginBottom: 1,
transition: 'background 0.1s, color 0.1s',
}}
>
{icon}
<span style={{ flex: 1 }}>{label}</span>
{after}
</div>
);
}
/* ── Helpers ──────────────────────────────────────────────── */
function SectionLabel({ children }: { children: string }) {
return (
<div style={{
padding: '12px 12px 5px',
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
fontSize: '0.5625rem',
fontWeight: 500,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: T.dim,
userSelect: 'none',
flexShrink: 0,
}}>
{children}
</div>
);
}
function HSep() {
return <div style={{ height: 1, background: T.sep, margin: '6px 0', flexShrink: 0 }} />;
}
function ActiveDot() {
return (
<div
style={{
height: 1,
background: 'rgba(255,255,255,0.04)',
margin: '8px 0',
flexShrink: 0,
}}
/>
<span style={{
marginLeft: 'auto',
width: 5,
height: 5,
borderRadius: '50%',
background: T.accent,
flexShrink: 0,
display: 'block',
}} />
);
}
+147
View File
@@ -0,0 +1,147 @@
import type { ComponentSnapshot } from '@originmain/diff-engine';
interface ArtboardSnapshots {
before: ComponentSnapshot;
after: ComponentSnapshot;
}
const dashboardCard: ArtboardSnapshots = {
before: {
id: 'dashboard-card',
name: 'DashboardCard',
filePath: 'src/components/DashboardCard.tsx',
props: {
title: 'Revenue Overview',
value: '$12,450',
delta: 2.4,
period: 'monthly',
loading: false,
},
styles: {
borderRadius: '8px',
accentColor: '#2A6CD4',
padding: '16',
},
},
after: {
id: 'dashboard-card',
name: 'DashboardCard',
filePath: 'src/components/DashboardCard.tsx',
props: {
title: 'Revenue Overview',
value: '$12,450',
delta: 2.4,
period: 'monthly',
loading: false,
},
styles: {
borderRadius: '12px',
accentColor: '#0066FF',
padding: '20',
},
},
};
const userProfile: ArtboardSnapshots = {
before: {
id: 'user-profile',
name: 'UserProfile',
filePath: 'src/components/UserProfile.tsx',
props: {
name: 'Sarah Chen',
role: 'Designer',
plan: 'Pro',
avatarSize: 40,
},
styles: {
avatarGradient: 'linear-gradient(135deg, #6D28D9, #2563EB)',
buttonVariant: 'outline',
},
},
after: {
id: 'user-profile',
name: 'UserProfile',
filePath: 'src/components/UserProfile.tsx',
props: {
name: 'Sarah Chen',
role: 'Design Engineer',
plan: 'Team',
avatarSize: 52,
},
styles: {
avatarGradient: 'linear-gradient(135deg, #7C3AED, #0066FF)',
buttonVariant: 'ghost',
},
},
};
const navSidebar: ArtboardSnapshots = {
before: {
id: 'nav-sidebar',
name: 'NavSidebar',
filePath: 'src/components/NavSidebar.tsx',
props: {
brand: 'Origin',
collapsed: false,
activeItem: 'Dashboard',
},
styles: {
background: '#F5F5F5',
itemPadding: '6px 10px',
borderRadius: '4px',
},
},
after: {
id: 'nav-sidebar',
name: 'NavSidebar',
filePath: 'src/components/NavSidebar.tsx',
props: {
brand: 'Originmain',
collapsed: false,
activeItem: 'Dashboard',
},
styles: {
background: '#FAFAFA',
itemPadding: '7px 12px',
borderRadius: '5px',
},
},
};
const dataTable: ArtboardSnapshots = {
before: {
id: 'data-table',
name: 'DataTable',
filePath: 'src/components/DataTable.tsx',
props: {
title: 'Components',
striped: false,
pageSize: 10,
},
styles: {
headerColor: '#71717A',
rowHeight: '32px',
},
},
after: {
id: 'data-table',
name: 'DataTable',
filePath: 'src/components/DataTable.tsx',
props: {
title: 'Component Inventory',
striped: true,
pageSize: 10,
},
styles: {
headerColor: '#A1A1AA',
rowHeight: '36px',
},
},
};
export const ARTBOARD_SNAPSHOTS: Record<string, ArtboardSnapshots> = {
'dashboard-card': dashboardCard,
'user-profile': userProfile,
'nav-sidebar': navSidebar,
'data-table': dataTable,
};
+12
View File
@@ -0,0 +1,12 @@
import { useMemo } from 'react';
import { diffComponents, type DiffResult } from '@originmain/diff-engine';
import { ARTBOARD_SNAPSHOTS } from '@/data/artboard-snapshots';
export function useDiff(artboardId: string | null): DiffResult | null {
return useMemo(() => {
if (!artboardId) return null;
const snapshots = ARTBOARD_SNAPSHOTS[artboardId];
if (!snapshots) return null;
return diffComponents(snapshots.before, snapshots.after);
}, [artboardId]);
}