diff --git a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage.tsx b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage.tsx index fc235d22e59..c42c0698d10 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage.tsx @@ -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, @@ -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 @@ -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. @@ -64,116 +60,73 @@ export function HeroWorkflowStage({ canvas = STAGE_CANVAS, selectedId, }: HeroWorkflowStageProps) { - const containerRef = useRef(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 ( - + + ) + })} + ) } diff --git a/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx b/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx index 4237c1a4397..42aa3408516 100644 --- a/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx +++ b/apps/sim/app/(landing)/components/shared/editor-loop/editor-loop.tsx @@ -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. */ @@ -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, } diff --git a/apps/sim/app/(landing)/components/shared/hero-loop-shell/hero-loop-shell.tsx b/apps/sim/app/(landing)/components/shared/hero-loop-shell/hero-loop-shell.tsx index 6982d1c1d4f..363ad3fef2a 100644 --- a/apps/sim/app/(landing)/components/shared/hero-loop-shell/hero-loop-shell.tsx +++ b/apps/sim/app/(landing)/components/shared/hero-loop-shell/hero-loop-shell.tsx @@ -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. */ @@ -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', @@ -38,27 +37,25 @@ export function HeroLoopShell({ activeItem, children, }: HeroLoopShellProps) { - const { regionRef, scale } = useDesignScale() - return ( -
-
- -
{children}
-
-
+ ) } diff --git a/apps/sim/app/(landing)/components/shared/platform-loop-constants.ts b/apps/sim/app/(landing)/components/shared/platform-loop-constants.ts new file mode 100644 index 00000000000..33dd1b18391 --- /dev/null +++ b/apps/sim/app/(landing)/components/shared/platform-loop-constants.ts @@ -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 diff --git a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-platform-loop.tsx b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-platform-loop.tsx index b278b1c107c..6ded478dc7e 100644 --- a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-platform-loop.tsx +++ b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-platform-loop.tsx @@ -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, @@ -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 { @@ -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, } diff --git a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx index 0e2b27371a7..5bfe90737fc 100644 --- a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx +++ b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar.tsx @@ -107,7 +107,7 @@ export const EnterpriseSidebar = memo(function EnterpriseSidebar({ activeItem = 'New chat', }: EnterpriseSidebarProps = {}) { return ( -
+
{/* The exact Brightwave mark the homepage capture seeds diff --git a/apps/sim/app/(landing)/files/components/files-hero-loop.tsx b/apps/sim/app/(landing)/files/components/files-hero-loop.tsx index 01b7a910cb2..b1dba0325ea 100644 --- a/apps/sim/app/(landing)/files/components/files-hero-loop.tsx +++ b/apps/sim/app/(landing)/files/components/files-hero-loop.tsx @@ -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. */ @@ -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, } diff --git a/apps/sim/app/(landing)/hooks/use-design-scale.ts b/apps/sim/app/(landing)/hooks/use-design-scale.ts deleted file mode 100644 index e0db050762b..00000000000 --- a/apps/sim/app/(landing)/hooks/use-design-scale.ts +++ /dev/null @@ -1,40 +0,0 @@ -'use client' - -import { useLayoutEffect, useRef, useState } from 'react' - -/** - * The platform heroes' shared design space - the 1280x735 "mini app" geometry - * every hero loop lays out in (matching the homepage capture's CSS layout), - * so each hero reads at the identical scale inside the shared demo window. - */ -export const DESIGN = { width: 1280, height: 735 } as const - -/** Fade-out length before a hero loop's cycle restarts. */ -export const RESET_FADE_MS = 300 - -/** - * Tracks the rendered region's width via `ResizeObserver` and derives the - * scale that fits the {@link DESIGN}-space layer to it, keeping the live - * layer's proportions locked to the window's. Attach `regionRef` to the - * loop's outer region and apply `transform: scale(${scale})` to the - * design-space layer. - */ -export function useDesignScale() { - const regionRef = useRef(null) - const [scale, setScale] = useState(1) - - useLayoutEffect(() => { - const el = regionRef.current - if (!el) return - const measure = () => { - const w = el.getBoundingClientRect().width - if (w > 40) setScale(w / DESIGN.width) - } - measure() - const ro = new ResizeObserver(measure) - ro.observe(el) - return () => ro.disconnect() - }, []) - - return { regionRef, scale } -} diff --git a/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx b/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx index 870f070acbf..3a78ed891fa 100644 --- a/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx +++ b/apps/sim/app/(landing)/knowledge/components/knowledge-hero-loop.tsx @@ -14,7 +14,7 @@ import { ZendeskIcon, } from '@/components/icons' import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell' -import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale' +import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants' import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle' /** Sidebar content for the knowledge hero - a team living in its docs. */ @@ -149,7 +149,7 @@ export function KnowledgeHeroLoop() { ), setTimeout(() => setSyncPhase('syncing'), syncAt), setTimeout(() => setSyncPhase('synced'), syncAt + SYNC_MS), - setTimeout(() => setFading(true), totalMs - RESET_FADE_MS), + setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS), ], totalMs, } diff --git a/apps/sim/app/(landing)/logs/components/logs-hero-loop.tsx b/apps/sim/app/(landing)/logs/components/logs-hero-loop.tsx index fee7fbc350c..a031a2343b8 100644 --- a/apps/sim/app/(landing)/logs/components/logs-hero-loop.tsx +++ b/apps/sim/app/(landing)/logs/components/logs-hero-loop.tsx @@ -5,7 +5,7 @@ import type { BadgeProps } from '@sim/emcn' import { Badge, cn } from '@sim/emcn' import { ArrowUpDown, Download, Library, ListFilter, Search, Workflow } from '@sim/emcn/icons' import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell' -import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale' +import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants' import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle' /** Sidebar content for the logs hero - a team living in its run history. */ @@ -259,7 +259,7 @@ export function LogsHeroLoop() { ), setTimeout(() => setLiveState('running'), LIVE_APPEAR_MS), setTimeout(() => setLiveState('completed'), LIVE_COMPLETE_MS), - setTimeout(() => setFading(true), totalMs - RESET_FADE_MS), + setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS), ], totalMs, } diff --git a/apps/sim/app/(landing)/tables/components/tables-hero-loop.tsx b/apps/sim/app/(landing)/tables/components/tables-hero-loop.tsx index ef66bd7e1b2..6ecd49fd818 100644 --- a/apps/sim/app/(landing)/tables/components/tables-hero-loop.tsx +++ b/apps/sim/app/(landing)/tables/components/tables-hero-loop.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { Checkbox, cn } from '@sim/emcn' import { ChevronDown, Table, TypeBoolean, TypeText } from '@sim/emcn/icons' import { HeroLoopShell } from '@/app/(landing)/components/shared/hero-loop-shell' -import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale' +import { PLATFORM_LOOP_RESET_FADE_MS } from '@/app/(landing)/components/shared/platform-loop-constants' import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle' import styles from '@/app/(landing)/tables/components/tables-hero-loop.module.css' @@ -377,7 +377,7 @@ export function TablesHeroLoop() { ...Array.from({ length: TOTAL_FILLED_CELLS }, (_, i) => setTimeout(() => setFilledCount(i + 1), sweepAt + i * CELL_STEP_MS) ), - setTimeout(() => setFading(true), totalMs - RESET_FADE_MS), + setTimeout(() => setFading(true), totalMs - PLATFORM_LOOP_RESET_FADE_MS), ], totalMs, }