Skip to content

Commit 43114e9

Browse files
committed
fix(docs): preserve focus in workflow illustrations
1 parent eeae10f commit 43114e9

5 files changed

Lines changed: 66 additions & 13 deletions

File tree

apps/docs/components/workflow-preview/docs-block-node.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client'
22

33
import { type ComponentType, memo, useContext } from 'react'
4+
import { cn } from '@sim/emcn'
45
import {
56
type CanvasSentenceSegment,
67
CanvasSentenceView,
@@ -13,6 +14,7 @@ import { normalizeBlockType, resolveIcon } from '@/components/workflow-preview/b
1314
import { PreviewSelectionContext } from '@/components/workflow-preview/preview-selection-context'
1415
import {
1516
BLOCK_STAGGER,
17+
DIMMED_PREVIEW_CLASS,
1618
EASE_OUT,
1719
type PreviewTool,
1820
} from '@/components/workflow-preview/workflow-data'
@@ -38,6 +40,7 @@ export interface DocsBlockData extends Record<string, unknown> {
3840
index?: number
3941
animate?: boolean
4042
isHighlighted?: boolean
43+
isDimmed?: boolean
4144
}
4245

4346
export type DocsBlockNodeType = Node<DocsBlockData, 'previewBlock'>
@@ -66,6 +69,7 @@ export const DocsBlockNode = memo(function DocsBlockNode({
6669
index = 0,
6770
animate = false,
6871
isHighlighted = false,
72+
isDimmed = false,
6973
} = data
7074

7175
/** The View gates router handle topology on `type === 'router_v2'`. */
@@ -133,7 +137,7 @@ export const DocsBlockNode = memo(function DocsBlockNode({
133137

134138
return (
135139
<m.div
136-
className='relative transition-opacity duration-300'
140+
className={cn('relative transition-opacity duration-300', isDimmed && DIMMED_PREVIEW_CLASS)}
137141
initial={animate ? { opacity: 0 } : false}
138142
animate={{ opacity: 1 }}
139143
transition={{ duration: 0.45, delay, ease: EASE_OUT }}
Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
'use client'
22

33
import { memo, useContext } from 'react'
4+
import { cn } from '@sim/emcn'
45
import { type SubflowNodeData, SubflowNodeView } from '@sim/workflow-renderer'
56
import type { Node, NodeProps } from '@xyflow/react'
67
import { PreviewSelectionContext } from '@/components/workflow-preview/preview-selection-context'
8+
import { DIMMED_PREVIEW_CLASS } from '@/components/workflow-preview/workflow-data'
79

810
export interface DocsContainerData extends Record<string, unknown> {
911
name: string
1012
blockType: string
1113
size?: { width: number; height: number }
1214
parentId?: string
1315
isHighlighted?: boolean
16+
isDimmed?: boolean
1417
}
1518

1619
export type DocsContainerNodeType = Node<DocsContainerData, 'previewContainer'>
1720

1821
/**
1922
* Docs adapter for loop/parallel container blocks: maps the static preview data
20-
* to {@link SubflowNodeView}'s read-only `isPreview` shape. Carries no stores,
21-
* hooks, or queries — it only reshapes data into View props.
23+
* to {@link SubflowNodeView}'s read-only `isPreview` shape. Carries no stores
24+
* or queries — it only reshapes data into View props.
2225
*/
2326
export const DocsContainerNode = memo(function DocsContainerNode({
2427
id,
@@ -36,15 +39,17 @@ export const DocsContainerNode = memo(function DocsContainerNode({
3639
}
3740

3841
return (
39-
<SubflowNodeView
40-
id={id}
41-
data={subflowData}
42-
isEnabled
43-
isLocked={false}
44-
isFocused={false}
45-
nestingLevel={0}
46-
canEditWorkflow={false}
47-
onSelect={() => selectBlock?.(id)}
48-
/>
42+
<div className={cn('h-full w-full', data.isDimmed && DIMMED_PREVIEW_CLASS)}>
43+
<SubflowNodeView
44+
id={id}
45+
data={subflowData}
46+
isEnabled
47+
isLocked={false}
48+
isFocused={false}
49+
nestingLevel={0}
50+
canEditWorkflow={false}
51+
onSelect={() => selectBlock?.(id)}
52+
/>
53+
</div>
4954
)
5055
})

apps/docs/components/workflow-preview/workflow-data.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,40 @@ describe('authored canvas presentation', () => {
106106
}
107107
})
108108
})
109+
110+
describe('authored diagram focus', () => {
111+
it('keeps every node visible when no step is emphasized', () => {
112+
const { nodes } = toReactFlowElements(workflow)
113+
expect(nodes.every((node) => !node.data.isDimmed)).toBe(true)
114+
})
115+
116+
it('de-emphasizes surrounding cards and containers for a highlighted step', () => {
117+
const { nodes } = toReactFlowElements(workflow, false, { highlightBlock: 'agent' })
118+
expect(nodes.filter((node) => node.data.isDimmed).map((node) => node.id)).toEqual([
119+
'start',
120+
'loop',
121+
'parallel',
122+
])
123+
expect(nodes.find((node) => node.id === 'agent')?.data.isHighlighted).toBe(true)
124+
})
125+
126+
it('keeps both the authored focus and inspected block at full emphasis', () => {
127+
const { nodes } = toReactFlowElements(workflow, false, {
128+
highlightBlock: 'agent',
129+
selectedBlock: 'loop',
130+
})
131+
expect(nodes.filter((node) => !node.data.isDimmed).map((node) => node.id)).toEqual([
132+
'loop',
133+
'agent',
134+
])
135+
})
136+
137+
it('de-emphasizes nodes when the illustration focuses on an edge', () => {
138+
const { nodes, edges } = toReactFlowElements(workflow, false, {
139+
highlightEdge: 'loop-agent',
140+
})
141+
expect(nodes.every((node) => node.data.isDimmed)).toBe(true)
142+
expect(edges.find((edge) => edge.id === 'loop-agent')?.style?.opacity).toBe(1)
143+
expect(edges.find((edge) => edge.id === 'start-loop')?.style?.opacity).toBeLessThan(1)
144+
})
145+
})

apps/docs/components/workflow-preview/workflow-data.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ const EDGE_STYLE_HIGHLIGHT = { stroke: 'var(--brand-secondary)', strokeWidth: 2.
8282
/** Edges leaving a block's error port render red, matching the editor. */
8383
const EDGE_STYLE_ERROR = { stroke: 'var(--text-error)', strokeWidth: 2 } as const
8484

85+
/** De-emphasizes diagram context without reducing the contrast of its text. */
86+
export const DIMMED_PREVIEW_CLASS =
87+
'[&_[data-workflow-block-border]]:opacity-35 [&_[data-workflow-type-accent]]:grayscale [&_[data-subflow-type-tag]]:grayscale'
88+
8589
/** Optional emphasis: light one block or one edge and dim everything else. */
8690
export interface HighlightOptions {
8791
highlightBlock?: string
@@ -165,6 +169,7 @@ export function toReactFlowElements(
165169
index,
166170
animate,
167171
isHighlighted: highlightBlock === block.id || selectedBlock === block.id,
172+
isDimmed: hasHighlight && highlightBlock !== block.id && selectedBlock !== block.id,
168173
}
169174
return {
170175
...commonNode,
@@ -191,6 +196,7 @@ export function toReactFlowElements(
191196
index,
192197
animate,
193198
isHighlighted: highlightBlock === block.id || selectedBlock === block.id,
199+
isDimmed: hasHighlight && highlightBlock !== block.id && selectedBlock !== block.id,
194200
}
195201
return {
196202
...commonNode,

packages/workflow-renderer/src/workflow-block/workflow-block-border.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,6 +1717,7 @@ export function WorkflowBlockBorder({
17171717
return (
17181718
<svg
17191719
ref={svgRef}
1720+
data-workflow-block-border=''
17201721
viewBox={`${-BORDER_PADDING_PX} ${-BORDER_PADDING_PX} ${size.width + BORDER_PADDING_PX * 2} ${size.height + BORDER_PADDING_PX * 2}`}
17211722
preserveAspectRatio='none'
17221723
className='pointer-events-none absolute z-0 overflow-visible'

0 commit comments

Comments
 (0)