diff --git a/src/components/bom/BomPanel.tsx b/src/components/bom/BomPanel.tsx index 69cbce1..05f949a 100644 --- a/src/components/bom/BomPanel.tsx +++ b/src/components/bom/BomPanel.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react' import { computeBom } from '../../domain/bom' +import { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' function formatCost(value: number): string { @@ -8,7 +9,8 @@ function formatCost(value: number): string { export default function BomPanel() { const project = useProjectStore((s) => s.project) - const bom = useMemo(() => computeBom(project), [project]) + const catalog = useCatalogStore((s) => s.catalog) + const bom = useMemo(() => computeBom(project, catalog), [project, catalog]) const hasMissingCost = bom.devices.some((line) => line.devicesMissingCost > 0) || bom.cables.some((line) => line.connectionsMissingCost > 0) diff --git a/src/components/canvas/FlowCanvas.tsx b/src/components/canvas/FlowCanvas.tsx index e14216c..6ac64ea 100644 --- a/src/components/canvas/FlowCanvas.tsx +++ b/src/components/canvas/FlowCanvas.tsx @@ -16,6 +16,7 @@ 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 { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' import CableEdge, { type CableFlowEdge } from './CableEdge' import DeviceNode, { type DeviceFlowNode, type PortInfo } from './DeviceNode' @@ -38,9 +39,10 @@ function FlowCanvasInner() { const selectDevice = useProjectStore((s) => s.selectDevice) const selectConnection = useProjectStore((s) => s.selectConnection) const { screenToFlowPosition } = useReactFlow() + const catalog = useCatalogStore((s) => s.catalog) - const portTypes = useMemo(() => allPortTypes(project), [project]) - const cableTypes = useMemo(() => allCableTypes(project), [project]) + const portTypes = useMemo(() => allPortTypes(catalog), [catalog]) + const cableTypes = useMemo(() => allCableTypes(catalog), [catalog]) const nodes: DeviceFlowNode[] = useMemo( () => @@ -58,10 +60,10 @@ function FlowCanvasInner() { type: 'device', position: device.position, selected: device.id === selectedDeviceId, - data: { device, categoryName: deviceCategoryName(project, device.category), portInfos }, + data: { device, categoryName: deviceCategoryName(catalog, device.category), portInfos }, } }), - [project, portTypes, selectedDeviceId], + [project, catalog, portTypes, selectedDeviceId], ) const edges: CableFlowEdge[] = useMemo( @@ -92,14 +94,14 @@ function FlowCanvasInner() { const sourcePortId = parsePortId(c.sourceHandle) const targetPortId = parsePortId(c.targetHandle) if (!c.source || !c.target || !sourcePortId || !targetPortId) return false - return validateConnection(project, { + return validateConnection(project, catalog, { sourceDeviceId: c.source, sourcePortId, targetDeviceId: c.target, targetPortId, }).valid }, - [project], + [project, catalog], ) const onConnect = useCallback( @@ -111,12 +113,12 @@ function FlowCanvasInner() { const targetDevice = project.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(project, sourcePort.portTypeId) - const targetPortType = targetPort && getPortType(project, targetPort.portTypeId) + const sourcePortType = sourcePort && getPortType(catalog, sourcePort.portTypeId) + const targetPortType = targetPort && getPortType(catalog, targetPort.portTypeId) const defaultCableType = - sourcePortType && targetPortType && cableTypesForConnection(project, sourcePortType, targetPortType)[0] + sourcePortType && targetPortType && cableTypesForConnection(catalog, sourcePortType, targetPortType)[0] if (!defaultCableType) return - addConnection({ + addConnection(catalog, { sourceDeviceId: connection.source, sourcePortId, targetDeviceId: connection.target, @@ -124,7 +126,7 @@ function FlowCanvasInner() { cableTypeId: defaultCableType.id, }) }, - [project, addConnection], + [project, catalog, addConnection], ) const onNodesChange = useCallback( @@ -161,9 +163,9 @@ function FlowCanvasInner() { if (!templateId) return event.preventDefault() const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }) - addDeviceFromTemplate(templateId, position) + addDeviceFromTemplate(catalog, templateId, position) }, - [screenToFlowPosition, addDeviceFromTemplate], + [screenToFlowPosition, catalog, addDeviceFromTemplate], ) return ( diff --git a/src/components/common/CategorySelect.tsx b/src/components/common/CategorySelect.tsx index 93f450e..1bcd5bd 100644 --- a/src/components/common/CategorySelect.tsx +++ b/src/components/common/CategorySelect.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { allDeviceCategories } from '../../domain/project' -import { useProjectStore } from '../../state/projectStore' +import { useCatalogStore } from '../../state/catalogStore' const NEW_CATEGORY_VALUE = '__new__' @@ -16,20 +16,20 @@ export default function CategorySelect({ onChange: (categoryId: string) => void className?: string }) { - const project = useProjectStore((s) => s.project) - const addCustomDeviceCategory = useProjectStore((s) => s.addCustomDeviceCategory) + const catalog = useCatalogStore((s) => s.catalog) + const addCustomDeviceCategory = useCatalogStore((s) => s.addCustomDeviceCategory) const [creating, setCreating] = useState(false) const [draftName, setDraftName] = useState('') const categories = useMemo( - () => [...allDeviceCategories(project)].sort((a, b) => a.name.localeCompare(b.name)), - [project], + () => [...allDeviceCategories(catalog)].sort((a, b) => a.name.localeCompare(b.name)), + [catalog], ) - const commitNewCategory = () => { + const commitNewCategory = async () => { const trimmed = draftName.trim() if (trimmed) { - onChange(addCustomDeviceCategory(trimmed)) + onChange(await addCustomDeviceCategory(trimmed)) } setCreating(false) setDraftName('') diff --git a/src/components/inspector/ConnectionInspector.tsx b/src/components/inspector/ConnectionInspector.tsx index 640c624..b15de00 100644 --- a/src/components/inspector/ConnectionInspector.tsx +++ b/src/components/inspector/ConnectionInspector.tsx @@ -1,9 +1,11 @@ import { useMemo } from 'react' import { cableTypesForConnection, getPortType } from '../../domain/project' +import { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' export default function ConnectionInspector({ connectionId }: { connectionId: string }) { const project = useProjectStore((s) => s.project) + const catalog = useCatalogStore((s) => s.catalog) const updateConnection = useProjectStore((s) => s.updateConnection) const removeConnection = useProjectStore((s) => s.removeConnection) @@ -15,10 +17,10 @@ export default function ConnectionInspector({ connectionId }: { connectionId: st const availableCableTypes = useMemo(() => { if (!sourcePort || !targetPort) return [] - const sourcePortType = getPortType(project, sourcePort.portTypeId) - const targetPortType = getPortType(project, targetPort.portTypeId) - return sourcePortType && targetPortType ? cableTypesForConnection(project, sourcePortType, targetPortType) : [] - }, [project, sourcePort, targetPort]) + const sourcePortType = getPortType(catalog, sourcePort.portTypeId) + const targetPortType = getPortType(catalog, targetPort.portTypeId) + return sourcePortType && targetPortType ? cableTypesForConnection(catalog, sourcePortType, targetPortType) : [] + }, [catalog, sourcePort, targetPort]) const cableCostPerUnit = availableCableTypes.find((ct) => ct.id === connection?.cableTypeId)?.costPerUnit diff --git a/src/components/inspector/DeviceInspector.tsx b/src/components/inspector/DeviceInspector.tsx index 5b64943..cf3d1f7 100644 --- a/src/components/inspector/DeviceInspector.tsx +++ b/src/components/inspector/DeviceInspector.tsx @@ -2,11 +2,13 @@ import { useMemo, useState } from 'react' import { PORT_DIRECTIONS } from '../../domain/constants' import { allPortTypes } from '../../domain/project' import type { PortDirection } from '../../domain/types' +import { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' import CategorySelect from '../common/CategorySelect' export default function DeviceInspector({ deviceId }: { deviceId: string }) { const project = useProjectStore((s) => s.project) + const catalog = useCatalogStore((s) => s.catalog) const updateDevice = useProjectStore((s) => s.updateDevice) const removeDevice = useProjectStore((s) => s.removeDevice) const addPort = useProjectStore((s) => s.addPort) @@ -17,8 +19,8 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) { const device = project.devices.find((d) => d.id === deviceId) const portTypes = useMemo( - () => [...allPortTypes(project)].sort((a, b) => a.name.localeCompare(b.name)), - [project], + () => [...allPortTypes(catalog)].sort((a, b) => a.name.localeCompare(b.name)), + [catalog], ) if (!device) return null diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index a3e5ac9..73e15c0 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -1,4 +1,5 @@ import { useEffect } from 'react' +import { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' import FlowCanvas from '../canvas/FlowCanvas' import DevicePalette from '../palette/DevicePalette' @@ -9,12 +10,15 @@ import TopBar from './TopBar' export default function AppShell() { const isLoaded = useProjectStore((s) => s.isLoaded) const loadInitialDiagram = useProjectStore((s) => s.loadInitialDiagram) + const isCatalogLoaded = useCatalogStore((s) => s.isLoaded) + const loadCatalog = useCatalogStore((s) => s.loadCatalog) useEffect(() => { loadInitialDiagram() - }, [loadInitialDiagram]) + loadCatalog() + }, [loadInitialDiagram, loadCatalog]) - if (!isLoaded) { + if (!isLoaded || !isCatalogLoaded) { return
Loading…
} diff --git a/src/components/palette/CableTypeManager.tsx b/src/components/palette/CableTypeManager.tsx index 11875ff..9bba471 100644 --- a/src/components/palette/CableTypeManager.tsx +++ b/src/components/palette/CableTypeManager.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react' import { allCableTypes, allPortTypes } from '../../domain/project' import type { CableType, PortType } from '../../domain/types' -import { useProjectStore } from '../../state/projectStore' +import { useCatalogStore } from '../../state/catalogStore' const UNITS: CableType['unit'][] = ['ft', 'm'] @@ -129,19 +129,19 @@ function CableTypeForm({ } export default function CableTypesPanel() { - const project = useProjectStore((s) => s.project) - const addCustomCableType = useProjectStore((s) => s.addCustomCableType) - const updateCustomCableType = useProjectStore((s) => s.updateCustomCableType) + const catalog = useCatalogStore((s) => s.catalog) + const addCustomCableType = useCatalogStore((s) => s.addCustomCableType) + const updateCustomCableType = useCatalogStore((s) => s.updateCustomCableType) const [creating, setCreating] = useState(false) const [editingId, setEditingId] = useState(null) const portTypes = useMemo( - () => [...allPortTypes(project)].sort((a, b) => a.name.localeCompare(b.name)), - [project], + () => [...allPortTypes(catalog)].sort((a, b) => a.name.localeCompare(b.name)), + [catalog], ) const allTypes = useMemo( - () => [...allCableTypes(project)].sort((a, b) => a.name.localeCompare(b.name)), - [project], + () => [...allCableTypes(catalog)].sort((a, b) => a.name.localeCompare(b.name)), + [catalog], ) const customTypes = useMemo(() => allTypes.filter((ct) => ct.custom), [allTypes]) const builtInTypes = useMemo(() => allTypes.filter((ct) => !ct.custom), [allTypes]) diff --git a/src/components/palette/DevicePalette.tsx b/src/components/palette/DevicePalette.tsx index 55c796e..275be69 100644 --- a/src/components/palette/DevicePalette.tsx +++ b/src/components/palette/DevicePalette.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react' -import { allDeviceCategories, allDeviceTemplates, hiddenBuiltInDeviceTemplates } from '../../domain/project' +import { allDeviceCategories, allDeviceTemplates, hiddenDeviceTemplates } from '../../domain/project' import type { DeviceTemplate } from '../../domain/types' +import { useCatalogStore } from '../../state/catalogStore' import { useProjectStore } from '../../state/projectStore' import { TEMPLATE_DRAG_MIME } from '../canvas/FlowCanvas' import Chevron from '../common/Chevron' @@ -10,9 +11,11 @@ import DeviceTemplateEditor from './DeviceTemplateEditor' export default function DevicePalette() { const project = useProjectStore((s) => s.project) const addDeviceFromTemplate = useProjectStore((s) => s.addDeviceFromTemplate) - const removeCustomDeviceTemplate = useProjectStore((s) => s.removeCustomDeviceTemplate) - const hideBuiltInDeviceTemplate = useProjectStore((s) => s.hideBuiltInDeviceTemplate) - const restoreBuiltInDeviceTemplate = useProjectStore((s) => s.restoreBuiltInDeviceTemplate) + const catalog = useCatalogStore((s) => s.catalog) + const hiddenPublicIds = useCatalogStore((s) => s.hiddenPublicDeviceTemplateIds) + const removeCustomDeviceTemplate = useCatalogStore((s) => s.removeCustomDeviceTemplate) + const hidePublicDeviceTemplate = useCatalogStore((s) => s.hidePublicDeviceTemplate) + const restorePublicDeviceTemplate = useCatalogStore((s) => s.restorePublicDeviceTemplate) const [editorTarget, setEditorTarget] = useState<'new' | DeviceTemplate | null>(null) const [portTypesOpen, setPortTypesOpen] = useState(false) const [hiddenListOpen, setHiddenListOpen] = useState(false) @@ -21,10 +24,10 @@ export default function DevicePalette() { const [expandedCategories, setExpandedCategories] = useState>(new Set()) const groups = useMemo(() => { - const categories = allDeviceCategories(project) + const categories = allDeviceCategories(catalog) const categoryName = (id: string) => categories.find((c) => c.id === id)?.name ?? id const byCategory = new Map() - for (const template of allDeviceTemplates(project)) { + for (const template of allDeviceTemplates(catalog, hiddenPublicIds)) { const list = byCategory.get(template.category) ?? [] list.push(template) byCategory.set(template.category, list) @@ -36,7 +39,7 @@ export default function DevicePalette() { templates: templates.sort((a, b) => a.name.localeCompare(b.name)), })) .sort((a, b) => a.categoryName.localeCompare(b.categoryName)) - }, [project]) + }, [catalog, hiddenPublicIds]) const toggleCategory = (categoryId: string) => { setExpandedCategories((prev) => { @@ -59,15 +62,15 @@ export default function DevicePalette() { const handleQuickAdd = (template: DeviceTemplate) => { // Cascade placement so repeated quick-adds don't stack exactly on top of each other. const offset = (project.devices.length % 8) * 24 - addDeviceFromTemplate(template.id, { x: 80 + offset, y: 80 + offset }) + addDeviceFromTemplate(catalog, template.id, { x: 80 + offset, y: 80 + offset }) } const handleDelete = (template: DeviceTemplate) => { - if (template.builtIn) { + if (!template.custom) { const confirmed = window.confirm( - `Hide "${template.name}" from your device library? It's a built-in default, so you can bring it back later from "Show hidden defaults" at the bottom of this panel. Devices already placed on the canvas from it won't be affected.`, + `Hide "${template.name}" from your device library? It's a public default, so you can bring it back later from "Show hidden defaults" at the bottom of this panel. Devices already placed on the canvas from it won't be affected.`, ) - if (confirmed) hideBuiltInDeviceTemplate(template.id) + if (confirmed) hidePublicDeviceTemplate(template.id) return } const confirmed = window.confirm( @@ -76,7 +79,7 @@ export default function DevicePalette() { if (confirmed) removeCustomDeviceTemplate(template.id) } - const hiddenDefaults = useMemo(() => hiddenBuiltInDeviceTemplates(project), [project]) + const hiddenDefaults = useMemo(() => hiddenDeviceTemplates(catalog, hiddenPublicIds), [catalog, hiddenPublicIds]) return (