Wire the app to the shared Supabase catalog (public catalog read integration)

Moves port/cable/category/device-template data from per-diagram embedded
storage to the global public/private catalog backed by Supabase, per
organized-ideas.md's "live reference, not snapshot" decision.

- domain/types.ts, project.ts, compatibility.ts, bom.ts: lookup functions
  now take an explicit Catalog parameter instead of deriving data from
  Project — Project is reduced to just diagram-scoped fields.
- New CatalogRepository/SupabaseCatalogRepository (mirrors the
  DiagramRepository pattern) and catalogStore.ts, replacing the
  catalog-related actions that used to live in projectStore.
- device_templates gets a plain-text manufacturer column for now (the
  normalized manufacturer catalog from organized-ideas.md §3 is its own
  future pass, not blocking this one).
- domain/library.ts is no longer imported by the app — it's now only the
  source scripts/generate-seed.mjs reads to produce supabase/seed.sql.
- Swept every UI call site via tsc -b until clean; oxlint clean; 23/23
  pgTAP RLS tests still passing after a `supabase db reset`.
This commit is contained in:
2026-09-07 23:42:00 -05:00
parent 93cb4a9617
commit b97777444a
22 changed files with 656 additions and 323 deletions
+19 -16
View File
@@ -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<Set<string>>(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<string, DeviceTemplate[]>()
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 (
<aside className="flex h-full w-64 shrink-0 flex-col border-r border-slate-200 bg-slate-50">
@@ -129,11 +132,11 @@ export default function DevicePalette() {
<div className="truncate font-medium text-slate-700">{template.name}</div>
<div className="truncate text-[10px] text-slate-400">
{template.ports.length} port{template.ports.length === 1 ? '' : 's'}
{!template.builtIn ? ' · custom' : ''}
{template.custom ? ' · custom' : ''}
</div>
</div>
<div className="ml-2 flex shrink-0 items-center gap-1 opacity-0 group-hover:opacity-100">
{!template.builtIn && (
{template.custom && (
<button
onClick={() => setEditorTarget(template)}
title="Edit this device"
@@ -144,7 +147,7 @@ export default function DevicePalette() {
)}
<button
onClick={() => handleDelete(template)}
title={template.builtIn ? 'Hide this default device' : 'Delete from library'}
title={template.custom ? 'Delete from library' : 'Hide this default device'}
className="rounded bg-slate-100 px-1.5 py-0.5 text-slate-500 hover:bg-red-50 hover:text-red-600"
>
✕
@@ -183,7 +186,7 @@ export default function DevicePalette() {
<li key={template.id} className="flex items-center justify-between rounded bg-white px-2 py-1 text-xs">
<span className="truncate text-slate-600">{template.name}</span>
<button
onClick={() => restoreBuiltInDeviceTemplate(template.id)}
onClick={() => restorePublicDeviceTemplate(template.id)}
className="shrink-0 rounded bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600 hover:bg-indigo-100 hover:text-indigo-700"
>
Restore