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 (