{"version":3,"sources":["../src/utils/handle-ids.ts","../src/utils/node-ids.ts","../src/utils/splice-positioning.ts","../src/types/index.ts","../src/utils/port-position.ts","../src/utils/render-ports.ts","../src/utils/port-contract.ts","../src/utils/port-coercion.ts","../src/utils/power-connectors.ts","../src/utils/document-adapter.ts","../src/utils/path-layout.ts","../src/utils/connection-rules.ts"],"names":["DIRECTIONAL_HANDLE_SUFFIX","FACE_ALIAS_HANDLE_SUFFIX","normalizeDirectionalHandleId","handleId","normalizeHandleId","isPowerPortType","portType","PDU_INLET_HANDLE_ID","BOARD_INCOMER_HANDLE_ID","POWER_INLET_HANDLE_IDS","isPowerInletHandleId","HEX_PAIR","RESERVED_SEGMENT_CHAR","encodeHandleSegment","segment","decodeHandleSegment","decoded","i","percentEscapeChar","ch","hex","baseCollidesWithGrammar","base","classifyHandleBase","escapeCollidingLabel","label","buildDevicePortBase","group","encodedGroup","buildPortHandleId","portBlockName","port","decodePortHandleId","delimiter","looksLikePowerHandleId","compact","applyDirectionalPortHandleId","direction","options","OUTLET_BASE","FIBER_TUBE_BASE","FIBER_BASE","TUBE_BASE","SIDE_PORT_BASE","SPLIT_IO_BASE","BREAKER_WAY_BASE","fiberTube","fiber","tube","sidePort","splitIo","breakerWay","alternateShapes","shape","parseHandleId","raw","directionMatch","afterDirection","faceMatch","parsed","alternates","buildFibreHandleId","buildTubeHandleId","buildSpliceHandleId","side","buildCouplerHandleId","index","buildSplitIoHandleId","io","buildBreakerWayHandleId","wayId","applyFaceAliasHandleId","face","sanitizeHandleIdForMatching","handlesMatch","a","b","findEdgesForPortHandle","edges","nodeId","matched","edge","getOppositeSideHandleId","encodeHandle","opts","out","decodeHandle","RACK_NODE_ID_PREFIX","DEVICE_NODE_ID_PREFIX","buildRackNodeId","rackId","buildDeviceNodeId","deviceId","parseNodeId","EQUIPMENT_NODE_TYPES","CONTAINER_NODE_TYPES","getNodeKind","node","type","isRackNode","isDeviceNode","HOLDER_HEIGHT_PX_SINGLE","HOLDER_HEIGHT_PX_RIBBON","HOLDER_HEIGHT_PX","TRAY_HEADER_HEIGHT","TRAY_BOTTOM_PADDING","HOLDERS_PER_TRAY","SPLICE_WIDTH_PX","SPLICE_HEIGHT_PX_SINGLE","SPLICE_HEIGHT_PX_RIBBON","SPLICE_HEIGHT_PX","TRAY_WIDTH_PX","TRAY_NODE_HEIGHT_SINGLE","TRAY_NODE_HEIGHT_RIBBON","TRAY_NODE_HEIGHT","SPLICE_CENTER_X","calculateSplicePositionFromNumber","tray","holderPosition","calculateSplicePositionFromNumberWithData","isRibbonMode","startY","holderHeight","baseOffset","spliceY","Width","isRecord","value","toNumber","normalizeSemanticPositionTokens","token","canonicalizeSideToken","hasCenterToken","tokens","resolveAxisOffsetPercent","primarySide","hasLeft","hasRight","hasTop","hasBottom","hasCenter","resolveSemanticEdgePortPosition","offsetPercent","resolveSemanticCompositePortPosition","hasFront","hasRear","xPercent","yPercent","parseSemanticPortPosition","normalizePortPositionInput","sideRaw","semanticPosition","offsetPx","widthPercent","heightPercent","DEFAULT_FRONT_GROUP_NAME","DEFAULT_REAR_GROUP_NAME","DEFAULT_POWER_GROUP_NAME","DISTRIBUTED_OFFSET_MIN_PERCENT","DISTRIBUTED_OFFSET_MAX_PERCENT","clampPercent","normalizePortAnchor","getNormalizedPortPosition","position","hasExplicitAbsoluteCoordinates","hasExplicitOffset","resolveRenderPortFace","resolveRenderPortGroupName","getEntrySortValue","entry","toDistributedOffsetPercent","count","buildDistributedOffsetMap","entries","distributedEntries","offsetPercentByIndex","entriesByAnchor","anchor","anchorEntries","sortedEntries","left","right","orderDifference","toPortPosition","normalizedPosition","distributedOffsetPercent","resolvedOffsetPercent","toPort","normalizePortBlockCollection","portBlocks","normalizedBlocks","block","portsByUnpositionedSide","ports","isGroupedPortBlockArray","isFlatDevicePortArray","buildPortBlocksFromPorts","devicePorts","bucketsByKey","indexedPorts","distributedOffsetsByFace","groupName","key","existingBucket","rearPorts","bucket","distributedOffsetByIndex","getResolvedPortBlocks","data","toText","trimmed","normalizeFaceInput","normalized","CONNECTOR_TYPES","normalizeConnectorType","resolveDefaultFace","resolveDefaultGroup","resolveDevicePortFace","resolveDevicePortGroup","normalizeDevicePorts","order","normalizePortBlocks","rawPort","resolveNodeDevicePorts","flatPorts","frontFromPorts","frontPorts","rearFromGroups","blockPorts","flattenDevicePortsFromGroups","portGroups","rearPortGroups","appendFace","blocks","buildDevicePortGroupsFromPorts","listPossiblePortHandleIds","baseHandle","normalizedDeviceType","describePortHandle","nodeData","handle","sanitize","normalizedRequestedHandle","findInBlocks","portBlock","candidate","validatePortHandleOnNodeData","resolveDevicePortBlocksFromFlatPorts","FIBER_CONNECTOR_TYPES","isFibreConnectorType","toPortCableType","getPDUPortType","portIndex","MAX_POWER_PORTS","KNOWN_DIAGRAM_NODE_KINDS","CONTAINER_SUBTYPES","IMAGE_RENDER_KEYS","DIMENSION_RENDER_KEYS","GENERAL_RENDER_KEYS","UI_NODE_STATE_KEYS","EXCLUDED_CONFIG_KEYS","setOwn","target","clonePosition","clonePortPosition","toDiagramNodeKind","toLegacyNodeType","normalizedSubtype","subtype","toDiagramPlacement","placement","parentId","toDiagramRender","render","images","acc","dimensions","toDiagramExternalRefs","refs","toDiagramConfig","config","extractUiState","nodes","previousUi","nodeState","resolvedFace","inferEndpointMedium","normalizedType","buildPduPortBlocks","powerPorts","_","connectorType","toPortBasedEndpoints","endpoints","frontIds","endpoint","rearAliasBase","collectFrontEndpointIds","createSyntheticEndpoints","way","fibersPerTube","tubeIndex","fiberCount","tubeCount","tubeStartIndex","portCount","inputCount","outputCount","toDiagramEndpoints","ENDPOINT_SOURCE_KEYS","endpointSourceFingerprint","fingerprint","sameFingerprint","endpointDerivationCache","copyEndpoints","toDiagramEndpointsCached","cached","mergeEndpoints","previousEndpoints","nextEndpoints","previousById","previous","mergeDocumentNode","previousNode","nextNode","networkGraphToDiagramDocument","snapshot","previousDocument","previousNodesById","aliasedIdsByNodeId","aliased","toEndpointId","previousEdgesById","nextEdge","endpointToPortType","connector","endpointPortFields","connectedEndpointIds","endpointsToDevicePorts","endpointsToPortBlocks","grouped","existing","name","applyRenderToLegacyData","uiEntry","nextData","applyExternalRefsToLegacyData","ref","buildEndpointIndex","byId","findEndpointRecord","endpointId","endpointById","effectiveEquipmentDeviceType","configured","resolveEndpointHandleId","role","deviceType","isPowerEndpoint","resolveNodePlacement","sanitizeDocumentNodes","rawNodes","seenIds","isEdgeEnd","diagramDocumentToNetworkGraph","document","input","documentNodes","rawEdges","documentEdges","nodesById","endpointsByNodeId","connectedByNodeId","markConnected","record","set","uiNodeState","noConnections","legacyType","connected","rearEndpoints","frontEndpoints","producedNodeIds","danglingEdgeIds","id","buildPathLayoutGraph","steps","spacing","origin","step","layoutEdges","normalizeHandle","_node","classifyFibreEndpoint","ALLOWED_FIBER_PAIRS","isTubeHandleClass","evaluateFibreConnectionRule","sourceNode","sourceHandle","targetNode","targetHandle","sourceIsRibbon","targetIsRibbon","sourceClass","targetClass"],"mappings":"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","file":"model.mjs","sourcesContent":["import type { NetworkEdge, Port } from \"../types\";\n\nconst DIRECTIONAL_HANDLE_SUFFIX = /-(source|target)$/i;\nconst FACE_ALIAS_HANDLE_SUFFIX = /--(rear|front)$/i;\n\n/**\n * Normalize source/target directional suffixes used for bidirectional handles.\n * Example: `Rear Ports-1-target` -\\> `Rear Ports-1`.\n */\nexport function normalizeDirectionalHandleId(\n  handleId: string | null | undefined\n): string {\n  if (!handleId) return \"\";\n  return handleId.replace(DIRECTIONAL_HANDLE_SUFFIX, \"\");\n}\n\n/**\n * Normalize rendered handle ids back to their canonical endpoint id.\n * This removes the directional suffix and the split-face alias, in the order\n * the renderer composes them: `<base>[--rear|--front][-source|-target]`.\n * (Previously only `--rear` was stripped; `--front` ghost aliases now\n * normalize too.)\n *\n * Losslessness: the strip is unconditional, so it is only safe when the base\n * itself cannot end in a reserved suffix. {@link buildPortHandleId} guarantees\n * this for device ports — a label like `target` or `foo-target` is escaped at\n * build time (`<group>-%74arget`, `<group>-foo%2Dtarget`) so it survives\n * normalization intact. Bare power labels and explicit `port.id`s are emitted\n * verbatim, so a caller that chooses an id ending in `-source`/`-target`/\n * `--rear`/`--front` owns that collision.\n */\nexport function normalizeHandleId(handleId: string | null | undefined): string {\n  return normalizeDirectionalHandleId(handleId).replace(\n    FACE_ALIAS_HANDLE_SUFFIX,\n    \"\"\n  );\n}\n\n/**\n * Returns true when a port type is considered power-related.\n */\nexport function isPowerPortType(portType: string | undefined): boolean {\n  return (\n    typeof portType === \"string\" && portType.toLowerCase().includes(\"power\")\n  );\n}\n\n/**\n * Handle id of a vertical PDU's supply inlet — the single point where a branch\n * circuit from a distribution-board breaker way lands.\n *\n * Deliberately non-numeric so it can never collide with an outlet id, which is\n * always a bare 1-based number (`\"1\"…\"N\"`).\n * @public\n */\nexport const PDU_INLET_HANDLE_ID = \"inlet\";\n\n/**\n * Handle id of a distribution board's incomer — the board's own supply inlet,\n * mounted by `DistributionBoardNode`. The board analogue of\n * {@link PDU_INLET_HANDLE_ID}.\n * @public\n */\nexport const BOARD_INCOMER_HANDLE_ID = \"incomer\";\n\nconst POWER_INLET_HANDLE_IDS = new Set<string>([\n  PDU_INLET_HANDLE_ID,\n  BOARD_INCOMER_HANDLE_ID,\n]);\n\n/**\n * Whether a handle id names a power-distribution node's supply inlet\n * (`inlet` on a PDU strip, `incomer` on a distribution board). Directional and\n * face-alias suffixes are normalized away first.\n * @public\n */\nexport function isPowerInletHandleId(handleId: string | null | undefined): boolean {\n  if (!handleId) return false;\n  return POWER_INLET_HANDLE_IDS.has(normalizeHandleId(handleId).trim().toLowerCase());\n}\n\n// ---------------------------------------------------------------------------\n// Device-port group/label codec\n//\n// buildPortHandleId joins a non-power port's group and label with `-`. A raw\n// join is non-injective — group `A` + label `B-C` and group `A-B` + label `C`\n// both produce `A-B-C` — and a clean join can also reproduce a fixed grammar\n// production (group `fiber` + label `3` -> `fiber-3`) or a directional/face\n// suffix (label `target` -> `<group>-target`, which normalizeHandleId would\n// truncate). Each segment is therefore percent-escaped over the two structural\n// characters only — the delimiter `-` (`%2D`) and the escape introducer `%`\n// (`%25`) — so the joined base has exactly one literal `-` (the join) and is\n// unambiguously splittable. A segment containing neither character is emitted\n// verbatim, so the overwhelmingly common case is byte-for-byte unchanged.\n// ---------------------------------------------------------------------------\n\n/** Two hex digits, for the tolerant percent-decoder. */\nconst HEX_PAIR = /^[0-9a-fA-F]{2}$/;\n\n/** A character the join grammar reserves and must escape (`-` or `%`). */\nconst RESERVED_SEGMENT_CHAR = /[-%]/;\n\n/**\n * Percent-escape the two structural characters (`%` then `-`) in a group/label\n * segment. Segments with neither are returned unchanged.\n */\nfunction encodeHandleSegment(segment: string): string {\n  if (!RESERVED_SEGMENT_CHAR.test(segment)) {\n    return segment;\n  }\n  return segment.replace(/%/g, \"%25\").replace(/-/g, \"%2D\");\n}\n\n/**\n * Inverse of {@link encodeHandleSegment}: a tolerant single-pass percent-decode\n * that reverses `%XX` escapes and leaves any malformed `%` literal, so it never\n * throws.\n */\nfunction decodeHandleSegment(segment: string): string {\n  if (!segment.includes(\"%\")) {\n    return segment;\n  }\n  let decoded = \"\";\n  for (let i = 0; i < segment.length; i += 1) {\n    if (\n      segment[i] === \"%\" &&\n      i + 2 < segment.length &&\n      HEX_PAIR.test(segment.slice(i + 1, i + 3))\n    ) {\n      decoded += String.fromCharCode(Number.parseInt(segment.slice(i + 1, i + 3), 16));\n      i += 2;\n    } else {\n      decoded += segment[i];\n    }\n  }\n  return decoded;\n}\n\n/** Percent-escape a single (ASCII) character, e.g. `3` -> `%33`. */\nfunction percentEscapeChar(ch: string): string {\n  const hex = ch.charCodeAt(0).toString(16).toUpperCase();\n  return `%${hex.length === 1 ? `0${hex}` : hex}`;\n}\n\n/**\n * True when a freshly joined device-port base would be misread as a fixed\n * grammar shape or stripped by a normalizer (it ends in a directional or face\n * suffix). Such a base must be disambiguated at build time.\n */\nfunction baseCollidesWithGrammar(base: string): boolean {\n  return (\n    classifyHandleBase(base).kind !== \"port\" ||\n    DIRECTIONAL_HANDLE_SUFFIX.test(base) ||\n    FACE_ALIAS_HANDLE_SUFFIX.test(base)\n  );\n}\n\n/**\n * Escape a label whose plain join would collide with the fixed grammar or a\n * reserved suffix. The first character — always ASCII in the colliding cases\n * (a digit, or the leading letter of `source`/`target`/`cable-button`) — is\n * percent-escaped, which no fixed shape or reserved suffix can begin with; the\n * remainder is escaped normally. The standard decoder reverses it unchanged.\n */\nfunction escapeCollidingLabel(label: string): string {\n  return percentEscapeChar(label[0]) + encodeHandleSegment(label.slice(1));\n}\n\n/**\n * Build the `<group>-<label>` base for a non-power device port with both\n * segments escaped (see the codec note above). Injective and — via\n * `decodePortHandleId` — reversible.\n */\nfunction buildDevicePortBase(group: string, label: string): string {\n  const encodedGroup = encodeHandleSegment(group);\n  const base = `${encodedGroup}-${encodeHandleSegment(label)}`;\n  if (label.length > 0 && baseCollidesWithGrammar(base)) {\n    return `${encodedGroup}-${escapeCollidingLabel(label)}`;\n  }\n  return base;\n}\n\n/**\n * Build a stable handle id for a port.\n *\n * Three shapes, in priority order:\n * 1. An explicit `port.id` is returned verbatim — it is the caller's contract,\n *    including any collision with a fixed grammar shape.\n * 2. A power port returns its `label` verbatim, preserving legacy ids (bare\n *    outlet numbers, `PSU-A`, ...).\n * 3. Otherwise the group and label are percent-escaped and joined\n *    (`<group>-<label>`) so the result is injective and never aliases a fixed\n *    grammar production or a directional/face suffix. Invert with\n *    `decodePortHandleId`.\n *\n * NOTE (pre-1.0 wire-format revision): device-port segments containing `-` or\n * `%` now encode those characters (`%2D` / `%25`), and a clean label that would\n * otherwise collide (e.g. group `fiber` + label `3` becomes `fiber-%33`) gets a\n * first-character escape. Segments with no reserved character are unchanged, so\n * common ids are stable.\n */\nexport function buildPortHandleId(\n  portBlockName: string,\n  port: Pick<Port, \"label\" | \"id\"> & { type?: string }\n): string {\n  if (typeof port.id === \"string\" && port.id.length > 0) {\n    return port.id;\n  }\n  if (isPowerPortType(port.type)) {\n    return port.label;\n  }\n  return buildDevicePortBase(portBlockName, port.label);\n}\n\n/**\n * Invert {@link buildPortHandleId}'s device-port group/label encoding, returning\n * the original `{ group, label }`. Only meaningful for the `<group>-<label>`\n * form: power labels and explicit `port.id`s are emitted verbatim by\n * {@link buildPortHandleId} and must not be decoded (there is no group to\n * recover). The split is unambiguous because the encoding leaves exactly one\n * literal `-` (the join delimiter) in the id.\n *\n * Intentionally NOT part of the public entry surface: it is a partial inverse\n * (meaningless on power labels and explicit ids), so it stays internal to\n * avoid a foot-gun. Exported from the module for the grammar's own tests only.\n * @internal\n */\nexport function decodePortHandleId(handleId: string): { group: string; label: string } {\n  const delimiter = handleId.indexOf(\"-\");\n  if (delimiter === -1) {\n    return { group: decodeHandleSegment(handleId), label: \"\" };\n  }\n  return {\n    group: decodeHandleSegment(handleId.slice(0, delimiter)),\n    label: decodeHandleSegment(handleId.slice(delimiter + 1)),\n  };\n}\n\n/**\n * Heuristic for whether a handle id represents a power endpoint.\n *\n * Prefer {@link applyDirectionalPortHandleId}'s `portType` option when the port\n * is available — this string heuristic is a fallback only.\n *\n * Matches library power-id shapes (`Power Ports-1`, `pwr-1`, `power_ac-1`,\n * `PSU-A`, `outlet-3`) without treating every label that merely contains the\n * word \"power\" (e.g. `Power Room Uplinks-1`) as a power endpoint.\n */\nexport function looksLikePowerHandleId(handleId: string): boolean {\n  const normalized = normalizeDirectionalHandleId(handleId).toLowerCase().trim();\n  const compact = normalized.replace(/\\s+/g, \" \");\n\n  if (/^(pwr|power_ac|power_dc)([-_]|$)/.test(compact)) return true;\n  if (/^power([-_ ]?ports)?[-_ ]?\\d+$/.test(compact)) return true;\n  if (/^pwr[-_]?\\d+$/.test(compact)) return true;\n  if (/^(psu|outlet)([-_]|\\d|$)/.test(compact)) return true;\n  if (/^way-.+$/.test(compact)) return true;\n  // The supply inlet of a PDU strip / distribution board is a power endpoint,\n  // so (like a way) it never takes a directional suffix.\n  if (POWER_INLET_HANDLE_IDS.has(compact)) return true;\n  if (/(^|[-_/ ])(psu|outlet)([-_/ ]|\\d|$)/.test(compact)) return true;\n  return false;\n}\n\n/**\n * Options for {@link applyDirectionalPortHandleId}.\n */\nexport interface ApplyDirectionalPortHandleIdOptions {\n  /**\n   * The port's `type` (e.g. `rj45`, `power_ac`). When provided, power-ness is\n   * decided from it via {@link isPowerPortType} — the same rule the renderer\n   * uses to decide whether a port mounts directional handles at all. Always\n   * pass this when the port is available.\n   */\n  portType?: string;\n}\n\n/**\n * Apply a directional (`source`/`target`) suffix to a rendered handle id — the\n * inverse of {@link normalizeDirectionalHandleId}. Any existing directional\n * suffix is replaced first, and passing no direction is a no-op.\n * Example: `Rear Ports-1` + `target` -\\> `Rear Ports-1-target`.\n *\n * Power endpoints are matched without a direction, so power handles are\n * returned unchanged. Whether a handle is a power handle is decided from\n * `options.portType` when given (exact — the renderer's rule). Without it the\n * decision falls back to {@link looksLikePowerHandleId}, which recognises\n * library power-id shapes (`Power Ports-1`, `pwr-*`, `PSU-*`, `outlet-*`)\n * without treating labels that merely contain the word \"power\" as power.\n * Pass `portType` whenever you have the port.\n */\nexport function applyDirectionalPortHandleId(\n  handleId: string,\n  direction?: \"source\" | \"target\" | null,\n  options?: ApplyDirectionalPortHandleIdOptions\n): string {\n  if (!direction) return handleId;\n  const hasPortType =\n    typeof options?.portType === \"string\" && options.portType.length > 0;\n  const isPower = hasPortType\n    ? isPowerPortType(options.portType)\n    : looksLikePowerHandleId(handleId);\n  if (isPower) return handleId;\n  return `${normalizeDirectionalHandleId(handleId)}-${direction}`;\n}\n\n// ---------------------------------------------------------------------------\n// Typed handle-id grammar\n//\n// Handle ids are the library's de-facto wire protocol: they are what React\n// Flow stores on edges, what documents persist, and what consumers match\n// against. This section is the single authority for that grammar. The full\n// composed form is:\n//\n//   <base>[--rear|--front][-source|-target]\n//\n// where <base> is one of the shapes below. Everything here is *structural* —\n// it reads only the string. Shapes whose tokens can collide with port group\n// names (a group literally named `fiber`, a port label `left-3`) cannot be\n// told apart from the string alone; callers that have the node should combine\n// this with the node type (as `classifyFibreEndpoint` does).\n// ---------------------------------------------------------------------------\n\n/**\n * The structural shape of a handle id's base (suffixes already stripped).\n *\n * - `outlet` — vertical-PDU outlets: a bare number (`\"4\"`).\n * - `fiber` — `fiber-<n>` (tube/cable nodes) or `fiber-<tube>-<n>`\n *   (FibreFlowMap rows).\n * - `tube` — `tube-<n>` (tube and multi-tube-cable nodes).\n * - `splice-side` — bare `left` / `right` (splice nodes).\n * - `side-port` — `left-<n>` / `right-<n>` (coupler ports).\n * - `split-io` — `input-<n>` / `output-<n>` (fibre-split nodes).\n * - `cable-button` — the literal `cable-button` connector on cable nodes.\n * - `breaker-way` — `way-<wayId>` (distribution-board breaker ways).\n * - `port` — everything else: device-family ports built by\n *   {@link buildPortHandleId} (`<group>-<label>`, a bare power label, or an\n *   explicit `port.id`). A dashed base is decoded into `group`/`label` (the\n *   segments are percent-escaped so the join delimiter is unambiguous); a\n *   dash-free base, which can never be a group-label join, is flagged\n *   `verbatim` (an opaque `port.id` or bare power label).\n * @public\n */\nexport type HandleShape =\n  | { kind: \"outlet\"; outlet: number }\n  | { kind: \"fiber\"; fiber: number; tube?: number }\n  | { kind: \"tube\"; tube: number }\n  | { kind: \"splice-side\"; side: \"left\" | \"right\" }\n  | { kind: \"side-port\"; side: \"left\" | \"right\"; index: number }\n  | { kind: \"split-io\"; io: \"input\" | \"output\"; index: number }\n  | { kind: \"cable-button\" }\n  | { kind: \"breaker-way\"; wayId: string }\n  | {\n      kind: \"port\";\n      /**\n       * Decoded port group, when the base is a `<group>-<label>` join (has a\n       * dash). Best-effort structural decode: for a verbatim `port.id` or power\n       * label that happens to contain a dash (`PSU-A`) this is the split before\n       * the first dash, not necessarily a real group.\n       */\n      group?: string;\n      /** Decoded port label — same best-effort decode as `group` above. */\n      label?: string;\n      /**\n       * `true` when the base has no dash, so it cannot be a group-label join —\n       * it is an opaque `port.id` or a bare power label with no `group`/`label`\n       * reading.\n       */\n      verbatim?: boolean;\n    };\n\n/**\n * A structurally parsed handle id.\n * @public\n */\nexport interface ParsedHandleId {\n  /** The id exactly as given. */\n  raw: string;\n  /** `raw` with the directional suffix and face alias stripped. */\n  base: string;\n  /** Directional suffix, when present (`X-source` / `X-target`). */\n  direction?: \"source\" | \"target\";\n  /**\n   * Split-face alias, when present (`X--rear` / `X--front`) — added by the\n   * renderer when both faces of a device are shown and a handle id would\n   * otherwise collide with its opposite face.\n   */\n  faceAlias?: \"front\" | \"rear\";\n  /** Structural classification of {@link ParsedHandleId.base}. */\n  shape: HandleShape;\n  /**\n   * Other shapes the base could structurally be, when it is genuinely\n   * ambiguous. Present (and non-empty) only when there is a second reading;\n   * absent otherwise. The primary {@link ParsedHandleId.shape} is never changed\n   * by this — `alternates` is a pure signal.\n   *\n   * The classic case is a verbatim `port.id` that happens to match a fixed\n   * shape: `tube-5` classifies as `{ kind: \"tube\", tube: 5 }` but could be an\n   * explicit port id, so `alternates` is `[{ kind: \"port\", verbatim: true }]`.\n   * A dashed group-label port carries the same verbatim alternate (it could be\n   * one opaque id). Only node-context-aware classification (which handle-ids\n   * cannot do from the string alone) can pick the right reading.\n   */\n  alternates?: HandleShape[];\n}\n\nconst OUTLET_BASE = /^\\d+$/;\nconst FIBER_TUBE_BASE = /^fiber-(\\d+)-(\\d+)$/;\nconst FIBER_BASE = /^fiber-(\\d+)$/;\nconst TUBE_BASE = /^tube-(\\d+)$/;\nconst SIDE_PORT_BASE = /^(left|right)-(\\d+)$/;\nconst SPLIT_IO_BASE = /^(input|output)-(\\d+)$/;\nconst BREAKER_WAY_BASE = /^way-(.+)$/;\n\nfunction classifyHandleBase(base: string): HandleShape {\n  if (OUTLET_BASE.test(base)) {\n    return { kind: \"outlet\", outlet: Number(base) };\n  }\n  const fiberTube = FIBER_TUBE_BASE.exec(base);\n  if (fiberTube) {\n    return { kind: \"fiber\", tube: Number(fiberTube[1]), fiber: Number(fiberTube[2]) };\n  }\n  const fiber = FIBER_BASE.exec(base);\n  if (fiber) {\n    return { kind: \"fiber\", fiber: Number(fiber[1]) };\n  }\n  const tube = TUBE_BASE.exec(base);\n  if (tube) {\n    return { kind: \"tube\", tube: Number(tube[1]) };\n  }\n  if (base === \"left\" || base === \"right\") {\n    return { kind: \"splice-side\", side: base };\n  }\n  const sidePort = SIDE_PORT_BASE.exec(base);\n  if (sidePort) {\n    return {\n      kind: \"side-port\",\n      side: sidePort[1] as \"left\" | \"right\",\n      index: Number(sidePort[2]),\n    };\n  }\n  const splitIo = SPLIT_IO_BASE.exec(base);\n  if (splitIo) {\n    return {\n      kind: \"split-io\",\n      io: splitIo[1] as \"input\" | \"output\",\n      index: Number(splitIo[2]),\n    };\n  }\n  if (base === \"cable-button\") {\n    return { kind: \"cable-button\" };\n  }\n  const breakerWay = BREAKER_WAY_BASE.exec(base);\n  if (breakerWay) {\n    return { kind: \"breaker-way\", wayId: decodeHandleSegment(breakerWay[1]) };\n  }\n  // A dashed base is always a possible `<group>-<label>` join — decode it (the\n  // percent-escapes are reversed). A dash-free base cannot be a join, so it is\n  // an opaque verbatim id / bare power label.\n  if (base.includes(\"-\")) {\n    const { group, label } = decodePortHandleId(base);\n    return { kind: \"port\", group, label };\n  }\n  return { kind: \"port\", verbatim: true };\n}\n\n/**\n * The alternate structural readings of a classified base, when it is genuinely\n * ambiguous. Every fixed shape, and every dashed group-label port, could also\n * be an explicit verbatim `port.id`; a bare verbatim port has no second reading.\n */\nfunction alternateShapes(shape: HandleShape): HandleShape[] {\n  if (shape.kind === \"port\") {\n    return shape.verbatim ? [] : [{ kind: \"port\", verbatim: true }];\n  }\n  return [{ kind: \"port\", verbatim: true }];\n}\n\n/**\n * Parse a rendered/stored handle id into its structural parts: base, optional\n * directional suffix, optional split-face alias, and the {@link HandleShape}\n * of the base. Total — never throws; unrecognized bases classify as `port`.\n * @public\n */\nexport function parseHandleId(handleId: string | null | undefined): ParsedHandleId {\n  const raw = handleId ?? \"\";\n  const directionMatch = DIRECTIONAL_HANDLE_SUFFIX.exec(raw);\n  const afterDirection = directionMatch\n    ? raw.slice(0, raw.length - directionMatch[0].length)\n    : raw;\n  const faceMatch = FACE_ALIAS_HANDLE_SUFFIX.exec(afterDirection);\n  const base = faceMatch\n    ? afterDirection.slice(0, afterDirection.length - faceMatch[0].length)\n    : afterDirection;\n  const shape = classifyHandleBase(base);\n  const parsed: ParsedHandleId = { raw, base, shape };\n  if (directionMatch) {\n    parsed.direction = directionMatch[1].toLowerCase() as \"source\" | \"target\";\n  }\n  if (faceMatch) {\n    parsed.faceAlias = faceMatch[1].toLowerCase() as \"front\" | \"rear\";\n  }\n  const alternates = alternateShapes(shape);\n  if (alternates.length > 0) {\n    parsed.alternates = alternates;\n  }\n  return parsed;\n}\n\n/**\n * Build a fiber handle id: `fiber-<n>`, or `fiber-<tube>-<n>` when a tube\n * index is given (the FibreFlowMap row form).\n * @public\n */\nexport function buildFibreHandleId(fiber: number, tube?: number): string {\n  return tube === undefined ? `fiber-${fiber}` : `fiber-${tube}-${fiber}`;\n}\n\n/**\n * Build a tube handle id: `tube-<n>`.\n * @public\n */\nexport function buildTubeHandleId(tube: number): string {\n  return `tube-${tube}`;\n}\n\n/**\n * Build a splice handle id: the bare `left` / `right` sides of a splice node.\n * @public\n */\nexport function buildSpliceHandleId(side: \"left\" | \"right\"): string {\n  return side;\n}\n\n/**\n * Build a coupler port handle id: `left-<n>` / `right-<n>` (1-based).\n * @public\n */\nexport function buildCouplerHandleId(side: \"left\" | \"right\", index: number): string {\n  return `${side}-${index}`;\n}\n\n/**\n * Build a fibre-split io handle id: `input-<n>` / `output-<n>`.\n * @public\n */\nexport function buildSplitIoHandleId(io: \"input\" | \"output\", index: number): string {\n  return `${io}-${index}`;\n}\n\n/**\n * Build a distribution-board breaker way handle id: `way-<wayId>`.\n *\n * The way id is percent-escaped exactly as {@link buildPortHandleId} escapes\n * its segments, so a way id containing `-` or `%` (or the `--rear` face alias\n * suffix) cannot be misread by {@link parseHandleId}; the parse side decodes\n * it symmetrically. Ids with no reserved character are unchanged.\n *\n * Ways are power endpoints, so the id carries no directional suffix (see\n * {@link applyDirectionalPortHandleId}) and {@link looksLikePowerHandleId}\n * recognises the prefix.\n * @public\n */\nexport function buildBreakerWayHandleId(wayId: string): string {\n  return `way-${encodeHandleSegment(wayId)}`;\n}\n\n/**\n * Append a split-face alias to a handle id: `X` + `rear` -\\> `X--rear`.\n * Any existing alias is replaced. The renderer applies this when a hidden or\n * ghost face would mount a handle whose id collides with the visible face.\n * @public\n */\nexport function applyFaceAliasHandleId(\n  handleId: string,\n  face: \"front\" | \"rear\"\n): string {\n  return `${handleId.replace(FACE_ALIAS_HANDLE_SUFFIX, \"\")}--${face}`;\n}\n\n/**\n * Normalize a handle id for tolerant matching by stripping `+` characters.\n * Port labels like `SFP+-1` historically produced handles that some producers\n * emitted `+`-stripped (`SFP-1`), so comparisons between independently built\n * handles must be `+`-insensitive. Apply to BOTH sides of a comparison — this\n * is a matching normalization, not part of the build grammar:\n * {@link buildPortHandleId} keeps `+` so built ids always equal rendered ids.\n *\n * This mapping is intentionally MANY-TO-ONE and lossy: it cannot be inverted\n * and must never be persisted as a canonical id. Distinct handles collapse\n * together — e.g. `SFP+-1` and `SFP-1` both sanitize to `SFP-1`, so a port\n * genuinely labelled `SFP-1` is indistinguishable from one labelled `SFP+-1`\n * under this comparison. Use it only as a last-resort tolerant match, applied\n * to both sides.\n *\n * Interaction with the injective build grammar (pre-1.0 wire-format revision):\n * {@link buildPortHandleId} percent-escapes `-` (`%2D`) and `%` (`%25`) inside\n * group/label segments. This sanitizer strips ONLY `+` and deliberately does\n * NOT decode those escapes, so tolerant matching requires both sides to share\n * the same escaping discipline. A lookup built through {@link buildPortHandleId}\n * (or a bare, unescaped label) still matches `+`-insensitively — a `+`-dropped\n * but still-escaped composite `Group-SFP%2D1` matches the registered\n * `Group-SFP+%2D1`. But a fully hand-built composite that both drops `+` AND\n * writes a raw `-` where the builder now writes `%2D` — `Group-SFP-1` against\n * `Group-SFP+%2D1` — no longer matches. That narrowed case is unreachable\n * through the builder (the single source of composite ids) and is pinned as a\n * boundary in the tests. Decoding is intentionally kept OUT of this function:\n * mounts.\n *\n * @internal Internal building block of {@link handlesMatch}; not part of the\n * public API. Consumers must compare handles with {@link handlesMatch} (or find\n * edges with {@link findEdgesForPortHandle}), which apply this rule to both\n * sides along with directional/face normalization.\n */\nexport function sanitizeHandleIdForMatching(handleId: string): string {\n  return handleId.replace(/\\+/g, \"\");\n}\n\n/**\n * True iff two handle ids refer to the same handle under the library's own\n * equivalence relation. This is the one function to use for \"is this edge on\n * this port?\" — it replaces bespoke candidate-set matchers.\n *\n * @remarks\n * The relation, applied to BOTH sides: strip the directional suffix and the\n * split-face alias ({@link normalizeHandleId} semantics), then apply the\n * narrowed `+`-insensitive rule (drop `+`, escaping-aware), then\n * compare. So `Rear Ports-1-target`, `Rear Ports-1--rear` and `Rear Ports-1`\n * all match, and `SFP+-1` matches `SFP-1`.\n *\n * It is intentionally string-structural and does NOT percent-decode: it matches\n * builder-produced ids (the single source of composite ids), where both sides\n * share the same escaping discipline. A hand-built composite that drops `+` AND\n * writes a raw `-` where the builder writes `%2D` (`Group-SFP-1` vs\n * `Group-SFP+%2D1`) is a deliberate non-match — that form is unreachable through\n * {@link buildPortHandleId}. Prefer this over the raw sanitizer, which leaks\n * matching concerns onto callers.\n *\n * @example\n * ```ts\n * handlesMatch(\"Rear Ports-1-target\", \"Rear Ports-1\"); // true\n * handlesMatch(\"Ports-1--rear\", \"Ports-1\");            // true\n * handlesMatch(\"SFP+-1\", \"SFP-1\");                      // true\n * handlesMatch(\"Ports-1\", \"Ports-2\");                   // false\n * ```\n * @public\n */\nexport function handlesMatch(a: string, b: string): boolean {\n  return (\n    sanitizeHandleIdForMatching(normalizeHandleId(a)) ===\n    sanitizeHandleIdForMatching(normalizeHandleId(b))\n  );\n}\n\n/**\n * Return the ids of every edge attached to the given `handleId` on `nodeId`,\n * matching handles with {@link handlesMatch}. An edge qualifies when\n * `source === nodeId` and its `sourceHandle` matches, or `target === nodeId` and\n * its `targetHandle` matches. Edges with an undefined `sourceHandle`/\n * `targetHandle` on the relevant side are skipped.\n *\n * @remarks\n * This is the multi-edge companion to {@link handlesMatch} and the replacement\n * for consumers' hand-rolled per-port edge scans. A self-loop edge (both ends on\n * `nodeId`, both handles matching) is returned once. The scan is a single linear\n * pass over `edges`; it does not build an index, so for repeated lookups over a\n * large edge set a caller may still want its own Map.\n *\n * @example\n * ```ts\n * const handle = buildPortHandleId(\"Rear Ports\", { label: \"1\" });\n * findEdgesForPortHandle(edges, \"device-1\", handle); // [\"e1\", \"e2\"]\n * ```\n * @public\n */\nexport function findEdgesForPortHandle(\n  edges: NetworkEdge[],\n  nodeId: string,\n  handleId: string\n): string[] {\n  const matched: string[] = [];\n  for (const edge of edges) {\n    if (\n      edge.source === nodeId &&\n      edge.sourceHandle !== undefined &&\n      handlesMatch(edge.sourceHandle, handleId)\n    ) {\n      matched.push(edge.id);\n      continue;\n    }\n    if (\n      edge.target === nodeId &&\n      edge.targetHandle !== undefined &&\n      handlesMatch(edge.targetHandle, handleId)\n    ) {\n      matched.push(edge.id);\n    }\n  }\n  return matched;\n}\n\n/**\n * For pass-through nodes with two sides, return the handle id of the opposite\n * side: splice `left` \\<-\\> `right`, coupler `left-<n>` \\<-\\> `right-<n>`.\n * Returns `undefined` for every other shape. Directional suffixes and face\n * aliases are stripped (neither occurs on splice/coupler handles in practice).\n * @public\n */\nexport function getOppositeSideHandleId(handleId: string): string | undefined {\n  const { shape } = parseHandleId(handleId);\n  if (shape.kind === \"splice-side\") {\n    return shape.side === \"left\" ? \"right\" : \"left\";\n  }\n  if (shape.kind === \"side-port\") {\n    return buildCouplerHandleId(shape.side === \"left\" ? \"right\" : \"left\", shape.index);\n  }\n  return undefined;\n}\n\n/**\n * Options for {@link encodeHandle}: the optional suffixes composed onto the\n * base, in grammar order `<base>[--rear|--front][-source|-target]`.\n * @public\n */\nexport interface EncodeHandleOptions {\n  /** Split-face alias to append (`--rear` / `--front`). */\n  face?: \"front\" | \"rear\";\n  /**\n   * Directional suffix to append (`-source` / `-target`). Applied structurally\n   * regardless of power-ness — use {@link applyDirectionalPortHandleId} instead\n   * when you need power ports left un-suffixed.\n   */\n  direction?: \"source\" | \"target\";\n}\n\n/**\n * The single structured encoder: turn a {@link HandleShape} into its handle-id\n * string, dispatching to the individual typed builders. The inverse of\n * {@link decodeHandle}/{@link parseHandleId} for the base classification.\n *\n * @remarks\n * Use this when you hold structured handle data (e.g. from a `DiagramEndpoint`)\n * and need the React Flow boundary string, instead of remembering which builder\n * a kind maps to. A `port` shape REQUIRES string `group` and `label` — it is\n * encoded through {@link buildPortHandleId} (so the same escaping/injectivity\n * applies). A `verbatim` port has no id to emit and cannot be re-encoded: pass\n * an explicit id to {@link buildPortHandleId} yourself for that case.\n *\n * Round-trip: `decodeHandle(encodeHandle(shape)).shape` deep-equals `shape` for\n * every non-verbatim kind.\n *\n * @throws TypeError when a `port` shape is missing `group` or `label`.\n * @example\n * ```ts\n * encodeHandle({ kind: \"fiber\", tube: 2, fiber: 7 });          // \"fiber-2-7\"\n * encodeHandle({ kind: \"port\", group: \"Rear Ports\", label: \"1\" }); // \"Rear Ports-1\"\n * encodeHandle({ kind: \"port\", group: \"Ports\", label: \"1\" }, { face: \"rear\", direction: \"source\" });\n * // \"Ports-1--rear-source\"\n * ```\n * @public\n */\nexport function encodeHandle(\n  shape: HandleShape,\n  opts?: EncodeHandleOptions\n): string {\n  let base: string;\n  switch (shape.kind) {\n    case \"outlet\":\n      base = String(shape.outlet);\n      break;\n    case \"fiber\":\n      base = buildFibreHandleId(shape.fiber, shape.tube);\n      break;\n    case \"tube\":\n      base = buildTubeHandleId(shape.tube);\n      break;\n    case \"splice-side\":\n      base = buildSpliceHandleId(shape.side);\n      break;\n    case \"side-port\":\n      base = buildCouplerHandleId(shape.side, shape.index);\n      break;\n    case \"split-io\":\n      base = buildSplitIoHandleId(shape.io, shape.index);\n      break;\n    case \"cable-button\":\n      base = \"cable-button\";\n      break;\n    case \"breaker-way\":\n      base = buildBreakerWayHandleId(shape.wayId);\n      break;\n    case \"port\": {\n      if (typeof shape.group !== \"string\" || typeof shape.label !== \"string\") {\n        throw new TypeError(\n          \"encodeHandle: a 'port' shape requires string 'group' and 'label'. \" +\n            \"Verbatim/opaque port ids cannot be re-encoded (there is no id in the \" +\n            \"shape) — pass the explicit id to buildPortHandleId instead.\"\n        );\n      }\n      base = buildPortHandleId(shape.group, { label: shape.label });\n      break;\n    }\n  }\n  let out = opts?.face ? applyFaceAliasHandleId(base, opts.face) : base;\n  if (opts?.direction) {\n    out = `${out}-${opts.direction}`;\n  }\n  return out;\n}\n\n/**\n * Decode a handle-id string into its structural parts. A documented alias of\n * {@link parseHandleId} — identical behavior, named for symmetry with\n * {@link encodeHandle}.\n *\n * @remarks\n * Total (never throws). Use the {@link encodeHandle}/`decodeHandle` pair when\n * you think in structured {@link HandleShape}s; use `parseHandleId` directly\n * when the \"parse a rendered id\" framing reads better. See\n * {@link ParsedHandleId.alternates} for the ambiguity signal on verbatim ids.\n *\n * @example\n * ```ts\n * decodeHandle(\"fiber-2-7\").shape; // { kind: \"fiber\", tube: 2, fiber: 7 }\n * decodeHandle(encodeHandle(x)).shape; // deep-equals x for non-verbatim x\n * ```\n * @public\n */\nexport function decodeHandle(\n  handleId: string | null | undefined\n): ParsedHandleId {\n  return parseHandleId(handleId);\n}\n","import type { DiagramNodeKind, NetworkNode, NetworkNodeType } from \"../types\";\n\n// ---------------------------------------------------------------------------\n// Node-id authority\n//\n// The inventory/netbox converters mint top-level node ids by prefixing the\n// domain id: `rack-<id>` (see inventory.ts inventoryRackToNetworkNode) and\n// `device-<id>` (inventoryDeviceToNetworkNode). Consumers were reverse-\n// engineering those prefixes in dozens of places. This module is the single\n// authority for building/parsing them, mirroring what handle-ids does for\n// handle strings.\n//\n// IMPORTANT: `node.type` is the authoritative discriminator for what a node IS\n// (see {@link getNodeKind}/{@link isRackNode}/{@link isDeviceNode}). The id\n// helpers here only cover the converter-minted prefix grammar — they must NOT\n// be used to decide a node's kind, and they do not understand compound consumer\n// grammars layered on top of the prefix (e.g. `device-<rackId>-<unitId>`): they\n// return everything after the prefix verbatim as the id.\n// ---------------------------------------------------------------------------\n\nconst RACK_NODE_ID_PREFIX = \"rack-\";\nconst DEVICE_NODE_ID_PREFIX = \"device-\";\n\n/**\n * Build the top-level node id for a rack: `rack-<rackId>` — the exact form the\n * inventory/netbox converters mint.\n *\n * @remarks\n * Use this instead of hand-writing the `rack-` prefix so consumer code stays in\n * lockstep with the converters. Pairs with {@link parseNodeId} and\n * {@link buildDeviceNodeId}. This is an id-string helper only; to decide whether\n * a node is a rack, use {@link isRackNode} (which reads `node.type`).\n *\n * @example\n * ```ts\n * buildRackNodeId(\"42\"); // \"rack-42\"\n * buildRackNodeId(42);   // \"rack-42\"\n * ```\n * @public\n */\nexport function buildRackNodeId(rackId: string | number): string {\n  return `${RACK_NODE_ID_PREFIX}${rackId}`;\n}\n\n/**\n * Build the top-level node id for a device: `device-<deviceId>` — the exact\n * form the inventory/netbox converters mint.\n *\n * @remarks\n * Consumers that layer a compound scheme on top (e.g.\n * `device-<rackId>-<unitId>`) can pass the already-composed id part as\n * `deviceId`; {@link parseNodeId} returns it back verbatim. This is an\n * id-string helper only; to decide whether a node is a device/equipment node,\n * use {@link isDeviceNode} (which reads `node.type`).\n *\n * @example\n * ```ts\n * buildDeviceNodeId(\"7\");             // \"device-7\"\n * buildDeviceNodeId(`${rackId}-${u}`); // \"device-<rackId>-<u>\"\n * ```\n * @public\n */\nexport function buildDeviceNodeId(deviceId: string | number): string {\n  return `${DEVICE_NODE_ID_PREFIX}${deviceId}`;\n}\n\n/**\n * A parsed converter-minted node id.\n * @public\n */\nexport interface ParsedNodeId {\n  /** Which converter prefix produced the id. */\n  kind: \"rack\" | \"device\";\n  /**\n   * Everything after the prefix, verbatim. For a compound consumer scheme\n   * (`device-<rackId>-<unitId>`) this is the whole `<rackId>-<unitId>` tail —\n   * decomposing it further is the consumer's concern.\n   */\n  id: string;\n}\n\n/**\n * Parse a converter-minted node id back into `{ kind, id }`, or `null` when the\n * id carries neither the `rack-` nor the `device-` prefix.\n *\n * @remarks\n * Total and prefix-only: it never throws and only recognizes the two\n * converter-minted prefixes. It deliberately does NOT classify fibre-scene node\n * ids (`fiber-3`, `tube-2`, `closure-1`, …) or bare ids — those return `null`.\n * `kind` is derived from the id STRING here purely to invert the builders; do\n * not use it as a node's semantic kind (that is `node.type`'s job — see\n * {@link getNodeKind}).\n *\n * @example\n * ```ts\n * parseNodeId(\"rack-42\");          // { kind: \"rack\", id: \"42\" }\n * parseNodeId(\"device-r1-u2\");     // { kind: \"device\", id: \"r1-u2\" }\n * parseNodeId(\"fiber-3\");          // null\n * ```\n * @public\n */\nexport function parseNodeId(\n  nodeId: string | null | undefined\n): ParsedNodeId | null {\n  if (!nodeId) return null;\n  if (nodeId.startsWith(RACK_NODE_ID_PREFIX)) {\n    return { kind: \"rack\", id: nodeId.slice(RACK_NODE_ID_PREFIX.length) };\n  }\n  if (nodeId.startsWith(DEVICE_NODE_ID_PREFIX)) {\n    return { kind: \"device\", id: nodeId.slice(DEVICE_NODE_ID_PREFIX.length) };\n  }\n  return null;\n}\n\n/** The `node.type`s the document model unifies as the `equipment` kind. */\nconst EQUIPMENT_NODE_TYPES: ReadonlySet<string> = new Set<NetworkNodeType>([\n  \"server\",\n  \"switch\",\n  \"router\",\n  \"device\",\n]);\n\n/** The `node.type`s the document model unifies as the `container` kind. */\nconst CONTAINER_NODE_TYPES: ReadonlySet<string> = new Set<NetworkNodeType>([\n  \"patch-panel\",\n  \"closure\",\n  \"splice-tray\",\n]);\n\n/**\n * Minimal shape read by the type-driven predicates — only `type` matters.\n *\n * @remarks\n * This is the parameter type of the public {@link getNodeKind},\n * {@link isRackNode} and {@link isDeviceNode}, so it is exported for consumers\n * that need to name it (wrapper helpers, generic utilities). Any object with a\n * `type` satisfies it — a full {@link NetworkNode}, a document node, or a bare\n * `{ type: \"rack\" }` literal.\n *\n * @public\n */\nexport type NodeTypeCarrier =\n  | Pick<NetworkNode, \"type\">\n  | { type?: string | null };\n\n/**\n * Map a node's `type` to its {@link DiagramNodeKind}, mirroring the document\n * adapter's authoritative discrimination (`toDiagramNodeKind`). Returns `null`\n * when `type` is missing/empty.\n *\n * @remarks\n * `node.type` — never the id string — is authoritative for what a node is. Use\n * this (and {@link isRackNode}/{@link isDeviceNode}) rather than sniffing id\n * prefixes. The mapping collapses `server`/`switch`/`router`/`device` into\n * `equipment`, `vertical-pdu` into `pdu`, `patch-panel`/`closure`/`splice-tray`\n * into `container`, and `fibre-split` into `splitter`; the remaining fibre/rack\n * types map 1:1. Subtype detail (the adapter's `subtype`) is intentionally not\n * returned — this is a kind-only helper.\n *\n * @example\n * ```ts\n * getNodeKind({ type: \"switch\" }); // \"equipment\"\n * getNodeKind({ type: \"rack\" });   // \"rack\"\n * getNodeKind({ type: undefined }); // null\n * ```\n * @public\n */\nexport function getNodeKind(node: NodeTypeCarrier): DiagramNodeKind | null {\n  const type = node?.type;\n  if (typeof type !== \"string\" || type.length === 0) return null;\n  if (type === \"vertical-pdu\") return \"pdu\";\n  if (EQUIPMENT_NODE_TYPES.has(type)) return \"equipment\";\n  if (CONTAINER_NODE_TYPES.has(type)) return \"container\";\n  if (type === \"fibre-split\") return \"splitter\";\n  // rack, fiber, tube, cable, multi-tube-cable, splice, coupler,\n  // fibre-flow-cable and fibre-flow-closure map straight through.\n  return type as DiagramNodeKind;\n}\n\n/**\n * True when a node is a rack (`node.type === \"rack\"`). Type-driven — never sniffs\n * the id.\n *\n * @remarks\n * Prefer this over `node.id.startsWith(\"rack-\")`: the id prefix is a converter\n * detail, whereas `node.type` is the authoritative discriminator and survives\n * the document round-trip.\n *\n * @example\n * ```ts\n * isRackNode({ type: \"rack\" });   // true\n * isRackNode({ type: \"device\" }); // false\n * ```\n * @public\n */\nexport function isRackNode(node: NodeTypeCarrier): boolean {\n  return node?.type === \"rack\";\n}\n\n/**\n * True when a node is a device/equipment node — i.e. its {@link getNodeKind} is\n * `equipment` (`server`, `switch`, `router` or `device`). Type-driven — never\n * sniffs the id.\n *\n * @remarks\n * This is broader than `node.type === \"device\"`: the document model unifies the\n * whole equipment family under one kind, and a rack-mounted switch or server is\n * as much a \"device node\" as one the inventory converter happened to mint with\n * `type: \"device\"`. If you need only the literal `device` type, compare\n * `node.type` directly. Note the asymmetry with {@link buildDeviceNodeId}: the\n * `device-` id prefix only covers converter-minted device ids, whereas this\n * predicate covers the semantic equipment family.\n *\n * @example\n * ```ts\n * isDeviceNode({ type: \"switch\" }); // true\n * isDeviceNode({ type: \"rack\" });   // false\n * ```\n * @public\n */\nexport function isDeviceNode(node: NodeTypeCarrier): boolean {\n  return getNodeKind(node) === \"equipment\";\n}\n","import { NetworkNode } from '../types';\n\n/**\n * Height of each holder position in pixels (one splice per holder)\n * Different heights for ribbon mode (taller) vs single mode (compact)\n */\nexport const HOLDER_HEIGHT_PX_SINGLE = 20; // For single mode splices (16px height)\nexport const HOLDER_HEIGHT_PX_RIBBON = 36; // For ribbon mode splices (32px height)\n// Default to single mode for backward compatibility\nexport const HOLDER_HEIGHT_PX = HOLDER_HEIGHT_PX_SINGLE;\n\n/**\n * Tray header height in pixels (smaller than rack header for compact trays)\n */\nexport const TRAY_HEADER_HEIGHT = 32;\n\n/**\n * Bottom padding for tray in pixels\n */\nexport const TRAY_BOTTOM_PADDING = 4;\n\n/**\n * Number of holders per tray (12f per tray)\n */\nexport const HOLDERS_PER_TRAY = 12;\n\n/**\n * Rendered splice width in pixels — the exact width the SpliceNode renderer\n * uses.\n *\n * @remarks\n * Exposed so consumers composing a fibre scene by hand (positioning splices to\n * line up with rendered tray/holder geometry) use the real value instead of a\n * local copy that would silently drift when the renderer changes. Pairs with\n * {@link SPLICE_HEIGHT_PX} and {@link calculateSplicePositionFromNumber}.\n * @public\n */\nexport const SPLICE_WIDTH_PX = 69;\n\n/**\n * Rendered splice height in pixels for single-mode (compact) splices.\n * @public\n */\nexport const SPLICE_HEIGHT_PX_SINGLE = 16;\n\n/**\n * Rendered splice height in pixels for ribbon-mode (taller) splices.\n * @public\n */\nexport const SPLICE_HEIGHT_PX_RIBBON = 32;\n\n/**\n * Rendered splice height in pixels. Defaults to single-mode; for ribbon splices\n * use {@link SPLICE_HEIGHT_PX_RIBBON}.\n *\n * @remarks\n * The height counterpart to {@link SPLICE_WIDTH_PX}: the exact value the\n * SpliceNode renderer uses, exposed so hand-composed fibre scenes don't fork a\n * magic number that drifts.\n * @public\n */\nexport const SPLICE_HEIGHT_PX = SPLICE_HEIGHT_PX_SINGLE;\n\n/**\n * Tray width in pixels (narrower to fit splice properly)\n */\nexport const TRAY_WIDTH_PX = 80;\n\n/**\n * Rendered splice-tray height for single-mode splices (default).\n */\nexport const TRAY_NODE_HEIGHT_SINGLE =\n  TRAY_HEADER_HEIGHT + HOLDERS_PER_TRAY * HOLDER_HEIGHT_PX_SINGLE + TRAY_BOTTOM_PADDING;\n\n/**\n * Rendered splice-tray height when the tray holds ribbon splices.\n */\nexport const TRAY_NODE_HEIGHT_RIBBON =\n  TRAY_HEADER_HEIGHT + HOLDERS_PER_TRAY * HOLDER_HEIGHT_PX_RIBBON + TRAY_BOTTOM_PADDING;\n\n/** Default tray height — compact single-mode layout. */\nexport const TRAY_NODE_HEIGHT = TRAY_NODE_HEIGHT_SINGLE;\n\nexport function getTrayNodeHeight(isRibbonMode = false): number {\n  return isRibbonMode ? TRAY_NODE_HEIGHT_RIBBON : TRAY_NODE_HEIGHT_SINGLE;\n}\n\n/**\n * Fixed X position to center splices within tray content\n */\nexport const SPLICE_CENTER_X = (TRAY_WIDTH_PX - SPLICE_WIDTH_PX) / 2;\n\n/**\n * Calculate holder position from Y coordinate within a tray\n * Uses average height to work with both single and ribbon splices\n */\nexport const calculateHolderPositionFromY = (\n  tray: NetworkNode,\n  y: number\n): number => {\n  const startY = TRAY_HEADER_HEIGHT;\n  const relativeY = y - startY;\n  // Use average height to work reasonably with both modes\n  const avgHeight = (HOLDER_HEIGHT_PX_SINGLE + HOLDER_HEIGHT_PX_RIBBON) / 2;\n  const holderPosition = Math.round(relativeY / avgHeight) + 1;\n  const maxHolders = HOLDERS_PER_TRAY;\n  return Math.max(1, Math.min(maxHolders, holderPosition));\n};\n\n/**\n * Snap a splice to the nearest holder position within a tray\n */\nexport const snapToSplicePosition = (\n  splice: NetworkNode,\n  tray: NetworkNode,\n  newPosition: { x: number; y: number },\n  allNodes: NetworkNode[]\n): { x: number; y: number; holderPosition?: number } => {\n  if (!splice.parentId || splice.parentId !== tray.id) {\n    return newPosition;\n  }\n\n  // Determine if this is a ribbon splice\n  const isRibbonMode = splice.data?.mode === 'ribbon';\n  const holderHeight = isRibbonMode ? HOLDER_HEIGHT_PX_RIBBON : HOLDER_HEIGHT_PX_SINGLE;\n  const spliceHeight = isRibbonMode ? SPLICE_HEIGHT_PX_RIBBON : SPLICE_HEIGHT_PX_SINGLE;\n\n  const startY = TRAY_HEADER_HEIGHT;\n  const relativeY = newPosition.y - startY;\n  const calculatedPosition = Math.round(relativeY / holderHeight) + 1;\n  const maxPosition = HOLDERS_PER_TRAY;\n  const snappedPosition = Math.max(1, Math.min(maxPosition, calculatedPosition));\n\n  // Check if position is available\n  if (!isHolderPositionAvailable(tray, splice, snappedPosition, allNodes)) {\n    const availablePosition = findNextAvailableHolderPosition(tray, splice, allNodes, snappedPosition);\n    if (availablePosition !== snappedPosition) {\n      // Center splice vertically in holder slot\n      const baseOffset = (holderHeight - spliceHeight) / 2;\n      return {\n        x: SPLICE_CENTER_X, // Center splice in tray\n        y: startY + (availablePosition - 1) * holderHeight + baseOffset,\n        holderPosition: availablePosition,\n      };\n    }\n  }\n\n  // Snap to exact holder position, center vertically in holder slot\n  const baseOffset = (holderHeight - spliceHeight) / 2;\n  const snappedY = startY + (snappedPosition - 1) * holderHeight + baseOffset;\n\n  return {\n    x: SPLICE_CENTER_X, // Center splice in tray\n    y: snappedY,\n    holderPosition: snappedPosition,\n  };\n};\n\n/**\n * Check if a holder position is available in a tray\n */\nexport const isHolderPositionAvailable = (\n  tray: NetworkNode,\n  splice: NetworkNode,\n  position: number,\n  allNodes: NetworkNode[]\n): boolean => {\n  const trayChildren = allNodes.filter(\n    (n) => n.parentId === tray.id && n.type === 'splice' && n.id !== splice.id\n  );\n\n  return !trayChildren.some((child) => {\n    const childPosition = (child.data as NetworkNode['data']).holderPosition;\n    return childPosition === position;\n  });\n};\n\n/**\n * Find the next available holder position\n */\nexport const findNextAvailableHolderPosition = (\n  tray: NetworkNode,\n  splice: NetworkNode,\n  allNodes: NetworkNode[],\n  startFrom: number = 1\n): number => {\n  for (let pos = startFrom; pos <= HOLDERS_PER_TRAY; pos++) {\n    if (isHolderPositionAvailable(tray, splice, pos, allNodes)) {\n      return pos;\n    }\n  }\n\n  // Try positions before startFrom\n  for (let pos = startFrom - 1; pos >= 1; pos--) {\n    if (isHolderPositionAvailable(tray, splice, pos, allNodes)) {\n      return pos;\n    }\n  }\n\n  return 1; // Fallback to position 1\n};\n\n/**\n * Compute the `{ x, y }` offset (relative to the tray node) at which a splice in\n * a given 1-based holder position renders, in single (compact) mode.\n *\n * @remarks\n * Pure geometry — no DOM, React or reactflow. Exposed so consumers hand-building\n * a fibre closure scene (a tray with splices/tubes wired up and rendered through\n * `NetworkDiagram`) position splices with the same formula the renderer uses,\n * instead of forking it. This single-mode variant has no access to splice data;\n * for ribbon splices use {@link calculateSplicePositionFromNumberWithData} with\n * `isRibbonMode: true`. `x` is fixed at {@link SPLICE_CENTER_X}.\n *\n * @example\n * ```ts\n * calculateSplicePositionFromNumber(tray, 1); // { x: SPLICE_CENTER_X, y: 34 }\n * ```\n * @public\n */\nexport const calculateSplicePositionFromNumber = (\n  tray: NetworkNode,\n  holderPosition: number\n): { x: number; y: number } => {\n  // Default to single mode for backward compatibility\n  return calculateSplicePositionFromNumberWithData(tray, holderPosition, false);\n};\n\n/**\n * Compute the `{ x, y }` offset (relative to the tray node) at which a splice in\n * a given 1-based holder position renders, honoring the splice mode.\n *\n * @remarks\n * Pure geometry — no DOM, React or reactflow. The mode-aware companion to\n * {@link calculateSplicePositionFromNumber}: ribbon splices are taller\n * ({@link SPLICE_HEIGHT_PX_RIBBON} in {@link HOLDER_HEIGHT_PX_RIBBON} holders)\n * than single splices ({@link SPLICE_HEIGHT_PX_SINGLE} in\n * {@link HOLDER_HEIGHT_PX_SINGLE} holders); the splice is centered vertically in\n * its holder slot. `x` is fixed at {@link SPLICE_CENTER_X}.\n *\n * @example\n * ```ts\n * calculateSplicePositionFromNumberWithData(tray, 1, true); // ribbon: { x, y: 34 }\n * ```\n * @public\n */\nexport const calculateSplicePositionFromNumberWithData = (\n  tray: NetworkNode,\n  holderPosition: number,\n  isRibbonMode: boolean = false\n): { x: number; y: number } => {\n  const startY = TRAY_HEADER_HEIGHT;\n  const holderHeight = isRibbonMode ? HOLDER_HEIGHT_PX_RIBBON : HOLDER_HEIGHT_PX_SINGLE;\n  const spliceHeight = isRibbonMode ? SPLICE_HEIGHT_PX_RIBBON : SPLICE_HEIGHT_PX_SINGLE;\n  // Center splice vertically in holder slot\n  const baseOffset = (holderHeight - spliceHeight) / 2;\n  const spliceY = startY + (holderPosition - 1) * holderHeight + baseOffset;\n\n  return {\n    x: SPLICE_CENTER_X, // Center splice in tray\n    y: spliceY,\n  };\n};\n","/**\n * Rack device width configuration\n * @public\n */\nimport type {\n  Connection,\n  Edge as ReactFlowEdge,\n  EdgeTypes,\n  Node as ReactFlowNode,\n  NodeTypes,\n  ConnectionMode,\n  FitViewOptions,\n  ReactFlowInstance,\n  Viewport,\n} from \"@xyflow/react\";\nimport type { FibreFlowCable } from \"./fibre-flow\";\n\nexport enum Width {\n  /** Full width device (19\" rack width) */\n  FULL = \"full\",\n  /** Half width device (9.5\" rack width) */\n  HALF = \"half\",\n}\n\n/**\n * Port configuration for network devices\n * @public\n */\n/**\n * Port type enumeration\n * @public\n */\nexport type PortType =\n  | \"ethernet\"\n  | \"rj45\"\n  | \"fiber\"\n  | \"console\"\n  | \"mgmt\"\n  | \"usb\"\n  | \"vga\"\n  | \"serial\"\n  | \"power_ac\"\n  | \"power_c13\"\n  | \"power_c19\";\n\n/**\n * Cable types used for connected network ports.\n * @public\n */\nexport type PortCableType =\n  | \"smf\"\n  | \"cat6\"\n  | \"om3\"\n  | \"om4\"\n  | \"om5\"\n  | \"cat5e\"\n  | \"cat6a\"\n  | \"cat7\"\n  | \"fiber\"\n  | \"ethernet\";\n\n/**\n * Device image fit options.\n *\n * Values mirror object-fit terms for API ergonomics and are mapped to valid\n * background-size values by renderers where needed.\n *\n * @public\n */\nexport type DeviceImageFit =\n  | \"cover\"\n  | \"contain\"\n  | \"contain-height\"\n  | \"fill\"\n  | \"none\"\n  | \"scale-down\";\n\n/**\n * Device image repeat options.\n * @public\n */\nexport type DeviceImageRepeat =\n  | \"no-repeat\"\n  | \"repeat\"\n  | \"repeat-x\"\n  | \"repeat-y\";\n\n/**\n * Shared image configuration for rack-mounted devices and device nodes.\n * @public\n */\nexport interface DeviceImageOptions {\n  /** Optional front image URL for device background (PNG, SVG, JPEG, etc.) */\n  frontImageUrl?: string;\n  /** Optional rear image URL for device background (PNG, SVG, JPEG, etc.) */\n  rearImageUrl?: string;\n  /** Optional attribution URL for front image (e.g. source/license link) */\n  frontImageAttribution?: string;\n  /** Optional attribution URL for rear image (e.g. source/license link) */\n  rearImageAttribution?: string;\n  /** Optional background sizing for device images */\n  imageFit?: DeviceImageFit;\n  /** Optional background position for device images */\n  imagePosition?: string;\n  /** Optional background repeat for device images */\n  imageRepeat?: DeviceImageRepeat;\n  /** When true, stretch device image to fill the device bounds (overrides imageFit) */\n  stretchToFit?: boolean;\n}\n\n/**\n * Explicit handle placement for a device/PDU port.\n * @public\n */\nexport interface PortPosition {\n  /**\n   * Handle side on the node. When the offset is omitted, ports on the same\n   * side and face are distributed automatically across groups.\n   */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  /** Side offset as percentage (0-100) */\n  offsetPercent?: number;\n  /** Side offset in pixels */\n  offsetPx?: number;\n  /** Absolute X position as percentage (0-100) of the owning face frame (element centre). Requires `yPercent`. */\n  xPercent?: number;\n  /** Absolute Y position as percentage (0-100) of the owning face frame (element centre). Requires `xPercent`. */\n  yPercent?: number;\n  /**\n   * Authored element width as a percentage (0-100) of the owning face frame.\n   *\n   * Contractual: percent-based geometry is the render contract shared by ports,\n   * cages, and bays across every surface (2D elevation, 3D, faceplate). Paired\n   * with `heightPercent` it is the element's bounding box, and because it is\n   * frame-relative it survives image rescaling.\n   */\n  widthPercent?: number;\n  /**\n   * Authored element height as a percentage (0-100) of the owning face frame.\n   * Contractual — see {@link PortPosition.widthPercent}.\n   */\n  heightPercent?: number;\n}\n\n/**\n * Authoring input for port/bay geometry: the canonical {@link PortPosition}\n * object, or a semantic edge string such as `'left-bottom'` that\n * {@link normalizePortPositionInput} expands. Renderers always normalize\n * before reading fields.\n * @public\n */\nexport type PortPositionInput = PortPosition | string;\n\n/**\n * IEC 60320 appliance couplers modelled by the renderer and the power\n * validation mate table. Inlets are even-numbered, connectors odd.\n * @public\n */\nexport type PowerConnectorType =\n  | \"C13\"\n  | \"C14\"\n  | \"C15\"\n  | \"C16\"\n  | \"C19\"\n  | \"C20\"\n  | \"C21\"\n  | \"C22\";\n\n/**\n * Recommended simple port input for device and PDU rendering.\n * Pass a flat array in `ports`; `react-networks` groups entries into front/rear\n * `PortBlock[]` internally using each port's `face` and `group`.\n * @public\n */\nexport interface DevicePort {\n  /** Stable handle identifier override (defaults to `group + label`, or `label` for power) */\n  id?: string;\n  /** Display label for the port */\n  label: string;\n  /** Front/rear device face. `back` is accepted as an alias for `rear`. */\n  face?: DeviceFace | \"back\";\n  /** Optional block/group name (for example `Network Ports`, `QSFP28`, `Power Ports`) */\n  group?: string;\n  /** Optional source/detected family label used for visual kind inference */\n  sourceTypeLabel?: string;\n  /** Whether the port is currently connected */\n  connected?: boolean;\n  /** Type of port for styling and validation */\n  type?: PortType;\n  /** Cable type for connection validation */\n  cableType?: PortCableType;\n  /** Optional power connector type for rendering (mainly for PDU ports) */\n  connectorType?: PowerConnectorType;\n  /** Optional compatibility keys. When omitted, defaults are derived from type/cable/connector data. */\n  compatibility?: string[];\n  /** Optional sort order within a face/group bucket */\n  order?: number;\n  /**\n   * Optional placement hint.\n   * Use `side` with `offsetPercent`/`offsetPx` for edge-anchored placement,\n   * or `xPercent`/`yPercent` for exact coordinates.\n   * With no resolvable placement, the renderer distributes ports across the\n   * whole face, including ports from other groups. A lone `xPercent` or\n   * `yPercent` is incomplete and uses that automatic placement.\n   * Semantic edge strings (e.g. `'left-bottom'`) are also accepted and\n   * normalized by the renderer.\n   */\n  position?: PortPositionInput;\n}\n\nexport interface Port {\n  /** Stable handle identifier override (defaults to `label`) */\n  id?: string;\n  /** Display label for the port */\n  label: string;\n  /** Optional source/detected family label used for visual kind inference */\n  sourceTypeLabel?: string;\n  /** Whether the port is currently connected */\n  connected: boolean;\n  /** Type of port for styling and validation */\n  type?: PortType;\n  /** Cable type for connection validation */\n  cableType?: PortCableType;\n  /** Optional power connector type for rendering (mainly for PDU ports) */\n  connectorType?: PowerConnectorType;\n  /** Optional compatibility keys. When omitted, defaults are derived from type/cable/connector data. */\n  compatibility?: string[];\n  /** Optional explicit handle placement (object or semantic edge string) */\n  position?: PortPositionInput;\n}\n\n/**\n * Group of related ports on a device\n * @public\n */\nexport interface PortBlock {\n  /** Name of the port group */\n  name: string;\n  /** Array of ports in this group */\n  ports: Port[];\n}\n\n/**\n * Containable bay classification. Mirrors dc_designer `bay_kind`.\n *\n * A bay HOLDS a field-replaceable unit (drive, PSU, fan, line card, …) and —\n * unlike a port — never mounts a cable handle. Transceiver cages are the one\n * capability overlap (containable AND connectable): they are authored as\n * ports with `bay` metadata (see `TemplatePort`) so the frozen handle-id\n * grammar and cable endpoints survive optic swaps, and therefore do NOT appear\n * as a `bayKind` value here — `'transceiver_cage'` lives on `TemplatePort.bay`.\n *\n * @public\n */\nexport type TemplateBayKind =\n  | \"storage_bay\"\n  | \"psu_bay\"\n  | \"fan_bay\"\n  | \"pcie_slot\"\n  | \"linecard_slot\"\n  | \"supervisor_slot\"\n  | \"fabric_slot\"\n  | \"io_slot\"\n  | \"other\";\n\n/**\n * Occupancy/health of a single bay instance.\n * `predicted` marks a modelled (not yet observed) state.\n * @public\n */\nexport type DeviceBayStatus = \"ok\" | \"failed\" | \"predicted\";\n\n/**\n * Per-bay instance state, keyed by bay id on node data. Rides on node data\n * alongside `handleSideMap`; the renderer paints occupancy/failed treatment\n * from it.\n * @public\n */\nexport interface DeviceBayInstanceState {\n  /** Whether the bay currently holds a unit. */\n  occupied?: boolean;\n  /** Health/occupancy status driving the bay marker's visual treatment. */\n  status?: DeviceBayStatus;\n}\n\n/**\n * A per-slot sub-rectangle inside a bay (e.g. one socket in a DIMM bank, one\n * drive position in a carrier). Geometry uses the same `PortPosition` percent\n * contract as ports and bays.\n * @public\n */\nexport interface DeviceBaySlot {\n  /** Stable identifier for the slot (unique within its bay). */\n  id: string;\n  /** Human-readable label. */\n  label: string;\n  /** Slot centre + bounding box as percentages of the owning face frame. */\n  position?: PortPosition;\n}\n\n/**\n * Containable-only element on a device face: the renderer draws an outline +\n * label for it but never mounts a React Flow handle. The node-data analogue of\n * the template `TemplateBay`; `face` selects the owning face (default\n * front, `back` accepted as an alias for `rear`).\n * @public\n */\nexport interface DeviceBay {\n  /** Stable identifier for the bay (unique within the device). */\n  id: string;\n  /** Human-readable label drawn on the bay outline. */\n  label: string;\n  /** Bay classification (drives visual treatment and click payloads). */\n  bayKind: TemplateBayKind;\n  /** Front/rear device face. `back` is accepted as an alias for `rear`. */\n  face?: DeviceFace | \"back\";\n  /** Bay centre + bounding box as percentages of the owning face frame. */\n  position?: PortPosition;\n  /** Optional per-slot sub-rectangles. */\n  slots?: DeviceBaySlot[];\n}\n\n/**\n * Network node types supported by the diagram\n * @public\n */\nexport type NetworkNodeType =\n  | \"rack\"\n  | \"switch\"\n  | \"router\"\n  | \"server\"\n  | \"fiber\"\n  | \"patch-panel\"\n  | \"device\"\n  | \"vertical-pdu\"\n  | \"distribution-board\"\n  | \"splice\"\n  | \"splice-tray\"\n  | \"tube\"\n  | \"cable\"\n  | \"multi-tube-cable\"\n  | \"coupler\"\n  | \"closure\"\n  | \"fibre-split\"\n  | \"fibre-flow-cable\"\n  | \"fibre-flow-closure\";\n\n/**\n * Device status states\n * @public\n */\nexport type DeviceStatus = \"active\" | \"inactive\" | \"maintenance\";\n\n/**\n * Device face orientations\n * @public\n */\nexport type DeviceFace = \"front\" | \"rear\";\n\nexport type DiagramPortHandleRef = {\n  nodeId: string;\n  handleId: string;\n};\n\nexport type DiagramPortClickPayload = {\n  nodeId: string;\n  handleId: string;\n  groupName: string;\n  portLabel: string;\n  face: DeviceFace;\n};\n\n/**\n * Reference to a single bay on a node — the bay analogue of\n * {@link DiagramPortHandleRef}. Used by `NetworkDiagram.highlightedBays`.\n * @public\n */\nexport type DiagramBayRef = {\n  nodeId: string;\n  bayId: string;\n};\n\n/**\n * Payload for a bay click — the bay analogue of {@link DiagramPortClickPayload}\n * (`bayId`/`bayKind`/`bayLabel` in place of `handleId`/`groupName`/`portLabel`).\n * @public\n */\nexport type DiagramBayClickPayload = {\n  nodeId: string;\n  bayId: string;\n  bayKind: TemplateBayKind;\n  bayLabel: string;\n  face: DeviceFace;\n};\n\n/**\n * Rack rendering layout modes.\n * @public\n */\nexport type RackLayoutMode = \"single-face\" | \"split-face\";\n\n/**\n * Rack mounting behavior for a device.\n * @public\n */\nexport type RackMountMode = \"front\" | \"rear\" | \"both\";\n\n/**\n * Power port side configuration\n * @public\n */\nexport type PowerSide = \"left\" | \"right\";\n\n/**\n * U numbering direction for racks\n * @public\n */\nexport type UNumberingDirection = \"bottom-up\" | \"top-down\";\n\n/**\n * Shared node data fields available across node types.\n * Specific node kinds can narrow these fields via `NetworkNodeDataByType`.\n * @public\n */\nexport interface NetworkNodeDataBase extends DeviceImageOptions {\n  /** Display label for the node */\n  label: string;\n  /** Current status of the node/device */\n  status?: DeviceStatus | string;\n  /** Simple flat port input or grouped port blocks. Use `portGroups` / `rearPortGroups` for explicit grouped control. */\n  ports?: DevicePort[] | PortBlock[] | number;\n  /** Advanced grouped front port input */\n  portGroups?: PortBlock[];\n  /** Advanced grouped rear port input */\n  rearPortGroups?: PortBlock[];\n  /** Grouped rear port blocks (rack JSON alias for `rearPortGroups`) */\n  rearPorts?: PortBlock[];\n  /** List of connected node IDs */\n  connections?: string[];\n  /** NetBox integration ID */\n  netboxId?: number | string;\n  /** NetBox object type */\n  netboxType?: \"device\" | \"rack\" | \"interface\" | \"cable\" | string;\n  /** Inventory integration ID */\n  inventoryId?: number | string;\n  /** Inventory object type */\n  inventoryType?: \"device\" | \"rack\" | \"interface\" | \"cable\" | string;\n  /** Rack height in U units */\n  uHeight?: number;\n  /** Device position within rack in U units */\n  uPosition?: number;\n  /** Whether this node is currently mounted inside a rack container */\n  isRackMounted?: boolean;\n  /** Front/rear orientation for devices and rack faces */\n  face?: DeviceFace;\n  /** Rack rendering layout mode */\n  rackLayout?: RackLayoutMode;\n  /** Gap in pixels between front and rear faces in split-face mode */\n  rackFaceGapPx?: number;\n  /** Device mount behavior within a rack */\n  mountMode?: RackMountMode;\n  /** Device type specification */\n  deviceType?: string;\n\n  /**\n   * When false, device elevation renders as a status-colored block without front/rear photos.\n   * Defaults to true (show photos when URLs exist).\n   */\n  showDeviceImages?: boolean;\n  /** Device manufacturer */\n  manufacturer?: string;\n  /** Device role in network */\n  role?: string;\n  /** Device width configuration */\n  width?: Width | number;\n  /** Power port side for rear face */\n  powerSide?: PowerSide;\n  /** Vertical PDU height in centimeters */\n  heightCm?: number;\n  /** Number of AC power ports */\n  powerPorts?: number;\n  /** Optional explicit PDU connector sequence */\n  portTypes?: Array<\"C13\" | \"C19\">;\n  /** Optional grouped PDU ports (supports custom positions/types) */\n  pduPortGroups?: PortBlock[];\n  /** Vertical PDU width in pixels */\n  widthPx?: number;\n  /** Optional management unit position for vertical PDUs */\n  mgmt_unit?: \"top\" | \"middle\" | \"bottom\";\n  /** U numbering direction for rack nodes */\n  uNumberingDirection?: UNumberingDirection;\n  /** Custom header text for rack nodes */\n  customHeaderText?: string;\n  /** Rack UI marker visibility */\n  showUMarkers?: boolean;\n  /** Holder position within tray (1-12) for splice nodes */\n  holderPosition?: number;\n  /** Loss value in dB for splice nodes */\n  lossDb?: number;\n  /** Splice mode */\n  mode?: \"single\" | \"ribbon\";\n  /** Legacy ribbon flag */\n  ribbon?: boolean;\n  /** Ribbon fiber IDs */\n  ribbonFiberIds?: number[];\n  /** Connector type for coupler nodes */\n  connector?: \"SC\" | \"LC\";\n  /** Number of fibers for coupler nodes (1, 2, or 4) */\n  couplerFiberCount?: 1 | 2 | 4;\n  /** Optional port labels for left side of coupler */\n  portLabelsLeft?: string[];\n  /** Optional port labels for right side of coupler */\n  portLabelsRight?: string[];\n  /** When true, coupler output (right) ports inherit left-side fiber color instead of default yellow. */\n  passThroughColor?: boolean;\n  /** Slot position for couplers inside panels (1-based, vertical slots aligned to right edge) */\n  slotPosition?: number;\n  /** Width override for panel-like container nodes */\n  panelWidth?: number;\n  /** Height override for panel-like container nodes */\n  panelHeight?: number;\n  /** Width override for closure nodes */\n  closureWidth?: number;\n  /** Height override for closure nodes */\n  closureHeight?: number;\n  /** Optional rack width in pixels (for rack nodes, default: 300px) */\n  rackWidthPx?: number;\n  /** Number of rows for container nodes (patch-panel, closure) */\n  rows?: number;\n  /** Number of columns for container nodes (patch-panel, closure) */\n  cols?: number;\n  /** Number of trays for closure nodes */\n  trayCount?: number;\n  /** Header text for container nodes */\n  header?: string;\n  /** Type of fibre splitter: 'splitter' or 'WDM' (default: 'splitter') */\n  splitterType?: \"splitter\" | \"WDM\";\n  /** Number of input fibers on the left (1 or 2, default: 1) */\n  inputCount?: number;\n  /** Number of output fibers on the right (1-64, default: 8) */\n  outputCount?: number;\n  /** Tube index (1-based) */\n  tubeIndex?: number;\n  /** Tube count for multi-tube cables */\n  tubeCount?: number;\n  /** Tube start index for multi-tube cables */\n  tubeStartIndex?: number;\n  /** Number of fibers per tube */\n  fibersPerTube?: number;\n  /** Total fiber count for cables */\n  fiberCount?: number;\n  /** Cable identifier */\n  cableId?: string;\n  /** Primary node color override */\n  color?: string;\n  /** Rotation in degrees for rotatable nodes */\n  rotation?: number;\n  /** Tube handle orientation */\n  startOn?: \"left-center\" | \"left\" | \"right\";\n  /** Rack metadata for integrations */\n  facilityId?: string | number | null;\n  /** Rack serial metadata */\n  serial?: string | null;\n  /** Asset tag metadata */\n  assetTag?: string | null;\n  /** Site metadata */\n  site?: string;\n  /** Tenant metadata */\n  tenant?: string;\n  /** Primary IPv4 metadata */\n  primaryIp4?: string;\n  /** Primary IPv6 metadata */\n  primaryIp6?: string;\n  /** Rack name metadata */\n  rack?: string | number | null;\n  /** Optional arrays used by inventory/netbox transforms */\n  racks?: unknown[];\n  devices?: unknown[];\n  cables?: unknown[];\n  /** UI color mode metadata */\n  colorMode?: \"light\" | \"dark\" | \"system\";\n  /** Generic dimensions used by some helpers */\n  height?: number;\n  headerHeight?: number;\n  /** Optional precomputed map for handle-to-face resolution */\n  handleSideMap?: Record<string, DeviceFace>;\n  /**\n   * Per-bay instance state keyed by bay id (occupancy/health). Rides on node\n   * data alongside `handleSideMap`; DeviceNode paints bay markers from it.\n   */\n  bayState?: Record<string, DeviceBayInstanceState>;\n  /** Additional extension metadata */\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Rack node data.\n * @public\n */\nexport interface RackNodeData extends NetworkNodeDataBase {\n  ports?: number;\n}\n\n/**\n * What kind of power-distribution equipment a `vertical-pdu` node stands for.\n * The renderer draws them all as a vertical strip; the label is carried so\n * consumers and validation can tell a rack PDU from an RPP, a busway tap, or a\n * sub-panel.\n * @public\n */\nexport type PduBoardType = \"pdu\" | \"rpp\" | \"panel\" | \"busway\" | \"other\";\n\n/** Supply family of a PDU strip. @public */\nexport type PduSupply = \"ac\" | \"dc\";\n\n/**\n * A/B designation of the branch circuit feeding a PDU. `primary` is the A\n * feed, `secondary` the B feed; redundancy rules read it to tell the two\n * halves of a 2N design apart.\n * @public\n */\nexport type PowerFeedType = \"primary\" | \"secondary\";\n\n/**\n * Which half of an IEC 60320 / IEC 60309 pairing a connector is. Inlets on\n * equipment are male (pins); cord connectors are female (sockets).\n * @public\n */\nexport type PowerConnectorGender = \"male\" | \"female\";\n\n/**\n * Vertical PDU node data.\n *\n * Everything here is optional and additive — a PDU authored before the inlet\n * model (label + `powerPorts` + `portTypes`) renders and validates exactly as\n * it did, and the inlet handle is not mounted at all unless inlet data is\n * present.\n *\n * The strip has two electrically distinct sides:\n *\n * - the **inlet** — where a branch circuit from a distribution-board breaker\n *   way lands (fixed wiring: IEC 60309, hardwired, C20, NEMA locking). One per\n *   strip, handle id `inlet`.\n * - the **outlets** — where equipment cords land, handle ids `\"1\"…\"N\"`\n *   top-down under IEC 60320 mate rules.\n *\n * **Reserved handle ids.** `inlet` (and `incomer`, the distribution-board\n * spelling) name the supply inlet. Do not give an outlet in `pduPortGroups` an\n * `id` or `label` that lowercases to either: the strip would mount two handles\n * with the same id. A declared outlet deliberately wins that collision — it\n * classifies as a cord source, not as the supply inlet, so the cords it carries\n * still validate — but the inlet handle then becomes unreachable. Number the\n * outlets, or give them a distinct prefix (`rear-power-1`).\n * @public\n */\nexport interface VerticalPduNodeData extends NetworkNodeDataBase {\n  /**\n   * Free-text inlet connector designation, e.g. `\"IEC 60309 32A\"`,\n   * `\"C20\"`, `\"NEMA L6-30P\"`, `\"hardwired\"`. Free text because the inlet\n   * families a rack PDU can carry are regional and open-ended; the connector\n   * *mate* rules only apply to the IEC 60320 cord side (the outlets).\n   */\n  inletConnector?: string;\n  /** Inlet current rating in amps (the branch-circuit rating the strip takes). */\n  inletAmps?: number;\n  /** Inlet gender — equipment inlets are normally `male`. */\n  inletGender?: PowerConnectorGender;\n  /** A/B designation of the feed landing on the inlet. */\n  feedType?: PowerFeedType;\n  /** What kind of distribution equipment this strip stands for. */\n  boardType?: PduBoardType;\n  /** Supply family. */\n  supply?: PduSupply;\n  /** Nominal supply voltage in volts (integer, e.g. 230). */\n  voltage?: number;\n  /** Supply phase count. Outlets are attributed round-robin across it. */\n  phases?: 1 | 3;\n  /**\n   * Rating in amps of the strip's own outlet/bank overcurrent protection\n   * (internal breakers or fuses), when it has any. PWR-A06 judges the cords\n   * below a strip that declares it against this rating instead of the upstream\n   * way's.\n   */\n  outletProtectionA?: number;\n  /** Optional equipment name, distinct from the drawn `label`. */\n  name?: string;\n}\n\n/**\n * Device-like node data (servers/switches/routers/generic device).\n * @public\n */\nexport interface DeviceNodeData extends NetworkNodeDataBase {\n  ports?: DevicePort[] | PortBlock[] | number;\n  /**\n   * Containable-only bays drawn as outlines + labels (never cable handles).\n   * Each bay's `face` selects the owning face (default front).\n   */\n  bays?: DeviceBay[];\n  /**\n   * Nameplate/maximum power draw in **watts**. Named after NetBox's\n   * `DeviceType.airflow`-adjacent power fields (`maximum_draw`), which are watts\n   * — keep the unit in watts so a NetBox sync is a straight field copy.\n   */\n  maximumDrawW?: number;\n  /**\n   * Allocated (design/budgeted) power draw in **watts**. NetBox's\n   * `allocated_draw`; used by load rules to sum a board way's committed load.\n   */\n  allocatedDrawW?: number;\n  /**\n   * How many power feeds this device is expected to have (NetBox models this as\n   * redundant power ports). `2` marks an A+B device, so losing one feed is\n   * `degraded` rather than unpowered.\n   */\n  expectedFeeds?: 1 | 2;\n}\n\n/**\n * Switch state of a single breaker way.\n * @public\n */\nexport type BreakerSwitchState = \"closed\" | \"open\" | \"tripped\";\n\n/**\n * Phase label carried by a breaker way. Purely a label — the library does not\n * model three-phase electrically (see PRD workstream C non-goals).\n * @public\n */\nexport type BreakerPhase = \"L1\" | \"L2\" | \"L3\" | \"3P\";\n\n/**\n * One breaker way (slot) on a distribution board. A way is not its own node in\n * the physical view: it is a connection endpoint on the board, mounting exactly\n * one power handle.\n * @public\n */\nexport interface BreakerWay {\n  /** Stable identifier for the way (unique within the board). */\n  id: string;\n  /** Human-readable label drawn on the way row (e.g. `1`, `L1-3`). */\n  label: string;\n  /** Pole count (1 = single-pole, 3 = three-pole). */\n  poles?: number;\n  /** Breaker rating in amps. */\n  ratingA?: number;\n  /** Phase label. `3P` (or `poles: 3`) marks a three-phase way. */\n  phase?: BreakerPhase;\n  /**\n   * Circuit voltage this way supplies, overriding the board's: line-to-neutral\n   * for a single-pole way, line-to-line for a 2-pole or three-phase way.\n   */\n  voltageV?: number;\n  /**\n   * Optional explicit placement as percentages of the board frame. Omitted ways\n   * are auto-laid-out in the generated faceplate grid, which is the normal case.\n   */\n  position?: PortPosition;\n}\n\n/**\n * Per-way instance state keyed by way id, mirroring {@link DeviceBayInstanceState}.\n * @public\n */\nexport interface BreakerWayInstanceState {\n  /** Whether the breaker is closed, deliberately open, or tripped. */\n  switchState: BreakerSwitchState;\n  /** Optional health status, reusing the bay status vocabulary. */\n  status?: DeviceBayStatus;\n}\n\n/**\n * Distribution board node data. Boards are free-floating by default; set\n * {@link DistributionBoardNodeData.rackMountable} to opt a board into\n * rack assignment.\n * @public\n */\nexport interface DistributionBoardNodeData extends NetworkNodeDataBase {\n  /** The board's breaker ways, rendered top-to-bottom in column order. */\n  ways: BreakerWay[];\n  /** Per-way switch/health state keyed by way id. */\n  wayState?: Record<string, BreakerWayInstanceState>;\n  /**\n   * Nominal board voltage in volts — the legacy single figure. When neither\n   * {@link DistributionBoardNodeData.phaseVoltageV} nor\n   * {@link DistributionBoardNodeData.lineToLineVoltageV} is set, the load rules\n   * (PWR-B01/B02) use it unchanged for every way: as the line-to-neutral\n   * voltage of single-pole ways and the line-to-line voltage of 2-pole /\n   * three-phase ways. That is only right when all the board's ways are one\n   * kind (PWR-B03 flags boards where it cannot be); declare the explicit pair\n   * for a mixed board.\n   */\n  voltageV?: number;\n  /**\n   * Nominal **line-to-neutral** (phase) voltage in volts — what a single-pole\n   * way supplies (`230` on a 230/400 V board, `120` on 120/208 V, `277` on\n   * 277/480 V). Defaults to `lineToLineVoltageV / √3` when only that is set.\n   */\n  phaseVoltageV?: number;\n  /**\n   * Nominal **line-to-line** voltage in volts — what a 2-pole or three-phase\n   * way supplies (`400`, `208`, `480`). Defaults to `phaseVoltageV × √3` when\n   * only that is set. With no voltage declared at all the rules assume\n   * 120/208 V under `NEC` and 230/400 V under `IEC` (and PWR-B03 says so).\n   */\n  lineToLineVoltageV?: number;\n  /**\n   * Power factor (0–1] the load rules apply when converting nameplate watts to\n   * amps. Defaults to 1 (watts treated as volt-amperes, NetBox's convention).\n   */\n  powerFactor?: number;\n  /**\n   * What kind of distribution asset this board is. Drives the header badge\n   * only — an `rpp` is drawn `RPP`, a `busway` `BUSWAY`, and anything else\n   * (including omitted) falls back to `DB`. Purely a labelling concern: every\n   * board type shares one electrical model of ways and an incomer.\n   */\n  boardType?: PduBoardType;\n  /**\n   * Rating of the board's incomer/main device in amps, shown in the header.\n   * Distinct from a way's `ratingA`: this is the device feeding the busbar,\n   * not one of the outgoing ways.\n   */\n  incomerRatingA?: number;\n  /** Which code vocabulary consumer rules should read this board under. */\n  codeMode?: \"NEC\" | \"IEC\";\n  /**\n   * Continuous-load derate ceiling as a percentage of a way's rating. Defaults\n   * by `codeMode`: 80 under `NEC` (the default mode), 100 under `IEC`.\n   */\n  maxUtilization?: number;\n  /**\n   * Opt-in flag marking this board as rack-mountable. `distribution-board` is\n   * deliberately NOT in `RACK_ASSIGNABLE_NODE_TYPES` — boards are wall-mounted\n   * free-floating nodes by default, and this flag is the hook for the\n   * rack-mounted minority.\n   */\n  rackMountable?: boolean;\n  /** Number of way columns in the generated faceplate (default 1). */\n  wayColumns?: number;\n}\n\n/**\n * Fibre flow cable node data.\n * @public\n */\nexport interface FibreFlowCableNodeData extends NetworkNodeDataBase {\n  cable: FibreFlowCable;\n  side: \"left\" | \"right\";\n  rowHeight: number;\n  columnWidth: number;\n  showTubeLabels: boolean;\n  showFiberNumbers: boolean;\n  layout: unknown;\n  colorMode?: \"dark\" | \"light\";\n}\n\n/**\n * Fibre flow closure node data.\n * @public\n */\nexport interface FibreFlowClosureNodeData extends NetworkNodeDataBase {\n  width?: number;\n  height?: number;\n  headerHeight?: number;\n  colorMode?: \"dark\" | \"light\";\n}\n\n/**\n * Mapping from node type to corresponding data type.\n * @public\n */\nexport interface NetworkNodeDataByType {\n  rack: RackNodeData;\n  switch: DeviceNodeData;\n  router: DeviceNodeData;\n  server: DeviceNodeData;\n  fiber: NetworkNodeDataBase;\n  \"patch-panel\": NetworkNodeDataBase;\n  device: DeviceNodeData;\n  \"vertical-pdu\": VerticalPduNodeData;\n  \"distribution-board\": DistributionBoardNodeData;\n  splice: NetworkNodeDataBase;\n  \"splice-tray\": NetworkNodeDataBase;\n  tube: NetworkNodeDataBase;\n  cable: NetworkNodeDataBase;\n  \"multi-tube-cable\": NetworkNodeDataBase;\n  coupler: NetworkNodeDataBase;\n  closure: NetworkNodeDataBase;\n  \"fibre-split\": NetworkNodeDataBase;\n  \"fibre-flow-cable\": FibreFlowCableNodeData;\n  \"fibre-flow-closure\": FibreFlowClosureNodeData;\n}\n\n/**\n * Union of all node data variants.\n * @public\n */\nexport type NetworkNodeData = NetworkNodeDataByType[NetworkNodeType];\n\n/**\n * Network node structure with data discriminated by `type`.\n * @public\n */\nexport type NetworkNode<\n  TType extends NetworkNodeType = NetworkNodeType,\n  TExtra = unknown,\n> = TType extends NetworkNodeType\n  ? ReactFlowNode<\n      NetworkNodeDataByType[TType] & TExtra & Record<string, unknown>,\n      TType\n    > & {\n      /** Type of network node */\n      type: TType;\n      /** Node data and configuration */\n      data: NetworkNodeDataByType[TType] & TExtra;\n    }\n  : never;\n\n/**\n * React Flow node specialized for this library's data. Use as the type argument\n * to `NodeProps` in custom node components under `@xyflow/react` v12\n * (v12 parameterizes `NodeProps`/`NodeType` by the node, not the data).\n * @public\n */\nexport type NetworkFlowNode = ReactFlowNode<\n  NetworkNodeData & Record<string, unknown>,\n  NetworkNodeType\n>;\n\n/**\n * Network edge types\n * @public\n */\nexport type NetworkEdgeType =\n  | \"fiber\"\n  | \"ethernet\"\n  | \"power\"\n  | \"smoothstep\"\n  | \"step\"\n  | \"thick-cable\"\n  | \"fibre-flow\"\n  | \"fibre-flow-link\";\n\n/**\n * Edge data payload.\n * @public\n */\nexport interface NetworkEdgeData {\n  /**\n   * Index signature so this data satisfies `@xyflow/react` v12's `Edge<Data>`\n   * constraint (`Record<string, unknown>`). Edge data is never `Omit`-ed, so\n   * (unlike the node data types) an index signature is safe here.\n   */\n  [key: string]: unknown;\n  /** Connection bandwidth */\n  bandwidth?: string;\n  /**\n   * Cable length, in the source system's unit (see {@link NetworkEdgeData.lengthUnit}).\n   * Unit-less: prefer {@link NetworkEdgeData.lengthCm} for comparisons.\n   */\n  length?: number;\n  /** Unit of {@link NetworkEdgeData.length} as the source system reported it (e.g. `m`, `ft`). */\n  lengthUnit?: string;\n  /**\n   * Cable length normalised to centimetres. The NetBox/inventory converters set\n   * it when the source length has a known unit; PWR-A12 reads it in preference\n   * to the unit-less `length`.\n   */\n  lengthCm?: number;\n  /** Edge color for styling */\n  color?: string;\n  /** Edge kind for styling/validation */\n  kind?: \"fiber\" | \"power\" | \"network\";\n  /** Whether this edge represents a ribbon fiber connection */\n  isRibbon?: boolean;\n  /** Array of fiber IDs for ribbon mode (6-12 fibers) */\n  ribbonFiberIds?: number[];\n  /** Highlight edge with a pulsing stroke */\n  highlight?: boolean;\n  /** Override highlight color (defaults to edge color) */\n  highlightColor?: string;\n  /** Override highlight max stroke width */\n  highlightWidth?: number;\n  /** Optional tag for grouping edges in demos */\n  traceTag?: string;\n  /** Legacy striped edge flag */\n  striped?: boolean;\n  /** Cable type metadata used by demos/helpers */\n  cableType?: PortCableType;\n  /** NetBox integration metadata */\n  netboxId?: number | string;\n  netboxType?: \"device\" | \"rack\" | \"interface\" | \"cable\" | string;\n  /** Inventory integration metadata */\n  inventoryId?: number | string;\n  inventoryType?: \"device\" | \"rack\" | \"interface\" | \"cable\" | string;\n  /** Status metadata */\n  status?: string;\n  /** Endpoint metadata */\n  terminationA?: string;\n  terminationB?: string;\n  /** Optional display label for the edge */\n  label?: string;\n  /** Optional opacity for styling (0–1) */\n  opacity?: number;\n  /** Optional power edge stroke width override */\n  powerStrokeWidth?: number;\n  /** Optional power edge stroke dasharray override */\n  powerStrokeDasharray?: string;\n  /** Optional power edge opacity override (0–1) */\n  powerOpacity?: number;\n  /**\n   * Power energisation overlay: `true` renders the edge as live (marching\n   * dashes), `false` renders it dimmed/grey. Omit to keep the classic\n   * (unaware) power edge styling. Produced by `computeEnergization`.\n   */\n  energized?: boolean;\n  /**\n   * Direction power travels across the edge, relative to its own endpoints.\n   * Draws a flow arrow on an energised edge; ignored otherwise.\n   */\n  flowDirection?: \"source-to-target\" | \"target-to-source\";\n  /** Additional extension metadata */\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Network edge data structure\n * @public\n */\nexport type NetworkEdge<TData extends NetworkEdgeData = NetworkEdgeData> = {\n  /** Unique identifier for the edge */\n  id: string;\n  /** Source node ID */\n  source: string;\n  /** Target node ID */\n  target: string;\n  /** Source handle/port ID */\n  sourceHandle?: string;\n  /** Target handle/port ID */\n  targetHandle?: string;\n  /** Type of network connection */\n  type?: NetworkEdgeType;\n  /** Whether the edge should be hidden from rendering */\n  hidden?: boolean;\n  /**\n   * Whether the edge may be deleted by user interaction (keyboard Delete,\n   * batch delete). Mirrors React Flow's `Edge.deletable`; defaults to true.\n   * Deletion programmatically through `edges` prop updates is unaffected.\n   */\n  deletable?: boolean;\n  /**\n   * Width (px) of the invisible interaction stroke rendered beneath the edge,\n   * forwarded to React Flow's `Edge.interactionWidth`. It widens the edge's\n   * click/hover target, but the stroke also sits above neighboring node\n   * handles in z-order — an oversized value can occlude nearby ports and make\n   * them unreachable for connection drops in dense layouts. When omitted, the\n   * diagram's `edgeInteractionWidth` default applies.\n   */\n  interactionWidth?: number;\n  /** Edge data and configuration */\n  data?: TData;\n};\n\n/**\n * React Flow edge specialized for this library's data. Use as the type argument\n * to `EdgeProps` in custom edge components under `@xyflow/react` v12\n * (v12 parameterizes `EdgeProps` by the edge, not the data).\n * @public\n */\nexport type NetworkFlowEdge = ReactFlowEdge<NetworkEdgeData, NetworkEdgeType>;\n\n/**\n * Canonical persisted/editor document schema independent from React Flow handle ids.\n * Prefer this model for saving diagrams, syncing external systems, and controlled\n * document-mode usage via `document` / `onDocumentChange`.\n * @public\n */\nexport interface DiagramDocument {\n  /**\n   * Document schema version. Always `1` for documents emitted by this library.\n   * A missing version on input is treated as legacy v0 (identical to `1` today);\n   * a version greater than `1` is parsed best-effort with a console warning.\n   */\n  version: 1;\n  nodes: DiagramNode[];\n  edges: DiagramEdge[];\n  ui?: DiagramUiState;\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Node kinds used by the document model.\n * @public\n */\nexport type DiagramNodeKind =\n  | \"rack\"\n  | \"equipment\"\n  | \"pdu\"\n  | \"distribution-board\"\n  | \"fiber\"\n  | \"tube\"\n  | \"cable\"\n  | \"multi-tube-cable\"\n  | \"splice\"\n  | \"coupler\"\n  | \"splitter\"\n  | \"container\"\n  | \"fibre-flow-cable\"\n  | \"fibre-flow-closure\";\n\n/**\n * Placement data for document nodes.\n * @public\n */\nexport interface DiagramNodePlacement {\n  position: {\n    x: number;\n    y: number;\n  };\n  parentId?: string;\n  extent?: \"parent\";\n  rack?: {\n    uPosition?: number;\n    mountMode?: RackMountMode;\n    rackLayout?: RackLayoutMode;\n    rackWidthPx?: number;\n    rackFaceGapPx?: number;\n  };\n  container?: {\n    slotPosition?: number;\n    holderPosition?: number;\n  };\n}\n\n/**\n * Endpoint definition for document nodes.\n * @public\n */\nexport interface DiagramEndpoint {\n  /**\n   * Opaque unique key for this endpoint within its document. The shipped\n   * adapters mint handle-grammar strings here for render-side stability, but\n   * consumers may supply any unique string — the reverse adapter resolves\n   * edges through the endpoint RECORD, so renaming a port's `label` does not\n   * require re-keying when ids are consumer-minted. Do not parse this value.\n   *\n   * When a device mounts the same port id on both faces, the shipped forward\n   * adapter mints the rear endpoint as `<id>--rear` (the renderer's split-face\n   * handle alias) so ids stay unique; the reverse adapter gives the rebuilt rear\n   * port back its shared `<id>`.\n   */\n  id: string;\n  label: string;\n  face?: DeviceFace | \"internal\";\n  group?: string;\n  role?:\n    | \"network\"\n    | \"power\"\n    | \"fiber\"\n    | \"tube\"\n    | \"splice\"\n    | \"coupler\"\n    | \"splitter\"\n    | \"cable\"\n    | \"generic\";\n  medium?: \"ethernet\" | \"fiber\" | \"power\" | \"tube\" | \"generic\";\n  type?: PortType;\n  cableType?: PortCableType;\n  connector?: string;\n  /**\n   * Free-text source-of-truth port type label (e.g. `\"QSFP28\"`, `\"RJ45\"`) used\n   * to pick the port's connector visual. Preserved verbatim across the document\n   * round-trip so a port keeps its icon.\n   */\n  sourceTypeLabel?: string;\n  compatibility?: string[];\n  layout?: PortPosition;\n}\n\n/**\n * Render-focused node data for the document model.\n * @public\n */\nexport interface DiagramNodeRender {\n  status?: DeviceStatus | string;\n  face?: DeviceFace;\n  width?: Width | number;\n  widthPx?: number;\n  height?: number;\n  header?: string;\n  color?: string;\n  colorMode?: \"light\" | \"dark\" | \"system\";\n  rackWidthPx?: number;\n  rackFaceGapPx?: number;\n  images?: DeviceImageOptions;\n  dimensions?: Record<string, number | undefined>;\n  showUMarkers?: boolean;\n  customHeaderText?: string;\n  powerSide?: PowerSide;\n  rotation?: number;\n}\n\n/**\n * External reference attached to a document node.\n * @public\n */\nexport interface DiagramExternalRef {\n  source: \"netbox\" | \"inventory\" | string;\n  id: string | number;\n  type?: string;\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Document node.\n * @public\n */\nexport interface DiagramNode {\n  id: string;\n  kind: DiagramNodeKind;\n  subtype?: string;\n  label: string;\n  placement: DiagramNodePlacement;\n  endpoints?: DiagramEndpoint[];\n  render?: DiagramNodeRender;\n  config?: Record<string, unknown>;\n  externalRefs?: DiagramExternalRef[];\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Endpoint reference used by document edges.\n * @public\n */\nexport interface DiagramEdgeEndpoint {\n  nodeId: string;\n  endpointId?: string | null;\n}\n\n/**\n * Document edge.\n * @public\n */\nexport interface DiagramEdge {\n  id: string;\n  source: DiagramEdgeEndpoint;\n  target: DiagramEdgeEndpoint;\n  type?: NetworkEdgeType;\n  hidden?: boolean;\n  /** Mirrors {@link NetworkEdge.deletable}; omitted when the runtime edge left it unset. */\n  deletable?: boolean;\n  /** Mirrors {@link NetworkEdge.interactionWidth}; omitted when unset. */\n  interactionWidth?: number;\n  data?: NetworkEdgeData;\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Non-topology UI state stored alongside a document.\n * @public\n */\nexport interface DiagramUiState {\n  nodeState?: Record<\n    string,\n    {\n      face?: DeviceFace;\n      handleSideMap?: Record<string, DeviceFace>;\n    }\n  >;\n  metadata?: Record<string, unknown>;\n}\n\n/**\n * Rack device types\n * @public\n */\nexport type RackDeviceType =\n  | \"server\"\n  | \"switch\"\n  | \"router\"\n  | \"patch-panel\"\n  | \"ups\";\n\n/**\n * Rack configuration structure\n * @public\n */\nexport interface RackConfig {\n  /** Unique rack identifier */\n  id: string;\n  /** Rack display name */\n  name: string;\n  /** Rack position coordinates */\n  position: { x: number; y: number };\n  /** Rack height in U units */\n  units: number;\n  /** U numbering direction (default: 'bottom-up') */\n  uNumberingDirection?: UNumberingDirection;\n  /** Custom header text for rack */\n  customHeaderText?: string;\n  /** Rack rendering layout mode */\n  rackLayout?: RackLayoutMode;\n  /** Gap in pixels between front and rear faces in split-face mode */\n  rackFaceGapPx?: number;\n  /** Devices mounted in the rack */\n  devices: RackDevice[];\n  /** Optional rack width in pixels (default: 300px which represents standard 48.26cm/19\" rack) */\n  rackWidthPx?: number;\n}\n\n/**\n * Device mounted in a rack\n * @public\n */\nexport interface RackDevice extends DeviceImageOptions {\n  /** Unique device identifier */\n  id: string;\n  /** Device display name */\n  name: string;\n  /** Starting U position in rack */\n  unit: number;\n  /** Device height in U units */\n  height: number;\n  /** Device type */\n  type: RackDeviceType;\n  /** Device width configuration */\n  width?: Width;\n  /** Device mount behavior within a rack */\n  mountMode?: RackMountMode;\n  /** Device role in network */\n  role?: string;\n  /** Recommended simple port input (flat `DevicePort[]` or grouped `PortBlock[]`). */\n  ports?: DevicePort[] | PortBlock[];\n  /** Current status of the device */\n  status?: DeviceStatus | string;\n  /** Advanced grouped front port input */\n  portGroups?: PortBlock[];\n  /** Advanced grouped rear port input */\n  rearPortGroups?: PortBlock[];\n  /** Grouped rear port blocks (rack JSON alias for `rearPortGroups`) */\n  rearPorts?: PortBlock[];\n  /** Power port side for rear face */\n  powerSide?: PowerSide;\n  /**\n   * Containable-only bays drawn as outlines + labels (never cable handles).\n   * Each bay's `face` selects the owning face (default front).\n   *\n   * Passed straight through to the device node's own `bays` node-data field, so\n   * authoring bays here is equivalent to authoring them on node data — hosts\n   * building from `rackConfig.devices[]` no longer need the per-device extra\n   * node-data channel just to render bays.\n   */\n  bays?: DeviceBay[];\n  /** Connected device IDs */\n  connections?: string[];\n}\n\n/**\n * Fiber cable types\n * @public\n */\nexport type FibreCableType = \"single-mode\" | \"multi-mode\";\n\n/**\n * Fiber cable configuration\n * @public\n */\nexport interface FibreCable {\n  /** Unique cable identifier */\n  id: string;\n  /** Cable display name */\n  name: string;\n  /** Fiber cable type */\n  type: FibreCableType;\n  /** Cable length */\n  length: number;\n  /** Cable color for visualization */\n  color: string;\n  /** Connection endpoints */\n  connections: {\n    /** Source connection point */\n    from: string;\n    /** Target connection point */\n    to: string;\n  };\n}\n\n/**\n * Splice configuration structure\n * @public\n */\nexport interface SpliceConfig {\n  /** Unique splice identifier */\n  id: string;\n  /** Splice display name */\n  name: string;\n  /** Holder position within tray (1-12) */\n  holder: number;\n  /** Loss value in dB */\n  lossDb?: number;\n  /** Splice mode: 'single' for individual fiber, 'ribbon' for ribbon splices */\n  mode?: \"single\" | \"ribbon\";\n  /** For ribbon mode: array of fiber IDs (6-12) in the ribbon */\n  ribbonFiberIds?: number[];\n}\n\n/**\n * Splice tray configuration structure\n * @public\n */\nexport interface SpliceTrayConfig {\n  /** Unique tray identifier */\n  id: string;\n  /** Tray display name */\n  name: string;\n  /** Tray position coordinates */\n  position: { x: number; y: number };\n  /** Splices in this tray */\n  splices: SpliceConfig[];\n}\n\n// Inventory/CMDB integrations are generic and not tied to specific vendors\n\n/**\n * Fine-grained interaction controls for network diagrams.\n * @public\n */\nexport interface NetworkDiagramInteractionOptions {\n  /** Enable node dragging. */\n  nodesDraggable?: boolean;\n  /** Enable creating connections from node handles. */\n  nodesConnectable?: boolean;\n  /** Enable selecting nodes/edges. */\n  elementsSelectable?: boolean;\n  /** Enable keyboard/mouse focus on edges. */\n  edgesFocusable?: boolean;\n  /** Enable reconnecting existing edges. */\n  edgesReconnectable?: boolean;\n  /** Enable click-to-connect behavior on handles. */\n  connectOnClick?: boolean;\n  /** Enable panning by dragging the canvas. */\n  panOnDrag?: boolean;\n  /** Enable panning by scrolling. */\n  panOnScroll?: boolean;\n  /** Enable zoom via mouse wheel/trackpad scroll. */\n  zoomOnScroll?: boolean;\n  /** Enable pinch-zoom gestures. */\n  zoomOnPinch?: boolean;\n  /** Enable zoom on double click. */\n  zoomOnDoubleClick?: boolean;\n  /** Enable deleting selected edges with keyboard keys. */\n  allowKeyboardDelete?: boolean;\n  /** Show or hide the built-in React Flow controls widget. */\n  showControls?: boolean;\n  /** Show or hide the minimap. */\n  showMiniMap?: boolean;\n  /** Show or hide the interactive lock/unlock button in controls. */\n  showInteractiveControl?: boolean;\n}\n\n/**\n * Selection action panel options rendered above selected racks/devices.\n * @public\n */\nexport interface NetworkDiagramActionPanelOptions {\n  /** Enable or disable the floating action panel entirely. Defaults to true. */\n  enabled?: boolean;\n  /** Show or hide the front/rear toggle button. Defaults to true. */\n  showRearToggle?: boolean;\n  /** Show or hide the De-rack button for rack-mounted devices. Defaults to true. */\n  showDeRack?: boolean;\n  /** Show a rack's Zoom to edit button, which enlarges its U rows. Defaults to true. */\n  showZoomToEdit?: boolean;\n}\n\n/**\n * Serializable flow snapshot payload for save/restore helpers.\n * @public\n */\nexport interface NetworkDiagramFlowObject {\n  /** Snapshot nodes */\n  nodes: NetworkNode[];\n  /** Snapshot edges */\n  edges: NetworkEdge[];\n  /** Optional viewport state */\n  viewport?: Viewport;\n}\n\n/**\n * Options for exported diagram images.\n * @public\n */\nexport interface NetworkDiagramToImageOptions {\n  /** Export width in px (clamped to 1-8192). */\n  width?: number;\n  /** Export height in px (clamped to 1-8192). */\n  height?: number;\n  backgroundColor?: string;\n  /** Export padding in px (clamped to 0-2048). */\n  padding?: number;\n  /** Device pixel ratio multiplier (clamped to 0.1-4). */\n  pixelRatio?: number;\n  /** JPEG/WebP quality (clamped to 0-1 when used). */\n  quality?: number;\n  filter?: (domNode: HTMLElement) => boolean;\n}\n\n/**\n * Fit-view options that support targeting node IDs directly.\n * @public\n */\nexport interface NetworkDiagramFitViewOptions extends Omit<\n  FitViewOptions,\n  \"nodes\"\n> {\n  /** Optional node IDs to fit */\n  nodes?: string[];\n}\n\n/**\n * Exposed methods for save/restore and export integrations.\n * @public\n */\nexport interface NetworkDiagramFlowMethods {\n  toObject: () => NetworkDiagramFlowObject;\n  setViewport: (viewport: Viewport) => void;\n  setNodes: (nodes: NetworkNode[]) => void;\n  setEdges: (edges: NetworkEdge[]) => void;\n  toImage: (options?: NetworkDiagramToImageOptions) => Promise<string>;\n  getNodes: () => NetworkNode[];\n  getEdges: () => NetworkEdge[];\n  /**\n   * Fit all nodes or an ID subset using configured policy defaults. Per-call\n   * options override them, and subset floors use current measured bounds.\n   */\n  fitView: (options?: NetworkDiagramFitViewOptions) => void;\n}\n\n/**\n * Network diagram component props\n * @public\n */\nexport interface NetworkDiagramProps {\n  /** Controlled document model. When provided, it takes precedence over nodes/edges props. */\n  document?: DiagramDocument;\n  /** Initial document for uncontrolled document mode. */\n  initialDocument?: DiagramDocument;\n  /** Callback when the document changes in document mode. */\n  onDocumentChange?: (document: DiagramDocument) => void;\n  /** Controlled nodes array */\n  nodes?: NetworkNode[];\n  /** Controlled edges array */\n  edges?: NetworkEdge[];\n  /** Callback when nodes change (controlled mode) */\n  onNodesChange?: (nodes: NetworkNode[]) => void;\n  /** Callback when edges change (controlled mode) */\n  onEdgesChange?: (edges: NetworkEdge[]) => void;\n  /** Initial nodes for uncontrolled mode */\n  initialNodes?: NetworkNode[];\n  /** Initial edges for uncontrolled mode */\n  initialEdges?: NetworkEdge[];\n  /** Enable debug mode */\n  debug?: boolean;\n  /** Callback when a node is clicked or selection changes (null when deselected) */\n  onNodeClick?: (node: NetworkNode | null) => void;\n  /** Callback when an edge is clicked */\n  onEdgeClick?: (edge: NetworkEdge) => void;\n  /** Callback when the diagram background (pane) is clicked */\n  onPaneClick?: () => void;\n  /** Custom React Flow node types to merge with defaults */\n  nodeTypes?: NodeTypes;\n  /** Custom React Flow edge types to merge with defaults */\n  edgeTypes?: EdgeTypes;\n  /** Optional onConnect override (React Flow connection handler) */\n  onConnect?: (connection: Connection) => void;\n  /** Optional onEdgeUpdate override */\n  onEdgeUpdate?: (oldEdge: ReactFlowEdge, newConnection: Connection) => void;\n  /** Optional isValidConnection override */\n  isValidConnection?: (connection: Connection) => boolean;\n  /** Optional connection mode override */\n  connectionMode?: ConnectionMode;\n  /** Callback when a device face changes */\n  onFaceChange?: (nodeId: string, face: DeviceFace) => void;\n  /** Callback when rack face changes */\n  onRackFaceChange?: (rackId: string, face: DeviceFace) => void;\n  /** CSS class name */\n  className?: string;\n  /** Inline styles */\n  style?: React.CSSProperties;\n  /** If true, vertically align rack bottoms to the same baseline */\n  alignRacksToBottom?: boolean;\n  /** Allow rack-mounted devices to move between racks/ungroup on drag and drop. Defaults to true. */\n  reAssignable?: boolean;\n  /** Color mode for the diagram */\n  colorMode?: \"light\" | \"dark\" | \"system\";\n  /** Callback when React Flow instance is initialized */\n  onInit?: (instance: ReactFlowInstance) => void;\n  /** Callback to get React Flow methods for save/restore */\n  onFlowMethods?: (methods: NetworkDiagramFlowMethods) => void;\n  /** Show download button */\n  showDownloadButton?: boolean;\n  /** Download button position */\n  downloadButtonPosition?:\n    | \"top-right\"\n    | \"top-left\"\n    | \"bottom-right\"\n    | \"bottom-left\";\n  /** Custom download button text */\n  downloadButtonText?: string;\n  /** Download button styling options */\n  downloadButtonStyle?: React.CSSProperties;\n  /** Use smoothstep edges for tube connections (both tube-to-cable and cable-to-tube). Default is fiber. */\n  useSmoothstepEdgesForTubes?: boolean;\n  /** Force read-only behavior for the diagram (no drag/connect/delete/reconnect/pan/zoom edits). */\n  readOnly?: boolean;\n  /** Centralized interaction options for consistent edit/lock behavior. */\n  interaction?: NetworkDiagramInteractionOptions;\n  /** Configuration for the floating action panel shown on selected rack/device nodes. */\n  actionPanel?: NetworkDiagramActionPanelOptions;\n  /** Optional fitView options passed to React Flow */\n  fitViewOptions?: FitViewOptions;\n  /** Whether React Flow performs its built-in fit-on-init. Defaults to `true`.\n   *\n   * The init fit does not run at mount — it runs once node dimensions are first\n   * measured, which can be seconds later. A host that restores a persisted\n   * viewport must set this to `false`, otherwise the init fit lands *after* the\n   * restore and silently overwrites the restored pan/zoom.\n   *\n   * Ignored when `initialViewport.mode` is `'fit'` (library computes bounds from\n   * declared node dimensions instead).\n   */\n  fitViewOnInit?: boolean;\n  /**\n   * Minimum zoom passed to React Flow. Defaults to 0.1 so the always-on\n   * fitView can actually fit tall scenes (React Flow's own 0.5 default clamps\n   * the fit and crops full-height racks, which viewport culling then never\n   * mounts).\n   */\n  minZoom?: number;\n  /** Maximum zoom passed to React Flow. Defaults to 4. */\n  maxZoom?: number;\n  /**\n   * Let React Flow render only viewport-visible nodes and edges (virtualization).\n   *\n   * Defaults to `false`, which renders every element — the safest choice for\n   * offscreen measurement, edge routing, and the initial fit-to-view, since all\n   * nodes mount and report their dimensions up front.\n   *\n   * Enable this for very large topologies where mounting every rack is a\n   * performance cost. The tradeoff: virtualization improves large-diagram\n   * performance but can affect offscreen behaviour — nodes outside the viewport\n   * are not mounted, so their measured dimensions, the edges routed to/from\n   * them, and any fit-to-view that depends on their bounds may be wrong until\n   * they scroll into view. With a tight `minZoom` a fit-to-view can cull\n   * off-screen nodes so they never mount at all.\n   *\n   * Export-safe: this is force-disabled for the duration of a\n   * {@link NetworkDiagramFlowMethods.toImage} capture (and the built-in download\n   * button), so a PNG export includes every element even when virtualization is\n   * on. Culling is restored once the capture completes.\n   */\n  onlyRenderVisibleElements?: boolean;\n  /** Node IDs to keep at full opacity when diagram focus dimming is enabled */\n  highlightedNodeIds?: string[];\n  /** Edge IDs to keep at full opacity when diagram focus dimming is enabled */\n  highlightedEdgeIds?: string[];\n  /**\n   * When true, non-highlighted nodes and edges are dimmed while focus IDs are set.\n   * Edges connected to highlighted nodes remain in focus unless\n   * `connectEdgesToHighlightedNodes` is false.\n   */\n  dimUnhighlighted?: boolean;\n  /** Opacity for dimmed nodes/edges (0-1). Defaults to 0.25 */\n  dimOpacity?: number;\n  /** When false, only explicit highlightedEdgeIds stay bright during focus. */\n  connectEdgesToHighlightedNodes?: boolean;\n  /**\n   * Declarative multi-severity highlighting. Each group paints its member edges\n   * with the group's `color`/`width` through the same per-edge highlight channel\n   * (`data.highlight`/`highlightColor`/`highlightWidth`) the built-in edge\n   * renderers read, so groups render identically to hand-authored highlights.\n   *\n   * Overlap policy: **later groups win** — treat the array as severity tiers\n   * ordered least-to-most specific, so the highest-priority tier is placed last.\n   *\n   * When `highlightGroups` is non-empty and `dimUnhighlighted` is set, the dim\n   * complement is the union of every group's `nodeIds`/`edgeIds` plus the legacy\n   * `highlightedNodeIds`/`highlightedEdgeIds` props. `label` is carried through\n   * for future legend rendering and is unused by the diagram today.\n   */\n  highlightGroups?: ReadonlyArray<NetworkDiagramHighlightGroup>;\n  /** When false, devices render without rack photos. Status border still uses `getStatusColor`. Default true. */\n  showDeviceImages?: boolean;\n  /** Port handles kept highlighted during diagram trace/focus. */\n  highlightedPortHandles?: DiagramPortHandleRef[];\n  /** Fired when a port handle is clicked (typically in read-only trace mode). */\n  onPortClick?: (payload: DiagramPortClickPayload) => void;\n  /**\n   * Bays kept highlighted (e.g. the exact failed-drive slot to pull). The bay\n   * analogue of `highlightedPortHandles`; delivered through the same\n   * instance-scoped highlight store.\n   */\n  highlightedBays?: DiagramBayRef[];\n  /** Fired when a bay outline is clicked. The bay analogue of `onPortClick`. */\n  onBayClick?: (payload: DiagramBayClickPayload) => void;\n  /**\n   * Pointer distance (px) within which a drag snaps onto a handle, forwarded to\n   * React Flow's `connectionRadius`. Larger values make small port handles easier\n   * to target; smaller values reduce mis-targeting on densely packed devices.\n   * When omitted, React Flow's default is used.\n   */\n  connectionRadius?: number;\n  /**\n   * Default `interactionWidth` (px) applied to rendered edges, i.e. the width\n   * of each edge's invisible click/hover stroke. React Flow's component\n   * default is 20px, which blankets handles spaced closer than ~10px to a\n   * running edge (stacked splice ports, patch panels) and swallows connection\n   * drops meant for them. Defaults to 8 — still an easy click target while\n   * keeping tightly packed ports reachable. Per-edge `interactionWidth`\n   * overrides this value; pass 0 to disable edge interaction strokes.\n   */\n  edgeInteractionWidth?: number;\n  /**\n   * Fired when a connection attempt is rejected by the diagram's validation\n   * (incompatible endpoints, an occupied non-replaceable handle, a disallowed\n   * fiber pairing, etc.). Lets host apps surface feedback instead of failing\n   * silently. Not called for the continuous hover validation during a drag —\n   * only when an actual connect or reconnect attempt is rejected.\n   */\n  onConnectRejected?: (payload: NetworkConnectionRejection) => void;\n  /**\n   * Library-managed initial camera. Prefer this over demo-level fitView timers.\n   * `'fit'` uses declared top-level node dimensions (v12 SSR model).\n   */\n  initialViewport?: NetworkDiagramInitialViewport;\n  /** Fired once after `initialViewport` fit or restore completes. */\n  onViewportReady?: () => void;\n  /** Hide the canvas until the initial viewport is applied. Defaults to true when fitting. */\n  hideUntilViewportReady?: boolean;\n  /**\n   * What to show inside the canvas when the diagram has no nodes to render.\n   *\n   * Defaults to a small centred, themed \"No nodes to display\" message. Pass\n   * your own node for a custom empty state, or `false` to render nothing (the\n   * bare grid). The built-in overlay is `pointer-events: none`, so\n   * drag-and-drop onto an empty canvas keeps working; keep that in mind if you\n   * supply your own — an interactive empty state must opt its own controls\n   * back in with `pointer-events: auto`.\n   *\n   * The overlay appears only after the node count has been zero for a short\n   * moment (~150ms), so a controlled diagram that renders `nodes={[]}` for a\n   * tick while data loads does not flash the message. While you know you are\n   * loading, pass `emptyState={false}` to suppress it entirely.\n   */\n  emptyState?: React.ReactNode | false;\n  /**\n   * Wrap the React Flow subtree in `DiagramErrorBoundary` so a throwing\n   * custom node type renders a themed fallback inside the canvas instead of\n   * unmounting the host page. Defaults to `true`; pass `false` to opt out and\n   * let errors propagate to your own boundary.\n   *\n   * Because it defaults to `true`, diagram render errors no longer reach an\n   * error boundary you wrap around `NetworkDiagram`, nor any reporting hooked\n   * up to it. Pass {@link NetworkDiagramProps.onError} to keep reporting them,\n   * or `errorBoundary={false}` for the previous propagate-to-you behaviour.\n   */\n  errorBoundary?: boolean;\n  /**\n   * Fallback rendered in place of the diagram when the built-in error boundary\n   * catches an error. A render prop receives the error and a `reset` callback\n   * that clears the error and re-renders the diagram. Ignored when\n   * `errorBoundary` is `false`.\n   */\n  errorFallback?: React.ReactNode | ((error: Error, reset: () => void) => React.ReactNode);\n  /**\n   * Called when the built-in error boundary catches an error. This is the way\n   * to keep existing error reporting (Sentry, etc.) working now that the\n   * boundary is on by default and swallows errors before they reach your own\n   * boundary. Ignored when `errorBoundary` is `false`.\n   */\n  onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n}\n\n/**\n * One tier of {@link NetworkDiagramProps.highlightGroups} declarative\n * highlighting.\n *\n * @public\n * @remarks\n * A group names some `nodeIds`/`edgeIds` and paints its member edges with\n * `color`/`width` via the built-in per-edge highlight channel. Use one group\n * per severity/category (e.g. error, warning, info) and order the array\n * least-to-most specific, since **later groups win** where members overlap. The\n * optional `label` is reserved for future legend rendering and is not drawn by\n * the diagram today.\n *\n * @example\n * ```tsx\n * const groups: NetworkDiagramHighlightGroup[] = [\n *   { edgeIds: warningEdgeIds, color: \"#d97706\", width: 4, label: \"Warning\" },\n *   { edgeIds: errorEdgeIds, nodeIds: errorNodeIds, color: \"#dc2626\", width: 6, label: \"Error\" },\n * ];\n * <NetworkDiagram nodes={nodes} edges={edges} highlightGroups={groups} dimUnhighlighted />\n * ```\n */\nexport interface NetworkDiagramHighlightGroup {\n  /** Node IDs in this tier. Contributes to the dim complement when dimming. */\n  readonly nodeIds?: readonly string[];\n  /** Edge IDs in this tier. Painted with `color`/`width`; also kept bright when dimming. */\n  readonly edgeIds?: readonly string[];\n  /** Highlight color for member edges. When omitted, edges keep their own color. */\n  readonly color?: string;\n  /** Highlight stroke width for member edges. When omitted, the renderer default is used. */\n  readonly width?: number;\n  /** Human-readable tier name, reserved for future legend rendering. Unused in render today. */\n  readonly label?: string;\n}\n\n/** Initial camera strategy for {@link NetworkDiagram}. @public */\nexport type NetworkDiagramInitialViewport =\n  | {\n      mode: 'fit';\n      padding?: number;\n      minZoom?: number;\n      maxZoom?: number;\n      /**\n       * Refit when top-level node layout changes. Default (unset): refit until\n       * the user moves the camera (pan, zoom, Zoom to edit, or setViewport),\n       * then leave their camera alone. `true` always refits; `false` never does.\n       */\n      refitOnLayoutChange?: boolean;\n    }\n  | { mode: 'restore'; viewport: Viewport }\n  | { mode: 'none' };\n\n/** Payload describing why a connection attempt was rejected. */\nexport interface NetworkConnectionRejection {\n  /** The connection the user attempted to make. */\n  connection: Connection;\n  /** Human-readable reason the connection was rejected. */\n  reason: string;\n  /** Whether the rejection happened while creating a new edge or reconnecting an existing one. */\n  phase: \"connect\" | \"reconnect\";\n}\n\nexport * from \"./netbox\";\n","import type { PortPosition } from '../types';\n\nconst EDGE_OFFSET_LOW_PERCENT = 12;\nconst EDGE_OFFSET_CENTER_LOW_PERCENT = 34;\nconst EDGE_OFFSET_CENTER_PERCENT = 50;\nconst EDGE_OFFSET_CENTER_HIGH_PERCENT = 66;\nconst EDGE_OFFSET_HIGH_PERCENT = 88;\nconst FACE_POSITION_FRONT_PERCENT = 22;\nconst FACE_POSITION_REAR_PERCENT = 78;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst toNumber = (value: unknown): number | undefined => {\n  if (typeof value === 'number' && Number.isFinite(value)) {\n    return value;\n  }\n  if (typeof value === 'string' && value.trim().length > 0) {\n    const parsed = Number(value);\n    if (Number.isFinite(parsed)) {\n      return parsed;\n    }\n  }\n  return undefined;\n};\n\nconst normalizeSemanticPositionTokens = (value: string): string[] =>\n  value\n    .trim()\n    .toLowerCase()\n    .split(/[^a-z0-9]+/)\n    .filter((token) => token.length > 0);\n\nconst canonicalizeSideToken = (token: string): NonNullable<PortPosition['side']> | undefined => {\n  if (token === 'top' || token === 'up') {\n    return 'top';\n  }\n  if (token === 'right') {\n    return 'right';\n  }\n  if (token === 'bottom' || token === 'down') {\n    return 'bottom';\n  }\n  if (token === 'left') {\n    return 'left';\n  }\n  return undefined;\n};\n\nconst hasCenterToken = (tokens: string[]): boolean =>\n  tokens.includes('center') || tokens.includes('centre') || tokens.includes('middle') || tokens.includes('mid');\n\nconst resolveAxisOffsetPercent = (\n  primarySide: NonNullable<PortPosition['side']>,\n  tokens: string[],\n): number | undefined => {\n  const hasLeft = tokens.includes('left');\n  const hasRight = tokens.includes('right');\n  const hasTop = tokens.includes('top') || tokens.includes('up');\n  const hasBottom = tokens.includes('bottom') || tokens.includes('down');\n  const hasCenter = hasCenterToken(tokens);\n\n  if (primarySide === 'top' || primarySide === 'bottom') {\n    if (hasLeft && hasCenter && !hasRight) return EDGE_OFFSET_CENTER_LOW_PERCENT;\n    if (hasRight && hasCenter && !hasLeft) return EDGE_OFFSET_CENTER_HIGH_PERCENT;\n    if (hasLeft && !hasRight) return EDGE_OFFSET_LOW_PERCENT;\n    if (hasRight && !hasLeft) return EDGE_OFFSET_HIGH_PERCENT;\n    if (hasCenter) return EDGE_OFFSET_CENTER_PERCENT;\n    return undefined;\n  }\n\n  if (hasTop && hasCenter && !hasBottom) return EDGE_OFFSET_CENTER_LOW_PERCENT;\n  if (hasBottom && hasCenter && !hasTop) return EDGE_OFFSET_CENTER_HIGH_PERCENT;\n  if (hasTop && !hasBottom) return EDGE_OFFSET_LOW_PERCENT;\n  if (hasBottom && !hasTop) return EDGE_OFFSET_HIGH_PERCENT;\n  if (hasCenter) return EDGE_OFFSET_CENTER_PERCENT;\n  return undefined;\n};\n\nconst resolveSemanticEdgePortPosition = (tokens: string[]): PortPosition | undefined => {\n  const hasFaceTokens = tokens.includes('front') || tokens.includes('rear') || tokens.includes('back');\n  if (hasFaceTokens) {\n    return undefined;\n  }\n\n  const primarySide = tokens\n    .map(canonicalizeSideToken)\n    .find((side): side is NonNullable<PortPosition['side']> => side !== undefined);\n\n  if (!primarySide) {\n    return undefined;\n  }\n\n  const offsetPercent = resolveAxisOffsetPercent(primarySide, tokens);\n  if (offsetPercent === undefined) {\n    return undefined;\n  }\n\n  return {\n    side: primarySide,\n    offsetPercent,\n  };\n};\n\nconst resolveSemanticCompositePortPosition = (tokens: string[]): PortPosition | undefined => {\n  if (tokens.length === 0) {\n    return undefined;\n  }\n\n  const hasLeft = tokens.includes('left');\n  const hasRight = tokens.includes('right');\n  const hasCenter = hasCenterToken(tokens);\n  const hasFront = tokens.includes('front') || tokens.includes('top') || tokens.includes('up');\n  const hasRear = tokens.includes('rear') || tokens.includes('back') || tokens.includes('bottom') || tokens.includes('down');\n\n  let xPercent: number | undefined;\n  if (hasLeft && hasCenter && !hasRight) {\n    xPercent = EDGE_OFFSET_CENTER_LOW_PERCENT;\n  } else if (hasRight && hasCenter && !hasLeft) {\n    xPercent = EDGE_OFFSET_CENTER_HIGH_PERCENT;\n  } else if (hasLeft && !hasRight) {\n    xPercent = EDGE_OFFSET_LOW_PERCENT;\n  } else if (hasRight && !hasLeft) {\n    xPercent = EDGE_OFFSET_HIGH_PERCENT;\n  } else if (hasCenter) {\n    xPercent = EDGE_OFFSET_CENTER_PERCENT;\n  }\n\n  let yPercent: number | undefined;\n  if (hasFront && !hasRear) {\n    yPercent = FACE_POSITION_FRONT_PERCENT;\n  } else if (hasRear && !hasFront) {\n    yPercent = FACE_POSITION_REAR_PERCENT;\n  } else if (hasCenter) {\n    yPercent = EDGE_OFFSET_CENTER_PERCENT;\n  }\n\n  if (xPercent === undefined || yPercent === undefined) {\n    return undefined;\n  }\n\n  return { xPercent, yPercent };\n};\n\nexport const parseSemanticPortPosition = (value: string): PortPosition | undefined => {\n  const tokens = normalizeSemanticPositionTokens(value);\n  if (tokens.length === 0) {\n    return undefined;\n  }\n\n  if (tokens.length === 1) {\n    const side = canonicalizeSideToken(tokens[0]);\n    return side ? { side } : undefined;\n  }\n\n  return resolveSemanticEdgePortPosition(tokens) ?? resolveSemanticCompositePortPosition(tokens);\n};\n\nexport const normalizePortPositionInput = (value: unknown): PortPosition | undefined => {\n  if (typeof value === 'string') {\n    return parseSemanticPortPosition(value);\n  }\n\n  if (!isRecord(value)) {\n    return undefined;\n  }\n\n  const sideRaw = value.side ?? value.port_side ?? value.portSide;\n  const semanticPosition = typeof sideRaw === 'string' ? parseSemanticPortPosition(sideRaw) : undefined;\n  const simpleSide = typeof sideRaw === 'string' ? canonicalizeSideToken(sideRaw.trim().toLowerCase()) : undefined;\n\n  const side = simpleSide ?? semanticPosition?.side;\n  const offsetPercent = toNumber(\n    value.offsetPercent ??\n    value.offset_percent ??\n    value.offsetPct ??\n    value.offset_pct\n  ) ?? semanticPosition?.offsetPercent;\n  const offsetPx = toNumber(\n    value.offsetPx ??\n    value.offset_px ??\n    value.offsetPixels ??\n    value.offset_pixels\n  ) ?? semanticPosition?.offsetPx;\n  const xPercent = toNumber(\n    value.xPercent ??\n    value.x_percent ??\n    value.xPct ??\n    value.x_pct ??\n    value.horizontalPercent ??\n    value.horizontal_percent ??\n    value.x\n  ) ?? semanticPosition?.xPercent;\n  const yPercent = toNumber(\n    value.yPercent ??\n    value.y_percent ??\n    value.yPct ??\n    value.y_pct ??\n    value.verticalPercent ??\n    value.vertical_percent ??\n    value.y\n  ) ?? semanticPosition?.yPercent;\n  const widthPercent = toNumber(\n    value.widthPercent ??\n    value.width_percent ??\n    value.widthPct ??\n    value.width_pct ??\n    value.wPercent ??\n    value.w_percent\n  );\n  const heightPercent = toNumber(\n    value.heightPercent ??\n    value.height_percent ??\n    value.heightPct ??\n    value.height_pct ??\n    value.hPercent ??\n    value.h_percent\n  );\n\n  if (\n    side === undefined &&\n    offsetPercent === undefined &&\n    offsetPx === undefined &&\n    xPercent === undefined &&\n    yPercent === undefined &&\n    widthPercent === undefined &&\n    heightPercent === undefined\n  ) {\n    return undefined;\n  }\n\n  return {\n    side,\n    offsetPercent,\n    offsetPx,\n    xPercent,\n    yPercent,\n    widthPercent,\n    heightPercent,\n  };\n};\n","import type {\n  DevicePort,\n  DeviceFace,\n  NetworkNode,\n  Port,\n  PortBlock,\n  PortPosition,\n} from '../types';\nimport { isPowerPortType } from './handle-ids';\nimport { normalizePortPositionInput } from './port-position';\n\ntype IndexedPort = {\n  index: number;\n  port: DevicePort;\n  position: PortPosition | undefined;\n};\n\ntype GroupedPortBucket = {\n  face: DeviceFace;\n  groupName: string;\n  entries: IndexedPort[];\n};\n\nconst DEFAULT_FRONT_GROUP_NAME = 'Network Ports';\nconst DEFAULT_REAR_GROUP_NAME = 'Rear Network Ports';\nconst DEFAULT_POWER_GROUP_NAME = 'Power Ports';\nconst DISTRIBUTED_OFFSET_MIN_PERCENT = 8;\nconst DISTRIBUTED_OFFSET_MAX_PERCENT = 92;\n\nfunction clampPercent(value: number): number {\n  return Math.max(0, Math.min(100, value));\n}\n\nfunction normalizePortAnchor(value: PortPosition['side'] | undefined): PortPosition['side'] | undefined {\n  if (value === 'top' || value === 'right' || value === 'bottom' || value === 'left') {\n    return value;\n  }\n  return undefined;\n}\n\nfunction getNormalizedPortPosition(position: unknown): PortPosition | undefined {\n  return normalizePortPositionInput(position);\n}\n\nfunction hasExplicitAbsoluteCoordinates(position: PortPosition | undefined): boolean {\n  return (\n    typeof position?.xPercent === 'number' &&\n    typeof position?.yPercent === 'number'\n  );\n}\n\nfunction hasExplicitOffset(position: PortPosition | undefined): boolean {\n  return (\n    typeof position?.offsetPercent === 'number' ||\n    typeof position?.offsetPx === 'number'\n  );\n}\n\nfunction resolveRenderPortFace(port: DevicePort): DeviceFace {\n  if (port.face === 'rear' || port.face === 'back') {\n    return 'rear';\n  }\n  if (port.face === 'front') {\n    return 'front';\n  }\n  return isPowerPortType(port.type) ? 'rear' : 'front';\n}\n\nfunction resolveRenderPortGroupName(port: DevicePort, face: DeviceFace): string {\n  if (typeof port.group === 'string' && port.group.trim().length > 0) {\n    return port.group.trim();\n  }\n  if (isPowerPortType(port.type)) {\n    return DEFAULT_POWER_GROUP_NAME;\n  }\n  return face === 'rear' ? DEFAULT_REAR_GROUP_NAME : DEFAULT_FRONT_GROUP_NAME;\n}\n\nfunction getEntrySortValue(entry: IndexedPort): number {\n  return typeof entry.port.order === 'number' && Number.isFinite(entry.port.order)\n    ? entry.port.order\n    : entry.index;\n}\n\nfunction toDistributedOffsetPercent(index: number, count: number): number {\n  if (count <= 1) {\n    return 50;\n  }\n  return (\n    DISTRIBUTED_OFFSET_MIN_PERCENT +\n    (index * (DISTRIBUTED_OFFSET_MAX_PERCENT - DISTRIBUTED_OFFSET_MIN_PERCENT)) / (count - 1)\n  );\n}\n\nfunction buildDistributedOffsetMap(entries: IndexedPort[]): Map<number, number> {\n  const distributedEntries = entries.filter((entry) => {\n    const anchor = normalizePortAnchor(entry.position?.side);\n    return !!anchor && !hasExplicitAbsoluteCoordinates(entry.position) && !hasExplicitOffset(entry.position);\n  });\n  const offsetPercentByIndex = new Map<number, number>();\n  const entriesByAnchor = new Map<NonNullable<PortPosition['side']>, IndexedPort[]>();\n\n  distributedEntries.forEach((entry) => {\n    const anchor = normalizePortAnchor(entry.position?.side);\n    if (!anchor) {\n      return;\n    }\n    const group = entriesByAnchor.get(anchor);\n    if (group) {\n      group.push(entry);\n      return;\n    }\n    entriesByAnchor.set(anchor, [entry]);\n  });\n\n  entriesByAnchor.forEach((anchorEntries) => {\n    const sortedEntries = [...anchorEntries].sort((left, right) => {\n      const orderDifference = getEntrySortValue(left) - getEntrySortValue(right);\n      if (orderDifference !== 0) {\n        return orderDifference;\n      }\n      return left.index - right.index;\n    });\n    sortedEntries.forEach((entry, index) => {\n      offsetPercentByIndex.set(entry.index, toDistributedOffsetPercent(index, sortedEntries.length));\n    });\n  });\n\n  return offsetPercentByIndex;\n}\n\nfunction toPortPosition(\n  normalizedPosition: PortPosition | undefined,\n  distributedOffsetPercent?: number,\n): PortPosition | undefined {\n  const anchor = normalizePortAnchor(normalizedPosition?.side);\n  if (hasExplicitAbsoluteCoordinates(normalizedPosition)) {\n    const position: PortPosition = {\n      xPercent: clampPercent(normalizedPosition?.xPercent as number),\n      yPercent: clampPercent(normalizedPosition?.yPercent as number),\n    };\n    if (typeof normalizedPosition?.widthPercent === 'number' && Number.isFinite(normalizedPosition.widthPercent)) {\n      position.widthPercent = clampPercent(normalizedPosition.widthPercent);\n    }\n    if (typeof normalizedPosition?.heightPercent === 'number' && Number.isFinite(normalizedPosition.heightPercent)) {\n      position.heightPercent = clampPercent(normalizedPosition.heightPercent);\n    }\n    if (anchor) {\n      position.side = anchor;\n    }\n    return position;\n  }\n\n  if (!anchor) {\n    return undefined;\n  }\n\n  const position: PortPosition = { side: anchor };\n  if (typeof normalizedPosition?.offsetPx === 'number' && Number.isFinite(normalizedPosition.offsetPx)) {\n    position.offsetPx = normalizedPosition.offsetPx;\n    return position;\n  }\n\n  const explicitOffsetPercent =\n    typeof normalizedPosition?.offsetPercent === 'number' && Number.isFinite(normalizedPosition.offsetPercent)\n      ? clampPercent(normalizedPosition.offsetPercent)\n      : undefined;\n  const resolvedOffsetPercent =\n    explicitOffsetPercent ?? (typeof distributedOffsetPercent === 'number'\n      ? clampPercent(distributedOffsetPercent)\n      : undefined);\n\n  if (typeof resolvedOffsetPercent === 'number') {\n    position.offsetPercent = resolvedOffsetPercent;\n  }\n\n  return position;\n}\n\nfunction toPort(entry: IndexedPort, distributedOffsetPercent?: number): Port {\n  const position = toPortPosition(entry.position, distributedOffsetPercent);\n  return {\n    id: entry.port.id,\n    label: entry.port.label,\n    sourceTypeLabel: entry.port.sourceTypeLabel,\n    connected: entry.port.connected ?? false,\n    type: entry.port.type,\n    cableType: entry.port.cableType,\n    connectorType: entry.port.connectorType,\n    compatibility: Array.isArray(entry.port.compatibility) ? [...entry.port.compatibility] : undefined,\n    ...(position ? { position } : {}),\n  };\n}\n\nfunction normalizePortBlockCollection(portBlocks: PortBlock[]): PortBlock[] {\n  const normalizedBlocks = portBlocks.map((block) => ({\n    ...block,\n    ports: block.ports.map((port) => {\n      const position = getNormalizedPortPosition(port.position);\n      return {\n        ...port,\n        ...(position ? { position } : { position: undefined }),\n      };\n    }),\n  }));\n\n  // A side without an offset is an automatic placement request. Distribute it\n  // across the whole face collection rather than restarting inside each block.\n  const portsByUnpositionedSide = new Map<\n    NonNullable<PortPosition['side']>,\n    Port[]\n  >();\n  normalizedBlocks.forEach((block) => {\n    block.ports.forEach((port) => {\n      const position = port.position as PortPosition | undefined;\n      const side = normalizePortAnchor(position?.side);\n      if (\n        !side ||\n        hasExplicitAbsoluteCoordinates(position) ||\n        hasExplicitOffset(position)\n      ) {\n        return;\n      }\n      const ports = portsByUnpositionedSide.get(side);\n      if (ports) {\n        ports.push(port);\n      } else {\n        portsByUnpositionedSide.set(side, [port]);\n      }\n    });\n  });\n\n  // These are owned output ports from the normalization pass above. Fill in\n  // automatic offsets in place instead of cloning every block a second time.\n  portsByUnpositionedSide.forEach((ports) => {\n    ports.forEach((port, index) => {\n      port.position = {\n        ...(port.position as PortPosition),\n        offsetPercent: toDistributedOffsetPercent(index, ports.length),\n      };\n    });\n  });\n\n  return normalizedBlocks;\n}\n\nfunction isGroupedPortBlockArray(value: unknown): value is PortBlock[] {\n  return (\n    Array.isArray(value) &&\n    value.length > 0 &&\n    value.every(\n      (entry) =>\n        entry !== null &&\n        typeof entry === 'object' &&\n        typeof (entry as PortBlock).name === 'string' &&\n        Array.isArray((entry as PortBlock).ports),\n    )\n  );\n}\n\nfunction isFlatDevicePortArray(value: unknown): value is DevicePort[] {\n  return (\n    Array.isArray(value) &&\n    value.length > 0 &&\n    value.every(\n      (entry) =>\n        entry !== null &&\n        typeof entry === 'object' &&\n        typeof (entry as DevicePort).label === 'string' &&\n        !(\n          typeof (entry as { name?: unknown }).name === 'string' &&\n          Array.isArray((entry as { ports?: unknown }).ports)\n        ),\n    )\n  );\n}\n\n/**\n * Normalize flat `DevicePort[]` input into explicit front/rear grouped blocks.\n * This is the bridge between the recommended simple port schema and the\n * lower-level grouped renderer input.\n * @public\n */\nexport function buildPortBlocksFromPorts(\n  devicePorts: DevicePort[],\n): { ports: PortBlock[]; rearPorts: PortBlock[] } {\n  const bucketsByKey = new Map<string, GroupedPortBucket>();\n  // Normalize each accepted port once. The placement, grouping and output\n  // passes share this representation rather than repeatedly parsing aliases.\n  const indexedPorts: IndexedPort[] = [];\n  devicePorts.forEach((port, index) => {\n    if (typeof port.label !== 'string' || port.label.trim().length === 0) return;\n    indexedPorts.push({\n      index,\n      port: { ...port, label: port.label.trim() },\n      position: getNormalizedPortPosition(port.position),\n    });\n  });\n  const distributedOffsetsByFace = {\n    front: buildDistributedOffsetMap(\n      indexedPorts.filter(({ port }) => resolveRenderPortFace(port) === 'front'),\n    ),\n    rear: buildDistributedOffsetMap(\n      indexedPorts.filter(({ port }) => resolveRenderPortFace(port) === 'rear'),\n    ),\n  };\n\n  indexedPorts.forEach((entry) => {\n    const face = resolveRenderPortFace(entry.port);\n    const groupName = resolveRenderPortGroupName(entry.port, face);\n    const key = `${face}:${groupName}`;\n    const existingBucket = bucketsByKey.get(key);\n    if (existingBucket) {\n      existingBucket.entries.push(entry);\n    } else {\n      bucketsByKey.set(key, { face, groupName, entries: [entry] });\n    }\n  });\n\n  const ports: PortBlock[] = [];\n  const rearPorts: PortBlock[] = [];\n\n  bucketsByKey.forEach((bucket) => {\n    const sortedEntries = [...bucket.entries].sort((left, right) => {\n      const orderDifference = getEntrySortValue(left) - getEntrySortValue(right);\n      if (orderDifference !== 0) {\n        return orderDifference;\n      }\n      return left.index - right.index;\n    });\n    const distributedOffsetByIndex = distributedOffsetsByFace[bucket.face];\n    const block: PortBlock = {\n      name: bucket.groupName,\n      ports: sortedEntries.map((entry) =>\n        toPort(entry, distributedOffsetByIndex.get(entry.index)),\n      ),\n    };\n    if (bucket.face === 'rear') {\n      rearPorts.push(block);\n      return;\n    }\n    ports.push(block);\n  });\n\n  return { ports, rearPorts };\n}\n\n/**\n * Node-data subset accepted by {@link getResolvedPortBlocks}: a flat\n * `DevicePort[]` on `ports`, grouped `PortBlock[]` on\n * `ports`/`portGroups`/`rearPortGroups`, or a `rearPorts` block fallback.\n * @public\n */\nexport type ResolvedPortBlockInput = Pick<\n  NetworkNode['data'],\n  'ports' | 'portGroups' | 'rearPortGroups'\n> & {\n  rearPorts?: PortBlock[];\n};\n\n/**\n * Resolve the grouped front/rear port blocks from any supported input form:\n * flat `ports: DevicePort[]`, grouped `PortBlock[]`, or explicit\n * `portGroups` / `rearPortGroups`.\n * @public\n */\nexport function getResolvedPortBlocks(\n  data: ResolvedPortBlockInput | undefined,\n): { ports: PortBlock[]; rearPorts: PortBlock[] } {\n  if (isGroupedPortBlockArray(data?.ports)) {\n    return {\n      ports: normalizePortBlockCollection(data.ports),\n      rearPorts: normalizePortBlockCollection(\n        Array.isArray(data?.rearPortGroups)\n          ? data.rearPortGroups\n          : isGroupedPortBlockArray(data?.rearPorts)\n            ? data.rearPorts\n            : [],\n      ),\n    };\n  }\n\n  if (isFlatDevicePortArray(data?.ports)) {\n    return buildPortBlocksFromPorts(data.ports);\n  }\n\n  return {\n    ports: normalizePortBlockCollection(Array.isArray(data?.portGroups) ? data.portGroups : []),\n    rearPorts: normalizePortBlockCollection(\n      Array.isArray(data?.rearPortGroups)\n        ? data.rearPortGroups\n        : isGroupedPortBlockArray(data?.rearPorts)\n          ? data.rearPorts\n          : [],\n    ),\n  };\n}\n\n/**\n * Resolve the front-facing grouped port blocks for a node/device data object.\n * @public\n */\nexport function getFrontPortBlocks(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): PortBlock[] {\n  return getResolvedPortBlocks(data).ports;\n}\n\n/**\n * Resolve the rear-facing grouped port blocks for a node/device data object.\n * @public\n */\nexport function getRearPortBlocks(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): PortBlock[] {\n  return getResolvedPortBlocks(data).rearPorts;\n}\n\nexport function countPortBlocks(portBlocks: PortBlock[] | undefined): number {\n  if (!Array.isArray(portBlocks)) {\n    return 0;\n  }\n\n  return portBlocks.reduce((total, block) => {\n    return total + (Array.isArray(block.ports) ? block.ports.length : 0);\n  }, 0);\n}\n\n/**\n * Determine whether a node/device data object resolves to any front ports.\n * @public\n */\nexport function hasFrontPorts(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): boolean {\n  return countPortBlocks(getResolvedPortBlocks(data).ports) > 0;\n}\n\n/**\n * Determine whether a node/device data object resolves to any rear ports.\n * @public\n */\nexport function hasRearPorts(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): boolean {\n  return countPortBlocks(getResolvedPortBlocks(data).rearPorts) > 0;\n}\n\n/**\n * Choose the default device face based on the resolved front/rear port data.\n * @public\n */\nexport function getDefaultDeviceFace(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): DeviceFace {\n  const hasFront = hasFrontPorts(data);\n  const hasRear = hasRearPorts(data);\n  return hasRear && !hasFront ? 'rear' : 'front';\n}\n","import type { DeviceFace, DevicePort, NetworkNodeDataBase, Port, PortBlock, PortCableType, PortType } from '../types';\nimport { buildPortHandleId, isPowerPortType, normalizeHandleId } from './handle-ids';\nimport { normalizePortPositionInput } from './port-position';\nimport { buildPortBlocksFromPorts, getResolvedPortBlocks } from './render-ports';\n\nconst DEFAULT_FRONT_GROUP_NAME = 'Network Ports';\nconst DEFAULT_REAR_GROUP_NAME = 'Rear Network Ports';\nconst DEFAULT_POWER_GROUP_NAME = 'Power Ports';\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst toText = (value: unknown): string | undefined => {\n  if (typeof value !== 'string') {\n    return undefined;\n  }\n  const trimmed = value.trim();\n  return trimmed.length > 0 ? trimmed : undefined;\n};\n\nconst normalizeFaceInput = (value: unknown): DeviceFace | undefined => {\n  const normalized = toText(value)?.toLowerCase();\n  if (normalized === 'front') {\n    return 'front';\n  }\n  if (normalized === 'rear' || normalized === 'back') {\n    return 'rear';\n  }\n  return undefined;\n};\n\n/**\n * Every IEC 60320 designation the mate table in `validation/connectors` knows.\n * Narrower whitelists silently dropped C15/C16/C21/C22, so those pairings could\n * never be checked.\n */\nconst CONNECTOR_TYPES: readonly NonNullable<DevicePort['connectorType']>[] = [\n  'C13',\n  'C14',\n  'C15',\n  'C16',\n  'C19',\n  'C20',\n  'C21',\n  'C22',\n];\n\nconst normalizeConnectorType = (\n  value: unknown,\n): NonNullable<DevicePort['connectorType']> | undefined => {\n  const normalized = toText(value)?.toUpperCase();\n  if (\n    normalized !== undefined &&\n    (CONNECTOR_TYPES as readonly string[]).includes(normalized)\n  ) {\n    return normalized as NonNullable<DevicePort['connectorType']>;\n  }\n  return undefined;\n};\n\nconst resolveDefaultFace = (portType: string | undefined): DeviceFace =>\n  isPowerPortType(portType) ? 'rear' : 'front';\n\nconst resolveDefaultGroup = ({\n  face,\n  type,\n}: {\n  face: DeviceFace;\n  type: string | undefined;\n}): string => {\n  if (isPowerPortType(type)) {\n    return DEFAULT_POWER_GROUP_NAME;\n  }\n  return face === 'rear' ? DEFAULT_REAR_GROUP_NAME : DEFAULT_FRONT_GROUP_NAME;\n};\n\n/**\n * Resolve the effective device face for a flat port input.\n * @public\n */\nexport function resolveDevicePortFace(\n  port: Pick<DevicePort, 'face' | 'type'>,\n): DeviceFace {\n  return normalizeFaceInput(port.face) ?? resolveDefaultFace(port.type);\n}\n\n/**\n * Resolve the effective port group name for a flat port input.\n * @public\n */\nexport function resolveDevicePortGroup(\n  port: Pick<DevicePort, 'face' | 'group' | 'type'>,\n): string {\n  return (\n    toText(port.group) ??\n    resolveDefaultGroup({\n      face: resolveDevicePortFace(port),\n      type: port.type,\n    })\n  );\n}\n\n/**\n * Normalize unknown **flat** port array input into canonical {@link DevicePort} entries.\n *\n * This handles a flat `DevicePort[]` only; grouped `PortBlock[]` input returns\n * `undefined` (blocks have no `label`). To resolve flat ports from any supported\n * form — including grouped `PortBlock[]` — use {@link resolveNodeDevicePorts}.\n * @public\n */\nexport function normalizeDevicePorts(value: unknown): DevicePort[] | undefined {\n  if (!Array.isArray(value)) {\n    return undefined;\n  }\n\n  const ports = value\n    .map((entry): DevicePort | null => {\n      if (!isRecord(entry)) {\n        return null;\n      }\n\n      const label = toText(entry.label);\n      if (!label) {\n        return null;\n      }\n\n      const type = toText(entry.type) as PortType | undefined;\n      const face = resolveDevicePortFace({\n        face: normalizeFaceInput(entry.face),\n        type,\n      });\n      const group = resolveDevicePortGroup({\n        face,\n        group: toText(entry.group),\n        type,\n      });\n      const position = normalizePortPositionInput(entry.position ?? entry);\n      const order =\n        typeof entry.order === 'number' && Number.isFinite(entry.order) ? entry.order : undefined;\n\n      const port: DevicePort = {\n        ...(toText(entry.id) ? { id: toText(entry.id) } : {}),\n        label,\n        face,\n        group,\n        connected: typeof entry.connected === 'boolean' ? entry.connected : undefined,\n        ...(type ? { type } : {}),\n        ...(toText(entry.cableType) ? { cableType: toText(entry.cableType) as PortCableType } : {}),\n        ...(normalizeConnectorType(entry.connectorType)\n          ? { connectorType: normalizeConnectorType(entry.connectorType) }\n          : {}),\n        ...(typeof order === 'number' ? { order } : {}),\n        ...(position ? { position } : {}),\n      };\n\n      return port;\n    })\n    .filter((entry): entry is DevicePort => entry !== null);\n\n  return ports.length > 0 ? ports : undefined;\n}\n\nfunction normalizePortBlocks(value: unknown, face: DeviceFace): DevicePort[] {\n  if (!Array.isArray(value)) {\n    return [];\n  }\n\n  return value.flatMap((entry) => {\n    if (!isRecord(entry) || !Array.isArray(entry.ports)) {\n      return [];\n    }\n\n    return entry.ports\n      .map((rawPort, index): DevicePort | null => {\n        if (!isRecord(rawPort)) {\n          return null;\n        }\n\n        const label = toText(rawPort.label);\n        if (!label) {\n          return null;\n        }\n\n        const type = toText(rawPort.type) as PortType | undefined;\n        const group = resolveDevicePortGroup({\n          face,\n          group: toText(entry.name),\n          type,\n        });\n        const position = normalizePortPositionInput(rawPort.position ?? rawPort);\n        const order =\n          typeof rawPort.order === 'number' && Number.isFinite(rawPort.order)\n            ? rawPort.order\n            : index;\n\n        return {\n          ...(toText(rawPort.id) ? { id: toText(rawPort.id) } : {}),\n          label,\n          face,\n          group,\n          connected: typeof rawPort.connected === 'boolean' ? rawPort.connected : undefined,\n          ...(type ? { type } : {}),\n          ...(toText(rawPort.cableType)\n            ? { cableType: toText(rawPort.cableType) as PortCableType }\n            : {}),\n          ...(normalizeConnectorType(rawPort.connectorType)\n            ? { connectorType: normalizeConnectorType(rawPort.connectorType) }\n            : {}),\n          order,\n          ...(position ? { position } : {}),\n        };\n      })\n      .filter((port): port is DevicePort => port !== null);\n  });\n}\n\n/**\n * Resolve flat device ports from any supported port input.\n *\n * Accepts every form the renderer accepts (`getResolvedPortBlocks`): a node-data\n * record (`ports` / `portGroups` / `rearPortGroups` / `rearPorts`), a flat\n * `DevicePort[]`, or a grouped `PortBlock[]`. Grouped `PortBlock[]` carried on\n * `ports` are treated as front groups (taking precedence over `portGroups`, matching\n * the renderer); rear groups come from `rearPortGroups`, falling back to `rearPorts`.\n * @public\n */\nexport function resolveNodeDevicePorts(value: unknown): DevicePort[] | undefined {\n  if (isRecord(value)) {\n    const flatPorts = normalizeDevicePorts(value.ports);\n    if (flatPorts) {\n      return flatPorts;\n    }\n\n    // `ports` may instead carry grouped PortBlock[] (front). It wins over\n    // `portGroups`, mirroring getResolvedPortBlocks' precedence.\n    const frontFromPorts = normalizePortBlocks(value.ports, 'front');\n    const frontPorts =\n      frontFromPorts.length > 0\n        ? frontFromPorts\n        : normalizePortBlocks(value.portGroups, 'front');\n    const rearFromGroups = normalizePortBlocks(value.rearPortGroups, 'rear');\n    const rearPorts =\n      rearFromGroups.length > 0\n        ? rearFromGroups\n        : normalizePortBlocks(value.rearPorts, 'rear');\n    const ports = [...frontPorts, ...rearPorts];\n    return ports.length > 0 ? ports : undefined;\n  }\n\n  // Bare array input: a flat DevicePort[] or a grouped PortBlock[] (front groups).\n  const flatPorts = normalizeDevicePorts(value);\n  if (flatPorts) {\n    return flatPorts;\n  }\n  const blockPorts = normalizePortBlocks(value, 'front');\n  return blockPorts.length > 0 ? blockPorts : undefined;\n}\n\n/**\n * Flatten grouped port blocks into canonical flat {@link DevicePort} entries.\n * @public\n */\nexport function flattenDevicePortsFromGroups({\n  portGroups,\n  rearPortGroups,\n}: {\n  portGroups?: PortBlock[];\n  rearPortGroups?: PortBlock[];\n}): DevicePort[] {\n  const ports: DevicePort[] = [];\n\n  const appendFace = (blocks: PortBlock[] | undefined, face: DeviceFace) => {\n    blocks?.forEach((block) => {\n      block.ports.forEach((port, index) => {\n        ports.push({\n          ...(typeof port.id === 'string' && port.id.trim().length > 0 ? { id: port.id } : {}),\n          label: port.label,\n          face,\n          group: block.name,\n          connected: port.connected,\n          ...(port.type ? { type: port.type } : {}),\n          ...(port.cableType ? { cableType: port.cableType } : {}),\n          ...(port.connectorType ? { connectorType: port.connectorType } : {}),\n          order: index,\n          ...(port.position ? { position: port.position } : {}),\n        });\n      });\n    });\n  };\n\n  appendFace(portGroups, 'front');\n  appendFace(rearPortGroups, 'rear');\n  return ports;\n}\n\n/**\n * Build grouped port blocks from flat device port input.\n * @public\n */\nexport function buildDevicePortGroupsFromPorts(\n  ports: DevicePort[] | undefined,\n): {\n  portGroups: PortBlock[];\n  rearPortGroups: PortBlock[];\n} {\n  if (!Array.isArray(ports) || ports.length === 0) {\n    return { portGroups: [], rearPortGroups: [] };\n  }\n\n  const { ports: portGroups, rearPorts: rearPortGroups } = buildPortBlocksFromPorts(ports);\n  return {\n    portGroups,\n    rearPortGroups,\n  };\n}\n\nexport interface ListPossiblePortHandleIdsOptions {\n  deviceType?: string;\n  face?: DeviceFace;\n}\n\n/**\n * List the **canonical** handle ids that may exist on a rendered node for a port.\n *\n * The returned ids are in canonical form. When a device renders both faces and a\n * front/rear handle id collides, the renderer disambiguates the hidden face with a\n * `--rear` / `--front` suffix (see `normalizeHandleId`). This function does not emit\n * those split-face aliases, so callers reconciling stored/rendered handles should\n * compare through {@link normalizeHandleId} (which strips the suffix) rather than by\n * raw string equality. {@link validatePortHandleOnNodeData} does this for you.\n * @public\n */\nexport function listPossiblePortHandleIds(\n  portBlockName: string,\n  port: Pick<Port, 'id' | 'label' | 'type'>,\n  options: ListPossiblePortHandleIdsOptions = {},\n): string[] {\n  const baseHandle = buildPortHandleId(portBlockName, port);\n  // Power-ness must be decided with the same rule the renderer and handle-id\n  // builder use (`isPowerPortType`, a substring match), otherwise this list\n  // diverges from the mounted handles for types like `power` / `power_dc` and\n  // reports a phantom `-target`/`-source` handle that was never rendered.\n  if (isPowerPortType(port.type)) {\n    return [baseHandle];\n  }\n\n  const normalizedDeviceType = String(options.deviceType ?? '').toLowerCase();\n  const face = options.face ?? 'front';\n\n  if (normalizedDeviceType === 'switch' || normalizedDeviceType === 'router') {\n    return [baseHandle, `${baseHandle}-target`];\n  }\n\n  if (normalizedDeviceType === 'patch-panel' || normalizedDeviceType === 'patch_panel') {\n    return face === 'rear'\n      ? [baseHandle, `${baseHandle}-target`]\n      : [baseHandle, `${baseHandle}-source`];\n  }\n\n  return [baseHandle];\n}\n\n/**\n * A rendered/stored handle id resolved back to the port that owns it.\n * @public\n */\nexport interface DescribedPortHandle {\n  /** The port the handle belongs to. */\n  port: DevicePort;\n  /** The port block (group) the port renders under. */\n  blockName: string;\n  /** The device face the block renders on. */\n  face: DeviceFace;\n}\n\n/**\n * Resolve a handle id back to the port that owns it — the inverse of\n * {@link buildPortHandleId} / {@link listPossiblePortHandleIds} over a node's\n * resolved port blocks. Directional suffixes and split-face aliases are\n * normalized before comparison (via {@link normalizeHandleId}), so ids taken\n * straight from edges match. Pass `options.sanitizeHandle` to match under an\n * additional app-side normalization (e.g. a `+`-insensitive comparison). Prefer\n * {@link handlesMatch} / {@link findEdgesForPortHandle} for whole-handle matching.\n * Returns `undefined` when no port on the node renders that handle.\n * @public\n */\nexport function describePortHandle(\n  nodeData: Pick<NetworkNodeDataBase, 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n  handle: string,\n  options: {\n    deviceType?: string;\n    sanitizeHandle?: (value: string) => string;\n  } = {},\n): DescribedPortHandle | undefined {\n  const sanitize = options.sanitizeHandle ?? ((value: string) => normalizeHandleId(value));\n  const normalizedRequestedHandle = sanitize(handle);\n  const { ports, rearPorts } = getResolvedPortBlocks(nodeData);\n\n  const findInBlocks = (blocks: PortBlock[], face: DeviceFace): DescribedPortHandle | undefined => {\n    for (const portBlock of blocks) {\n      for (const port of portBlock.ports) {\n        const possibleHandles = listPossiblePortHandleIds(portBlock.name, port, {\n          deviceType: options.deviceType,\n          face,\n        });\n        if (\n          possibleHandles.some(\n            (candidate) => sanitize(candidate) === normalizedRequestedHandle,\n          )\n        ) {\n          return { port, blockName: portBlock.name, face };\n        }\n      }\n    }\n    return undefined;\n  };\n\n  return findInBlocks(ports, 'front') ?? findInBlocks(rearPorts, 'rear');\n}\n\n/**\n * Validate that a handle id exists on node data after grouping resolution.\n * @public\n */\nexport function validatePortHandleOnNodeData(\n  nodeData: Pick<NetworkNodeDataBase, 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n  handle: string,\n  options: {\n    deviceType?: string;\n    sanitizeHandle?: (value: string) => string;\n  } = {},\n): boolean {\n  return describePortHandle(nodeData, handle, options) !== undefined;\n}\n\n/**\n * Convert flat ports to grouped blocks using the same rules as renderers.\n * @public\n */\nexport function resolveDevicePortBlocksFromFlatPorts(\n  ports: DevicePort[],\n): { portGroups: PortBlock[]; rearPortGroups: PortBlock[] } {\n  return buildDevicePortGroupsFromPorts(ports);\n}\n","import type { PortCableType } from '../types';\n\n/**\n * Connector/transceiver form-factors that carry fibre rather than copper.\n *\n * Optical modules (SFP/QSFP families) and bare fibre connectors (LC/SC/MPO)\n * classify as fibre; RJ45/copper form-factors do not.\n */\nconst FIBER_CONNECTOR_TYPES: ReadonlySet<string> = new Set([\n  'LC',\n  'SC',\n  'MPO',\n  'SFP',\n  'SFP+',\n  'SFP28',\n  'QSFP',\n  'QSFP+',\n  'QSFP28',\n  'QSFP56',\n  'QSFP-DD',\n]);\n\n/**\n * Classify a port/transceiver type string as fibre-bearing.\n *\n * Matching is case-insensitive and trims surrounding whitespace. Optical\n * transceiver families (SFP/SFP+/SFP28/QSFP/QSFP+/QSFP28/QSFP56/QSFP-DD) and\n * bare fibre connectors (LC/SC/MPO) return `true`; everything else (RJ45,\n * copper, power, unknown) returns `false`.\n *\n * Server-safe: pure data code with no react/reactflow dependency.\n * @public\n */\nexport function isFibreConnectorType(portType: string): boolean {\n  return FIBER_CONNECTOR_TYPES.has(String(portType ?? '').trim().toUpperCase());\n}\n\n/**\n * Coerce a free-form cable descriptor into a canonical {@link PortCableType}.\n *\n * Accepts common aliases used by inventory sources — e.g. `smf_os2`,\n * `single-mode`, `mmf_om4` — and normalizes them to the library's cable-type\n * vocabulary. Unrecognized values fall back to `cat6` when they look like a\n * copper category (`cat*`) and to `ethernet` otherwise. Nullish/blank input\n * resolves to `ethernet`.\n *\n * Server-safe: pure data code with no react/reactflow dependency.\n * @public\n */\nexport function toPortCableType(value: string): PortCableType {\n  const normalized = String(value ?? '')\n    .trim()\n    .toLowerCase();\n  switch (normalized) {\n    case 'smf':\n    case 'smf_os1':\n    case 'smf_os2':\n    case 'single-mode':\n    case 'single_mode':\n      return 'smf';\n    case 'om3':\n    case 'mmf_om3':\n      return 'om3';\n    case 'om4':\n    case 'mmf_om4':\n      return 'om4';\n    case 'om5':\n    case 'mmf_om5':\n      return 'om5';\n    case 'cat5e':\n    case 'cat6':\n    case 'cat6a':\n    case 'cat7':\n      return normalized as PortCableType;\n    case 'fiber':\n    case 'ethernet':\n      return normalized as PortCableType;\n    default:\n      return normalized.includes('cat') ? 'cat6' : 'ethernet';\n  }\n}\n","/**\n * Power connector utilities for consistent styling across devices and PDUs\n */\n\nexport interface PowerConnectorConfig {\n  type: 'C13' | 'C14' | 'C19' | 'C20' | 'AC';\n  color: string;\n  borderColor: string;\n  amperage: number;\n  voltage: number;\n  phases: number;\n}\n\nexport const POWER_CONNECTORS: Record<string, PowerConnectorConfig> = {\n  C13: {\n    type: 'C13',\n    color: '#f59e0b', // Orange\n    borderColor: '#d97706',\n    amperage: 10,\n    voltage: 230,\n    phases: 1\n  },\n  C14: {\n    type: 'C14',\n    color: '#f59e0b', // Orange\n    borderColor: '#d97706',\n    amperage: 10,\n    voltage: 230,\n    phases: 1\n  },\n  C19: {\n    type: 'C19',\n    color: '#fbbf24', // Amber\n    borderColor: '#d97706',\n    amperage: 16,\n    voltage: 230,\n    phases: 1\n  },\n  C20: {\n    type: 'C20',\n    color: '#fbbf24', // Amber\n    borderColor: '#d97706',\n    amperage: 16,\n    voltage: 230,\n    phases: 1\n  },\n  AC: {\n    type: 'AC',\n    color: '#60a5fa', // Blue (generic AC/NEMA)\n    borderColor: '#2563eb',\n    amperage: 15,\n    voltage: 120,\n    phases: 1\n  }\n};\n\nexport interface PowerPortStyle {\n  width: number;\n  height: number;\n  borderRadius?: string;\n  clipPath?: string;\n  color: string;\n  borderColor: string;\n  fontSize: string;\n}\n\n/**\n * Get consistent power port styling for any connector type\n */\nexport function getPowerPortStyle(connectorType: string): PowerPortStyle {\n  const connector = POWER_CONNECTORS[connectorType];\n  if (!connector) {\n    throw new Error(`Unknown connector type: ${connectorType}`);\n  }\n\n  const isHighPower = connector.amperage === 16; // C19/C20 are 16A\n  const isGenericAc = connector.type === 'AC';\n\n  // Size: C13/C14 are intentionally 30% larger while keeping C19/C20 unchanged.\n  const width = isHighPower ? 12 : isGenericAc ? 10 : 8 * 1.3;\n  const height = isHighPower ? 10 : isGenericAc ? 7 : 5 * 1.3;\n\n  // Shape: C13/C14 have chamfered corners, C19/C20 and generic AC use rounded corners.\n  const cornerStyle = isHighPower || isGenericAc\n    ? { borderRadius: isGenericAc ? '2px' : '3px' }\n    : {\n        clipPath: `polygon(2px 0, calc(100% - 2px) 0, 100% 2px, 100% 100%, 0 100%, 0 2px)`,\n        borderRadius: '0px'\n      };\n\n  return {\n    width,\n    height,\n    color: connector.color,\n    borderColor: connector.borderColor,\n    fontSize: isHighPower ? '3px' : isGenericAc ? '3.5px' : '4px',\n    ...cornerStyle\n  };\n}\n\n/**\n * Get connector configuration by type\n */\nexport function getConnectorConfig(connectorType: string): PowerConnectorConfig {\n  const connector = POWER_CONNECTORS[connectorType];\n  if (!connector) {\n    throw new Error(`Unknown connector type: ${connectorType}`);\n  }\n  return connector;\n}\n\n/**\n * Determine if a connector is high power (16A)\n */\nexport function isHighPowerConnector(connectorType: string): boolean {\n  const connector = getConnectorConfig(connectorType);\n  return connector.amperage === 16;\n}\n\n/** IEC 60320 designation embedded in a port `type` such as `power_c13`. */\nconst PORT_TYPE_CONNECTOR = /c(13|14|15|16|19|20|21|22)\\b/i;\n\n/**\n * Recover a connector designation from a port's `type` when the port carries no\n * explicit `connectorType` — `power_c13` becomes `C13`.\n *\n * The single implementation of that inference: the endpoint classifier\n * (`endpoint-metadata`), the validation graph and the renderer all read a\n * typed-only port through here, so a rule can never resolve a different\n * connector than the canvas drew.\n * @public\n */\nexport function inferConnectorFromPortType(portType: unknown): string | undefined {\n  if (typeof portType !== 'string') return undefined;\n  const match = PORT_TYPE_CONNECTOR.exec(portType);\n  return match ? `C${match[1]}` : undefined;\n}\n\n/**\n * Whether a PDU strip's data declares a supply inlet.\n *\n * The three fields the renderer reads before it mounts the inlet handle\n * (`inletConnector` / `inletAmps` / `inletGender`) — `feedType`, `voltage` and\n * `phases` only enrich the tooltip and are not an inlet on their own. Stated\n * once so the renderer, the canvas validator and PWR-A13 agree on when a strip\n * has somewhere for a branch circuit to land.\n * @public\n */\nexport function hasPduInlet(data: unknown): boolean {\n  if (!data || typeof data !== 'object') return false;\n  const { inletConnector, inletAmps, inletGender } = data as {\n    inletConnector?: unknown;\n    inletAmps?: unknown;\n    inletGender?: unknown;\n  };\n  if (typeof inletConnector === 'string' && inletConnector.trim().length > 0) return true;\n  if (typeof inletAmps === 'number' && Number.isFinite(inletAmps)) return true;\n  const gender =\n    typeof inletGender === 'string' ? inletGender.trim().toLowerCase() : undefined;\n  return gender === 'male' || gender === 'female';\n}\n\n/**\n * Get PDU port type based on position in cycle\n * PDU cycle: [8xC13, 2xC19] = 10 ports per cycle\n */\nexport function getPDUPortType(portIndex: number): string {\n  const cyclePosition = portIndex % 10; // 10 ports per cycle\n  return cyclePosition < 8 ? 'C13' : 'C19';\n}\n\n/**\n * Connector designation of a vertical-PDU outlet at a 1-based `position`.\n *\n * Prefers the strip's explicit `data.portTypes` sequence and falls back to the\n * default 8×C13 + 2×C19 cycle — the same precedence the renderer uses, so the\n * connector a rule reasons about is the connector that was drawn.\n * @public\n */\nexport function getPduOutletConnectorType(\n  portTypes: ReadonlyArray<string | undefined> | undefined,\n  position: number\n): string {\n  const index = Math.max(0, Math.floor(position) - 1);\n  const explicit = portTypes?.[index];\n  if (typeof explicit === 'string' && explicit.trim().length > 0) {\n    return explicit.trim().toUpperCase();\n  }\n  return getPDUPortType(index);\n}\n\n/**\n * Zero-based phase index of a PDU outlet at a 1-based `position` on an\n * `phases`-phase strip.\n *\n * Convention: outlets are attributed to phases **round-robin from the top** —\n * `(position - 1) % phases`. Real strips vary (some group outlets in per-phase\n * banks), but round-robin is the convention the consumer application uses, and\n * a phase-balance number is only meaningful if both sides attribute outlets the\n * same way. A non-positive or non-finite `phases` is read as single-phase, for\n * which every outlet is phase 0.\n * @public\n */\nexport function toOutletPhaseIndex(position: number, phases: number): number {\n  const count =\n    Number.isFinite(phases) && phases >= 1 ? Math.floor(phases) : 1;\n  const slot = Number.isFinite(position) ? Math.floor(position) : 1;\n  return (((slot - 1) % count) + count) % count;\n}\n\n/** Phase labels in round-robin order, indexed by {@link toOutletPhaseIndex}. */\nexport const PHASE_LABELS = ['L1', 'L2', 'L3'] as const;\n\n/**\n * Phase label (`L1`/`L2`/`L3`) of a PDU outlet at a 1-based `position`, or\n * `undefined` on a single-phase strip (where there is nothing to attribute) or\n * beyond the three labelled phases.\n * @public\n */\nexport function getPduOutletPhaseLabel(\n  position: number,\n  phases: number\n): (typeof PHASE_LABELS)[number] | undefined {\n  if (!Number.isFinite(phases) || phases < 2) return undefined;\n  return PHASE_LABELS[toOutletPhaseIndex(position, phases)];\n}\n\n/**\n * Get device connector type based on device name\n */\nexport function getDeviceConnectorType(deviceName: string): string {\n  const name = deviceName.toLowerCase();\n  \n  // High power devices use C20\n  if (name.includes('mx304') || name.includes('mx480') || name.includes('mx960')) {\n    return 'C20';\n  }\n  \n  // Standard devices use C14\n  return 'C14';\n}\n","import type {\n  DevicePort,\n  DiagramDocument,\n  DiagramEdge,\n  DiagramEdgeEndpoint,\n  DiagramEndpoint,\n  DiagramExternalRef,\n  DiagramNode,\n  DiagramNodeKind,\n  DiagramNodePlacement,\n  DiagramNodeRender,\n  DiagramUiState,\n  NetworkEdge,\n  NetworkNode,\n  NetworkNodeType,\n  Port,\n  PortBlock,\n  PortPosition,\n  PortPositionInput,\n  PortType,\n} from '../types';\nimport {\n  applyFaceAliasHandleId,\n  buildBreakerWayHandleId,\n  buildPortHandleId,\n  isPowerPortType,\n  normalizeDirectionalHandleId,\n  normalizeHandleId,\n  parseHandleId,\n} from './handle-ids';\nimport { normalizePortPositionInput } from './port-position';\nimport { getPDUPortType } from './power-connectors';\nimport { getResolvedPortBlocks } from './render-ports';\n\nexport interface NetworkGraphSnapshot {\n  nodes: NetworkNode[];\n  edges: NetworkEdge[];\n}\n\n/**\n * Hard ceiling on how many power outlets a PDU may allocate. A physical PDU has\n * at most a few dozen outlets; this generous cap keeps a hostile persisted\n * `powerPorts` (e.g. `2_000_000_000`) from triggering an unbounded\n * `Array.from({ length })` allocation on either the serialize path here or the\n * client render in {@link file://../components/VerticalPDU.tsx}. Both sinks\n * share this single constant.\n */\nexport const MAX_POWER_PORTS = 256;\n\n/**\n * Lenient input accepted by {@link diagramDocumentToNetworkGraph}. Everything is\n * optional so partial / legacy documents degrade gracefully instead of throwing:\n * a missing `version` is treated as legacy v0 (identical to `1`), and missing\n * `nodes`/`edges` default to empty. A full {@link DiagramDocument} is assignable.\n */\nexport type DiagramDocumentInput = {\n  version?: number;\n  nodes?: DiagramNode[];\n  edges?: DiagramEdge[];\n  ui?: DiagramUiState;\n  metadata?: Record<string, unknown>;\n};\n\nconst KNOWN_DIAGRAM_NODE_KINDS: ReadonlySet<DiagramNodeKind> = new Set<DiagramNodeKind>([\n  'rack',\n  'equipment',\n  'pdu',\n  'distribution-board',\n  'fiber',\n  'tube',\n  'cable',\n  'multi-tube-cable',\n  'splice',\n  'coupler',\n  'splitter',\n  'container',\n  'fibre-flow-cable',\n  'fibre-flow-closure',\n]);\n\nconst CONTAINER_SUBTYPES = new Set(['patch-panel', 'closure', 'splice-tray']);\nconst IMAGE_RENDER_KEYS = [\n  'frontImageUrl',\n  'rearImageUrl',\n  'frontImageAttribution',\n  'rearImageAttribution',\n  'imageFit',\n  'imagePosition',\n  'imageRepeat',\n  'stretchToFit',\n] as const;\nconst DIMENSION_RENDER_KEYS = [\n  'panelWidth',\n  'panelHeight',\n  'closureWidth',\n  'closureHeight',\n  'headerHeight',\n] as const;\nconst GENERAL_RENDER_KEYS = [\n  'status',\n  'width',\n  'widthPx',\n  'height',\n  'header',\n  'color',\n  'colorMode',\n  'showUMarkers',\n  'customHeaderText',\n  'powerSide',\n  'rotation',\n  'rackWidthPx',\n  'rackFaceGapPx',\n] as const;\nconst UI_NODE_STATE_KEYS = ['face', 'handleSideMap'] as const;\nconst EXCLUDED_CONFIG_KEYS = new Set<string>([\n  'label',\n  'ports',\n  'portGroups',\n  'rearPortGroups',\n  // `rearPorts` is a documented alias for `rearPortGroups` (resolved by\n  // getResolvedPortBlocks). Exclude it too, else a rearPorts-only node leaks its\n  // whole port array into the opaque config blob.\n  'rearPorts',\n  'pduPortGroups',\n  'connections',\n  'netboxId',\n  'netboxType',\n  'inventoryId',\n  'inventoryType',\n  'uPosition',\n  'mountMode',\n  'rackLayout',\n  'slotPosition',\n  'holderPosition',\n  // Derived on the reverse pass from placement.rack; never re-persist it into\n  // config or round-trips accumulate a stale copy.\n  'isRackMounted',\n  ...IMAGE_RENDER_KEYS,\n  ...DIMENSION_RENDER_KEYS,\n  ...GENERAL_RENDER_KEYS,\n  ...UI_NODE_STATE_KEYS,\n]);\n\n/**\n * Assign an own, enumerable property — safe for keys such as `__proto__` that\n * plain assignment would treat as a prototype mutation.\n */\nfunction setOwn(target: Record<string, unknown>, key: string, value: unknown): void {\n  Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction clonePosition(position: { x?: number; y?: number } | undefined): { x: number; y: number } {\n  // Number.isFinite rejects NaN/Infinity (which typeof reports as 'number'),\n  // so a malformed coordinate degrades to 0 instead of corrupting layout.\n  return {\n    x: Number.isFinite(position?.x) ? (position!.x as number) : 0,\n    y: Number.isFinite(position?.y) ? (position!.y as number) : 0,\n  };\n}\n\nfunction clonePortPosition(position: PortPositionInput | undefined): PortPosition | undefined {\n  const normalized = normalizePortPositionInput(position);\n  if (!normalized) {\n    return undefined;\n  }\n\n  return { ...normalized };\n}\n\nfunction toDiagramNodeKind(node: NetworkNode): { kind: DiagramNodeKind; subtype?: string } {\n  if (node.type === 'vertical-pdu') {\n    return { kind: 'pdu' };\n  }\n\n  if (node.type === 'server' || node.type === 'switch' || node.type === 'router' || node.type === 'device') {\n    // `subtype` carries the runtime node TYPE so the reverse pass restores it\n    // exactly. The free-text `data.deviceType` (often a vendor model, e.g.\n    // `Catalyst 9300`) travels in `config` like any other data field — using it\n    // as the subtype made a `switch` with a model name come back as `device`.\n    return { kind: 'equipment', subtype: node.type };\n  }\n\n  if (node.type === 'patch-panel' || node.type === 'closure' || node.type === 'splice-tray') {\n    return { kind: 'container', subtype: node.type };\n  }\n\n  if (node.type === 'fibre-split') {\n    return { kind: 'splitter' };\n  }\n\n  if (node.type === 'fibre-flow-cable' || node.type === 'fibre-flow-closure') {\n    return { kind: node.type };\n  }\n\n  return { kind: node.type as DiagramNodeKind };\n}\n\nfunction toLegacyNodeType(node: DiagramNode): NetworkNodeType {\n  if (node.kind === 'equipment') {\n    const normalizedSubtype = (node.subtype ?? 'device').toLowerCase().replace(/_/g, '-');\n    if (normalizedSubtype === 'server' || normalizedSubtype === 'switch' || normalizedSubtype === 'router') {\n      return normalizedSubtype;\n    }\n    return 'device';\n  }\n\n  if (node.kind === 'pdu') {\n    return 'vertical-pdu';\n  }\n\n  if (node.kind === 'container') {\n    const subtype = node.subtype ?? 'patch-panel';\n    if (CONTAINER_SUBTYPES.has(subtype)) {\n      return subtype as NetworkNodeType;\n    }\n    return 'patch-panel';\n  }\n\n  if (node.kind === 'splitter') {\n    return 'fibre-split';\n  }\n\n  return node.kind as NetworkNodeType;\n}\n\nfunction toDiagramPlacement(node: NetworkNode): DiagramNodePlacement {\n  const placement: DiagramNodePlacement = {\n    position: clonePosition(node.position),\n  };\n\n  const parentId = node.parentId;\n  if (parentId) {\n    placement.parentId = parentId;\n  }\n  if (node.extent === 'parent') {\n    placement.extent = 'parent';\n  }\n\n  const data = node.data ?? {};\n  if (\n    typeof data.uPosition === 'number' ||\n    data.mountMode === 'front' ||\n    data.mountMode === 'rear' ||\n    data.mountMode === 'both' ||\n    data.rackLayout === 'single-face' ||\n    data.rackLayout === 'split-face'\n  ) {\n    placement.rack = {\n      ...(typeof data.uPosition === 'number' ? { uPosition: data.uPosition } : {}),\n      ...(data.mountMode ? { mountMode: data.mountMode } : {}),\n      ...(data.rackLayout ? { rackLayout: data.rackLayout } : {}),\n      ...(typeof data.rackWidthPx === 'number' ? { rackWidthPx: data.rackWidthPx } : {}),\n      ...(typeof data.rackFaceGapPx === 'number' ? { rackFaceGapPx: data.rackFaceGapPx } : {}),\n    };\n  }\n\n  if (typeof data.slotPosition === 'number' || typeof data.holderPosition === 'number') {\n    placement.container = {\n      ...(typeof data.slotPosition === 'number' ? { slotPosition: data.slotPosition } : {}),\n      ...(typeof data.holderPosition === 'number' ? { holderPosition: data.holderPosition } : {}),\n    };\n  }\n\n  return placement;\n}\n\nfunction toDiagramRender(node: NetworkNode): DiagramNodeRender | undefined {\n  const data = node.data ?? {};\n  const render: DiagramNodeRender = {};\n\n  GENERAL_RENDER_KEYS.forEach((key) => {\n    const value = data[key];\n    if (value !== undefined) {\n      (render as Record<string, unknown>)[key] = value;\n    }\n  });\n\n  const images = IMAGE_RENDER_KEYS.reduce<Record<string, unknown>>((acc, key) => {\n    const value = data[key];\n    if (value !== undefined) {\n      acc[key] = value;\n    }\n    return acc;\n  }, {});\n  if (Object.keys(images).length > 0) {\n    render.images = images as DiagramNodeRender['images'];\n  }\n\n  const dimensions = DIMENSION_RENDER_KEYS.reduce<Record<string, number | undefined>>((acc, key) => {\n    const value = data[key];\n    if (typeof value === 'number') {\n      acc[key] = value;\n    }\n    return acc;\n  }, {});\n  if (Object.keys(dimensions).length > 0) {\n    render.dimensions = dimensions;\n  }\n\n  return Object.keys(render).length > 0 ? render : undefined;\n}\n\nfunction toDiagramExternalRefs(node: NetworkNode): DiagramExternalRef[] | undefined {\n  const refs: DiagramExternalRef[] = [];\n  const data = node.data ?? {};\n\n  if (data.netboxId !== undefined) {\n    refs.push({\n      source: 'netbox',\n      id: data.netboxId,\n      type: data.netboxType,\n    });\n  }\n\n  if (data.inventoryId !== undefined) {\n    refs.push({\n      source: 'inventory',\n      id: data.inventoryId,\n      type: data.inventoryType,\n    });\n  }\n\n  return refs.length > 0 ? refs : undefined;\n}\n\nfunction toDiagramConfig(node: NetworkNode): Record<string, unknown> | undefined {\n  const data = node.data ?? {};\n  const config: Record<string, unknown> = {};\n\n  Object.entries(data).forEach(([key, value]) => {\n    // `ports` is excluded because port ARRAYS serialize as endpoints. A numeric\n    // `ports` (the rack header's port count) has no endpoint form, so it is\n    // kept as plain config instead of being dropped.\n    if (EXCLUDED_CONFIG_KEYS.has(key) && !(key === 'ports' && !Array.isArray(value))) {\n      return;\n    }\n    if ((key === 'rackWidthPx' || key === 'rackFaceGapPx') && node.type !== 'rack') {\n      return;\n    }\n    setOwn(config, key, value);\n  });\n\n  // Equipment without a device type is normalised to `deviceType = node.type`,\n  // the value the reverse pass has always filled in. Writing it here keeps\n  // re-serialisation idempotent (graph → doc → graph → doc is stable).\n  if (\n    (node.type === 'server' || node.type === 'switch' || node.type === 'router' || node.type === 'device') &&\n    !(typeof config.deviceType === 'string' && config.deviceType.trim().length > 0)\n  ) {\n    config.deviceType = node.type;\n  }\n\n  return Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction extractUiState(nodes: NetworkNode[], previousUi?: DiagramUiState): DiagramUiState | undefined {\n  const nodeState: NonNullable<DiagramUiState['nodeState']> = {};\n\n  nodes.forEach((node) => {\n    const data = node.data ?? {};\n    const entry: NonNullable<DiagramUiState['nodeState']>[string] = {};\n    const resolvedFace =\n      data.face === 'front' || data.face === 'rear'\n        ? data.face\n        : undefined;\n\n    if (resolvedFace) {\n      entry.face = resolvedFace;\n    }\n    if (data.handleSideMap && typeof data.handleSideMap === 'object') {\n      entry.handleSideMap = { ...data.handleSideMap };\n    }\n\n    if (Object.keys(entry).length > 0) {\n      setOwn(nodeState, node.id, entry);\n    }\n  });\n\n  if (Object.keys(nodeState).length === 0 && !previousUi?.metadata) {\n    return undefined;\n  }\n\n  return {\n    ...(Object.keys(nodeState).length > 0 ? { nodeState } : {}),\n    ...(previousUi?.metadata ? { metadata: previousUi.metadata } : {}),\n  };\n}\n\nfunction inferEndpointMedium(portType: string | undefined): DiagramEndpoint['medium'] {\n  const normalizedType = String(portType ?? '').toLowerCase();\n  if (normalizedType.includes('power')) return 'power';\n  if (normalizedType.includes('fiber')) return 'fiber';\n  if (normalizedType.includes('console') || normalizedType.includes('mgmt') || normalizedType.includes('usb')) {\n    return 'generic';\n  }\n  if (normalizedType.includes('ethernet') || normalizedType.includes('rj45')) return 'ethernet';\n  return 'generic';\n}\n\nfunction buildPduPortBlocks(node: NetworkNode): PortBlock[] {\n  const data = node.data ?? {};\n  if (Array.isArray(data.pduPortGroups) && data.pduPortGroups.length > 0) {\n    return data.pduPortGroups;\n  }\n\n  const { ports, rearPorts } = getResolvedPortBlocks(data);\n  if (rearPorts.length > 0) {\n    return rearPorts;\n  }\n  if (ports.length > 0) {\n    return ports;\n  }\n\n  const powerPorts = Math.min(\n    MAX_POWER_PORTS,\n    typeof data.powerPorts === 'number' && Number.isFinite(data.powerPorts) && data.powerPorts > 0\n      ? data.powerPorts\n      : 36\n  );\n\n  return [\n    {\n      name: 'Power',\n      ports: Array.from({ length: powerPorts }, (_, index) => {\n        const connectorType = getPDUPortType(index);\n        return {\n          id: `${index + 1}`,\n          label: `${index + 1}`,\n          connected: false,\n          type: connectorType === 'C19' ? 'power_c19' : 'power_c13',\n          connectorType: connectorType as Port['connectorType'],\n        };\n      }),\n    },\n  ];\n}\n\nfunction toPortBasedEndpoints(node: NetworkNode): DiagramEndpoint[] {\n  // Read the resolved blocks directly. Endpoint construction below already\n  // clones compatibility and normalized layout, so an intermediate deep clone\n  // of every block/port only duplicates work on each document round trip.\n  const blocksByFace: Array<{ face: 'front' | 'rear'; blocks: PortBlock[] }> =\n    node.type === 'vertical-pdu'\n      ? [{ face: 'rear', blocks: buildPduPortBlocks(node) }]\n      : (() => {\n          const { ports, rearPorts } = getResolvedPortBlocks(node.data);\n          return [\n            { face: 'front' as const, blocks: ports },\n            { face: 'rear' as const, blocks: rearPorts },\n          ];\n        })();\n\n  const endpoints: DiagramEndpoint[] = blocksByFace.flatMap(({ face, blocks }) =>\n    blocks.flatMap((block) =>\n      block.ports.map((port) => ({\n        // Mint the endpoint id through the same grammar the renderer mounts\n        // handles with ({@link buildPortHandleId}): an explicit `port.id`\n        // verbatim, a bare label for power ports, else a percent-escaped\n        // `<group>-<label>` join. Edges persist\n        // `endpointId = normalizeHandleId(edge.sourceHandle)` — i.e. this exact\n        // builder output — so a raw `${block.name}-${port.label}` join would\n        // diverge for any label/group containing `-` or `%` (or a label that\n        // collides with a fixed grammar shape, e.g. group `fiber` + label `3`),\n        // and the edge would dangle after a document round-trip. Building the id\n        // the same way keeps persisted endpoint ids and edge endpointIds equal\n        // by construction.\n        id: buildPortHandleId(block.name, port),\n        label: port.label,\n        face,\n        group: block.name,\n        role: isPowerPortType(port.type) ? 'power' : 'network',\n        medium: inferEndpointMedium(port.type),\n        type: port.type,\n        cableType: port.cableType,\n        connector: port.connectorType,\n        sourceTypeLabel: port.sourceTypeLabel,\n        compatibility: Array.isArray(port.compatibility) ? [...port.compatibility] : undefined,\n        layout: clonePortPosition(port.position),\n      }))\n    )\n  );\n\n  // A front and a rear port that resolve to the same handle id (same explicit\n  // `port.id`, or same group + label on both faces) are disambiguated by the\n  // renderer with the split-face alias: the rear handle mounts as\n  // `<id>--rear`. Mint the rear endpoint with that same alias so endpoint ids\n  // stay unique and an edge on the rear port keeps pointing at the rear port\n  // after a round trip (instead of collapsing onto the front one).\n  const frontIds = new Set(\n    endpoints.filter((endpoint) => endpoint.face === 'front').map((endpoint) => endpoint.id)\n  );\n  if (frontIds.size > 0) {\n    for (const endpoint of endpoints) {\n      if (endpoint.face === 'rear' && frontIds.has(endpoint.id)) {\n        endpoint.id = applyFaceAliasHandleId(endpoint.id, 'rear');\n      }\n    }\n  }\n  return endpoints;\n}\n\n/**\n * Whether `endpoint` is the rear twin of a front endpoint, minted by\n * {@link toPortBasedEndpoints} as `<frontId>--rear`. Returns the shared base id\n * (what the port's own `id` is) when it is, else `undefined`.\n */\nfunction rearAliasBase(\n  endpoint: DiagramEndpoint,\n  frontIds: ReadonlySet<string>\n): string | undefined {\n  if (endpoint.face !== 'rear' || !endpoint.id.endsWith('--rear')) return undefined;\n  const base = endpoint.id.slice(0, -'--rear'.length);\n  return frontIds.has(base) ? base : undefined;\n}\n\nfunction collectFrontEndpointIds(endpoints: readonly DiagramEndpoint[]): Set<string> {\n  return new Set(\n    endpoints.filter((endpoint) => endpoint.face !== 'rear').map((endpoint) => endpoint.id)\n  );\n}\n\nfunction createSyntheticEndpoints(node: NetworkNode): DiagramEndpoint[] {\n  const data = node.data ?? {};\n\n  if (node.type === 'distribution-board') {\n    const ways = Array.isArray(data.ways) ? data.ways : [];\n    return ways\n      .filter((way): way is { id: string; label?: string } => !!way && typeof way.id === 'string')\n      .map((way) => ({\n        id: buildBreakerWayHandleId(way.id),\n        label: typeof way.label === 'string' ? way.label : way.id,\n        role: 'power' as const,\n        medium: 'power' as const,\n      }));\n  }\n\n  if (node.type === 'tube') {\n    const fibersPerTube = Math.max(1, Math.min(24, Number(data.fibersPerTube) || 12));\n    const tubeIndex = Math.max(1, Number(data.tubeIndex) || 1);\n    return [\n      ...Array.from({ length: fibersPerTube }, (_, index) => ({\n        id: `fiber-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'fiber' as const,\n        medium: 'fiber' as const,\n      })),\n      {\n        id: `tube-${tubeIndex}`,\n        label: `Tube ${tubeIndex}`,\n        role: 'tube',\n        medium: 'tube',\n      },\n    ];\n  }\n\n  if (node.type === 'cable') {\n    const fiberCount = Math.max(1, Math.min(24, Number(data.fiberCount) || 12));\n    return [\n      ...Array.from({ length: fiberCount }, (_, index) => ({\n        id: `fiber-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'fiber' as const,\n        medium: 'fiber' as const,\n      })),\n      {\n        id: 'cable-button',\n        label: 'Cable',\n        role: 'cable',\n      },\n    ];\n  }\n\n  if (node.type === 'multi-tube-cable') {\n    const tubeCount = Math.max(1, Math.min(24, Number(data.tubeCount) || 12));\n    const tubeStartIndex = Math.max(1, Number(data.tubeStartIndex) || 1);\n    return [\n      ...Array.from({ length: tubeCount }, (_, index) => ({\n        id: `tube-${tubeStartIndex + index}`,\n        label: `${tubeStartIndex + index}`,\n        role: 'tube' as const,\n        medium: 'tube' as const,\n      })),\n      {\n        id: 'cable-button',\n        label: 'Cable',\n        role: 'cable',\n      },\n    ];\n  }\n\n  if (node.type === 'splice') {\n    return [\n      { id: 'left', label: 'Left', role: 'splice', medium: 'fiber' },\n      { id: 'right', label: 'Right', role: 'splice', medium: 'fiber' },\n    ];\n  }\n\n  if (node.type === 'coupler') {\n    const portCount = typeof data.couplerFiberCount === 'number' && [1, 2, 4].includes(data.couplerFiberCount)\n      ? data.couplerFiberCount\n      : 2;\n    return [\n      ...Array.from({ length: portCount }, (_, index) => ({\n        id: `left-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'coupler' as const,\n        medium: 'fiber' as const,\n      })),\n      ...Array.from({ length: portCount }, (_, index) => ({\n        id: `right-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'coupler' as const,\n        medium: 'fiber' as const,\n      })),\n    ];\n  }\n\n  if (node.type === 'fibre-split') {\n    const inputCount = Math.max(1, Math.min(2, Number(data.inputCount) || 1));\n    const outputCount = Math.max(1, Math.min(64, Number(data.outputCount) || 8));\n    return [\n      ...Array.from({ length: inputCount }, (_, index) => ({\n        id: `input-${index + 1}`,\n        label: `Input ${index + 1}`,\n        role: 'splitter' as const,\n        medium: 'fiber' as const,\n      })),\n      ...Array.from({ length: outputCount }, (_, index) => ({\n        id: `output-${index + 1}`,\n        label: `Output ${index + 1}`,\n        role: 'splitter' as const,\n        medium: 'fiber' as const,\n      })),\n    ];\n  }\n\n  return [];\n}\n\nfunction toDiagramEndpoints(node: NetworkNode): DiagramEndpoint[] | undefined {\n  const endpoints = node.type === 'vertical-pdu' || Array.isArray(node.data?.ports) || Array.isArray(node.data?.portGroups) || Array.isArray(node.data?.rearPortGroups) || Array.isArray(node.data?.rearPorts) || Array.isArray(node.data?.pduPortGroups)\n    ? toPortBasedEndpoints(node)\n    : createSyntheticEndpoints(node);\n\n  return endpoints.length > 0 ? endpoints : undefined;\n}\n\n/**\n * `node.data` fields the endpoint derivation reads. The cache fingerprints the\n * identity (and, for arrays, the length) of each, so replacing or growing a\n * port collection — even on the same `data` object — invalidates the entry.\n */\nconst ENDPOINT_SOURCE_KEYS = [\n  'ports',\n  'portGroups',\n  'rearPortGroups',\n  'rearPorts',\n  'pduPortGroups',\n  'powerPorts',\n  'ways',\n  'fibersPerTube',\n  'tubeIndex',\n  'fiberCount',\n  'tubeCount',\n  'tubeStartIndex',\n  'couplerFiberCount',\n  'inputCount',\n  'outputCount',\n] as const;\n\nfunction endpointSourceFingerprint(data: Record<string, unknown>): unknown[] {\n  const fingerprint: unknown[] = [];\n  for (const key of ENDPOINT_SOURCE_KEYS) {\n    const value = data[key];\n    fingerprint.push(value, Array.isArray(value) ? value.length : -1);\n  }\n  return fingerprint;\n}\n\nfunction sameFingerprint(a: readonly unknown[], b: readonly unknown[]): boolean {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i += 1) {\n    if (!Object.is(a[i], b[i])) return false;\n  }\n  return true;\n}\n\n/**\n * One derivation of a node's endpoints, cached against the `node.data` object\n * that produced it. `nodeId`/`nodeType` are stored alongside so a data object\n * shared by two different nodes (a hazard, not the norm) is detected and\n * recomputed rather than aliased, and `fingerprint` catches an in-place\n * replacement of a port collection on the same data object.\n */\ninterface EndpointDerivation {\n  nodeId: string;\n  nodeType: NetworkNodeType;\n  fingerprint: unknown[];\n  endpoints: DiagramEndpoint[] | undefined;\n}\n\n/**\n * Module-scoped memo of {@link toDiagramEndpoints}, keyed on `node.data`\n * identity. A position-only drag reframes `node.position` but keeps `node.data`\n * referentially stable, so the (potentially large) endpoint derivation runs\n * once and later frames only copy the cached records. Keys are consumer\n * `node.data` objects, so entries die with them (no unbounded growth).\n *\n * Limitation: editing a port object's fields in place (e.g. `port.label = …`\n * on the same port object inside the same array) is not detected — replace\n * the port, its array, or `data` (the React immutable-update convention).\n */\nconst endpointDerivationCache = new WeakMap<object, EndpointDerivation>();\n\n/** Detached copy of an endpoint list so callers can never mutate the cache. */\nfunction copyEndpoints(endpoints: DiagramEndpoint[] | undefined): DiagramEndpoint[] | undefined {\n  return endpoints?.map((endpoint) => ({\n    ...endpoint,\n    ...(endpoint.compatibility ? { compatibility: [...endpoint.compatibility] } : {}),\n    ...(endpoint.layout ? { layout: { ...endpoint.layout } } : {}),\n  }));\n}\n\n/**\n * Memoized {@link toDiagramEndpoints}. Recomputes on any data-identity change,\n * on a port-collection change detected by the fingerprint, and when the same\n * data object surfaces under a different node id/type. Always returns a fresh\n * copy, so a consumer mutating an emitted document cannot corrupt later ones.\n * Nodes with no object `data` bypass the cache (nothing stable to key on).\n */\nfunction toDiagramEndpointsCached(node: NetworkNode): DiagramEndpoint[] | undefined {\n  const data: unknown = node.data;\n  if (!data || typeof data !== 'object') {\n    return toDiagramEndpoints(node);\n  }\n\n  const fingerprint = endpointSourceFingerprint(data as Record<string, unknown>);\n  const cached = endpointDerivationCache.get(data);\n  if (\n    cached &&\n    cached.nodeId === node.id &&\n    cached.nodeType === node.type &&\n    sameFingerprint(cached.fingerprint, fingerprint)\n  ) {\n    return copyEndpoints(cached.endpoints);\n  }\n\n  const endpoints = toDiagramEndpoints(node);\n  endpointDerivationCache.set(data, { nodeId: node.id, nodeType: node.type, fingerprint, endpoints });\n  return copyEndpoints(endpoints);\n}\n\nfunction mergeEndpoints(\n  previousEndpoints: DiagramEndpoint[] | undefined,\n  nextEndpoints: DiagramEndpoint[] | undefined\n): DiagramEndpoint[] | undefined {\n  if (!nextEndpoints || nextEndpoints.length === 0) {\n    return nextEndpoints;\n  }\n\n  const previousById = new Map((previousEndpoints ?? []).map((endpoint) => [endpoint.id, endpoint]));\n  return nextEndpoints.map((endpoint) => {\n    const previous = previousById.get(endpoint.id);\n    if (!previous) {\n      return endpoint;\n    }\n\n    return {\n      ...previous,\n      ...endpoint,\n      compatibility: endpoint.compatibility ?? previous.compatibility,\n      layout: endpoint.layout ?? previous.layout,\n    };\n  });\n}\n\nfunction mergeDocumentNode(previousNode: DiagramNode | undefined, nextNode: DiagramNode): DiagramNode {\n  if (!previousNode) {\n    return nextNode;\n  }\n\n  return {\n    ...previousNode,\n    ...nextNode,\n    placement: nextNode.placement,\n    render: nextNode.render,\n    config: nextNode.config,\n    externalRefs: nextNode.externalRefs,\n    metadata: nextNode.metadata ?? previousNode.metadata,\n    endpoints: mergeEndpoints(previousNode.endpoints, nextNode.endpoints),\n  };\n}\n\n/**\n * Convert a runtime {@link NetworkGraphSnapshot} into the canonical, persistable\n * {@link DiagramDocument}. The emitted document always carries `version: 1`.\n *\n * Container kinds (`patch-panel`/`closure`/`splice-tray`) serialize their ports\n * as face-tagged `endpoints`, which {@link diagramDocumentToNetworkGraph}\n * reconstructs back into `portGroups`/`rearPortGroups`.\n */\nexport function networkGraphToDiagramDocument(\n  snapshot: NetworkGraphSnapshot,\n  previousDocument?: DiagramDocument\n): DiagramDocument {\n  const previousNodesById = new Map((previousDocument?.nodes ?? []).map((node) => [node.id, node]));\n\n  const nodes = snapshot.nodes.map((node) =>\n    mergeDocumentNode(previousNodesById.get(node.id), {\n      id: node.id,\n      ...toDiagramNodeKind(node),\n      label: node.data?.label ?? node.id,\n      placement: toDiagramPlacement(node),\n      render: toDiagramRender(node),\n      config: toDiagramConfig(node),\n      externalRefs: toDiagramExternalRefs(node),\n      metadata: previousNodesById.get(node.id)?.metadata,\n      endpoints: toDiagramEndpointsCached(node),\n    })\n  );\n\n  // Per node: the endpoint ids that exist on both faces, i.e. whose rear twin\n  // was minted as `<id>--rear` (see toPortBasedEndpoints).\n  const aliasedIdsByNodeId = new Map<string, Set<string>>();\n  nodes.forEach((node) => {\n    const endpoints = node.endpoints ?? [];\n    const frontIds = collectFrontEndpointIds(endpoints);\n    const aliased = new Set<string>();\n    for (const endpoint of endpoints) {\n      const base = rearAliasBase(endpoint, frontIds);\n      if (base !== undefined) aliased.add(base);\n    }\n    if (aliased.size > 0) aliasedIdsByNodeId.set(node.id, aliased);\n  });\n\n  /**\n   * Endpoint id an edge handle persists as: the handle with its directional\n   * suffix and split-face alias stripped — except a `--rear` alias on a port id\n   * that exists on both faces, which names the rear twin endpoint.\n   */\n  const toEndpointId = (nodeId: string, handle: string | null | undefined): string | undefined => {\n    if (!handle) return undefined;\n    const normalized = normalizeHandleId(handle);\n    const aliased = aliasedIdsByNodeId.get(nodeId);\n    if (aliased?.has(normalized) && parseHandleId(normalizeDirectionalHandleId(handle)).faceAlias === 'rear') {\n      return applyFaceAliasHandleId(normalized, 'rear');\n    }\n    return normalized;\n  };\n\n  const previousEdgesById = new Map((previousDocument?.edges ?? []).map((edge) => [edge.id, edge]));\n  const edges = snapshot.edges.map((edge) => {\n    const nextEdge: DiagramEdge = {\n      id: edge.id,\n      source: {\n        nodeId: edge.source,\n        endpointId: toEndpointId(edge.source, edge.sourceHandle),\n      },\n      target: {\n        nodeId: edge.target,\n        endpointId: toEndpointId(edge.target, edge.targetHandle),\n      },\n      type: edge.type,\n      hidden: edge.hidden === true ? true : undefined,\n      deletable: typeof edge.deletable === 'boolean' ? edge.deletable : undefined,\n      interactionWidth:\n        typeof edge.interactionWidth === 'number' && Number.isFinite(edge.interactionWidth)\n          ? edge.interactionWidth\n          : undefined,\n      data: edge.data ? { ...edge.data } : undefined,\n      metadata: previousEdgesById.get(edge.id)?.metadata,\n    };\n\n    const previous = previousEdgesById.get(edge.id);\n    return previous ? { ...previous, ...nextEdge } : nextEdge;\n  });\n\n  return {\n    version: 1,\n    nodes,\n    edges,\n    ui: extractUiState(snapshot.nodes, previousDocument?.ui),\n    metadata: previousDocument?.metadata,\n  };\n}\n\n/**\n * Port type for a rebuilt port. An endpoint's own `type` wins. Without one,\n * only power endpoints get an inferred type (their connector family drives\n * power semantics); every other untyped endpoint stays untyped — inventing\n * `ethernet` changed the port's icon and compatibility on every load and made\n * re-serialisation non-idempotent.\n */\nfunction endpointToPortType(endpoint: DiagramEndpoint): PortType | undefined {\n  if (endpoint.type) {\n    return endpoint.type;\n  }\n\n  if (endpoint.role === 'power' || endpoint.medium === 'power') {\n    const connector = String(endpoint.connector ?? '').toLowerCase();\n    if (connector.includes('c19') || connector.includes('c20')) return 'power_c19';\n    if (connector.includes('c13') || connector.includes('c14')) return 'power_c13';\n    return 'power_ac';\n  }\n\n  // Legacy hand-authored documents that name a medium but no type.\n  if (endpoint.medium === 'fiber') return 'fiber';\n  if (endpoint.medium === 'ethernet') return 'ethernet';\n  return undefined;\n}\n\n/** Shared per-port fields of a rebuilt {@link DevicePort} / {@link Port}. */\nfunction endpointPortFields(\n  endpoint: DiagramEndpoint,\n  frontIds: ReadonlySet<string>,\n  connectedEndpointIds: ReadonlySet<string>\n) {\n  const type = endpointToPortType(endpoint);\n  return {\n    // The rear twin of a both-faces port id was persisted as `<id>--rear`; the\n    // port itself keeps the shared id (the renderer re-applies the alias).\n    id: rearAliasBase(endpoint, frontIds) ?? endpoint.id,\n    label: endpoint.label,\n    connected: connectedEndpointIds.has(endpoint.id),\n    ...(type ? { type } : {}),\n    cableType: endpoint.cableType,\n    connectorType: endpoint.connector as DevicePort['connectorType'],\n    sourceTypeLabel: endpoint.sourceTypeLabel,\n    compatibility:\n      endpoint.compatibility && endpoint.compatibility.length > 0\n        ? [...endpoint.compatibility]\n        : undefined,\n    position: clonePortPosition(endpoint.layout),\n  };\n}\n\nfunction endpointsToDevicePorts(\n  endpoints: DiagramEndpoint[],\n  frontIds: ReadonlySet<string>,\n  connectedEndpointIds: ReadonlySet<string>\n): DevicePort[] {\n  return endpoints.map((endpoint) => {\n    const fields = endpointPortFields(endpoint, frontIds, connectedEndpointIds);\n    return {\n      ...fields,\n      face: endpoint.face === 'rear' ? 'rear' : endpoint.face === 'front' ? 'front' : undefined,\n      group: endpoint.group,\n    } as DevicePort;\n  });\n}\n\nfunction endpointsToPortBlocks(\n  endpoints: DiagramEndpoint[],\n  frontIds: ReadonlySet<string>,\n  connectedEndpointIds: ReadonlySet<string>\n): PortBlock[] {\n  const grouped = new Map<string, Port[]>();\n\n  endpoints.forEach((endpoint) => {\n    const groupName = endpoint.group ?? 'Ports';\n    const port = endpointPortFields(endpoint, frontIds, connectedEndpointIds) as Port;\n    const existing = grouped.get(groupName);\n    if (existing) {\n      existing.push(port);\n      return;\n    }\n    grouped.set(groupName, [port]);\n  });\n\n  return [...grouped.entries()].map(([name, ports]) => ({ name, ports }));\n}\n\nfunction applyRenderToLegacyData(\n  data: NetworkNode['data'],\n  render: DiagramNodeRender | undefined,\n  uiEntry: NonNullable<DiagramUiState['nodeState']>[string] | undefined\n): NetworkNode['data'] {\n  if (!render && !uiEntry) {\n    return data;\n  }\n\n  const nextData = { ...data };\n  if (render) {\n    GENERAL_RENDER_KEYS.forEach((key) => {\n      const value = render[key];\n      if (value !== undefined) {\n        (nextData as Record<string, unknown>)[key] = value;\n      }\n    });\n\n    if (render.images) {\n      IMAGE_RENDER_KEYS.forEach((key) => {\n        const value = render.images?.[key];\n        if (value !== undefined) {\n          (nextData as Record<string, unknown>)[key] = value;\n        }\n      });\n    }\n\n    if (render.dimensions) {\n      DIMENSION_RENDER_KEYS.forEach((key) => {\n        const value = render.dimensions?.[key];\n        if (typeof value === 'number') {\n          (nextData as Record<string, unknown>)[key] = value;\n        }\n      });\n    }\n  }\n\n  const resolvedFace = uiEntry?.face;\n  if (resolvedFace) {\n    nextData.face = resolvedFace;\n  }\n  if (uiEntry?.handleSideMap) {\n    nextData.handleSideMap = { ...uiEntry.handleSideMap };\n  }\n\n  return nextData;\n}\n\nfunction applyExternalRefsToLegacyData(data: NetworkNode['data'], refs: DiagramExternalRef[] | undefined): NetworkNode['data'] {\n  if (!refs || refs.length === 0) {\n    return data;\n  }\n\n  const nextData = { ...data };\n  refs.forEach((ref) => {\n    if (ref.source === 'netbox') {\n      nextData.netboxId = ref.id;\n      if (ref.type) {\n        nextData.netboxType = ref.type;\n      }\n    }\n    if (ref.source === 'inventory') {\n      nextData.inventoryId = ref.id;\n      if (ref.type) {\n        nextData.inventoryType = ref.type;\n      }\n    }\n  });\n\n  return nextData;\n}\n\n/**\n * Build a per-node `endpointId -> endpoint` index for O(1) resolution. First\n * write wins so the lookup mirrors `Array.prototype.find` (first match) exactly;\n * nodes with no endpoints are omitted. Computing this once before the edge loop\n * turns {@link resolveEndpointHandleId} from an O(edges x endpoints) linear scan\n * into O(edges + endpoints).\n */\nfunction buildEndpointIndex(nodes: DiagramNode[]): Map<string, Map<string, DiagramEndpoint>> {\n  const index = new Map<string, Map<string, DiagramEndpoint>>();\n  for (const node of nodes) {\n    if (!node.endpoints || node.endpoints.length === 0) {\n      continue;\n    }\n    const byId = new Map<string, DiagramEndpoint>();\n    for (const endpoint of node.endpoints) {\n      if (!byId.has(endpoint.id)) {\n        byId.set(endpoint.id, endpoint);\n      }\n    }\n    index.set(node.id, byId);\n  }\n  return index;\n}\n\n/**\n * Resolve an edge's endpoint reference to the endpoint RECORD it names: by raw\n * id first (opaque ids, and the `<id>--rear` twin of a both-faces port), then\n * by the normalized id (a hand-authored edge may carry a directional/face\n * suffix the persisted endpoint id lacks).\n */\nfunction findEndpointRecord(\n  endpointId: string,\n  endpointById: Map<string, DiagramEndpoint> | undefined\n): DiagramEndpoint | undefined {\n  if (!endpointById) return undefined;\n  return endpointById.get(endpointId) ?? endpointById.get(normalizeHandleId(endpointId));\n}\n\n/**\n * The device type the renderer reads for an equipment node: `config.deviceType`\n * (what `DeviceNode` reads from `data.deviceType`), falling back to the subtype\n * for legacy/hand-authored documents that carried the device type there.\n */\nfunction effectiveEquipmentDeviceType(node: DiagramNode): string {\n  const configured = node.config?.deviceType;\n  const deviceType =\n    typeof configured === 'string' && configured.trim().length > 0 ? configured : node.subtype;\n  return (deviceType ?? '').toLowerCase().replace(/_/g, '-');\n}\n\n/**\n * Resolve a document edge's endpoint reference into the React Flow\n * `sourceHandle`/`targetHandle` the reconstructed port actually mounts.\n *\n * The endpoint {@link DiagramEndpoint.id} is treated as an *opaque* key, not a\n * handle-grammar string: the edge is resolved to its endpoint RECORD (via the\n * per-node index — {@link buildEndpointIndex}), and the handle base is the\n * record's own id, which is exactly what `endpointsToDevicePorts` /\n * `endpointsToPortBlocks` stamp onto `port.id` and therefore what\n * {@link buildPortHandleId} mounts (for the `<id>--rear` twin of a both-faces\n * port, the id IS the rear handle's split-face alias). Directional\n * (`-source`/`-target`) decisions are then derived from the record's\n * STRUCTURED fields (`face`/`role`/`medium`) plus the node's rendered device\n * type — never from the id text.\n *\n * This is the single resolution path for both id styles: opaque consumer keys\n * (uuid-like, including ones whose text coincidentally ends in a reserved\n * suffix) and the handle-grammar strings the shipped adapters mint round-trip\n * identically. When an edge references a missing endpoint, the base falls back\n * to the normalized edge id so a dangling legacy reference still resolves\n * byte-for-byte as before.\n */\nfunction resolveEndpointHandleId(\n  node: DiagramNode | undefined,\n  endpointId: string | null | undefined,\n  role: 'source' | 'target',\n  endpointById?: Map<string, DiagramEndpoint>\n): string | undefined {\n  if (!endpointId) {\n    return undefined;\n  }\n\n  const endpoint = endpointById\n    ? findEndpointRecord(endpointId, endpointById)\n    : node?.endpoints?.find(\n        (candidate) =>\n          candidate.id === endpointId || candidate.id === normalizeHandleId(endpointId)\n      );\n\n  // Base = the endpoint's own id (what the rebuilt port mounts its handle with),\n  // NOT the normalized edge string. Falls back to the normalized edge id only\n  // for a dangling reference with no matching endpoint.\n  const base = endpoint ? endpoint.id : normalizeHandleId(endpointId);\n\n  if (!node || node.kind !== 'equipment') {\n    return base;\n  }\n\n  const deviceType = effectiveEquipmentDeviceType(node);\n  const isPowerEndpoint = endpoint?.role === 'power' || endpoint?.medium === 'power';\n\n  if ((deviceType === 'switch' || deviceType === 'router') && role === 'target' && !isPowerEndpoint) {\n    return `${base}-target`;\n  }\n\n  if (deviceType === 'patch-panel') {\n    const face = endpoint?.face === 'rear' ? 'rear' : 'front';\n    if (role === 'source' && face === 'front') {\n      return `${base}-source`;\n    }\n    if (role === 'target' && face === 'rear') {\n      return `${base}-target`;\n    }\n  }\n\n  return base;\n}\n\n/**\n * Resolve a document node's placement defensively. Missing placement or a\n * missing/partial position degrades to `{ x: 0, y: 0 }` (with a warning) instead\n * of throwing on a partial document.\n */\nfunction resolveNodePlacement(node: DiagramNode): DiagramNodePlacement {\n  const placement = isRecord(node.placement) ? (node.placement as DiagramNodePlacement) : undefined;\n  if (!placement) {\n    console.warn(\n      `[react-networks] Document node \"${node.id}\" is missing placement; defaulting position to { x: 0, y: 0 }.`\n    );\n    return { position: { x: 0, y: 0 } };\n  }\n  const position = placement.position;\n  // Number.isFinite (not typeof === 'number') so NaN/Infinity coordinates from a\n  // malformed document are rejected rather than corrupting React Flow layout.\n  if (!position || !Number.isFinite(position.x) || !Number.isFinite(position.y)) {\n    console.warn(\n      `[react-networks] Document node \"${node.id}\" is missing a valid placement.position; defaulting to { x: 0, y: 0 }.`\n    );\n    return { ...placement, position: clonePosition(position) };\n  }\n  return placement;\n}\n\n/**\n * Coerce untrusted document nodes: drop non-record entries and entries without\n * a string id or a known kind (with a warning each), drop malformed endpoint\n * entries, and keep the first node for a duplicated id.\n */\nfunction sanitizeDocumentNodes(rawNodes: unknown): DiagramNode[] {\n  if (rawNodes === undefined || rawNodes === null) return [];\n  if (!Array.isArray(rawNodes)) {\n    console.warn('[react-networks] DiagramDocument `nodes` is not an array; treating it as empty.');\n    return [];\n  }\n  const out: DiagramNode[] = [];\n  const seenIds = new Set<string>();\n  rawNodes.forEach((raw, index) => {\n    if (!isRecord(raw) || typeof raw.id !== 'string' || raw.id.length === 0) {\n      console.warn(`[react-networks] Skipping malformed document node at index ${index} (not an object with a string id).`);\n      return;\n    }\n    const node = raw as unknown as DiagramNode;\n    if (!KNOWN_DIAGRAM_NODE_KINDS.has(node.kind)) {\n      console.warn(\n        `[react-networks] Skipping document node \"${node.id}\" with unknown kind \"${String(node.kind)}\".`\n      );\n      return;\n    }\n    if (seenIds.has(node.id)) {\n      console.warn(`[react-networks] Skipping duplicate document node id \"${node.id}\".`);\n      return;\n    }\n    seenIds.add(node.id);\n    const endpoints = Array.isArray(node.endpoints)\n      ? node.endpoints.filter(\n          (endpoint): endpoint is DiagramEndpoint =>\n            isRecord(endpoint) && typeof endpoint.id === 'string' && endpoint.id.length > 0\n        )\n      : undefined;\n    const label = typeof node.label === 'string' ? node.label : node.id;\n    out.push({\n      ...node,\n      label,\n      config: isRecord(node.config) ? node.config : undefined,\n      endpoints: endpoints && endpoints.length > 0 ? endpoints : undefined,\n      externalRefs: Array.isArray(node.externalRefs)\n        ? node.externalRefs.filter((ref): ref is DiagramExternalRef => isRecord(ref))\n        : undefined,\n    });\n  });\n  return out;\n}\n\nfunction isEdgeEnd(value: unknown): value is DiagramEdgeEndpoint {\n  return isRecord(value) && typeof value.nodeId === 'string' && value.nodeId.length > 0;\n}\n\n/**\n * Convert a persisted {@link DiagramDocument} (or a lenient\n * {@link DiagramDocumentInput}) back into a runtime graph.\n *\n * Untrusted input never throws: malformed nodes/endpoints/edges are skipped\n * with a `console.warn`, and edges whose source or target node is absent from\n * the produced graph (unknown kind, malformed, or simply missing) are dropped\n * instead of being emitted dangling.\n */\nexport function diagramDocumentToNetworkGraph(document: DiagramDocumentInput): NetworkGraphSnapshot {\n  const input: DiagramDocumentInput = isRecord(document) ? document : {};\n  if (typeof input.version === 'number' && input.version > 1) {\n    console.warn(\n      `[react-networks] Unknown DiagramDocument version ${input.version}; parsing best-effort as version 1.`\n    );\n  }\n\n  const documentNodes = sanitizeDocumentNodes(input.nodes);\n  const rawEdges: unknown[] = Array.isArray(input.edges) ? input.edges : [];\n  if (input.edges !== undefined && input.edges !== null && !Array.isArray(input.edges)) {\n    console.warn('[react-networks] DiagramDocument `edges` is not an array; treating it as empty.');\n  }\n  const documentEdges = rawEdges.filter((edge, index): edge is DiagramEdge => {\n    if (isRecord(edge) && typeof edge.id === 'string' && isEdgeEnd(edge.source) && isEdgeEnd(edge.target)) {\n      return true;\n    }\n    console.warn(\n      `[react-networks] Skipping malformed document edge at index ${index} (needs an id and source/target node ids).`\n    );\n    return false;\n  });\n\n  const nodesById = new Map(documentNodes.map((node) => [node.id, node]));\n  const endpointsByNodeId = buildEndpointIndex(documentNodes);\n\n  // Mark the endpoint RECORDS edges land on, so a both-faces port's front and\n  // rear twins are tracked independently.\n  const connectedByNodeId = new Map<string, Set<string>>();\n  const markConnected = (ref: DiagramEdgeEndpoint) => {\n    if (!ref.endpointId) return;\n    const record = findEndpointRecord(ref.endpointId, endpointsByNodeId.get(ref.nodeId));\n    if (!record) return;\n    let set = connectedByNodeId.get(ref.nodeId);\n    if (!set) {\n      set = new Set();\n      connectedByNodeId.set(ref.nodeId, set);\n    }\n    set.add(record.id);\n  };\n  documentEdges.forEach((edge) => {\n    markConnected(edge.source);\n    markConnected(edge.target);\n  });\n\n  const uiNodeState = isRecord(input.ui?.nodeState) ? input.ui!.nodeState! : {};\n  const noConnections: ReadonlySet<string> = new Set();\n\n  const nodes: NetworkNode[] = [];\n  for (const node of documentNodes) {\n    const placement = resolveNodePlacement(node);\n    const legacyType = toLegacyNodeType(node);\n    const connected = connectedByNodeId.get(node.id) ?? noConnections;\n    let data: NetworkNode['data'] = {\n      label: node.label,\n      ...(node.config ?? {}),\n    } as NetworkNode['data'];\n\n    const uiEntry = Object.prototype.hasOwnProperty.call(uiNodeState, node.id)\n      ? uiNodeState[node.id]\n      : undefined;\n    data = applyRenderToLegacyData(data, isRecord(node.render) ? node.render : undefined, isRecord(uiEntry) ? uiEntry : undefined);\n    data = applyExternalRefsToLegacyData(data, node.externalRefs);\n\n    if (legacyType === 'device' || legacyType === 'server' || legacyType === 'switch' || legacyType === 'router') {\n      // `config.deviceType` (already spread above) is authoritative. Legacy\n      // documents carried the device type in `subtype` instead.\n      if (typeof data.deviceType !== 'string' || data.deviceType.trim().length === 0) {\n        data.deviceType = node.subtype ?? (legacyType === 'device' ? 'device' : legacyType);\n      }\n      if (node.endpoints && node.endpoints.length > 0) {\n        data.ports = endpointsToDevicePorts(node.endpoints, collectFrontEndpointIds(node.endpoints), connected);\n      }\n    } else if (legacyType === 'vertical-pdu') {\n      const rearEndpoints = (node.endpoints ?? []).filter((endpoint) => endpoint.face !== 'front');\n      if (rearEndpoints.length > 0) {\n        data.pduPortGroups = endpointsToPortBlocks(rearEndpoints, noConnections, connected);\n        data.powerPorts = rearEndpoints.length;\n      }\n    } else if (legacyType === 'patch-panel' || legacyType === 'closure' || legacyType === 'splice-tray') {\n      // Mirror the forward pass, which serializes container ports as face-tagged\n      // endpoints, by reconstructing front -> portGroups and rear -> rearPortGroups.\n      const endpoints = node.endpoints ?? [];\n      const frontIds = collectFrontEndpointIds(endpoints);\n      const frontEndpoints = endpoints.filter((endpoint) => endpoint.face !== 'rear');\n      const rearEndpoints = endpoints.filter((endpoint) => endpoint.face === 'rear');\n      if (frontEndpoints.length > 0) {\n        data.portGroups = endpointsToPortBlocks(frontEndpoints, frontIds, connected);\n      }\n      if (rearEndpoints.length > 0) {\n        data.rearPortGroups = endpointsToPortBlocks(rearEndpoints, frontIds, connected);\n      }\n    }\n\n    if (placement.rack) {\n      if (placement.rack.rackLayout) {\n        data.rackLayout = placement.rack.rackLayout;\n      }\n      if (typeof placement.rack.rackWidthPx === 'number') {\n        data.rackWidthPx = placement.rack.rackWidthPx;\n      }\n      if (typeof placement.rack.rackFaceGapPx === 'number') {\n        data.rackFaceGapPx = placement.rack.rackFaceGapPx;\n      }\n    }\n\n    if (legacyType !== 'rack' && placement.rack) {\n      data.isRackMounted = true;\n      if (typeof placement.rack.uPosition === 'number') {\n        data.uPosition = placement.rack.uPosition;\n      }\n      if (placement.rack.mountMode) {\n        data.mountMode = placement.rack.mountMode;\n      }\n    }\n\n    if (placement.container) {\n      if (typeof placement.container.slotPosition === 'number') {\n        data.slotPosition = placement.container.slotPosition;\n      }\n      if (typeof placement.container.holderPosition === 'number') {\n        data.holderPosition = placement.container.holderPosition;\n      }\n    }\n\n    nodes.push({\n      id: node.id,\n      type: legacyType,\n      position: clonePosition(placement.position),\n      ...(placement.parentId ? { parentId: placement.parentId } : {}),\n      ...(placement.extent ? { extent: placement.extent } : {}),\n      data,\n    } as NetworkNode);\n  }\n\n  const producedNodeIds = new Set(nodes.map((node) => node.id));\n  const danglingEdgeIds: string[] = [];\n  const edges: NetworkEdge[] = [];\n  for (const edge of documentEdges) {\n    if (!producedNodeIds.has(edge.source.nodeId) || !producedNodeIds.has(edge.target.nodeId)) {\n      danglingEdgeIds.push(edge.id);\n      continue;\n    }\n    edges.push({\n      id: edge.id,\n      source: edge.source.nodeId,\n      target: edge.target.nodeId,\n      ...(edge.source.endpointId ? { sourceHandle: resolveEndpointHandleId(nodesById.get(edge.source.nodeId), edge.source.endpointId, 'source', endpointsByNodeId.get(edge.source.nodeId)) } : {}),\n      ...(edge.target.endpointId ? { targetHandle: resolveEndpointHandleId(nodesById.get(edge.target.nodeId), edge.target.endpointId, 'target', endpointsByNodeId.get(edge.target.nodeId)) } : {}),\n      ...(edge.type ? { type: edge.type } : {}),\n      ...(edge.hidden === true ? { hidden: true } : {}),\n      ...(typeof edge.deletable === 'boolean' ? { deletable: edge.deletable } : {}),\n      ...(typeof edge.interactionWidth === 'number' && Number.isFinite(edge.interactionWidth)\n        ? { interactionWidth: edge.interactionWidth }\n        : {}),\n      ...(isRecord(edge.data) ? { data: { ...edge.data } } : {}),\n    });\n  }\n  if (danglingEdgeIds.length > 0) {\n    console.warn(\n      `[react-networks] Dropped ${danglingEdgeIds.length} document edge(s) whose source or target node is not in the document: ${danglingEdgeIds\n        .slice(0, 5)\n        .map((id) => `\"${id}\"`)\n        .join(', ')}${danglingEdgeIds.length > 5 ? ', …' : ''}.`\n    );\n  }\n\n  return { nodes, edges };\n}\n","import type { NetworkEdge, NetworkNode } from '../types';\n\nexport type PathLayoutDirection = 'horizontal' | 'vertical';\n\nexport interface PathLayoutStepInput {\n  id: string;\n  type?: NetworkNode['type'];\n  data?: NetworkNode['data'];\n}\n\nexport interface PathLayoutEdgeInput {\n  id: string;\n  source: string;\n  target: string;\n  type?: NetworkEdge['type'];\n  data?: NetworkEdge['data'];\n}\n\nexport interface BuildPathLayoutOptions {\n  direction?: PathLayoutDirection;\n  spacing?: number;\n  origin?: { x: number; y: number };\n}\n\nconst DEFAULT_SPACING = 220;\n\n/**\n * Lay out a sequential path of nodes and connecting edges with deterministic spacing.\n * @public\n */\nexport function buildPathLayoutGraph(\n  steps: PathLayoutStepInput[],\n  edges: PathLayoutEdgeInput[],\n  options: BuildPathLayoutOptions = {},\n): { nodes: NetworkNode[]; edges: NetworkEdge[] } {\n  const direction = options.direction ?? 'horizontal';\n  const spacing = options.spacing ?? DEFAULT_SPACING;\n  const origin = options.origin ?? { x: 0, y: 0 };\n\n  const nodes = steps.map((step, index) => ({\n    id: step.id,\n    type: step.type ?? 'fiber',\n    position:\n      direction === 'vertical'\n        ? { x: origin.x, y: origin.y + index * spacing }\n        : { x: origin.x + index * spacing, y: origin.y },\n    data: step.data ?? { label: step.id },\n  })) as NetworkNode[];\n\n  const layoutEdges: NetworkEdge[] = edges.map((edge) => ({\n    id: edge.id,\n    source: edge.source,\n    target: edge.target,\n    type: edge.type ?? 'fiber',\n    data: edge.data,\n  }));\n\n  return { nodes, edges: layoutEdges };\n}\n","import type { NetworkNode } from '../types';\nimport { normalizeHandleId, parseHandleId } from './handle-ids';\n\nexport const isFibreNode = (node?: NetworkNode) =>\n  node?.type === 'tube' ||\n  node?.type === 'splice' ||\n  node?.type === 'coupler' ||\n  node?.type === 'fibre-split' ||\n  node?.type === 'cable' ||\n  node?.type === 'multi-tube-cable';\n\nexport const isFibreConnection = (sourceNode?: NetworkNode, targetNode?: NetworkNode) =>\n  isFibreNode(sourceNode) || isFibreNode(targetNode);\n\n/**\n * Normalize a rendered handle id back to its canonical endpoint id.\n * The `node` parameter is retained for call-site ergonomics and future\n * node-specific normalization; today all node types share one rule.\n */\nexport const normalizeHandle = (_node?: NetworkNode, handle?: string | null) =>\n  handle ? normalizeHandleId(handle) : '';\n\n/** Result shape shared with the store's `ValidationResult`. */\nexport interface ConnectionRuleResult {\n  valid: boolean;\n  reason?: string;\n}\n\n/**\n * Structural classification of a handle that participates in fiber routing.\n * This is the single place that maps a (node, handle) pair to its fiber role;\n * the connection matrix below is expressed purely in terms of these classes.\n */\nexport type FibreEndpointClass =\n  | 'tube-fiber'\n  | 'tube-tube'\n  | 'cable-tube'\n  | 'cable-button'\n  | 'splice-side'\n  | 'coupler-port'\n  | 'splitter-input'\n  | 'splitter-output'\n  | 'other';\n\nexport function classifyFibreEndpoint(\n  node: NetworkNode | undefined,\n  handle: string | null | undefined,\n): FibreEndpointClass {\n  if (!node || !handle) {\n    return 'other';\n  }\n\n  // Structural shape from the single grammar authority; the node type turns\n  // it into semantics (left/right means splice-side on a splice but a port on\n  // a coupler, etc.).\n  const { shape } = parseHandleId(handle);\n  switch (node.type) {\n    case 'tube':\n      if (shape.kind === 'fiber') return 'tube-fiber';\n      if (shape.kind === 'tube') return 'tube-tube';\n      return 'other';\n    case 'cable':\n      if (shape.kind === 'fiber') return 'tube-fiber';\n      if (shape.kind === 'cable-button') return 'cable-button';\n      return 'other';\n    case 'multi-tube-cable':\n      if (shape.kind === 'tube') return 'cable-tube';\n      if (shape.kind === 'cable-button') return 'cable-button';\n      return 'other';\n    case 'splice':\n      return shape.kind === 'splice-side' ? 'splice-side' : 'other';\n    case 'coupler':\n      return shape.kind === 'side-port' ? 'coupler-port' : 'other';\n    case 'fibre-split':\n      if (shape.kind === 'split-io') {\n        return shape.io === 'input' ? 'splitter-input' : 'splitter-output';\n      }\n      return 'other';\n    default:\n      return 'other';\n  }\n}\n\n/**\n * Allowed *directed* `source>target` fiber-endpoint pairs.\n *\n * This list is an exact expansion of the legacy boolean rule set. Directionality\n * is preserved deliberately: e.g. a splitter input may be a connection target\n * (feed in) when paired with a splice, but never a source toward a splice — so\n * `splice-side>splitter-input` is present while `splitter-input>splice-side` is not.\n */\nconst ALLOWED_FIBER_PAIRS: ReadonlySet<string> = new Set([\n  // Tube fiber <-> splice (either direction)\n  'tube-fiber>splice-side',\n  'splice-side>tube-fiber',\n  // Splice <-> coupler (either direction)\n  'splice-side>coupler-port',\n  'coupler-port>splice-side',\n  // Splice -> splitter input (feed in only)\n  'splice-side>splitter-input',\n  // Splice <-> splitter output (either direction)\n  'splice-side>splitter-output',\n  'splitter-output>splice-side',\n  // Coupler <-> coupler\n  'coupler-port>coupler-port',\n  // Coupler <-> splitter (either input or output, either direction)\n  'coupler-port>splitter-input',\n  'splitter-input>coupler-port',\n  'coupler-port>splitter-output',\n  'splitter-output>coupler-port',\n  // Splitter output <-> tube fiber (either direction)\n  'splitter-output>tube-fiber',\n  'tube-fiber>splitter-output',\n  // Cable trunk button <-> cable trunk button\n  'cable-button>cable-button',\n  // Cable tube <-> tube tube (either direction)\n  'cable-tube>tube-tube',\n  'tube-tube>cable-tube',\n]);\n\nfunction isTubeHandleClass(value: FibreEndpointClass): boolean {\n  return value === 'tube-tube' || value === 'cable-tube';\n}\n\n/**\n * Evaluate whether a fiber-domain connection is structurally allowed.\n *\n * This is the single source of truth for fiber connection rules, consumed by\n * both the headless store validator and the interactive `NetworkDiagram`\n * validator. It does not consider handle occupancy (capacity) — fiber handles\n * support edge replacement, so occupancy is resolved by the caller.\n */\nexport function evaluateFibreConnectionRule(\n  sourceNode: NetworkNode | undefined,\n  sourceHandle: string | null | undefined,\n  targetNode: NetworkNode | undefined,\n  targetHandle: string | null | undefined,\n): ConnectionRuleResult {\n  if (sourceNode?.type === 'splice' && targetNode?.type === 'splice') {\n    const sourceIsRibbon = sourceNode.data?.mode === 'ribbon';\n    const targetIsRibbon = targetNode.data?.mode === 'ribbon';\n    if (sourceIsRibbon !== targetIsRibbon) {\n      return { valid: false, reason: 'Ribbon splices can only connect to other ribbon splices' };\n    }\n  }\n\n  const sourceClass = classifyFibreEndpoint(sourceNode, sourceHandle);\n  const targetClass = classifyFibreEndpoint(targetNode, targetHandle);\n\n  if (ALLOWED_FIBER_PAIRS.has(`${sourceClass}>${targetClass}`)) {\n    return { valid: true };\n  }\n\n  if (isTubeHandleClass(sourceClass) && isTubeHandleClass(targetClass)) {\n    return { valid: false, reason: 'Tube handles can only connect between cable and tube nodes' };\n  }\n\n  return { valid: false, reason: 'Connection not allowed by fiber rules' };\n}\n"]}