Skip to content

Commit 778f391

Browse files
committed
feat: enhance mobile UI with animations and improved DAG visualization (#329)
# Enhanced Mobile UI and Interaction Experience This PR improves the mobile user experience with smoother animations, better event visualization, and more intuitive interactions: - Added transition animations using Svelte's `fade` and `fly` effects for smoother UI transitions - Created a new `DAGNode` component with integrated pulse animation for better workflow visualization - Improved the mobile code panel with fade transitions and tap-to-dismiss functionality - Enhanced the explanation panel with clearer input/output displays and more concise step descriptions - Redesigned the welcome modal with clearer explanations and better layout - Added a mobile-friendly events bar that shows event badges and expands to a full event stream view - Improved the pulse dot animation with a new "exploding" effect for better visual feedback - Updated the DAG visualization to use the new node component and improved layout - Optimized the worker configuration with faster polling intervals The changes focus on making the demo more intuitive on mobile devices while maintaining the desktop experience, with special attention to animation timing, touch interactions, and visual feedback.
1 parent ae99c6e commit 778f391

File tree

13 files changed

+1084
-468
lines changed

13 files changed

+1084
-468
lines changed

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

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<script lang="ts">
2-
import { onMount, createEventDispatcher } from 'svelte';
2+
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
3+
import { fade } from 'svelte/transition';
34
import { codeToHtml } from 'shiki';
45
import { FLOW_CODE, getStepFromLine, FLOW_SECTIONS } from '$lib/data/flow-code';
56
import type { createFlowState } from '$lib/stores/pgflow-state-improved.svelte';
67
import StatusBadge from '$lib/components/StatusBadge.svelte';
78
import PulseDot from '$lib/components/PulseDot.svelte';
8-
import MiniDAG from '$lib/components/MiniDAG.svelte';
99
1010
interface Props {
1111
flowState: ReturnType<typeof createFlowState>;
@@ -16,7 +16,7 @@
1616
let { flowState, selectedStep, hoveredStep }: Props = $props();
1717
1818
const dispatch = createEventDispatcher<{
19-
'step-selected': { stepSlug: string };
19+
'step-selected': { stepSlug: string | null };
2020
'step-hovered': { stepSlug: string | null };
2121
}>();
2222
@@ -25,6 +25,7 @@
2525
let highlightedSectionsExpanded = $state<Record<string, string>>({});
2626
let codeContainer: HTMLElement | undefined = $state(undefined);
2727
let isMobile = $state(false);
28+
let cleanupHandlers: (() => void) | undefined;
2829
2930
// Section order for mobile rendering
3031
const SECTION_ORDER = ['flow_config', 'fetchArticle', 'summarize', 'extractKeywords', 'publish'];
@@ -126,11 +127,17 @@
126127
function setupClickHandlersDelayed() {
127128
setTimeout(() => {
128129
if (codeContainer) {
129-
setupClickHandlers();
130+
cleanupHandlers = setupClickHandlers();
130131
}
131132
}, 50);
132133
}
133134
135+
onDestroy(() => {
136+
if (cleanupHandlers) {
137+
cleanupHandlers();
138+
}
139+
});
140+
134141
// Re-setup handlers when view changes
135142
$effect(() => {
136143
const mobile = isMobile;
@@ -145,6 +152,9 @@
145152
function setupClickHandlers() {
146153
if (!codeContainer) return;
147154
155+
// Store handlers for cleanup
156+
const handlers: Array<{ element: Element; type: string; handler: EventListener }> = [];
157+
148158
// Find all line elements
149159
const lines = codeContainer.querySelectorAll('.line');
150160
lines.forEach((line, index) => {
@@ -167,19 +177,32 @@
167177
dispatch('step-selected', { stepSlug });
168178
};
169179
line.addEventListener('click', clickHandler);
180+
handlers.push({ element: line, type: 'click', handler: clickHandler });
170181
171182
// Hover handlers - dispatch hover events (desktop only)
172183
if (!isMobile) {
173-
line.addEventListener('mouseenter', () => {
184+
const enterHandler = () => {
174185
dispatch('step-hovered', { stepSlug });
175-
});
176-
177-
line.addEventListener('mouseleave', () => {
186+
};
187+
const leaveHandler = () => {
178188
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 });
180196
}
181197
}
182198
});
199+
200+
// Return cleanup function
201+
return () => {
202+
handlers.forEach(({ element, type, handler }) => {
203+
element.removeEventListener(type, handler);
204+
});
205+
};
183206
}
184207
185208
// Update line highlighting and borders based on step status, selected, and hovered steps
@@ -221,22 +244,32 @@
221244
<div class="code-panel-wrapper">
222245
{#if isMobile && selectedStep}
223246
<!-- 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+
>
226260
{#if highlightedSectionsExpanded[selectedStep]}
227261
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
228262
{@html highlightedSectionsExpanded[selectedStep]}
229263
{/if}
230264
</div>
231-
{#if selectedStep !== 'flow_config'}
232-
<div class="mini-dag-container">
233-
<MiniDAG {selectedStep} />
234-
</div>
235-
{/if}
236-
</div>
265+
{/key}
237266
{:else if isMobile}
238267
<!-- 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+
>
240273
{#each SECTION_ORDER as sectionSlug, index (sectionSlug)}
241274
{@const stepStatus = getStepStatus(sectionSlug)}
242275
{@const isDimmed = selectedStep && sectionSlug !== selectedStep}
@@ -305,31 +338,19 @@
305338
position: relative;
306339
}
307340
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-
321341
.code-panel {
322342
overflow-x: auto;
323343
border-radius: 5px;
324344
}
325345
326346
.code-panel.mobile-selected {
327347
/* Compact height when showing only selected step on mobile */
328-
min-height: auto;
348+
min-height: 120px;
329349
font-size: 12px;
330350
background: #0d1117;
331351
position: relative;
332352
flex: 1;
353+
cursor: pointer;
333354
}
334355
335356
.code-panel.mobile-selected :global(pre) {
@@ -344,6 +365,8 @@
344365
/* Mobile: Container for separate section blocks */
345366
font-size: 12px;
346367
border-radius: 0;
368+
will-change: opacity;
369+
background: #0d1117;
347370
}
348371
349372
/* Mobile: Smaller font, no border radius (touches edges) */
@@ -408,6 +431,8 @@
408431
border-radius: 5px;
409432
line-height: 1.5;
410433
font-size: 13px; /* Desktop default */
434+
display: table;
435+
min-width: 100%;
411436
}
412437
413438
/* Mobile: Smaller padding */
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
import { Handle, Position } from '@xyflow/svelte';
3+
import PulseDot from './PulseDot.svelte';
4+
5+
interface Props {
6+
data: { label: string };
7+
}
8+
9+
let { data }: Props = $props();
10+
</script>
11+
12+
<div class="dag-node-content">
13+
<PulseDot />
14+
{data.label}
15+
<Handle type="target" position={Position.Top} />
16+
<Handle type="source" position={Position.Bottom} />
17+
</div>
18+
19+
<style>
20+
.dag-node-content {
21+
position: relative;
22+
padding: 6px;
23+
font-size: 14px;
24+
text-align: center;
25+
min-width: 120px;
26+
width: 120px;
27+
box-sizing: border-box;
28+
display: flex;
29+
align-items: center;
30+
justify-content: center;
31+
}
32+
</style>

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { SvelteFlow } from '@xyflow/svelte';
44
import '@xyflow/svelte/dist/style.css';
55
import type { createFlowState } from '$lib/stores/pgflow-state-improved.svelte';
6+
import DAGNode from './DAGNode.svelte';
67
78
interface Props {
89
flowState: ReturnType<typeof createFlowState>;
@@ -15,6 +16,11 @@
1516
let containerElement: HTMLDivElement | undefined = $state(undefined);
1617
let shouldFitView = $state(true);
1718
19+
// Custom node types with PulseDot
20+
const nodeTypes = {
21+
dagNode: DAGNode
22+
};
23+
1824
// Re-center when container or window resizes
1925
onMount(() => {
2026
const handleResize = () => {
@@ -88,37 +94,37 @@
8894
}
8995
9096
// Define the 4-step DAG structure - reactive to step states and selection
91-
// Vertical spacing between nodes (81px between levels)
97+
// Vertical spacing between nodes (110px between levels)
9298
// Shifted up by 30px to center better in viewport
9399
let nodes = $derived([
94100
{
95101
id: 'fetchArticle',
96-
type: 'default',
102+
type: 'dagNode',
97103
position: { x: 150, y: -30 },
98104
data: { label: 'fetchArticle' },
99105
class: getNodeClass('fetchArticle'),
100106
draggable: false
101107
},
102108
{
103109
id: 'summarize',
104-
type: 'default',
105-
position: { x: 50, y: 51 },
110+
type: 'dagNode',
111+
position: { x: 50, y: 80 },
106112
data: { label: 'summarize' },
107113
class: getNodeClass('summarize'),
108114
draggable: false
109115
},
110116
{
111117
id: 'extractKeywords',
112-
type: 'default',
113-
position: { x: 250, y: 51 },
118+
type: 'dagNode',
119+
position: { x: 250, y: 80 },
114120
data: { label: 'extractKeywords' },
115121
class: getNodeClass('extractKeywords'),
116122
draggable: false
117123
},
118124
{
119125
id: 'publish',
120-
type: 'default',
121-
position: { x: 150, y: 132 },
126+
type: 'dagNode',
127+
position: { x: 150, y: 190 },
122128
data: { label: 'publish' },
123129
class: getNodeClass('publish'),
124130
draggable: false
@@ -223,8 +229,9 @@
223229
<SvelteFlow
224230
{nodes}
225231
{edges}
232+
{nodeTypes}
226233
fitView={shouldFitView}
227-
fitViewOptions={{ padding: 0.1 }}
234+
fitViewOptions={{ padding: 0.15 }}
228235
panOnDrag={false}
229236
zoomOnScroll={false}
230237
zoomOnPinch={false}

0 commit comments

Comments
 (0)