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:
@@ -0,0 +1,83 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
const POPOVER_WIDTH = 256
|
||||
const VIEWPORT_MARGIN = 8
|
||||
|
||||
/**
|
||||
* A small "i" affordance that reveals a block of help text on click — for a
|
||||
* concept that needs more than a one-line inline hint can carry, without
|
||||
* permanently taking up screen space. Click-toggled rather than hover-only
|
||||
* so it works the same on touch as with a mouse, and stays open long enough
|
||||
* to actually read instead of vanishing the moment the pointer drifts.
|
||||
*
|
||||
* Renders the popover through a portal to `document.body`, positioned via
|
||||
* the trigger's own bounding rect rather than as a normal absolutely-
|
||||
* positioned child — this component is mostly used inside a Modal, whose
|
||||
* scrollable body clips anything (including absolutely-positioned children)
|
||||
* that pokes past its edge, which cut the popover off before this existed.
|
||||
*/
|
||||
export default function InfoTooltip({ text }: { text: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState<{ top: number; left: number } | null>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const popoverRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const toggle = () => {
|
||||
if (!open && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect()
|
||||
const left = Math.min(Math.max(rect.left, VIEWPORT_MARGIN), window.innerWidth - POPOVER_WIDTH - VIEWPORT_MARGIN)
|
||||
setPosition({ top: rect.bottom + 4, left })
|
||||
}
|
||||
setOpen((v) => !v)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onOutsideClick = (e: MouseEvent) => {
|
||||
const target = e.target as Node
|
||||
if (buttonRef.current?.contains(target) || popoverRef.current?.contains(target)) return
|
||||
setOpen(false)
|
||||
}
|
||||
// Also close on scroll (e.g. the modal body it's usually opened inside)
|
||||
// rather than tracking the trigger's position live — simplest way to
|
||||
// avoid a stale popover that's drifted away from its "i" icon.
|
||||
const close = () => setOpen(false)
|
||||
document.addEventListener('mousedown', onOutsideClick)
|
||||
document.addEventListener('scroll', close, true)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onOutsideClick)
|
||||
document.removeEventListener('scroll', close, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
title="More info"
|
||||
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-0 bg-transparent p-0 text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-4 w-4">
|
||||
<circle cx="10" cy="10" r="8.25" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M10 9.5v5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<circle cx="10" cy="6.5" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
{open &&
|
||||
position &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popoverRef}
|
||||
style={{ position: 'fixed', top: position.top, left: position.left, width: POPOVER_WIDTH }}
|
||||
className="z-[60] rounded border border-slate-200 bg-white p-2 text-[11px] font-normal normal-case leading-snug text-slate-600 shadow-lg"
|
||||
>
|
||||
{text}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user