feat(landing): extend enterprise product-preview design to solutions and workflows pages (#5672)

* 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 <cursoragent@cursor.com>

* feat(landing): surface Platform and Solutions menus in top nav

Renders PLATFORM_MENU and SOLUTIONS_MENU alongside Resources (Platform,
Solutions, Resources trigger order), drops the internal-only Mothership
item, centers the five-item Platform panel's two-tile last row, and lets
the mobile sheet scroll now that all three sections outgrow a phone
viewport.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(landing): add scheduled tasks page and nav item

Adds the /scheduled-tasks landing page (SolutionsPage consumer with a
chat-free schedule-trigger editor hero and enterprise feature tiles
retold for recurring runs) and a sixth Scheduled Tasks item in the
Platform menu, restoring the clean 3x2 grid.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(landing): add knowledge, tables, files, and logs platform pages

Completes the Platform nav dropdown — its knowledge, tables, files, and
logs items previously 404'd. Each page is a SolutionsPage consumer with a
product-UI hero loop, enterprise-style feature tiles, CTA band, and SEO
metadata. Removes the dead platform-page component family (workflows was
its last consumer) and adds the new routes to the sitemap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(landing): consolidate hero-loop engine and code-window graphics, drop dead variant surface

- Extract shared useDesignScale + useMotionSafeCycle hooks and a HeroLoopShell
  so all seven hero loops share one scale/reduced-motion clock engine
- Merge workflows-editor-loop and scheduled-tasks-hero-loop into one
  parameterized shared EditorLoop (content-injection, same pattern as
  EnterpriseLoopContent)
- Consolidate the four identical code-window feature graphics into one shared
  CodeWindowGraphic + single CSS module; pages keep thin data wrappers
- Remove the unreachable 'split'/'standard'/align variant surface from
  SolutionsPage/SolutionsCardRow/SolutionsCard and trim unused barrel re-exports
- Enterprise hero now consumes the shared PlatformHeroVisual instead of an
  inline byte-identical copy
- Add /scheduled-tasks to the sitemap
- a11y: closed mobile-nav sheet is now invisible (out of tab order) with
  aria-controls wiring; decorative tables-hero checkboxes are no longer
  keyboard-focusable
- memo() the static EnterpriseSidebar; hoist per-render header arrays; remove
  comments that restated component TSDoc

* fix(landing): highlight Scheduled tasks in the scheduled-tasks hero sidebar

Thread activeNav through the shared EditorLoop content so the
scheduled-tasks hero highlights its own module row, matching the other
platform heroes.

* chore(landing): drop spacing constants orphaned by the variant removal

Remove the now-unconsumed cardRowHeaderStack/cardRowHeaderCtaGap/
cardTextToVisual/rowSubtitle constants, fix a stale cardVariant TSDoc
reference, and restore import order in the workflows editor-loop wrapper.

* chore(landing): single-source RESET_FADE_MS and drop the unused card frame size

RESET_FADE_MS now lives only in the shared loop-engine hook module; the
enterprise stage-data copy is removed. SolutionsVisualFrame loses its
size prop and cardHeight constant - only the 16:9 hero preset remained
in use after the split-variant removal.

* fix(landing): make the knowledge-answer question bubble visible on light tiles

The frameless vignette sits directly on the tile's --surface-3 fill, so
the build tile's --surface-3 bubble treatment vanished. The question
chip now uses the graphic family's white card chrome (--white fill,
--border-1 hairline), matching its own source card. Also restores
biome's import order in the workflows editor-loop wrapper (fixes the
failing lint:check CI step).

* chore(landing): share one ZipIcon across the files preview and files hero

Extract the duplicated zip-archive SVG into components/shared/zip-icon
and consume it from both the homepage files preview and the files hero
loop.

---------

Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Waleed Latif <walif6@gmail.com>
This commit is contained in:
andres
2026-07-14 17:38:19 -07:00
committed by GitHub
co-authored by andresdjasso Cursor Waleed Latif
parent c203f51545
commit 516dd7ed0f
123 changed files with 7390 additions and 1240 deletions
+5 -5
View File
@@ -85,13 +85,13 @@ Follow `.claude/rules/constitution.md` exactly: Sim is "the open-source AI works
│ ├── landing-shell/ # light wrapper + skip link + Navbar(stars) + Footer; wraps every page
│ ├── hero-cta/ # the one email-capture + Sign-up CTA (hero + every platform hero)
│ └── logos/ # the one customer-logo set; layout='grid' (hero) | 'row' (platform)
└── platform-page/ # the reusable platform-page layout (Workflows, Tables, Files, …)
├── platform-page.tsx, index.ts, types.ts, constants.ts # PlatformPage + the content contract + spacing
└── components/ # platform-hero, platform-logos-row, platform-card-row (→ card, pill-cta),
# platform-visual-frame, platform-structured-data
└── solutions-page/ # the reusable solutions/platform layout (Workflows, Knowledge, Tables, Files, Logs, solutions/*)
├── solutions-page.tsx, index.ts, types.ts, constants.ts # SolutionsPage + the content contract + spacing
└── components/ # solutions-hero, solutions-logos-row, solutions-card-row (→ card, pill-cta),
# solutions-visual-frame, solutions-structured-data
```
Each section component's TSDoc carries its layout spec - read it before implementing. Section components own their landmark (Navbar → `<header>`, Footer → `<footer>`, the rest → `<section>`); the shared `LandingShell` owns the page frame (light wrapper, skip link, navbar, footer, GitHub stars via `@/lib/github/stars` - fetched at build/revalidate time, never client-fetched), and the page's `<main>` owns the section order and rhythm. Platform routes consume `PlatformPage` with a single content `config` - see `platform-page/CLAUDE`-level TSDoc on `platform-page.tsx`. Sub-components of a section go in `components/<section>/components/`.
Each section component's TSDoc carries its layout spec - read it before implementing. Section components own their landmark (Navbar → `<header>`, Footer → `<footer>`, the rest → `<section>`); the shared `LandingShell` owns the page frame (light wrapper, skip link, navbar, footer, GitHub stars via `@/lib/github/stars` - fetched at build/revalidate time, never client-fetched), and the page's `<main>` owns the section order and rhythm. Platform and solutions routes consume `SolutionsPage` with a single content `config` - see the TSDoc on `solutions-page.tsx`. Sub-components of a section go in `components/<section>/components/`.
Absolute imports only in component code (`@/app/(landing)/components/...`); `index.ts` barrels use relative re-exports (`export { X } from './x'`), matching the workspace convention. Props interfaces for every component. No `utils.ts` until two files share a helper.
@@ -29,6 +29,13 @@ interface HeroWorkflowStageProps {
edges?: ReadonlyArray<readonly [string, string]>
/** 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<HTMLDivElement>(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 }}
>
<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>
)
})}
+2 -15
View File
@@ -18,22 +18,9 @@ export { Lightbox } from './lightbox'
export { LogoShell } from './logo-shell'
export { Mothership } from './mothership/mothership'
export { Navbar } from './navbar'
export type {
PlatformCardConfig,
PlatformCardRowConfig,
PlatformHeroConfig,
PlatformPageConfig,
PlatformPillCta,
} from './platform-page'
export { PlatformPage } from './platform-page'
export { PlatformHeroVisual } from './platform-hero-visual'
export { ProductDemo } from './product-demo'
export { ShareButton } from './share-button'
export { SiteStructuredData } from './site-structured-data'
export type {
SolutionsCardConfig,
SolutionsCardRowConfig,
SolutionsHeroConfig,
SolutionsPageConfig,
SolutionsPillCta,
} from './solutions-page'
export type { SolutionsPageConfig } from './solutions-page'
export { SolutionsPage } from './solutions-page'
@@ -6,8 +6,9 @@ import type {
} from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import { LandingPreviewResource } from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/landing-preview-resource'
import { ownerCell } from '@/app/(landing)/components/landing-preview/components/landing-preview-resource/utils'
import { ZipIcon } from '@/app/(landing)/components/shared/zip-icon'
/** Generic audio/zip icon using basic SVG since no dedicated component exists */
/** Generic audio icon using basic SVG since no dedicated component exists */
function AudioIcon({ className }: { className?: string }) {
return (
<svg
@@ -46,27 +47,6 @@ function JsonlIcon({ className }: { className?: string }) {
)
}
function ZipIcon({ className }: { className?: string }) {
return (
<svg
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
className={className}
>
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
<path d='M10 6h1' />
<path d='M10 10h1' />
<path d='M10 14h1' />
<path d='M9 18h2v2h-2z' />
</svg>
)
}
const COLUMNS: PreviewColumn[] = [
{ id: 'name', header: 'Name' },
{ id: 'size', header: 'Size' },
@@ -23,8 +23,12 @@ import { DEMO_HREF, SIGNUP_HREF } from '@/app/(landing)/constants'
* leaf hydrates; the desktop nav stays server-rendered.
*
* The sheet locks body scroll while open and closes on route change intent
* (any link tap) and on `Escape`. Motion is a short token-driven
* transform/opacity that collapses under `prefers-reduced-motion`.
* (any link tap) and on `Escape`. With all three mega-menus expanded the
* sections outgrow a phone viewport, so the sheet caps its height at the
* space under the bar (`100dvh` minus the bar's 62px) and scrolls internally
* (`overscroll-contain` keeps the locked page from rubber-banding behind it).
* Motion is a short token-driven transform/opacity that collapses under
* `prefers-reduced-motion`.
*/
interface MobileNavProps {
@@ -35,9 +39,9 @@ interface MobileNavProps {
/**
* Standalone top-level routes shown in the sheet alongside the expanded mega-menu
* sections. Pricing is a standalone link on the desktop nav (not a mega-menu), so
* it stays a single row here too. Restoring `PLATFORM_MENU`/`SOLUTIONS_MENU` to
* {@link NAV_MENUS} automatically expands them here as grouped sections too - the
* sheet mirrors the desktop nav's information architecture with no extra edit.
* it stays a single row here too. Every menu in {@link NAV_MENUS} expands here as
* a grouped section automatically - the sheet mirrors the desktop nav's
* information architecture with no extra edit.
*/
const STANDALONE_LINKS = [
{ label: 'Enterprise', href: '/enterprise' },
@@ -78,6 +82,7 @@ export function MobileNav({ stars }: MobileNavProps) {
type='button'
aria-label={open ? 'Close menu' : 'Open menu'}
aria-expanded={open}
aria-controls='mobile-nav-sheet'
onClick={() => setOpen((v) => !v)}
className='flex size-[30px] items-center justify-center rounded-lg border border-[var(--border-1)] text-[var(--text-icon)] transition-colors hover:bg-[var(--surface-hover)]'
>
@@ -95,12 +100,13 @@ export function MobileNav({ stars }: MobileNavProps) {
) : null}
<div
id='mobile-nav-sheet'
className={cn(
'absolute top-full right-0 left-0 z-50 origin-top border-[var(--border)] border-b transition-[opacity,transform] duration-200 motion-reduce:transition-none',
'absolute top-full right-0 left-0 z-50 max-h-[calc(100dvh-62px)] origin-top overflow-y-auto overscroll-contain border-[var(--border)] border-b transition-[opacity,transform,visibility] duration-200 motion-reduce:transition-none',
NAVBAR_GLASS_SURFACE,
open
? 'pointer-events-auto translate-y-0 opacity-100'
: '-translate-y-2 pointer-events-none opacity-0'
? 'pointer-events-auto visible translate-y-0 opacity-100'
: '-translate-y-2 pointer-events-none invisible opacity-0'
)}
>
<div className='mx-auto flex w-full max-w-[1460px] flex-col gap-1 px-5 pt-2 pb-5'>
@@ -7,11 +7,6 @@ import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-m
export const PLATFORM_MENU: NavMenu = {
label: 'Platform',
items: [
{
title: 'Mothership',
description: 'Build and command agents in natural language',
href: '/',
},
{
title: 'Workflows',
description: 'Design agent logic visually',
@@ -32,6 +27,11 @@ export const PLATFORM_MENU: NavMenu = {
description: 'One file store for team and agents',
href: '/files',
},
{
title: 'Scheduled Tasks',
description: 'Run agents on a cadence',
href: '/scheduled-tasks',
},
{
title: 'Logs',
description: 'Trace every agent decision',
@@ -121,13 +121,9 @@ export const SOLUTIONS_MENU: NavMenu = {
}
/**
* Navbar mega-menus that are currently rendered, in trigger order - shared by
* desktop and mobile nav (the desktop bar maps this list; mobile nav derives
* its visible sections from these labels).
*
* `PLATFORM_MENU` and `SOLUTIONS_MENU` are intentionally omitted for now to hide
* those sections from navigation while their pages stay live and reachable by
* direct URL. The menu definitions above are kept intact - to restore a section,
* add its constant back to this list.
* Navbar mega-menus in trigger order - shared by desktop and mobile nav (the
* desktop bar maps this list; mobile nav derives its visible sections from
* these labels). Platform leads with the product modules, Solutions follows
* with the per-team pages, and Resources closes with learning surfaces.
*/
export const NAV_MENUS = [RESOURCES_MENU] as const
export const NAV_MENUS = [PLATFORM_MENU, SOLUTIONS_MENU, RESOURCES_MENU] as const
@@ -30,6 +30,12 @@ import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-m
* The panel chrome replicates the `ChipModal` framed-card look exactly: an outer
* `--surface-4` ring (`p-[3px]`, overlay shadow) wrapping an inner `--bg`
* surface, with the item grid padded inside.
*
* The grid renders three visual columns on six tracks (each tile spans two),
* which keeps six-item menus pixel-identical to a plain three-column grid while
* letting a five-item menu center its two-tile last row - the second-to-last
* tile starts on track two, so the short row sits symmetrically instead of
* leaving a hole in the corner.
*/
interface NavMenuChipProps {
@@ -74,7 +80,14 @@ export function NavMenuChip({ menu }: NavMenuChipProps) {
<div className={cn(PANEL_BASE, !closed && PANEL_REVEAL)}>
<div className='w-[840px] rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-[var(--shadow-overlay)]'>
<div className='rounded-lg border border-[var(--border-1)] bg-[var(--bg)] p-2'>
<div className='grid grid-cols-3 gap-1' role='group' aria-label={label}>
<div
className={cn(
'grid grid-cols-6 gap-1 [&>*]:col-span-2',
items.length % 3 === 2 && '[&>*:nth-last-child(2)]:col-start-2'
)}
role='group'
aria-label={label}
>
{items.map((item) => (
<NavMenuItem key={item.title} item={item} onSelect={handleSelect} />
))}
@@ -0,0 +1 @@
export { PlatformHeroVisual } from '@/app/(landing)/components/platform-hero-visual/platform-hero-visual'
@@ -0,0 +1,38 @@
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 every hero - the enterprise page
* included - reuses the exact backdrop, window geometry (`aspect-[1080/620]`
* at 83.08% width), and shadow treatment the enterprise page established.
*
* 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 (
<div className='relative h-full w-full'>
<Image
fill
priority
fetchPriority='high'
alt=''
className='object-cover object-center'
sizes='(max-width: 1024px) 100vw, 1300px'
src='/landing/enterprise-hero-background.webp'
/>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 flex aspect-[1080/620] w-[83.08%] flex-col overflow-hidden rounded-[10px] bg-[var(--surface-1)] shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_6px_0_rgba(0,0,0,0.05),0_4px_42px_0_rgba(0,0,0,0.06)]'>
<div className='relative flex-1'>{children}</div>
</div>
</div>
)
}
@@ -1,4 +0,0 @@
export { PlatformCardRow } from './platform-card-row'
export { PlatformHero } from './platform-hero'
export { PlatformLogosRow } from './platform-logos-row'
export { PlatformStructuredData } from './platform-structured-data'
@@ -1,2 +0,0 @@
export { PlatformCard } from './platform-card'
export { PlatformPillCta } from './platform-pill-cta'
@@ -1 +0,0 @@
export { PlatformCard } from './platform-card'
@@ -1,42 +0,0 @@
import { cn } from '@sim/emcn'
import { PlatformVisualFrame } from '@/app/(landing)/components/platform-page/components/platform-visual-frame'
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
import type { PlatformCardConfig } from '@/app/(landing)/components/platform-page/types'
/**
* A single platform card - an `<article>` with an `<h3>` title, a body-color
* description, and a reserved visual panel beneath. Text sits directly on the
* canvas (matching the hero and every other landing section); only the visual
* carries the `--surface-2` panel chrome, so the product mock reads as the one
* elevated surface and never blends into a competing card fill.
*
* The card owns the gap between its text and visual (`cardTextToVisual`) and the
* title→description stack (`cardTextStack`) - both from named spacing constants.
* The text block grows (`flex-1`) so the visual pins to the bottom of the
* grid-stretched cell: every card's visual aligns on one baseline regardless of
* description length. The visual lands in a fixed-height {@link PlatformVisualFrame}
* so the row stays uniform and CLS is zero.
*
* A content unit only: it accepts copy and a visual node, never any layout knob.
*/
interface PlatformCardProps {
card: PlatformCardConfig
/** Stable id wiring the `<h3>` into the page outline. */
headingId: string
}
export function PlatformCard({ card, headingId }: PlatformCardProps) {
return (
<article className={cn('flex h-full flex-col', PLATFORM_SPACING.cardTextToVisual)}>
<div className={cn('flex flex-1 flex-col', PLATFORM_SPACING.cardTextStack)}>
<h3 id={headingId} className='text-[18px] text-[var(--text-primary)] leading-[1.3]'>
{card.title}
</h3>
<p className='text-[15px] text-[var(--text-body)] leading-[1.5]'>{card.description}</p>
</div>
<PlatformVisualFrame size='card'>{card.visual}</PlatformVisualFrame>
</article>
)
}
@@ -1 +0,0 @@
export { PlatformPillCta } from './platform-pill-cta'
@@ -1,47 +0,0 @@
'use client'
import { ChipLink } from '@sim/emcn'
import { ArrowRight } from 'lucide-react'
import type { PlatformPillCta as PlatformPillCtaConfig } from '@/app/(landing)/components/platform-page/types'
/**
* The card-row pill CTA - a single primary `ChipLink` with a trailing arrow,
* matching the reference image's "Learn about automations →". The chip owns its
* own chrome and spacing; this component only wires the label, the href, and the
* arrow icon, exposing no layout knobs.
*
* Client leaf: `ChipLink` is a Client Component and its `rightIcon` is a
* component reference (`ArrowRight`), which cannot cross the server→client
* boundary as a prop - so the icon must be wired from client code, exactly as the
* navbar's `NavMenuChip` does. The props it receives ({@link PlatformPillCtaConfig})
* are plain serializable data, so the surrounding layout stays Server Components.
*
* Link safety: an external href (absolute `http(s)://`) renders with
* `rel='noopener noreferrer'` and `target='_blank'`; an internal href routes
* through the Next `<Link>` that `ChipLink` is built on - so every link is
* crawlable and safe with no per-page ceremony.
*/
interface PlatformPillCtaProps {
cta: PlatformPillCtaConfig
}
/** Returns true for absolute external URLs (http/https), which need rel/target hardening. */
function isExternalHref(href: string): boolean {
return /^https?:\/\//.test(href)
}
export function PlatformPillCta({ cta }: PlatformPillCtaProps) {
const external = isExternalHref(cta.href)
return (
<ChipLink
variant='primary'
href={cta.href}
rightIcon={ArrowRight}
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
>
{cta.label}
</ChipLink>
)
}
@@ -1 +0,0 @@
export { PlatformCardRow } from './platform-card-row'
@@ -1,72 +0,0 @@
import { cn } from '@sim/emcn'
import {
PlatformCard,
PlatformPillCta,
} from '@/app/(landing)/components/platform-page/components/platform-card-row/components'
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
import type { PlatformCardRowConfig } from '@/app/(landing)/components/platform-page/types'
/**
* A card row - the core repeating unit of a platform page. A header block (an
* `<h2>` 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.
*
* Rendered as a labelled `<section>` for a clean, crawlable landmark; each card
* is an `<article>` with an `<h3>`, keeping the strict H2 → H3 hierarchy. Every
* gap (header sub-stack, header-to-grid, and inter-card) is owned by named
* spacing constants; this component exposes no layout prop.
*/
interface PlatformCardRowProps {
row: PlatformCardRowConfig
}
/** Maps a supported card count to its grid column class; anything else falls back to three-up. */
const GRID_COLS: Record<number, string> = {
3: 'grid-cols-3',
4: 'grid-cols-4',
}
export function PlatformCardRow({ row }: PlatformCardRowProps) {
const headingId = `platform-row-${row.id}-heading`
const gridCols = GRID_COLS[row.cards.length] ?? GRID_COLS[3]
return (
<section
id={`platform-row-${row.id}`}
aria-labelledby={headingId}
className={cn('flex flex-col', PLATFORM_SPACING.cardRowHeaderToGrid)}
>
<div className={cn('flex flex-col items-start', PLATFORM_SPACING.cardRowHeaderStack)}>
<h2
id={headingId}
className='max-w-[760px] text-balance text-[32px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[24px]'
>
{row.title}
</h2>
<p className='max-w-[640px] text-[20px] text-[var(--text-body)] leading-[1.5]'>
{row.subtitle}
</p>
<PlatformPillCta cta={row.cta} />
</div>
<div
className={cn(
'grid',
gridCols,
'max-sm:grid-cols-1 max-md:grid-cols-2',
PLATFORM_SPACING.cardGridGap
)}
>
{row.cards.map((card, index) => (
<PlatformCard
key={`${row.id}-${card.title}`}
card={card}
headingId={`platform-row-${row.id}-card-${index}-heading`}
/>
))}
</div>
</section>
)
}
@@ -1 +0,0 @@
export { PlatformHero } from './platform-hero'
@@ -1,61 +0,0 @@
import { cn } from '@sim/emcn'
import { HeroCta } from '@/app/(landing)/components/hero-cta'
import { LANDING_HERO_CTA_GAP } from '@/app/(landing)/components/landing-layout'
import { PlatformVisualFrame } from '@/app/(landing)/components/platform-page/components/platform-visual-frame'
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
import type { PlatformHeroConfig } from '@/app/(landing)/components/platform-page/types'
/**
* Platform hero - the only `<h1>` on a platform page. Left-aligned header copy
* (headline + supporting description) sits above the same CTA as the landing
* hero ({@link HeroCta}, the single source of truth), then a full-width platform
* visual underneath.
*
* The header column and the visual are stacked in one flex column; the header's
* own sub-stack (headline → description → CTA) and the gap down to the visual are
* both owned by named spacing constants, so a consumer page passes only copy and
* a visual node - never any spacing. The visual renders into a reserved-aspect
* {@link PlatformVisualFrame} (CLS = 0).
*
* Carries the page's sr-only ~50-word product summary for AI citation (GEO). The
* section's horizontal gutter is owned by `PlatformPage`; this component sets none.
*/
interface PlatformHeroProps {
hero: PlatformHeroConfig
}
export function PlatformHero({ hero }: PlatformHeroProps) {
return (
<section
id='platform-hero'
aria-labelledby='platform-hero-heading'
className={cn(
'flex flex-col',
PLATFORM_SPACING.heroTopPadding,
PLATFORM_SPACING.heroToVisual
)}
>
<p className='sr-only'>{hero.summary}</p>
<div className={cn('flex flex-col items-start text-left', PLATFORM_SPACING.heroStack)}>
<h1
id='platform-hero-heading'
className='max-w-[900px] text-balance text-[48px] text-[var(--text-primary)] leading-[1.1] max-sm:text-[32px] max-xl:text-[40px]'
>
{hero.heading}
</h1>
<p className='max-w-[640px] text-[20px] text-[var(--text-body)] leading-[1.5]'>
{hero.description}
</p>
<div className={cn('max-sm:w-full', LANDING_HERO_CTA_GAP)}>
<HeroCta />
</div>
</div>
<PlatformVisualFrame size='hero'>{hero.visual}</PlatformVisualFrame>
</section>
)
}
@@ -1 +0,0 @@
export { PlatformLogosRow } from './platform-logos-row'
@@ -1,30 +0,0 @@
import { Logos } from '@/app/(landing)/components/logos'
/**
* Platform logos row - the same customer wordmarks as the landing hero, in a
* single horizontally-centered row at the shared `gap-x-24` rhythm (owned by the
* shared {@link Logos} component, `row` layout). Takes no props and exposes no
* spacing: its horizontal gutter comes from `PlatformPage` and its inter-section
* spacing from the page's `<main>` gap.
*
* Wrapped as a labelled `<section>` so it is a discrete, crawlable landmark; the
* heading is sr-only because the logos are a proof band rather than a content
* section, but the H2 keeps the page's heading hierarchy intact. The section is
* `relative` so the `sr-only` (`position: absolute`) heading is contained by it
* rather than falling back to the document root - matching {@link Features}'s
* `relative` wrapper for its own `sr-only` heading, and avoiding a phantom root
* scrollbar (the heading's un-offset static position would otherwise inflate
* `document.documentElement`'s scroll height by its own position in the page).
*/
export function PlatformLogosRow() {
return (
<section id='platform-logos' aria-labelledby='platform-logos-heading' className='relative'>
<h2 id='platform-logos-heading' className='sr-only'>
Companies building AI agents with Sim
</h2>
<div className='flex justify-center'>
<Logos layout='row' />
</div>
</section>
)
}
@@ -1 +0,0 @@
export { PlatformStructuredData } from './platform-structured-data'
@@ -1,68 +0,0 @@
import { SITE_URL } from '@/lib/core/utils/urls'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import type { PlatformPageConfig } from '@/app/(landing)/components/platform-page/types'
/**
* JSON-LD for a platform page - a `WebPage` (about a `WebApplication`) plus a
* `BreadcrumbList`, rendered server-side before any visible content so crawlers
* and AI answer engines read the structured data first.
*
* Everything is derived from the same {@link PlatformPageConfig} that drives the
* visible sections, so the structured data can never drift from the page: the
* `WebApplication.featureList` is the deduped set of card titles actually
* rendered, the page name/description come from the hero, and the breadcrumb
* from the module + path. The page author maintains zero schema by hand.
*
* Server Component; no client cost. Internal to the platform layout - emitted by
* `PlatformPage`, never rendered by a consumer directly.
*/
interface PlatformStructuredDataProps {
config: PlatformPageConfig
}
export function PlatformStructuredData({ config }: PlatformStructuredDataProps) {
const { module, path, hero, rows } = config
const url = `${SITE_URL}${path}`
const featureList = Array.from(
new Set(rows.flatMap((row) => row.cards.map((card) => card.title)))
)
const jsonLd = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebPage',
'@id': `${url}#webpage`,
url,
name: hero.heading,
description: hero.summary,
isPartOf: { '@id': `${SITE_URL}#website` },
about: { '@id': `${url}#application` },
breadcrumb: { '@id': `${url}#breadcrumb` },
inLanguage: 'en-US',
},
{
'@type': 'BreadcrumbList',
'@id': `${url}#breadcrumb`,
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
{ '@type': 'ListItem', position: 2, name: module, item: url },
],
},
{
'@type': 'WebApplication',
'@id': `${url}#application`,
name: `Sim ${module}`,
description: hero.summary,
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
url,
featureList,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
},
],
}
return <JsonLd data={jsonLd} />
}
@@ -1 +0,0 @@
export { PlatformVisualFrame } from './platform-visual-frame'
@@ -1,41 +0,0 @@
import type { ReactNode } from 'react'
import { cn } from '@sim/emcn'
import { PLATFORM_VISUAL } from '@/app/(landing)/components/platform-page/constants'
/**
* The one escape hatch in the platform layout - a fixed-dimension frame that
* holds a page-supplied visual `ReactNode`. The frame owns its chrome (the
* hero-visual family: `--surface-2` fill, `--border-1` hairline, `rounded-lg`,
* `overflow-hidden`) and, crucially, its dimensions: a `hero` frame reserves a
* 16:9 aspect ratio, a `card` frame a fixed height. Because the size is reserved
* before paint and the node fills `h-full w-full` inside, a dropped-in node can
* neither shift surrounding layout (CLS = 0) nor change the frame's own padding.
*
* The frame is decorative chrome around product visuals, so it is `aria-hidden`;
* the page's meaning lives in the adjacent headings and copy.
*/
interface PlatformVisualFrameProps {
/**
* Reserved-dimension preset.
* - `hero` - full-width 16:9 frame for the platform hero visual.
* - `card` - fixed-height frame for a card's visual panel.
*/
size: 'hero' | 'card'
/** The page-supplied visual island or static panel. Fills the frame; owns no chrome. */
children: ReactNode
}
export function PlatformVisualFrame({ size, children }: PlatformVisualFrameProps) {
return (
<div
aria-hidden='true'
className={cn(
'w-full overflow-hidden rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)]',
size === 'hero' ? PLATFORM_VISUAL.heroAspect : PLATFORM_VISUAL.cardHeight
)}
>
<div className='h-full w-full'>{children}</div>
</div>
)
}
@@ -1,60 +0,0 @@
/**
* Platform-layout spacing - the single source of truth for every gutter, gap,
* inset, and reserved dimension used across the platform-page components.
*
* The padding fortress lives here. No platform component accepts a `className`,
* `style`, or any layout-override prop, and none hard-codes a spacing value
* inline. Every measurement a reviewer might want to audit - the horizontal
* gutter, the inter-section rhythm, the card-grid gaps, the card stack, and the
* fixed visual-slot dimensions - is named in this one file. To change spacing
* you edit a constant here; a consumer page literally cannot reach it.
*
* All values are Tailwind class fragments (not raw numbers) so they compose
* directly into `className` strings inside the components and stay legible.
*/
export const PLATFORM_SPACING = {
/**
* The one horizontal gutter, owned solely by `PlatformPage`. Matches the
* navbar and landing hero exactly (`px-20 max-lg:px-8 max-sm:px-5`) so platform
* content starts on the wordmark's vertical line at every breakpoint. Sections
* and cards never set their own gutter.
*/
gutter: 'px-20 max-lg:px-8 max-sm:px-5',
/**
* Inter-section vertical rhythm - the gap of the `<main>` flex column that
* `PlatformPage` owns. Sections carry no vertical margin/padding of their own,
* so this is the only knob for the space between the hero, logos, and every
* card row. Tightens on smaller screens in lockstep with the landing `<main>`
* (`gap-[120px] max-lg:gap-[88px] max-sm:gap-16`).
*/
sectionRhythm: 'gap-[120px] max-lg:gap-[88px] max-sm:gap-16',
/** Hero text top padding, matching the landing hero (`pt-[112px] max-sm:pt-12`). */
heroTopPadding: 'pt-[112px] max-sm:pt-12',
/** Vertical stack gap inside the hero header column (headline → description → CTA). */
heroStack: 'gap-[22px]',
/** Gap between the hero header column and the full-width hero visual beneath it. */
heroToVisual: 'gap-12',
/** Gap between a card row's header block and the card grid beneath it. */
cardRowHeaderToGrid: 'gap-12',
/** Vertical stack gap inside a card row's header (title → subtitle → CTA). */
cardRowHeaderStack: 'gap-5',
/** Gap between cards within a card-row grid (both axes). */
cardGridGap: 'gap-8',
/** Minimum gap between a card's text block and its visual panel. */
cardTextToVisual: 'gap-5',
/** Vertical stack gap inside a card's text block (title → description). */
cardTextStack: 'gap-2',
} as const
/**
* Reserved fixed dimensions for the component-owned visual frames. A dropped-in
* `ReactNode` renders into a frame of exactly these dimensions, so it can never
* shift surrounding layout (CLS = 0) nor change its own frame padding. The node
* fills `h-full w-full` inside; it owns nothing about the frame.
*/
export const PLATFORM_VISUAL = {
/** Full-width hero visual aspect ratio - reserves height before paint. */
heroAspect: 'aspect-[16/9]',
/** Fixed height of a card's visual panel - uniform across every card. */
cardHeight: 'h-[240px]',
} as const
@@ -1,8 +0,0 @@
export { PlatformPage } from './platform-page'
export type {
PlatformCardConfig,
PlatformCardRowConfig,
PlatformHeroConfig,
PlatformPageConfig,
PlatformPillCta,
} from './types'
@@ -1,59 +0,0 @@
import { cn } from '@sim/emcn'
import {
PlatformCardRow,
PlatformHero,
PlatformLogosRow,
PlatformStructuredData,
} from '@/app/(landing)/components/platform-page/components'
import { PLATFORM_SPACING } from '@/app/(landing)/components/platform-page/constants'
import type { PlatformPageConfig } from '@/app/(landing)/components/platform-page/types'
/**
* The reusable platform-page content stack - the single component six routes
* (Workflows, Tables, Files, Knowledge Base, Scheduled Tasks, Logs) consume with
* near-zero ceremony. A route renders the shared shell and drops in one
* `<PlatformPage config={…} />`.
*
* This component owns the entire `<main>`: the shared `max-w-[1460px]` content
* column (centered with `mx-auto`, matching the navbar and landing sections), the
* one horizontal gutter (`PLATFORM_SPACING.gutter`), and the inter-section
* vertical rhythm (`PLATFORM_SPACING.sectionRhythm`, the `<main>` 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.
*
* The order is fixed: structured data first (before visible content, derived
* from the same config so it never drifts) → platform hero (the page's only
* `<h1>`) → 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.
*/
interface PlatformPageProps {
/** The complete page content - identity, hero, and ordered card rows. */
config: PlatformPageConfig
}
export function PlatformPage({ config }: PlatformPageProps) {
return (
<>
<PlatformStructuredData config={config} />
<main
id='main-content'
className={cn(
'mx-auto flex w-full max-w-[1460px] flex-col',
PLATFORM_SPACING.sectionRhythm,
PLATFORM_SPACING.gutter
)}
>
<PlatformHero hero={config.hero} />
<PlatformLogosRow />
{config.rows.map((row) => (
<PlatformCardRow key={row.id} row={row} />
))}
</main>
</>
)
}
@@ -1,96 +0,0 @@
import type { ReactNode } from 'react'
/**
* Platform-page configuration - the entire content contract a route passes to
* {@link PlatformPage}. Every field is content-only: strings for copy, `ReactNode`
* exclusively for the designated visual/animation slots, and typed config arrays
* for card rows. There is deliberately no `className`, `style`, width, height,
* padding, margin, or any other layout knob anywhere in this tree - spacing
* lives entirely inside the components (see `PLATFORM_SPACING`). A page describes
* WHAT to show; the layout decides WHERE and with how much space.
*/
/** A single pill call-to-action - label plus destination, used by card rows. */
export interface PlatformPillCta {
/** Visible link text. A trailing arrow is added by the component. */
label: string
/** Destination href. Internal hrefs render as Next `<Link>`; external as a safe anchor. */
href: string
}
/** The platform hero - header copy, the shared CTA, and a full-width visual. */
export interface PlatformHeroConfig {
/**
* The page's single `<h1>`. Per the constitution it should name the module and
* "Sim"/"AI workspace" (e.g. "Workflows - the visual builder in Sim, the AI workspace").
*/
heading: string
/** Supporting description beneath the heading, in the body color. */
description: string
/**
* ~50-word sr-only atomic summary for AI citation (GEO). Names "Sim" explicitly
* and states what the module is, who it's for, and what it does.
*/
summary: string
/**
* The full-width hero visual - a page-supplied client island or static panel.
* Renders into a component-owned frame with reserved aspect ratio (CLS = 0) and
* is marked `aria-hidden`; it owns nothing about the frame's chrome or spacing.
*/
visual: ReactNode
}
/** A single card - text plus a reserved visual panel. Rendered as an `<article>`. */
export interface PlatformCardConfig {
/** The card's `<h3>` title. */
title: string
/**
* Supporting description beneath the title, in the body color. Self-contained
* and names "Sim" - never "the platform" or a bare pronoun - so each card is an
* independently quotable answer block.
*/
description: string
/**
* The card's visual/animation - a page-supplied node. Renders into a
* component-owned, fixed-height frame (CLS = 0), marked `aria-hidden`. The card
* owns the spacing around both the text and this frame.
*/
visual: ReactNode
}
/**
* 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.
*/
export interface PlatformCardRowConfig {
/**
* Stable section id for the `<section>` landmark and `aria-labelledby` wiring.
* Must be unique within the page (e.g. `'build'`).
*/
id: string
/** The row's `<h2>` title, in the headline color - reads as an answer to a user question. */
title: string
/** Supporting subtitle beneath the title, in the body color, naming "Sim". */
subtitle: string
/** The row's single pill CTA. */
cta: PlatformPillCta
/** The cards in this row - 3 or 4. The grid derives its columns from this length. */
cards: PlatformCardConfig[]
}
/**
* The complete platform-page content: page identity (for structured data), one
* hero, plus ordered card rows. A route passes exactly this object to
* {@link PlatformPage} and nothing else.
*/
export interface PlatformPageConfig {
/** Module name, e.g. "Workflows" - used in the breadcrumb and schema.org name. */
module: string
/** Canonical path, e.g. "/workflows" - used to build the JSON-LD `url`/breadcrumb. */
path: string
/** The hero (the page's only `<h1>`). */
hero: PlatformHeroConfig
/** Card rows rendered in order beneath the logos row. */
rows: PlatformCardRowConfig[]
}
@@ -0,0 +1,62 @@
/**
* The CodeWindowGraphic's motion: the code lines stamp in top to bottom
* one after another — the audit tile's one-shot settle, never re-played;
* the caret blink is Tailwind's `animate-pulse`, disabled through its
* own `motion-reduce:animate-none`. Under prefers-reduced-motion the
* lines render fully settled.
*/
.line0,
.line1,
.line2,
.line3,
.line4,
.line5 {
animation: code-window-line-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.line0 {
animation-delay: 0.35s;
}
.line1 {
animation-delay: 0.5s;
}
.line2 {
animation-delay: 0.65s;
}
.line3 {
animation-delay: 0.8s;
}
.line4 {
animation-delay: 0.95s;
}
.line5 {
animation-delay: 1.1s;
}
@keyframes code-window-line-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.line0,
.line1,
.line2,
.line3,
.line4,
.line5 {
animation: none;
}
}
@@ -0,0 +1,107 @@
import type { ReactNode } from 'react'
import { cn } from '@sim/emcn'
import styles from '@/app/(landing)/components/shared/code-window-graphic/code-window-graphic.module.css'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
/** One inline run of code text inside a {@link CodeWindowGraphic} line. */
export interface CodeSegment {
/** Segment text. */
text: string
/** Ink treatment on the dark tile: `muted` scaffolding or `primary` payload. */
tone?: 'muted' | 'primary'
}
interface CodeWindowGraphicProps {
/**
* Title-bar mark, rendered inside the outlined `size-6` icon box - pass a
* `size-[14px]` icon in `--text-muted-inverse` to match the family.
*/
icon: ReactNode
/** Filename shown beside the icon in the title bar. */
filename: string
/** Code excerpt, one segment array per line - at most six lines animate. */
lines: readonly (readonly CodeSegment[])[]
}
/** Per-line stamp-in classes - the stagger order is baked into each class's delay. */
const LINE_STEP_CLASSES = [
styles.line0,
styles.line1,
styles.line2,
styles.line3,
styles.line4,
styles.line5,
] as const
/** Segment inks on the dark tile - the deploy tile's dark-surface palette. */
const SEGMENT_TONE_CLASS = {
muted: 'text-[var(--text-muted-inverse)]',
primary: 'text-[var(--text-inverse)]',
} as const
/** Shared hairline ink for the window outline, header rule, and icon box. */
const OUTLINE_INK = 'border-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'
/**
* The dark-tile outlined editor window shared by the platform pages' "from
* code" feature graphics (Workflows, Knowledge, Tables, Files): the
* agent-code tile's exact slot geometry (`top-5`, `left-0`, bleeding off
* the right and bottom edges, `rounded-tl-xl`) drawn as an outlined shell -
* faint `--text-muted-inverse` hairlines with the dark tile showing
* through, the deploy tile's browser-window vocabulary. Its `h-12` title
* bar pairs the caller's icon (in an outlined `size-6` icon box, the
* lifecycle header's treatment in dark ink) with the caller's filename
* over a hairline rule.
*
* Inside, the caller's code excerpt sits settled in mono - scaffolding in
* `--text-muted-inverse`, payload in `--text-inverse` - with a blinking
* caret holding the last line. The lines stamp in top to bottom once (from
* `code-window-graphic.module.css`, the audit tile's one-shot settle);
* under `prefers-reduced-motion` the excerpt renders fully settled with no
* caret blink.
*/
export function CodeWindowGraphic({ icon, filename, lines }: CodeWindowGraphicProps) {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className={cn(
'absolute top-5 right-0 bottom-0 left-0 rounded-tl-xl border-t border-l',
OUTLINE_INK
)}
>
<div className={cn('flex h-12 items-center gap-2 border-b px-4', OUTLINE_INK)}>
<span
className={cn('flex size-6 items-center justify-center rounded-md border', OUTLINE_INK)}
>
{icon}
</span>
<span className='font-medium text-[var(--text-inverse)] text-base'>{filename}</span>
</div>
<div className='space-y-2 p-4 font-mono text-[var(--text-muted-inverse)] text-caption leading-[1.7]'>
{lines.map((line, index) => (
<div key={index} className={cn('flex gap-3', LINE_STEP_CLASSES[index])}>
<span className='w-3 select-none text-right text-[var(--text-muted-inverse)]'>
{index + 1}
</span>
<code className='truncate whitespace-pre'>
{line.map((segment, segmentIndex) => (
<span
key={segmentIndex}
className={segment.tone && SEGMENT_TONE_CLASS[segment.tone]}
>
{segment.text}
</span>
))}
{index === lines.length - 1 && (
<span className='ml-px inline-block h-[1.1em] w-px translate-y-[2px] animate-pulse bg-[var(--text-inverse)] align-text-bottom motion-reduce:animate-none' />
)}
</code>
</div>
))}
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1 @@
export { type CodeSegment, CodeWindowGraphic } from './code-window-graphic'
@@ -0,0 +1,120 @@
'use client'
import { useState } from 'react'
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 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. */
const IDLE_HOLD_MS = 900
/** Block N (build order) pops in at IDLE_HOLD_MS + N * BUILD_STEP_MS. */
const BUILD_STEP_MS = 620
/** The story block gets its selection ring this long after the last block. */
const SELECT_AFTER_MS = 700
/** The finished, selected canvas holds this long before the fade. */
const SELECTED_HOLD_MS = 5200
/** Domain content one editor-loop hero replays - sidebar identity + staged flow. */
export interface EditorLoopContent {
/** Recent-chat entries in the sidebar - four fill the design height. */
sidebarChats: readonly string[]
/** Deployed workflows in the sidebar - five fill the design height. */
sidebarWorkflows: readonly string[]
/** Canvas blocks, ordered by build sequence. */
blocks: BlockDef[]
/** Source → target pairs, drawn in order as their endpoints land on canvas. */
edges: ReadonlyArray<readonly [string, string]>
/** Design-space bounding box of the block layout. */
canvas: { width: number; height: number }
/** The block the "editing" beat selects once the flow is assembled. */
selectedBlockId: string
/** Workspace-nav row to highlight in the sidebar; unset keeps New chat active. */
activeNav?: EnterpriseSidebarProps['activeNav']
}
interface EditorLoopProps {
/** The page's sidebar identity and staged workflow. */
content: EditorLoopContent
}
/**
* The chat-free sibling of the enterprise platform loop, shared by the
* workflows and scheduled-tasks heroes. Same architecture (the
* {@link HeroLoopShell}'s fixed 1280x735 design-space layer scaled to the
* window, a parent-owned clock driving a presentational stage, reduced-motion
* showing the finished frame) and the same live sidebar, but the workspace
* pane is the editor canvas itself: the content's workflow assembles block by
* block (edges stroke-draw as endpoints land), then the story block picks up
* the real editor's selection ring - the "being edited" beat - before the
* scene fades and the cycle restarts.
*
* Everything is `pointer-events-none` decorative, matching each hero's
* `aria-hidden` frame. Under `prefers-reduced-motion` the loop never starts:
* the fully-built, selected canvas renders statically.
*/
export function EditorLoop({ content }: EditorLoopProps) {
const [builtCount, setBuiltCount] = useState(0)
const [selected, setSelected] = useState(false)
const [fading, setFading] = useState(false)
const [cycleId, setCycleId] = useState(0)
useMotionSafeCycle(
{
scheduleCycle: () => {
setFading(false)
setBuiltCount(0)
setSelected(false)
setCycleId((c) => c + 1)
const selectAt =
IDLE_HOLD_MS + (content.blocks.length - 1) * BUILD_STEP_MS + SELECT_AFTER_MS
const totalMs = selectAt + SELECTED_HOLD_MS
return {
timers: [
...content.blocks.map((_, i) =>
setTimeout(() => setBuiltCount(i + 1), IDLE_HOLD_MS + i * BUILD_STEP_MS)
),
setTimeout(() => setSelected(true), selectAt),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
],
totalMs,
}
},
showFinished: () => {
setFading(false)
setBuiltCount(content.blocks.length)
setSelected(true)
},
},
[content]
)
return (
<HeroLoopShell
chats={content.sidebarChats}
workflows={content.sidebarWorkflows}
activeNav={content.activeNav}
>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
className={cn(
'h-full w-full transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<HeroWorkflowStage
key={cycleId}
builtCount={builtCount}
blocks={content.blocks}
edges={content.edges}
canvas={content.canvas}
selectedId={selected ? content.selectedBlockId : undefined}
/>
</div>
</div>
</HeroLoopShell>
)
}
@@ -0,0 +1 @@
export { EditorLoop, type EditorLoopContent } from './editor-loop'
@@ -0,0 +1,60 @@
'use client'
import type { ReactNode } from 'react'
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. */
workspaceName?: string
/** Recent-chat entries in the sidebar - four fill the design height. */
chats: readonly string[]
/** Deployed-workflow entries in the sidebar - five fill the design height. */
workflows: readonly string[]
/** Workspace-nav row to highlight; unset keeps New chat active. */
activeNav?: EnterpriseSidebarProps['activeNav']
/** The workspace pane's contents, rendered inside the inset pane gutter. */
children: ReactNode
}
/**
* 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.
*/
export function HeroLoopShell({
workspaceName = 'Brightwave',
chats,
workflows,
activeNav,
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}
chats={chats}
workflows={workflows}
activeNav={activeNav}
/>
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>{children}</div>
</div>
</div>
)
}
@@ -0,0 +1 @@
export { HeroLoopShell } from './hero-loop-shell'
@@ -0,0 +1 @@
export { ZipIcon } from './zip-icon'
@@ -0,0 +1,28 @@
import type { SVGProps } from 'react'
/**
* Generic zip-archive glyph shared by the landing files surfaces (the
* homepage files preview and the files hero loop) - no dedicated zip
* component exists in the icon set. Inherits `currentColor` and sizes via
* className, matching the icon-set convention.
*/
export function ZipIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
{...props}
>
<path d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z' />
<path d='M14 2v4a2 2 0 0 0 2 2h4' />
<path d='M10 6h1' />
<path d='M10 10h1' />
<path d='M10 14h1' />
<path d='M9 18h2v2h-2z' />
</svg>
)
}
@@ -1,9 +1,5 @@
import { cn } from '@sim/emcn'
import { SolutionsPillCta } from '@/app/(landing)/components/solutions-page/components/solutions-card-row/components/solutions-pill-cta'
import {
SOLUTIONS_SPACING,
SOLUTIONS_TEXT_MEASURE,
} from '@/app/(landing)/components/solutions-page/constants'
import { SOLUTIONS_SPACING } from '@/app/(landing)/components/solutions-page/constants'
import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solutions-page/types'
/**
@@ -11,67 +7,27 @@ import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solution
* a single pill CTA, stacked with named spacing constants. Extracted from
* {@link SolutionsCardRow} so layouts that place row headers inside a shared
* grid (the enterprise feature grid) render the exact same header chrome.
*
* Only the row header stack can opt into centered text; the `feature` variant
* is the tighter, smaller treatment used by feature-tile pages.
*/
interface SolutionsCardRowHeaderProps {
row: SolutionsCardRowConfig
/** Stable id wiring the `<h2>` into the page outline. */
headingId: string
/** Header stack alignment. Defaults to the original left-aligned layout. */
align?: 'left' | 'center'
/** Header typography treatment. Defaults to the original larger solutions header. */
variant?: 'standard' | 'feature'
}
export function SolutionsCardRowHeader({
row,
headingId,
align = 'left',
variant = 'standard',
}: SolutionsCardRowHeaderProps) {
const centered = align === 'center'
const featureHeader = variant === 'feature'
export function SolutionsCardRowHeader({ row, headingId }: SolutionsCardRowHeaderProps) {
return (
<div
className={cn(
'flex flex-col',
centered ? 'items-center text-center' : 'items-start text-left',
featureHeader ? 'gap-3' : SOLUTIONS_SPACING.cardRowHeaderStack
)}
>
<div className='flex flex-col items-start gap-3 text-left'>
<h2
id={headingId}
className={cn(
'text-balance text-[var(--text-primary)] leading-[1.3]',
featureHeader
? 'max-w-[540px] font-medium text-[22px] max-sm:text-[20px]'
: 'max-w-[760px] text-[32px] max-sm:text-[24px]'
)}
className='max-w-[540px] text-balance font-medium text-[22px] text-[var(--text-primary)] leading-[1.3] max-sm:text-[20px]'
>
{row.title}
</h2>
<p
className={cn(
featureHeader ? 'w-full min-w-0 max-w-[48ch]' : SOLUTIONS_TEXT_MEASURE.rowSubtitle,
'text-pretty',
featureHeader
? 'text-[15px] text-[var(--text-muted)] leading-[1.6]'
: 'text-[20px] text-[var(--text-body)] leading-[1.5]'
)}
>
<p className='w-full min-w-0 max-w-[48ch] text-pretty text-[15px] text-[var(--text-muted)] leading-[1.6]'>
{row.subtitle}
</p>
<div
className={
featureHeader
? SOLUTIONS_SPACING.cardRowHeaderCtaGapFeature
: SOLUTIONS_SPACING.cardRowHeaderCtaGap
}
>
<div className={SOLUTIONS_SPACING.cardRowHeaderCtaGapFeature}>
<SolutionsPillCta cta={row.cta} />
</div>
</div>
@@ -1,5 +1,4 @@
import { cn } from '@sim/emcn'
import { SolutionsVisualFrame } from '@/app/(landing)/components/solutions-page/components/solutions-visual-frame'
import {
SOLUTIONS_FEATURE_TILE_TONE,
SOLUTIONS_SPACING,
@@ -10,16 +9,20 @@ import type { SolutionsCardConfig } from '@/app/(landing)/components/solutions-p
/**
* A single solutions card - an `<article>` with an `<h3>` title, a body-color
* description, and a reserved visual area. The default split variant keeps copy
* on the page canvas with a framed visual beneath it. The feature-tile variant
* moves copy and the visual slot into one larger bordered surface for pages that
* need a unified callout card.
* description, and a reserved visual area, all inside one larger bordered
* feature-tile surface.
*
* The card owns the gap between its text and visual (`cardTextToVisual`) and the
* title→description stack (`cardTextStack`) - both from named spacing constants.
* The split variant lands the visual in a fixed-height
* {@link SolutionsVisualFrame}; the feature-tile variant reserves a larger
* flexible slot inside its own frame for future UI.
* The card owns the title→description stack (`cardTextStack`) from named
* spacing constants and reserves a larger flexible visual slot inside its own
* frame.
*
* 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.
@@ -29,71 +32,88 @@ interface SolutionsCardProps {
card: SolutionsCardConfig
/** Stable id wiring the `<h3>` into the page outline. */
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) {
const featureTile = variant === 'featureTile'
export function SolutionsCard({ card, headingId, tabletSpan = false }: SolutionsCardProps) {
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 textBlock = (
<div className={cn('flex flex-col', SOLUTIONS_SPACING.cardTextStack)}>
<h3
id={headingId}
className={cn(
'leading-[1.3]',
featureTile
? cn('font-medium text-[16px]', featureTileTone.title)
: 'text-[18px] text-[var(--text-primary)]'
)}
>
{card.title}
</h3>
<p
className={cn(
SOLUTIONS_TEXT_MEASURE.cardDescription,
'text-pretty leading-[1.5]',
featureTile
? cn('text-[14px]', featureTileDescription)
: 'text-[15px] text-[var(--text-body)]'
)}
>
{card.description}
</p>
</div>
)
if (featureTile) {
return (
<article
className={cn(
'flex h-full flex-col overflow-hidden rounded-lg',
featureTileTone.surface,
SOLUTIONS_VISUAL.featureTileMinHeight,
SOLUTIONS_SPACING.cardFeatureTilePadding
)}
>
{textBlock}
<div
aria-hidden='true'
className='-mr-8 -mb-8 max-lg:-mr-6 max-lg:-mb-6 mt-8 min-h-[240px] w-[calc(100%+2rem)] flex-1 max-lg:min-h-[210px] max-lg:w-[calc(100%+1.5rem)]'
>
<div className='h-full w-full'>{card.visual}</div>
</div>
</article>
)
}
const wide = tabletSpan
return (
<article className={cn('flex h-full flex-col', SOLUTIONS_SPACING.cardTextToVisual)}>
<div className='flex flex-1 flex-col'>{textBlock}</div>
<div
className={cn(
'h-full',
SOLUTIONS_VISUAL.featureTileContainer,
wide && 'sm:max-lg:col-span-2'
)}
>
<article
className={cn(
'relative h-full overflow-hidden rounded-lg',
featureTileTone.surface,
SOLUTIONS_VISUAL.featureTileScale,
SOLUTIONS_VISUAL.featureTileMinHeight,
wide && 'sm:max-lg:min-h-[360px]'
)}
>
<div
className={cn(
'absolute top-0 left-0 flex flex-col',
SOLUTIONS_VISUAL.featureTileCanvas,
SOLUTIONS_SPACING.cardFeatureTilePadding,
wide && 'sm:max-lg:flex-row sm:max-lg:gap-10'
)}
>
<div
className={cn(
'flex flex-col',
SOLUTIONS_SPACING.cardTextStack,
wide && 'sm:max-lg:w-[38%] sm:max-lg:shrink-0 sm:max-lg:self-center'
)}
>
<h3
id={headingId}
className={cn('font-medium text-[16px] leading-[1.3]', featureTileTone.title)}
>
{card.title}
</h3>
<p
className={cn(
SOLUTIONS_TEXT_MEASURE.cardDescription,
'text-pretty text-[14px] leading-[1.5]',
featureTileDescription
)}
>
{card.description}
</p>
</div>
<SolutionsVisualFrame size='card'>{card.visual}</SolutionsVisualFrame>
</article>
<div
aria-hidden='true'
className={cn(
'-mr-8 -mb-8 max-lg:-mr-6 max-lg:-mb-6 mt-8 min-h-[240px] w-[calc(100%+2rem)] flex-1 max-lg:w-[calc(100%+1.5rem)]',
wide && 'sm:max-lg:mt-0 sm:max-lg:w-auto sm:max-lg:min-w-0'
)}
>
<div className='h-full w-full'>{card.visual}</div>
</div>
</div>
</article>
</div>
)
}
@@ -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
* `<h2>` 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 `<section>` for a clean, crawlable landmark; each card
* is an `<article>` with an `<h3>`, keeping the strict H2 → H3 hierarchy. Every
@@ -21,28 +29,18 @@ import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solution
interface SolutionsCardRowProps {
row: SolutionsCardRowConfig
/** Header stack alignment. Defaults to the original left-aligned layout. */
align?: 'left' | 'center'
/** Card treatment. Defaults to the original split copy + visual layout. */
cardVariant?: 'split' | 'featureTile'
/** Header typography treatment. Defaults to the original larger solutions header. */
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<number, string> = {
2: 'grid-cols-2',
3: 'grid-cols-3',
4: 'grid-cols-4',
}
export function SolutionsCardRow({
row,
align = 'left',
cardVariant = 'split',
headerVariant = 'standard',
}: SolutionsCardRowProps) {
export function SolutionsCardRow({ row }: 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 (
<section
@@ -50,12 +48,7 @@ export function SolutionsCardRow({
aria-labelledby={headingId}
className={cn('flex flex-col', SOLUTIONS_SPACING.cardRowHeaderToGrid)}
>
<SolutionsCardRowHeader
row={row}
headingId={headingId}
align={align}
variant={headerVariant}
/>
<SolutionsCardRowHeader row={row} headingId={headingId} />
<div
className={cn(
@@ -70,7 +63,7 @@ export function SolutionsCardRow({
key={`${row.id}-${card.title}`}
card={card}
headingId={`solutions-row-${row.id}-card-${index}-heading`}
variant={cardVariant}
tabletSpan={row.cards.length === 3 && row.columns === undefined && index === 2}
/>
))}
</div>
@@ -121,7 +121,7 @@ export function SolutionsHero({ hero, align = 'left', variant = 'solutions' }: S
</div>
</div>
<SolutionsVisualFrame size='hero'>{hero.visual}</SolutionsVisualFrame>
<SolutionsVisualFrame>{hero.visual}</SolutionsVisualFrame>
</section>
)
}
@@ -6,8 +6,8 @@ import { SOLUTIONS_VISUAL } from '@/app/(landing)/components/solutions-page/cons
* The one escape hatch in the solutions layout - a fixed-dimension frame that
* holds a page-supplied visual `ReactNode`. The frame owns its chrome (the
* hero-visual family: `--surface-2` fill, `--border-1` hairline, `rounded-lg`,
* `overflow-hidden`) and, crucially, its dimensions: a `hero` frame reserves a
* 16:9 aspect ratio, a `card` frame a fixed height. Because the size is reserved
* `overflow-hidden`) and, crucially, its dimensions: it reserves a full-width
* 16:9 aspect ratio for the solutions hero visual. Because the size is reserved
* before paint and the node fills `h-full w-full` inside, a dropped-in node can
* neither shift surrounding layout (CLS = 0) nor change the frame's own padding.
*
@@ -16,23 +16,17 @@ import { SOLUTIONS_VISUAL } from '@/app/(landing)/components/solutions-page/cons
*/
interface SolutionsVisualFrameProps {
/**
* Reserved-dimension preset.
* - `hero` - full-width 16:9 frame for the solutions hero visual.
* - `card` - fixed-height frame for a card's visual panel.
*/
size: 'hero' | 'card'
/** The page-supplied visual island or static panel. Fills the frame; owns no chrome. */
children: ReactNode
}
export function SolutionsVisualFrame({ size, children }: SolutionsVisualFrameProps) {
export function SolutionsVisualFrame({ children }: SolutionsVisualFrameProps) {
return (
<div
aria-hidden='true'
className={cn(
'w-full overflow-hidden rounded-lg border border-[var(--border-1)] bg-[var(--surface-2)]',
size === 'hero' ? SOLUTIONS_VISUAL.heroAspect : SOLUTIONS_VISUAL.cardHeight
SOLUTIONS_VISUAL.heroAspect
)}
>
<div className='h-full w-full'>{children}</div>
@@ -42,20 +42,15 @@ export const SOLUTIONS_SPACING = {
heroToVisual: 'gap-12',
/** Gap between a card row's header block and the card grid beneath it. */
cardRowHeaderToGrid: 'gap-12',
/** Vertical stack gap inside a card row's header (title → subtitle → CTA). */
cardRowHeaderStack: 'gap-5',
/**
* Extra top separation for the header CTA over the stack gap. Title and
* subtitle are one copy group and stay tight; the CTA is a separate action
* group, so its subtitle→CTA gap lands at 2× the title→subtitle gap
* (standard: 20px + 20px = 40px; feature: 12px + 12px = 24px).
* Extra top separation for the header CTA over the header's gap-3 stack.
* Title and subtitle are one copy group and stay tight; the CTA is a
* separate action group, so its subtitle→CTA gap lands at 2× the
* title→subtitle gap (12px + 12px = 24px).
*/
cardRowHeaderCtaGap: 'mt-5',
cardRowHeaderCtaGapFeature: 'mt-3',
/** Gap between cards within a card-row grid (both axes). */
cardGridGap: 'gap-8',
/** Minimum gap between a card's text block and its visual panel. */
cardTextToVisual: 'gap-5',
/** Vertical stack gap inside a card's text block (title → description). */
cardTextStack: 'gap-2',
/** Inner padding for feature tiles where copy and the visual slot share one frame. */
@@ -71,8 +66,6 @@ export const SOLUTIONS_SPACING = {
export const SOLUTIONS_TEXT_MEASURE = {
/** Hero support copy: broad enough for the primary value prop, still under long-form prose width. */
heroDescription: 'w-full min-w-0 max-w-[58ch]',
/** Section subtitles: slightly tighter than hero copy so centered headers feel intentional. */
rowSubtitle: 'w-full min-w-0 max-w-[52ch]',
/** Card descriptions: short scan lines inside three-up card grids. */
cardDescription: 'min-w-0 max-w-[38ch]',
} as const
@@ -86,16 +79,40 @@ export const SOLUTIONS_TEXT_MEASURE = {
export const SOLUTIONS_VISUAL = {
/** Full-width hero visual aspect ratio - reserves height before paint. */
heroAspect: 'aspect-[16/9]',
/** 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
/**
@@ -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,
@@ -14,25 +16,26 @@ import type { SolutionsPageConfig } from '@/app/(landing)/components/solutions-p
* near-zero ceremony. A route renders the shared shell and drops in one
* `<SolutionsPage config={…} />`.
*
* Structurally this mirrors `PlatformPage` today; the two are deliberately
* separate so the solutions layout and its components can diverge from the
* platform layout without coupling.
*
* This component owns the entire `<main>`: 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 `<main>` 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 `<main>`: 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 `<main>` because it owns its own
* width cap and gutter; the `<main>`'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
* `<h1>`) → 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.
* `<h1>`) → 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 {
@@ -46,17 +49,24 @@ export function SolutionsPage({ config }: SolutionsPageProps) {
<SolutionsStructuredData config={config} />
<main
id='main-content'
className={cn(
'mx-auto flex w-full max-w-[1460px] flex-col',
SOLUTIONS_SPACING.sectionRhythm,
SOLUTIONS_SPACING.gutter
)}
className={cn('flex w-full flex-col', SOLUTIONS_SPACING.sectionRhythm)}
>
<SolutionsHero hero={config.hero} />
<SolutionsLogosRow />
{config.rows.map((row) => (
<SolutionsCardRow key={row.id} row={row} />
))}
<div
className={cn(
'flex flex-col',
LANDING_CONTENT_WIDTH,
SOLUTIONS_SPACING.sectionRhythm,
SOLUTIONS_SPACING.gutter
)}
>
<SolutionsHero hero={config.hero} />
<SolutionsLogosRow />
{config.rows.map((row) => (
<SolutionsCardRow key={row.id} row={row} />
))}
</div>
<Cta />
</main>
</>
)
@@ -59,8 +59,7 @@ export interface SolutionsCardConfig {
*/
visual: ReactNode
/**
* Feature-tile surface tone - only read when the row renders
* `cardVariant='featureTile'`. Defaults to `'light'` so tiles can mix light
* Feature-tile surface tone. Defaults to `'light'` so tiles can mix light
* and dark backgrounds within the same row.
*/
featureTileTone?: SolutionsFeatureTileTone
@@ -77,7 +76,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 +93,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
}
/**
@@ -99,7 +99,7 @@ export function EnterpriseFeatureGrid({ rows }: EnterpriseFeatureGridProps) {
TABLET_HEADER_ORDER[rowIndex]
)}
>
<SolutionsCardRowHeader row={row} headingId={headingId} variant='feature' />
<SolutionsCardRowHeader row={row} headingId={headingId} />
</div>
{row.cards.map((card, cardIndex) => (
<div
@@ -109,7 +109,6 @@ export function EnterpriseFeatureGrid({ rows }: EnterpriseFeatureGridProps) {
<SolutionsCard
card={card}
headingId={`solutions-row-${row.id}-card-${cardIndex}-heading`}
variant='featureTile'
/>
</div>
))}
@@ -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'
)}
>
<p className='mb-7 text-[30px] text-[var(--text-primary)]'>{ENTERPRISE_GREETING}</p>
<p className='mb-7 text-[30px] text-[var(--text-primary)]'>{greeting}</p>
<div className='w-full max-w-[576px]'>
<Composer active={hasText}>
@@ -192,7 +209,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps)
{isTyping && <Caret />}
</>
) : (
<span className='font-[380] text-[var(--text-muted)]'>{COMPOSER_PLACEHOLDER}</span>
<span className='font-[380] text-[var(--text-muted)]'>{placeholder}</span>
)}
</Composer>
@@ -208,7 +225,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps)
</span>
</div>
<div className='mt-2 flex flex-col'>
{SUGGESTED_ACTIONS.map((action, i) => {
{suggestedActions.map((action, i) => {
const Icon = ACTION_ICONS[i]
return (
<span
@@ -242,7 +259,7 @@ export function EnterpriseHomeStage({ phase, fading }: EnterpriseHomeStageProps)
>
<div className='mx-auto flex min-h-0 w-full max-w-[640px] flex-1 flex-col gap-6 overflow-hidden px-6 pt-6'>
<div className='max-w-[82%] self-end rounded-2xl bg-[var(--surface-3)] px-4 py-3 text-[15px] text-[var(--text-primary)] leading-[1.5]'>
{ENTERPRISE_PROMPT}
{prompt}
</div>
{phase === 'dispatch' && (
<ThinkingLoader size={26} phase labelRatio={0.58} className='mt-1' />
@@ -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(' ')}
</p>
</div>
<div className='mx-auto mb-5 w-[calc(100%-40px)] max-w-[600px] shrink-0'>
@@ -1,27 +1,29 @@
'use client'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
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 { 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'
import { RESET_FADE_MS } from '@/app/(landing)/hooks/use-design-scale'
import { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
/**
* The window interior's design space, matching the homepage loop's capture
* geometry (the 2560x1470 shot is a 1280x735 CSS layout shown in the 1080x620
* window, so the app's native type reads at the same ~84.4% "mini app" scale):
* the sidebar column is 249px, and the workspace container is inset 7-8px.
*/
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
@@ -37,133 +39,100 @@ 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() {
const regionRef = useRef<HTMLDivElement>(null)
export function EnterprisePlatformLoop({
content = ENTERPRISE_LOOP_CONTENT,
}: EnterprisePlatformLoopProps = {}) {
const [phase, setPhase] = useState<EnterpriseLoopPhase>('idle')
const [stageOpen, setStageOpen] = useState(false)
const [builtCount, setBuiltCount] = useState(0)
const [fading, setFading] = useState(false)
const [cycleId, setCycleId] = useState(0)
const [scale, setScale] = useState(1)
// 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(() => {
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()
}, [])
const timeline = useMemo(() => buildLoopTimeline(content), [content])
const blockCount = content.stageBlocks.length
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
let timers: ReturnType<typeof setTimeout>[] = []
const clearScheduled = () => {
timers.forEach(clearTimeout)
timers = []
}
const showFinished = () => {
clearScheduled()
setFading(false)
setPhase('reply')
setStageOpen(true)
setBuiltCount(ENTERPRISE_STAGE_BLOCKS.length)
}
const runCycle = () => {
setFading(false)
setPhase('idle')
setStageOpen(false)
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('reply'), LOOP_TIMELINE.reply),
setTimeout(() => setFading(true), LOOP_TIMELINE.total - RESET_FADE_MS),
setTimeout(runCycle, LOOP_TIMELINE.total),
]
}
const syncMotionPreference = () => {
clearScheduled()
if (media.matches) {
showFinished()
return
}
runCycle()
}
syncMotionPreference()
media.addEventListener('change', syncMotionPreference)
return () => {
media.removeEventListener('change', syncMotionPreference)
clearScheduled()
}
}, [])
useMotionSafeCycle(
{
scheduleCycle: () => {
setFading(false)
setPhase('idle')
setStageOpen(false)
setBuiltCount(0)
setCycleId((c) => c + 1)
return {
timers: [
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'), timeline.reply),
setTimeout(() => setFading(true), timeline.total - RESET_FADE_MS),
],
totalMs: timeline.total,
}
},
showFinished: () => {
setFading(false)
setPhase('reply')
setStageOpen(true)
setBuiltCount(blockCount)
},
},
[timeline, blockCount]
)
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 />
<div className='h-full min-w-0 flex-1 py-[7px] pr-[8px]'>
<div className='flex h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div className='relative h-full min-w-0 flex-1'>
<EnterpriseHomeStage phase={phase} fading={fading} />
</div>
<div
className={cn(
'h-full shrink-0 overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]',
stageOpen ? 'w-1/2 border-l' : 'w-0 min-w-0 border-l-0'
)}
>
<div
className={cn(
'h-full w-full transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<HeroWorkflowStage
key={cycleId}
builtCount={builtCount}
blocks={ENTERPRISE_STAGE_BLOCKS}
edges={ENTERPRISE_STAGE_EDGES}
canvas={ENTERPRISE_STAGE_CANVAS}
/>
</div>
</div>
<HeroLoopShell
workspaceName={content.workspaceName}
chats={content.sidebarChats}
workflows={content.sidebarWorkflows}
>
<div className='flex h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div className='relative h-full min-w-0 flex-1'>
<EnterpriseHomeStage
phase={phase}
fading={fading}
greeting={content.greeting}
placeholder={content.placeholder}
prompt={content.prompt}
reply={content.reply}
suggestedActions={content.suggestedActions}
/>
</div>
<div
className={cn(
'h-full shrink-0 overflow-hidden border-[var(--border)] bg-[var(--bg)] transition-[width,min-width,border-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]',
stageOpen ? 'w-1/2 border-l' : 'w-0 min-w-0 border-l-0'
)}
>
<div
className={cn(
'h-full w-full transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<HeroWorkflowStage
key={cycleId}
builtCount={builtCount}
blocks={content.stageBlocks}
edges={content.stageEdges}
canvas={content.stageCanvas}
/>
</div>
</div>
</div>
</div>
</HeroLoopShell>
)
}
@@ -1,3 +1,4 @@
import { memo } from 'react'
import { ChevronDown, cn, Home, Library } from '@sim/emcn'
import {
Calendar,
@@ -71,6 +72,21 @@ function SectionLabel({ label, actions }: { label: string; actions?: boolean })
)
}
export 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[]
/**
* Workspace-nav row to render active (e.g. `'Tables'`) - the platform pages
* highlight their own module instead of New chat. Unset keeps the enterprise
* default (New chat active).
*/
activeNav?: (typeof WORKSPACE_NAV)[number]['label']
}
/**
* 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 +94,18 @@ 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. Memoized - the sidebar is
* fully static per props, and every consuming loop re-renders on each clock
* tick with stable sidebar props.
*/
export function EnterpriseSidebar() {
export const EnterpriseSidebar = memo(function EnterpriseSidebar({
workspaceName = 'Brightwave',
chats = SIDEBAR_CHATS,
workflows = SIDEBAR_WORKFLOWS,
activeNav,
}: EnterpriseSidebarProps = {}) {
return (
<div className='flex h-full w-[249px] flex-shrink-0 flex-col bg-[var(--surface-1)] pt-3'>
{/* Workspace header, matching the real product's WorkspaceHeader chip
@@ -99,14 +125,14 @@ export function EnterpriseSidebar() {
height={16}
className='size-[16px] flex-shrink-0 rounded-sm'
/>
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>Brightwave</span>
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>{workspaceName}</span>
<ChevronDown className='h-[6px] w-[10px] flex-shrink-0 text-[var(--text-icon)]' />
</div>
<PanelLeft className='mr-1.5 size-[16px] flex-shrink-0 text-[var(--text-icon)]' />
</div>
<div className='mt-2.5 flex flex-shrink-0 flex-col gap-0.5 px-2'>
<IconRow icon={Home} label='New chat' active />
<IconRow icon={Home} label='New chat' active={!activeNav} />
<IconRow icon={Search} label='Search' />
<IconRow icon={Integration} label='Integrations' />
</div>
@@ -114,7 +140,7 @@ export function EnterpriseSidebar() {
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<SectionLabel label='Chats' />
<div className='flex flex-col gap-0.5 px-2'>
{SIDEBAR_CHATS.map((chat) => (
{chats.map((chat) => (
<TextRow key={chat} label={chat} />
))}
</div>
@@ -124,7 +150,12 @@ export function EnterpriseSidebar() {
<SectionLabel label='Workspace' />
<div className='flex flex-col gap-0.5 px-2'>
{WORKSPACE_NAV.map((item) => (
<IconRow key={item.label} icon={item.icon} label={item.label} />
<IconRow
key={item.label}
icon={item.icon}
label={item.label}
active={item.label === activeNav}
/>
))}
</div>
</div>
@@ -132,7 +163,7 @@ export function EnterpriseSidebar() {
<div className='flex min-h-0 flex-1 flex-col overflow-hidden pt-3.5'>
<SectionLabel label='Workflows' actions />
<div className='flex flex-col gap-0.5 px-2'>
{SIDEBAR_WORKFLOWS.map((workflow) => (
{workflows.map((workflow) => (
<TextRow key={workflow} label={workflow} />
))}
</div>
@@ -144,4 +175,4 @@ export function EnterpriseSidebar() {
</div>
</div>
)
}
})
@@ -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<readonly [string, string]>
/** 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". */
@@ -160,21 +207,32 @@ export const BUILD_STEP_MS = 620
const REPLY_AFTER_MS = 500
/** The finished scene (reply + built canvas) holds this long. */
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 }
}
@@ -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'
>
<div className='relative h-[250px] w-[320px] shrink-0 max-sm:scale-100 max-md:scale-[0.8] lg:max-[1180px]:scale-[0.8]'>
<div className='relative h-[250px] w-[320px] shrink-0'>
<svg
className='absolute inset-0'
fill='none'
@@ -3,7 +3,7 @@ import Image from 'next/image'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/audit-trail-graphic.module.css'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
interface AuditEntry {
export interface AuditEntry {
/** Human-readable action label, matching how the audit UI renders `AuditAction` codes. */
action: string
/** Attributed actor shown on the detail line. */
@@ -25,7 +25,7 @@ interface AuditEntry {
* tuned, a Zendesk credential access is recorded, and the trail reaches
* back to the workflow's creation.
*/
const ENTRIES: readonly AuditEntry[] = [
const ENTRIES: readonly [AuditEntry, AuditEntry, AuditEntry, AuditEntry] = [
{
action: 'Workflow deployed',
actor: 'Maya Chen',
@@ -95,15 +95,30 @@ const ROW_TONES = [
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center instead of the bled slot's
* center.
*
* The ledger records are parametrizable so other landing pages (finance,
* workflows) can retell the append-only trail with their own domain's
* events; the defaults keep the enterprise page's Support-agent ledger
* byte-identical. Exactly four entries keep the fixed tone ramp and the
* mask-gradient dissolve intact.
*
* On the wide spanned tile of the two-column band (container ≥500px inside
* `sm`..`lg`) the ledger column relaxes to 400px, giving the entries a
* roomier measure that matches the wide side-by-side tile.
*/
export function AuditTrailGraphic() {
interface AuditTrailGraphicProps {
/** Newest-first ledger records; exactly four to match the tone ramp. */
entries?: readonly [AuditEntry, AuditEntry, AuditEntry, AuditEntry]
}
export function AuditTrailGraphic({ entries = ENTRIES }: AuditTrailGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px]'>
<div className='w-full max-w-[312px] sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div className='mb-4 flex items-center justify-between'>
<span className='font-medium text-[var(--text-primary)] text-base'>Audit log</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
@@ -112,7 +127,7 @@ export function AuditTrailGraphic() {
</div>
<div className='flex flex-col gap-1.5 [mask-image:linear-gradient(to_bottom,black_55%,transparent_100%)]'>
{ENTRIES.map((entry, index) => {
{entries.map((entry, index) => {
const tone = ROW_TONES[index]
const newest = index === 0
@@ -59,8 +59,36 @@ const DEPLOY_LOADER_INK = {
* `bottom-5`), keeping the tile row horizontally aligned; both connector
* lines are `flex-1` with mirrored margins, so the Deploy button stays
* equidistant between the agent pill and the browser at any tile height.
*
* Every label is parametrizable so other landing pages (engineering,
* IT, finance, workflows) can retell the click-to-live moment with their
* own agent and outcome; the defaults keep the enterprise page's
* Support-agent deploy byte-identical. Geometry, motion, and inks never
* change with the copy.
*/
export function DeployGraphic() {
interface DeployGraphicProps {
/** Agent pill label. */
agentName?: string
/** Version tag beside the agent name. */
versionTag?: string
/** Action button label. */
buttonLabel?: string
/** Address-bar URL of the deployed agent. */
url?: string
/** Status line inside the dark browser window. */
statusLabel?: string
/** Right-aligned status timestamp. */
timeLabel?: string
}
export function DeployGraphic({
agentName = 'Support agent',
versionTag = 'v3',
buttonLabel = 'Deploy',
url = 'sim.ai/agents/support',
statusLabel = 'Live in production',
timeLabel = 'Just now',
}: DeployGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
@@ -68,10 +96,8 @@ export function DeployGraphic() {
className='absolute inset-0 flex flex-col items-center pr-8 max-lg:pr-6'
>
<div className='mt-1 flex items-center gap-1.5 rounded-[12px] bg-[var(--surface-2)] py-1.5 pr-1.5 pl-2.5 shadow-sm'>
<span className='font-medium text-[var(--text-secondary)] text-caption'>
Support agent
</span>
<ChipTag variant='mono'>v3</ChipTag>
<span className='font-medium text-[var(--text-secondary)] text-caption'>{agentName}</span>
<ChipTag variant='mono'>{versionTag}</ChipTag>
</div>
<span className='relative mt-1.5 min-h-3 w-px flex-1 overflow-hidden bg-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'>
@@ -85,7 +111,9 @@ export function DeployGraphic() {
)}
>
<ThinkingLoader variant='relay' size={18} style={DEPLOY_LOADER_INK} />
<span className={cn(chipContentLabelClass, 'text-[15px] text-current')}>Deploy</span>
<span className={cn(chipContentLabelClass, 'text-[15px] text-current')}>
{buttonLabel}
</span>
</span>
<span className='relative mt-2.5 mb-1.5 min-h-3 w-px flex-1 overflow-hidden bg-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'>
@@ -125,17 +153,17 @@ export function DeployGraphic() {
</span>
<span className='flex min-w-0 flex-1 items-center gap-1.5 rounded-md bg-[var(--text-muted)] px-2 py-1'>
<Lock className='size-[10px] shrink-0 text-[var(--text-muted-inverse)]' />
<span className='truncate text-[var(--text-inverse)] text-caption'>
sim.ai/agents/support
</span>
<span className='truncate text-[var(--text-inverse)] text-caption'>{url}</span>
</span>
</div>
<div className='flex items-center gap-2 px-3 pt-2.5 pb-4'>
<CircleCheck className='size-[14px] shrink-0 text-[var(--text-muted-inverse)]' />
<span className='min-w-0 flex-1 font-medium text-[var(--text-inverse)] text-small'>
Live in production
{statusLabel}
</span>
<span className='shrink-0 text-[var(--text-muted-inverse)] text-caption'>
{timeLabel}
</span>
<span className='shrink-0 text-[var(--text-muted-inverse)] text-caption'>Just now</span>
</div>
</div>
</div>
@@ -4,10 +4,20 @@ interface FeatureGraphicShellProps {
children: ReactNode
}
/** Shared crop canvas for platform-faithful enterprise feature previews. */
/**
* Shared crop canvas for platform-faithful enterprise feature previews.
*
* The `420px` cap keeps graphics at their designed measure on wide fluid
* tiles (single-column phones, 2-up desktop rows). When the tile is the
* full-width spanned card of a 3-card row in the two-column band (see
* `SolutionsCard`'s `tabletSpan` - the only case where a tile's query
* container reaches 500px inside `sm`..`lg`), the cap lifts so window-chrome
* graphics keep bleeding off the tile's right edge and centered vignettes
* center on the true wide slot.
*/
export function FeatureGraphicShell({ children }: FeatureGraphicShellProps) {
return (
<div className='relative mx-auto h-full min-h-[260px] w-full max-w-[420px] overflow-hidden'>
<div className='relative mx-auto h-full min-h-[260px] w-full max-w-[420px] overflow-hidden sm:max-lg:[@container(min-width:500px)]:max-w-none'>
<div className='relative h-full min-h-[260px]'>{children}</div>
</div>
)
@@ -4,7 +4,7 @@ import Image from 'next/image'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/it-platform-teams-graphic.module.css'
interface PolicyControl {
export interface PolicyControl {
/** Enforced control name, one per pillar of the tile's claim. */
label: string
/** Right-aligned enforcement detail. */
@@ -16,7 +16,7 @@ interface PolicyControl {
* access controls (SSO), governance (reviewer approval), and audit
* trails (retention).
*/
const CONTROLS: readonly PolicyControl[] = [
const CONTROLS: readonly [PolicyControl, PolicyControl, PolicyControl] = [
{ label: 'Single sign-on', detail: 'Enforced' },
{ label: 'Reviewer approval', detail: 'Required' },
{ label: 'Audit history', detail: '90 days' },
@@ -51,8 +51,37 @@ const CONTROLS: readonly PolicyControl[] = [
* slot's center. The column is fluid (`w-full max-w-[312px]`) so it
* never exceeds the compensated slot at narrow tile widths — the policy
* name truncates instead of clipping.
*
* Every label is parametrizable so other landing pages (IT, HR,
* compliance) can retell the checklist-under-a-highlight composition for
* their own domain — an incident runbook, an onboarding plan, an
* evidence schedule; the defaults keep the enterprise page's workspace
* policy vignette byte-identical. Chrome, motion, and layout never
* change with the copy.
*/
export function ItPlatformTeamsGraphic() {
interface ItPlatformTeamsGraphicProps {
/** Header title. */
title?: string
/** Mono badge on the header's right side, beside the avatar. */
badgeLabel?: string
/** Title line of the highlighted white card. */
cardTitle?: string
/** Scope line beneath the card title. */
cardSubtitle?: string
/** Grey tag carrying the card's pulsing state. */
cardTag?: string
/** The three checked rows beneath the card. */
controls?: readonly [PolicyControl, PolicyControl, PolicyControl]
}
export function ItPlatformTeamsGraphic({
title = 'Workspace',
badgeLabel = 'Managed by IT',
cardTitle = 'Production policy',
cardSubtitle = 'Applies to every workspace',
cardTag = 'Active',
controls = CONTROLS,
}: ItPlatformTeamsGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
@@ -62,7 +91,7 @@ export function ItPlatformTeamsGraphic() {
<div className='w-full max-w-[312px]'>
<div className='mb-3 flex items-center justify-between gap-2'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-base'>
Workspace
{title}
</span>
<span className='flex shrink-0 items-center gap-1.5'>
<span className='relative size-4 overflow-hidden rounded-full shadow-sm'>
@@ -75,7 +104,7 @@ export function ItPlatformTeamsGraphic() {
/>
</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Managed by IT
{badgeLabel}
</ChipTag>
</span>
</div>
@@ -83,19 +112,19 @@ export function ItPlatformTeamsGraphic() {
<div className='flex items-center gap-3 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm'>
<span className='min-w-0 flex-1'>
<span className='block truncate font-medium text-[var(--text-primary)] text-small'>
Production policy
{cardTitle}
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
Applies to every workspace
{cardSubtitle}
</span>
</span>
<ChipTag variant='gray' className={cn('shrink-0 shadow-none', styles.activePulse)}>
Active
{cardTag}
</ChipTag>
</div>
<div className='mt-1.5 px-3'>
{CONTROLS.map((control, index) => (
{controls.map((control, index) => (
<div
key={control.label}
className={cn(
@@ -39,6 +39,9 @@ interface Port {
fillClass: string
}
/** Left-to-right label triple for one row of ports. */
type PortLabels = readonly [string, string, string]
/**
* Inbound ops tools across the top, staggered like a constellation rather
* than a rank: Zendesk rides highest on the center axis (x 140), Gmail
@@ -199,17 +202,41 @@ function PortTag({ port }: { port: Port }) {
* center. The fixed-size canvas is centered with a transform (`left-1/2`
* + `-translate-x-1/2`, the standards tile's approach) so at narrow tile
* widths it keeps its wiring geometry and overflows both edges equally
* instead of drifting. 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 canvas scales down via transform so the outer tool pills
* are never cropped.
* instead of drifting. Narrow grid columns are handled by the feature
* tile itself, which zooms its whole design-space canvas down
* proportionally (see `SOLUTIONS_VISUAL`), so the outer tool pills are
* never cropped without this graphic carrying its own breakpoint scaling.
*
* The source/destination tool names are parametrizable so other landing
* pages (IT, HR, and other solutions) can retell the switchboard with
* their own domain's tools; the defaults keep the enterprise page's
* Gmail/Zendesk/Sheets → Slack/Salesforce/Jira routing byte-identical.
* Labels map onto the fixed port geometry left-to-right, so wiring,
* timing, and layout never change with the copy.
*/
export function OperationsTeamsGraphic() {
interface OperationsTeamsGraphicProps {
/** Inbound tool names across the top row, left to right. */
sourceLabels?: PortLabels
/** Outbound tool names across the bottom row, left to right. */
destinationLabels?: PortLabels
}
export function OperationsTeamsGraphic({
sourceLabels,
destinationLabels,
}: OperationsTeamsGraphicProps = {}) {
const sources = sourceLabels
? SOURCES.map((port, index) => ({ ...port, label: sourceLabels[index] }))
: SOURCES
const destinations = destinationLabels
? DESTINATIONS.map((port, index) => ({ ...port, label: destinationLabels[index] }))
: DESTINATIONS
return (
<FeatureGraphicShell>
<div aria-hidden='true' className='absolute inset-0 pr-8 pb-8 max-lg:pr-6 max-lg:pb-6'>
<div className='relative h-full'>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 h-[248px] w-[280px] max-sm:scale-100 max-md:scale-[0.85] lg:max-[1180px]:scale-[0.85]'>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 h-[248px] w-[280px]'>
<svg
className='absolute inset-0'
fill='none'
@@ -256,7 +283,7 @@ export function OperationsTeamsGraphic() {
/>
</svg>
{SOURCES.map((port) => (
{sources.map((port) => (
<span key={port.label}>
<span
className={cn(
@@ -269,7 +296,7 @@ export function OperationsTeamsGraphic() {
</span>
))}
{DESTINATIONS.map((port) => (
{destinations.map((port) => (
<span key={port.label}>
<span
className={cn(
@@ -1,13 +1,25 @@
import type { ReactNode } from 'react'
import { ChipTag, cn } from '@sim/emcn'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/run-monitoring-graphic.module.css'
interface LogField {
export interface LogField {
/** Row label, matching the workspace Log Details panel's key column. */
label: string
/** Right-aligned value cell — plain text or a quiet mono chip. */
value: ReactNode
/** Right-aligned value cell text. */
value: string
/**
* Value treatment: `strong` for the primary-name row, `chip` for the
* quiet grey mono chip, `mono` for a bare mono value.
*/
variant: 'strong' | 'chip' | 'mono'
}
/** One key/value pair of the closing JSON output card. */
export interface OutputPair {
/** JSON key, rendered inside the muted scaffolding. */
key: string
/** Raw value literal, rendered in the stronger ink (include quotes for strings). */
value: string
}
/**
@@ -17,36 +29,39 @@ interface LogField {
* row's monochrome vocabulary.
*/
const LOG_FIELDS: readonly LogField[] = [
{
label: 'Workflow',
value: (
<span className='truncate font-medium text-[var(--text-primary)] text-caption'>
Support agent
</span>
),
},
{
label: 'Run ID',
value: (
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
afda69e9
</ChipTag>
),
},
{
label: 'Trigger',
value: (
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Schedule
</ChipTag>
),
},
{
label: 'Duration',
value: <span className='font-mono text-[var(--text-secondary)] text-caption'>1.56s</span>,
},
{ label: 'Workflow', value: 'Support agent', variant: 'strong' },
{ label: 'Run ID', value: 'afda69e9', variant: 'chip' },
{ label: 'Trigger', value: 'Schedule', variant: 'chip' },
{ label: 'Duration', value: '1.56s', variant: 'mono' },
] as const
/** The default two-line JSON payload of the enterprise run. */
const OUTPUT_PAIRS: readonly [OutputPair, OutputPair] = [
{ key: 'status', value: '"completed"' },
{ key: 'resolved', value: '24' },
] as const
/** Renders one field's right-aligned value cell in its declared treatment. */
function fieldValue(field: LogField) {
if (field.variant === 'chip') {
return (
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
{field.value}
</ChipTag>
)
}
if (field.variant === 'mono') {
return (
<span className='font-mono text-[var(--text-secondary)] text-caption'>{field.value}</span>
)
}
return (
<span className='truncate font-medium text-[var(--text-primary)] text-caption'>
{field.value}
</span>
)
}
/**
* The workspace's Log Details panel told as a frameless vignette (the
* access and standards tiles' composition): no window chrome — a small
@@ -71,29 +86,57 @@ const LOG_FIELDS: readonly LogField[] = [
* padding to land on the tile's visible center instead of the bled
* slot's center. The column is fluid (`w-full max-w-[312px]`) so it
* never exceeds the compensated slot at narrow tile widths — the
* workflow value and JSON lines truncate instead of clipping.
* workflow value and JSON lines truncate instead of clipping. On the
* wide spanned tile of the two-column band (container ≥500px inside
* `sm`..`lg`) the column relaxes to 400px so the key-value ledger's
* rows take the wide slot's airier measure.
*
* Every label is parametrizable so other landing pages (IT, HR,
* finance, workflows) can retell the live-run panel for their own
* domain's monitors; the defaults keep the enterprise page's
* Support-agent run byte-identical. Chrome, motion, and layout never
* change with the copy.
*/
export function RunMonitoringGraphic() {
interface RunMonitoringGraphicProps {
/** Panel title. */
title?: string
/** Live-status label beside the pulsing dot. */
statusLabel?: string
/** The key-value ledger rows. */
fields?: readonly LogField[]
/** Label above the JSON output card. */
outputLabel?: string
/** The two key/value pairs of the JSON output card. */
outputPairs?: readonly [OutputPair, OutputPair]
}
export function RunMonitoringGraphic({
title = 'Log details',
statusLabel = 'Live',
fields = LOG_FIELDS,
outputLabel = 'Workflow output',
outputPairs = OUTPUT_PAIRS,
}: RunMonitoringGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px]'>
<div className='w-full max-w-[312px] sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div className='mb-1.5 flex items-center justify-between gap-2'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-base'>
Log details
{title}
</span>
<span className='flex shrink-0 items-center gap-1.5'>
<span
className={cn('size-2 rounded-full bg-[var(--text-primary)]', styles.livePulse)}
/>
<span className='text-[var(--text-muted)] text-caption'>Live</span>
<span className='text-[var(--text-muted)] text-caption'>{statusLabel}</span>
</span>
</div>
{LOG_FIELDS.map((field, index) => (
{fields.map((field, index) => (
<div
key={field.label}
className={cn(
@@ -102,21 +145,21 @@ export function RunMonitoringGraphic() {
)}
>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>{field.label}</span>
{field.value}
{fieldValue(field)}
</div>
))}
<div className='mt-2'>
<span className='block text-[var(--text-muted)] text-caption'>Workflow output</span>
<span className='block text-[var(--text-muted)] text-caption'>{outputLabel}</span>
<div className='mt-1.5 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2 font-mono text-caption leading-[1.6] shadow-sm'>
<div className='truncate whitespace-pre'>
<span className='text-[var(--text-muted)]'>{'{ "status": '}</span>
<span className='text-[var(--text-secondary)]'>"completed"</span>
<span className='text-[var(--text-muted)]'>{`{ "${outputPairs[0].key}": `}</span>
<span className='text-[var(--text-secondary)]'>{outputPairs[0].value}</span>
<span className='text-[var(--text-muted)]'>,</span>
</div>
<div className='truncate whitespace-pre'>
<span className='text-[var(--text-muted)]'>{' "resolved": '}</span>
<span className='text-[var(--text-secondary)]'>24</span>
<span className='text-[var(--text-muted)]'>{` "${outputPairs[1].key}": `}</span>
<span className='text-[var(--text-secondary)]'>{outputPairs[1].value}</span>
<span className='text-[var(--text-muted)]'>{' }'}</span>
</div>
</div>
@@ -5,7 +5,11 @@ import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/featu
import styles from '@/app/(landing)/enterprise/components/feature-graphics/staging-graphic.module.css'
/** Named pre-promotion checks, each rendered as a passing row. */
const CHECKS = ['Evals passed', 'No breaking changes', 'Approved by Ops'] as const
const CHECKS: readonly [string, string, string] = [
'Evals passed',
'No breaking changes',
'Approved by Ops',
] as const
/**
* A GitHub-style promotion surface compressed to tile scale, on the
@@ -47,8 +51,45 @@ const CHECKS = ['Evals passed', 'No breaking changes', 'Approved by Ops'] as con
* tile widths — text rows truncate instead of clipping, and the
* `Staging → Live` tags yield below `xl` so the promotion bar keeps the
* Promote button inside the window.
*
* Every label is parametrizable so other landing pages (engineering,
* finance, HR, workflows) can retell the gated-promotion moment for
* their own domain — a PR merge, an invoice approval, a PTO request;
* the defaults keep the enterprise page's Support-agent promotion
* byte-identical. Chrome, motion, and layout never change with the copy.
*/
export function StagingGraphic() {
interface StagingGraphicProps {
/** Window title. */
title?: string
/** Mono tag beside the title. */
headerTag?: string
/** Mono chip leading the highlighted change card. */
changeTag?: string
/** Change summary beside the chip. */
changeTitle?: string
/** "Actor · time" attribution line under the change. */
attribution?: string
/** The three pre-promotion check rows. */
checks?: readonly [string, string, string]
/** Left environment tag in the promotion bar. */
fromLabel?: string
/** Right environment tag in the promotion bar. */
toLabel?: string
/** Solid action button label. */
actionLabel?: string
}
export function StagingGraphic({
title = 'Support agent',
headerTag = 'v4',
changeTag = 'a3f8c21',
changeTitle = 'Tighten refund policy checks',
attribution = 'Maya Chen · 2h ago',
checks = CHECKS,
fromLabel = 'Staging',
toLabel = 'Live',
actionLabel = 'Promote',
}: StagingGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
@@ -58,19 +99,19 @@ export function StagingGraphic() {
<div className='w-full max-w-[312px] rounded-xl border border-[var(--border-1)]'>
<div className='flex h-10 items-center gap-2 border-[var(--border-1)] border-b px-4'>
<span className='min-w-0 flex-1 truncate font-medium text-[var(--text-primary)] text-base'>
Support agent
{title}
</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
v4
{headerTag}
</ChipTag>
</div>
<div className='px-3 py-2.5'>
<div className='rounded-lg border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm'>
<span className='flex items-center gap-2'>
<ChipTag variant='mono'>a3f8c21</ChipTag>
<ChipTag variant='mono'>{changeTag}</ChipTag>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-small'>
Tighten refund policy checks
{changeTitle}
</span>
</span>
<span className='mt-1.5 flex items-center gap-1.5'>
@@ -83,13 +124,13 @@ export function StagingGraphic() {
className='size-full scale-110 object-cover'
/>
</span>
<span className='text-[var(--text-muted)] text-caption'>Maya Chen · 2h ago</span>
<span className='text-[var(--text-muted)] text-caption'>{attribution}</span>
</span>
</div>
</div>
<div className='flex flex-col gap-2 border-[var(--border-1)] border-t px-4 py-2.5'>
{CHECKS.map((check) => (
{checks.map((check) => (
<span key={check} className='flex items-center gap-2'>
<CircleCheck className='size-[13px] text-[var(--text-icon)]' />
<span className='text-[var(--text-secondary)] text-caption'>{check}</span>
@@ -100,11 +141,11 @@ export function StagingGraphic() {
<div className='flex items-center justify-between gap-3 border-[var(--border-1)] border-t px-4 py-2.5'>
<span className='hidden min-w-0 items-center gap-1.5 xl:flex'>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Staging
{fromLabel}
</ChipTag>
<ArrowRight className='size-[12px] shrink-0 text-[var(--text-icon)]' />
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Live
{toLabel}
</ChipTag>
</span>
<Button
@@ -113,7 +154,7 @@ export function StagingGraphic() {
tabIndex={-1}
className={cn('pointer-events-none ml-auto', styles.promotePulse)}
>
Promote
{actionLabel}
</Button>
</div>
</div>
@@ -55,17 +55,37 @@ import styles from '@/app/(landing)/enterprise/components/feature-graphics/stand
* bled slot's center. The canvas itself is centered with a transform
* (`left-1/2` + `-translate-x-1/2`) rather than flex `justify-center`
* because an overflowing fixed-size flex item start-aligns instead of
* centering once the tile gets narrower than the canvas. 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 canvas scales
* down via transform so the SOC 2 / Open source chips are never cropped.
* centering once the tile gets narrower than the canvas. Narrow grid
* columns are handled by the feature tile itself, which zooms its whole
* design-space canvas down proportionally (see `SOLUTIONS_VISUAL`), so
* the SOC 2 / Open source chips are never cropped without this graphic
* carrying its own breakpoint scaling.
*
* The two claim chips and the seal's state pill are parametrizable so
* other landing pages (compliance) can retell the certification seal
* with their own standards; the defaults keep the enterprise page's
* SOC 2 / Open source seal byte-identical. Geometry, motion, and inks
* never change with the copy.
*/
export function StandardsGraphic() {
interface StandardsGraphicProps {
/** Left claim chip, anchored to the seal's left connector. */
leftLabel?: string
/** Right claim chip, anchored to the seal's right connector. */
rightLabel?: string
/** State pill beneath the seal. */
sealLabel?: string
}
export function StandardsGraphic({
leftLabel = 'SOC 2',
rightLabel = 'Open source',
sealLabel = 'Verified',
}: StandardsGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div aria-hidden='true' className='absolute inset-0 pr-8 max-lg:pr-6'>
<div className='relative h-full'>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 h-[250px] w-[320px] max-sm:scale-100 max-md:scale-[0.8] lg:max-[1180px]:scale-[0.8]'>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 h-[250px] w-[320px]'>
<div className='absolute top-0 left-[48px] size-[224px] rounded-full border border-[color:color-mix(in_srgb,var(--text-muted-inverse)_22%,transparent)] [mask-image:linear-gradient(to_bottom,black_30%,transparent_92%)]' />
<div className='absolute top-[36px] left-[84px] size-[152px] rounded-full border border-[color:color-mix(in_srgb,var(--text-muted-inverse)_35%,transparent)]' />
@@ -147,14 +167,14 @@ export function StandardsGraphic() {
</div>
<span className='-translate-x-full -translate-y-1/2 absolute top-[112px] left-[90px] whitespace-nowrap'>
<ChipTag variant='mono'>SOC 2</ChipTag>
<ChipTag variant='mono'>{leftLabel}</ChipTag>
</span>
<span className='-translate-y-1/2 absolute top-[112px] left-[230px] whitespace-nowrap'>
<ChipTag variant='mono'>Open source</ChipTag>
<ChipTag variant='mono'>{rightLabel}</ChipTag>
</span>
<span className='-translate-x-1/2 absolute top-[172px] left-1/2 flex h-5 items-center rounded-md bg-[var(--text-muted)] px-1.5 font-medium text-[var(--text-inverse)] text-caption'>
Verified
{sealLabel}
</span>
</div>
</div>
@@ -4,7 +4,7 @@ import Image from 'next/image'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/technical-teams-graphic.module.css'
interface DiffLine {
export interface DiffLine {
/** Gutter marker — space for context, `-` for removed, `+` for added. */
marker: ' ' | '-' | '+'
/** The code content after the marker. */
@@ -60,20 +60,51 @@ const MARKER_TONES: Record<DiffLine['marker'], string> = {
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center instead of the bled
* slot's center. The column is fluid (`w-full max-w-[312px]`) so it
* slot's center. On the wide spanned tile of the two-column band
* (container ≥500px inside `sm`..`lg`) the column relaxes to 400px so
* the diff lines and review card use the wide slot's measure.
* The column is fluid (`w-full max-w-[312px]`) so it
* never exceeds the compensated slot at narrow tile widths — diff lines
* and the reviewer's attribution truncate instead of clipping.
*
* The diff excerpt and review verdict are parametrizable so other
* landing pages (engineering, compliance) can retell the change-review
* moment with their own domain's diff — a code change, a policy edit;
* the defaults keep the enterprise page's Support-agent review
* byte-identical. Chrome, motion, and layout never change with the copy.
*/
export function TechnicalTeamsGraphic() {
interface TechnicalTeamsGraphicProps {
/** The mono diff excerpt, seven lines to keep the tile's vertical rhythm. */
diffLines?: readonly DiffLine[]
/** Reviewer's display name on the verdict card. */
reviewerName?: string
/** Attribution line beneath the reviewer's name. */
reviewerAction?: string
/** Grey tag carrying the verdict's pulsing state. */
verdictTag?: string
/** Closing hairline-ruled row label. */
footerLabel?: string
/** Right-aligned closing row detail. */
footerDetail?: string
}
export function TechnicalTeamsGraphic({
diffLines = DIFF_LINES,
reviewerName = 'Jordan Lee',
reviewerAction = 'Approved these changes',
verdictTag = 'Approved',
footerLabel = 'Review threads resolved',
footerDetail = '2 of 2',
}: TechnicalTeamsGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px]'>
<div className='w-full max-w-[312px] sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div className='px-3 font-mono text-caption leading-[1.8]'>
{DIFF_LINES.map((line) => (
{diffLines.map((line) => (
<div
key={`${line.marker}${line.code}`}
className={cn('truncate whitespace-pre', MARKER_TONES[line.marker])}
@@ -95,23 +126,23 @@ export function TechnicalTeamsGraphic() {
</span>
<span className='min-w-0 flex-1'>
<span className='block truncate font-medium text-[var(--text-primary)] text-small'>
Jordan Lee
{reviewerName}
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
Approved these changes
{reviewerAction}
</span>
</span>
<ChipTag variant='gray' className={cn('shrink-0', styles.approvedPulse)}>
Approved
{verdictTag}
</ChipTag>
</div>
<div className='mt-1.5 flex h-9 items-center gap-2 px-3'>
<CircleCheck className='size-[13px] shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 flex-1 truncate text-[var(--text-secondary)] text-caption'>
Review threads resolved
{footerLabel}
</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>2 of 2</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>{footerDetail}</span>
</div>
</div>
</div>
@@ -1,7 +1,7 @@
import { cn } from '@sim/emcn'
import Image from 'next/image'
import { Cta } from '@/app/(landing)/components/cta/cta'
import { LANDING_CONTENT_WIDTH, LANDING_GUTTER } from '@/app/(landing)/components/landing-layout'
import { PlatformHeroVisual } from '@/app/(landing)/components/platform-hero-visual'
import {
SolutionsHero,
SolutionsLogosRow,
@@ -31,9 +31,9 @@ import {
* Enterprise landing page (`/enterprise`) - the flagship surface for teams
* evaluating Sim as their enterprise AI agent platform.
*
* Structurally it mirrors {@link SolutionsPage} (hero → logos → card rows) but
* composes its own `<main>` so it can append the shared homepage CTA that
* `SolutionsPage` does not render. The four feature rows render through
* Structurally it mirrors {@link SolutionsPage} (hero → logos → card rows →
* shared homepage CTA) but composes its own `<main>` so the hero can render
* full-bleed in its `home` variant. The four feature rows render through
* {@link EnterpriseFeatureGrid} - one shared grid that regroups the 12 cards
* into 4/4/2/2 in the two-column band so no section leaves an orphan cell.
* The shared `SOLUTIONS_SPACING` constants own the enterprise content gutter
@@ -53,31 +53,17 @@ const ENTERPRISE_CONFIG: SolutionsPageConfig = {
summary:
'Sim is the open-source enterprise AI agent platform where IT, operations, and technical teams build, deploy, and govern enterprise AI agents in one AI workspace. Connect 1,000+ integrations and every major LLM, with security, role-based access, approvals, observability, versioning, and audit trails for reliable deployment across teams.',
/**
* The enterprise architectural backdrop with the enterprise-specific
* platform loop framed in front of it: the same white demo window the
* homepage hero uses, filled by the {@link EnterprisePlatformLoop} - a
* sibling of the homepage `HeroPlatformLoop` that renders the whole
* interior live (Brightwave sidebar + the real new-chat home view) and
* replays an enterprise prompt. The `variant='home'` hero renders this
* into the same `aspect-[1300/720]` media frame the homepage uses.
* The shared {@link PlatformHeroVisual} backdrop-plus-demo-window
* composition, filled by the {@link EnterprisePlatformLoop} - a sibling of
* the homepage `HeroPlatformLoop` that renders the whole interior live
* (Brightwave sidebar + the real new-chat home view) and replays an
* enterprise prompt. The `variant='home'` hero renders this into the same
* `aspect-[1300/720]` media frame the homepage uses.
*/
visual: (
<>
<Image
fill
priority
fetchPriority='high'
alt=''
className='object-cover object-center'
sizes='(max-width: 1024px) 100vw, 1300px'
src='/landing/enterprise-hero-background.webp'
/>
<div className='-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 flex aspect-[1080/620] w-[83.08%] flex-col overflow-hidden rounded-[10px] bg-[var(--surface-1)] shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_2px_6px_0_rgba(0,0,0,0.05),0_4px_42px_0_rgba(0,0,0,0.06)]'>
<div className='relative flex-1'>
<EnterprisePlatformLoop />
</div>
</div>
</>
<PlatformHeroVisual>
<EnterprisePlatformLoop />
</PlatformHeroVisual>
),
},
rows: [
@@ -0,0 +1,49 @@
/**
* The FileLibraryGraphic's motion: the library rows stamp in top to
* bottom one after another — the audit tile's one-shot settle, never
* re-played, since the library is filled once. Under
* prefers-reduced-motion the rows render fully settled.
*/
.row0,
.row1,
.row2,
.row3 {
animation: file-library-row-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.row0 {
animation-delay: 0.35s;
}
.row1 {
animation-delay: 0.5s;
}
.row2 {
animation-delay: 0.65s;
}
.row3 {
animation-delay: 0.8s;
}
@keyframes file-library-row-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.row0,
.row1,
.row2,
.row3 {
animation: none;
}
}
@@ -0,0 +1,147 @@
import { ChipTag, cn } from '@sim/emcn'
import { AgentIcon } from '@/components/icons'
import { CsvIcon, DocxIcon, PdfIcon } from '@/components/icons/document-icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css'
interface LibraryRow {
/** File name in the row's leading position. */
name: string
/** Document-type glyph beside the name. */
icon: React.ComponentType<{ className?: string }>
/** Attribution line - "owner · size". */
meta: string
/** Right-aligned relative timestamp. */
time: string
/** Marks the owner as a Sim agent, rendering the agent glyph on the badge. */
agent?: boolean
}
/**
* The library vignette's four rows - the newest an agent-produced report
* on the highlight card, the rest a mix of human uploads and agent
* artifacts so the shared-store story reads in one glance.
*/
const ROWS: readonly [LibraryRow, LibraryRow, LibraryRow, LibraryRow] = [
{
name: 'weekly-report.pdf',
icon: PdfIcon,
meta: 'Report agent · 1.8 MB',
time: 'Now',
agent: true,
},
{
name: 'brand-guidelines.pdf',
icon: PdfIcon,
meta: 'Maya Chen · 5.1 MB',
time: '2h ago',
},
{
name: 'invoice-batch-march.csv',
icon: CsvIcon,
meta: 'Invoice agent · 812 KB',
time: 'Yesterday',
agent: true,
},
{
name: 'onboarding-playbook.docx',
icon: DocxIcon,
meta: 'Jordan Lee · 1.1 MB',
time: 'Jun 12',
},
] as const
/** Per-row ink treatments, quieter with age like the audit ledger. */
const ROW_TONES = [
'text-[var(--text-primary)]',
'text-[var(--text-secondary)]',
'text-[var(--text-muted)]',
'text-[var(--text-muted)]',
] as const
/** Per-index stamp-in classes — the stagger order is baked into each class's delay. */
const ROW_STEP_CLASSES = [styles.row0, styles.row1, styles.row2, styles.row3] as const
/** The owner badge - an outlined circle holding the agent glyph or nothing. */
function OwnerBadge({ row }: { row: LibraryRow }) {
return (
<span className='flex size-7 shrink-0 items-center justify-center rounded-md border border-[var(--border-1)] bg-[var(--surface-2)] text-[var(--text-icon)]'>
{row.agent ? <AgentIcon className='size-[13px]' /> : <row.icon className='size-[14px]' />}
</span>
)
}
/**
* Sim's shared file library told as a frameless, centered vignette (the
* audit tile's composition): a small "Files" header with a `Shared` mono
* ChipTag (fill stepped up to `--surface-6` so the pill stays legible on
* the grey ground) above four library rows - a document-type icon box,
* the file name in the row's regular sans face, an "owner · size"
* attribution line, and a right-aligned timestamp. Human uploads and
* agent-produced artifacts sit interleaved, which is the tile's whole
* claim. The newest record - an agent's report, written seconds ago - is
* the selected row: it sits on a solid white card wearing the family's
* highlight chrome exactly (`--white` fill, 1px `--border-1` hairline,
* `rounded-xl`, `shadow-sm`), while older rows rest directly on the tile
* and quieten with age until a mask gradient dissolves the oldest.
*
* Motion (from `file-library-graphic.module.css`): the rows stamp in top
* to bottom once - the audit tile's one-shot settle, never re-played -
* and are removed under `prefers-reduced-motion`.
*
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center. On the wide spanned tile
* of the two-column band (container ≥500px inside `sm`..`lg`) the column
* relaxes to 400px, matching the family's wide-tile measure.
*/
export function FileLibraryGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px] sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div className='mb-4 flex items-center justify-between'>
<span className='font-medium text-[var(--text-primary)] text-base'>Files</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Shared
</ChipTag>
</div>
<div className='flex flex-col gap-1.5 [mask-image:linear-gradient(to_bottom,black_55%,transparent_100%)]'>
{ROWS.map((row, index) => {
const newest = index === 0
return (
<div
key={row.name}
className={cn(
'flex items-center gap-3 px-3 py-2.5',
ROW_STEP_CLASSES[index],
newest &&
'rounded-xl border border-[var(--border-1)] bg-[var(--white)] shadow-sm'
)}
>
<OwnerBadge row={row} />
<span className='min-w-0 flex-1'>
<span className={cn('block truncate font-medium text-small', ROW_TONES[index])}>
{row.name}
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
{row.meta}
</span>
</span>
<span className='shrink-0 self-start pt-px text-[var(--text-muted)] text-caption'>
{row.time}
</span>
</div>
)
})}
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,60 @@
import { File } from '@sim/emcn/icons'
import {
type CodeSegment,
CodeWindowGraphic,
} from '@/app/(landing)/components/shared/code-window-graphic'
/**
* The `report-agent.ts` excerpt — an agent reading a file from the shared
* store and writing its output back, the page's story reached from code.
*/
const CODE_LINES: readonly CodeSegment[][] = [
[
{ text: 'import', tone: 'muted' },
{ text: ' ' },
{ text: '{ files }', tone: 'primary' },
{ text: ' ' },
{ text: 'from', tone: 'muted' },
{ text: ' ' },
{ text: "'@sim/sdk'", tone: 'primary' },
],
[
{ text: 'const', tone: 'muted' },
{ text: ' ' },
{ text: 'brief', tone: 'primary' },
{ text: ' ' },
{ text: '= await', tone: 'muted' },
],
[{ text: ' ' }, { text: "files.read('q3-brief.pdf')", tone: 'primary' }],
[{ text: 'await', tone: 'muted' }, { text: ' ' }, { text: 'files.upload({', tone: 'primary' }],
[
{ text: ' ' },
{ text: 'name:', tone: 'muted' },
{ text: ' ' },
{ text: "'weekly-report.pdf'", tone: 'primary' },
{ text: ',' },
],
[
{ text: ' ' },
{ text: 'content:', tone: 'muted' },
{ text: ' ' },
{ text: 'draft(brief)', tone: 'primary' },
{ text: ',' },
],
] as const
/**
* Files reached from code told in the shared {@link CodeWindowGraphic}
* editor window: the `File` mark and the `report-agent.ts` filename over
* an SDK excerpt reading a brief out of Sim's file store and uploading
* the finished report back.
*/
export function FilesSdkGraphic() {
return (
<CodeWindowGraphic
icon={<File className='size-[14px] text-[var(--text-muted-inverse)]' />}
filename='report-agent.ts'
lines={CODE_LINES}
/>
)
}
@@ -0,0 +1,319 @@
'use client'
import type { ReactNode } from 'react'
import { useState } from 'react'
import { cn } from '@sim/emcn'
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 { 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. */
const SIDEBAR_CHATS = [
'Parse vendor invoices',
'Draft the weekly report',
'Summarize the data room',
'Zip campaign assets',
] as const
/** Deployed workflows in the sidebar - five fill the design height. */
const SIDEBAR_WORKFLOWS = [
'Invoice parsing',
'Weekly report',
'Contract review',
'Asset export',
'Data-room sync',
] as const
interface FileOwner {
/** Display name in the owner cell. */
name: string
/** Initial on the circular badge; agent owners render the agent glyph instead. */
initial?: string
/** Marks the owner as a Sim agent, swapping the initial badge for the agent glyph. */
agent?: boolean
}
interface FileRowData {
/** File name in the leading column. */
name: string
/** Type glyph shared by the name and type columns. */
icon: React.ComponentType<{ className?: string }>
/** Human-readable file size. */
size: string
/** Short type label (PDF, CSV, ...). */
type: string
/** Relative created timestamp. */
created: string
/** Owner cell - a teammate or a Sim agent. */
owner: FileOwner
}
/**
* The resting library - a believable mix of human uploads and agent-produced
* artifacts, so the "one store for team and agents" story reads at a glance.
*/
const BASE_ROWS: readonly FileRowData[] = [
{
name: 'q3-board-deck.pdf',
icon: PdfIcon,
size: '4.2 MB',
type: 'PDF',
created: '2 hours ago',
owner: { name: 'Maya C.', initial: 'M' },
},
{
name: 'invoice-batch-march.csv',
icon: CsvIcon,
size: '812 KB',
type: 'CSV',
created: '5 hours ago',
owner: { name: 'Invoice agent', agent: true },
},
{
name: 'brand-guidelines.pdf',
icon: PdfIcon,
size: '5.1 MB',
type: 'PDF',
created: 'Yesterday',
owner: { name: 'Jordan L.', initial: 'J' },
},
{
name: 'contract-summary.docx',
icon: DocxIcon,
size: '324 KB',
type: 'DOCX',
created: 'Yesterday',
owner: { name: 'Contract agent', agent: true },
},
{
name: 'onboarding-playbook.docx',
icon: DocxIcon,
size: '1.1 MB',
type: 'DOCX',
created: '3 days ago',
owner: { name: 'Sam O.', initial: 'S' },
},
{
name: 'product-screenshots.zip',
icon: ZipIcon,
size: '18.7 MB',
type: 'ZIP',
created: '1 week ago',
owner: { name: 'Alex M.', initial: 'A' },
},
{
name: 'customer-feedback.csv',
icon: CsvIcon,
size: '96 KB',
type: 'CSV',
created: '1 week ago',
owner: { name: 'Maya C.', initial: 'M' },
},
] as const
/** The agent-produced artifact that drops in at the top of the library. */
const DROPPED_ROW: FileRowData = {
name: 'weekly-report.pdf',
icon: PdfIcon,
size: '1.8 MB',
type: 'PDF',
created: 'Just now',
owner: { name: 'Report agent', agent: true },
}
/** The empty pane holds this long before the first row stamps in. */
const IDLE_HOLD_MS = 700
/** Row N stamps in at IDLE_HOLD_MS + N * ROW_STEP_MS. */
const ROW_STEP_MS = 220
/** The settled library holds this long before the agent's file drops in. */
const DROP_AFTER_MS = 1400
/** The completed frame (with the dropped file) holds this long before the fade. */
const DROPPED_HOLD_MS = 4800
/** Shared grid template for the header and every file row. */
const ROW_GRID = 'grid grid-cols-[minmax(0,1fr)_110px_130px_170px_200px]'
/** Column headers matching the real Files table. */
const COL_HEADERS = ['Name', 'Size', 'Type', 'Created', 'Owner'] as const
/** Renders the owner cell - an initial badge for teammates, the agent glyph for agents. */
function OwnerCell({ owner }: { owner: FileOwner }) {
return (
<span className='flex min-w-0 items-center gap-3 font-medium text-sm'>
<span className='flex size-[14px] flex-shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
{owner.agent ? <AgentIcon className='size-[8px]' /> : owner.initial}
</span>
<span className='truncate text-[var(--text-secondary)]'>{owner.name}</span>
</span>
)
}
/** One static file row on the shared grid - name, size, type, created, owner. */
function FileRow({ row }: { row: FileRowData }) {
const Icon = row.icon
return (
<div className={cn(ROW_GRID, 'h-[40px] items-center')}>
<span className='flex min-w-0 items-center gap-3 px-6 font-medium text-[var(--text-body)] text-sm'>
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate'>{row.name}</span>
</span>
<span className='px-6 font-medium text-[var(--text-secondary)] text-sm'>{row.size}</span>
<span className='flex items-center gap-3 px-6 font-medium text-[var(--text-secondary)] text-sm'>
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
{row.type}
</span>
<span className='px-6 font-medium text-[var(--text-secondary)] text-sm'>{row.created}</span>
<span className='px-6'>
<OwnerCell owner={row.owner} />
</span>
</div>
)
}
/** Quiet toolbar chip - the options bar's Filter and Sort affordances. */
function ToolbarChip({ icon, label }: { icon: ReactNode; label: string }) {
return (
<span className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
{icon}
{label}
</span>
)
}
/**
* The files hero's platform loop - the workflows editor loop's architecture
* (a fixed 1280x735 design-space layer scaled to the window via
* ResizeObserver + `transform: scale`, a parent-owned clock, reduced-motion
* showing the finished frame) with the same live {@link EnterpriseSidebar}
* highlighting its Files nav row. The workspace pane is the Files library
* itself: the 44px title bar (File icon, "Files", "Upload file"), the
* search/Filter/Sort options bar, and the Name/Size/Type/Created/Owner
* table holding a mix of human uploads and agent-produced artifacts.
*
* The loop's beats: the resting rows stamp in top to bottom, the library
* settles, then a new agent-produced file ("weekly-report.pdf · Report
* agent · Just now") drops in at the top of the table - the page's story,
* an agent writing its output into the shared store - before the scene
* fades and the cycle restarts.
*
* Everything is `pointer-events-none` decorative, matching the hero's
* `aria-hidden` frame. Under `prefers-reduced-motion` the loop never
* starts: the full library including the agent's file renders statically.
*/
export function FilesHeroLoop() {
const [rowCount, setRowCount] = useState(0)
const [dropped, setDropped] = useState(false)
const [fading, setFading] = useState(false)
useMotionSafeCycle({
scheduleCycle: () => {
setFading(false)
setRowCount(0)
setDropped(false)
const dropAt = IDLE_HOLD_MS + (BASE_ROWS.length - 1) * ROW_STEP_MS + DROP_AFTER_MS
const totalMs = dropAt + DROPPED_HOLD_MS
return {
timers: [
...BASE_ROWS.map((_, i) =>
setTimeout(() => setRowCount(i + 1), IDLE_HOLD_MS + i * ROW_STEP_MS)
),
setTimeout(() => setDropped(true), dropAt),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
],
totalMs,
}
},
showFinished: () => {
setFading(false)
setRowCount(BASE_ROWS.length)
setDropped(true)
},
})
return (
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeNav='Files'>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
className={cn(
'flex h-full w-full flex-col transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<div className='flex h-[44px] flex-shrink-0 items-center justify-between border-[var(--border)] border-b px-6'>
<div className='flex items-center gap-3'>
<File className='size-[14px] text-[var(--text-icon)]' />
<span className='font-medium text-[var(--text-body)] text-sm'>Files</span>
</div>
<span className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Plus className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Upload file
</span>
</div>
<div className='flex flex-shrink-0 items-center justify-between border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex flex-1 items-center gap-2.5'>
<Search className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='text-[var(--text-subtle)] text-caption'>Search files...</span>
</div>
<div className='flex items-center gap-1.5'>
<ToolbarChip
icon={<ListFilter className='mr-1.5 size-[14px] text-[var(--text-icon)]' />}
label='Filter'
/>
<ToolbarChip
icon={<ArrowUpDown className='mr-1.5 size-[14px] text-[var(--text-icon)]' />}
label='Sort'
/>
</div>
</div>
<div
className={cn(
ROW_GRID,
'h-10 flex-shrink-0 items-center shadow-[inset_0_-1px_0_var(--border)]'
)}
>
{COL_HEADERS.map((header) => (
<span key={header} className='px-6 text-[var(--text-muted)] text-caption'>
{header}
</span>
))}
</div>
<div className='min-h-0 flex-1 overflow-hidden'>
<div
className={cn(
'overflow-hidden transition-all duration-500 ease-out',
dropped ? 'max-h-[40px] opacity-100' : 'max-h-0 opacity-0'
)}
>
<div
className={cn(
'bg-[var(--surface-2)] transition-transform duration-500 ease-out',
dropped ? 'translate-y-0' : '-translate-y-2'
)}
>
<FileRow row={DROPPED_ROW} />
</div>
</div>
{BASE_ROWS.map((row, index) => (
<div
key={row.name}
className={cn(
'transition-all duration-300 ease-out',
index < rowCount ? 'translate-y-0 opacity-100' : '-translate-y-1 opacity-0'
)}
>
<FileRow row={row} />
</div>
))}
</div>
</div>
</div>
</HeroLoopShell>
)
}
+171
View File
@@ -0,0 +1,171 @@
import {
PlatformHeroVisual,
SolutionsPage,
type SolutionsPageConfig,
} from '@/app/(landing)/components'
import {
AuditTrailGraphic,
OperationsTeamsGraphic,
RunMonitoringGraphic,
} from '@/app/(landing)/enterprise/components/feature-graphics'
import { FileLibraryGraphic } from '@/app/(landing)/files/components/feature-graphics/file-library-graphic'
import { FilesSdkGraphic } from '@/app/(landing)/files/components/feature-graphics/files-sdk-graphic'
import { FilesHeroLoop } from '@/app/(landing)/files/components/files-hero-loop'
import { DocumentDraftGraphic } from '@/app/(landing)/solutions/components/feature-graphics'
/**
* Files platform page - a consumer of {@link SolutionsPage} rendered with
* the enterprise page's feature-tile treatment.
*
* The whole page is one typed {@link SolutionsPageConfig} rendered inside
* the shared route-group layout chrome: identity (for structured data), a
* hero, and two rows of three feature tiles. The story is one file store
* shared by the team and its agents - agents read files as inputs, produce
* files as outputs, and every artifact lands in the same place. Every
* visual slot carries a feature graphic in the enterprise design language:
* two files-specific vignettes (the shared library ledger and the SDK
* upload window) plus the switchboard, audit ledger, run monitor, and
* document draft retold for file routing, file history, parse runs, and
* agent-written reports.
*/
const FILES_CONFIG: SolutionsPageConfig = {
module: 'Files',
path: '/files',
hero: {
eyebrow: 'Files',
heading: 'One file store for your team and every agent in Sim.',
description:
'Upload, create, and share in Files, the file store in Sim, the open-source AI workspace. Agents read files as inputs, produce files as outputs, and share them with your team in one place.',
summary:
'Files is the file store in Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Teams upload, create, and share files in one place, and Sim agents read those files as inputs, parse them, and produce new files as outputs shared with everyone.',
visual: (
<PlatformHeroVisual>
<FilesHeroLoop />
</PlatformHeroVisual>
),
},
rows: [
{
id: 'store',
title: 'One place for every file.',
subtitle:
'Sim keeps uploads, agent outputs, and shared documents in a single store your whole workspace can reach.',
cta: { label: 'Explore Files', href: '/signup' },
cards: [
{
title: 'One shared library',
description:
"Sim stores your team's uploads and your agents' outputs side by side, so nothing lives in a silo.",
visual: <FileLibraryGraphic />,
},
{
title: 'Files flow through workflows',
description:
'Sim routes files from uploads, email, and drives into agent runs, then delivers the results where your team works.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: (
<OperationsTeamsGraphic
sourceLabels={['Uploads', 'Gmail', 'Drive']}
destinationLabels={['Slack', 'Reports', 'Archive']}
/>
),
},
{
title: 'Every file action on the record',
description:
'Sim logs every upload, parse, and share, so teams always know where a file came from.',
visual: (
<AuditTrailGraphic
entries={[
{
action: 'File uploaded',
actor: 'Maya Chen',
resource: 'brand-guidelines.pdf',
time: 'Now',
avatar: '/landing/team-avatar-1.jpg',
},
{
action: 'Report generated',
actor: 'Report agent',
resource: 'weekly-report.pdf',
time: '12 min ago',
avatar: '/landing/team-avatar-2.jpg',
},
{
action: 'File parsed',
actor: 'Invoice agent',
resource: 'vendor-invoices.pdf',
time: '1h ago',
avatar: '/landing/team-avatar-3.jpg',
},
{
action: 'Folder shared',
actor: 'Jordan Lee',
resource: 'Q3 planning',
time: 'Jun 12',
avatar: '/landing/team-avatar-1.jpg',
},
]}
/>
),
},
],
},
{
id: 'agents',
title: 'Agents read, parse, and produce files.',
subtitle:
'Sim agents take files as inputs, pull the data out, and write new files back to the store for your team.',
cta: { label: 'Build a file-handling agent', href: '/signup' },
cards: [
{
title: 'Read and parse anything',
description:
'Sim agents take PDFs, spreadsheets, and docs as inputs and pull out the fields your workflow needs.',
visual: (
<RunMonitoringGraphic
title='Parse run'
fields={[
{ label: 'File', value: 'vendor-invoices.pdf', variant: 'strong' },
{ label: 'Trigger', value: 'File upload', variant: 'chip' },
{ label: 'Pages', value: '32', variant: 'mono' },
{ label: 'Duration', value: '2.4s', variant: 'mono' },
]}
outputLabel='Extracted output'
outputPairs={[
{ key: 'status', value: '"parsed"' },
{ key: 'fields', value: '214' },
]}
/>
),
},
{
title: 'Produce files as outputs',
description:
'Sim agents draft reports, exports, and summaries, then save them to Files for the whole team.',
visual: (
<DocumentDraftGraphic
title='Weekly report'
statusTag='Agent-drafted'
footerLabel='Saved to Files'
footerDetail='Just now'
/>
),
},
{
title: 'Read and write from code',
description:
"Sim's SDK gives every agent the same file store, so code reads inputs and uploads outputs in a few lines.",
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: <FilesSdkGraphic />,
},
],
},
],
}
export default function Files() {
return <SolutionsPage config={FILES_CONFIG} />
}
+20
View File
@@ -0,0 +1,20 @@
import { buildLandingMetadata } from '@/lib/landing/seo'
import Files from '@/app/(landing)/files/files'
export const revalidate = 3600
const TITLE = 'Files | One File Store for Teams and Agents in Sim, the AI Workspace'
const DESCRIPTION =
'Files is the file store in Sim, the open-source AI workspace. Upload, create, and share files in one place — agents read them as inputs and produce them as outputs.'
export const metadata = buildLandingMetadata({
title: TITLE,
description: DESCRIPTION,
path: '/files',
keywords:
'AI workspace, file store for AI agents, shared file storage, AI agents read files, agents generate files, document parsing agents, AI file management, open-source AI workspace',
})
export default function Page() {
return <Files />
}
@@ -0,0 +1,40 @@
'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<HTMLDivElement>(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 }
}
@@ -0,0 +1,66 @@
'use client'
import { type DependencyList, useEffect } from 'react'
/** Timers one cycle schedules; cleared on restart, unmount, or motion change. */
type CycleTimer = ReturnType<typeof setTimeout>
interface MotionSafeCycleOptions {
/**
* Starts one animation cycle: resets the loop's state (including any
* remount `cycleId` bump), schedules the cycle's beats, and returns the
* pending timers plus the cycle's total length. The hook restarts the
* cycle `totalMs` after it begins, so the callback only schedules the
* beats within one cycle - including the final fade-out.
*/
scheduleCycle: () => { timers: CycleTimer[]; totalMs: number }
/** Jumps the loop to its finished frame for reduced-motion viewers. */
showFinished: () => void
}
/**
* The hero loops' shared clock scaffold: watches
* `matchMedia('(prefers-reduced-motion: reduce)')` and either replays
* {@link MotionSafeCycleOptions.scheduleCycle} on repeat or renders the
* static finished frame via {@link MotionSafeCycleOptions.showFinished}.
* Reacts live to preference changes and clears every pending timer on
* restart, preference flip, and unmount.
*
* @param deps - Effect dependencies; the loop restarts from scratch when
* they change. Defaults to mount-only.
*/
export function useMotionSafeCycle(
{ scheduleCycle, showFinished }: MotionSafeCycleOptions,
deps: DependencyList = []
) {
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
let timers: CycleTimer[] = []
const clearScheduled = () => {
timers.forEach(clearTimeout)
timers = []
}
const runCycle = () => {
const { timers: scheduled, totalMs } = scheduleCycle()
timers = [...scheduled, setTimeout(runCycle, totalMs)]
}
const syncMotionPreference = () => {
clearScheduled()
if (media.matches) {
showFinished()
return
}
runCycle()
}
syncMotionPreference()
media.addEventListener('change', syncMotionPreference)
return () => {
media.removeEventListener('change', syncMotionPreference)
clearScheduled()
}
}, deps)
}
@@ -0,0 +1,63 @@
/**
* The ConnectorSyncGraphic's motion: the connector rows stamp in top to
* bottom one after another — the audit tile's one-shot settle, never
* re-played — then the syncing row's icon box carries the family's
* shared quiet 6s ring-pulse beat. The status dot blink is Tailwind's
* `animate-pulse`, disabled through its own `motion-reduce:animate-none`.
* Under prefers-reduced-motion the rows render fully settled.
*/
.row0,
.row1,
.row2 {
animation: connector-sync-row-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.row0 {
animation-delay: 0.35s;
}
.row1 {
animation-delay: 0.55s;
}
.row2 {
animation-delay: 0.75s;
}
@keyframes connector-sync-row-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.iconPulse {
animation: connector-sync-icon-pulse 6s ease-out 1.4s infinite;
}
@keyframes connector-sync-icon-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 30%, transparent);
}
36% {
box-shadow: 0 0 0 5px color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.row0,
.row1,
.row2,
.iconPulse {
animation: none;
}
}
@@ -0,0 +1,126 @@
import { ChipTag, cn } from '@sim/emcn'
import { ConfluenceIcon, GoogleDriveIcon, NotionIcon } from '@/components/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/knowledge/components/feature-graphics/connector-sync-graphic.module.css'
interface ConnectorRow {
/** Connector brand mark. */
icon: React.ComponentType<{ className?: string }>
/** Connector name. */
name: string
/** Document count attribution line. */
documents: string
/** Right-aligned sync status. */
status: string
/** Whether this row is the mid-sync one carrying the live beat. */
syncing?: boolean
}
/**
* Three sources feeding one knowledge base: Notion mid-sync as the live
* beat, Drive and Confluence settled from earlier passes.
*/
const CONNECTOR_ROWS: readonly ConnectorRow[] = [
{
icon: NotionIcon,
name: 'Notion',
documents: '128 docs',
status: 'Syncing',
syncing: true,
},
{
icon: GoogleDriveIcon,
name: 'Google Drive',
documents: '342 docs',
status: 'Synced · 2m ago',
},
{
icon: ConfluenceIcon,
name: 'Confluence',
documents: '96 docs',
status: 'Synced · 1h ago',
},
] as const
/** Per-row stamp-in classes - the stagger order is baked into each class's delay. */
const ROW_STEP_CLASSES = [styles.row0, styles.row1, styles.row2] as const
/**
* Source syncing told as a frameless connector ledger (the audit tile's
* composition): a small "Connectors" header with an `Auto-sync` mono
* ChipTag (fill stepped up to `--surface-6` so the pill stays legible on
* the grey ground) above three source rows, each a white card in the
* audit tile's exact chrome (`--white` fill, 1px `--border-1` hairline,
* `rounded-xl`, `shadow-sm`) pairing the connector's real brand mark in
* the lifecycle header's outlined `size-6` icon box with the source name,
* its document count, and a right-aligned sync status. The top row is
* mid-sync - a pulsing dot beside "Syncing" and the family's quiet 6s
* ring pulse on its icon box - while the settled rows read "Synced".
*
* Motion (from `connector-sync-graphic.module.css`): the rows stamp in
* top to bottom once - the audit tile's one-shot settle, never re-played -
* then the syncing row's icon box carries the shared ring-pulse beat. The
* dot blink is Tailwind's `animate-pulse` with its own
* `motion-reduce:animate-none`; everything renders settled under
* `prefers-reduced-motion`.
*
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center. The column is fluid
* (`w-full max-w-[312px]`), relaxing to 400px on the wide spanned tile of
* the two-column band (container ≥500px inside `sm`..`lg`).
*/
export function ConnectorSyncGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px] sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div className='mb-4 flex items-center justify-between'>
<span className='font-medium text-[var(--text-primary)] text-base'>Connectors</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Auto-sync
</ChipTag>
</div>
<div className='flex flex-col gap-2'>
{CONNECTOR_ROWS.map((row, index) => (
<div
key={row.name}
className={cn(
'flex items-center gap-2.5 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm',
ROW_STEP_CLASSES[index]
)}
>
<span
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-md border border-[var(--border-1)]',
row.syncing && styles.iconPulse
)}
>
<row.icon className='size-[14px]' />
</span>
<span className='min-w-0 flex-1'>
<span className='block truncate font-medium text-[var(--text-primary)] text-small'>
{row.name}
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
{row.documents}
</span>
</span>
<span className='flex shrink-0 items-center gap-1.5 text-[var(--text-muted)] text-caption'>
{row.syncing && (
<span className='size-1.5 animate-pulse rounded-full bg-[var(--text-secondary)] motion-reduce:animate-none' />
)}
{row.status}
</span>
</div>
))}
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,58 @@
import { Database } from '@sim/emcn/icons'
import {
type CodeSegment,
CodeWindowGraphic,
} from '@/app/(landing)/components/shared/code-window-graphic'
/**
* A knowledge search step inside an agent's logic - retrieve the passages
* that ground the answer, then answer from them.
*/
const CODE_LINES: readonly CodeSegment[][] = [
[
{ text: 'const', tone: 'muted' },
{ text: ' ' },
{ text: 'docs', tone: 'primary' },
{ text: ' ' },
{ text: '= await', tone: 'muted' },
{ text: ' ' },
{ text: 'knowledge', tone: 'primary' },
],
[{ text: ' .search({' }],
[
{ text: ' ' },
{ text: 'base:', tone: 'muted' },
{ text: ' ' },
{ text: "'Support KB'", tone: 'primary' },
{ text: ',' },
],
[
{ text: ' ' },
{ text: 'query:', tone: 'muted' },
{ text: ' ' },
{ text: 'ticket.question', tone: 'primary' },
{ text: ',' },
],
[{ text: ' })' }],
[
{ text: 'return', tone: 'muted' },
{ text: ' ' },
{ text: 'agent.answer({ docs })', tone: 'primary' },
],
] as const
/**
* Knowledge retrieval inside agent logic, told in the shared
* {@link CodeWindowGraphic} editor window: a `Database` mark and the
* `answer-bot.ts` filename over a knowledge search step retrieving the
* Support KB passages that ground the agent's answer.
*/
export function KnowledgeQueryGraphic() {
return (
<CodeWindowGraphic
icon={<Database className='size-[14px] text-[var(--text-muted-inverse)]' />}
filename='answer-bot.ts'
lines={CODE_LINES}
/>
)
}
@@ -0,0 +1,281 @@
'use client'
import { useState } from 'react'
import { cn } from '@sim/emcn'
import { ArrowUpDown, Database, ListFilter, Plus, Search } from '@sim/emcn/icons'
import {
ConfluenceIcon,
GithubIcon,
GoogleDriveIcon,
IntercomIcon,
MicrosoftSharepointIcon,
NotionIcon,
SalesforceIcon,
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 { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
/** Sidebar content for the knowledge hero - a team living in its docs. */
const SIDEBAR_CHATS = [
'Refund policy question',
'Onboarding doc digest',
'Security review answers',
'Docs freshness audit',
] as const
/** Deployed workflows in the sidebar - five fill the design height. */
const SIDEBAR_WORKFLOWS = [
'Docs sync monitor',
'Support answer bot',
'Doc ingestion pipeline',
'Weekly KB digest',
'Stale-doc alerts',
] as const
interface KnowledgeBaseRow {
/** Knowledge-base name in the leading cell. */
name: string
/** Document count at rest. */
documents: string
/** Document count after the sync beat lands (sync row only). */
documentsSynced?: string
/** Token count at rest. */
tokens: string
/** Token count after the sync beat lands (sync row only). */
tokensSynced?: string
/** Connector marks clustered in the Connectors cell. */
connectors: readonly React.ComponentType<{ className?: string }>[]
/** Created cell text. */
created: string
}
/**
* The knowledge-bases table, in the real Knowledge Base module's column
* vocabulary (Name / Documents / Tokens / Connectors / Created). The first
* row is the sync-beat row: its counts step up when the "Syncing" beat
* resolves.
*/
const BASE_ROWS: readonly KnowledgeBaseRow[] = [
{
name: 'Product Documentation',
documents: '847',
documentsSynced: '861',
tokens: '1,284,392',
tokensSynced: '1,309,204',
connectors: [NotionIcon, GoogleDriveIcon],
created: '2 days ago',
},
{
name: 'Customer Support KB',
documents: '1,203',
tokens: '2,847,293',
connectors: [ZendeskIcon, IntercomIcon],
created: '1 week ago',
},
{
name: 'Engineering Wiki',
documents: '634',
tokens: '1,932,405',
connectors: [ConfluenceIcon, GithubIcon],
created: 'March 12th, 2026',
},
{
name: 'Sales Playbook',
documents: '92',
tokens: '418,570',
connectors: [SalesforceIcon, GoogleDriveIcon],
created: 'March 5th, 2026',
},
{
name: 'People & Policies',
documents: '156',
tokens: '521,882',
connectors: [MicrosoftSharepointIcon, NotionIcon],
created: 'February 28th, 2026',
},
] as const
/** The empty table holds this long before the first row stamps in. */
const IDLE_HOLD_MS = 700
/** Row N stamps in at IDLE_HOLD_MS + N * ROW_STEP_MS. */
const ROW_STEP_MS = 430
/** The sync beat starts this long after the last row lands. */
const SYNC_AFTER_MS = 900
/** How long the first row shows "Syncing" before its counts update. */
const SYNC_MS = 2200
/** The settled, synced table holds this long before the fade. */
const SYNCED_HOLD_MS = 3400
/** Column headers matching the real Knowledge Base table. */
const COL_HEADERS = ['Name', 'Documents', 'Tokens', 'Connectors', 'Created'] as const
/** The sync beat's lifecycle inside one cycle. */
type SyncPhase = 'idle' | 'syncing' | 'synced'
/**
* The knowledge hero's module loop - the WorkflowsEditorLoop architecture
* (fixed 1280x735 design-space layer scaled to the window via ResizeObserver
* + `transform: scale`, a parent-owned clock, reduced-motion showing the
* finished frame) with the workspace pane retelling the Knowledge Base
* module: the 44px title bar (Database mark, "New base"), the search /
* Filter / Sort options bar, and the knowledge-bases table in the real
* module's column vocabulary. The rows stamp in one by one, then the top
* base's connector cluster shows a brief "Syncing" beat that resolves into
* updated document and token counts before the scene fades and the cycle
* restarts.
*
* Everything is `pointer-events-none` decorative, matching the hero's
* `aria-hidden` frame. Under `prefers-reduced-motion` the loop never starts:
* the fully-populated, synced table renders statically.
*/
export function KnowledgeHeroLoop() {
const [visibleRows, setVisibleRows] = useState(0)
const [syncPhase, setSyncPhase] = useState<SyncPhase>('idle')
const [fading, setFading] = useState(false)
useMotionSafeCycle({
scheduleCycle: () => {
setFading(false)
setVisibleRows(0)
setSyncPhase('idle')
const syncAt = IDLE_HOLD_MS + (BASE_ROWS.length - 1) * ROW_STEP_MS + SYNC_AFTER_MS
const totalMs = syncAt + SYNC_MS + SYNCED_HOLD_MS
return {
timers: [
...BASE_ROWS.map((_, i) =>
setTimeout(() => setVisibleRows(i + 1), IDLE_HOLD_MS + i * ROW_STEP_MS)
),
setTimeout(() => setSyncPhase('syncing'), syncAt),
setTimeout(() => setSyncPhase('synced'), syncAt + SYNC_MS),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
],
totalMs,
}
},
showFinished: () => {
setFading(false)
setVisibleRows(BASE_ROWS.length)
setSyncPhase('synced')
},
})
return (
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeNav='Knowledge base'>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
className={cn(
'flex h-full w-full flex-col transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<div className='flex h-[44px] flex-shrink-0 items-center justify-between border-[var(--border)] border-b px-6'>
<div className='flex items-center gap-3'>
<Database className='size-[14px] text-[var(--text-icon)]' />
<span className='font-medium text-[var(--text-body)] text-sm'>Knowledge Base</span>
</div>
<div className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Plus className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
New base
</div>
</div>
<div className='flex flex-shrink-0 items-center justify-between border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center gap-2.5'>
<Search className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='text-[var(--text-subtle)] text-caption'>
Search knowledge bases...
</span>
</div>
<div className='flex items-center gap-1.5'>
<div className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ListFilter className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Filter
</div>
<div className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ArrowUpDown className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Sort
</div>
</div>
</div>
<div className='min-h-0 flex-1 overflow-hidden'>
<table className='w-full table-fixed text-sm'>
<colgroup>
<col />
<col style={{ width: 140 }} />
<col style={{ width: 150 }} />
<col style={{ width: 170 }} />
<col style={{ width: 190 }} />
</colgroup>
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{COL_HEADERS.map((header) => (
<th
key={header}
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-[var(--text-muted)] text-caption'
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{BASE_ROWS.map((row, index) => {
const isSyncRow = index === 0
const synced = isSyncRow && syncPhase === 'synced'
return (
<tr
key={row.name}
className={cn(
'transition-all duration-300 ease-out',
index < visibleRows
? 'translate-y-0 opacity-100'
: '-translate-y-1 opacity-0'
)}
>
<td className='px-6 py-2.5 align-middle'>
<span className='flex min-w-0 items-center gap-3 font-medium text-[var(--text-body)] text-sm'>
<Database className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate'>{row.name}</span>
</span>
</td>
<td className='px-6 py-2.5 align-middle font-medium text-[var(--text-secondary)] text-sm'>
{synced && row.documentsSynced ? row.documentsSynced : row.documents}
</td>
<td className='px-6 py-2.5 align-middle font-medium text-[var(--text-secondary)] text-sm'>
{synced && row.tokensSynced ? row.tokensSynced : row.tokens}
</td>
<td className='px-6 py-2.5 align-middle'>
<span className='flex items-center gap-2.5'>
<span className='flex items-center gap-1'>
{row.connectors.map((Icon, iconIndex) => (
<Icon key={iconIndex} className='size-3.5 flex-shrink-0' />
))}
</span>
{isSyncRow && syncPhase === 'syncing' && (
<span className='flex items-center gap-1.5 text-[var(--text-muted)] text-caption'>
<span className='size-1.5 animate-pulse rounded-full bg-[var(--text-secondary)] motion-reduce:animate-none' />
Syncing
</span>
)}
{synced && (
<span className='text-[var(--text-muted)] text-caption'>Updated</span>
)}
</span>
</td>
<td className='px-6 py-2.5 align-middle font-medium text-[var(--text-secondary)] text-sm'>
{row.created}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
</HeroLoopShell>
)
}
@@ -0,0 +1,175 @@
import {
PlatformHeroVisual,
SolutionsPage,
type SolutionsPageConfig,
} from '@/app/(landing)/components'
import {
AuditTrailGraphic,
OperationsTeamsGraphic,
RunMonitoringGraphic,
} from '@/app/(landing)/enterprise/components/feature-graphics'
import { ConnectorSyncGraphic } from '@/app/(landing)/knowledge/components/feature-graphics/connector-sync-graphic'
import { KnowledgeQueryGraphic } from '@/app/(landing)/knowledge/components/feature-graphics/knowledge-query-graphic'
import { KnowledgeHeroLoop } from '@/app/(landing)/knowledge/components/knowledge-hero-loop'
import { KnowledgeAnswerGraphic } from '@/app/(landing)/solutions/components/feature-graphics'
/**
* Knowledge Base platform page - a consumer of {@link SolutionsPage}
* rendered with the enterprise page's feature-tile treatment.
*
* The whole page is one typed {@link SolutionsPageConfig} rendered inside
* the shared route-group layout chrome: identity (for structured data), a
* hero, and two rows of three cards. The story is agent memory - sync your
* sources in through connectors, get answers grounded in your own docs
* with citations, reach the same knowledge from any workflow, and let Sim
* keep it all fresh on its own. Two knowledge-specific vignettes (the
* connector ledger and the dark knowledge-search code window) sit beside
* enterprise graphics retold for knowledge work: the grounded chat answer,
* the sync-event audit ledger, and the document-processing log panel.
*
* The JSON-LD emitted by {@link SolutionsPage} is structurally identical
* to the platform page's (`WebPage` + `BreadcrumbList` +
* `WebApplication`), so the feature-tile treatment is SEO-neutral.
*/
const KNOWLEDGE_CONFIG: SolutionsPageConfig = {
module: 'Knowledge Base',
path: '/knowledge',
hero: {
eyebrow: 'Knowledge Base',
heading: "Give your agents memory of your company's data in Sim.",
description:
"Knowledge Base is your agents' memory in Sim, the open-source AI workspace. Upload docs, sync sources like Notion and Google Drive, and get answers grounded in your own data, with citations.",
summary:
'Knowledge Base is the agent-memory module in Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Teams upload docs or sync sources like Notion, Google Drive, and Confluence, and every agent answers from that data with citations, kept fresh automatically.',
visual: (
<PlatformHeroVisual>
<KnowledgeHeroLoop />
</PlatformHeroVisual>
),
},
rows: [
{
id: 'memory',
title: 'Give agents memory of your data.',
subtitle:
'Sim turns your docs and sources into searchable memory every agent can read, so answers come from your data instead of guesswork.',
cta: { label: 'Explore Knowledge Base', href: '/signup' },
cards: [
{
title: 'Sync your sources',
description:
'Connect Notion, Google Drive, Confluence, and more. Sim pulls the documents in and keeps every source syncing on its own.',
visual: <ConnectorSyncGraphic />,
},
{
title: 'Answers grounded in your docs',
description:
'Agents in Sim answer from your knowledge base and cite the document behind every answer, so teams can check the source.',
visual: (
<KnowledgeAnswerGraphic
question='What is our refund policy for annual plans?'
answer='Annual plans are refunded pro-rata within 30 days of renewal — after that, the remaining term converts to account credit.'
sourceLabel='Billing policy'
sourceDetail='Cited from your knowledge base'
/>
),
},
{
title: 'Search knowledge from any workflow',
description:
'Drop a knowledge search step into any workflow. Sim retrieves the passages the agent needs mid-run, in visual builds or in code.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: <KnowledgeQueryGraphic />,
},
],
},
{
id: 'fresh',
title: 'Knowledge that stays fresh on its own.',
subtitle:
'Sim re-syncs your sources, processes every new document, and logs each update, so agent memory never drifts out of date.',
cta: { label: 'See how syncing works', href: '/signup' },
cards: [
{
title: 'Fresh without the upkeep',
description:
'Sim re-syncs each connected source on a schedule and records every update, so nobody has to re-upload a doc again.',
visual: (
<AuditTrailGraphic
entries={[
{
action: 'Source synced',
actor: 'Notion',
resource: '128 docs updated',
time: 'Now',
avatar: '/landing/team-avatar-1.jpg',
},
{
action: 'Document processed',
actor: 'Sim',
resource: 'Pricing guide v4',
time: '4 min ago',
avatar: '/landing/team-avatar-2.jpg',
},
{
action: 'Sync completed',
actor: 'Google Drive',
resource: '342 docs checked',
time: '1h ago',
avatar: '/landing/team-avatar-3.jpg',
},
{
action: 'Base created',
actor: 'Maya Chen',
resource: 'Product Documentation',
time: 'Jun 2',
avatar: '/landing/team-avatar-1.jpg',
},
]}
/>
),
},
{
title: 'Watch documents process',
description:
'Sim splits each document into searchable passages and shows the work: source, passage count, tokens, and status for every file.',
visual: (
<RunMonitoringGraphic
title='Document details'
statusLabel='Processing'
fields={[
{ label: 'Document', value: 'Pricing guide v4.pdf', variant: 'strong' },
{ label: 'Source', value: 'Notion', variant: 'chip' },
{ label: 'Passages', value: '164', variant: 'mono' },
{ label: 'Tokens', value: '48,392', variant: 'mono' },
]}
outputLabel='Processing output'
outputPairs={[
{ key: 'status', value: '"indexed"' },
{ key: 'passages', value: '164' },
]}
/>
),
},
{
title: 'One memory for every agent',
description:
'Every agent in Sim reads from the same knowledge base — in Chat, in workflows, and in deployed agents — so answers stay consistent.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: (
<OperationsTeamsGraphic
sourceLabels={['Notion', 'Drive', 'Confluence']}
destinationLabels={['Chat', 'Workflows', 'Agents']}
/>
),
},
],
},
],
}
export default function Knowledge() {
return <SolutionsPage config={KNOWLEDGE_CONFIG} />
}
+20
View File
@@ -0,0 +1,20 @@
import { buildLandingMetadata } from '@/lib/landing/seo'
import Knowledge from '@/app/(landing)/knowledge/knowledge'
export const revalidate = 3600
const TITLE = 'Knowledge Base | Agent Memory in Sim, the AI Workspace'
const DESCRIPTION =
"Knowledge Base is your agents' memory in Sim, the open-source AI workspace. Upload docs, sync sources like Notion and Google Drive, and get answers grounded in your own data, with citations."
export const metadata = buildLandingMetadata({
title: TITLE,
description: DESCRIPTION,
path: '/knowledge',
keywords:
'AI workspace, knowledge base for AI agents, agent memory, ground AI answers in company data, sync documents to AI agents, AI answers with citations, open-source AI workspace',
})
export default function Page() {
return <Knowledge />
}
@@ -0,0 +1,60 @@
/**
* The FailureAlertGraphic's motion — a one-shot causal settle: the failed
* run row stamps in, a white pulse falls once down the connector hairline,
* and the Slack alert window stamps in as the pulse lands. Never re-played.
* Under prefers-reduced-motion everything renders settled with no pulse.
*/
.runRow {
animation: failure-alert-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
animation-delay: 0.35s;
}
.sweep {
position: absolute;
inset-inline: 0;
top: -40%;
height: 40%;
background: linear-gradient(to bottom, transparent, var(--text-inverse));
animation: failure-alert-sweep 0.45s ease-in 0.85s forwards;
opacity: 0;
}
.alertWindow {
animation: failure-alert-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
animation-delay: 1.2s;
}
@keyframes failure-alert-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes failure-alert-sweep {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 1;
transform: translateY(350%);
}
}
@media (prefers-reduced-motion: reduce) {
.runRow,
.alertWindow {
animation: none;
}
.sweep {
animation: none;
opacity: 0;
}
}
@@ -0,0 +1,96 @@
import { cn } from '@sim/emcn'
import { SlackIcon } from '@/components/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/logs/components/feature-graphics/failure-alert-graphic.module.css'
/** Shared hairline ink for the run row, connector, and notification window. */
const OUTLINE_INK = 'border-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'
/**
* A failure caught by alerting, told top to bottom as the deploy tile's
* causal vignette in the dark tiles' outlined ink: the failed run at the
* top - an outlined pill pairing the workflow name with an outlined
* "Error" tag and the failure time - a faint vertical hairline linking
* cause to effect, and a minimal outlined Slack window rising from the
* bottom edge with the alert Sim sent: the #oncall channel in the header
* (the Slack mark is a real brand logo, the one place hex is allowed),
* and the failure message with the failing block named, linking back to
* the run's trace.
*
* Motion (from `failure-alert-graphic.module.css`): the run row stamps
* in first, a white pulse falls down the connector, and the alert window
* stamps in as the pulse lands - a one-shot settle, never re-played.
* Under `prefers-reduced-motion` everything renders settled with no
* pulse.
*/
export function FailureAlertGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex flex-col items-center pr-8 max-lg:pr-6'
>
<div
className={cn(
'mt-1 flex items-center gap-2 rounded-[10px] border py-1.5 pr-1.5 pl-2.5',
OUTLINE_INK,
styles.runRow
)}
>
<span className='font-medium text-[var(--text-inverse)] text-caption'>
Nightly data sync
</span>
<span
className={cn(
'flex h-5 items-center rounded-md border px-1.5 font-medium text-[var(--text-muted-inverse)] text-caption',
OUTLINE_INK
)}
>
Error
</span>
<span className='text-[var(--text-muted-inverse)] text-caption'>2:14 AM</span>
</div>
<span
className={cn(
'relative mt-2 mb-2 min-h-4 w-px flex-1 overflow-hidden',
'bg-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'
)}
>
<span className={styles.sweep} />
</span>
<div
className={cn(
'relative w-full rounded-t-xl border border-b-0',
OUTLINE_INK,
styles.alertWindow
)}
>
<div className={cn('flex h-11 items-center gap-2 border-b px-3', OUTLINE_INK)}>
<span
className={cn(
'flex size-6 items-center justify-center rounded-md border',
OUTLINE_INK
)}
>
<SlackIcon className='size-[12px]' />
</span>
<span className='min-w-0 flex-1 truncate font-medium text-[var(--text-inverse)] text-small'>
#oncall
</span>
<span className='shrink-0 text-[var(--text-muted-inverse)] text-caption'>Now</span>
</div>
<div className='px-3 pt-2.5 pb-4'>
<span className='block font-medium text-[var(--text-inverse)] text-small'>
Run failed: Nightly data sync
</span>
<span className='mt-1 block text-[var(--text-muted-inverse)] text-caption'>
Timeout in Post to ledger · Open the trace in Sim
</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,42 @@
/**
* The FilterRunsGraphic's motion: the matched result rows stamp in top to
* bottom one after another — the audit tile's one-shot settle, never
* re-played. Under prefers-reduced-motion the rows render fully settled.
*/
.row0,
.row1,
.row2 {
animation: filter-runs-row-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.row0 {
animation-delay: 0.45s;
}
.row1 {
animation-delay: 0.6s;
}
.row2 {
animation-delay: 0.75s;
}
@keyframes filter-runs-row-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.row0,
.row1,
.row2 {
animation: none;
}
}
@@ -0,0 +1,106 @@
import { Badge, ChipTag, cn, Search } from '@sim/emcn'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/logs/components/feature-graphics/filter-runs-graphic.module.css'
interface FilteredRun {
/** Workflow name of the matched run. */
workflow: string
/** Run timestamp, newest first. */
time: string
}
/** The three failed runs the query and filters surface, newest first. */
const MATCHED_RUNS: readonly FilteredRun[] = [
{ workflow: 'Nightly data sync', time: 'Jul 12 2:14 AM' },
{ workflow: 'Nightly data sync', time: 'Jul 9 2:14 AM' },
{ workflow: 'Nightly data sync', time: 'Jul 6 2:15 AM' },
] as const
/** Active filter chips above the results, the options bar's vocabulary. */
const FILTER_CHIPS = ['Status: Error', 'Trigger: Schedule', 'Past 7 days'] as const
/** Per-row stamp-in classes - the stagger order is baked into each class's delay. */
const ROW_STEP_CLASSES = [styles.row0, styles.row1, styles.row2] as const
/**
* Search and filters told as a frameless centered vignette (the audit and
* monitoring tiles' composition): a small "Search runs" header with a
* match-count mono ChipTag (fill stepped up to `--surface-6` so the pill
* stays legible on the grey ground), the query itself lifted onto the
* tile's highlight - a white card in the audit tile's exact chrome
* (`--white` fill, 1px `--border-1` hairline, rounded, `shadow-sm`) with
* a blinking caret holding the query - a row of quiet mono filter chips,
* and the three matched runs as airy rows ruled by 1px `--border-1`
* hairlines, each pairing the workflow name with its Error badge and
* timestamp.
*
* Motion: the result rows stamp in top to bottom once (from
* `filter-runs-graphic.module.css`, the audit tile's one-shot settle);
* the caret blink is Tailwind's `animate-pulse`, disabled through its own
* `motion-reduce:animate-none`. Under `prefers-reduced-motion` the rows
* render fully settled.
*
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center instead of the bled
* slot's center. On the wide spanned tile of the two-column band
* (container ≥500px inside `sm`..`lg`) the column relaxes to 400px.
*/
export function FilterRunsGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px] sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div className='mb-2.5 flex items-center justify-between'>
<span className='font-medium text-[var(--text-primary)] text-base'>Search runs</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
3 matches
</ChipTag>
</div>
<div className='flex items-center gap-2.5 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2 shadow-sm'>
<Search className='size-[14px] shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 truncate text-[var(--text-body)] text-caption'>
nightly sync
<span className='ml-px inline-block h-[1.1em] w-px translate-y-[2px] animate-pulse bg-[var(--text-primary)] align-text-bottom motion-reduce:animate-none' />
</span>
</div>
<div className='mt-2.5 flex flex-wrap items-center gap-1.5'>
{FILTER_CHIPS.map((chip) => (
<ChipTag key={chip} variant='mono' className='bg-[var(--surface-6)]'>
{chip}
</ChipTag>
))}
</div>
<div className='mt-1.5'>
{MATCHED_RUNS.map((run, index) => (
<div
key={run.time}
className={cn(
'flex h-10 items-center justify-between gap-3',
index > 0 && 'border-[var(--border-1)] border-t',
ROW_STEP_CLASSES[index]
)}
>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-caption'>
{run.workflow}
</span>
<span className='flex shrink-0 items-center gap-2.5'>
<Badge variant='gray' size='sm' dot>
Error
</Badge>
<span className='text-[var(--text-muted)] text-caption'>{run.time}</span>
</span>
</div>
))}
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,3 @@
export { FailureAlertGraphic } from './failure-alert-graphic'
export { FilterRunsGraphic } from './filter-runs-graphic'
export { RunTraceGraphic } from './run-trace-graphic'
@@ -0,0 +1,54 @@
/**
* The RunTraceGraphic's motion: the trace rows stamp in top to bottom
* one after another — the agent-code tile's one-shot settle, never
* re-played. Under prefers-reduced-motion the rows render fully settled.
*/
.row0,
.row1,
.row2,
.row3,
.row4 {
animation: run-trace-row-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.row0 {
animation-delay: 0.35s;
}
.row1 {
animation-delay: 0.5s;
}
.row2 {
animation-delay: 0.65s;
}
.row3 {
animation-delay: 0.8s;
}
.row4 {
animation-delay: 0.95s;
}
@keyframes run-trace-row-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.row0,
.row1,
.row2,
.row3,
.row4 {
animation: none;
}
}
@@ -0,0 +1,151 @@
import { cn, Library } from '@sim/emcn'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/logs/components/feature-graphics/run-trace-graphic.module.css'
interface TraceSpanRow {
/** Block name in the trace tree. */
name: string
/** Duration text, right-aligned in mono. */
duration: string
/** Indent class for child spans nested under the agent. */
indentClass?: string
/** Waterfall bar geometry - left offset and width as arbitrary classes. */
barClass: string
/** Bar ink - parents solid, children lighter, so depth reads at a glance. */
barTone: 'parent' | 'child'
}
/**
* The support-routing run's trace distilled to tile scale: the run's
* top-level blocks with the agent's tool call and model reply nested
* beneath it, each span's waterfall bar offset by when it started.
*/
const TRACE_SPANS: readonly TraceSpanRow[] = [
{
name: 'Start',
duration: '12ms',
barClass: 'left-0 w-[4%]',
barTone: 'parent',
},
{
name: 'Support agent',
duration: '1.24s',
barClass: 'left-[5%] w-[66%]',
barTone: 'parent',
},
{
name: 'Search tickets',
duration: '420ms',
indentClass: 'pl-3',
barClass: 'left-[9%] w-[24%]',
barTone: 'child',
},
{
name: 'Generate reply',
duration: '540ms',
indentClass: 'pl-3',
barClass: 'left-[38%] w-[30%]',
barTone: 'child',
},
{
name: 'Send to Slack',
duration: '180ms',
barClass: 'left-[74%] w-[11%]',
barTone: 'parent',
},
] as const
/** Per-row stamp-in classes - the stagger order is baked into each class's delay. */
const ROW_STEP_CLASSES = [styles.row0, styles.row1, styles.row2, styles.row3, styles.row4] as const
/** Span-name inks on the dark tile - parents brighter than nested children. */
const NAME_TONE_CLASS = {
parent: 'text-[var(--text-inverse)]',
child: 'text-[var(--text-muted-inverse)]',
} as const
/** Waterfall bar inks - the same two-step ramp as the names. */
const BAR_TONE_CLASS = {
parent: 'bg-[var(--text-inverse)] opacity-80',
child: 'bg-[var(--text-inverse)] opacity-40',
} as const
/** Shared hairline ink for the window outline, header rule, and icon box. */
const OUTLINE_INK = 'border-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'
/**
* A run's block-by-block trace told inside the agent-code tile's dark
* outlined window: the window keeps that tile's exact slot geometry
* (`top-5`, `left-0`, bleeding off the right and bottom edges,
* `rounded-tl-xl`) as an outlined shell - faint `--text-muted-inverse`
* hairlines with the dark tile showing through. Its `h-12` title bar
* pairs the Library icon (in an outlined `size-6` icon box, the
* agent-code header's treatment) with the run's workflow name and the
* run's total duration in mono on the right.
*
* Inside, the workspace trace view's vocabulary at tile scale: each span
* is a row with its block name (children indented and quieter, the real
* tree's depth ramp), a waterfall bar offset by when the span started
* and sized by how long it ran, and a right-aligned mono duration. The
* rows stamp in top to bottom once (from `run-trace-graphic.module.css`,
* the agent-code tile's one-shot settle); under `prefers-reduced-motion`
* the trace renders fully settled.
*/
export function RunTraceGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className={cn(
'absolute top-5 right-0 bottom-0 left-0 rounded-tl-xl border-t border-l',
OUTLINE_INK
)}
>
<div className={cn('flex h-12 items-center gap-2 border-b px-4', OUTLINE_INK)}>
<span
className={cn('flex size-6 items-center justify-center rounded-md border', OUTLINE_INK)}
>
<Library className='size-[14px] text-[var(--text-muted-inverse)]' />
</span>
<span className='min-w-0 flex-1 truncate font-medium text-[var(--text-inverse)] text-base'>
Support ticket routing
</span>
<span className='shrink-0 font-mono text-[var(--text-muted-inverse)] text-caption'>
1.86s
</span>
</div>
<div className='flex flex-col p-4'>
{TRACE_SPANS.map((span, index) => (
<div
key={span.name}
className={cn('flex h-9 items-center gap-3', ROW_STEP_CLASSES[index])}
>
<span
className={cn(
'w-[38%] shrink-0 truncate font-medium text-caption',
NAME_TONE_CLASS[span.barTone],
span.indentClass
)}
>
{span.name}
</span>
<span className='relative h-full min-w-0 flex-1'>
<span
className={cn(
'-translate-y-1/2 absolute top-1/2 h-[6px] rounded-full',
BAR_TONE_CLASS[span.barTone],
span.barClass
)}
/>
</span>
<span className='w-11 shrink-0 text-right font-mono text-[var(--text-muted-inverse)] text-caption'>
{span.duration}
</span>
</div>
))}
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,394 @@
'use client'
import { useState } from 'react'
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 { useMotionSafeCycle } from '@/app/(landing)/hooks/use-motion-safe-cycle'
/** Sidebar content for the logs hero - a team living in its run history. */
const SIDEBAR_CHATS = [
'Debug nightly sync failure',
'Cost of the support agent',
'Error rate this week',
'Export June run history',
] as const
/** Deployed workflows in the sidebar - the same names the runs table shows. */
const SIDEBAR_WORKFLOWS = [
'Support ticket routing',
'Lead enrichment',
'Invoice matching',
'Nightly data sync',
'Churn-risk alerts',
] as const
type LogStatus = 'completed' | 'error' | 'running'
interface LogRowData {
workflowName: string
date: string
status: LogStatus
cost: string
triggerLabel: string
duration: string
}
type BadgeVariant = BadgeProps['variant']
/** Status → Badge variant, matching the real Logs table's treatment. */
const STATUS_VARIANT: Record<LogStatus, BadgeVariant> = {
completed: 'gray-secondary',
error: 'gray',
running: 'gray',
}
const STATUS_LABELS: Record<LogStatus, string> = {
completed: 'Completed',
error: 'Error',
running: 'Running',
}
/**
* The run the loop watches land live: it fades in at the top of the table
* as "Running", then flips to "Completed" with its cost and duration.
*/
const LIVE_ROW = {
workflowName: 'Support ticket routing',
date: 'Jul 12 9:41 AM',
triggerLabel: 'Webhook',
runningCost: '-',
runningDuration: '-',
completedCost: '3 credits',
completedDuration: '1.8s',
} as const
/**
* The settled run history beneath the live row - eleven rows fill the
* design height under the title and options bars. Chat-triggered runs
* read "Sim agent" per the constitution's run-log rule.
*/
const HISTORY_ROWS: readonly LogRowData[] = [
{
workflowName: 'Lead enrichment',
date: 'Jul 12 9:12 AM',
status: 'completed',
cost: '2 credits',
triggerLabel: 'API',
duration: '2.4s',
},
{
workflowName: 'Nightly data sync',
date: 'Jul 12 2:14 AM',
status: 'error',
cost: '1 credit',
triggerLabel: 'Schedule',
duration: '38.2s',
},
{
workflowName: 'Invoice matching',
date: 'Jul 11 6:48 PM',
status: 'completed',
cost: '4 credits',
triggerLabel: 'Sim agent',
duration: '5.3s',
},
{
workflowName: 'Support ticket routing',
date: 'Jul 11 4:02 PM',
status: 'completed',
cost: '3 credits',
triggerLabel: 'Webhook',
duration: '1.7s',
},
{
workflowName: 'Churn-risk alerts',
date: 'Jul 11 9:00 AM',
status: 'completed',
cost: '5 credits',
triggerLabel: 'Schedule',
duration: '11.9s',
},
{
workflowName: 'Lead enrichment',
date: 'Jul 11 8:31 AM',
status: 'completed',
cost: '2 credits',
triggerLabel: 'API',
duration: '2.1s',
},
{
workflowName: 'Weekly digest',
date: 'Jul 11 7:00 AM',
status: 'completed',
cost: '6 credits',
triggerLabel: 'Schedule',
duration: '24.6s',
},
{
workflowName: 'Invoice matching',
date: 'Jul 10 5:19 PM',
status: 'completed',
cost: '4 credits',
triggerLabel: 'Manual',
duration: '4.8s',
},
{
workflowName: 'Support ticket routing',
date: 'Jul 10 2:44 PM',
status: 'completed',
cost: '3 credits',
triggerLabel: 'Webhook',
duration: '2.0s',
},
{
workflowName: 'Nightly data sync',
date: 'Jul 10 2:14 AM',
status: 'completed',
cost: '1 credit',
triggerLabel: 'Schedule',
duration: '31.5s',
},
{
workflowName: 'Churn-risk alerts',
date: 'Jul 9 9:00 AM',
status: 'completed',
cost: '5 credits',
triggerLabel: 'Schedule',
duration: '12.4s',
},
] as const
/** Column headers matching the real Logs table. */
const COL_HEADERS = ['Workflow', 'Date', 'Status', 'Cost', 'Trigger', 'Duration'] as const
/** Column widths matching the workspace Logs table's proportions. */
const COL_WIDTHS = ['22%', '18%', '13%', '15%', '14%', '18%'] as const
/** The empty table holds this long before history rows stamp in. */
const HISTORY_START_MS = 500
/** History row N fades in at HISTORY_START_MS + N * HISTORY_STEP_MS. */
const HISTORY_STEP_MS = 90
/** The live run appears at the top of the table as "Running" here. */
const LIVE_APPEAR_MS = 2400
/** The live run flips to "Completed" with cost and duration here. */
const LIVE_COMPLETE_MS = 4800
/** The finished table holds this long before the fade. */
const COMPLETED_HOLD_MS = 4400
type LiveState = 'hidden' | 'running' | 'completed'
interface LogsTableRowProps {
row: LogRowData
visible: boolean
}
/** One settled history row - fades in on its scheduled beat. */
function LogsTableRow({ row, visible }: LogsTableRowProps) {
return (
<tr
className={cn(
'h-[44px] transition-opacity duration-300 ease-out',
visible ? 'opacity-100' : 'opacity-0'
)}
>
<td className='px-6 align-middle'>
<div className='flex items-center gap-2'>
<Workflow className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-caption'>
{row.workflowName}
</span>
</div>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>{row.date}</td>
<td className='px-6 align-middle'>
<Badge variant={STATUS_VARIANT[row.status]} size='sm' dot>
{STATUS_LABELS[row.status]}
</Badge>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>{row.cost}</td>
<td className='px-6 align-middle'>
<Badge variant='gray-secondary' size='sm'>
{row.triggerLabel}
</Badge>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{row.duration}
</td>
</tr>
)
}
/**
* The logs hero's platform loop - the workflows editor loop's architecture
* (fixed 1280x735 design-space layer scaled to the window via ResizeObserver
* + `transform: scale`, a parent-owned clock, reduced-motion showing the
* finished frame) with the workspace pane replaced by a static rendering of
* the real Logs surface: the 44px title bar (Library icon, "Logs", Export,
* Logs/Dashboard tabs), the search/Filter/Sort options bar, and the runs
* table with Workflow / Date / Status / Cost / Trigger / Duration columns.
*
* The loop's beats: the run history stamps in row by row, a new run then
* fades in at the top as "Running" (its slot is reserved from the start, so
* nothing shifts), flips to "Completed" with its cost and duration, holds,
* fades, and restarts. Under `prefers-reduced-motion` the loop never
* starts: the finished table - live run completed - renders statically.
*
* Everything is `pointer-events-none` decorative, matching the hero's
* `aria-hidden` frame.
*/
export function LogsHeroLoop() {
const [historyCount, setHistoryCount] = useState(0)
const [liveState, setLiveState] = useState<LiveState>('hidden')
const [fading, setFading] = useState(false)
const [cycleId, setCycleId] = useState(0)
useMotionSafeCycle({
scheduleCycle: () => {
setFading(false)
setHistoryCount(0)
setLiveState('hidden')
setCycleId((c) => c + 1)
const totalMs = LIVE_COMPLETE_MS + COMPLETED_HOLD_MS
return {
timers: [
...HISTORY_ROWS.map((_, i) =>
setTimeout(() => setHistoryCount(i + 1), HISTORY_START_MS + i * HISTORY_STEP_MS)
),
setTimeout(() => setLiveState('running'), LIVE_APPEAR_MS),
setTimeout(() => setLiveState('completed'), LIVE_COMPLETE_MS),
setTimeout(() => setFading(true), totalMs - RESET_FADE_MS),
],
totalMs,
}
},
showFinished: () => {
setFading(false)
setHistoryCount(HISTORY_ROWS.length)
setLiveState('completed')
},
})
const liveCompleted = liveState === 'completed'
return (
<HeroLoopShell chats={SIDEBAR_CHATS} workflows={SIDEBAR_WORKFLOWS} activeNav='Logs'>
<div className='h-full w-full overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--bg)]'>
<div
key={cycleId}
className={cn(
'flex h-full w-full flex-col transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
<div className='flex h-[44px] flex-shrink-0 items-center border-[var(--border)] border-b px-6'>
<div className='flex w-full items-center justify-between'>
<div className='flex items-center gap-3'>
<Library className='size-[14px] text-[var(--text-icon)]' />
<span className='font-medium text-[var(--text-body)] text-sm'>Logs</span>
</div>
<div className='flex items-center gap-1'>
<span className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<Download className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Export
</span>
<span className='rounded-md bg-[var(--surface-active)] px-2 py-1 text-[var(--text-body)] text-caption'>
Logs
</span>
<span className='rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
Dashboard
</span>
</div>
</div>
</div>
<div className='flex-shrink-0 border-[var(--border)] border-b px-6 py-2.5'>
<div className='flex items-center justify-between'>
<div className='flex flex-1 items-center gap-2.5'>
<Search className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='flex-1 text-[var(--text-muted)] text-caption'>Search logs...</span>
</div>
<div className='flex items-center gap-1.5'>
<span className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ListFilter className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Filter
</span>
<span className='flex items-center rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption'>
<ArrowUpDown className='mr-1.5 size-[14px] text-[var(--text-icon)]' />
Sort
</span>
</div>
</div>
</div>
<div className='min-h-0 flex-1 overflow-hidden'>
<table className='w-full table-fixed text-sm'>
<colgroup>
{COL_WIDTHS.map((width, index) => (
<col key={index} style={{ width }} />
))}
</colgroup>
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{COL_HEADERS.map((label) => (
<th
key={label}
className='h-10 px-6 py-1.5 text-left align-middle font-normal text-[var(--text-muted)] text-caption'
>
{label}
</th>
))}
</tr>
</thead>
<tbody>
<tr
className={cn(
'h-[44px] transition-opacity duration-300 ease-out',
liveState === 'hidden' ? 'opacity-0' : 'opacity-100'
)}
>
<td className='px-6 align-middle'>
<div className='flex items-center gap-2'>
<Workflow className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-caption'>
{LIVE_ROW.workflowName}
</span>
</div>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{LIVE_ROW.date}
</td>
<td className='px-6 align-middle'>
<Badge variant={liveCompleted ? 'gray-secondary' : 'gray'} size='sm' dot>
{liveCompleted ? 'Completed' : 'Running'}
</Badge>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{liveCompleted ? LIVE_ROW.completedCost : LIVE_ROW.runningCost}
</td>
<td className='px-6 align-middle'>
<Badge variant='gray-secondary' size='sm'>
{LIVE_ROW.triggerLabel}
</Badge>
</td>
<td className='px-6 align-middle text-[var(--text-secondary)] text-caption'>
{liveCompleted ? LIVE_ROW.completedDuration : LIVE_ROW.runningDuration}
</td>
</tr>
{HISTORY_ROWS.map((row, index) => (
<LogsTableRow
key={`${row.workflowName}-${row.date}`}
row={row}
visible={index < historyCount}
/>
))}
</tbody>
</table>
</div>
</div>
</div>
</HeroLoopShell>
)
}
+174
View File
@@ -0,0 +1,174 @@
import {
PlatformHeroVisual,
SolutionsPage,
type SolutionsPageConfig,
} from '@/app/(landing)/components'
import {
AuditTrailGraphic,
RunMonitoringGraphic,
StagingGraphic,
} from '@/app/(landing)/enterprise/components/feature-graphics'
import {
FailureAlertGraphic,
FilterRunsGraphic,
RunTraceGraphic,
} from '@/app/(landing)/logs/components/feature-graphics'
import { LogsHeroLoop } from '@/app/(landing)/logs/components/logs-hero-loop'
/**
* Logs platform page - a consumer of {@link SolutionsPage} rendered with
* the enterprise page's feature-tile treatment.
*
* The whole page is one typed {@link SolutionsPageConfig} rendered inside
* the shared route-group layout chrome: identity (for structured data), a
* hero carrying the live Logs table loop, and two rows of three feature
* tiles. Where the workflows page's "Trace every run" card introduces run
* tracing in passing, this page goes deeper on the same story: the trace
* itself (a new block-by-block waterfall vignette), live run details, the
* run record, search and filters, failure debugging (the staging surface
* retold as a failed run's snapshot), and alerting (a new failed-run →
* Slack-alert vignette).
*
* The JSON-LD emitted by {@link SolutionsPage} is structurally identical
* to the platform page's (`WebPage` + `BreadcrumbList` +
* `WebApplication`), so the feature-tile treatment is SEO-neutral.
*/
const LOGS_CONFIG: SolutionsPageConfig = {
module: 'Logs',
path: '/logs',
hero: {
eyebrow: 'Logs',
heading: 'Trace every agent decision, block by block, in Sim.',
description:
'Logs give teams full visibility into every run in Sim, the open-source AI workspace. Follow each agent run block by block, filter and search across every run, and catch failures the moment they happen.',
summary:
'Logs is the visibility layer in Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Sim records every agent run block by block, so teams trace each decision, tool call, and output, filter and search across every run, and catch failures with alerts.',
visual: (
<PlatformHeroVisual>
<LogsHeroLoop />
</PlatformHeroVisual>
),
},
rows: [
{
id: 'trace',
title: 'See exactly what every agent did.',
subtitle:
'Sim records every agent run block by block, so teams can follow each decision, tool call, and output on one timeline.',
cta: { label: 'Explore Logs in Sim', href: '/signup' },
cards: [
{
title: 'Trace runs block by block',
description:
'Sim captures every step of a run, each block, tool call, and model response with its duration, so no agent decision is a black box.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: <RunTraceGraphic />,
},
{
title: 'Watch runs as they happen',
description:
'Sim shows each run the moment it starts, with its trigger, duration, and output, so teams see agents working in real time.',
visual: (
<RunMonitoringGraphic
fields={[
{ label: 'Workflow', value: 'Support ticket routing', variant: 'strong' },
{ label: 'Status', value: 'Completed', variant: 'chip' },
{ label: 'Latency', value: '1.86s', variant: 'mono' },
{ label: 'Tokens', value: '8.4K', variant: 'mono' },
]}
outputPairs={[
{ key: 'status', value: '"completed"' },
{ key: 'tokens', value: '8412' },
]}
/>
),
},
{
title: 'Every run on the record',
description:
'Sim keeps a permanent history of every run and follow-up, so teams can answer what happened, when, and who resolved it.',
visual: (
<AuditTrailGraphic
entries={[
{
action: 'Error resolved',
actor: 'Maya Chen',
resource: 'Nightly data sync',
time: 'Now',
avatar: '/landing/team-avatar-1.jpg',
},
{
action: 'Run traced',
actor: 'Jordan Lee',
resource: 'Invoice matching · run 8f2a',
time: '12 min ago',
avatar: '/landing/team-avatar-2.jpg',
},
{
action: 'Logs exported',
actor: 'Sam Ortiz',
resource: 'June runs · CSV',
time: '1h ago',
avatar: '/landing/team-avatar-3.jpg',
},
{
action: 'Alert acknowledged',
actor: 'Maya Chen',
resource: 'Churn-risk alerts',
time: 'Jul 8',
avatar: '/landing/team-avatar-1.jpg',
},
]}
/>
),
},
],
},
{
id: 'catch',
title: 'Catch failures before they spread.',
subtitle:
'Sim surfaces failed runs the moment they happen, with the alerts and context teams need to fix them fast.',
cta: { label: 'Start tracing runs', href: '/signup' },
cards: [
{
title: 'Filter to the runs that matter',
description:
'Search and filter every run in Sim by workflow, status, trigger, and time, so one failure is never buried under a thousand successes.',
visual: <FilterRunsGraphic />,
},
{
title: 'Debug with full context',
description:
'Sim snapshots each run, so teams open a failed run with its inputs, outputs, and trace intact and see exactly which block broke.',
visual: (
<StagingGraphic
title='Nightly data sync'
headerTag='run 8f2a'
changeTag='error'
changeTitle='Timeout in Post to ledger'
attribution='Maya Chen · reviewing now'
checks={['Inputs captured', 'Output snapshot saved', 'Trace preserved']}
fromLabel='Error'
toLabel='Resolved'
actionLabel='Review run'
/>
),
},
{
title: 'Catch failures with alerting',
description:
'When a run fails, Sim alerts the channel your team already watches, naming the failing block and linking straight to the trace.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: <FailureAlertGraphic />,
},
],
},
],
}
export default function Logs() {
return <SolutionsPage config={LOGS_CONFIG} />
}
+20
View File
@@ -0,0 +1,20 @@
import { buildLandingMetadata } from '@/lib/landing/seo'
import Logs from '@/app/(landing)/logs/logs'
export const revalidate = 3600
const TITLE = 'Logs | Trace Every Agent Run in Sim, the AI Workspace'
const DESCRIPTION =
'Logs is the visibility layer in Sim, the open-source AI workspace. Trace every agent run block by block, filter and search across runs, and catch failures with alerts.'
export const metadata = buildLandingMetadata({
title: TITLE,
description: DESCRIPTION,
path: '/logs',
keywords:
'AI workspace, AI agent logs, agent observability, trace agent runs, LLM run logs, AI agent monitoring, workflow run history, open-source AI agent platform',
})
export default function Page() {
return <Logs />
}
@@ -0,0 +1,117 @@
'use client'
import { AgentIcon, GmailIcon, GoogleSheetsIcon, ScheduleIcon, SlackIcon } from '@/components/icons'
import { EditorLoop, type EditorLoopContent } from '@/app/(landing)/components/shared/editor-loop'
/**
* The scheduled-tasks hero's content for the shared {@link EditorLoop}: a
* recurring-ops workspace sidebar and the complete scheduled digest workflow -
* a schedule trigger holding the cadence, the digest agent, and a three-way
* fan-out to Slack, Gmail, and Sheets. The schedule trigger is the block the
* "editing" beat selects, since the cadence is this page's story. Colors
* follow the stage convention - grey ramp for platform blocks, brand tiles
* only for real third-party marks (multicolor Gmail/Sheets glyphs sit on
* white tiles with a hairline, the Jira treatment). Blocks are ordered by
* build sequence; an edge draws once both endpoints are on canvas.
*/
const SCHEDULED_TASKS_EDITOR_CONTENT: EditorLoopContent = {
activeNav: 'Scheduled tasks',
sidebarChats: [
'Morning digest setup',
'Move sync to nightly',
'Weekly KPI report',
'Retry failed runs',
],
sidebarWorkflows: [
'Morning digest',
'Nightly data sync',
'Weekly KPI report',
'Invoice sweep',
'Churn-risk alerts',
],
blocks: [
{
id: 'schedule',
name: 'Schedule',
icon: ScheduleIcon,
bgColor: 'var(--text-muted)',
isTrigger: true,
rows: [
{ title: 'Cadence', value: 'Weekdays' },
{ title: 'Time', value: '9:00 AM PT' },
],
x: 555,
y: 20,
},
{
id: 'agent',
name: 'Digest agent',
icon: AgentIcon,
bgColor: 'var(--text-primary)',
rows: [
{ title: 'Messages', value: '-' },
{ title: 'Model', value: '-' },
],
x: 555,
y: 280,
},
{
id: 'slack',
name: 'Post to Slack',
icon: SlackIcon,
bgColor: '#611F69',
isTerminal: true,
rows: [
{ title: 'Channel', value: '-' },
{ title: 'Message', value: '-' },
],
x: 100,
y: 560,
},
{
id: 'gmail',
name: 'Send by email',
icon: GmailIcon,
bgColor: '#FFFFFF',
tileBorder: true,
isTerminal: true,
rows: [
{ title: 'To', value: '-' },
{ title: 'Subject', value: '-' },
],
x: 555,
y: 560,
},
{
id: 'sheets',
name: 'Append to Sheets',
icon: GoogleSheetsIcon,
bgColor: '#FFFFFF',
tileBorder: true,
isTerminal: true,
rows: [
{ title: 'Spreadsheet', value: '-' },
{ title: 'Range', value: '-' },
],
x: 1010,
y: 560,
},
],
edges: [
['schedule', 'agent'],
['agent', 'slack'],
['agent', 'gmail'],
['agent', 'sheets'],
],
canvas: { width: 1360, height: 780 },
selectedBlockId: 'schedule',
}
/**
* The scheduled-tasks hero's editor loop - the shared {@link EditorLoop}
* replaying the morning-digest workflow with the schedule trigger as the
* "being edited" beat, the cadence being this page's story.
*/
export function ScheduledTasksHeroLoop() {
return <EditorLoop content={SCHEDULED_TASKS_EDITOR_CONTENT} />
}
@@ -0,0 +1,20 @@
import { buildLandingMetadata } from '@/lib/landing/seo'
import ScheduledTasks from '@/app/(landing)/scheduled-tasks/scheduled-tasks'
export const revalidate = 3600
const TITLE = 'Scheduled Tasks | Run Agents on a Cadence in Sim, the AI Workspace'
const DESCRIPTION =
'Scheduled Tasks runs your AI agents on a cadence in Sim, the open-source AI workspace. Schedule any workflow from 15-minute intervals to monthly or custom cron, timezone-aware, with every run traced.'
export const metadata = buildLandingMetadata({
title: TITLE,
description: DESCRIPTION,
path: '/scheduled-tasks',
keywords:
'AI workspace, scheduled AI agents, cron AI workflows, recurring agent runs, AI task scheduler, schedule workflow automation, open-source AI agent platform, agentic workflows',
})
export default function Page() {
return <ScheduledTasks />
}
@@ -0,0 +1,192 @@
import {
PlatformHeroVisual,
SolutionsPage,
type SolutionsPageConfig,
} from '@/app/(landing)/components'
import {
AuditTrailGraphic,
DeployGraphic,
ItPlatformTeamsGraphic,
OperationsTeamsGraphic,
RunMonitoringGraphic,
} from '@/app/(landing)/enterprise/components/feature-graphics'
import { ScheduledTasksHeroLoop } from '@/app/(landing)/scheduled-tasks/components/scheduled-tasks-hero-loop'
import { DocumentDraftGraphic } from '@/app/(landing)/solutions/components/feature-graphics'
/**
* Scheduled Tasks platform page - a consumer of {@link SolutionsPage}
* rendered with the enterprise page's feature-tile treatment.
*
* The page tells the product's real scheduling story: workflows carry
* schedule triggers (from every-15-minutes intervals through daily, weekly,
* and monthly cadences to custom cron, timezone-aware), and the workspace's
* Scheduled Tasks calendar shows every upcoming and finished run with
* pause/resume and full run history. Every visual slot reuses a
* parametrized enterprise feature graphic retold for recurring runs - the
* policy checklist as a schedule card, the routing fan as the delivery
* fan-out, the drafted document as the digest that arrives on time, the
* monitoring panel, deploy window, and audit ledger for the operate story.
*
* The JSON-LD emitted by {@link SolutionsPage} is structurally identical to
* the workflows page's (`WebPage` + `BreadcrumbList` + `WebApplication`).
*/
const SCHEDULED_TASKS_CONFIG: SolutionsPageConfig = {
module: 'Scheduled Tasks',
path: '/scheduled-tasks',
hero: {
eyebrow: 'Scheduled Tasks',
heading: 'Set agents to run on a schedule, and let the work happen on its own.',
description:
'Scheduled Tasks runs your agents on a cadence in Sim, the open-source AI workspace. Pick a time from every 15 minutes to monthly or write a cron, timezone-aware, with every run traced.',
summary:
'Scheduled Tasks is the scheduler in Sim, the open-source AI workspace where teams build, deploy, and manage AI agents. Put any workflow on a cadence, from 15-minute intervals to daily, weekly, and monthly runs or custom cron, timezone-aware, then watch every scheduled run land on the workspace calendar with full run history.',
visual: (
<PlatformHeroVisual>
<ScheduledTasksHeroLoop />
</PlatformHeroVisual>
),
},
rows: [
{
id: 'schedule',
title: 'Put any agent on a cadence.',
subtitle:
'Sim turns a workflow into a scheduled task with one trigger, from 15-minute intervals to monthly closes, in your timezone.',
cta: { label: 'Schedule your first agent', href: '/signup' },
cards: [
{
title: 'Pick the cadence',
description:
'Choose minutes, hourly, daily, weekly, or monthly, or write a cron expression for exact control.',
visual: (
<ItPlatformTeamsGraphic
title='Schedule'
badgeLabel='Recurring'
cardTitle='Morning digest'
cardSubtitle='Weekdays at 9:00 AM'
cardTag='Active'
controls={[
{ label: 'Timezone', detail: 'US Pacific' },
{ label: 'Custom cron', detail: 'Supported' },
{ label: 'Pause anytime', detail: 'One click' },
]}
/>
),
},
{
title: 'Fan out on every run',
description:
'Each run pulls from your systems and delivers wherever the work lives, without anyone pressing go.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: (
<OperationsTeamsGraphic
sourceLabels={['Gmail', 'Stripe', 'Linear']}
destinationLabels={['Slack', 'Email', 'Sheets']}
/>
),
},
{
title: 'Wake up to the output',
description:
'The digest, report, or sync is done before the day starts. Sim delivers it where your team looks first.',
visual: (
<DocumentDraftGraphic
title='Monday digest'
statusTag='On schedule'
footerLabel='Delivered to #ops'
footerDetail='9:00 AM'
/>
),
},
],
},
{
id: 'operate',
title: 'Stay on top of every run.',
subtitle:
'Scheduled runs land on the workspace calendar, and Sim keeps the full history so nothing happens silently.',
cta: { label: 'See runs in action', href: '/signup' },
cards: [
{
title: 'Trace each scheduled run',
description:
'Every run is logged block by block, with the trigger, duration, and output a click away.',
visual: (
<RunMonitoringGraphic
fields={[
{ label: 'Workflow', value: 'Morning digest', variant: 'strong' },
{ label: 'Run ID', value: 'a7c93e41', variant: 'chip' },
{ label: 'Trigger', value: 'Schedule', variant: 'chip' },
{ label: 'Duration', value: '3.4s', variant: 'mono' },
]}
outputPairs={[
{ key: 'delivered', value: 'true' },
{ key: 'items', value: '12' },
]}
/>
),
},
{
title: 'Keep it running',
description:
'Schedules stay live in production. Pause, resume, or retune the cadence without redeploying.',
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: (
<DeployGraphic
agentName='Digest agent'
versionTag='v8'
url='sim.ai/agents/digest'
statusLabel='On schedule · live'
timeLabel='Next run 9:00 AM'
/>
),
},
{
title: 'Keep the history',
description:
'Every run and every schedule change lands in one record, so teams see what ran and when.',
visual: (
<AuditTrailGraphic
entries={[
{
action: 'Run completed',
actor: 'Morning digest',
resource: '12 items delivered',
time: 'Now',
avatar: '/landing/team-avatar-1.jpg',
},
{
action: 'Schedule updated',
actor: 'Maya Chen',
resource: 'Weekdays · 9:00 AM',
time: '2h ago',
avatar: '/landing/team-avatar-2.jpg',
},
{
action: 'Sync completed',
actor: 'Nightly data sync',
resource: '4,120 rows',
time: '1d ago',
avatar: '/landing/team-avatar-3.jpg',
},
{
action: 'Schedule created',
actor: 'Jordan Lee',
resource: 'Morning digest',
time: 'Jun 2',
avatar: '/landing/team-avatar-1.jpg',
},
]}
/>
),
},
],
},
],
}
export default function ScheduledTasks() {
return <SolutionsPage config={SCHEDULED_TASKS_CONFIG} />
}
@@ -1,23 +1,45 @@
import { SolutionsPage, type SolutionsPageConfig } from '@/app/(landing)/components'
import {
PlatformHeroVisual,
SolutionsPage,
type SolutionsPageConfig,
} from '@/app/(landing)/components'
import {
AccessControlGraphic,
AuditTrailGraphic,
ItPlatformTeamsGraphic,
StandardsGraphic,
TechnicalTeamsGraphic,
} from '@/app/(landing)/enterprise/components/feature-graphics'
import { ComplianceHeroLoop } from '@/app/(landing)/solutions/compliance/components/compliance-hero-loop'
import { DocumentDraftGraphic } from '@/app/(landing)/solutions/components/feature-graphics'
/**
* Compliance solution page - a reference consumer of {@link SolutionsPage}.
* Compliance solution page - a consumer of {@link SolutionsPage} rendered
* with the enterprise page's feature-tile treatment.
*
* The whole page is one typed {@link SolutionsPageConfig} rendered inside the
* shared route-group layout's chrome. Visual slots are `null`, so each renders the
* layout's reserved placeholder panel; a real page swaps in its own client
* island without touching the layout.
* The whole page is one typed {@link SolutionsPageConfig} rendered inside
* the shared route-group layout's chrome. Every visual slot carries an
* enterprise feature graphic - reused directly where its story fits the
* card (the audit ledger, the access role graph) or retold through the
* graphics' content props for compliance's use cases (evidence schedules,
* framework monitoring, policy review) - so the page shares the enterprise
* design language without any new visual vocabulary.
*/
const COMPLIANCE_CONFIG: SolutionsPageConfig = {
module: 'Compliance',
path: '/solutions/compliance',
hero: {
eyebrow: 'Compliance',
heading: 'Automate evidence, control checks, and audit reports with Sim agents.',
description:
'Compliance teams build AI agents in Sim, the open-source AI workspace, that monitor controls and assemble a clean, defensible record, keeping the organization continuously audit-ready instead of scrambling once a year.',
summary:
'Sim is the open-source AI workspace where compliance teams build, deploy, and manage AI agents that automate evidence collection, control monitoring, and reporting, keeping the organization continuously audit-ready across 1,000+ integrations.',
visual: null,
visual: (
<PlatformHeroVisual>
<ComplianceHeroLoop />
</PlatformHeroVisual>
),
},
rows: [
{
@@ -30,18 +52,50 @@ const COMPLIANCE_CONFIG: SolutionsPageConfig = {
title: 'Collect evidence',
description:
'Sim gathers screenshots, logs, and records from your systems on a schedule.',
visual: null,
visual: (
<ItPlatformTeamsGraphic
title='Evidence'
badgeLabel='On schedule'
cardTitle='SOC 2 · Q3 collection'
cardSubtitle='Runs weekly across systems'
cardTag='Running'
controls={[
{ label: 'Access logs exported', detail: 'Done' },
{ label: 'Configs snapshotted', detail: 'Done' },
{ label: 'Screenshots captured', detail: 'Queued' },
]}
/>
),
},
{
title: 'Monitor controls',
description: 'Sim continuously checks controls and flags drift the moment it appears.',
visual: null,
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: <StandardsGraphic rightLabel='ISO 27001' sealLabel='Monitored' />,
},
{
title: 'Check policies',
description:
'Sim reviews changes against your policies and raises anything out of bounds.',
visual: null,
visual: (
<TechnicalTeamsGraphic
diffLines={[
{ marker: ' ', code: "policy: 'data-retention'," },
{ marker: ' ', code: "scope: 'production'," },
{ marker: ' ', code: 'rules: [' },
{ marker: ' ', code: " encrypt: 'at-rest'," },
{ marker: '-', code: ' retainDays: 30,' },
{ marker: '+', code: ' retainDays: 90,' },
{ marker: ' ', code: ']' },
]}
reviewerName='Jordan Lee'
reviewerAction='Requested policy review'
verdictTag='Flagged'
footerLabel='Policy checks passed'
footerDetail='11 of 12'
/>
),
},
],
},
@@ -54,18 +108,25 @@ const COMPLIANCE_CONFIG: SolutionsPageConfig = {
{
title: 'Review access',
description: 'Sim runs periodic access reviews and routes exceptions for sign-off.',
visual: null,
visual: <AccessControlGraphic />,
},
{
title: 'Generate reports',
description:
'Sim assembles audit-ready reports from live evidence, not stale spreadsheets.',
visual: null,
visual: (
<DocumentDraftGraphic
title='SOC 2 report'
statusTag='Audit-ready'
footerLabel='Built from live evidence'
footerDetail='Today'
/>
),
},
{
title: 'Trace every action',
description: 'Sim logs every run block by block, so auditors see exactly what happened.',
visual: null,
visual: <AuditTrailGraphic />,
},
],
},
@@ -0,0 +1,117 @@
'use client'
import { AgentIcon, ConditionalIcon, ScheduleIcon, SlackIcon, TableIcon } from '@/components/icons'
import { EnterprisePlatformLoop } from '@/app/(landing)/enterprise/components/enterprise-platform-loop'
import type { EnterpriseLoopContent } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
/**
* Compliance content for the shared platform loop - an evidence-collection
* agent: the prompt asks for weekly SOC 2 evidence sweeps with drift flags,
* the sidebar reads like the GRC team's Brightwave workspace, and the staged
* workflow builds the evidence flow on the enterprise stage geometry. The
* trigger is a Schedule block (the prompt's "every Monday" cadence); the only
* brand tile is Slack's, where drift gets flagged.
*/
const COMPLIANCE_LOOP_CONTENT: EnterpriseLoopContent = {
workspaceName: 'Brightwave GRC',
greeting: 'What should we get done, Dana?',
placeholder: 'Ask Sim to automate a process.',
prompt:
'Every Monday, pull access logs and config changes from our core systems, check them against SOC 2 controls, and flag any drift for review.',
reply:
"On it. I'll collect the evidence every Monday, map it to your SOC 2 controls, and flag any drift for the team to review.",
sidebarChats: [
'SOC 2 evidence gaps',
'Vendor DPA renewals',
'Q3 access review',
'Policy update rollout',
],
sidebarWorkflows: [
'Evidence collection',
'Access review',
'Vendor risk scoring',
'Policy attestation',
'Audit trail export',
],
suggestedActions: [
'Collect this week\u2019s SOC 2 evidence',
'Review stale access grants',
'Summarize open audit findings',
'Draft the vendor risk report',
],
stageBlocks: [
{
id: 'schedule',
name: 'Schedule',
icon: ScheduleIcon,
bgColor: 'var(--text-muted)',
isTrigger: true,
rows: [{ title: 'Cadence', value: '-' }],
x: 155,
y: 12,
},
{
id: 'collect',
name: 'Collect evidence',
icon: AgentIcon,
bgColor: 'var(--text-primary)',
rows: [
{ title: 'Messages', value: '-' },
{ title: 'Model', value: '-' },
],
x: 155,
y: 172,
},
{
id: 'controls',
name: 'Check controls',
icon: ConditionalIcon,
bgColor: 'var(--text-secondary)',
rows: [{ title: 'Conditions', value: '-' }],
x: 155,
y: 372,
},
{
id: 'drift',
name: 'Flag drift',
icon: SlackIcon,
bgColor: '#611F69',
isTerminal: true,
rows: [
{ title: 'Channel', value: '-' },
{ title: 'Message', value: '-' },
],
x: 0,
y: 560,
},
{
id: 'ledger',
name: 'Evidence log',
icon: TableIcon,
bgColor: 'var(--text-body)',
isTerminal: true,
rows: [
{ title: 'Table', value: '-' },
{ title: 'Operation', value: '-' },
],
x: 310,
y: 560,
},
],
stageEdges: [
['schedule', 'collect'],
['collect', 'controls'],
['controls', 'drift'],
['controls', 'ledger'],
],
stageCanvas: { width: 560, height: 680 },
}
/**
* The compliance hero's platform loop - the shared enterprise loop replayed
* with the evidence-collection story above. Client wrapper so the icon-bearing
* content object never crosses a server/client boundary.
*/
export function ComplianceHeroLoop() {
return <EnterprisePlatformLoop content={COMPLIANCE_LOOP_CONTENT} />
}
@@ -0,0 +1,74 @@
/**
* The DocumentDraftGraphic's motion: the greeked prose bars draw in
* left-to-right one after another — a one-shot write, never re-typed,
* like the audit tile's stamp-in — and the header's status tag then
* carries the family's shared quiet 6s ring-pulse beat. Under
* prefers-reduced-motion everything renders fully drawn and static.
*/
.bar0,
.bar1,
.bar2,
.bar3,
.bar4 {
animation: document-draft-bar 0.6s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.bar0 {
animation-delay: 0.35s;
}
.bar1 {
animation-delay: 0.55s;
}
.bar2 {
animation-delay: 0.75s;
}
.bar3 {
animation-delay: 0.95s;
}
.bar4 {
animation-delay: 1.15s;
}
@keyframes document-draft-bar {
from {
opacity: 0;
transform: scaleX(0);
}
to {
opacity: 1;
transform: scaleX(1);
}
}
.statusPulse {
animation: document-draft-status-pulse 6s ease-out infinite;
}
@keyframes document-draft-status-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 30%, transparent);
}
36% {
box-shadow: 0 0 0 5px color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.bar0,
.bar1,
.bar2,
.bar3,
.bar4,
.statusPulse {
animation: none;
}
}
@@ -0,0 +1,106 @@
import { ChipTag, cn } from '@sim/emcn'
import { CircleCheck, File } from '@sim/emcn/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/solutions/components/feature-graphics/document-draft-graphic.module.css'
/**
* Per-line draft-bar widths, varied so the greeked page reads as prose —
* a short opening line, two full paragraph lines, then a second
* paragraph that trails off mid-line.
*/
const DRAFT_BAR_WIDTHS = ['w-[62%]', 'w-full', 'w-[86%]', 'w-[94%]', 'w-[54%]'] as const
/** Per-index draw classes — the stagger order is baked into each class's delay. */
const DRAFT_BAR_CLASSES = [styles.bar0, styles.bar1, styles.bar2, styles.bar3, styles.bar4] as const
/**
* A document being drafted by a Sim agent, told inside the lifecycle
* tile's outlined window chrome: the window is a 1px `--border-1`
* hairline outline with no fill (tile grey showing through), anchored to
* the text column's left edge (`left-0`, `top-5`) and bleeding off the
* right and bottom edges with a `rounded-tl-xl` top-left corner — the
* same slot geometry as the build, lifecycle, and rollback windows. The
* title bar pairs the document name (led by a `File` icon in the
* lifecycle header's outlined `size-6` icon box) with a right-aligned
* mono status tag on the grey-ground `--surface-6` pill fill, at the
* family's `h-12` header height.
*
* Inside, the draft is greeked: quiet `--surface-6` prose bars of varied
* measure draw in left-to-right one after another (from
* `document-draft-graphic.module.css` — a one-shot stagger like the
* audit tile's stamp-in, since the draft is written once, not re-typed),
* and the closing delivery row is the tile's highlight — a white card in
* the audit tile's exact chrome (`--white` fill, 1px `--border-1`
* hairline, `rounded-xl`, `shadow-sm`) pairing a passing circle-check
* and the delivery claim with a right-aligned timestamp. The header's
* status tag carries the family's shared quiet 6s ring pulse. Both
* motions are removed under `prefers-reduced-motion`.
*
* The window bleeds `2rem` past the tile's visible right edge (`1.5rem`
* under `max-lg`), so the header and body carry matching extra right
* padding (`pr-12 max-lg:pr-10`) to keep the status tag and the white
* card fully inside the visible tile — the rollback window's
* compensation.
*/
interface DocumentDraftGraphicProps {
/** Document name in the title bar. */
title?: string
/** Mono status tag on the title bar's right side. */
statusTag?: string
/** Delivery claim on the closing white card. */
footerLabel?: string
/** Right-aligned timestamp on the closing white card. */
footerDetail?: string
}
export function DocumentDraftGraphic({
title = 'Draft',
statusTag = 'Auto-drafted',
footerLabel = 'Ready for review',
footerDetail = 'Just now',
}: DocumentDraftGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute top-5 right-0 bottom-0 left-0 rounded-tl-xl border-[var(--border-1)] border-t border-l'
>
<div className='flex h-12 items-center gap-2 border-[var(--border-1)] border-b px-4 pr-12 max-lg:pr-10'>
<span className='flex size-6 shrink-0 items-center justify-center rounded-md border border-[var(--border-1)]'>
<File className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='min-w-0 flex-1 truncate font-medium text-[var(--text-primary)] text-base'>
{title}
</span>
<ChipTag
variant='mono'
className={cn('shrink-0 bg-[var(--surface-6)]', styles.statusPulse)}
>
{statusTag}
</ChipTag>
</div>
<div className='flex flex-col gap-3 p-4 pr-12 max-lg:pr-10'>
{DRAFT_BAR_WIDTHS.map((width, index) => (
<span
key={width}
className={cn(
'h-2 origin-left rounded-full bg-[var(--surface-6)]',
width,
DRAFT_BAR_CLASSES[index]
)}
/>
))}
<div className='mt-2 flex items-center gap-2 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm'>
<CircleCheck className='size-[13px] shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 flex-1 truncate font-medium text-[var(--text-primary)] text-small'>
{footerLabel}
</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>{footerDetail}</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,2 @@
export { DocumentDraftGraphic } from './document-draft-graphic'
export { KnowledgeAnswerGraphic } from './knowledge-answer-graphic'
@@ -0,0 +1,62 @@
/**
* The KnowledgeAnswerGraphic's motion: the exchange stamps in top to
* bottom — question, answer, then source card — the audit tile's
* one-shot settle from above, never re-played, and the source card's
* icon box then carries the family's shared quiet 6s ring-pulse beat.
* Under prefers-reduced-motion everything renders settled and static.
*/
.stepQuestion,
.stepAnswer,
.stepSource {
animation: knowledge-answer-stamp-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.stepQuestion {
animation-delay: 0.35s;
}
.stepAnswer {
animation-delay: 0.85s;
}
.stepSource {
animation-delay: 1.35s;
}
@keyframes knowledge-answer-stamp-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.sourcePulse {
animation: knowledge-answer-source-pulse 6s ease-out 1.8s infinite;
}
@keyframes knowledge-answer-source-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 30%, transparent);
}
36% {
box-shadow: 0 0 0 5px color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.stepQuestion,
.stepAnswer,
.stepSource,
.sourcePulse {
animation: none;
}
}
@@ -0,0 +1,105 @@
import { cn } from '@sim/emcn'
import { BookOpen } from '@sim/emcn/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/solutions/components/feature-graphics/knowledge-answer-graphic.module.css'
/**
* An internal help-desk agent answering from the team's own docs, told
* as a frameless chat vignette (the audit and monitoring tiles'
* composition — no window chrome): the employee's question sits
* right-aligned as a chat bubble in the family's white card chrome
* (`--white` fill, 1px `--border-1` hairline — the build tile's
* `--surface-3` bubble would vanish against this tile's own
* `--surface-3` fill), the agent's grounded answer reads back as plain
* `--text-primary` prose, and the tile's highlight is the citation — a
* white source card in the audit tile's exact chrome (`--white` fill,
* 1px `--border-1` hairline, `rounded-xl`, `shadow-sm`) pairing a
* `BookOpen` icon in the lifecycle header's outlined `size-6` icon box
* with the knowledge-base document the answer came from.
*
* Motion (from `knowledge-answer-graphic.module.css`): the exchange
* stamps in top to bottom one element after another — question, answer,
* then source — the audit tile's one-shot settle, never re-played — and
* the source card's icon box then carries the family's shared quiet 6s
* ring pulse to mark the grounding as the point. Both are removed under
* `prefers-reduced-motion`.
*
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center instead of the bled
* slot's center. The column is fluid (`w-full max-w-[312px]`) so it
* never exceeds the compensated slot at narrow tile widths — the source
* label truncates instead of clipping. On the wide spanned tile of the
* two-column band (container ≥500px inside `sm`..`lg`) the column
* relaxes to 400px so the exchange breathes into the wide slot.
*/
interface KnowledgeAnswerGraphicProps {
/** The employee's question bubble. */
question?: string
/** The agent's grounded answer. */
answer?: string
/** Document name on the source card. */
sourceLabel?: string
/** Attribution line beneath the source name. */
sourceDetail?: string
}
export function KnowledgeAnswerGraphic({
question = 'How do I reset my SSO password?',
answer = 'Head to id.acme.com, choose "Forgot password", and follow the email link — no ticket needed.',
sourceLabel = 'IT handbook',
sourceDetail = 'Answered from your docs',
}: KnowledgeAnswerGraphicProps = {}) {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='flex w-full max-w-[312px] flex-col gap-3 sm:max-lg:[@container(min-width:500px)]:max-w-[400px]'>
<div
className={cn(
'max-w-[85%] self-end rounded-lg border border-[var(--border-1)] bg-[var(--white)] px-3 py-2 text-[var(--text-primary)] text-caption leading-[1.5]',
styles.stepQuestion
)}
>
{question}
</div>
<p
className={cn(
'text-[var(--text-primary)] text-caption leading-[1.6]',
styles.stepAnswer
)}
>
{answer}
</p>
<div
className={cn(
'flex items-center gap-2.5 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm',
styles.stepSource
)}
>
<span
className={cn(
'flex size-6 shrink-0 items-center justify-center rounded-md border border-[var(--border-1)]',
styles.sourcePulse
)}
>
<BookOpen className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='min-w-0 flex-1'>
<span className='block truncate font-medium text-[var(--text-primary)] text-small'>
{sourceLabel}
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
{sourceDetail}
</span>
</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,119 @@
'use client'
import { AgentIcon, ConditionalIcon, JiraIcon, SlackIcon, StartIcon } from '@/components/icons'
import { EnterprisePlatformLoop } from '@/app/(landing)/enterprise/components/enterprise-platform-loop'
import type { EnterpriseLoopContent } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
/**
* Engineering content for the shared platform loop - an on-call triage agent:
* the prompt asks for alert triage into Jira and Slack, the sidebar reads like
* the engineering team's Brightwave workspace, and the staged workflow builds
* the alert-triage flow on the enterprise stage geometry (spine at x=155,
* terminals fanned at y=560). Colors follow the stage convention - grey ramp
* for platform blocks, brand tiles only for real third-party marks (Slack's
* plum, Jira's mark on a hairlined white tile).
*/
const ENGINEERING_LOOP_CONTENT: EnterpriseLoopContent = {
workspaceName: 'Brightwave Engineering',
greeting: 'What should we get done, Alex?',
placeholder: 'Ask Sim to automate a process.',
prompt:
'When a PagerDuty alert fires, pull recent deploys and error traces, triage severity, and file a Jira issue with the context on-call needs.',
reply:
"On it. I'll triage each alert against recent deploys and traces, set severity, and file the Jira issue with full context for on-call.",
sidebarChats: [
'Flaky deploy pipeline',
'PR review backlog',
'Incident 4213 postmortem',
'Staging env access',
],
sidebarWorkflows: [
'Code review agent',
'On-call triage',
'Release notes drafts',
'CI failure triage',
'Runbook sync',
],
suggestedActions: [
'Review open pull requests in GitHub',
'Triage overnight PagerDuty alerts',
'Summarize failing CI runs',
'Draft release notes for v2.31',
],
stageBlocks: [
{
id: 'start',
name: 'Start',
icon: StartIcon,
bgColor: 'var(--text-muted)',
isTrigger: true,
rows: [{ title: 'Inputs', value: '-' }],
x: 155,
y: 12,
},
{
id: 'triage',
name: 'Triage alert',
icon: AgentIcon,
bgColor: 'var(--text-primary)',
rows: [
{ title: 'Messages', value: '-' },
{ title: 'Model', value: '-' },
],
x: 155,
y: 172,
},
{
id: 'severity',
name: 'Set severity',
icon: ConditionalIcon,
bgColor: 'var(--text-secondary)',
rows: [{ title: 'Conditions', value: '-' }],
x: 155,
y: 372,
},
{
id: 'oncall',
name: 'Notify on-call',
icon: SlackIcon,
bgColor: '#611F69',
isTerminal: true,
rows: [
{ title: 'Channel', value: '-' },
{ title: 'Message', value: '-' },
],
x: 0,
y: 560,
},
{
id: 'jira',
name: 'File Jira issue',
icon: JiraIcon,
bgColor: '#FFFFFF',
tileBorder: true,
isTerminal: true,
rows: [
{ title: 'Project', value: '-' },
{ title: 'Summary', value: '-' },
],
x: 310,
y: 560,
},
],
stageEdges: [
['start', 'triage'],
['triage', 'severity'],
['severity', 'oncall'],
['severity', 'jira'],
],
stageCanvas: { width: 560, height: 680 },
}
/**
* The engineering hero's platform loop - the shared enterprise loop replayed
* with the on-call triage story above. Client wrapper so the icon-bearing
* content object never crosses a server/client boundary.
*/
export function EngineeringHeroLoop() {
return <EnterprisePlatformLoop content={ENGINEERING_LOOP_CONTENT} />
}
@@ -1,23 +1,45 @@
import { SolutionsPage, type SolutionsPageConfig } from '@/app/(landing)/components'
import {
PlatformHeroVisual,
SolutionsPage,
type SolutionsPageConfig,
} from '@/app/(landing)/components'
import {
DeployGraphic,
OperationsTeamsGraphic,
RunMonitoringGraphic,
StagingGraphic,
TechnicalTeamsGraphic,
} from '@/app/(landing)/enterprise/components/feature-graphics'
import { DocumentDraftGraphic } from '@/app/(landing)/solutions/components/feature-graphics'
import { EngineeringHeroLoop } from '@/app/(landing)/solutions/engineering/components/engineering-hero-loop'
/**
* Engineering solution page - a reference consumer of {@link SolutionsPage}.
* Engineering solution page - a consumer of {@link SolutionsPage} rendered
* with the enterprise page's feature-tile treatment.
*
* The whole page is one typed {@link SolutionsPageConfig} rendered inside the
* shared route-group layout's chrome. Visual slots are `null`, so each renders the
* layout's reserved placeholder panel; a real page swaps in its own client
* island without touching the layout.
* The whole page is one typed {@link SolutionsPageConfig} rendered inside
* the shared route-group layout's chrome. Every visual slot carries an
* enterprise feature graphic - reused directly where its story fits the
* card (the code-review diff, the promotion window) or retold through the
* graphics' content props for engineering's use cases (on-call routing,
* CI/CD deploys, runbook docs) - so the page shares the enterprise design
* language without any new visual vocabulary.
*/
const ENGINEERING_CONFIG: SolutionsPageConfig = {
module: 'Engineering',
path: '/solutions/engineering',
hero: {
eyebrow: 'Engineering',
heading: 'Automate code review, on-call, and docs with Sim agents.',
description:
'Engineering teams build AI agents in Sim, the open-source AI workspace, wired into GitHub, CI/CD, and 1,000+ integrations, visually, conversationally, or with code.',
summary:
'Sim is the open-source AI workspace where engineering teams build, deploy, and manage AI agents across the software lifecycle, automating code review, on-call triage, and documentation, wired into GitHub, CI/CD, and 1,000+ integrations.',
visual: null,
visual: (
<PlatformHeroVisual>
<EngineeringHeroLoop />
</PlatformHeroVisual>
),
},
rows: [
{
@@ -30,18 +52,32 @@ const ENGINEERING_CONFIG: SolutionsPageConfig = {
title: 'Review pull requests',
description:
'Sim agents review diffs, flag risks, and leave inline comments before a human looks.',
visual: null,
visual: <TechnicalTeamsGraphic />,
},
{
title: 'Triage on-call',
description:
'Sim reads alerts, gathers context, and proposes a fix so on-call starts ahead.',
visual: null,
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: (
<OperationsTeamsGraphic
sourceLabels={['PagerDuty', 'Datadog', 'Sentry']}
destinationLabels={['Slack', 'Jira', 'GitHub']}
/>
),
},
{
title: 'Generate docs',
description: 'Sim keeps READMEs and runbooks in sync with the code as it changes.',
visual: null,
visual: (
<DocumentDraftGraphic
title='runbook.md'
statusTag='Synced'
footerLabel='Updated from main'
footerDetail='Just now'
/>
),
},
],
},
@@ -54,19 +90,53 @@ const ENGINEERING_CONFIG: SolutionsPageConfig = {
{
title: 'GitHub and GitLab',
description: 'Sim agents act on issues, PRs, and releases across your repositories.',
visual: null,
visual: (
<StagingGraphic
title='Pull request'
headerTag='#482'
changeTag='b7e2f19'
changeTitle='Add retry to webhook handler'
attribution='Sim agent · 5m ago'
checks={['CI checks passed', 'Review approved', 'No merge conflicts']}
fromLabel='Feature'
toLabel='Main'
actionLabel='Merge'
/>
),
},
{
title: 'CI/CD pipelines',
description:
'Sim triggers on builds and deploys, so agents react the moment something ships.',
visual: null,
featureTileTone: 'dark',
featureTileDescriptionTone: 'soft',
visual: (
<DeployGraphic
agentName='Deploy agent'
versionTag='v12'
url='sim.ai/agents/deploy'
statusLabel='Build passed · live'
/>
),
},
{
title: 'Observability',
description:
'Sim pulls from your logs and traces to give agents real production context.',
visual: null,
visual: (
<RunMonitoringGraphic
fields={[
{ label: 'Workflow', value: 'On-call triage', variant: 'strong' },
{ label: 'Run ID', value: 'c4e81b2a', variant: 'chip' },
{ label: 'Trigger', value: 'Alert', variant: 'chip' },
{ label: 'Duration', value: '2.14s', variant: 'mono' },
]}
outputPairs={[
{ key: 'severity', value: '"low"' },
{ key: 'resolved', value: 'true' },
]}
/>
),
},
],
},
@@ -0,0 +1,69 @@
/**
* The ReconcileGraphic's motion: the matched rows stamp in top to bottom
* one after another (the audit tile's one-shot settle — a match is
* confirmed once, never re-played), the exception card lands last, and
* its Flagged tag then carries the family's shared quiet 6s ring-pulse
* beat. Under prefers-reduced-motion everything renders settled and
* static.
*/
.row0,
.row1,
.row2,
.exceptionIn {
animation: reconcile-stamp-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) backwards;
}
.row0 {
animation-delay: 0.35s;
}
.row1 {
animation-delay: 0.6s;
}
.row2 {
animation-delay: 0.85s;
}
.exceptionIn {
animation-delay: 1.2s;
}
@keyframes reconcile-stamp-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.flaggedPulse {
animation: reconcile-flagged-pulse 6s ease-out 1.6s infinite;
}
@keyframes reconcile-flagged-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 30%, transparent);
}
36% {
box-shadow: 0 0 0 5px color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 0%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.row0,
.row1,
.row2,
.exceptionIn,
.flaggedPulse {
animation: none;
}
}
@@ -0,0 +1,121 @@
import { ChipTag, cn } from '@sim/emcn'
import { CircleCheck } from '@sim/emcn/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
import styles from '@/app/(landing)/solutions/finance/components/feature-graphics/reconcile-graphic.module.css'
interface MatchedRow {
/** Transaction label. */
label: string
/** Right-aligned amount. */
amount: string
}
/** The auto-matched transactions, quiet passing rows above the exception. */
const MATCHED_ROWS: readonly MatchedRow[] = [
{ label: 'Stripe payout', amount: '$8,214.20' },
{ label: 'Payroll run', amount: '$42,900.00' },
{ label: 'AWS invoice', amount: '$3,118.75' },
] as const
/** Per-index stamp-in classes — the stagger order is baked into each class's delay. */
const ROW_STEP_CLASSES = [styles.row0, styles.row1, styles.row2] as const
/**
* Account reconciliation told as a frameless match ledger (the audit and
* monitoring tiles' composition — no window chrome): a small
* "Reconciliation" header with a mono period tag (fill stepped up to
* `--surface-6` so the pill stays legible on the grey ground) sits
* directly on the tile above the matched transactions — quiet
* hairline-ruled rows, each a passing circle-check, the transaction
* label, and a right-aligned mono amount. The tile's highlight is the
* one exception: a white card in the audit tile's exact chrome
* (`--white` fill, 1px `--border-1` hairline, `rounded-xl`, `shadow-sm`)
* pairing the unmatched transaction and its routed-for-review line with
* a `Flagged` tag that carries the tile's one repeating motion, the
* family's shared quiet 6s ring pulse.
*
* Motion (from `reconcile-graphic.module.css`): the matched rows stamp
* in top to bottom one after another — a one-shot settle, the audit
* tile's append vocabulary, since matches are confirmed once — then the
* exception card lands last and its tag pulses. Everything is removed
* under `prefers-reduced-motion`.
*
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so this centered vignette adds matching right
* padding to land on the tile's visible center instead of the bled
* slot's center. The column is fluid (`w-full max-w-[312px]`) so it
* never exceeds the compensated slot at narrow tile widths — labels
* truncate instead of clipping.
*/
export function ReconcileGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 max-lg:pr-6'
>
<div className='w-full max-w-[312px]'>
<div className='mb-1.5 flex items-center justify-between gap-2'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-base'>
Reconciliation
</span>
<ChipTag variant='mono' className='shrink-0 bg-[var(--surface-6)]'>
March close
</ChipTag>
</div>
{MATCHED_ROWS.map((row, index) => (
<div
key={row.label}
className={cn(
'flex h-9 items-center gap-2',
index > 0 && 'border-[var(--border-1)] border-t',
ROW_STEP_CLASSES[index]
)}
>
<CircleCheck className='size-[13px] shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 flex-1 truncate text-[var(--text-secondary)] text-caption'>
{row.label}
</span>
<span className='shrink-0 font-mono text-[var(--text-muted)] text-caption'>
{row.amount}
</span>
</div>
))}
<div
className={cn(
'mt-2 flex items-center gap-3 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm',
styles.exceptionIn
)}
>
<span className='min-w-0 flex-1'>
<span className='flex items-center gap-2'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-small'>
Wire transfer
</span>
<span className='shrink-0 font-mono text-[var(--text-secondary)] text-caption'>
$12,400.00
</span>
</span>
<span className='mt-0.5 block truncate text-[var(--text-muted)] text-caption'>
No ledger match · routed for review
</span>
</span>
<ChipTag variant='gray' className={cn('shrink-0 shadow-none', styles.flaggedPulse)}>
Flagged
</ChipTag>
</div>
<div className='mt-1.5 flex h-9 items-center gap-2 px-3'>
<CircleCheck className='size-[13px] shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 flex-1 truncate text-[var(--text-secondary)] text-caption'>
Matched automatically
</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>132 of 133</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}

Some files were not shown because too many files have changed in this diff Show More