@@ -13,6 +13,7 @@ import {
1313 BLOCK_WIDTH ,
1414 type BlockDef ,
1515} from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
16+ import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage'
1617
1718/** Breathing room between the canvas bounds and the card edges, in card px. */
1819const STAGE_MARGIN = 20
@@ -37,10 +38,12 @@ interface HeroWorkflowStageProps {
3738
3839/**
3940 * The hero window's live workflow canvas - the right-pane counterpart of the
40- * chat loop. One stable SVG viewBox owns both the edge and block coordinate
41- * systems, so drawing a line or revealing a block never changes the canvas's
42- * measured scale. Blocks pop in one by one as `builtCount` advances and edges
43- * stroke-draw once both endpoints exist.
41+ * chat loop. A fixed HTML design surface owns both the edge and block
42+ * coordinate systems, so drawing a line or revealing a block never changes
43+ * the canvas's measured scale. SVG renders only the native edge paths; block
44+ * cards stay in ordinary HTML to avoid WebKit's foreignObject compositing bugs.
45+ * Blocks pop in one by one as `builtCount` advances and edges stroke-draw once
46+ * both endpoints exist.
4447 *
4548 * Decorative and `aria-hidden` (via the parent frame), so blocks are NOT
4649 * draggable - `pointer-events-none`, matching the rest of the hero animation.
@@ -68,65 +71,70 @@ export function HeroWorkflowStage({
6871 )
6972
7073 return (
71- < svg
72- aria-hidden = 'true'
73- className = 'size-full overflow-hidden'
74- viewBox = { ` ${ - STAGE_MARGIN / 2 } ${ - STAGE_MARGIN / 2 } ${ canvas . width + STAGE_MARGIN } ${ canvas . height + STAGE_MARGIN } ` }
75- preserveAspectRatio = 'xMidYMid meet '
76- fill = 'none '
74+ < ResponsiveDesignStage
75+ width = { canvas . width }
76+ height = { canvas . height }
77+ inset = { STAGE_MARGIN }
78+ className = 'size-full '
79+ contentClassName = 'relative '
7780 >
78- { edges . map ( ( [ from , to ] ) => {
79- const source = blocksById . get ( from )
80- const target = blocksById . get ( to )
81- if ( ! source || ! target ) return null
82- const visible = builtIds . has ( from ) && builtIds . has ( to )
83- const s = handleAnchors ( source ) . out
84- const t = handleAnchors ( target ) . in
85- return (
86- < path
87- key = { `${ from } -${ to } ` }
88- d = { verticalSmoothStep ( s . x , s . y , t . x , t . y ) }
89- pathLength = { 1 }
90- stroke = 'var(--workflow-edge)'
91- strokeWidth = { 2 }
92- strokeLinecap = 'round'
93- className = { cn (
94- 'transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]' ,
95- visible ? '[stroke-dashoffset:0]' : '[stroke-dashoffset:1]'
96- ) }
97- />
98- )
99- } ) }
81+ < svg
82+ aria-hidden = 'true'
83+ className = 'pointer-events-none absolute inset-0 size-full overflow-visible'
84+ viewBox = { `0 0 ${ canvas . width } ${ canvas . height } ` }
85+ fill = 'none'
86+ >
87+ { edges . map ( ( [ from , to ] ) => {
88+ const source = blocksById . get ( from )
89+ const target = blocksById . get ( to )
90+ if ( ! source || ! target ) return null
91+ const visible = builtIds . has ( from ) && builtIds . has ( to )
92+ const s = handleAnchors ( source ) . out
93+ const t = handleAnchors ( target ) . in
94+ return (
95+ < path
96+ key = { `${ from } -${ to } ` }
97+ d = { verticalSmoothStep ( s . x , s . y , t . x , t . y ) }
98+ pathLength = { 1 }
99+ stroke = 'var(--workflow-edge)'
100+ strokeWidth = { 2 }
101+ strokeLinecap = 'round'
102+ className = { cn (
103+ 'transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]' ,
104+ visible ? '[stroke-dashoffset:0]' : '[stroke-dashoffset:1]'
105+ ) }
106+ />
107+ )
108+ } ) }
109+ </ svg >
100110
101111 { blocks . map ( ( block ) => {
102112 const built = builtIds . has ( block . id )
103113 return (
104- < foreignObject
114+ < div
105115 key = { block . id }
106- x = { block . x }
107- y = { block . y }
108- width = { BLOCK_WIDTH }
109- height = { blockHeight ( block ) }
110- overflow = 'visible'
116+ className = { cn (
117+ 'pointer-events-none absolute origin-center transition-[opacity,scale] duration-300 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]' ,
118+ built ? 'scale-100 opacity-100' : 'scale-[0.94] opacity-0'
119+ ) }
120+ style = { {
121+ left : block . x ,
122+ top : block . y ,
123+ width : BLOCK_WIDTH ,
124+ height : blockHeight ( block ) ,
125+ } }
111126 >
112- < div
127+ < StageBlockCard block = { block } />
128+ < span
129+ aria-hidden
113130 className = { cn (
114- 'pointer-events-none relative size-full origin-center transition-[opacity,scale] duration-300 will-change-[opacity,transform] [ transition-timing-function:cubic-bezier(0.22,1,0.36,1)] ' ,
115- built ? 'scale-100 opacity-100' : 'scale-[0.94] opacity-0'
131+ 'pointer-events-none absolute inset-0 rounded-[13px] ring-[1.75px] ring-[var(--text-secondary)] transition-opacity duration-300 ease-out ' ,
132+ selectedId === block . id && built ? 'opacity-100' : 'opacity-0'
116133 ) }
117- >
118- < StageBlockCard block = { block } />
119- < span
120- aria-hidden
121- className = { cn (
122- 'pointer-events-none absolute inset-0 rounded-[13px] ring-[1.75px] ring-[var(--text-secondary)] transition-opacity duration-300 ease-out' ,
123- selectedId === block . id && built ? 'opacity-100' : 'opacity-0'
124- ) }
125- />
126- </ div >
127- </ foreignObject >
134+ />
135+ </ div >
128136 )
129137 } ) }
130- </ svg >
138+ </ ResponsiveDesignStage >
131139 )
132140}
0 commit comments