Add manufacturer catalog, category/manufacturer library management, and built-in cable flag

Normalizes manufacturer as a shared catalog entity (like device categories)
instead of free text on each device, giving the admin duplicate-detection
nudge a reliable signal. Adds full CRUD (including Admin direct-publish,
bypassing the submission queue) for categories, manufacturers, port types,
and cable types, plus a Categories & Manufacturers library modal and a
browse-by-manufacturer/search view in the device palette. Adds
Port.builtInCable so a captive/permanently-attached cable (a keyboard's USB
lead, a budget AVR's power cord) can be flagged and excluded from the BOM.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
2026-09-28 10:04:05 -05:00
co-authored by Claude Sonnet 5
parent 00777077c2
commit 29c3ed46a3
28 changed files with 2201 additions and 320 deletions
+26 -2
View File
@@ -1,5 +1,5 @@
import { v4 as uuid } from 'uuid'
import type { CableType, Catalog, Device, DeviceCategoryDef, Diagram, DeviceTemplate, PortType } from './types'
import type { CableType, Catalog, Device, DeviceCategoryDef, Diagram, DeviceTemplate, Manufacturer, PortType } from './types'
export function allPortTypes(catalog: Catalog): PortType[] {
return catalog.portTypes
@@ -35,6 +35,22 @@ export function deviceCategoryName(catalog: Catalog, categoryId: string): string
return getDeviceCategory(catalog, categoryId)?.name ?? categoryId
}
export function allManufacturers(catalog: Catalog): Manufacturer[] {
return catalog.manufacturers
}
export function getManufacturer(catalog: Catalog, manufacturerId: string): Manufacturer | undefined {
return allManufacturers(catalog).find((m) => m.id === manufacturerId)
}
/** Friendly name for a manufacturer id — unlike `deviceCategoryName`,
* `manufacturerId` is optional (not every device has a known manufacturer),
* so this returns undefined rather than a fallback string when it's unset
* or, for older data, references a manufacturer that no longer exists. */
export function manufacturerName(catalog: Catalog, manufacturerId: string | undefined): string | undefined {
return manufacturerId ? getManufacturer(catalog, manufacturerId)?.name : undefined
}
export function getPortType(catalog: Catalog, portTypeId: string): PortType | undefined {
return allPortTypes(catalog).find((pt) => pt.id === portTypeId)
}
@@ -43,6 +59,14 @@ export function getCableType(catalog: Catalog, cableTypeId: string): CableType |
return allCableTypes(catalog).find((ct) => ct.id === cableTypeId)
}
/** Best-effort friendly label for a raw family string: the name of whichever
* port type owns that family, or the raw string itself if none matches
* (e.g. a family that only ever existed as hand-typed text, from before
* FamilySelect started sourcing new families from an existing port type). */
export function familyLabel(portTypes: PortType[], family: string): string {
return portTypes.find((pt) => pt.family === family)?.name ?? family
}
/** The set of connector families a port type will mate with: its own family
* plus anything it explicitly lists as also-compatible. Shared with
* `compatibility.ts` so "does A connect to B" and "which cable fits" agree. */
@@ -145,7 +169,7 @@ export function createDeviceFromTemplate(template: DeviceTemplate, position: { x
name: template.name,
templateId: template.id,
category: template.category,
manufacturer: template.manufacturer,
manufacturerId: template.manufacturerId,
model: template.model,
ports: template.ports.map((port) => ({ ...port, id: uuid() })),
cost: template.cost,