|
1 | 1 | <script lang="ts"> |
2 | | - import { onMount, createEventDispatcher } from 'svelte'; |
| 2 | + import { onMount, onDestroy, createEventDispatcher } from 'svelte'; |
| 3 | + import { fade } from 'svelte/transition'; |
3 | 4 | import { codeToHtml } from 'shiki'; |
4 | 5 | import { FLOW_CODE, getStepFromLine, FLOW_SECTIONS } from '$lib/data/flow-code'; |
5 | 6 | import type { createFlowState } from '$lib/stores/pgflow-state-improved.svelte'; |
6 | 7 | import StatusBadge from '$lib/components/StatusBadge.svelte'; |
7 | 8 | import PulseDot from '$lib/components/PulseDot.svelte'; |
8 | | - import MiniDAG from '$lib/components/MiniDAG.svelte'; |
9 | 9 |
|
10 | 10 | interface Props { |
11 | 11 | flowState: ReturnType<typeof createFlowState>; |
|
16 | 16 | let { flowState, selectedStep, hoveredStep }: Props = $props(); |
17 | 17 |
|
18 | 18 | const dispatch = createEventDispatcher<{ |
19 | | - 'step-selected': { stepSlug: string }; |
| 19 | + 'step-selected': { stepSlug: string | null }; |
20 | 20 | 'step-hovered': { stepSlug: string | null }; |
21 | 21 | }>(); |
22 | 22 |
|
|
25 | 25 | let highlightedSectionsExpanded = $state<Record<string, string>>({}); |
26 | 26 | let codeContainer: HTMLElement | undefined = $state(undefined); |
27 | 27 | let isMobile = $state(false); |
| 28 | + let cleanupHandlers: (() => void) | undefined; |
28 | 29 |
|
29 | 30 | // Section order for mobile rendering |
30 | 31 | const SECTION_ORDER = ['flow_config', 'fetchArticle', 'summarize', 'extractKeywords', 'publish']; |
|
126 | 127 | function setupClickHandlersDelayed() { |
127 | 128 | setTimeout(() => { |
128 | 129 | if (codeContainer) { |
129 | | - setupClickHandlers(); |
| 130 | + cleanupHandlers = setupClickHandlers(); |
130 | 131 | } |
131 | 132 | }, 50); |
132 | 133 | } |
133 | 134 |
|
| 135 | + onDestroy(() => { |
| 136 | + if (cleanupHandlers) { |
| 137 | + cleanupHandlers(); |
| 138 | + } |
| 139 | + }); |
| 140 | +
|
134 | 141 | // Re-setup handlers when view changes |
135 | 142 | $effect(() => { |
136 | 143 | const mobile = isMobile; |
|
145 | 152 | function setupClickHandlers() { |
146 | 153 | if (!codeContainer) return; |
147 | 154 |
|
| 155 | + // Store handlers for cleanup |
| 156 | + const handlers: Array<{ element: Element; type: string; handler: EventListener }> = []; |
| 157 | +
|
148 | 158 | // Find all line elements |
149 | 159 | const lines = codeContainer.querySelectorAll('.line'); |
150 | 160 | lines.forEach((line, index) => { |
|
167 | 177 | dispatch('step-selected', { stepSlug }); |
168 | 178 | }; |
169 | 179 | line.addEventListener('click', clickHandler); |
| 180 | + handlers.push({ element: line, type: 'click', handler: clickHandler }); |
170 | 181 |
|
171 | 182 | // Hover handlers - dispatch hover events (desktop only) |
172 | 183 | if (!isMobile) { |
173 | | - line.addEventListener('mouseenter', () => { |
| 184 | + const enterHandler = () => { |
174 | 185 | dispatch('step-hovered', { stepSlug }); |
175 | | - }); |
176 | | -
|
177 | | - line.addEventListener('mouseleave', () => { |
| 186 | + }; |
| 187 | + const leaveHandler = () => { |
178 | 188 | dispatch('step-hovered', { stepSlug: null }); |
179 | | - }); |
| 189 | + }; |
| 190 | +
|
| 191 | + line.addEventListener('mouseenter', enterHandler); |
| 192 | + line.addEventListener('mouseleave', leaveHandler); |
| 193 | +
|
| 194 | + handlers.push({ element: line, type: 'mouseenter', handler: enterHandler }); |
| 195 | + handlers.push({ element: line, type: 'mouseleave', handler: leaveHandler }); |
180 | 196 | } |
181 | 197 | } |
182 | 198 | }); |
| 199 | +
|
| 200 | + // Return cleanup function |
| 201 | + return () => { |
| 202 | + handlers.forEach(({ element, type, handler }) => { |
| 203 | + element.removeEventListener(type, handler); |
| 204 | + }); |
| 205 | + }; |
183 | 206 | } |
184 | 207 |
|
185 | 208 | // Update line highlighting and borders based on step status, selected, and hovered steps |
|
221 | 244 | <div class="code-panel-wrapper"> |
222 | 245 | {#if isMobile && selectedStep} |
223 | 246 | <!-- Mobile: Show only selected section in explanation panel (expanded version) with optional mini DAG --> |
224 | | - <div class="mobile-code-container"> |
225 | | - <div class="code-panel mobile-selected"> |
| 247 | + {#key selectedStep} |
| 248 | + <div |
| 249 | + class="code-panel mobile-selected" |
| 250 | + in:fade={{ duration: 250 }} |
| 251 | + out:fade={{ duration: 150 }} |
| 252 | + onclick={(e) => { |
| 253 | + // Handle clicks anywhere in code panel |
| 254 | + e.stopPropagation(); |
| 255 | + dispatch('step-selected', { stepSlug: null }); |
| 256 | + }} |
| 257 | + role="button" |
| 258 | + tabindex="0" |
| 259 | + > |
226 | 260 | {#if highlightedSectionsExpanded[selectedStep]} |
227 | 261 | <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
228 | 262 | {@html highlightedSectionsExpanded[selectedStep]} |
229 | 263 | {/if} |
230 | 264 | </div> |
231 | | - {#if selectedStep !== 'flow_config'} |
232 | | - <div class="mini-dag-container"> |
233 | | - <MiniDAG {selectedStep} /> |
234 | | - </div> |
235 | | - {/if} |
236 | | - </div> |
| 265 | + {/key} |
237 | 266 | {:else if isMobile} |
238 | 267 | <!-- Mobile: Show all sections as separate blocks --> |
239 | | - <div class="code-panel mobile-sections"> |
| 268 | + <div |
| 269 | + class="code-panel mobile-sections" |
| 270 | + in:fade={{ duration: 250 }} |
| 271 | + out:fade={{ duration: 150 }} |
| 272 | + > |
240 | 273 | {#each SECTION_ORDER as sectionSlug, index (sectionSlug)} |
241 | 274 | {@const stepStatus = getStepStatus(sectionSlug)} |
242 | 275 | {@const isDimmed = selectedStep && sectionSlug !== selectedStep} |
|
305 | 338 | position: relative; |
306 | 339 | } |
307 | 340 |
|
308 | | - .mobile-code-container { |
309 | | - display: flex; |
310 | | - gap: 12px; |
311 | | - align-items: center; |
312 | | - } |
313 | | -
|
314 | | - .mini-dag-container { |
315 | | - flex-shrink: 0; |
316 | | - width: 95px; |
317 | | - padding-right: 12px; |
318 | | - opacity: 0.7; |
319 | | - } |
320 | | -
|
321 | 341 | .code-panel { |
322 | 342 | overflow-x: auto; |
323 | 343 | border-radius: 5px; |
324 | 344 | } |
325 | 345 |
|
326 | 346 | .code-panel.mobile-selected { |
327 | 347 | /* Compact height when showing only selected step on mobile */ |
328 | | - min-height: auto; |
| 348 | + min-height: 120px; |
329 | 349 | font-size: 12px; |
330 | 350 | background: #0d1117; |
331 | 351 | position: relative; |
332 | 352 | flex: 1; |
| 353 | + cursor: pointer; |
333 | 354 | } |
334 | 355 |
|
335 | 356 | .code-panel.mobile-selected :global(pre) { |
|
344 | 365 | /* Mobile: Container for separate section blocks */ |
345 | 366 | font-size: 12px; |
346 | 367 | border-radius: 0; |
| 368 | + will-change: opacity; |
| 369 | + background: #0d1117; |
347 | 370 | } |
348 | 371 |
|
349 | 372 | /* Mobile: Smaller font, no border radius (touches edges) */ |
|
408 | 431 | border-radius: 5px; |
409 | 432 | line-height: 1.5; |
410 | 433 | font-size: 13px; /* Desktop default */ |
| 434 | + display: table; |
| 435 | + min-width: 100%; |
411 | 436 | } |
412 | 437 |
|
413 | 438 | /* Mobile: Smaller padding */ |
|
0 commit comments