feat: add FreeCAD-aligned web CAD frontend baseline
This commit is contained in:
441
src/App.tsx
Normal file
441
src/App.tsx
Normal file
@@ -0,0 +1,441 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Archive,
|
||||
ArrowDownToLine,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Box,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Circle,
|
||||
CircleDot,
|
||||
Clock3,
|
||||
Code2,
|
||||
Command,
|
||||
Copy,
|
||||
Download,
|
||||
FileBox,
|
||||
FilePlus2,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
HardDrive,
|
||||
HelpCircle,
|
||||
Layers3,
|
||||
LayoutGrid,
|
||||
ListTree,
|
||||
Menu,
|
||||
MoreHorizontal,
|
||||
PanelBottom,
|
||||
PanelLeft,
|
||||
PanelRight,
|
||||
Pause,
|
||||
Pencil,
|
||||
Plus,
|
||||
Rotate3D,
|
||||
Save,
|
||||
Search,
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
SquareStack,
|
||||
Trash2,
|
||||
Undo2,
|
||||
Redo2,
|
||||
Upload,
|
||||
UserRound,
|
||||
WandSparkles,
|
||||
X,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from 'lucide-react'
|
||||
import { menuDefinitions, pinnedWorkbenches, workbenchDefinitions, type MenuName, type WorkbenchId } from './freecadManifest'
|
||||
|
||||
type Page = 'start' | 'projects' | 'workspace' | 'import' | 'export' | 'settings' | 'help' | 'diagnostics' | 'sync'
|
||||
type Workbench = WorkbenchId
|
||||
|
||||
type Icon = ComponentType<{ size?: string | number; strokeWidth?: string | number; className?: string }>
|
||||
|
||||
const pageFromPath = (): Page => {
|
||||
const path = window.location.pathname
|
||||
if (path.startsWith('/projects')) return 'projects'
|
||||
if (path.startsWith('/workspace')) return 'workspace'
|
||||
if (path.startsWith('/import')) return 'import'
|
||||
if (path.startsWith('/export')) return 'export'
|
||||
if (path.startsWith('/settings')) return 'settings'
|
||||
if (path.startsWith('/help')) return 'help'
|
||||
if (path.startsWith('/diagnostics')) return 'diagnostics'
|
||||
if (path.startsWith('/sync')) return 'sync'
|
||||
return 'start'
|
||||
}
|
||||
|
||||
const iconMap: Record<string, Icon> = {
|
||||
box: Box, 'circle-dot': CircleDot, 'square-stack': SquareStack, 'file-text': FileText, 'layout-grid': LayoutGrid,
|
||||
layers: Layers3, archive: Archive, pencil: Pencil, copy: Copy, 'arrow-up': ArrowDownToLine, 'arrow-down': ArrowDownToLine,
|
||||
rotate: Rotate3D, 'sliders-horizontal': SlidersHorizontal, search: Search, upload: Upload, download: Download, 'file-plus': FilePlus2,
|
||||
'code-2': Code2, 'settings-2': Settings2, 'circle-check': CheckCircle2, 'triangle-alert': AlertTriangle, eye: CircleDot,
|
||||
'zoom-in': ZoomIn, 'grid-3x3': LayoutGrid, play: Check, plus: Plus, minus: X, circle: Circle, route: Rotate3D, 'git-branch': Layers3,
|
||||
'layers-2': Layers3, 'flip-horizontal': ArrowLeft, repeat: Rotate3D, 'refresh-cw': Rotate3D, shuffle: MoreHorizontal,
|
||||
'corner-down-right': SlidersHorizontal, scissors: X, triangle: Box, paperclip: Copy, 'move-horizontal': ArrowRight, 'move-vertical': ArrowDownToLine,
|
||||
magnet: CircleDot, move: ArrowRight, type: FileText, 'panels-top-left': LayoutGrid, maximize: PanelRight, table: LayoutGrid, tag: FileText,
|
||||
'rows-3': ListTree, link: Copy, paintbrush: WandSparkles, pin: PanelLeft, anchor: CircleDot, split: ArrowRight, target: CircleDot,
|
||||
protractor: SlidersHorizontal, activity: Rotate3D, lock: Circle, spline: Rotate3D, crosshair: CircleDot, 'file-box': FileBox,
|
||||
}
|
||||
|
||||
const workbenchIconKeys: Record<Workbench, string> = {
|
||||
'Part Design': 'box', Part: 'box', Sketcher: 'circle-dot', Draft: 'square-stack', BIM: 'layers', TechDraw: 'file-text', Spreadsheet: 'layout-grid', Assembly: 'layers', CAM: 'settings-2', FEM: 'activity', Mesh: 'archive', Surface: 'spline', Inspection: 'search',
|
||||
}
|
||||
|
||||
const workbenchMeta: Record<Workbench, { icon: Icon; accent: string; description: string }> = Object.fromEntries(
|
||||
Object.entries(workbenchDefinitions).map(([id, definition]) => [id, { icon: iconMap[workbenchIconKeys[id as Workbench]] || MoreHorizontal, accent: id === 'Sketcher' ? 'violet' : id === 'Part' ? 'amber' : 'cyan', description: definition.description }]),
|
||||
) as Record<Workbench, { icon: Icon; accent: string; description: string }>
|
||||
|
||||
function CuboidIcon({ size = 18, strokeWidth = 1.8, className }: { size?: string | number; strokeWidth?: string | number; className?: string }) {
|
||||
return <Box size={size} strokeWidth={strokeWidth} className={className} />
|
||||
}
|
||||
|
||||
const projects = [
|
||||
{ name: 'Pump Housing', path: 'Local / Mechanical / Pump Housing', modified: 'Just now', objects: 18, state: 'Unsaved' },
|
||||
{ name: 'Mounting Bracket', path: 'Local / Concepts / Mounting Bracket', modified: 'Yesterday', objects: 9, state: 'Saved' },
|
||||
{ name: 'Motor Assembly', path: 'Local / Assemblies / Motor Assembly', modified: '28 Jul 2026', objects: 142, state: 'Saved' },
|
||||
{ name: 'Valve Body Study', path: 'Shared / Review / Valve Body Study', modified: '22 Jul 2026', objects: 31, state: 'Read only' },
|
||||
]
|
||||
|
||||
const treeData = [
|
||||
{ id: 'origin', label: 'Origin', type: 'folder', children: ['XY_Plane', 'XZ_Plane', 'YZ_Plane'] },
|
||||
{ id: 'body', label: 'Body', type: 'body', state: 'active', children: ['sketch', 'pad', 'pocket', 'fillet'] },
|
||||
{ id: 'sketch', label: 'Sketch', type: 'sketch', state: 'valid', detail: 'Fully constrained' },
|
||||
{ id: 'pad', label: 'Pad', type: 'feature', state: 'valid', detail: 'Length 42 mm' },
|
||||
{ id: 'pocket', label: 'Pocket', type: 'feature', state: 'warning', detail: 'Through all' },
|
||||
{ id: 'fillet', label: 'Fillet', type: 'feature', state: 'valid', detail: 'Radius 3 mm' },
|
||||
{ id: 'reference', label: 'Reference geometry', type: 'folder', children: ['DatumPlane', 'DatumAxis'] },
|
||||
]
|
||||
|
||||
const IconButton = ({ icon: IconComponent, label, onClick, active = false, disabled = false }: { icon: Icon; label: string; onClick?: () => void; active?: boolean; disabled?: boolean }) => (
|
||||
<button className={`icon-button ${active ? 'is-active' : ''}`} title={label} aria-label={label} onClick={onClick} disabled={disabled}>
|
||||
<IconComponent size={16} strokeWidth={1.8} />
|
||||
</button>
|
||||
)
|
||||
|
||||
const Badge = ({ tone, children }: { tone: 'cyan' | 'green' | 'amber' | 'red' | 'muted'; children: ReactNode }) => (
|
||||
<span className={`badge badge-${tone}`}>
|
||||
<span className="badge-dot" />
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = useState<Page>(pageFromPath)
|
||||
const [workbench, setWorkbench] = useState<Workbench>('Part Design')
|
||||
const [leftTab, setLeftTab] = useState<'model' | 'tasks'>('model')
|
||||
const [rightTab, setRightTab] = useState<'data' | 'view'>('data')
|
||||
const [bottomOpen, setBottomOpen] = useState(true)
|
||||
const [selectedObject, setSelectedObject] = useState('pad')
|
||||
const [notice, setNotice] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = () => setPage(pageFromPath())
|
||||
window.addEventListener('popstate', handlePopState)
|
||||
return () => window.removeEventListener('popstate', handlePopState)
|
||||
}, [])
|
||||
|
||||
const navigate = (next: Page, replace = false) => {
|
||||
const paths: Record<Page, string> = {
|
||||
start: '/start',
|
||||
projects: '/projects',
|
||||
workspace: '/workspace/pump-housing',
|
||||
import: '/import',
|
||||
export: '/export',
|
||||
settings: '/settings',
|
||||
help: '/help',
|
||||
diagnostics: '/diagnostics',
|
||||
sync: '/sync',
|
||||
}
|
||||
window.history[replace ? 'replaceState' : 'pushState']({}, '', paths[next])
|
||||
setPage(next)
|
||||
}
|
||||
|
||||
const openWorkspace = () => navigate('workspace')
|
||||
const currentWorkbench = workbenchMeta[workbench]
|
||||
const CurrentIcon = currentWorkbench.icon
|
||||
|
||||
const showNotice = (message: string) => {
|
||||
setNotice(message)
|
||||
window.setTimeout(() => setNotice(''), 2600)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<TopBar page={page} workbench={workbench} onNavigate={navigate} onOpenWorkspace={openWorkspace} />
|
||||
{page === 'workspace' ? (
|
||||
<Workspace
|
||||
workbench={workbench}
|
||||
setWorkbench={(value) => { setWorkbench(value); setLeftTab('tasks'); showNotice(`${value} workbench loaded`) }}
|
||||
leftTab={leftTab}
|
||||
setLeftTab={setLeftTab}
|
||||
rightTab={rightTab}
|
||||
setRightTab={setRightTab}
|
||||
bottomOpen={bottomOpen}
|
||||
setBottomOpen={setBottomOpen}
|
||||
selectedObject={selectedObject}
|
||||
setSelectedObject={setSelectedObject}
|
||||
workbenchIcon={CurrentIcon}
|
||||
showNotice={showNotice}
|
||||
/>
|
||||
) : (
|
||||
<PageFrame page={page} onNavigate={navigate} onOpenWorkspace={openWorkspace} showNotice={showNotice} />
|
||||
)}
|
||||
{notice && <div className="toast"><CheckCircle2 size={16} />{notice}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TopBar({ page, workbench, onNavigate, onOpenWorkspace }: { page: Page; workbench: Workbench; onNavigate: (page: Page) => void; onOpenWorkspace: () => void }) {
|
||||
const [openMenu, setOpenMenu] = useState<MenuName | null>(null)
|
||||
const handleMenuCommand = (command: string) => {
|
||||
setOpenMenu(null)
|
||||
if (command === 'help' || command === 'shortcuts') onNavigate('help')
|
||||
else if (command === 'diagnostics') onNavigate('diagnostics')
|
||||
else if (command === 'preferences' || command === 'project-settings' || command === 'parameters') onNavigate('settings')
|
||||
else if (command === 'import') onNavigate('import')
|
||||
else if (command === 'export') onNavigate('export')
|
||||
else if (command === 'open') onNavigate('projects')
|
||||
else onOpenWorkspace()
|
||||
}
|
||||
return (
|
||||
<header className="topbar">
|
||||
<div className="brand-lockup" onClick={() => onNavigate('start')} role="button" tabIndex={0}>
|
||||
<div className="brand-mark"><span /><span /><span /></div>
|
||||
<div><div className="brand-name">BitBybit</div><div className="brand-product">CAD studio</div></div>
|
||||
</div>
|
||||
<nav className="top-menu" aria-label="Application menu">
|
||||
{(Object.keys(menuDefinitions) as MenuName[]).map((menu) => <div className="menu-wrapper" key={menu}><button className={openMenu === menu ? 'is-open' : ''} onClick={() => setOpenMenu(openMenu === menu ? null : menu)} aria-expanded={openMenu === menu}>{menu}</button>{openMenu === menu && <div className="menu-popover">{menuDefinitions[menu].map((item) => <button key={item.command} onClick={() => handleMenuCommand(item.command)}><span>{item.label}</span>{'shortcut' in item && <kbd>{item.shortcut}</kbd>}</button>)}</div>}</div>)}
|
||||
</nav>
|
||||
<div className="topbar-spacer" />
|
||||
<div className="topbar-context">
|
||||
<div className="context-document"><span className="context-dot" />Pump Housing <span className="context-separator">/</span> {workbench}</div>
|
||||
<span className="save-state"><span className="save-dot" />Unsaved changes</span>
|
||||
</div>
|
||||
<IconButton icon={Undo2} label="Undo" />
|
||||
<IconButton icon={Redo2} label="Redo" disabled />
|
||||
<div className="topbar-divider" />
|
||||
<button className="command-search" onClick={() => onNavigate('help')}><Search size={15} /><span>Search commands</span><kbd>⌘ K</kbd></button>
|
||||
<IconButton icon={HelpCircle} label="Help" onClick={() => onNavigate('help')} />
|
||||
<button className="avatar-button" title="Account and sync" onClick={() => onNavigate('sync')}><UserRound size={16} /><span>MS</span></button>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function Workspace({ workbench, setWorkbench, leftTab, setLeftTab, rightTab, setRightTab, bottomOpen, setBottomOpen, selectedObject, setSelectedObject, workbenchIcon: WorkbenchIcon, showNotice }: {
|
||||
workbench: Workbench
|
||||
setWorkbench: (value: Workbench) => void
|
||||
leftTab: 'model' | 'tasks'
|
||||
setLeftTab: (tab: 'model' | 'tasks') => void
|
||||
rightTab: 'data' | 'view'
|
||||
setRightTab: (tab: 'data' | 'view') => void
|
||||
bottomOpen: boolean
|
||||
setBottomOpen: (open: boolean) => void
|
||||
selectedObject: string
|
||||
setSelectedObject: (id: string) => void
|
||||
workbenchIcon: Icon
|
||||
showNotice: (message: string) => void
|
||||
}) {
|
||||
return (
|
||||
<main className="workspace-page">
|
||||
<div className="workspace-toolbar">
|
||||
<div className="workbench-picker"><WorkbenchIcon size={17} /><select value={workbench} onChange={(event) => setWorkbench(event.target.value as Workbench)} aria-label="Workbench">{Object.keys(workbenchDefinitions).map((option) => <option key={option}>{option}</option>)}</select><ChevronDown size={14} /></div>
|
||||
<div className="toolbar-divider" />
|
||||
<IconButton icon={FilePlus2} label="New document" onClick={() => showNotice('New document prepared')} />
|
||||
<IconButton icon={FolderOpen} label="Open project" onClick={() => showNotice('Project browser opened')} />
|
||||
<IconButton icon={Save} label="Save project" onClick={() => showNotice('Saved to local workspace')} />
|
||||
<div className="toolbar-divider" />
|
||||
<IconButton icon={Box} label="Create body" onClick={() => showNotice('Create Body task opened')} active={workbench === 'Part Design'} />
|
||||
<IconButton icon={CircleDot} label="Create sketch" onClick={() => showNotice('Select a plane to create a sketch')} />
|
||||
<IconButton icon={Rotate3D} label="Transform" />
|
||||
<IconButton icon={SlidersHorizontal} label="Measure" />
|
||||
<div className="toolbar-spacer" />
|
||||
<div className="toolbar-status"><span className="status-pulse" />Local workspace</div>
|
||||
<IconButton icon={PanelLeft} label="Toggle left panel" />
|
||||
<IconButton icon={PanelRight} label="Toggle right panel" />
|
||||
</div>
|
||||
<WorkbenchNavBar workbench={workbench} setWorkbench={setWorkbench} showNotice={showNotice} />
|
||||
<div className="document-tabs">
|
||||
<button className="doc-tab is-active"><FileBox size={15} /><span>Pump Housing</span><span className="doc-unsaved" /><X size={13} /></button>
|
||||
<button className="doc-tab" onClick={() => showNotice('Open Mounting Bracket') }><FileBox size={15} /><span>Mounting Bracket</span><X size={13} /></button>
|
||||
<button className="new-tab" title="New document"><Plus size={16} /></button>
|
||||
<div className="document-tab-spacer" />
|
||||
<span className="document-meta">mm · 18 objects · v0.1 draft</span>
|
||||
</div>
|
||||
<div className="workspace-content">
|
||||
<aside className="combo-panel left-panel">
|
||||
<div className="panel-tabs"><button className={leftTab === 'model' ? 'is-active' : ''} onClick={() => setLeftTab('model')}><ListTree size={14} />Model</button><button className={leftTab === 'tasks' ? 'is-active' : ''} onClick={() => setLeftTab('tasks')}><SlidersHorizontal size={14} />Tasks <span className="tab-count">1</span></button></div>
|
||||
{leftTab === 'model' ? <div className="combo-model"><ModelTree selectedObject={selectedObject} setSelectedObject={setSelectedObject} showNotice={showNotice} /><div className="combo-property"><div className="property-heading"><div><span className="eyebrow">Property view</span><h2>{selectedObject === 'pad' ? 'Pad' : selectedObject === 'pocket' ? 'Pocket' : 'Body'}</h2></div><IconButton icon={MoreHorizontal} label="More object actions" /></div><div className="panel-tabs property-tabs"><button className={rightTab === 'data' ? 'is-active' : ''} onClick={() => setRightTab('data')}>Data</button><button className={rightTab === 'view' ? 'is-active' : ''} onClick={() => setRightTab('view')}>View</button></div>{rightTab === 'data' ? <DataProperties selectedObject={selectedObject} showNotice={showNotice} /> : <ViewProperties showNotice={showNotice} />}</div></div> : <div className="model-task-summary"><span className="eyebrow">Combo View task tab</span><p>Use the right Task panel for command parameters. This tab stays available for selection and dependency context.</p><button className="button button-outline" onClick={() => showNotice('Selection filter enabled')}><Search size={14} />Selection filter</button></div>}
|
||||
</aside>
|
||||
<section className="viewport-region">
|
||||
<Viewport selectedObject={selectedObject} setSelectedObject={setSelectedObject} workbench={workbench} showNotice={showNotice} />
|
||||
<div className="viewport-bottom-left"><div className="view-chip"><Circle size={8} fill="currentColor" /> Perspective</div><div className="view-chip">Grid 10 mm</div></div>
|
||||
<div className="viewport-bottom-right"><div className="axis-widget"><span className="axis-x">X</span><span className="axis-y">Y</span><span className="axis-z">Z</span><div className="axis-origin" /></div></div>
|
||||
</section>
|
||||
<aside className="task-dock right-panel"><div className="task-dock-title"><div><span className="eyebrow">Task panel</span><h2>{workbenchDefinitions[workbench].taskTitle}</h2></div><IconButton icon={MoreHorizontal} label="Task panel actions" /></div><TaskPanel workbench={workbench} showNotice={showNotice} /></aside>
|
||||
<FunctionRail workbench={workbench} showNotice={showNotice} />
|
||||
</div>
|
||||
<div className={`bottom-drawer ${bottomOpen ? 'is-open' : ''}`}>
|
||||
<div className="bottom-drawer-header"><div className="drawer-tabs"><button className="is-active"><PanelBottom size={14} />Report view <span className="tab-count">2</span></button><button><Clock3 size={14} />Jobs <span className="tab-count tab-count-green">1</span></button><button><Code2 size={14} />Diagnostics</button></div><button className="drawer-toggle" onClick={() => setBottomOpen(!bottomOpen)} title={bottomOpen ? 'Collapse bottom panel' : 'Expand bottom panel'}>{bottomOpen ? <ChevronDown size={15} /> : <ChevronRight size={15} />}</button></div>
|
||||
{bottomOpen && <div className="bottom-drawer-content"><div className="report-line"><CheckCircle2 size={14} className="icon-green" /><span>Recompute completed</span><span className="report-context">Document / Body / Fillet</span><span className="report-time">142 ms</span></div><div className="report-line"><AlertTriangle size={14} className="icon-amber" /><span>Reference face may change after Pocket edit</span><button onClick={() => showNotice('Dependency review opened')}>Review dependency</button><span className="report-time">just now</span></div></div>}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkbenchNavBar({ workbench, setWorkbench, showNotice }: { workbench: Workbench; setWorkbench: (value: Workbench) => void; showNotice: (message: string) => void }) {
|
||||
return <div className="workbench-nav" aria-label="Workbench navigation"><span className="nav-caption">Workbench</span>{pinnedWorkbenches.map((item) => { const IconComponent = workbenchMeta[item].icon; return <button key={item} className={workbench === item ? 'is-active' : ''} onClick={() => { setWorkbench(item); showNotice(`${item} workbench loaded`) }}><IconComponent size={13} /><span>{item}</span></button> })}<button className="nav-more" onClick={() => showNotice('All workbenches available from the picker')}><MoreHorizontal size={14} /><span>More workbenches</span></button></div>
|
||||
}
|
||||
|
||||
function FunctionRail({ workbench, showNotice }: { workbench: Workbench; showNotice: (message: string) => void }) {
|
||||
const groups = workbenchDefinitions[workbench].groups
|
||||
return <aside className="function-rail" aria-label={`${workbench} commands`}>{groups.map((group) => <div className="rail-group" key={group.label}><span className="rail-group-label">{group.label}</span>{group.commands.slice(0, 5).map((item) => { const IconComponent = iconMap[item.icon] || MoreHorizontal; return <button key={item.id} title={`${item.label}${item.shortcut ? ` (${item.shortcut})` : ''}`} aria-label={item.label} onClick={() => showNotice(`${item.label} command staged`)}><IconComponent size={15} /></button> })}</div>)}</aside>
|
||||
}
|
||||
|
||||
function ModelTree({ selectedObject, setSelectedObject, showNotice }: { selectedObject: string; setSelectedObject: (id: string) => void; showNotice: (message: string) => void }) {
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({ origin: false, body: true, reference: false })
|
||||
return <div className="model-tree">
|
||||
<div className="tree-toolbar"><div className="tree-search"><Search size={14} /><input placeholder="Filter objects" /></div><IconButton icon={MoreHorizontal} label="Model tree actions" /></div>
|
||||
<div className="tree-root"><div className="tree-document-row"><ChevronDown size={14} /><FileBox size={15} className="icon-cyan" /><span>Pump Housing</span><span className="tree-version">v18</span></div>
|
||||
{treeData.map((item) => <TreeItem key={item.id} item={item} level={1} expanded={expanded[item.id]} onToggle={() => setExpanded((current) => ({ ...current, [item.id]: !current[item.id] }))} selectedObject={selectedObject} setSelectedObject={setSelectedObject} showNotice={showNotice} />)}
|
||||
</div>
|
||||
<div className="tree-footer"><span><span className="legend-dot valid" />Valid</span><span><span className="legend-dot warning" />Needs review</span></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function TreeItem({ item, level, expanded, onToggle, selectedObject, setSelectedObject, showNotice }: { item: typeof treeData[number]; level: number; expanded?: boolean; onToggle: () => void; selectedObject: string; setSelectedObject: (id: string) => void; showNotice: (message: string) => void }) {
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
const itemIcon = item.type === 'body' ? Box : item.type === 'sketch' ? CircleDot : item.type === 'feature' ? SquareStack : Layers3
|
||||
const ItemIcon = itemIcon
|
||||
return <>
|
||||
<button className={`tree-row ${selectedObject === item.id ? 'is-selected' : ''} ${item.state === 'active' ? 'is-active' : ''}`} style={{ paddingLeft: `${level * 14}px` }} onClick={() => { setSelectedObject(item.id); if (item.state === 'warning') showNotice('This feature has a reference warning') }} onDoubleClick={() => showNotice(`Editing ${item.label}`)}>
|
||||
{hasChildren ? <span onClick={(event) => { event.stopPropagation(); onToggle() }}>{expanded ? <ChevronDown size={13} /> : <ChevronRight size={13} />}</span> : <span className="tree-spacer" />}
|
||||
<ItemIcon size={15} className={item.type === 'body' ? 'icon-cyan' : item.state === 'warning' ? 'icon-amber' : 'icon-muted'} />
|
||||
<span className="tree-label">{item.label}</span>
|
||||
{item.state === 'warning' && <AlertTriangle size={13} className="icon-amber" />}
|
||||
{item.state === 'active' && <span className="active-marker" />}
|
||||
</button>
|
||||
{expanded && hasChildren && <div className="tree-children">{item.children?.map((child) => <div className="tree-row tree-child" key={child}><span className="tree-spacer" /><Circle size={8} className="icon-muted" /><span className="tree-label">{child}</span></div>)}</div>}
|
||||
</>
|
||||
}
|
||||
|
||||
function TaskPanel({ workbench, showNotice }: { workbench: Workbench; showNotice: (message: string) => void }) {
|
||||
const definition = workbenchDefinitions[workbench]
|
||||
const isSketch = workbench === 'Sketcher'
|
||||
return <div className="task-panel"><div className="task-actions-top"><button className="button button-primary" onClick={() => showNotice('Task accepted')}>OK</button><button className="button button-outline" onClick={() => showNotice('Preview applied')}>Apply</button><button className="button button-quiet" onClick={() => showNotice('Task cancelled')}>Cancel</button></div><div className="task-header"><div className="task-icon"><Pencil size={16} /></div><div><span className="eyebrow">Active command</span><h2>{isSketch ? 'Edit Sketch' : definition.taskTitle}</h2></div><Badge tone="cyan">Preview</Badge></div><div className="task-body"><div className="task-step"><span className="step-index">1</span><div><strong>{isSketch ? 'Geometry and constraints' : definition.objectType}</strong><span>{definition.taskSummary}</span></div></div><label className="field-label">Primary value <span className="field-unit">mm</span><input className="field-input" value={isSketch ? 'Fully constrained' : '42.00'} readOnly /></label><label className="field-label">Operation<select className="field-input"><option>{workbench === 'Part Design' ? 'Dimension' : 'Contextual preview'}</option><option>Through all</option><option>Up to face</option></select></label><label className="check-row"><input type="checkbox" defaultChecked /><span>Preview result in viewport</span></label><div className="task-note"><AlertTriangle size={14} /><span>Changes remain local until the document is recomputed.</span></div></div></div>
|
||||
}
|
||||
|
||||
function DataProperties({ selectedObject, showNotice }: { selectedObject: string; showNotice: (message: string) => void }) {
|
||||
const isPocket = selectedObject === 'pocket'
|
||||
return <div className="properties-scroll"><div className="property-group"><div className="property-group-title">Identity <ChevronDown size={14} /></div><PropertyRow label="Label" value={isPocket ? 'Pocket' : 'Pad'} editable onClick={() => showNotice('Label editor opened')} /><PropertyRow label="Type" value={isPocket ? 'PartDesign::Pocket' : 'PartDesign::Pad'} /><PropertyRow label="Status" value={isPocket ? 'Warning' : 'Valid'} tone={isPocket ? 'amber' : 'green'} /></div><div className="property-group"><div className="property-group-title">Parameters <ChevronDown size={14} /></div><PropertyRow label="Length" value={isPocket ? 'Through all' : '42.00 mm'} editable onClick={() => showNotice('Length editor opened')} /><PropertyRow label="Profile" value="Sketch" link /><PropertyRow label="Direction" value="Normal sketch axis" /><PropertyRow label="Reversed" value="false" editable onClick={() => showNotice('Boolean editor opened')} /></div><div className="property-group"><div className="property-group-title">Dependencies <ChevronDown size={14} /></div><PropertyRow label="Base" value="Body" link /><PropertyRow label="Support" value="XY_Plane" link /><PropertyRow label="Children" value="Pocket, Fillet" link /></div><div className="property-group"><div className="property-group-title">Expressions <ChevronDown size={14} /></div><div className="expression-row"><Code2 size={13} /><span>Length</span><span className="expression-value">42 mm</span><button title="Edit expression" onClick={() => showNotice('Expression editor opened')}><Pencil size={13} /></button></div></div></div>
|
||||
}
|
||||
|
||||
function PropertyRow({ label, value, editable = false, link = false, tone, onClick }: { label: string; value: string; editable?: boolean; link?: boolean; tone?: 'amber' | 'green'; onClick?: () => void }) {
|
||||
return <div className="property-row"><span className="property-label">{label}</span><button className={`property-value ${editable ? 'is-editable' : ''} ${link ? 'is-link' : ''}`} onClick={onClick} disabled={!editable && !link}>{value}{tone && <span className={`status-pill ${tone}`}>{tone === 'green' ? 'OK' : 'Review'}</span>}</button></div>
|
||||
}
|
||||
|
||||
function ViewProperties({ showNotice }: { showNotice: (message: string) => void }) {
|
||||
return <div className="properties-scroll"><div className="property-group"><div className="property-group-title">Display <ChevronDown size={14} /></div><PropertyRow label="Visibility" value="Visible" editable onClick={() => showNotice('Visibility toggled')} /><PropertyRow label="Display mode" value="Flat lines" editable onClick={() => showNotice('Display mode menu opened')} /><PropertyRow label="Transparency" value="0 %" editable onClick={() => showNotice('Transparency editor opened')} /></div><div className="property-group"><div className="property-group-title">Appearance <ChevronDown size={14} /></div><PropertyRow label="Shape color" value="Graphite / 02" editable onClick={() => showNotice('Color picker opened')} /><PropertyRow label="Line color" value="Steel / 04" editable onClick={() => showNotice('Color picker opened')} /><PropertyRow label="Line width" value="1.0 px" editable onClick={() => showNotice('Line width editor opened')} /></div><div className="property-group"><div className="property-group-title">View state <ChevronDown size={14} /></div><PropertyRow label="Selection style" value="Object + edges" /><PropertyRow label="Tessellation" value="Adaptive" /><PropertyRow label="Render cache" value="Ready" tone="green" /></div></div>
|
||||
}
|
||||
|
||||
function Viewport({ selectedObject, setSelectedObject, workbench, showNotice }: { selectedObject: string; setSelectedObject: (id: string) => void; workbench: Workbench; showNotice: (message: string) => void }) {
|
||||
return <div className="viewport"><div className="viewport-header"><div className="viewport-title"><span className="eyebrow">{workbench}</span><strong>Body / Fillet</strong></div><div className="viewport-actions"><IconButton icon={ZoomOut} label="Zoom out" /><IconButton icon={ZoomIn} label="Zoom in" /><IconButton icon={Rotate3D} label="Orbit view" active /></div></div><div className="viewport-grid" onClick={() => setSelectedObject('')}><div className="grid-lines" /><div className={`mock-part ${selectedObject === 'pad' ? 'part-selected' : ''}`} onClick={(event) => { event.stopPropagation(); setSelectedObject('pad'); showNotice('Selected Pad') }}><div className="part-top" /><div className="part-front"><span className="part-hole one" /><span className="part-hole two" /><span className="part-cut" /></div><div className="part-side" /></div><div className="mock-datum-plane" /><div className="selection-callout"><span className="callout-line" /><span>Pad</span></div></div><div className="viewport-legend"><span><span className="legend-swatch selected" />Selected</span><span><span className="legend-swatch edge" />Edges</span><span><span className="legend-swatch datum" />Datum</span></div></div>
|
||||
}
|
||||
|
||||
function PageFrame({ page, onNavigate, onOpenWorkspace, showNotice }: { page: Page; onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void }) {
|
||||
const pages: Record<Exclude<Page, 'workspace'>, ReactNode> = {
|
||||
start: <StartPage onNavigate={onNavigate} onOpenWorkspace={onOpenWorkspace} showNotice={showNotice} />,
|
||||
projects: <ProjectsPage onNavigate={onNavigate} onOpenWorkspace={onOpenWorkspace} showNotice={showNotice} />,
|
||||
import: <FileFlowPage mode="import" onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
export: <FileFlowPage mode="export" onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
settings: <SettingsPage onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
help: <HelpPage onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
diagnostics: <DiagnosticsPage onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
sync: <SyncPage onNavigate={onNavigate} showNotice={showNotice} />,
|
||||
}
|
||||
return <div className="page-shell"><div className="page-content">{pages[page as Exclude<Page, 'workspace'>]}</div></div>
|
||||
}
|
||||
|
||||
function PageHeader({ eyebrow, title, description, actions, onBack }: { eyebrow: string; title: string; description: string; actions?: ReactNode; onBack?: () => void }) {
|
||||
return <div className="page-header"><div className="page-heading"><div className="page-eyebrow">{onBack && <button className="back-button" onClick={onBack} title="Back"><ArrowLeft size={15} /></button>}<span>{eyebrow}</span></div><h1>{title}</h1><p>{description}</p></div>{actions && <div className="page-actions">{actions}</div>}</div>
|
||||
}
|
||||
|
||||
function StartPage({ onNavigate, onOpenWorkspace, showNotice }: { onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void }) {
|
||||
return <div className="start-page">
|
||||
<PageHeader eyebrow="Local-first workspace" title="Start a new design." description="A focused CAD workspace for parametric parts, drawings and assemblies." actions={<><button className="button button-outline" onClick={() => onNavigate('projects')}><FolderOpen size={16} />Open project</button><button className="button button-primary" onClick={onOpenWorkspace}><Plus size={16} />New document</button></>} />
|
||||
<div className="start-grid"><section className="start-hero panel-surface"><div className="hero-copy"><span className="section-kicker"><Sparkles size={14} />FreeCAD-compatible workspace</span><h2>Model with a clear history.</h2><p>Build parts through ordered features, inspect every dependency and keep your project local by default.</p><div className="hero-actions"><button className="button button-primary" onClick={onOpenWorkspace}><FilePlus2 size={16} />Create blank document</button><button className="text-button" onClick={() => showNotice('Example gallery opened')}>Browse examples <ArrowRight size={14} /></button></div></div><div className="hero-schematic"><div className="schematic-axis axis-x-line" /><div className="schematic-axis axis-y-line" /><div className="schematic-block block-a" /><div className="schematic-block block-b" /><div className="schematic-ring" /><span className="schematic-label label-a">Feature history</span><span className="schematic-label label-b">Parametric part</span></div></section><section className="quick-panel panel-surface"><div className="section-title"><div><span className="section-kicker">Quick access</span><h3>Keep moving</h3></div><MoreHorizontal size={16} /></div><QuickAction icon={FolderOpen} title="Open a project" detail="Browse local files" onClick={() => onNavigate('projects')} /><QuickAction icon={Upload} title="Import a model" detail="STEP, IGES, STL and mesh" onClick={() => onNavigate('import')} /><QuickAction icon={WandSparkles} title="Start from an example" detail="Parametric reference models" onClick={() => showNotice('Example gallery opened')} /></section></div>
|
||||
<section className="recent-section"><div className="section-title"><div><span className="section-kicker">Recent projects</span><h3>Your local workspace</h3></div><button className="text-button" onClick={() => onNavigate('projects')}>View all <ArrowRight size={14} /></button></div><div className="recent-grid">{projects.slice(0, 3).map((project, index) => <ProjectCard project={project} key={project.name} index={index} onClick={onOpenWorkspace} />)}</div></section>
|
||||
<div className="start-footer"><div><HardDrive size={15} /><span>Local storage ready</span><Badge tone="green">OPFS available</Badge></div><div><span>BitBybit CAD Studio</span><span className="footer-separator">·</span><span>FreeCAD UI baseline 1.1.1</span></div></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
function QuickAction({ icon: ActionIcon, title, detail, onClick }: { icon: Icon; title: string; detail: string; onClick: () => void }) {
|
||||
return <button className="quick-action" onClick={onClick}><span className="quick-icon"><ActionIcon size={17} /></span><span><strong>{title}</strong><small>{detail}</small></span><ArrowRight size={15} /></button>
|
||||
}
|
||||
|
||||
function ProjectCard({ project, index, onClick }: { project: typeof projects[number]; index: number; onClick: () => void }) {
|
||||
return <button className="project-card" onClick={onClick}><div className={`project-preview preview-${index}`}><div className="preview-shape" /><div className="preview-grid" /></div><div className="project-card-info"><div className="project-card-title"><strong>{project.name}</strong><MoreHorizontal size={15} /></div><span>{project.path}</span><div className="project-card-meta"><span>{project.modified}</span><span>{project.objects} objects</span><Badge tone={project.state === 'Unsaved' ? 'amber' : project.state === 'Read only' ? 'muted' : 'green'}>{project.state}</Badge></div></div></button>
|
||||
}
|
||||
|
||||
function ProjectsPage({ onNavigate, onOpenWorkspace, showNotice }: { onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void }) {
|
||||
return <div className="projects-page"><PageHeader eyebrow="Project manager" title="Your projects." description="Local-first project storage with explicit save and recovery states." actions={<><button className="button button-outline" onClick={() => onNavigate('import')}><Upload size={16} />Import</button><button className="button button-primary" onClick={onOpenWorkspace}><Plus size={16} />New project</button></>} /><div className="manager-toolbar"><div className="large-search"><Search size={16} /><input placeholder="Search projects" /><kbd>/</kbd></div><div className="toolbar-select"><span>Sort by</span><select><option>Last modified</option><option>Name</option><option>Object count</option></select><ChevronDown size={14} /></div><IconButton icon={LayoutGrid} label="Grid view" active /><IconButton icon={ListTree} label="List view" /></div><div className="project-list">{projects.map((project, index) => <div className="project-list-row" key={project.name}><div className={`list-thumbnail preview-${index}`}><div className="preview-shape" /><div className="preview-grid" /></div><div className="list-main"><strong>{project.name}</strong><span>{project.path}</span></div><span className="list-count">{project.objects} objects</span><span className="list-modified">{project.modified}</span><Badge tone={project.state === 'Unsaved' ? 'amber' : project.state === 'Read only' ? 'muted' : 'green'}>{project.state}</Badge><button className="row-action" title="More project actions" onClick={() => showNotice(`Actions for ${project.name}`)}><MoreHorizontal size={16} /></button></div>)}</div><div className="storage-banner"><div className="storage-icon"><HardDrive size={18} /></div><div><strong>Local storage</strong><span>3.4 GB of 10 GB used · Last backup just now</span></div><button className="text-button" onClick={() => onNavigate('settings')}>Manage storage <ArrowRight size={14} /></button></div></div>
|
||||
}
|
||||
|
||||
function FileFlowPage({ mode, onNavigate, showNotice }: { mode: 'import' | 'export'; onNavigate: (page: Page) => void; showNotice: (message: string) => void }) {
|
||||
const isImport = mode === 'import'
|
||||
return <div className="flow-page"><PageHeader eyebrow={isImport ? 'Import model' : 'Export project'} title={isImport ? 'Bring a model into your workspace.' : 'Export a clean deliverable.'} description={isImport ? 'Review format, units and compatibility before the document is created.' : 'Choose an exchange format and keep the native project available for future edits.'} onBack={() => onNavigate('start')} /><div className="flow-layout"><div className="flow-steps"><FlowStep index="01" title="Choose file" active={!isImport} complete={isImport} /><FlowStep index="02" title="Review mapping" active={isImport} complete={false} /><FlowStep index="03" title="Confirm" active={false} complete={false} /></div><section className="flow-card panel-surface"><div className="flow-card-header"><div><span className="section-kicker">Step 01 / 03</span><h2>{isImport ? 'Select a CAD file' : 'Select an export format'}</h2></div><Badge tone="cyan">Local only</Badge></div>{isImport ? <><div className="drop-zone"><div className="drop-icon"><Upload size={20} /></div><strong>Drop a model here</strong><span>or browse from your device</span><button className="button button-outline" onClick={() => showNotice('File picker opened')}><FolderOpen size={15} />Browse files</button><small>STEP · IGES · STL · OBJ · PLY · FCStd</small></div><div className="flow-note"><AlertTriangle size={15} /><span>Imported objects will appear as editable or read-only proxies depending on format support.</span></div></> : <div className="format-grid">{['STEP', 'IGES', 'STL', 'GLB / GLTF', 'Web CAD package'].map((format, index) => <button key={format} className={`format-card ${index === 0 ? 'is-selected' : ''}`} onClick={() => showNotice(`${format} selected`)}><span className="format-icon">{index === 4 ? <FileBox size={18} /> : <FileText size={18} />}</span><strong>{format}</strong><small>{index === 0 ? 'Best for editable solids' : index === 2 ? 'Mesh export' : 'Exchange format'}</small>{index === 0 && <Check size={15} className="format-check" />}</button>)}</div>}<div className="flow-footer"><button className="button button-quiet" onClick={() => onNavigate('start')}>Cancel</button><button className="button button-primary" onClick={() => showNotice(isImport ? 'Next: review mapping' : 'Export queued')}><span>{isImport ? 'Continue' : 'Export'}</span><ArrowRight size={15} /></button></div></section></div></div>
|
||||
}
|
||||
|
||||
function FlowStep({ index, title, active, complete }: { index: string; title: string; active: boolean; complete: boolean }) {
|
||||
return <div className={`flow-step ${active ? 'is-active' : ''} ${complete ? 'is-complete' : ''}`}><span className="flow-index">{complete ? <Check size={13} /> : index}</span><span>{title}</span></div>
|
||||
}
|
||||
|
||||
function SettingsPage({ onNavigate, showNotice }: { onNavigate: (page: Page) => void; showNotice: (message: string) => void }) {
|
||||
return <div className="settings-page"><PageHeader eyebrow="Preferences" title="Workspace settings." description="Keep the FreeCAD workflow familiar and tune the browser workspace around it." onBack={() => onNavigate('start')} actions={<button className="button button-primary" onClick={() => showNotice('Preferences saved')}><Save size={16} />Save changes</button>} /><div className="settings-layout"><aside className="settings-nav"><button className="is-active"><SlidersHorizontal size={15} />General</button><button><Rotate3D size={15} />Navigation</button><button><Circle size={15} />Units & precision</button><button><Layers3 size={15} />Display</button><button><Command size={15} />Shortcuts</button><button><HardDrive size={15} />Storage</button><button><UserRound size={15} />Privacy & security</button></aside><section className="settings-content panel-surface"><SettingSection title="Document defaults" detail="New documents inherit these values."><SettingRow label="Unit system" detail="Used for new projects and property editors"><select><option>Metric (mm, kg, s)</option><option>Imperial (in, lb, s)</option></select></SettingRow><SettingRow label="Autosave interval" detail="A local snapshot is created after inactivity"><select><option>Every 5 minutes</option><option>Every 10 minutes</option><option>Manual only</option></select></SettingRow></SettingSection><SettingSection title="Viewport" detail="The default FreeCAD navigation preset is kept intact."><SettingRow label="Navigation preset" detail="Orbit, pan and zoom mapping"><select><option>FreeCAD CAD</option><option>Trackpad</option><option>Blender</option></select></SettingRow><SettingRow label="Display mode" detail="Default rendering for precise edges"><select><option>Flat lines</option><option>Shaded</option><option>Wireframe</option></select></SettingRow><SettingRow label="Show selection edges" detail="Highlight selected sub-shapes"><input type="checkbox" defaultChecked /></SettingRow></SettingSection><SettingSection title="Local storage" detail="No design data leaves this browser unless you export or sync it."><SettingRow label="OPFS workspace" detail="3.4 GB of 10 GB used"><span className="storage-meter"><span /></span></SettingRow><SettingRow label="Diagnostic uploads" detail="Share only when you explicitly attach a report"><span className="setting-value">Off</span></SettingRow></SettingSection></section></div></div>
|
||||
}
|
||||
|
||||
function SettingSection({ title, detail, children }: { title: string; detail: string; children: ReactNode }) {
|
||||
return <section className="setting-section"><div className="setting-section-heading"><h2>{title}</h2><p>{detail}</p></div>{children}</section>
|
||||
}
|
||||
|
||||
function SettingRow({ label, detail, children }: { label: string; detail: string; children: ReactNode }) {
|
||||
return <div className="setting-row"><div><strong>{label}</strong><span>{detail}</span></div><div className="setting-control">{children}</div></div>
|
||||
}
|
||||
|
||||
function HelpPage({ onNavigate, showNotice }: { onNavigate: (page: Page) => void; showNotice: (message: string) => void }) {
|
||||
const helpItems = [{ title: 'Create a feature-based part', detail: 'Body → Sketch → Pad → Pocket → Fillet', icon: Box }, { title: 'Understand the model tree', detail: 'Objects, links, Tip and recompute states', icon: ListTree }, { title: 'Navigate the viewport', detail: 'FreeCAD CAD preset and view controls', icon: Rotate3D }, { title: 'Recover a project', detail: 'Snapshots, compatibility and backups', icon: Archive }]
|
||||
return <div className="help-page"><PageHeader eyebrow="Help center" title="Find your next command." description="FreeCAD workflow references, keyboard shortcuts and compatibility notes." onBack={() => onNavigate('start')} /><div className="help-search"><Search size={18} /><input placeholder="Search commands, workbenches and help" autoFocus /><kbd>⌘ K</kbd></div><div className="help-grid">{helpItems.map((item) => <button className="help-card panel-surface" key={item.title} onClick={() => showNotice(`${item.title} opened`)}><span className="help-icon"><item.icon size={18} /></span><span><strong>{item.title}</strong><small>{item.detail}</small></span><ArrowRight size={15} /></button>)}</div><section className="shortcut-panel panel-surface"><div className="section-title"><div><span className="section-kicker">Keyboard</span><h3>Core shortcuts</h3></div><button className="text-button" onClick={() => showNotice('Shortcut editor opened')}>Customize <ArrowRight size={14} /></button></div><div className="shortcut-grid"><Shortcut keyName="⌘ K" label="Command search" /><Shortcut keyName="Space" label="Toggle visibility" /><Shortcut keyName="F" label="Fit all" /><Shortcut keyName="Esc" label="Cancel task or selection" /><Shortcut keyName="⌘ Z" label="Undo" /><Shortcut keyName="⌘ S" label="Save project" /></div></section></div>
|
||||
}
|
||||
|
||||
function Shortcut({ keyName, label }: { keyName: string; label: string }) {
|
||||
return <div className="shortcut-row"><kbd>{keyName}</kbd><span>{label}</span></div>
|
||||
}
|
||||
|
||||
function DiagnosticsPage({ onNavigate, showNotice }: { onNavigate: (page: Page) => void; showNotice: (message: string) => void }) {
|
||||
return <div className="diagnostics-page"><PageHeader eyebrow="System status" title="Everything is ready." description="A concise view of browser capabilities, local storage and the current document runtime." onBack={() => onNavigate('start')} actions={<button className="button button-outline" onClick={() => showNotice('Diagnostic package prepared')}><Download size={16} />Export report</button>} /><div className="health-grid"><HealthCard label="WebAssembly" value="Ready" detail="BitBybit runtime available" tone="green" icon={Code2} /><HealthCard label="Local storage" value="Available" detail="OPFS · 3.4 GB used" tone="green" icon={HardDrive} /><HealthCard label="Viewport" value="WebGL2" detail="WebGPU can be enabled" tone="cyan" icon={Rotate3D} /><HealthCard label="Document" value="2 warnings" detail="Pump Housing · v18" tone="amber" icon={AlertTriangle} /></div><section className="diagnostic-table panel-surface"><div className="section-title"><div><span className="section-kicker">Runtime</span><h3>Capability checks</h3></div><span className="last-checked">Last checked just now</span></div><DiagnosticRow name="BitBybit Facade" value="Connected" detail="API v0.1 · mock runtime" tone="green" /><DiagnosticRow name="Geometry Worker" value="Idle" detail="No active computation" tone="green" /><DiagnosticRow name="SQLite WASM" value="Connected" detail="Local database · schema v3" tone="green" /><DiagnosticRow name="OPFS" value="Available" detail="SharedAccessHandle pool" tone="green" /><DiagnosticRow name="FreeCAD baseline" value="1.1.1" detail="Compatibility manifest loaded" tone="cyan" /><DiagnosticRow name="Document warnings" value="2" detail="One downstream reference needs review" tone="amber" onClick={() => showNotice('Document diagnostics opened')} /></section></div>
|
||||
}
|
||||
|
||||
function HealthCard({ label, value, detail, tone, icon: HealthIcon }: { label: string; value: string; detail: string; tone: 'green' | 'cyan' | 'amber'; icon: Icon }) {
|
||||
return <div className="health-card panel-surface"><div className={`health-icon ${tone}`}><HealthIcon size={17} /></div><span className="section-kicker">{label}</span><strong>{value}</strong><small>{detail}</small></div>
|
||||
}
|
||||
|
||||
function DiagnosticRow({ name, value, detail, tone, onClick }: { name: string; value: string; detail: string; tone: 'green' | 'cyan' | 'amber'; onClick?: () => void }) {
|
||||
return <button className="diagnostic-row" onClick={onClick}><span className="diagnostic-name"><span className={`status-dot ${tone}`} />{name}</span><span className="diagnostic-detail">{detail}</span><strong className={`diagnostic-value ${tone}`}>{value}</strong><ChevronRight size={15} /></button>
|
||||
}
|
||||
|
||||
function SyncPage({ onNavigate, showNotice }: { onNavigate: (page: Page) => void; showNotice: (message: string) => void }) {
|
||||
return <div className="sync-page"><PageHeader eyebrow="Account and sync" title="Stay local by default." description="Cloud features are optional. Your current workspace is stored in this browser." onBack={() => onNavigate('start')} /><div className="sync-grid"><section className="sync-card panel-surface"><div className="sync-card-header"><span className="sync-logo"><UserRound size={18} /></span><div><span className="section-kicker">Not signed in</span><h2>Local workspace</h2></div><Badge tone="green">Active</Badge></div><p>Projects, recovery snapshots and cached geometry remain on this device until you explicitly export or sync them.</p><div className="sync-stats"><div><strong>4</strong><span>projects</span></div><div><strong>3.4 GB</strong><span>storage used</span></div><div><strong>Just now</strong><span>last backup</span></div></div><button className="button button-outline" onClick={() => showNotice('Sign-in flow is ready for the next phase')}><UserRound size={16} />Connect an account</button></section><section className="sync-card panel-surface"><div className="section-title"><div><span className="section-kicker">Optional services</span><h3>Choose when to share</h3></div><Settings2 size={16} /></div><SyncRow title="Project sync" detail="Keep a remote copy and resolve conflicts" disabled /><SyncRow title="Team review links" detail="Share read-only documents" disabled /><SyncRow title="Diagnostic uploads" detail="Attach reports only when requested" disabled /></section></div></div>
|
||||
}
|
||||
|
||||
function SyncRow({ title, detail, disabled }: { title: string; detail: string; disabled?: boolean }) {
|
||||
return <div className={`sync-row ${disabled ? 'is-disabled' : ''}`}><div><strong>{title}</strong><span>{detail}</span></div><span className="toggle"><span /></span></div>
|
||||
}
|
||||
|
||||
export default App
|
||||
Reference in New Issue
Block a user