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,9 +1,8 @@
'use client'

import { type CSSProperties, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useMemo } from 'react'
import { cn } from '@sim/emcn'
import { StageBlockCard } from '@/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card'
import {
blockHeight,
handleAnchors,
STAGE_BLOCKS,
STAGE_CANVAS,
Expand All @@ -15,8 +14,6 @@ import {
type BlockDef,
} from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'

/** Upper bound on the canvas render scale (the scale at the full 1300px cap). */
const MAX_STAGE_SCALE = 0.71
/** Breathing room between the canvas bounds and the card edges, in card px. */
const STAGE_MARGIN = 20

Expand All @@ -40,11 +37,10 @@ interface HeroWorkflowStageProps {

/**
* The hero window's live workflow canvas - the right-pane counterpart of the
* chat loop. Blocks pop in one by one as `builtCount` advances (staggered
* scale/fade entrances, edges stroke-draw once both endpoints exist) at their
* fixed positions. The edge SVG is `overflow-visible` - SVGs clip
* at their viewport by default, which would cut the lines if a block ever sat
* outside the design-canvas bounds.
* 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.
*
* Decorative and `aria-hidden` (via the parent frame), so blocks are NOT
* draggable - `pointer-events-none`, matching the rest of the hero animation.
Expand All @@ -64,116 +60,73 @@ export function HeroWorkflowStage({
canvas = STAGE_CANVAS,
selectedId,
}: HeroWorkflowStageProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [scale, setScale] = useState(MAX_STAGE_SCALE)
const blocksById = useMemo(() => new Map(blocks.map((b) => [b.id, b])), [blocks])

// Fit the design canvas to the card: scale down when the pane narrows so the
// branch blocks never clip, capped at the full-width scale. Measures LAYOUT
// size (offsetWidth/Height) - the stage lives inside the platform loop's
// scale-transformed design-space layer, and getBoundingClientRect's visual
// size would compound that outer scale into a double shrink.
useLayoutEffect(() => {
const el = containerRef.current
if (!el) return
const measure = () => {
const w = el.offsetWidth
const h = el.offsetHeight
if (w < 40 || h < 40) return
setScale(
Math.min(
MAX_STAGE_SCALE,
(w - STAGE_MARGIN) / canvas.width,
(h - STAGE_MARGIN) / canvas.height
)
)
}
measure()
const ro = new ResizeObserver(measure)
ro.observe(el)
return () => ro.disconnect()
}, [canvas.width, canvas.height])

const builtIds = useMemo(
() => new Set(blocks.slice(0, builtCount).map((b) => b.id)),
[blocks, builtCount]
)

return (
<div
ref={containerRef}
className='flex h-full w-full items-center justify-center overflow-hidden'
<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'
>
<div
className='relative shrink-0'
style={{
width: canvas.width * scale,
height: canvas.height * scale,
}}
>
<div
className='absolute top-0 left-0'
style={{
width: canvas.width,
height: canvas.height,
transform: `scale(${scale})`,
transformOrigin: '0 0',
}}
>
<svg
className='pointer-events-none absolute inset-0 overflow-visible'
width={canvas.width}
height={canvas.height}
viewBox={`0 0 ${canvas.width} ${canvas.height}`}
fill='none'
aria-hidden='true'
>
{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='transition-[stroke-dashoffset] duration-500 [stroke-dasharray:1] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)]'
style={{ strokeDashoffset: visible ? 0 : 1 } as CSSProperties}
/>
)
})}
</svg>
{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]'
)}
/>
)
})}

{blocks.map((block) => {
const built = builtIds.has(block.id)
return (
<div
key={block.id}
{blocks.map((block) => {
const built = builtIds.has(block.id)
return (
<foreignObject
key={block.id}
x={block.x}
y={block.y}
width={BLOCK_WIDTH}
height={blockHeight(block)}
overflow='visible'
>
<div
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'
)}
>
<StageBlockCard block={block} />
<span
aria-hidden
className={cn(
'pointer-events-none absolute 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'
'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'
)}
style={{ left: block.x, top: block.y, width: BLOCK_WIDTH }}
>
<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>
)
})}
</div>
</div>
</div>
/>
</div>
</foreignObject>
)
})}
</svg>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { cn } from '@sim/emcn'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import type { BlockDef } from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import type { EnterpriseSidebarProps } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'

/** The empty canvas holds this long before the first block lands. */
Expand Down Expand Up @@ -78,7 +78,7 @@ export function EditorLoop({ content }: EditorLoopProps) {
setTimeout(() => setBuiltCount(i + 1), IDLE_HOLD_MS + i * BUILD_STEP_MS)
),
setTimeout(() => setSelected(true), selectAt),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client'

import type { ReactNode } from 'react'
import { PLATFORM_LOOP_DESIGN } from '@/app/(landing)/components/shared/platform-loop-constants'
import {
EnterpriseSidebar,
type EnterpriseSidebarProps,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
import { DESIGN, useDesignScale } from '@/app/(landing)/hooks/use-design-scale'

interface HeroLoopShellProps {
/** Workspace name in the sidebar header chip. */
Expand All @@ -23,12 +23,11 @@ interface HeroLoopShellProps {
}

/**
* The platform heroes' shared scaled stage: a `pointer-events-none` region
* whose fixed 1280x735 design-space layer is fitted to the rendered width via
* {@link useDesignScale} (`ResizeObserver` + `transform: scale`), holding the
* live {@link EnterpriseSidebar} beside the workspace pane each loop supplies
* as children. Purely presentational - the hero that renders it owns the
* `aria-hidden` frame and the animation clock.
* 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}.
*/
export function HeroLoopShell({
workspaceName = 'Brightwave',
Expand All @@ -38,27 +37,25 @@ export function HeroLoopShell({
activeItem,
children,
}: HeroLoopShellProps) {
const { regionRef, scale } = useDesignScale()

return (
<div ref={regionRef} className='pointer-events-none absolute inset-0 overflow-hidden'>
<div
className='flex origin-top-left bg-[var(--surface-1)]'
style={{
width: DESIGN.width,
height: DESIGN.height,
transform: `scale(${scale})`,
}}
>
<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>
</div>
<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'
>
<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>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** Fixed design space shared by every live landing platform preview. */
export const PLATFORM_LOOP_DESIGN = { width: 1280, height: 735 } as const

/** Fade-out length before a platform preview restarts its animation cycle. */
export const PLATFORM_LOOP_RESET_FADE_MS = 300
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useMemo, useState } from 'react'
import { cn } from '@sim/emcn'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage'
import {
BUILD_STEP_MS,
Expand All @@ -12,7 +13,6 @@ import {
type EnterpriseLoopContent,
type EnterpriseLoopPhase,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'

interface EnterprisePlatformLoopProps {
Expand Down Expand Up @@ -78,7 +78,7 @@ export function EnterprisePlatformLoop({
setTimeout(() => setBuiltCount(i + 1), timeline.buildStart + i * BUILD_STEP_MS)
),
setTimeout(() => setPhase('reply'), timeline.reply),
setTimeout(() => setFading(true), timeline.total - RESET_FADE_MS),
setTimeout(() => setFading(true), timeline.total - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs: timeline.total,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const EnterpriseSidebar = memo(function EnterpriseSidebar({
activeItem = 'New chat',
}: EnterpriseSidebarProps = {}) {
return (
<div className='flex h-full w-[238px] flex-shrink-0 flex-col bg-[var(--surface-1)] pt-3'>
<div className='isolate flex h-full w-[238px] flex-shrink-0 flex-col bg-[var(--surface-1)] pt-3 will-change-transform'>
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className={cn(chipVariants(), 'min-w-0 flex-1')}>
{/* The exact Brightwave mark the homepage capture seeds
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/(landing)/files/components/files-hero-loop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { ArrowUpDown, File, ListFilter, Plus, Search } from '@sim/emcn/icons'
import { AgentIcon } from '@/components/icons'
import { CsvIcon, DocxIcon, PdfIcon } from '@/components/icons/document-icons'
import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell'
import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants'
import { ZipIcon } from '@/app/(landing)/components/shared/zip-icon'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'

/** Sidebar content for the files hero - a file-heavy team's workspace. */
Expand Down Expand Up @@ -222,7 +222,7 @@ export function FilesHeroLoop() {
setTimeout(() => setRowCount(i + 1), IDLE_HOLD_MS + i * ROW_STEP_MS)
),
setTimeout(() => setDropped(true), dropAt),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS),
],
totalMs,
}
Expand Down
Loading
Loading