diff --git a/src/lib/components/nodes/BaseNode.svelte b/src/lib/components/nodes/BaseNode.svelte index 4ee46c40..036b8e07 100644 --- a/src/lib/components/nodes/BaseNode.svelte +++ b/src/lib/components/nodes/BaseNode.svelte @@ -16,7 +16,7 @@ import { showTooltip, hideTooltip } from '$lib/components/Tooltip.svelte'; import { paramInput } from '$lib/actions/paramInput'; import { plotDataStore } from '$lib/plotting/processing/plotDataStore'; - import { PORT_LABEL, getPortPositionCalc, calculateNodeDimensions } from '$lib/constants/dimensions'; + import { getPortPositionCalc, calculateNodeDimensions } from '$lib/constants/dimensions'; import { truncatePortLabel } from '$lib/utils/portLabels'; import { containsMath, renderInlineMath, renderInlineMathSync, measureRenderedMath } from '$lib/utils/inlineMathRenderer'; import { getKatexCssUrl } from '$lib/utils/katexLoader'; @@ -93,8 +93,6 @@ const hasVisibleInputLabels = $derived(showInputLabels && data.inputs.length > 0); const hasVisibleOutputLabels = $derived(showOutputLabels && data.outputs.length > 0); - // For CSS class (show-labels when any labels are actually displayed) - const showPortLabels = $derived(hasVisibleInputLabels || hasVisibleOutputLabels); // Re-measure node when port labels toggle changes $effect(() => { @@ -229,7 +227,9 @@ : null ); - // Node dimensions - calculated from shared utility (same as SvelteFlow bounds) + // Node dimensions - calculated from shared utility (same as SvelteFlow bounds). + // Port-label visibility no longer enters the calculation; labels render + // outside the block bounds and don't affect layout. const nodeDimensions = $derived(calculateNodeDimensions( data.name, data.inputs.length, @@ -237,100 +237,49 @@ pinnedCount, rotation, typeDef?.name, - hasVisibleInputLabels, - hasVisibleOutputLabels, measuredName, showIcon )); - // Grid layout for port labels (computed in JS, replaces CSS grid-placement selectors) - const gridLayout = $derived(() => { - if (!showPortLabels) { - return { - columns: undefined, rows: undefined, - inputStyle: '', innerStyle: '', outputStyle: '' - }; + /** Inline style for a port label, positioning it outside the block edge + * next to its handle. The handle/wire is always *below* the label from + * the label's perspective — i.e. the anchor point sits at the label's + * bottom-left or bottom-right corner. + * + * Horizontal block: text horizontal, label sits just above the wire stub. + * Vertical block: `writing-mode: sideways-{lr|rl}` rotates the text + * parallel to the wire (no transform tricks needed for positioning, + * so the perpendicular offset works in screen-space directly). Top + * edge reads bottom-to-top, bottom edge top-to-bottom — both read + * *outward* from the block. */ + function portLabelStyle(isInput: boolean, portIndex: number, total: number): string { + const pos = getPortPositionCalc(portIndex, total); + const GAP = 10; // distance from block edge along the wire + const PERP = 5; // perpendicular offset off the wire path + + // Map (rotation, isInput) → which block edge hosts the port. + let edge: 'left' | 'right' | 'top' | 'bottom'; + if (rotation === 0) edge = isInput ? 'left' : 'right'; + else if (rotation === 2) edge = isInput ? 'right' : 'left'; + else if (rotation === 1) edge = isInput ? 'top' : 'bottom'; + else edge = isInput ? 'bottom' : 'top'; + + switch (edge) { + case 'left': + // Anchor (port) at label bottom-right. + return `right: 100%; margin-right: ${GAP}px; top: ${pos}; transform: translateY(calc(-100% - ${PERP}px)); text-align: right;`; + case 'right': + // Anchor at label bottom-left. + return `left: 100%; margin-left: ${GAP}px; top: ${pos}; transform: translateY(calc(-100% - ${PERP}px)); text-align: left;`; + case 'top': + // Reads bottom-to-top, label LEFT of wire. Anchor at bottom-right. + return `bottom: 100%; margin-bottom: ${GAP}px; left: ${pos}; writing-mode: sideways-lr; transform: translateX(calc(-100% - ${PERP}px)); text-align: end;`; + case 'bottom': + // Reads top-to-bottom, label RIGHT of wire. Anchor at top-left + // (= label's bottom-left if you tilt your head left to read). + return `top: 100%; margin-top: ${GAP}px; left: ${pos}; writing-mode: sideways-rl; transform: translateX(${PERP}px); text-align: start;`; } - - const labelSize = `${PORT_LABEL.columnWidth}px`; - let columns: string | undefined; - let rows: string | undefined; - let inputStyle = ''; - let innerStyle = ''; - let outputStyle = ''; - - if (isVertical) { - // Vertical: rows for labels, single column - const inputBorder = rotation === 1 ? 'border-bottom' : 'border-top'; - const outputBorder = rotation === 1 ? 'border-top' : 'border-bottom'; - const colStyle = 'grid-column: 1;'; - - if (hasVisibleInputLabels && hasVisibleOutputLabels) { - // rotation 1: input(row1) content(row2) output(row3) - // rotation 3: output(row1) content(row2) input(row3) - rows = `${labelSize} 1fr ${labelSize}`; - if (rotation === 1) { - inputStyle = `${colStyle} grid-row: 1; ${inputBorder}: 1px solid var(--border);`; - innerStyle = `${colStyle} grid-row: 2;`; - outputStyle = `${colStyle} grid-row: 3; ${outputBorder}: 1px solid var(--border);`; - } else { - outputStyle = `${colStyle} grid-row: 1; ${outputBorder}: 1px solid var(--border);`; - innerStyle = `${colStyle} grid-row: 2;`; - inputStyle = `${colStyle} grid-row: 3; ${inputBorder}: 1px solid var(--border);`; - } - } else if (hasVisibleInputLabels) { - rows = rotation === 1 ? `${labelSize} 1fr` : `1fr ${labelSize}`; - const inputRow = rotation === 1 ? 1 : 2; - const innerRow = rotation === 1 ? 2 : 1; - inputStyle = `${colStyle} grid-row: ${inputRow}; ${inputBorder}: 1px solid var(--border);`; - innerStyle = `${colStyle} grid-row: ${innerRow};`; - } else if (hasVisibleOutputLabels) { - rows = rotation === 1 ? `1fr ${labelSize}` : `${labelSize} 1fr`; - const outputRow = rotation === 1 ? 2 : 1; - const innerRow = rotation === 1 ? 1 : 2; - outputStyle = `${colStyle} grid-row: ${outputRow}; ${outputBorder}: 1px solid var(--border);`; - innerStyle = `${colStyle} grid-row: ${innerRow};`; - } - } else { - // Horizontal: columns for labels, single row - const rowStyle = 'grid-row: 1;'; - // rotation 0: inputs left (border-right), outputs right (border-left) - // rotation 2: inputs right (border-left), outputs left (border-right) - const inputBorder = rotation === 0 ? 'border-right' : 'border-left'; - const outputBorder = rotation === 0 ? 'border-left' : 'border-right'; - - if (hasVisibleInputLabels && hasVisibleOutputLabels) { - columns = `${labelSize} 1fr ${labelSize}`; - if (rotation === 0) { - // input(col1) content(col2) output(col3) - inputStyle = `${rowStyle} grid-column: 1; ${inputBorder}: 1px solid var(--border);`; - innerStyle = `${rowStyle} grid-column: 2;`; - outputStyle = `${rowStyle} grid-column: 3; ${outputBorder}: 1px solid var(--border);`; - } else { - // output(col1) content(col2) input(col3) - outputStyle = `${rowStyle} grid-column: 1; ${outputBorder}: 1px solid var(--border);`; - innerStyle = `${rowStyle} grid-column: 2;`; - inputStyle = `${rowStyle} grid-column: 3; ${inputBorder}: 1px solid var(--border);`; - } - } else if (hasVisibleInputLabels) { - // rotation 0: input(col1) content(col2) | rotation 2: content(col1) input(col2) - columns = rotation === 0 ? `${labelSize} 1fr` : `1fr ${labelSize}`; - const inputCol = rotation === 0 ? 1 : 2; - const innerCol = rotation === 0 ? 2 : 1; - inputStyle = `${rowStyle} grid-column: ${inputCol}; ${inputBorder}: 1px solid var(--border);`; - innerStyle = `${rowStyle} grid-column: ${innerCol};`; - } else if (hasVisibleOutputLabels) { - // rotation 0: content(col1) output(col2) | rotation 2: output(col1) content(col2) - columns = rotation === 0 ? `1fr ${labelSize}` : `${labelSize} 1fr`; - const outputCol = rotation === 0 ? 2 : 1; - const innerCol = rotation === 0 ? 1 : 2; - outputStyle = `${rowStyle} grid-column: ${outputCol}; ${outputBorder}: 1px solid var(--border);`; - innerStyle = `${rowStyle} grid-column: ${innerCol};`; - } - } - - return { columns, rows, inputStyle, innerStyle, outputStyle }; - }); + } // Check if this is a Subsystem or Interface node (using shapes utility) const isSubsystemNode = $derived(isSubsystem(data)); @@ -425,13 +374,17 @@ } } - // Handle mouse events for input handles + // Handle mouse events for input handles. The hover tooltip is suppressed + // when port labels are already shown — the label IS the name, no point + // also popping a tooltip on top of it. function handleInputMouseEnter(event: MouseEvent, port: { id: string; name: string }) { hoveredHandle.set({ nodeId: id, handleId: port.id, color: nodeColor }); - showTooltip(port.name, event.currentTarget as HTMLElement, getInputTooltipPosition()); + if (!hasVisibleInputLabels) { + showTooltip(port.name, event.currentTarget as HTMLElement, getInputTooltipPosition()); + } } - function handleInputMouseLeave(port: { id: string }) { + function handleInputMouseLeave(_port: { id: string }) { hoveredHandle.set(null); hideTooltip(); } @@ -439,10 +392,12 @@ // Handle mouse events for output handles function handleOutputMouseEnter(event: MouseEvent, port: { id: string; name: string }) { hoveredHandle.set({ nodeId: id, handleId: port.id, color: nodeColor }); - showTooltip(port.name, event.currentTarget as HTMLElement, getOutputTooltipPosition()); + if (!hasVisibleOutputLabels) { + showTooltip(port.name, event.currentTarget as HTMLElement, getOutputTooltipPosition()); + } } - function handleOutputMouseLeave(port: { id: string }) { + function handleOutputMouseLeave(_port: { id: string }) { hoveredHandle.set(null); hideTooltip(); } @@ -472,7 +427,6 @@ class:vertical={isVertical} class:preview-hovered={showPreview} class:subsystem-type={isSubsystemType} - class:show-labels={showPortLabels} class:missing-type={!typeDef && data.type !== NODE_TYPES.SUBSYSTEM && data.type !== NODE_TYPES.INTERFACE} data-rotation={rotation} style="width: {nodeDimensions.width}px; height: {nodeDimensions.height}px; --node-color: {nodeColor}; --preview-gap: {PREVIEW_GAP}px;" @@ -496,100 +450,77 @@ {/if} -
- - {#if hasVisibleInputLabels} - {#if isVertical} -
- {#each data.inputs as port, i} - - {truncatePortLabel(port.name)} - - {/each} -
+
+ +
+ {#if renderedNameHtml} + {@html renderedNameHtml} {:else} -
- {#each data.inputs as port, i} - - {truncatePortLabel(port.name)} - - {/each} -
+ {data.name} {/if} - {/if} - - -
- -
- {#if renderedNameHtml} - {@html renderedNameHtml} - {:else} - {data.name} - {/if} - {#if showIcon} -
- -
- {:else if typeDef} - {typeDef.name} - {:else if data.type !== NODE_TYPES.SUBSYSTEM && data.type !== NODE_TYPES.INTERFACE} - {data.type} (missing) - {/if} -
- - - {#if validPinnedParams().length > 0 && typeDef} - -
e.stopPropagation()} ondblclick={(e) => e.stopPropagation()}> - {#each validPinnedParams() as paramName} - {@const paramDef = typeDef.params.find(p => p.name === paramName)} - {#if paramDef} -
- - handlePinnedParamChange(paramName, e.currentTarget.value)} - onmousedown={(e) => e.stopPropagation()} - onfocus={(e) => e.stopPropagation()} - use:paramInput - /> -
- {/if} - {/each} + {#if showIcon} +
+
+ {:else if typeDef} + {typeDef.name} + {:else if data.type !== NODE_TYPES.SUBSYSTEM && data.type !== NODE_TYPES.INTERFACE} + {data.type} (missing) {/if}
- - {#if hasVisibleOutputLabels} - {#if isVertical} -
- {#each data.outputs as port, i} - - {truncatePortLabel(port.name)} - - {/each} -
- {:else} -
- {#each data.outputs as port, i} - - {truncatePortLabel(port.name)} - - {/each} -
- {/if} + + {#if validPinnedParams().length > 0 && typeDef} + +
e.stopPropagation()} ondblclick={(e) => e.stopPropagation()}> + {#each validPinnedParams() as paramName} + {@const paramDef = typeDef.params.find(p => p.name === paramName)} + {#if paramDef} +
+ + handlePinnedParamChange(paramName, e.currentTarget.value)} + onmousedown={(e) => e.stopPropagation()} + onfocus={(e) => e.stopPropagation()} + use:paramInput + /> +
+ {/if} + {/each} +
{/if}
+ + {#if hasVisibleInputLabels} + {#each data.inputs as port, i} + + {truncatePortLabel(port.name)} + + {/each} + {/if} + {#if hasVisibleOutputLabels} + {#each data.outputs as port, i} + + {truncatePortLabel(port.name)} + + {/each} + {/if} + {#if allowsDynamicInputs && selected}
@@ -715,15 +646,6 @@ flex-direction: column; } - /* Inner wrapper for content */ - .node-inner { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; - min-height: 0; - } - /* Content - centered in available space */ .node-content { flex: 1; @@ -1081,93 +1003,30 @@ } } - /* Port labels - grid layout when labels are shown */ - /* Grid template columns/rows are set via inline style from JS */ - .node.show-labels .node-clip { - display: grid; - } - - /* Label containers */ - .port-labels { - position: relative; - min-width: 0; - min-height: 0; - overflow: visible; - } - - /* Individual port labels (absolute positioning for horizontal) */ + /* Port labels: rendered as absolutely-positioned spans on the .node + * container, sitting just outside the block edge next to their handle. + * Position math (which edge, perpendicular offset, text-align) is set + * inline by portLabelStyle() — only typography lives in CSS. */ .port-label { position: absolute; font-size: 8px; + line-height: 1; color: var(--text-muted); white-space: nowrap; - transform: translateY(-50%); overflow: hidden; text-overflow: ellipsis; - max-width: 36px; - line-height: 1; - } - - /* Input labels: align right (near separator), away from handle edge */ - .port-labels-input .port-label { - right: 6px; - text-align: right; - } - - /* Output labels: align left (near separator), away from handle edge */ - .port-labels-output .port-label { - left: 6px; - text-align: left; - } - - /* Rotation 2: swap alignment */ - .node[data-rotation="2"] .port-labels-input .port-label { - right: auto; - left: 6px; - text-align: left; - } - .node[data-rotation="2"] .port-labels-output .port-label { - left: auto; - right: 6px; - text-align: right; - } - - /* Vertical rotation - row of labels with 90deg rotation */ - .port-labels-row { - position: relative; - } - - /* Reset horizontal-specific styles for vertical labels */ - .port-labels-row .port-label { - position: absolute; - width: auto; - max-width: none; - right: auto; - /* Use center origin for simpler positioning */ - transform-origin: center center; - /* text-align: left = text starts at original left edge = visual bottom after -90deg rotation */ - text-align: left; - } - - /* Input labels at top row: center vertically, shift toward bottom separator */ - .node.show-labels.vertical .port-labels-input .port-label { - top: 50%; - bottom: auto; - transform: translateX(-50%) translateY(calc(-50% + 6px)) rotate(-90deg); - } - - /* Output labels at bottom row: center vertically, shift toward top separator */ - .node.show-labels.vertical .port-labels-output .port-label { - top: 50%; - bottom: auto; - transform: translateX(-50%) translateY(calc(-50% - 6px)) rotate(-90deg); + max-width: 64px; + pointer-events: none; + transition: color 0.12s; } - /* Rotation 3: swap the vertical shifts */ - .node.show-labels.vertical[data-rotation="3"] .port-labels-input .port-label { - transform: translateX(-50%) translateY(calc(-50% - 6px)) rotate(-90deg); - } - .node.show-labels.vertical[data-rotation="3"] .port-labels-output .port-label { - transform: translateX(-50%) translateY(calc(-50% + 6px)) rotate(-90deg); + /* Highlight labels in the node accent color when either: + * - the block is selected, or + * - a port handle is hovered (only that single label). + * The colour comes from --node-color set on the parent .node. */ + .node.selected .port-label, + .port-label.hovered { + color: var(--node-color, var(--accent)); + font-weight: 500; } diff --git a/src/lib/constants/dimensions.ts b/src/lib/constants/dimensions.ts index f87f32bd..6b811c80 100644 --- a/src/lib/constants/dimensions.ts +++ b/src/lib/constants/dimensions.ts @@ -42,14 +42,6 @@ export const EVENT = { /** Export padding: 4 grid units = 40px */ export const EXPORT_PADDING = G.x4; -/** Port label dimensions (when labels are shown) */ -export const PORT_LABEL = { - /** Width of label column for horizontal ports: 4 grid units = 40px */ - columnWidth: G.x4, - /** Height of label row for vertical ports: 4 grid units = 40px (same as column width) */ - rowHeight: G.x4 -} as const; - /** * Round up to next 2G (20px) boundary. * This ensures nodes expand by 1G in each direction (symmetric from center). @@ -103,8 +95,9 @@ const ICON_CONTENT_HEIGHT = G.px(6); // 60 * Calculate node dimensions from node data. * Used by both SvelteFlow (for bounds) and BaseNode (for CSS). * - * @param hasVisibleInputLabels - True if input labels are visible (setting ON and inputs exist) - * @param hasVisibleOutputLabels - True if output labels are visible (setting ON and outputs exist) + * Port labels render outside the block (no dimension impact), so the + * label-toggle no longer enters this calculation. + * * @param measuredName - Optional measured dimensions for math-rendered names */ export function calculateNodeDimensions( @@ -114,8 +107,6 @@ export function calculateNodeDimensions( pinnedParamCount: number, rotation: number, typeName?: string, - hasVisibleInputLabels?: boolean, - hasVisibleOutputLabels?: boolean, measuredName?: { width: number; height: number } | null, showIcon?: boolean ): { width: number; height: number } { @@ -163,20 +154,10 @@ export function calculateNodeDimensions( contentHeight = NODE.baseHeight + pinnedParamsHeight; } - // Final dimensions accounting for port space - let width = contentWidth; - let height = isVertical + const width = contentWidth; + const height = isVertical ? snapTo2G(contentHeight) : snapTo2G(Math.max(contentHeight, minPortDimension)); - // Add space for port labels if visible - if (isVertical) { - if (hasVisibleInputLabels) height += PORT_LABEL.rowHeight; - if (hasVisibleOutputLabels) height += PORT_LABEL.rowHeight; - } else { - if (hasVisibleInputLabels) width += PORT_LABEL.columnWidth; - if (hasVisibleOutputLabels) width += PORT_LABEL.columnWidth; - } - return { width, height }; } diff --git a/src/lib/stores/graph/ports.ts b/src/lib/stores/graph/ports.ts index 18f77b2e..535a6349 100644 --- a/src/lib/stores/graph/ports.ts +++ b/src/lib/stores/graph/ports.ts @@ -5,6 +5,7 @@ import { get } from 'svelte/store'; import type { NodeInstance, PortInstance } from '$lib/nodes/types'; import { nodeRegistry } from '$lib/nodes/registry'; +import { getPortLabelConfigs } from '$lib/nodes/uiConfig'; import { NODE_TYPES } from '$lib/constants/nodeTypes'; import { PORT_COLORS } from '$lib/utils/colors'; import { PORT_NAME, HANDLE_ID } from '$lib/constants/handles'; @@ -284,38 +285,49 @@ function parsePythonList(value: unknown): string[] | null { } /** - * Sync port names from a labels parameter value - * Updates port names to match labels, keeps generic names for extras + * Pure helper: return a node with port names synced to its label-driven + * params (Scope.labels, Adder.operations, …). Used by both the live + * updateNodeParams path (via syncPortNamesFromLabels) and the model-load + * path (which walks the loaded node tree and applies it recursively + * before nodes hit the store). Returns the same node reference if no + * changes were needed so callers can short-circuit. + */ +export function applyPortLabelSync(node: NodeInstance): NodeInstance { + let result = node; + for (const config of getPortLabelConfigs(node.type)) { + const portsKey = config.direction === 'input' ? 'inputs' : 'outputs'; + const defaultName = config.direction === 'input' ? 'in' : 'out'; + const labels = (config.parser ?? parsePythonList)(result.params?.[config.param]); + if (!labels) continue; + + const currentPorts = result[portsKey] as PortInstance[]; + const newPorts = currentPorts.map((port, index) => { + const name = index < labels.length ? labels[index] : `${defaultName} ${index}`; + return port.name === name ? port : { ...port, name }; + }); + const changed = newPorts.some((p, i) => p.name !== currentPorts[i].name); + if (changed) { + result = { ...result, [portsKey]: newPorts }; + } + } + return result; +} + +/** + * Sync port names from a labels parameter value (live path: writes back to + * the store). Used by updateNodeParams when a label-driven param changes. */ export function syncPortNamesFromLabels( nodeId: string, - labelsValue: unknown, - direction: 'input' | 'output', - parser?: (value: unknown) => string[] | null + _labelsValue: unknown, + _direction: 'input' | 'output', + _parser?: (value: unknown) => string[] | null ): void { const currentGraph = getCurrentGraph(); const node = currentGraph.nodes.get(nodeId); if (!node) return; - - const labels = parser ? parser(labelsValue) : parsePythonList(labelsValue); - const config = getPortConfig(direction); - const currentPorts = node[config.portsKey] as PortInstance[]; - - // Build new port names: use label if available, else generic name - const newPorts = currentPorts.map((port, index) => { - const label = labels && index < labels.length ? labels[index] : `${config.defaultName} ${index}`; - if (port.name === label) { - return port; // No change needed - } - return { ...port, name: label }; - }); - - // Only update if something changed - const hasChanges = newPorts.some((p, i) => p.name !== currentPorts[i].name); - if (!hasChanges) return; - - updateNodeById(nodeId, n => ({ - ...n, - [config.portsKey]: newPorts - })); + const updated = applyPortLabelSync(node); + if (updated !== node) { + updateNodeById(nodeId, () => updated); + } } diff --git a/src/lib/stores/graph/serialization.ts b/src/lib/stores/graph/serialization.ts index e54d989a..49f00eda 100644 --- a/src/lib/stores/graph/serialization.ts +++ b/src/lib/stores/graph/serialization.ts @@ -5,6 +5,7 @@ import { get } from 'svelte/store'; import type { NodeInstance, Connection, Annotation } from '$lib/nodes/types'; import { NODE_TYPES } from '$lib/constants/nodeTypes'; +import { applyPortLabelSync } from './ports'; import { rootNodes, rootConnections, @@ -36,6 +37,26 @@ export function toJSON(): { nodes: NodeInstance[]; connections: Connection[]; an }; } +/** + * Walk a node list (recursing into Subsystem.graph) and apply port-label + * sync to every label-driven block (Scope, Spectrum, Adder, …) so port + * names match the loaded `labels` / `operations` params. Without this, + * nodes loaded from disk keep whatever names happened to be saved and + * only re-sync after the next param edit. + */ +function syncPortLabelsRecursively(nodes: NodeInstance[]): NodeInstance[] { + return nodes.map((node) => { + const synced = applyPortLabelSync(node); + if (synced.graph) { + return { + ...synced, + graph: { ...synced.graph, nodes: syncPortLabelsRecursively(synced.graph.nodes) } + }; + } + return synced; + }); +} + /** * Load state from JSON */ @@ -43,7 +64,8 @@ export function fromJSON(nodeList: NodeInstance[], connectionList: Connection[], if (!nodeList || !Array.isArray(nodeList)) { rootNodes.set(new Map()); } else { - rootNodes.set(new Map(nodeList.map(n => [n.id, n]))); + const synced = syncPortLabelsRecursively(nodeList); + rootNodes.set(new Map(synced.map(n => [n.id, n]))); } rootConnections.set(connectionList || []); rootAnnotations.set(new Map((annotationList || []).map(a => [a.id, a]))); diff --git a/src/lib/utils/portLabels.ts b/src/lib/utils/portLabels.ts index 67acca7c..501f5eec 100644 --- a/src/lib/utils/portLabels.ts +++ b/src/lib/utils/portLabels.ts @@ -31,12 +31,15 @@ export function getEffectivePortLabelVisibility( } /** - * Truncate port label for display. + * Truncate port label for display. The on-canvas labels are also clipped + * by CSS `text-overflow: ellipsis`, but this provides a hard limit for the + * SVG export path (where ellipsis isn't trivially available) and avoids + * overly long inline strings in the DOM. * * @param name - Port name - * @param maxChars - Maximum characters (default: 5) + * @param maxChars - Maximum characters (default: 8) * @returns Truncated name */ -export function truncatePortLabel(name: string, maxChars: number = 5): string { +export function truncatePortLabel(name: string, maxChars: number = 8): string { return name.length > maxChars ? name.slice(0, maxChars) : name; }