Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client'

import Image from 'next/image'
import { PLATFORM_LOOP_DESIGN } from '@/app/(landing)/components/shared/platform-loop-constants'
import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage'
import {
PREVIEW_SIDEBAR_CHATS,
PREVIEW_SIDEBAR_WORKFLOWS,
Expand All @@ -26,23 +28,22 @@ export function CapturedPlatformSurface({ src, sizes, activeItem }: CapturedPlat
return (
<div className='absolute inset-0 overflow-hidden'>
<Image src={src} alt='' fill sizes={sizes} className='object-cover' />
<svg
aria-hidden='true'
className='pointer-events-none absolute inset-0 size-full overflow-hidden'
viewBox='0 0 1280 735'
preserveAspectRatio='xMinYMin meet'
<ResponsiveDesignStage
width={PLATFORM_LOOP_DESIGN.width}
height={PLATFORM_LOOP_DESIGN.height}
align='start'
className='pointer-events-none absolute inset-0'
contentClassName='flex'
>
<foreignObject width='1280' height='735'>
<div className='flex h-full w-[249px] border-[var(--border)] border-r bg-[var(--surface-1)]'>
<EnterpriseSidebar
chats={PREVIEW_SIDEBAR_CHATS}
workflows={PREVIEW_SIDEBAR_WORKFLOWS}
activeItem={activeItem}
/>
<div className='min-w-0 flex-1 bg-[var(--surface-1)]' />
</div>
</foreignObject>
</svg>
<div className='flex h-full w-[249px] border-[var(--border)] border-r bg-[var(--surface-1)]'>
<EnterpriseSidebar
chats={PREVIEW_SIDEBAR_CHATS}
workflows={PREVIEW_SIDEBAR_WORKFLOWS}
activeItem={activeItem}
/>
<div className='min-w-0 flex-1 bg-[var(--surface-1)]' />
</div>
</ResponsiveDesignStage>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
BLOCK_WIDTH,
type BlockDef,
} from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage'

/** Breathing room between the canvas bounds and the card edges, in card px. */
const STAGE_MARGIN = 20
Expand All @@ -37,10 +38,12 @@ interface HeroWorkflowStageProps {

/**
* The hero window's live workflow canvas - the right-pane counterpart of the
* chat loop. One stable SVG viewBox owns both the edge and block coordinate
* systems, so drawing a line or revealing a block never changes the canvas's
* measured scale. Blocks pop in one by one as `builtCount` advances and edges
* stroke-draw once both endpoints exist.
* chat loop. A fixed HTML design surface owns both the edge and block
* coordinate systems, so drawing a line or revealing a block never changes
* the canvas's measured scale. SVG renders only the native edge paths; block
* cards stay in ordinary HTML to avoid WebKit's foreignObject compositing bugs.
* Blocks pop in one by one as `builtCount` advances and edges stroke-draw once
* both endpoints exist.
*
* Decorative and `aria-hidden` (via the parent frame), so blocks are NOT
* draggable - `pointer-events-none`, matching the rest of the hero animation.
Expand Down Expand Up @@ -68,65 +71,70 @@ export function HeroWorkflowStage({
)

return (
<svg
aria-hidden='true'
className='size-full overflow-hidden'
viewBox={`${-STAGE_MARGIN / 2} ${-STAGE_MARGIN / 2} ${canvas.width + STAGE_MARGIN} ${canvas.height + STAGE_MARGIN}`}
preserveAspectRatio='xMidYMid meet'
fill='none'
<ResponsiveDesignStage
width={canvas.width}
height={canvas.height}
inset={STAGE_MARGIN}
className='size-full'
contentClassName='relative'
>
{edges.map(([from, to]) => {
const source = blocksById.get(from)
const target = blocksById.get(to)
if (!source || !target) return null
const visible = builtIds.has(from) && builtIds.has(to)
const s = handleAnchors(source).out
const t = handleAnchors(target).in
return (
<path
key={`${from}-${to}`}
d={verticalSmoothStep(s.x, s.y, t.x, t.y)}
pathLength={1}
stroke='var(--workflow-edge)'
strokeWidth={2}
strokeLinecap='round'
className={cn(
'transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]',
visible ? '[stroke-dashoffset:0]' : '[stroke-dashoffset:1]'
)}
/>
)
})}
<svg
aria-hidden='true'
className='pointer-events-none absolute inset-0 size-full overflow-visible'
viewBox={`0 0 ${canvas.width} ${canvas.height}`}
fill='none'
>
{edges.map(([from, to]) => {
const source = blocksById.get(from)
const target = blocksById.get(to)
if (!source || !target) return null
const visible = builtIds.has(from) && builtIds.has(to)
const s = handleAnchors(source).out
const t = handleAnchors(target).in
return (
<path
key={`${from}-${to}`}
d={verticalSmoothStep(s.x, s.y, t.x, t.y)}
pathLength={1}
stroke='var(--workflow-edge)'
strokeWidth={2}
strokeLinecap='round'
className={cn(
'transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]',
visible ? '[stroke-dashoffset:0]' : '[stroke-dashoffset:1]'
)}
/>
)
})}
</svg>

{blocks.map((block) => {
const built = builtIds.has(block.id)
return (
<foreignObject
<div
key={block.id}
x={block.x}
y={block.y}
width={BLOCK_WIDTH}
height={blockHeight(block)}
overflow='visible'
className={cn(
'pointer-events-none absolute origin-center transition-[opacity,scale] duration-300 [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]',
built ? 'scale-100 opacity-100' : 'scale-[0.94] opacity-0'
)}
style={{
left: block.x,
top: block.y,
width: BLOCK_WIDTH,
height: blockHeight(block),
}}
>
<div
<StageBlockCard block={block} />
<span
aria-hidden
className={cn(
'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)]',
built ? 'scale-100 opacity-100' : 'scale-[0.94] opacity-0'
'pointer-events-none absolute inset-0 rounded-[13px] ring-[1.75px] ring-[var(--text-secondary)] transition-opacity duration-300 ease-out',
selectedId === block.id && built ? 'opacity-100' : 'opacity-0'
)}
>
<StageBlockCard block={block} />
<span
aria-hidden
className={cn(
'pointer-events-none absolute inset-0 rounded-[13px] ring-[1.75px] ring-[var(--text-secondary)] transition-opacity duration-300 ease-out',
selectedId === block.id && built ? 'opacity-100' : 'opacity-0'
)}
/>
</div>
</foreignObject>
/>
</div>
)
})}
</svg>
</ResponsiveDesignStage>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type { ReactNode } from 'react'
import { PLATFORM_LOOP_DESIGN } from '@/app/(landing)/components/shared/platform-loop-constants'
import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage'
import {
EnterpriseSidebar,
type EnterpriseSidebarProps,
Expand All @@ -23,11 +24,11 @@ interface HeroLoopShellProps {
}

/**
* The platform heroes' shared scaled stage. An SVG viewBox maps the fixed
* 1280x735 design space to the rendered window without applying a CSS
* transform to the whole app. Keeping that scale out of the animated HTML
* subtree prevents fractional repaint snapping in both the canvas and the
* otherwise-static {@link EnterpriseSidebar}.
* The platform heroes' shared responsive stage. The whole preview remains
* ordinary HTML, fitted from its fixed 1280x735 design space by
* {@link ResponsiveDesignStage}; SVG is reserved for native workflow paths.
* This keeps the sidebar and every animated descendant in one browser-safe
* layout coordinate system across Safari, Chromium, and Firefox.
*/
export function HeroLoopShell({
workspaceName = 'Brightwave',
Expand All @@ -38,24 +39,21 @@ export function HeroLoopShell({
children,
}: HeroLoopShellProps) {
return (
<svg
aria-hidden='true'
className='pointer-events-none absolute inset-0 size-full overflow-hidden'
viewBox={`0 0 ${PLATFORM_LOOP_DESIGN.width} ${PLATFORM_LOOP_DESIGN.height}`}
preserveAspectRatio='xMinYMin meet'
<ResponsiveDesignStage
width={PLATFORM_LOOP_DESIGN.width}
height={PLATFORM_LOOP_DESIGN.height}
align='start'
className='pointer-events-none absolute inset-0'
contentClassName='flex bg-[var(--surface-1)]'
>
<foreignObject width={PLATFORM_LOOP_DESIGN.width} height={PLATFORM_LOOP_DESIGN.height}>
<div className='flex size-full bg-[var(--surface-1)]'>
<EnterpriseSidebar
workspaceName={workspaceName}
profileName={profileName}
chats={chats}
workflows={workflows}
activeItem={activeItem}
/>
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>{children}</div>
</div>
</foreignObject>
</svg>
<EnterpriseSidebar
workspaceName={workspaceName}
profileName={profileName}
chats={chats}
workflows={workflows}
activeItem={activeItem}
/>
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>{children}</div>
</ResponsiveDesignStage>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage'
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest'
import { calculateFitScale } from '@/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage'

describe('calculateFitScale', () => {
it('fits the design surface to the limiting host dimension', () => {
expect(
calculateFitScale({
availableWidth: 1080,
availableHeight: 620,
designWidth: 1280,
designHeight: 735,
inset: 0,
maxScale: 1,
})
).toBeCloseTo(620 / 735)
})

it('reserves the requested inset before calculating the scale', () => {
expect(
calculateFitScale({
availableWidth: 500,
availableHeight: 700,
designWidth: 560,
designHeight: 700,
inset: 20,
maxScale: 1,
})
).toBeCloseTo(480 / 560)
})

it('does not upscale beyond the configured maximum', () => {
expect(
calculateFitScale({
availableWidth: 1600,
availableHeight: 1000,
designWidth: 1280,
designHeight: 735,
inset: 0,
maxScale: 1,
})
).toBe(1)
})

it('does not apply a scale before the host has measurable space', () => {
expect(
calculateFitScale({
availableWidth: 0,
availableHeight: 620,
designWidth: 1280,
designHeight: 735,
inset: 0,
maxScale: 1,
})
).toBe(0)
})
})
Loading
Loading