Skip to content

Commit 247a4f4

Browse files
committed
feat: demo - split code block to separate
1 parent 5c62786 commit 247a4f4

File tree

5 files changed

+212
-157
lines changed

5 files changed

+212
-157
lines changed

apps/demo/src/lib/components/CodePanel.svelte

Lines changed: 132 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { FLOW_CODE, getStepFromLine, FLOW_SECTIONS } from '$lib/data/flow-code';
55
import type { createFlowState } from '$lib/stores/pgflow-state-improved.svelte';
66
import StatusBadge from '$lib/components/StatusBadge.svelte';
7+
import PulseDot from '$lib/components/PulseDot.svelte';
78
89
interface Props {
910
flowState: ReturnType<typeof createFlowState>;
@@ -20,9 +21,13 @@
2021
2122
let highlightedCode = $state('');
2223
let highlightedSections = $state<Record<string, string>>({});
24+
let highlightedSectionsExpanded = $state<Record<string, string>>({});
2325
let codeContainer: HTMLElement | undefined = $state(undefined);
2426
let isMobile = $state(false);
2527
28+
// Section order for mobile rendering
29+
const SECTION_ORDER = ['flow_config', 'fetch_article', 'summarize', 'extract_keywords', 'publish'];
30+
2631
// Calculate step blocks (groups of lines) for status icon positioning
2732
const stepBlocks = $derived.by(() => {
2833
const blocks: Array<{ stepSlug: string; startLine: number; endLine: number }> = [];
@@ -80,10 +85,17 @@
8085
]
8186
});
8287
83-
// Generate separate highlighted sections for mobile (use mobileCode if available)
88+
// Generate separate highlighted sections
8489
for (const [slug, section] of Object.entries(FLOW_SECTIONS)) {
85-
const codeToRender = section.mobileCode || section.code;
86-
highlightedSections[slug] = await codeToHtml(codeToRender, {
90+
// Compact code for main mobile panel
91+
highlightedSections[slug] = await codeToHtml(section.code, {
92+
lang: 'typescript',
93+
theme: 'night-owl'
94+
});
95+
96+
// Expanded code for explanation panel (mobile-selected)
97+
const expandedCode = section.mobileCode || section.code;
98+
highlightedSectionsExpanded[slug] = await codeToHtml(expandedCode, {
8799
lang: 'typescript',
88100
theme: 'night-owl'
89101
});
@@ -190,15 +202,50 @@
190202

191203
<div class="code-panel-wrapper">
192204
{#if isMobile && selectedStep}
193-
<!-- Mobile: Show only selected section (including flow_config) -->
205+
<!-- Mobile: Show only selected section in explanation panel (expanded version) -->
194206
<div class="code-panel mobile-selected">
195-
{#if highlightedSections[selectedStep]}
207+
{#if highlightedSectionsExpanded[selectedStep]}
196208
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
197-
{@html highlightedSections[selectedStep]}
209+
{@html highlightedSectionsExpanded[selectedStep]}
198210
{/if}
199211
</div>
212+
{:else if isMobile}
213+
<!-- Mobile: Show all sections as separate blocks -->
214+
<div class="code-panel mobile-sections">
215+
{#each SECTION_ORDER as sectionSlug, index (sectionSlug)}
216+
{@const stepStatus = getStepStatus(sectionSlug)}
217+
{@const isDimmed = selectedStep && sectionSlug !== selectedStep}
218+
{@const isFirst = index === 0}
219+
{@const isLast = index === SECTION_ORDER.length - 1}
220+
<div
221+
class="code-section"
222+
class:section-dimmed={isDimmed}
223+
class:section-selected={selectedStep === sectionSlug}
224+
class:section-first={isFirst}
225+
class:section-last={isLast}
226+
data-step={sectionSlug}
227+
onclick={() => dispatch('step-selected', { stepSlug: sectionSlug })}
228+
role="button"
229+
tabindex="0"
230+
>
231+
<!-- Status indicator: left border -->
232+
{#if stepStatus}
233+
<div class="section-status-border status-{stepStatus}"></div>
234+
{/if}
235+
236+
<!-- Pulse dot (centered) -->
237+
<div class="pulse-dot-container">
238+
<PulseDot />
239+
</div>
240+
241+
<!-- Code content -->
242+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
243+
{@html highlightedSections[sectionSlug]}
244+
</div>
245+
{/each}
246+
</div>
200247
{:else}
201-
<!-- Desktop or no selection: Show full code -->
248+
<!-- Desktop: Show full code (unchanged) -->
202249
<div class="code-panel" bind:this={codeContainer}>
203250
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
204251
{@html highlightedCode}
@@ -222,14 +269,6 @@
222269
>
223270
<StatusBadge status={stepStatus} variant="icon-only" size="xl" />
224271
</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>
233272
{/if}
234273
{/each}
235274
</div>
@@ -249,11 +288,17 @@
249288
.code-panel.mobile-selected {
250289
/* Compact height when showing only selected step on mobile */
251290
min-height: auto;
252-
font-size: 15px;
291+
font-size: 13px;
253292
background: #0d1117;
254293
position: relative;
255294
}
256295
296+
.code-panel.mobile-sections {
297+
/* Mobile: Container for separate section blocks */
298+
font-size: 11px;
299+
border-radius: 0;
300+
}
301+
257302
/* Mobile: Smaller font, no border radius (touches edges) */
258303
@media (max-width: 768px) {
259304
.code-panel {
@@ -262,6 +307,52 @@
262307
}
263308
}
264309
310+
/* Mobile: Individual code section */
311+
.code-section {
312+
position: relative;
313+
cursor: pointer;
314+
transition:
315+
opacity 200ms ease,
316+
background-color 200ms ease;
317+
}
318+
319+
.code-section.section-dimmed {
320+
opacity: 0.4;
321+
}
322+
323+
.code-section.section-selected {
324+
background-color: rgba(88, 166, 255, 0.22);
325+
opacity: 1;
326+
}
327+
328+
/* Status border for mobile sections */
329+
.section-status-border {
330+
position: absolute;
331+
left: 0;
332+
top: 0;
333+
bottom: 0;
334+
width: 2px;
335+
pointer-events: none;
336+
}
337+
338+
.section-status-border.status-completed {
339+
background: #10b981; /* Green */
340+
}
341+
342+
.section-status-border.status-started {
343+
background: #3b82f6; /* Blue */
344+
}
345+
346+
/* Pulse dot container - centers the dot */
347+
.pulse-dot-container {
348+
position: absolute;
349+
top: 50%;
350+
left: 50%;
351+
transform: translate(-50%, -50%);
352+
pointer-events: none;
353+
z-index: 10;
354+
}
355+
265356
/* Override Shiki's default pre styling */
266357
.code-panel :global(pre) {
267358
margin: 0;
@@ -278,6 +369,31 @@
278369
}
279370
}
280371
372+
/* Mobile sections: Seamless styling */
373+
.mobile-sections .code-section :global(pre) {
374+
margin: 0;
375+
padding: 16px 8px;
376+
background: #0d1117 !important;
377+
border-radius: 0;
378+
line-height: 1.5;
379+
}
380+
381+
/* First section: keep top padding, remove bottom */
382+
.mobile-sections .code-section.section-first :global(pre) {
383+
padding-bottom: 0;
384+
}
385+
386+
/* Middle sections: remove all vertical padding */
387+
.mobile-sections .code-section:not(.section-first):not(.section-last) :global(pre) {
388+
padding-top: 0;
389+
padding-bottom: 0;
390+
}
391+
392+
/* Last section: keep bottom padding, remove top */
393+
.mobile-sections .code-section.section-last :global(pre) {
394+
padding-top: 0;
395+
}
396+
281397
.code-panel :global(code) {
282398
font-family: 'Fira Code', 'Monaco', 'Menlo', 'Courier New', monospace;
283399
}

apps/demo/src/lib/components/ExplanationPanel.svelte

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@
3434
dispatch('step-hovered', { stepSlug });
3535
}
3636
37-
// Check if a dependency button should be dimmed (when hovering something else)
38-
function isDepDimmed(depSlug: string): boolean {
39-
return hoveredStep !== null && hoveredStep !== depSlug;
40-
}
41-
4237
// Flow-level metadata for explanation
4338
const flowInfo = {
4439
name: 'article_flow',
@@ -317,11 +312,7 @@
317312
<div class="flex flex-col gap-1.5">
318313
{#each currentStepInfo.dependsOn as dep (dep)}
319314
<button
320-
class="font-mono text-sm px-2 py-1.5 rounded bg-secondary hover:bg-secondary/80 transition-all duration-200 cursor-pointer border border-transparent hover:border-blue-500 text-left {isDepDimmed(
321-
dep
322-
)
323-
? 'opacity-30'
324-
: 'opacity-100'}"
315+
class="font-mono text-sm px-2 py-1.5 rounded bg-secondary hover:bg-secondary/80 transition-all duration-200 cursor-pointer border border-transparent hover:border-blue-500 text-left"
325316
onclick={(e) => handleDependencyClick(dep, e)}
326317
onmouseenter={() => handleDependencyHover(dep)}
327318
onmouseleave={() => handleDependencyHover(null)}
@@ -342,11 +333,7 @@
342333
<div class="flex flex-col gap-1.5">
343334
{#each currentStepInfo.dependents as dep (dep)}
344335
<button
345-
class="font-mono text-sm px-2 py-1.5 rounded bg-secondary hover:bg-secondary/80 transition-all duration-200 cursor-pointer border border-transparent hover:border-blue-500 text-left {isDepDimmed(
346-
dep
347-
)
348-
? 'opacity-30'
349-
: 'opacity-100'}"
336+
class="font-mono text-sm px-2 py-1.5 rounded bg-secondary hover:bg-secondary/80 transition-all duration-200 cursor-pointer border border-transparent hover:border-blue-500 text-left"
350337
onclick={(e) => handleDependencyClick(dep, e)}
351338
onmouseenter={() => handleDependencyHover(dep)}
352339
onmouseleave={() => handleDependencyHover(null)}
@@ -423,11 +410,7 @@
423410
<div class="flex flex-col gap-1.5">
424411
{#each flowInfo.steps as step (step)}
425412
<button
426-
class="font-mono text-sm px-2 py-1.5 rounded bg-secondary hover:bg-secondary/80 transition-all duration-200 cursor-pointer border border-transparent hover:border-blue-500 text-left {isDepDimmed(
427-
step
428-
)
429-
? 'opacity-30'
430-
: 'opacity-100'}"
413+
class="font-mono text-sm px-2 py-1.5 rounded bg-secondary hover:bg-secondary/80 transition-all duration-200 cursor-pointer border border-transparent hover:border-blue-500 text-left"
431414
onclick={(e) => handleDependencyClick(step, e)}
432415
onmouseenter={() => handleDependencyHover(step)}
433416
onmouseleave={() => handleDependencyHover(null)}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script lang="ts">
2+
import { pulseDots } from '$lib/stores/pulse-dots.svelte';
3+
4+
// Component shows/hides based on global pulsing state
5+
const visible = $derived(pulseDots.pulsing);
6+
</script>
7+
8+
{#if visible}
9+
<div class="pulse-dot"></div>
10+
{/if}
11+
12+
<style>
13+
.pulse-dot {
14+
width: 10px;
15+
height: 10px;
16+
background: rgba(255, 159, 28, 1);
17+
border: 2px solid rgba(255, 255, 255, 0.9);
18+
border-radius: 50%;
19+
pointer-events: none;
20+
animation: pulse-dot 1s ease-out 3;
21+
box-shadow: 0 0 8px rgba(255, 159, 28, 0.8);
22+
23+
/* Center in parent container */
24+
position: absolute;
25+
top: 50%;
26+
left: 50%;
27+
transform: translate(-50%, -50%);
28+
}
29+
30+
@keyframes pulse-dot {
31+
0% {
32+
box-shadow:
33+
0 0 8px rgba(255, 159, 28, 0.8),
34+
0 0 0 0 rgba(255, 159, 28, 0.7);
35+
opacity: 1;
36+
}
37+
50% {
38+
box-shadow:
39+
0 0 12px rgba(255, 159, 28, 1),
40+
0 0 0 16px rgba(255, 159, 28, 0);
41+
opacity: 0.9;
42+
}
43+
100% {
44+
box-shadow:
45+
0 0 8px rgba(255, 159, 28, 0.8),
46+
0 0 0 0 rgba(255, 159, 28, 0);
47+
opacity: 1;
48+
}
49+
}
50+
</style>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Global store for triggering pulse dot animations
3+
* Simple trigger/dismiss mechanism - all mounted PulseDot components respond
4+
*/
5+
6+
let pulsing = $state(false);
7+
8+
export const pulseDots = {
9+
get pulsing() {
10+
return pulsing;
11+
},
12+
13+
trigger() {
14+
pulsing = true;
15+
// Auto-dismiss after animation completes (3 seconds)
16+
setTimeout(() => {
17+
pulsing = false;
18+
}, 3000);
19+
},
20+
21+
dismiss() {
22+
pulsing = false;
23+
}
24+
};

0 commit comments

Comments
 (0)