Files
Web_FreeCAD_Bitbybit/src/App.tsx

526 lines
55 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, 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,
RefreshCw,
Upload,
UserRound,
WandSparkles,
X,
ZoomIn,
ZoomOut,
} from 'lucide-react'
import { menuDefinitions, pinnedWorkbenches, workbenchDefinitions, type MenuName, type WorkbenchId } from './freecadManifest'
import { createMockFacade, type BitBybitViewportAdapter, type BitBybitWebCadFacade, type DocumentSnapshot, type FcstdInspection, type ModelTreeItem, type ObjectPropertySnapshot, type PropertyValue, type ShapeHandle } from './facade'
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 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 [leftTab, setLeftTab] = useState<'model' | 'tasks'>('model')
const [rightTab, setRightTab] = useState<'data' | 'view'>('data')
const [bottomOpen, setBottomOpen] = useState(true)
const [notice, setNotice] = useState('')
const facade = useMemo(() => createMockFacade(), [])
const [facadeState, setFacadeState] = useState(() => facade.getState())
useEffect(() => facade.subscribe((event) => {
if (event.type === 'state.changed') setFacadeState(event.state)
if (event.type === 'notice') {
setNotice(event.message)
window.setTimeout(() => setNotice(''), 2600)
}
}), [facade])
useEffect(() => () => facade.geometry.dispose(), [facade])
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 workbench = facadeState.activeWorkbench
const selectedObject = facadeState.selectedObjectId
const currentWorkbench = workbenchMeta[workbench]
const CurrentIcon = currentWorkbench.icon
const showNotice = useCallback((message: string) => facade.notify(message), [facade])
const setWorkbench = useCallback((value: Workbench) => facade.gui.workbench.setActive(value), [facade])
const setSelectedObject = useCallback((id: string) => id ? facade.selection.select(id) : facade.selection.clear(), [facade])
return (
<div className="app-shell" data-persistence-mode={facadeState.persistence.mode}>
<TopBar page={page} workbench={workbench} facade={facade} onNavigate={navigate} onOpenWorkspace={openWorkspace} />
{page === 'workspace' ? (
<Workspace
workbench={workbench}
setWorkbench={(value) => { setWorkbench(value); setLeftTab('tasks') }}
leftTab={leftTab}
setLeftTab={setLeftTab}
rightTab={rightTab}
setRightTab={setRightTab}
bottomOpen={bottomOpen}
setBottomOpen={setBottomOpen}
selectedObject={selectedObject}
setSelectedObject={setSelectedObject}
document={facadeState.document}
facade={facade}
workbenchIcon={CurrentIcon}
showNotice={showNotice}
/>
) : (
<PageFrame page={page} onNavigate={navigate} onOpenWorkspace={openWorkspace} showNotice={showNotice} facade={facade} />
)}
{notice && <div className="toast"><CheckCircle2 size={16} />{notice}</div>}
</div>
)
}
function TopBar({ page, workbench, facade, onNavigate, onOpenWorkspace }: { page: Page; workbench: Workbench; facade: BitBybitWebCadFacade; onNavigate: (page: Page) => void; onOpenWorkspace: () => void }) {
const [openMenu, setOpenMenu] = useState<MenuName | null>(null)
const document = facade.getState().document
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" />{document.label} <span className="context-separator">/</span> {workbench}</div>
<span className="save-state"><span className="save-dot" />{document.dirty ? 'Unsaved changes' : 'Saved locally'}</span>
</div>
<IconButton icon={Undo2} label="Undo" onClick={() => facade.history.undo()} disabled={!facade.history.canUndo()} />
<IconButton icon={Redo2} label="Redo" onClick={() => facade.history.redo()} disabled={!facade.history.canRedo()} />
<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, document, facade, 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
document: DocumentSnapshot
facade: BitBybitWebCadFacade
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={() => { facade.gui.command.execute({ commandId: 'new-document' }); showNotice('New document prepared') }} />
<IconButton icon={FolderOpen} label="Open project" onClick={() => showNotice('Project browser opened')} />
<IconButton icon={Save} label="Save project" onClick={() => { facade.gui.command.execute({ commandId: 'save' }); showNotice('Saved to local workspace') }} />
<IconButton icon={RefreshCw} label="Recompute document" disabled={document.readOnly} onClick={() => { void facade.app.document.recomputeAsync().then((result) => showNotice(result.status === 'completed' ? 'Recompute completed' : `Recompute ${result.status}`)) }} />
<div className="toolbar-divider" />
<IconButton icon={Box} label="Create body" onClick={() => { facade.gui.command.execute({ commandId: 'create-body' }); showNotice('Create Body task opened') }} active={workbench === 'Part Design'} />
<IconButton icon={CircleDot} label="Create sketch" onClick={() => { facade.gui.command.execute({ commandId: 'create-sketch' }); 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>{document.label}</span>{document.dirty && <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">{document.units} · {document.objects.length} objects · v{document.version}</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 document={document} selectedObject={selectedObject} setSelectedObject={setSelectedObject} showNotice={showNotice} /><div className="combo-property"><div className="property-heading"><div><span className="eyebrow">Property view</span><h2>{document.tree.find((item) => item.id === selectedObject)?.label || 'No selection'}</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><PropertyPanel key={rightTab} facade={facade} objectId={selectedObject} scope={rightTab} 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} facade={facade} 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} facade={facade} showNotice={showNotice} /></aside>
<FunctionRail workbench={workbench} facade={facade} 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">{document.recompute?.status === 'failed' ? <AlertTriangle size={14} className="icon-amber" /> : <CheckCircle2 size={14} className="icon-green" />}<span>Recompute {document.recompute?.status || 'idle'}</span><span className="report-context">{document.recompute?.order.length || 0} objects in plan</span><span className="report-time">generation {document.recompute?.generation || 0}</span></div>{(document.recompute?.errors || []).slice(0, 2).map((error) => <div className="report-line" key={`${error.objectId}-${error.code}`}><AlertTriangle size={14} className="icon-amber" /><span>{error.message}</span><button onClick={() => showNotice(`Dependency review: ${error.objectId}`)}>Review dependency</button><span className="report-time">{error.code}</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, facade, showNotice }: { workbench: Workbench; facade: BitBybitWebCadFacade; 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; const state = facade.gui.command.getState(item.id); const title = state.status === 'disabled' && state.reason ? `${item.label}: ${state.reason}` : `${item.label}${item.shortcut ? ` (${item.shortcut})` : ''}`; return <button key={item.id} title={title} aria-label={title} disabled={state.status === 'disabled'} onClick={() => { facade.gui.command.execute({ commandId: item.id }); showNotice(`${item.label} command staged`) }}><IconComponent size={15} /></button> })}</div>)}</aside>
}
function ModelTree({ document, selectedObject, setSelectedObject, showNotice }: { document: DocumentSnapshot; selectedObject: string; setSelectedObject: (id: string) => void; showNotice: (message: string) => void }) {
const [expanded, setExpanded] = useState<Record<string, boolean>>({ origin: false, body: true, reference: false })
const itemsById = new Map(document.tree.map((item) => [item.id, item]))
const childIds = new Set(document.tree.flatMap((item) => item.children || []))
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>{document.label}</span><span className="tree-version">v{document.version}</span></div>
{document.tree.filter((item) => !childIds.has(item.id)).map((item) => <TreeItem key={item.id} item={item} level={1} expanded={expanded[item.id]} onToggle={() => setExpanded((current) => ({ ...current, [item.id]: !current[item.id] }))} itemsById={itemsById} 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, itemsById, selectedObject, setSelectedObject, showNotice }: { item: ModelTreeItem; level: number; expanded?: boolean; onToggle: () => void; itemsById: Map<string, ModelTreeItem>; 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((childId) => { const child = itemsById.get(childId); const ChildIcon = child?.type === 'sketch' ? CircleDot : child?.type === 'feature' ? SquareStack : Circle; return child ? <button className={`tree-row tree-child ${selectedObject === child.id ? 'is-selected' : ''}`} key={child.id} onClick={() => { setSelectedObject(child.id); if (child.state === 'warning') showNotice('This feature has a reference warning') }} onDoubleClick={() => showNotice(`Editing ${child.label}`)}><span className="tree-spacer" /><ChildIcon size={child.type === 'feature' ? 15 : 13} className={child.state === 'warning' ? 'icon-amber' : 'icon-muted'} /><span className="tree-label">{child.label}</span>{child.state === 'warning' && <AlertTriangle size={13} className="icon-amber" />}</button> : <div className="tree-row tree-child" key={childId}><span className="tree-spacer" /><Circle size={8} className="icon-muted" /><span className="tree-label">{childId}</span></div> })}</div>}
</>
}
function TaskPanel({ workbench, facade, showNotice }: { workbench: Workbench; facade: BitBybitWebCadFacade; 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={() => { facade.task.apply(); showNotice('Task accepted') }}>OK</button><button className="button button-outline" onClick={() => { facade.task.update({ preview: true }); showNotice('Preview applied') }}>Apply</button><button className="button button-quiet" onClick={() => { facade.task.cancel(); 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 PropertyPanel({ facade, objectId, scope, showNotice }: { facade: BitBybitWebCadFacade; objectId: string; scope: 'data' | 'view'; showNotice: (message: string) => void }) {
const object = facade.app.document.getObject(objectId)
if (!object) return <div className="properties-empty">No object selected</div>
const properties = object.properties.filter((property) => property.scope === scope && !property.hidden)
const groups = new Map<string, ObjectPropertySnapshot[]>()
properties.forEach((property) => groups.set(property.group, [...(groups.get(property.group) ?? []), property]))
return <div className="properties-scroll">{[...groups].map(([group, entries]) => <div className="property-group" key={group}><div className="property-group-title">{group}<ChevronDown size={14} /></div>{entries.map((property) => <PropertyEditor key={`${objectId}-${property.name}-${String(property.value)}`} facade={facade} objectId={objectId} property={property} showNotice={showNotice} />)}</div>)}</div>
}
function PropertyEditor({ facade, objectId, property, showNotice }: { facade: BitBybitWebCadFacade; objectId: string; property: ObjectPropertySnapshot; showNotice: (message: string) => void }) {
const commit = (value: PropertyValue) => {
try {
facade.app.document.setProperty({ objectId, propertyName: property.name, value })
return true
} catch (error) {
showNotice(error instanceof Error ? error.message : String(error))
return false
}
}
const formatted = `${String(property.value ?? '')}${property.unit ? ` ${property.unit}` : ''}`
let editor: ReactNode
if (property.readOnly) editor = <span className="property-readonly">{formatted}</span>
else if (property.type === 'App::PropertyBool') editor = <input className="property-checkbox" type="checkbox" checked={Boolean(property.value)} aria-label={property.label} onChange={(event) => commit(event.target.checked)} />
else if (property.type === 'App::PropertyEnumeration') editor = <select className="property-control" value={String(property.value)} aria-label={property.label} onChange={(event) => commit(event.target.value)}>{property.options?.map((option) => <option key={option}>{option}</option>)}</select>
else if (property.type === 'App::PropertyLink') {
const document = facade.app.document.getActive()
const ids = [...new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))]
editor = <select className="property-control property-link-control" value={String(property.value ?? '')} aria-label={property.label} onChange={(event) => commit(event.target.value || null)}><option value="">None</option>{ids.filter((id) => id !== objectId).map((id) => <option value={id} key={id}>{document.tree.find((item) => item.id === id)?.label ?? id}</option>)}</select>
} else if (property.type === 'App::PropertyColor') editor = <label className="property-color"><input type="color" value={String(property.value)} aria-label={property.label} onChange={(event) => commit(event.target.value)} /><span>{String(property.value).toUpperCase()}</span></label>
else if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat') editor = <label className="property-number"><input className="property-control" type="number" defaultValue={Number(property.value)} min={property.type === 'App::PropertyLength' || property.type === 'App::PropertyPercent' ? 0 : undefined} max={property.type === 'App::PropertyPercent' ? 100 : undefined} step={property.type === 'App::PropertyLength' ? 0.1 : 1} aria-label={property.label} onBlur={(event) => { if (!commit(Number(event.target.value))) event.target.value = String(property.value) }} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }} /><span>{property.unit}</span></label>
else editor = <input className="property-control" defaultValue={String(property.value ?? '')} aria-label={property.label} onBlur={(event) => { if (!commit(event.target.value)) event.target.value = String(property.value ?? '') }} onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }} />
return <><div className="property-row"><span className="property-label">{property.label}</span><div className="property-editor">{editor}</div></div>{property.expression && <div className="property-expression"><Code2 size={12} /><span>{property.expression}</span></div>}</>
}
function Viewport({ selectedObject, setSelectedObject, workbench, facade, showNotice }: { selectedObject: string; setSelectedObject: (id: string) => void; workbench: Workbench; facade: BitBybitWebCadFacade; showNotice: (message: string) => void }) {
const hostRef = useRef<HTMLDivElement>(null)
const adapterRef = useRef<BitBybitViewportAdapter | null>(null)
useEffect(() => {
const host = hostRef.current
if (!host) return
const adapter = facade.viewport.createAdapter()
adapterRef.current = adapter
let cancelled = false
let shape: ShapeHandle | null = null
try {
adapter.mount(host)
} catch (error) {
showNotice(`WebGL viewport unavailable: ${error instanceof Error ? error.message : 'unknown error'}`)
adapter.dispose()
adapterRef.current = null
return
}
const loadGeometry = async () => {
const capabilities = await facade.geometry.initialize()
if (capabilities.status !== 'ready') throw new Error(capabilities.reason || 'OCCT geometry runtime unavailable')
const document = facade.app.document.getActive()
shape = await facade.geometry.createBox({ width: 2.7, length: 1.4, height: 1.6, center: [0, 0, 0], originOnCenter: true, documentId: document.id, documentVersion: document.version })
if (cancelled) {
await facade.geometry.release(shape)
shape = null
return
}
const mesh = await facade.geometry.mesh(shape, 0.05)
if (!cancelled) adapter.setMesh(mesh)
}
void loadGeometry().catch((error: unknown) => {
if (!cancelled) showNotice(`Geometry preview unavailable: ${error instanceof Error ? error.message : String(error)}`)
})
return () => {
cancelled = true
if (shape) void facade.geometry.release(shape)
adapter.dispose()
adapterRef.current = null
}
}, [facade, showNotice])
useEffect(() => adapterRef.current?.setSelection(selectedObject), [selectedObject])
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="three-viewport-host" ref={hostRef} aria-label="Three.js viewport" /></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, facade }: { page: Page; onNavigate: (page: Page) => void; onOpenWorkspace: () => void; showNotice: (message: string) => void; facade: BitBybitWebCadFacade }) {
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} facade={facade} />,
export: <FileFlowPage mode="export" onNavigate={onNavigate} showNotice={showNotice} facade={facade} />,
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 capability</span><Badge tone="cyan">Probe on open</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, facade }: { mode: 'import' | 'export'; onNavigate: (page: Page) => void; showNotice: (message: string) => void; facade: BitBybitWebCadFacade }) {
const isImport = mode === 'import'
const [fcstdReport, setFcstdReport] = useState<FcstdInspection | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const inspectFile = async (file: File) => {
if (!file.name.toLowerCase().endsWith('.fcstd')) { showNotice('Only FCStd inspection is connected to this import boundary'); return }
try {
const report = facade.project.fcstd.inspect(new Uint8Array(await file.arrayBuffer()))
setFcstdReport(report)
showNotice(`FCStd inspected: ${report.compatibility.level}`)
} catch (error) { setFcstdReport(null); showNotice(`FCStd rejected: ${error instanceof Error ? error.message : String(error)}`) }
}
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><input ref={fileInputRef} type="file" accept=".FCStd,.fcstd" hidden onChange={(event) => { const file = event.target.files?.[0]; if (file) void inspectFile(file); event.currentTarget.value = '' }} /><button className="button button-outline" onClick={() => fileInputRef.current?.click()}><FolderOpen size={15} />Browse files</button><small>STEP · IGES · STL · OBJ · PLY · FCStd</small></div><div className="flow-note"><AlertTriangle size={15} /><span>FCStd is currently a read-only metadata inspection boundary. Python/macros are isolated and never executed.</span></div>{fcstdReport && <div className="flow-note"><CheckCircle2 size={15} className="icon-green" /><span>{fcstdReport.label} · {fcstdReport.objects.length} objects · {fcstdReport.compatibility.level}; {fcstdReport.compatibility.warnings.join(' ') || 'No compatibility warnings.'}</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 ? (fcstdReport ? 'Metadata review complete; import mapping remains disabled' : 'Choose a file to review') : '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="Runtime checks." 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 OCCT package loads on demand" tone="green" icon={Code2} /><HealthCard label="Local storage" value="Capability probe" detail="SQLite WASM + OPFS worker" tone="cyan" 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 · single public entry" tone="green" /><DiagnosticRow name="Geometry Worker" value="BitBybit OCCT 1.1.1" detail="WASM Worker with Box, Boolean, feature and export boundaries" tone="green" /><DiagnosticRow name="SQLite WASM" value="Worker configured" detail="Schema v4 · OPFS VFS with memory fallback" tone="cyan" /><DiagnosticRow name="OPFS" value="Capability probe" detail="Requires cross-origin isolation; export fallback is explicit" tone="cyan" /><DiagnosticRow name="FreeCAD baseline" value="1.1.1" detail="Compatibility manifest loaded; unsupported commands remain disabled" 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