|
21 | 21 | import { NODE_TYPES } from '$lib/constants/nodeTypes'; |
22 | 22 | import { exportRecordingData } from '$lib/utils/csvExport'; |
23 | 23 | import { createRecordingDataState } from '$lib/stores/recordingData.svelte'; |
| 24 | + import { getPortLabelConfigs } from '$lib/nodes/uiConfig'; |
| 25 | + import { PORT_NAME } from '$lib/constants/handles'; |
24 | 26 |
|
25 | 27 | // Code preview state (declared early — referenced by subscription below) |
26 | 28 | let showCode = $state(false); |
| 29 | + let showPortLabels = $state(false); |
27 | 30 | let previewCode = $state(''); |
28 | 31 | let editorContainer = $state<HTMLDivElement | undefined>(undefined); |
29 | 32 | let editorView: import('@codemirror/view').EditorView | null = null; |
|
49 | 52 | node = graphStore.getNode(id) || null; |
50 | 53 | // Reset to properties view when opening a new node |
51 | 54 | showCode = false; |
| 55 | + showPortLabels = false; |
52 | 56 | destroyEditor(); |
53 | 57 | } else { |
54 | 58 | node = null; |
55 | 59 | showCode = false; |
| 60 | + showPortLabels = false; |
56 | 61 | destroyEditor(); |
57 | 62 | } |
58 | 63 | }); |
|
144 | 149 | const blockCode = generateBlockCode(node, allNodes, allConnections); |
145 | 150 | previewCode = header + blockCode; |
146 | 151 | copied = false; |
| 152 | + showPortLabels = false; |
147 | 153 | showCode = true; |
148 | 154 | setTimeout(() => initEditor(), 0); |
149 | 155 | } |
150 | 156 | } |
151 | 157 |
|
| 158 | + // Toggle port-labels view (same mutual-exclusion pattern as code view) |
| 159 | + function togglePortLabelsView() { |
| 160 | + if (showPortLabels) { |
| 161 | + showPortLabels = false; |
| 162 | + } else { |
| 163 | + showCode = false; |
| 164 | + destroyEditor(); |
| 165 | + showPortLabels = true; |
| 166 | + } |
| 167 | + } |
| 168 | +
|
152 | 169 | function copyToClipboard() { |
153 | 170 | navigator.clipboard.writeText(previewCode); |
154 | 171 | copied = true; |
|
177 | 194 | closeNodeDialog(); |
178 | 195 | } |
179 | 196 |
|
| 197 | + // Port-label editing — hide for blocks whose port names are driven by a |
| 198 | + // regular param (Scope.labels, Adder.operations, …); for those the param |
| 199 | + // itself is the source of truth and editing port names directly would be |
| 200 | + // overwritten on the next param change. |
| 201 | + const hasParamDrivenPortLabels = $derived(node ? getPortLabelConfigs(node.type).length > 0 : false); |
| 202 | + const hasEditablePortLabels = $derived( |
| 203 | + !!node && !hasParamDrivenPortLabels && (node.inputs.length > 0 || node.outputs.length > 0) |
| 204 | + ); |
| 205 | +
|
| 206 | + function handlePortNameChange(direction: 'input' | 'output', index: number, value: string) { |
| 207 | + if (!node) return; |
| 208 | + const id = node.id; |
| 209 | + const trimmed = value.trim(); |
| 210 | + const fallback = direction === 'input' ? PORT_NAME.input(index) : PORT_NAME.output(index); |
| 211 | + const name = trimmed === '' ? fallback : trimmed; |
| 212 | + historyStore.mutate(() => graphStore.updateNodePortName(id, direction, index, name)); |
| 213 | + } |
| 214 | +
|
180 | 215 | // Check if node is a recording node (Scope or Spectrum) |
181 | 216 | const isRecordingNode = $derived(node?.type === 'Scope' || node?.type === 'Spectrum'); |
182 | 217 |
|
|
256 | 291 | <div class="dialog-header"> |
257 | 292 | {#if showCode} |
258 | 293 | <span id="dialog-title">Python Code</span> |
| 294 | + {:else if showPortLabels} |
| 295 | + <span id="dialog-title">Port Labels</span> |
259 | 296 | {:else} |
260 | 297 | <div class="node-info"> |
261 | 298 | <input |
|
286 | 323 | <Icon name="copy" size={16} /> |
287 | 324 | {/if} |
288 | 325 | </button> |
289 | | - {:else} |
| 326 | + {:else if !showPortLabels} |
290 | 327 | <!-- Color picker --> |
291 | 328 | <ColorPicker |
292 | 329 | currentColor={currentColor} |
|
328 | 365 | </button> |
329 | 366 | {/if} |
330 | 367 | {/if} |
331 | | - <!-- Toggle code view button --> |
332 | | - <button |
333 | | - class="icon-btn" |
334 | | - onclick={toggleCodeView} |
335 | | - use:tooltip={showCode ? "View Properties" : "View Python Code"} |
336 | | - aria-label={showCode ? "View Properties" : "View Python Code"} |
337 | | - > |
338 | | - <Icon name={showCode ? "settings" : "braces"} size={16} /> |
339 | | - </button> |
| 368 | + <!-- Toggle port labels view button (hidden in code view) --> |
| 369 | + {#if showPortLabels || (!showCode && hasEditablePortLabels)} |
| 370 | + <button |
| 371 | + class="icon-btn" |
| 372 | + onclick={togglePortLabelsView} |
| 373 | + use:tooltip={showPortLabels ? "View Properties" : "Edit Port Labels"} |
| 374 | + aria-label={showPortLabels ? "View Properties" : "Edit Port Labels"} |
| 375 | + > |
| 376 | + <Icon name={showPortLabels ? "settings" : "tag"} size={16} /> |
| 377 | + </button> |
| 378 | + {/if} |
| 379 | + <!-- Toggle code view button (hidden in port-labels view) --> |
| 380 | + {#if !showPortLabels} |
| 381 | + <button |
| 382 | + class="icon-btn" |
| 383 | + onclick={toggleCodeView} |
| 384 | + use:tooltip={showCode ? "View Properties" : "View Python Code"} |
| 385 | + aria-label={showCode ? "View Properties" : "View Python Code"} |
| 386 | + > |
| 387 | + <Icon name={showCode ? "settings" : "braces"} size={16} /> |
| 388 | + </button> |
| 389 | + {/if} |
340 | 390 | <button class="icon-btn" onclick={closeNodeDialog} aria-label="Close"> |
341 | 391 | <Icon name="x" size={16} /> |
342 | 392 | </button> |
|
351 | 401 | <div class="loading">Loading...</div> |
352 | 402 | {/if} |
353 | 403 | </div> |
| 404 | + {:else if showPortLabels} |
| 405 | + <!-- Port labels view --> |
| 406 | + {#if node.inputs.length === 0 && node.outputs.length === 0} |
| 407 | + <div class="no-params">No ports to label</div> |
| 408 | + {:else} |
| 409 | + <div class="section"> |
| 410 | + <div class="params-grid"> |
| 411 | + {#each node.inputs as port, i (port.id)} |
| 412 | + <div class="param-item"> |
| 413 | + <label for="port-in-{i}">in {i}</label> |
| 414 | + <input |
| 415 | + id="port-in-{i}" |
| 416 | + type="text" |
| 417 | + value={port.name} |
| 418 | + placeholder={PORT_NAME.input(i)} |
| 419 | + onchange={(e) => handlePortNameChange('input', i, e.currentTarget.value)} |
| 420 | + /> |
| 421 | + </div> |
| 422 | + {/each} |
| 423 | + {#if node.inputs.length > 0 && node.outputs.length > 0} |
| 424 | + <div class="port-divider"></div> |
| 425 | + {/if} |
| 426 | + {#each node.outputs as port, i (port.id)} |
| 427 | + <div class="param-item"> |
| 428 | + <label for="port-out-{i}">out {i}</label> |
| 429 | + <input |
| 430 | + id="port-out-{i}" |
| 431 | + type="text" |
| 432 | + value={port.name} |
| 433 | + placeholder={PORT_NAME.output(i)} |
| 434 | + onchange={(e) => handlePortNameChange('output', i, e.currentTarget.value)} |
| 435 | + /> |
| 436 | + </div> |
| 437 | + {/each} |
| 438 | + </div> |
| 439 | + </div> |
| 440 | + {/if} |
354 | 441 | {:else} |
355 | 442 | <!-- Parameters --> |
356 | 443 | {#if typeDef.params.length > 0} |
|
413 | 500 | {/if} |
414 | 501 | </div> |
415 | 502 |
|
416 | | - {#if !showCode} |
| 503 | + {#if !showCode && !showPortLabels} |
417 | 504 | <div class="dialog-footer"> |
418 | 505 | <span class="hint">R rotate · X flip horizontal · Y flip vertical</span> |
419 | 506 | </div> |
|
436 | 523 | min-width: 0; |
437 | 524 | } |
438 | 525 |
|
| 526 | + .port-divider { |
| 527 | + height: 1px; |
| 528 | + background: var(--border); |
| 529 | + /* Extend past the dialog-body padding so the line spans edge-to-edge. */ |
| 530 | + margin: var(--space-xs) calc(-1 * var(--space-md)); |
| 531 | + } |
| 532 | +
|
439 | 533 | /* Pin button */ |
440 | 534 | .pin-btn { |
441 | 535 | flex-shrink: 0; |
|
0 commit comments