diff --git a/README.md b/README.md index 3ff36d9..5270f2f 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ src/ domain/ Framework-agnostic types + logic (compatibility rules, BOM math, the built-in connector/cable/device library). No React, no React Flow — safe to unit test or reuse from a future backend. - data/ Storage abstraction. `ProjectRepository` is the interface the - rest of the app codes against; `LocalStorageProjectRepository` - is the only implementation today. Swapping in a real backend - later (REST/GraphQL) means adding one new implementation of - that interface, not touching state/UI code. - state/ Zustand store (`projectStore`) — the single source of truth for - the current project. Auto-saves to the repository (debounced) + data/ Storage abstraction. `DiagramRepository` is the interface the + rest of the app codes against; `SupabaseDiagramRepository` is + the active implementation, with `LocalStorageDiagramRepository` + kept as a reference/fallback. Swapping storage means adding + one new implementation of that interface, not touching + state/UI code. + state/ Zustand store (`diagramStore`) — the single source of truth for + the current diagram. Auto-saves to the repository (debounced) on every change. components/ canvas/ React Flow wiring surface: custom device node, custom cable @@ -40,17 +41,19 @@ src/ - **PortType** — a connector kind (HDMI, XLR, Cat5e/6, ...). Two ports can be wired together only if their PortTypes share a compatibility `family`. - **DeviceTemplate** — a reusable device shape (its ports), shown in the - palette. Built-ins live in `domain/library.ts`; custom ones are saved on the - project. + palette. Built-ins live in `domain/library.ts` and seed the public catalog; + custom ones are your own private catalog entries (or submitted for public + review). - **Device** — an instance placed on the canvas, with its own copy of ports (editing an instance never mutates its template). - **CableType** — a physical cable spec, scoped to a PortType family. - **Connection** — a wire between two ports, referencing a CableType and an optional user-entered distance (there's no floor plan / scale model, so length is a manual estimate per connection). -- **Project** — devices + connections + any custom library entries. This is - the one object persisted (auto-saved to `localStorage`, or explicitly - exported/imported as JSON). +- **Diagram** — devices + connections. This is the one object persisted + (auto-saved to Supabase, or explicitly exported/imported as JSON); the + catalog (PortType/CableType/DeviceTemplate/category) is shared and global, + not part of any one diagram. ## Development diff --git a/src/components/bom/BomPanel.tsx b/src/components/bom/BomPanel.tsx index bd2574d..8d97968 100644 --- a/src/components/bom/BomPanel.tsx +++ b/src/components/bom/BomPanel.tsx @@ -1,7 +1,7 @@ import { Fragment, useMemo, useState } from 'react' import { computeBom } from '../../domain/bom' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import Chevron from '../common/Chevron' function formatCost(value: number): string { @@ -9,9 +9,9 @@ function formatCost(value: number): string { } export default function BomPanel() { - const project = useProjectStore((s) => s.project) + const diagram = useDiagramStore((s) => s.diagram) const catalog = useCatalogStore((s) => s.catalog) - const bom = useMemo(() => computeBom(project, catalog), [project, catalog]) + const bom = useMemo(() => computeBom(diagram, catalog), [diagram, catalog]) // Collapsed by default, per cable type — most diagrams only need the // per-type totals; the length breakdown is there when you're ready to // actually go buy cables. diff --git a/src/components/canvas/FlowCanvas.tsx b/src/components/canvas/FlowCanvas.tsx index 6732138..5883508 100644 --- a/src/components/canvas/FlowCanvas.tsx +++ b/src/components/canvas/FlowCanvas.tsx @@ -15,9 +15,9 @@ import { import '@xyflow/react/dist/style.css' import { useCallback, useMemo } from 'react' import { validateConnection } from '../../domain/compatibility' -import { allCableTypes, allPortTypes, cableTypesForConnection, deviceCategoryName, getPortType } from '../../domain/project' +import { allCableTypes, allPortTypes, cableTypesForConnection, deviceCategoryName, getPortType } from '../../domain/diagram' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import CableEdge, { type CableFlowEdge } from './CableEdge' import DeviceNode, { type DeviceFlowNode, type PortInfo } from './DeviceNode' import { buildHandleId, parsePortId } from './handleIds' @@ -28,19 +28,19 @@ const nodeTypes: NodeTypes = { device: DeviceNode } const edgeTypes: EdgeTypes = { cable: CableEdge } function FlowCanvasInner() { - const project = useProjectStore((s) => s.project) - const selectedDeviceId = useProjectStore((s) => s.selectedDeviceId) - const selectedConnectionId = useProjectStore((s) => s.selectedConnectionId) - const updateDevicePosition = useProjectStore((s) => s.updateDevicePosition) - const removeDevice = useProjectStore((s) => s.removeDevice) - const removeConnection = useProjectStore((s) => s.removeConnection) - const addConnection = useProjectStore((s) => s.addConnection) - const addDeviceFromTemplate = useProjectStore((s) => s.addDeviceFromTemplate) - const selectDevice = useProjectStore((s) => s.selectDevice) - const selectConnection = useProjectStore((s) => s.selectConnection) + const diagram = useDiagramStore((s) => s.diagram) + const selectedDeviceId = useDiagramStore((s) => s.selectedDeviceId) + const selectedConnectionId = useDiagramStore((s) => s.selectedConnectionId) + const updateDevicePosition = useDiagramStore((s) => s.updateDevicePosition) + const removeDevice = useDiagramStore((s) => s.removeDevice) + const removeConnection = useDiagramStore((s) => s.removeConnection) + const addConnection = useDiagramStore((s) => s.addConnection) + const addDeviceFromTemplate = useDiagramStore((s) => s.addDeviceFromTemplate) + const selectDevice = useDiagramStore((s) => s.selectDevice) + const selectConnection = useDiagramStore((s) => s.selectConnection) const { screenToFlowPosition } = useReactFlow() const catalog = useCatalogStore((s) => s.catalog) - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) // Owner/edit-collaborator/(a Super Admin, who getAccess always reports as // 'edit') can edit; a view-only collaborator can look but not touch — // organized-ideas.md §8's per-collaborator permissions. `access` is only @@ -53,10 +53,10 @@ function FlowCanvasInner() { const nodes: DeviceFlowNode[] = useMemo( () => - project.devices.map((device) => { + diagram.devices.map((device) => { const portInfos: PortInfo[] = device.ports.map((port) => { const portType = portTypes.find((pt) => pt.id === port.portTypeId) - const connectionCount = project.connections.filter( + const connectionCount = diagram.connections.filter( (c) => c.sourcePortId === port.id || c.targetPortId === port.id, ).length const max = portType?.maxConnections ?? 1 @@ -70,12 +70,12 @@ function FlowCanvasInner() { data: { device, categoryName: deviceCategoryName(catalog, device.category), portInfos }, } }), - [project, catalog, portTypes, selectedDeviceId], + [diagram, catalog, portTypes, selectedDeviceId], ) const edges: CableFlowEdge[] = useMemo( () => - project.connections.map((connection) => { + diagram.connections.map((connection) => { const cableType = cableTypes.find((ct) => ct.id === connection.cableTypeId) return { id: connection.id, @@ -92,7 +92,7 @@ function FlowCanvasInner() { }, } }), - [project.connections, cableTypes, selectedConnectionId], + [diagram.connections, cableTypes, selectedConnectionId], ) const isValidConnection = useCallback( @@ -101,14 +101,14 @@ function FlowCanvasInner() { const sourcePortId = parsePortId(c.sourceHandle) const targetPortId = parsePortId(c.targetHandle) if (!canEdit || !c.source || !c.target || !sourcePortId || !targetPortId) return false - return validateConnection(project, catalog, { + return validateConnection(diagram, catalog, { sourceDeviceId: c.source, sourcePortId, targetDeviceId: c.target, targetPortId, }).valid }, - [project, catalog, canEdit], + [diagram, catalog, canEdit], ) const onConnect = useCallback( @@ -117,8 +117,8 @@ function FlowCanvasInner() { const sourcePortId = parsePortId(connection.sourceHandle) const targetPortId = parsePortId(connection.targetHandle) if (!connection.source || !connection.target || !sourcePortId || !targetPortId) return - const sourceDevice = project.devices.find((d) => d.id === connection.source) - const targetDevice = project.devices.find((d) => d.id === connection.target) + const sourceDevice = diagram.devices.find((d) => d.id === connection.source) + const targetDevice = diagram.devices.find((d) => d.id === connection.target) const sourcePort = sourceDevice?.ports.find((p) => p.id === sourcePortId) const targetPort = targetDevice?.ports.find((p) => p.id === targetPortId) const sourcePortType = sourcePort && getPortType(catalog, sourcePort.portTypeId) @@ -134,7 +134,7 @@ function FlowCanvasInner() { cableTypeId: defaultCableType.id, }) }, - [project, catalog, canEdit, addConnection], + [diagram, catalog, canEdit, addConnection], ) const onNodesChange = useCallback( diff --git a/src/components/common/CategorySelect.tsx b/src/components/common/CategorySelect.tsx index 1bcd5bd..f1560b3 100644 --- a/src/components/common/CategorySelect.tsx +++ b/src/components/common/CategorySelect.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import { allDeviceCategories } from '../../domain/project' +import { allDeviceCategories } from '../../domain/diagram' import { useCatalogStore } from '../../state/catalogStore' const NEW_CATEGORY_VALUE = '__new__' diff --git a/src/components/inspector/ConnectionInspector.tsx b/src/components/inspector/ConnectionInspector.tsx index a717ea8..a111688 100644 --- a/src/components/inspector/ConnectionInspector.tsx +++ b/src/components/inspector/ConnectionInspector.tsx @@ -1,19 +1,19 @@ import { useMemo } from 'react' -import { cableTypesForConnection, getPortType } from '../../domain/project' +import { cableTypesForConnection, getPortType } from '../../domain/diagram' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' export default function ConnectionInspector({ connectionId }: { connectionId: string }) { - const project = useProjectStore((s) => s.project) + const diagram = useDiagramStore((s) => s.diagram) const catalog = useCatalogStore((s) => s.catalog) - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) const canEdit = access?.myPermission !== 'view' - const updateConnection = useProjectStore((s) => s.updateConnection) - const removeConnection = useProjectStore((s) => s.removeConnection) + const updateConnection = useDiagramStore((s) => s.updateConnection) + const removeConnection = useDiagramStore((s) => s.removeConnection) - const connection = project.connections.find((c) => c.id === connectionId) - const sourceDevice = project.devices.find((d) => d.id === connection?.sourceDeviceId) - const targetDevice = project.devices.find((d) => d.id === connection?.targetDeviceId) + const connection = diagram.connections.find((c) => c.id === connectionId) + const sourceDevice = diagram.devices.find((d) => d.id === connection?.sourceDeviceId) + const targetDevice = diagram.devices.find((d) => d.id === connection?.targetDeviceId) const sourcePort = sourceDevice?.ports.find((p) => p.id === connection?.sourcePortId) const targetPort = targetDevice?.ports.find((p) => p.id === connection?.targetPortId) diff --git a/src/components/inspector/DeviceInspector.tsx b/src/components/inspector/DeviceInspector.tsx index c38ff61..6452611 100644 --- a/src/components/inspector/DeviceInspector.tsx +++ b/src/components/inspector/DeviceInspector.tsx @@ -1,25 +1,25 @@ import { useMemo, useState } from 'react' import { PORT_DIRECTIONS } from '../../domain/constants' -import { allPortTypes } from '../../domain/project' +import { allPortTypes } from '../../domain/diagram' import type { PortDirection } from '../../domain/types' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import CategorySelect from '../common/CategorySelect' export default function DeviceInspector({ deviceId }: { deviceId: string }) { - const project = useProjectStore((s) => s.project) + const diagram = useDiagramStore((s) => s.diagram) const catalog = useCatalogStore((s) => s.catalog) - const access = useProjectStore((s) => s.access) + const access = useDiagramStore((s) => s.access) const canEdit = access?.myPermission !== 'view' - const updateDevice = useProjectStore((s) => s.updateDevice) - const removeDevice = useProjectStore((s) => s.removeDevice) - const addPort = useProjectStore((s) => s.addPort) - const clonePort = useProjectStore((s) => s.clonePort) - const updatePort = useProjectStore((s) => s.updatePort) - const removePort = useProjectStore((s) => s.removePort) + const updateDevice = useDiagramStore((s) => s.updateDevice) + const removeDevice = useDiagramStore((s) => s.removeDevice) + const addPort = useDiagramStore((s) => s.addPort) + const clonePort = useDiagramStore((s) => s.clonePort) + const updatePort = useDiagramStore((s) => s.updatePort) + const removePort = useDiagramStore((s) => s.removePort) const [focusPortId, setFocusPortId] = useState(null) - const device = project.devices.find((d) => d.id === deviceId) + const device = diagram.devices.find((d) => d.id === deviceId) const portTypes = useMemo( () => [...allPortTypes(catalog)].sort((a, b) => a.name.localeCompare(b.name)), [catalog], @@ -28,7 +28,7 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) { if (!device) return null const connectionCountForPort = (portId: string) => - project.connections.filter((c) => c.sourcePortId === portId || c.targetPortId === portId).length + diagram.connections.filter((c) => c.sourcePortId === portId || c.targetPortId === portId).length return (
diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 8136213..3ad5f88 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { useAdminReviewStore } from '../../state/adminReviewStore' import { useAuthStore } from '../../state/authStore' import { useCatalogStore } from '../../state/catalogStore' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import { useSubmissionStore } from '../../state/submissionStore' import FlowCanvas from '../canvas/FlowCanvas' import DevicePalette from '../palette/DevicePalette' @@ -12,8 +12,8 @@ import TopBar from './TopBar' import ViewOnlyBanner from './ViewOnlyBanner' export default function AppShell() { - const isLoaded = useProjectStore((s) => s.isLoaded) - const loadInitialDiagram = useProjectStore((s) => s.loadInitialDiagram) + const isLoaded = useDiagramStore((s) => s.isLoaded) + const loadInitialDiagram = useDiagramStore((s) => s.loadInitialDiagram) const isCatalogLoaded = useCatalogStore((s) => s.isLoaded) const loadCatalog = useCatalogStore((s) => s.loadCatalog) const loadMySubmissions = useSubmissionStore((s) => s.loadMySubmissions) diff --git a/src/components/layout/ConnectionErrorBanner.tsx b/src/components/layout/ConnectionErrorBanner.tsx index 73559f8..c591fbb 100644 --- a/src/components/layout/ConnectionErrorBanner.tsx +++ b/src/components/layout/ConnectionErrorBanner.tsx @@ -1,9 +1,9 @@ import { useEffect } from 'react' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' export default function ConnectionErrorBanner() { - const error = useProjectStore((s) => s.lastConnectionError) - const clear = useProjectStore((s) => s.clearConnectionError) + const error = useDiagramStore((s) => s.lastConnectionError) + const clear = useDiagramStore((s) => s.clearConnectionError) useEffect(() => { if (!error) return diff --git a/src/components/layout/DiagramManagerModal.tsx b/src/components/layout/DiagramManagerModal.tsx index 5c1a6b0..a274a9d 100644 --- a/src/components/layout/DiagramManagerModal.tsx +++ b/src/components/layout/DiagramManagerModal.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useProjectStore } from '../../state/projectStore' +import { useDiagramStore } from '../../state/diagramStore' import Modal from '../common/Modal' function formatUpdatedAt(iso: string): string { @@ -8,25 +8,25 @@ function formatUpdatedAt(iso: string): string { } export default function DiagramManagerModal({ onClose }: { onClose: () => void }) { - const project = useProjectStore((s) => s.project) - const diagramList = useProjectStore((s) => s.diagramList) - const refreshDiagramList = useProjectStore((s) => s.refreshDiagramList) - const newProject = useProjectStore((s) => s.newProject) - const switchToDiagram = useProjectStore((s) => s.switchToDiagram) - const deleteDiagram = useProjectStore((s) => s.deleteDiagram) + const diagram = useDiagramStore((s) => s.diagram) + const diagramList = useDiagramStore((s) => s.diagramList) + const refreshDiagramList = useDiagramStore((s) => s.refreshDiagramList) + const newDiagram = useDiagramStore((s) => s.newDiagram) + const switchToDiagram = useDiagramStore((s) => s.switchToDiagram) + const deleteDiagram = useDiagramStore((s) => s.deleteDiagram) useEffect(() => { refreshDiagramList() }, [refreshDiagramList]) const handleOpen = async (id: string) => { - if (id === project.id) return + if (id === diagram.id) return await switchToDiagram(id) onClose() } const handleNew = async () => { - await newProject() + await newDiagram() onClose() } @@ -52,7 +52,7 @@ export default function DiagramManagerModal({ onClose }: { onClose: () => void }