From e6672f15cbc65d31c04abd7c39989d752ed7a139 Mon Sep 17 00:00:00 2001 From: andresdjasso Date: Mon, 13 Jul 2026 22:37:21 -0700 Subject: [PATCH 01/10] feat(landing): extend enterprise product-preview design to solutions and workflows pages Parametrize enterprise feature graphics with content props and retell each solutions page's feature blocs for its domain (engineering, it, compliance, finance, hr) plus the workflows platform page. Add five new graphics in the same visual vocabulary, animated platform-UI heroes with domain-specific loops on all six pages, hero category tags, container-query proportional tile scaling with wide/2x2 layout options, and a shared pre-footer CTA band. Enterprise page renders identically. Co-authored-by: Cursor --- .../hero-workflow-stage.tsx | 15 + apps/sim/app/(landing)/components/index.ts | 1 + .../components/platform-hero-visual/index.ts | 1 + .../platform-hero-visual.tsx | 39 +++ .../solutions-card/solutions-card.tsx | 83 +++++- .../solutions-card-row/solutions-card-row.tsx | 21 +- .../components/solutions-page/constants.ts | 38 ++- .../solutions-page/solutions-page.tsx | 71 +++-- .../components/solutions-page/types.ts | 10 +- .../enterprise-home-stage.tsx | 41 ++- .../enterprise-platform-loop.tsx | 76 +++-- .../enterprise-sidebar.tsx | 24 +- .../enterprise-platform-loop/stage-data.ts | 76 ++++- .../access-control-graphic.tsx | 11 +- .../feature-graphics/audit-trail-graphic.tsx | 25 +- .../feature-graphics/deploy-graphic.tsx | 50 +++- .../feature-graphic-shell.tsx | 14 +- .../it-platform-teams-graphic.tsx | 47 ++- .../operations-teams-graphic.tsx | 43 ++- .../run-monitoring-graphic.tsx | 129 +++++--- .../feature-graphics/staging-graphic.tsx | 63 +++- .../feature-graphics/standards-graphic.tsx | 38 ++- .../technical-teams-graphic.tsx | 51 +++- .../app/(landing)/enterprise/enterprise.tsx | 6 +- .../solutions/compliance/compliance.tsx | 89 +++++- .../components/compliance-hero-loop.tsx | 117 ++++++++ .../document-draft-graphic.module.css | 74 +++++ .../document-draft-graphic.tsx | 106 +++++++ .../components/feature-graphics/index.ts | 2 + .../knowledge-answer-graphic.module.css | 62 ++++ .../knowledge-answer-graphic.tsx | 103 +++++++ .../components/engineering-hero-loop.tsx | 119 ++++++++ .../solutions/engineering/engineering.tsx | 98 ++++++- .../reconcile-graphic.module.css | 69 +++++ .../feature-graphics/reconcile-graphic.tsx | 121 ++++++++ .../finance/components/finance-hero-loop.tsx | 116 ++++++++ .../(landing)/solutions/finance/finance.tsx | 122 +++++++- .../solutions/hr/components/hr-hero-loop.tsx | 118 ++++++++ apps/sim/app/(landing)/solutions/hr/hr.tsx | 110 ++++++- .../solutions/it/components/it-hero-loop.tsx | 118 ++++++++ apps/sim/app/(landing)/solutions/it/it.tsx | 83 +++++- .../agent-code-graphic.module.css | 62 ++++ .../feature-graphics/agent-code-graphic.tsx | 136 +++++++++ .../components/feature-graphics/index.ts | 2 + .../workflow-canvas-graphic.module.css | 88 ++++++ .../workflow-canvas-graphic.tsx | 122 ++++++++ .../components/workflows-editor-loop.tsx | 276 ++++++++++++++++++ .../sim/app/(landing)/workflows/workflows.tsx | 115 ++++++-- 48 files changed, 3098 insertions(+), 303 deletions(-) create mode 100644 apps/sim/app/(landing)/components/platform-hero-visual/index.ts create mode 100644 apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx create mode 100644 apps/sim/app/(landing)/solutions/compliance/components/compliance-hero-loop.tsx create mode 100644 apps/sim/app/(landing)/solutions/components/feature-graphics/document-draft-graphic.module.css create mode 100644 apps/sim/app/(landing)/solutions/components/feature-graphics/document-draft-graphic.tsx create mode 100644 apps/sim/app/(landing)/solutions/components/feature-graphics/index.ts create mode 100644 apps/sim/app/(landing)/solutions/components/feature-graphics/knowledge-answer-graphic.module.css create mode 100644 apps/sim/app/(landing)/solutions/components/feature-graphics/knowledge-answer-graphic.tsx create mode 100644 apps/sim/app/(landing)/solutions/engineering/components/engineering-hero-loop.tsx create mode 100644 apps/sim/app/(landing)/solutions/finance/components/feature-graphics/reconcile-graphic.module.css create mode 100644 apps/sim/app/(landing)/solutions/finance/components/feature-graphics/reconcile-graphic.tsx create mode 100644 apps/sim/app/(landing)/solutions/finance/components/finance-hero-loop.tsx create mode 100644 apps/sim/app/(landing)/solutions/hr/components/hr-hero-loop.tsx create mode 100644 apps/sim/app/(landing)/solutions/it/components/it-hero-loop.tsx create mode 100644 apps/sim/app/(landing)/workflows/components/feature-graphics/agent-code-graphic.module.css create mode 100644 apps/sim/app/(landing)/workflows/components/feature-graphics/agent-code-graphic.tsx create mode 100644 apps/sim/app/(landing)/workflows/components/feature-graphics/index.ts create mode 100644 apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.module.css create mode 100644 apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.tsx create mode 100644 apps/sim/app/(landing)/workflows/components/workflows-editor-loop.tsx 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 20d1ffbb057..3164086c537 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 @@ -29,6 +29,13 @@ interface HeroWorkflowStageProps { edges?: ReadonlyArray /** Design-space bounding box of the block layout. Defaults with them. */ canvas?: { width: number; height: number } + /** + * Block to dress with the selection ring - graphite (`--text-secondary`) + * rather than the real canvas's blue, per the landing pages' grayscale + * language - the workflows hero uses this for its "being edited" beat. + * Off by default, so existing stages are unchanged. + */ + selectedId?: string } /** @@ -55,6 +62,7 @@ export function HeroWorkflowStage({ blocks = STAGE_BLOCKS, edges = STAGE_EDGES, canvas = STAGE_CANVAS, + selectedId, }: HeroWorkflowStageProps) { const containerRef = useRef(null) const [scale, setScale] = useState(MAX_STAGE_SCALE) @@ -154,6 +162,13 @@ export function HeroWorkflowStage({ style={{ left: block.x, top: block.y, width: BLOCK_WIDTH }} > + ) })} diff --git a/apps/sim/app/(landing)/components/index.ts b/apps/sim/app/(landing)/components/index.ts index 91c35fb1e93..45e4681cf35 100644 --- a/apps/sim/app/(landing)/components/index.ts +++ b/apps/sim/app/(landing)/components/index.ts @@ -18,6 +18,7 @@ export { Lightbox } from './lightbox' export { LogoShell } from './logo-shell' export { Mothership } from './mothership/mothership' export { Navbar } from './navbar' +export { PlatformHeroVisual } from './platform-hero-visual' export type { PlatformCardConfig, PlatformCardRowConfig, diff --git a/apps/sim/app/(landing)/components/platform-hero-visual/index.ts b/apps/sim/app/(landing)/components/platform-hero-visual/index.ts new file mode 100644 index 00000000000..862981f2db2 --- /dev/null +++ b/apps/sim/app/(landing)/components/platform-hero-visual/index.ts @@ -0,0 +1 @@ +export { PlatformHeroVisual } from '@/app/(landing)/components/platform-hero-visual/platform-hero-visual' diff --git a/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx b/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx new file mode 100644 index 00000000000..c96627243c1 --- /dev/null +++ b/apps/sim/app/(landing)/components/platform-hero-visual/platform-hero-visual.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react' +import Image from 'next/image' + +interface PlatformHeroVisualProps { + /** The live platform interior - a platform loop or editor loop island. */ + children: ReactNode +} + +/** + * The enterprise hero's visual composition, shared: the architectural backdrop + * (`enterprise-hero-background.webp`) with the white demo window framed in + * front of it, ready to hold a live platform interior (a chat platform loop or + * the workflows editor loop). Extracted so the solutions and workflows heroes + * reuse the exact backdrop, window geometry (`aspect-[1080/620]` at 83.08% + * width), and shadow treatment the enterprise page established - the + * enterprise page keeps its own inline copy, byte-identical. + * + * Renders inside a hero visual slot (the `variant='home'` media frame or the + * standard {@link SolutionsVisualFrame}); the outer `relative` wrapper anchors + * the `fill` image in frames that don't position themselves. + */ +export function PlatformHeroVisual({ children }: PlatformHeroVisualProps) { + return ( +
+ +
+
{children}
+
+
+ ) +} diff --git a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx index 2dd29e9ca1f..aad50cd18d4 100644 --- a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx +++ b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-card/solutions-card.tsx @@ -21,6 +21,14 @@ import type { SolutionsCardConfig } from '@/app/(landing)/components/solutions-p * {@link SolutionsVisualFrame}; the feature-tile variant reserves a larger * flexible slot inside its own frame for future UI. * + * Feature tiles scale proportionally: the tile's content (copy and graphic + * together) is authored on a design-space canvas and the whole bloc zooms + * down uniformly when its grid column is narrower than the 352px design + * width, so intermediate breakpoints render a smaller copy of the desktop + * tile instead of a squished one. See `SOLUTIONS_VISUAL` for the container + * query + `tan(atan2())` mechanics; at or above the design width the tile + * renders fluid at scale 1, byte-identical to the pre-scaler layout. + * * A content unit only: it accepts copy and a visual node plus a controlled visual * treatment, never arbitrary spacing or class overrides. */ @@ -31,17 +39,41 @@ interface SolutionsCardProps { headingId: string /** Visual treatment. Defaults to the original split text + framed visual layout. */ variant?: 'split' | 'featureTile' + /** + * Set by the row on the third card of a 3-card feature-tile row. In the + * two-column band (`sm`..`lg`) the card spans both grid columns instead of + * sitting orphaned beside an empty cell, and the tile switches to a wide + * side-by-side treatment: the copy block sits vertically centered in a left + * column while the visual slot takes the remaining width at a shorter + * 360px tile height, so the row reads as designed-for-wide rather than a + * stretched portrait tile. Graphics detect the wide tile themselves via a + * container query (the tile is ≥500px only when spanned in this band) and + * relax their column caps to match. No effect at `lg`+ or below `sm`. + */ + tabletSpan?: boolean } -export function SolutionsCard({ card, headingId, variant = 'split' }: SolutionsCardProps) { +export function SolutionsCard({ + card, + headingId, + variant = 'split', + tabletSpan = false, +}: SolutionsCardProps) { const featureTile = variant === 'featureTile' const featureTileTone = SOLUTIONS_FEATURE_TILE_TONE[card.featureTileTone ?? 'light'] const featureTileDescription = card.featureTileDescriptionTone === 'soft' && card.featureTileTone === 'dark' ? SOLUTIONS_FEATURE_TILE_TONE.dark.descriptionSoft : featureTileTone.description + const wide = featureTile && tabletSpan const textBlock = ( -
+

- {textBlock} - - - +
+ {textBlock} + + +
+ +

) } diff --git a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/solutions-card-row.tsx b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/solutions-card-row.tsx index cf6819e766a..7c60d5a6ec7 100644 --- a/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/solutions-card-row.tsx +++ b/apps/sim/app/(landing)/components/solutions-page/components/solutions-card-row/solutions-card-row.tsx @@ -10,7 +10,15 @@ import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solution * A card row - the core repeating unit of a solutions page. A header block (an * `

` title, a body-color subtitle, and a single pill CTA) sits above a grid * of cards. The grid column count is derived from `cards.length` - 3 cards render - * `grid-cols-3`, 4 render `grid-cols-4` - so the page never specifies layout. + * `grid-cols-3`, 4 render `grid-cols-4` - so the page never specifies layout. A + * row can opt down to a denser wrap via `row.columns` (e.g. 4 cards as a 2×2 + * grid); the grid's own gap keeps the wrapped rows at the standard inter-tile + * rhythm. + * + * In the two-column band (`sm`..`lg`) a 3-card feature-tile row would leave its + * third card orphaned beside an empty cell, so that card spans both columns and + * switches to the tile's wide side-by-side treatment (copy left, graphic + * right) - see {@link SolutionsCard}'s `tabletSpan`. * * Rendered as a labelled `
` for a clean, crawlable landmark; each card * is an `
` with an `

`, keeping the strict H2 → H3 hierarchy. Every @@ -29,8 +37,9 @@ interface SolutionsCardRowProps { headerVariant?: 'standard' | 'feature' } -/** Maps a supported card count to its grid column class; anything else falls back to three-up. */ +/** Maps a supported column count to its grid class; anything else falls back to three-up. */ const GRID_COLS: Record = { + 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', } @@ -42,7 +51,7 @@ export function SolutionsCardRow({ headerVariant = 'standard', }: SolutionsCardRowProps) { const headingId = `solutions-row-${row.id}-heading` - const gridCols = GRID_COLS[row.cards.length] ?? GRID_COLS[3] + const gridCols = GRID_COLS[row.columns ?? row.cards.length] ?? GRID_COLS[3] return (
))}

diff --git a/apps/sim/app/(landing)/components/solutions-page/constants.ts b/apps/sim/app/(landing)/components/solutions-page/constants.ts index 6c91d61ab76..72b41a23d99 100644 --- a/apps/sim/app/(landing)/components/solutions-page/constants.ts +++ b/apps/sim/app/(landing)/components/solutions-page/constants.ts @@ -89,13 +89,39 @@ export const SOLUTIONS_VISUAL = { /** Fixed height of a card's visual panel - uniform across every card. */ cardHeight: 'h-[240px]', /** - * Minimum height for framed feature tiles with copy and future UI in one - * surface. One value at every breakpoint: the tallest tile vignettes (audit - * ledger, staging panel) need ~300px of visual slot below the copy block, so - * shrinking the tile on small screens crops their tops against the slot's - * `overflow-hidden`. + * The feature-tile proportional-scaling system. Tiles are authored against a + * fixed design space - `352px` wide (the 3-up column at the widest desktop + * container) by `440px` tall - and the whole tile scales down uniformly, + * copy and graphic together, whenever its grid column gets narrower than + * that, like a zoomed-out desktop tile. Columns at or above 352px render + * fluid at scale 1, exactly the pre-scaler layout, so wide tiles (2-up rows, + * single-column phones) are untouched. + * + * Mechanics, pure CSS (no JS measurement): + * - {@link featureTileContainer} makes each tile's wrapper an inline-size + * query container, so `100cqw` reads the rendered column width. + * - {@link featureTileScale} derives the factor on the tile: + * `tan(atan2(100cqw, 352px))` is the standard trick for dividing two + * lengths into a plain number, clamped to 1. + * - {@link featureTileCanvas} is the design-space stage: an absolutely + * positioned layer sized `100%/scale` (= 352px wide when scaling engages) + * and shrunk back with `scale`, so content lays out at desktop geometry + * and the transform never leaves layout dead space. + * - {@link featureTileMinHeight} tracks the scaled canvas - `440px × scale` - + * so the tile's height follows the zoom instead of clipping or leaving an + * empty band. */ - featureTileMinHeight: 'min-h-[440px]', + featureTileContainer: '[container-type:inline-size]', + featureTileScale: '[--tile-scale:min(1,tan(atan2(100cqw,352px)))]', + featureTileCanvas: + 'h-[calc(100%/var(--tile-scale,1))] w-[calc(100%/var(--tile-scale,1))] origin-top-left [scale:var(--tile-scale,1)]', + /** + * Feature-tile minimum height - the `440px` design-space height, multiplied + * by the tile's current scale so height tracks the zoomed content. The + * tallest tile vignettes (audit ledger, staging panel) need ~300px of visual + * slot below the copy block in design space. + */ + featureTileMinHeight: 'min-h-[calc(440px*var(--tile-scale,1))]', } as const /** diff --git a/apps/sim/app/(landing)/components/solutions-page/solutions-page.tsx b/apps/sim/app/(landing)/components/solutions-page/solutions-page.tsx index 2d93f068c31..dd083741cd1 100644 --- a/apps/sim/app/(landing)/components/solutions-page/solutions-page.tsx +++ b/apps/sim/app/(landing)/components/solutions-page/solutions-page.tsx @@ -1,4 +1,6 @@ import { cn } from '@sim/emcn' +import { Cta } from '@/app/(landing)/components/cta/cta' +import { LANDING_CONTENT_WIDTH } from '@/app/(landing)/components/landing-layout' import { SolutionsCardRow, SolutionsHero, @@ -18,45 +20,68 @@ import type { SolutionsPageConfig } from '@/app/(landing)/components/solutions-p * separate so the solutions layout and its components can diverge from the * platform layout without coupling. * - * This component owns the entire `
`: the shared `max-w-[1460px]` content - * column (centered with `mx-auto`, matching the navbar and landing sections), the - * one horizontal gutter (`SOLUTIONS_SPACING.gutter`), and the inter-section - * vertical rhythm (`SOLUTIONS_SPACING.sectionRhythm`, the `
` flex gap). The - * hero, the logos row, and every card row carry no gutter and no inter-section - * margin of their own, so spacing is uniform and unreachable from a consumer - * page - the config is pure content (strings + `ReactNode` visual slots), with no - * layout knob anywhere in its tree. + * This component owns the entire `
`: an inner content column carries the + * shared `max-w-[1460px]` width (centered via `LANDING_CONTENT_WIDTH`, matching + * the navbar and landing sections), the one horizontal gutter + * (`SOLUTIONS_SPACING.gutter`), and the inter-section vertical rhythm + * (`SOLUTIONS_SPACING.sectionRhythm`) - the enterprise page's exact structure. + * The closing {@link Cta} sits directly in the `
` because it owns its own + * width cap and gutter; the `
`'s matching flex gap gives it the same + * rhythm after the last card row as every other section boundary. The hero, the + * logos row, and every card row carry no gutter and no inter-section margin of + * their own, so spacing is uniform and unreachable from a consumer page - the + * config is pure content (strings + `ReactNode` visual slots), with no layout + * knob anywhere in its tree. * * The order is fixed: structured data first (before visible content, derived * from the same config so it never drifts) → solutions hero (the page's only - * `

`) → centered logos row → the configured card rows in array order. The - * heading outline is strict H1 → H2 (per card row) → H3 (per card), never - * skipped. Server Component only - no client island lives here; the page - * supplies its own islands through the `visual` slots in the config. + * `

`) → centered logos row → the configured card rows in array order → the + * shared pre-footer CTA band. The heading outline is strict H1 → H2 (per card + * row + the CTA) → H3 (per card), never skipped. Server Component only - no + * client island lives here; the page supplies its own islands through the + * `visual` slots in the config. */ interface SolutionsPageProps { /** The complete page content - identity, hero, and ordered card rows. */ config: SolutionsPageConfig + /** + * Card treatment forwarded to every {@link SolutionsCardRow}. `featureTile` + * renders the enterprise page's framed feature tiles (copy + visual in one + * toned surface); the default keeps the original split layout. + */ + cardVariant?: 'split' | 'featureTile' } -export function SolutionsPage({ config }: SolutionsPageProps) { +export function SolutionsPage({ config, cardVariant = 'split' }: SolutionsPageProps) { return ( <>
- - - {config.rows.map((row) => ( - - ))} +
+ + + {config.rows.map((row) => ( + + ))} +
+ +
) diff --git a/apps/sim/app/(landing)/components/solutions-page/types.ts b/apps/sim/app/(landing)/components/solutions-page/types.ts index e1379dc3f28..d4465adda70 100644 --- a/apps/sim/app/(landing)/components/solutions-page/types.ts +++ b/apps/sim/app/(landing)/components/solutions-page/types.ts @@ -77,7 +77,8 @@ export type SolutionsFeatureTileTone = 'light' | 'dark' /** * A card row - the core repeating unit. A header (title + subtitle + CTA) above a * grid of 3 or 4 cards. The grid column count is derived from `cards.length`, so - * the page never specifies layout. + * the page never specifies layout - except the optional {@link columns} density + * override for rows whose cards need more width than their count would grant. */ export interface SolutionsCardRowConfig { /** @@ -93,6 +94,13 @@ export interface SolutionsCardRowConfig { cta: SolutionsPillCta /** The cards in this row - 3 or 4. The grid derives its columns from this length. */ cards: SolutionsCardConfig[] + /** + * Optional desktop column-count override. A 4-card row defaults to four-up, + * which squeezes tile vignettes; `columns: 2` renders it as a 2×2 grid + * instead (wrapping keeps the standard inter-tile gap on both axes). + * Breakpoint collapse (2-col under `lg`, 1-col under `sm`) is unchanged. + */ + columns?: 2 } /** diff --git a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage.tsx b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage.tsx index 329cbaa7851..638af760c23 100644 --- a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage.tsx +++ b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { ChevronDown, cn } from '@sim/emcn' import { ArrowRight, @@ -27,8 +27,6 @@ import { SUGGESTED_ACTIONS, } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data' -const REPLY_WORDS = ENTERPRISE_REPLY.split(' ') - /** * Reveals an incrementing count (typed chars, streamed words) at a fixed * step interval while `active`, deriving progress from ELAPSED time so a @@ -139,6 +137,16 @@ interface EnterpriseHomeStageProps { phase: EnterpriseLoopPhase /** True during the brief fade-out before the cycle restarts. */ fading: boolean + /** Personalized new-chat greeting. Defaults to the enterprise copy. */ + greeting?: string + /** Composer placeholder before typing starts. Defaults to the enterprise copy. */ + placeholder?: string + /** The prompt the loop types out. Defaults to the enterprise copy. */ + prompt?: string + /** Sim's streamed reply. Defaults to the enterprise copy. */ + reply?: string + /** Suggested-action rows under the composer. Defaults to the enterprise set. */ + suggestedActions?: readonly [string, string, string, string] } /** @@ -156,15 +164,24 @@ interface EnterpriseHomeStageProps { * ELAPSED time so throttled background tabs catch up instead of stalling * mid-sentence. */ -export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps) { +export function EnterpriseHomeStage({ + phase, + fading, + greeting = ENTERPRISE_GREETING, + placeholder = COMPOSER_PLACEHOLDER, + prompt = ENTERPRISE_PROMPT, + reply = ENTERPRISE_REPLY, + suggestedActions = SUGGESTED_ACTIONS, +}: EnterpriseHomeStageProps) { const isTyping = phase === 'typing' const isReply = phase === 'reply' const inConversation = phase === 'dispatch' || isReply const promptDone = phase === 'typed' || inConversation - const typedChars = useElapsedReveal(isTyping, PROMPT_CHAR_MS, ENTERPRISE_PROMPT.length) - const revealedWords = useElapsedReveal(isReply, REPLY_WORD_MS, REPLY_WORDS.length) + const replyWords = useMemo(() => reply.split(' '), [reply]) + const typedChars = useElapsedReveal(isTyping, PROMPT_CHAR_MS, prompt.length) + const revealedWords = useElapsedReveal(isReply, REPLY_WORD_MS, replyWords.length) - const visiblePrompt = promptDone ? ENTERPRISE_PROMPT : ENTERPRISE_PROMPT.slice(0, typedChars) + const visiblePrompt = promptDone ? prompt : prompt.slice(0, typedChars) const hasText = visiblePrompt.length > 0 return ( @@ -182,7 +199,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps) inConversation ? 'pointer-events-none opacity-0' : 'opacity-100' )} > -

{ENTERPRISE_GREETING}

+

{greeting}

@@ -192,7 +209,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps) {isTyping && } ) : ( - {COMPOSER_PLACEHOLDER} + {placeholder} )} @@ -208,7 +225,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps)
- {SUGGESTED_ACTIONS.map((action, i) => { + {suggestedActions.map((action, i) => { const Icon = ACTION_ICONS[i] return (
- {ENTERPRISE_PROMPT} + {prompt}
{phase === 'dispatch' && ( @@ -253,7 +270,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps) isReply ? 'opacity-100' : 'opacity-0' )} > - {REPLY_WORDS.slice(0, revealedWords).join(' ')} + {replyWords.slice(0, revealedWords).join(' ')}

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 3a34086dba2..67e8814a4c7 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 @@ -1,17 +1,16 @@ 'use client' -import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { cn } from '@sim/emcn' import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage' import { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage' import { EnterpriseSidebar } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar' import { BUILD_STEP_MS, - ENTERPRISE_STAGE_BLOCKS, - ENTERPRISE_STAGE_CANVAS, - ENTERPRISE_STAGE_EDGES, + buildLoopTimeline, + ENTERPRISE_LOOP_CONTENT, + type EnterpriseLoopContent, type EnterpriseLoopPhase, - LOOP_TIMELINE, RESET_FADE_MS, } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data' @@ -23,6 +22,16 @@ import { */ const DESIGN = { width: 1280, height: 735 } as const +interface EnterprisePlatformLoopProps { + /** + * Domain content the loop replays - sidebar identity, chat exchange, and + * staged workflow. Defaults to the enterprise page's own content, so + * existing consumers render exactly as before; the solutions pages pass + * their per-domain content through the same shape. + */ + content?: EnterpriseLoopContent +} + /** * The enterprise hero's platform loop - a sibling of the homepage * `HeroPlatformLoop` that shares its architecture (fixed design-space layer @@ -37,16 +46,18 @@ const DESIGN = { width: 1280, height: 735 } as const * Timeline (see `stage-data.ts` - later stages append beats there): idle * new-chat view → prompt types out → send arms → dispatch (user bubble + * thinking, full-width) → the stage pane slides in from the right (the real - * `MothershipView` `w-0 ↔ w-1/2` width transition) → the invoice workflow - * assembles block by block (the shared {@link HeroWorkflowStage}, staged with - * the enterprise flow) → the reply streams in → hold → fade → restart. + * `MothershipView` `w-0 ↔ w-1/2` width transition) → the staged workflow + * assembles block by block (the shared {@link HeroWorkflowStage}) → the reply + * streams in → hold → fade → restart. * * Everything is `pointer-events-none` decorative, matching the hero's * `aria-hidden` frame. Under `prefers-reduced-motion` the loop never starts: * the finished exchange, open stage, and fully-built workflow render * statically. */ -export function EnterprisePlatformLoop() { +export function EnterprisePlatformLoop({ + content = ENTERPRISE_LOOP_CONTENT, +}: EnterprisePlatformLoopProps = {}) { const regionRef = useRef(null) const [phase, setPhase] = useState('idle') const [stageOpen, setStageOpen] = useState(false) @@ -55,6 +66,9 @@ export function EnterprisePlatformLoop() { const [cycleId, setCycleId] = useState(0) const [scale, setScale] = useState(1) + const timeline = useMemo(() => buildLoopTimeline(content), [content]) + const blockCount = content.stageBlocks.length + // Track the rendered region width and scale the design-space layer to fill // it, keeping the live layer's proportions locked to the window's. useLayoutEffect(() => { @@ -84,7 +98,7 @@ export function EnterprisePlatformLoop() { setFading(false) setPhase('reply') setStageOpen(true) - setBuiltCount(ENTERPRISE_STAGE_BLOCKS.length) + setBuiltCount(blockCount) } const runCycle = () => { @@ -94,16 +108,16 @@ export function EnterprisePlatformLoop() { setBuiltCount(0) setCycleId((c) => c + 1) timers = [ - setTimeout(() => setPhase('typing'), LOOP_TIMELINE.typing), - setTimeout(() => setPhase('typed'), LOOP_TIMELINE.typed), - setTimeout(() => setPhase('dispatch'), LOOP_TIMELINE.dispatch), - setTimeout(() => setStageOpen(true), LOOP_TIMELINE.stageOpen), - ...ENTERPRISE_STAGE_BLOCKS.map((_, i) => - setTimeout(() => setBuiltCount(i + 1), LOOP_TIMELINE.buildStart + i * BUILD_STEP_MS) + setTimeout(() => setPhase('typing'), timeline.typing), + setTimeout(() => setPhase('typed'), timeline.typed), + setTimeout(() => setPhase('dispatch'), timeline.dispatch), + setTimeout(() => setStageOpen(true), timeline.stageOpen), + ...Array.from({ length: blockCount }, (_, i) => + setTimeout(() => setBuiltCount(i + 1), timeline.buildStart + i * BUILD_STEP_MS) ), - setTimeout(() => setPhase('reply'), LOOP_TIMELINE.reply), - setTimeout(() => setFading(true), LOOP_TIMELINE.total - RESET_FADE_MS), - setTimeout(runCycle, LOOP_TIMELINE.total), + setTimeout(() => setPhase('reply'), timeline.reply), + setTimeout(() => setFading(true), timeline.total - RESET_FADE_MS), + setTimeout(runCycle, timeline.total), ] } @@ -122,7 +136,7 @@ export function EnterprisePlatformLoop() { media.removeEventListener('change', syncMotionPreference) clearScheduled() } - }, []) + }, [timeline, blockCount]) return (
@@ -134,11 +148,23 @@ export function EnterprisePlatformLoop() { transform: `scale(${scale})`, }} > - +
- +
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 ba70b41c8ab..b8ee6d42df9 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 @@ -71,6 +71,15 @@ function SectionLabel({ label, actions }: { label: string; actions?: boolean }) ) } +interface EnterpriseSidebarProps { + /** Workspace name in the header chip. Defaults to the enterprise workspace. */ + workspaceName?: string + /** Recent-chat entries - four fill the design height. Defaults enterprise. */ + chats?: readonly string[] + /** Deployed-workflow entries - five fill the design height. Defaults enterprise. */ + workflows?: readonly string[] +} + /** * The Brightwave workspace sidebar, rendered live (the homepage loop keeps its * baked-screenshot sidebar; the enterprise loop draws its own so the content @@ -78,8 +87,15 @@ function SectionLabel({ label, actions }: { label: string; actions?: boolean }) * Search / Integrations, a filled-out Chats history, the Workspace nav, a full * Workflows section, and the Help / Settings footer. Purely decorative - * hover/click behavior is owned by the parent's `pointer-events-none` frame. + * The workspace name and the chat / workflow entries are injectable so each + * solutions hero can read like that team's workspace; defaults keep the + * enterprise page exactly as it renders today. */ -export function EnterpriseSidebar() { +export function EnterpriseSidebar({ + workspaceName = 'Brightwave', + chats = SIDEBAR_CHATS, + workflows = SIDEBAR_WORKFLOWS, +}: EnterpriseSidebarProps = {}) { return (
{/* Workspace header, matching the real product's WorkspaceHeader chip @@ -99,7 +115,7 @@ export function EnterpriseSidebar() { height={16} className='size-[16px] flex-shrink-0 rounded-sm' /> - Brightwave + {workspaceName}
@@ -114,7 +130,7 @@ export function EnterpriseSidebar() {
- {SIDEBAR_CHATS.map((chat) => ( + {chats.map((chat) => ( ))}
@@ -132,7 +148,7 @@ export function EnterpriseSidebar() {
- {SIDEBAR_WORKFLOWS.map((workflow) => ( + {workflows.map((workflow) => ( ))}
diff --git a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data.ts b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data.ts index 497881d33e5..a32d34a1910 100644 --- a/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data.ts +++ b/apps/sim/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data.ts @@ -146,6 +146,53 @@ export const ENTERPRISE_STAGE_CANVAS = { width: 560, height: 680 } as const /** Where the main pane is within one loop pass. */ export type EnterpriseLoopPhase = 'idle' | 'typing' | 'typed' | 'dispatch' | 'reply' +/** + * Everything domain-specific the platform loop replays - the sidebar's + * workspace identity, the chat exchange, and the staged workflow. The + * solutions pages supply their own content through this shape; the enterprise + * page renders {@link ENTERPRISE_LOOP_CONTENT} by default, so its hero stays + * byte-for-byte what it was before the loop was parametrized. + */ +export interface EnterpriseLoopContent { + /** Workspace name shown in the sidebar header. */ + workspaceName: string + /** The new-chat greeting, personalized like the real workspace Home. */ + greeting: string + /** Composer placeholder shown before the prompt types out. */ + placeholder: string + /** The prompt the loop types - concise enough to type on screen. */ + prompt: string + /** Sim's reply, streamed word by word once the workflow finishes building. */ + reply: string + /** Recent chats in the sidebar - exactly four fill the design height. */ + sidebarChats: readonly [string, string, string, string] + /** Deployed workflows in the sidebar - exactly five fill the design height. */ + sidebarWorkflows: readonly [string, string, string, string, string] + /** Suggested actions under the composer - one per leading icon. */ + suggestedActions: readonly [string, string, string, string] + /** The workflow the chat "builds" on the stage pane, in build order. */ + stageBlocks: BlockDef[] + /** Source → target pairs among {@link stageBlocks}, drawn in build order. */ + stageEdges: ReadonlyArray + /** Design-space bounding box of the staged block layout. */ + stageCanvas: { width: number; height: number } +} + +/** The enterprise hero's own loop content - the parametrized loop's default. */ +export const ENTERPRISE_LOOP_CONTENT: EnterpriseLoopContent = { + workspaceName: 'Brightwave', + greeting: ENTERPRISE_GREETING, + placeholder: COMPOSER_PLACEHOLDER, + prompt: ENTERPRISE_PROMPT, + reply: ENTERPRISE_REPLY, + sidebarChats: SIDEBAR_CHATS, + sidebarWorkflows: SIDEBAR_WORKFLOWS, + suggestedActions: SUGGESTED_ACTIONS, + stageBlocks: ENTERPRISE_STAGE_BLOCKS, + stageEdges: ENTERPRISE_STAGE_EDGES, + stageCanvas: ENTERPRISE_STAGE_CANVAS, +} + /** The idle new-chat view holds this long before typing starts. */ const IDLE_HOLD_MS = 1400 /** Rest on the fully-typed prompt before "send". */ @@ -163,18 +210,31 @@ const REPLY_HOLD_MS = 4800 /** Fade-out length before the cycle restarts. */ export const RESET_FADE_MS = 300 +/** Derived phase starts for one loop pass. */ +export interface LoopTimeline { + typing: number + typed: number + dispatch: number + stageOpen: number + buildStart: number + reply: number + total: number +} + /** - * Derived phase starts. Later stages (tables, files, knowledge base, logs) - * slot in after `reply` by extending {@link EnterpriseLoopPhase} and appending - * starts here. + * Derives the phase starts for a given loop content - the typing beat scales + * with the prompt's length and the build window with the staged block count, + * so every domain's pass keeps the enterprise pacing. Later stages (tables, + * files, knowledge base, logs) slot in after `reply` by extending + * {@link EnterpriseLoopPhase} and appending starts here. */ -export const LOOP_TIMELINE = (() => { +export function buildLoopTimeline(content: EnterpriseLoopContent): LoopTimeline { const typing = IDLE_HOLD_MS - const typed = typing + ENTERPRISE_PROMPT.length * PROMPT_CHAR_MS + const typed = typing + content.prompt.length * PROMPT_CHAR_MS const dispatch = typed + TYPED_HOLD_MS const stageOpen = dispatch + STAGE_OPEN_AFTER_MS const buildStart = stageOpen + BUILD_START_AFTER_MS - const reply = buildStart + (ENTERPRISE_STAGE_BLOCKS.length - 1) * BUILD_STEP_MS + REPLY_AFTER_MS + const reply = buildStart + (content.stageBlocks.length - 1) * BUILD_STEP_MS + REPLY_AFTER_MS const total = reply + REPLY_HOLD_MS - return { typing, typed, dispatch, stageOpen, buildStart, reply, total } as const -})() + return { typing, typed, dispatch, stageOpen, buildStart, reply, total } +} diff --git a/apps/sim/app/(landing)/enterprise/components/feature-graphics/access-control-graphic.tsx b/apps/sim/app/(landing)/enterprise/components/feature-graphics/access-control-graphic.tsx index 677de12d91f..a24147065c1 100644 --- a/apps/sim/app/(landing)/enterprise/components/feature-graphics/access-control-graphic.tsx +++ b/apps/sim/app/(landing)/enterprise/components/feature-graphics/access-control-graphic.tsx @@ -96,11 +96,10 @@ function edgePath(edge: Edge): string { * shrink at narrow tile widths its absolutely-positioned columns (fixed * `left-*` coordinates) would drift right of the box's true midline; * instead it keeps its geometry and overflows both edges equally, so the - * Support → Review axis always sits on the tile's center. On the narrow - * grid bands where the tile drops below the canvas width (small two-column - * screens and the 3-up row just past `lg`) the whole canvas scales down via - * transform — preserving that centering — so the outer team labels are - * never cropped. + * Support → Review axis always sits on the tile's center. Narrow grid + * columns are handled by the feature tile itself, which zooms its whole + * design-space canvas down proportionally (see `SOLUTIONS_VISUAL`), so + * this graphic never needs its own breakpoint scaling. */ export function AccessControlGraphic() { return ( @@ -109,7 +108,7 @@ export function AccessControlGraphic() { aria-hidden='true' className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6' > -
+