|
19 | 19 | }>(); |
20 | 20 |
|
21 | 21 | let highlightedCode = $state(''); |
| 22 | + let highlightedSections = $state<Record<string, string>>({}); |
22 | 23 | let codeContainer: HTMLElement | undefined = $state(undefined); |
| 24 | + let isMobile = $state(false); |
23 | 25 |
|
24 | 26 | // Calculate step blocks (groups of lines) for status icon positioning |
25 | 27 | const stepBlocks = $derived.by(() => { |
|
45 | 47 | return null; |
46 | 48 | } |
47 | 49 |
|
48 | | - // If flow has started but this step has no status yet, show as created |
| 50 | + // If flow has started but this step has no status yet, don't show indicator |
49 | 51 | if (!status) { |
50 | | - return 'created'; |
| 52 | + return null; |
| 53 | + } |
| 54 | +
|
| 55 | + // Only show indicators for started and completed |
| 56 | + if (status === 'started' || status === 'completed') { |
| 57 | + return status; |
51 | 58 | } |
52 | 59 |
|
53 | | - return status; |
| 60 | + return null; |
54 | 61 | } |
55 | 62 |
|
56 | 63 | onMount(async () => { |
57 | | - // Generate syntax highlighted HTML using Shiki |
| 64 | + // Detect mobile |
| 65 | + isMobile = window.innerWidth < 768; |
| 66 | + window.addEventListener('resize', () => { |
| 67 | + isMobile = window.innerWidth < 768; |
| 68 | + }); |
| 69 | +
|
| 70 | + // Generate syntax highlighted HTML for full code |
58 | 71 | highlightedCode = await codeToHtml(FLOW_CODE, { |
59 | 72 | lang: 'typescript', |
60 | 73 | theme: 'night-owl', |
61 | 74 | transformers: [ |
62 | 75 | { |
63 | 76 | line(node) { |
64 | | - // Add .line class to each line for click handling |
65 | 77 | node.properties.class = 'line'; |
66 | 78 | } |
67 | 79 | } |
68 | 80 | ] |
69 | 81 | }); |
70 | 82 |
|
71 | | - // Add click handlers to lines after rendering - need small delay |
| 83 | + // Generate separate highlighted sections for mobile (use mobileCode if available) |
| 84 | + for (const [slug, section] of Object.entries(FLOW_SECTIONS)) { |
| 85 | + const codeToRender = section.mobileCode || section.code; |
| 86 | + highlightedSections[slug] = await codeToHtml(codeToRender, { |
| 87 | + lang: 'typescript', |
| 88 | + theme: 'night-owl' |
| 89 | + }); |
| 90 | + } |
| 91 | +
|
| 92 | + // Add click handlers to lines after rendering |
| 93 | + setupClickHandlersDelayed(); |
| 94 | + }); |
| 95 | +
|
| 96 | + function setupClickHandlersDelayed() { |
72 | 97 | setTimeout(() => { |
73 | 98 | if (codeContainer) { |
74 | 99 | setupClickHandlers(); |
75 | 100 | } |
76 | 101 | }, 50); |
| 102 | + } |
| 103 | +
|
| 104 | + // Re-setup handlers when view changes |
| 105 | + $effect(() => { |
| 106 | + const mobile = isMobile; |
| 107 | + const selected = selectedStep; |
| 108 | +
|
| 109 | + // Setup handlers for full code view (desktop or mobile with no selection) |
| 110 | + if (codeContainer && (!mobile || !selected || selected === 'flow_config')) { |
| 111 | + setupClickHandlersDelayed(); |
| 112 | + } |
77 | 113 | }); |
78 | 114 |
|
79 | 115 | function setupClickHandlers() { |
|
92 | 128 | (line as HTMLElement).style.cursor = 'pointer'; |
93 | 129 |
|
94 | 130 | // Click handler |
95 | | - line.addEventListener('click', () => { |
| 131 | + const clickHandler = () => { |
96 | 132 | console.log('CodePanel: Line clicked, stepSlug:', stepSlug); |
97 | 133 | // Clear hover state before navigating |
98 | 134 | dispatch('step-hovered', { stepSlug: null }); |
99 | 135 |
|
100 | 136 | // All sections (including flow_config) dispatch their slug |
101 | 137 | dispatch('step-selected', { stepSlug }); |
102 | | - }); |
103 | | -
|
104 | | - // Hover handlers - dispatch hover events |
105 | | - line.addEventListener('mouseenter', () => { |
106 | | - dispatch('step-hovered', { stepSlug }); |
107 | | - }); |
108 | | -
|
109 | | - line.addEventListener('mouseleave', () => { |
110 | | - dispatch('step-hovered', { stepSlug: null }); |
111 | | - }); |
| 138 | + }; |
| 139 | + line.addEventListener('click', clickHandler); |
| 140 | +
|
| 141 | + // Hover handlers - dispatch hover events (desktop only) |
| 142 | + if (!isMobile) { |
| 143 | + line.addEventListener('mouseenter', () => { |
| 144 | + dispatch('step-hovered', { stepSlug }); |
| 145 | + }); |
| 146 | +
|
| 147 | + line.addEventListener('mouseleave', () => { |
| 148 | + dispatch('step-hovered', { stepSlug: null }); |
| 149 | + }); |
| 150 | + } |
112 | 151 | } |
113 | 152 | }); |
114 | 153 | } |
|
124 | 163 | if (!codeContainer) return; |
125 | 164 |
|
126 | 165 | const lines = codeContainer.querySelectorAll('.line'); |
| 166 | +
|
127 | 167 | lines.forEach((line) => { |
128 | 168 | const stepSlug = (line as HTMLElement).getAttribute('data-step'); |
129 | 169 | (line as HTMLElement).classList.remove('line-selected', 'line-hovered', 'line-dimmed'); |
|
149 | 189 | </script> |
150 | 190 |
|
151 | 191 | <div class="code-panel-wrapper"> |
152 | | - <div class="code-panel" bind:this={codeContainer}> |
153 | | - <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
154 | | - {@html highlightedCode} |
155 | | - |
156 | | - <!-- Step status icons overlaid on code blocks --> |
157 | | - {#each stepBlocks as block (block.stepSlug)} |
158 | | - {@const stepStatus = getStepStatus(block.stepSlug)} |
159 | | - {#if stepStatus} |
160 | | - {@const blockHeight = (block.endLine - block.startLine + 1) * 1.5} |
161 | | - {@const blockTop = (block.startLine - 1) * 1.5} |
162 | | - {@const iconTop = blockTop + blockHeight / 2} |
163 | | - {@const isDimmed = selectedStep && block.stepSlug !== selectedStep} |
164 | | - <div |
165 | | - class="step-status-container" |
166 | | - class:status-dimmed={isDimmed} |
167 | | - data-step={block.stepSlug} |
168 | | - data-start-line={block.startLine} |
169 | | - style="top: calc({iconTop}em + 12px);" |
170 | | - > |
171 | | - <StatusBadge status={stepStatus} variant="icon-only" size="xl" /> |
172 | | - </div> |
| 192 | + {#if isMobile && selectedStep} |
| 193 | + <!-- Mobile: Show only selected section (including flow_config) --> |
| 194 | + <div class="code-panel mobile-selected"> |
| 195 | + {#if highlightedSections[selectedStep]} |
| 196 | + <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
| 197 | + {@html highlightedSections[selectedStep]} |
173 | 198 | {/if} |
174 | | - {/each} |
175 | | - </div> |
| 199 | + </div> |
| 200 | + {:else} |
| 201 | + <!-- Desktop or no selection: Show full code --> |
| 202 | + <div class="code-panel" bind:this={codeContainer}> |
| 203 | + <!-- eslint-disable-next-line svelte/no-at-html-tags --> |
| 204 | + {@html highlightedCode} |
| 205 | + |
| 206 | + <!-- Step status indicators --> |
| 207 | + {#each stepBlocks as block (block.stepSlug)} |
| 208 | + {@const stepStatus = getStepStatus(block.stepSlug)} |
| 209 | + {#if stepStatus} |
| 210 | + {@const blockHeight = (block.endLine - block.startLine + 1) * 1.5} |
| 211 | + {@const blockTop = (block.startLine - 1) * 1.5} |
| 212 | + {@const iconTop = blockTop + blockHeight / 2} |
| 213 | + {@const isDimmed = selectedStep && block.stepSlug !== selectedStep} |
| 214 | + |
| 215 | + <!-- Desktop: Icon badge --> |
| 216 | + <div |
| 217 | + class="step-status-container hidden md:block" |
| 218 | + class:status-dimmed={isDimmed} |
| 219 | + data-step={block.stepSlug} |
| 220 | + data-start-line={block.startLine} |
| 221 | + style="top: calc({iconTop}em + 12px);" |
| 222 | + > |
| 223 | + <StatusBadge status={stepStatus} variant="icon-only" size="xl" /> |
| 224 | + </div> |
| 225 | + |
| 226 | + <!-- Mobile: Vertical colored border --> |
| 227 | + <div |
| 228 | + class="step-status-border md:hidden status-{stepStatus}" |
| 229 | + class:status-dimmed={isDimmed} |
| 230 | + data-step={block.stepSlug} |
| 231 | + style="top: calc({blockTop}em + 12px); height: calc({blockHeight}em);" |
| 232 | + ></div> |
| 233 | + {/if} |
| 234 | + {/each} |
| 235 | + </div> |
| 236 | + {/if} |
176 | 237 | </div> |
177 | 238 |
|
178 | 239 | <style> |
|
183 | 244 | .code-panel { |
184 | 245 | overflow-x: auto; |
185 | 246 | border-radius: 5px; |
| 247 | + } |
| 248 | +
|
| 249 | + .code-panel.mobile-selected { |
| 250 | + /* Compact height when showing only selected step on mobile */ |
| 251 | + min-height: auto; |
186 | 252 | font-size: 15px; |
187 | 253 | background: #0d1117; |
188 | 254 | position: relative; |
|
208 | 274 | /* Mobile: Smaller padding */ |
209 | 275 | @media (max-width: 768px) { |
210 | 276 | .code-panel :global(pre) { |
211 | | - padding: 8px 0; |
| 277 | + padding: 16px 8px; |
212 | 278 | } |
213 | 279 | } |
214 | 280 |
|
|
289 | 355 | .step-status-container.status-dimmed { |
290 | 356 | opacity: 0.4; |
291 | 357 | } |
| 358 | +
|
| 359 | + /* Step status border (mobile - vertical bar) */ |
| 360 | + .step-status-border { |
| 361 | + position: absolute; |
| 362 | + left: 0; |
| 363 | + width: 2px; |
| 364 | + pointer-events: none; |
| 365 | + transition: opacity 200ms ease; |
| 366 | + opacity: 1; |
| 367 | + } |
| 368 | +
|
| 369 | + /* Status colors for border based on step status */ |
| 370 | + .step-status-border.status-completed { |
| 371 | + background: #10b981; /* Green */ |
| 372 | + } |
| 373 | +
|
| 374 | + .step-status-border.status-started { |
| 375 | + background: #3b82f6; /* Blue */ |
| 376 | + } |
| 377 | +
|
| 378 | + .step-status-border.status-dimmed { |
| 379 | + opacity: 0.3; |
| 380 | + } |
292 | 381 | </style> |
0 commit comments