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:
@@ -5,6 +5,7 @@ import type { PortDirection } from '../../domain/types'
|
||||
import { useCatalogStore } from '../../state/catalogStore'
|
||||
import { useDiagramStore } from '../../state/diagramStore'
|
||||
import CategorySelect from '../common/CategorySelect'
|
||||
import ManufacturerSelect from '../common/ManufacturerSelect'
|
||||
|
||||
export default function DeviceInspector({ deviceId }: { deviceId: string }) {
|
||||
const diagram = useDiagramStore((s) => s.diagram)
|
||||
@@ -47,10 +48,9 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) {
|
||||
</label>
|
||||
<label className="text-xs font-medium text-slate-600">
|
||||
Manufacturer
|
||||
<input
|
||||
value={device.manufacturer ?? ''}
|
||||
onChange={(e) => updateDevice(device.id, { manufacturer: e.target.value })}
|
||||
className="mt-1 w-full rounded border border-slate-300 px-2 py-1 text-sm"
|
||||
<ManufacturerSelect
|
||||
value={device.manufacturerId}
|
||||
onChange={(manufacturerId) => updateDevice(device.id, { manufacturerId })}
|
||||
/>
|
||||
</label>
|
||||
<label className="text-xs font-medium text-slate-600">
|
||||
@@ -95,7 +95,7 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) {
|
||||
{device.ports.map((port) => {
|
||||
const inUse = connectionCountForPort(port.id) > 0
|
||||
return (
|
||||
<div key={port.id} className="flex items-center gap-1.5">
|
||||
<div key={port.id} className="flex flex-wrap items-center gap-1.5">
|
||||
<input
|
||||
ref={(el) => {
|
||||
if (el && focusPortId === port.id) {
|
||||
@@ -129,23 +129,38 @@ export default function DeviceInspector({ deviceId }: { deviceId: string }) {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => setFocusPortId(clonePort(device.id, port.id))}
|
||||
tabIndex={-1}
|
||||
title="Duplicate this port"
|
||||
className="shrink-0 rounded px-1 text-slate-400 hover:bg-indigo-50 hover:text-indigo-600"
|
||||
>
|
||||
⧉
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removePort(device.id, port.id)}
|
||||
disabled={inUse}
|
||||
tabIndex={-1}
|
||||
title={inUse ? 'Disconnect its cable first' : 'Remove port'}
|
||||
className="shrink-0 rounded px-1 text-slate-400 hover:bg-red-50 hover:text-red-500 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{/* Grouped so these three wrap to their own line as a unit
|
||||
(aligned to the row's end) rather than each one reflowing
|
||||
separately if the row ever gets tighter than it can fit. */}
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
onClick={() => updatePort(device.id, port.id, { builtInCable: !port.builtInCable })}
|
||||
tabIndex={-1}
|
||||
title="Built-in cable"
|
||||
className={`shrink-0 rounded px-1 ${
|
||||
port.builtInCable ? 'bg-amber-100 opacity-100 ring-1 ring-amber-300' : 'opacity-30 hover:opacity-60'
|
||||
}`}
|
||||
>
|
||||
🔌
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFocusPortId(clonePort(device.id, port.id))}
|
||||
tabIndex={-1}
|
||||
title="Duplicate this port"
|
||||
className="shrink-0 rounded px-1 text-slate-400 hover:bg-indigo-50 hover:text-indigo-600"
|
||||
>
|
||||
⧉
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removePort(device.id, port.id)}
|
||||
disabled={inUse}
|
||||
tabIndex={-1}
|
||||
title={inUse ? 'Disconnect its cable first' : 'Remove port'}
|
||||
className="shrink-0 rounded px-1 text-slate-400 hover:bg-red-50 hover:text-red-500 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user