chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,121 @@
import { cn } from '@sim/emcn'
import {
SolutionsCard,
SolutionsCardRowHeader,
} from '@/app/(landing)/components/solutions-page/components/solutions-card-row/components'
import { SOLUTIONS_SPACING } from '@/app/(landing)/components/solutions-page/constants'
import type { SolutionsCardRowConfig } from '@/app/(landing)/components/solutions-page/types'
/**
* The enterprise feature sections rendered as ONE shared CSS grid so cards can
* reflow across section boundaries in the two-column band (`sm`..`lg`,
* 640-1023px). Separate per-section grids (what {@link SolutionsCardRow}
* renders) leave an orphan cell there: each 3-card section breaks 2 + 1.
*
* Layout per breakpoint:
* - `lg`+ (3 columns): source order - every header is followed by its own 3
* cards, matching {@link SolutionsCardRow} exactly.
* - `sm`..`lg` (2 columns): the `sm:max-lg:order-*` classes regroup the 12
* cards into balanced blocks of 4 / 4 / 2 / 2 beneath the four headers, so
* no grid cell is ever empty. Cards borrowed from the next section render
* under the previous header in this band only.
* - below `sm` (1 column): source order again - each header stacks above its
* own 3 cards.
*
* Each section keeps its `<section aria-labelledby>` landmark via
* `display: contents`, so its header and cards participate directly in the
* outer grid while the document outline (H2 -> H3) and anchor ids stay
* identical to the `SolutionsCardRow` markup.
*
* Vertical rhythm is reproduced from the flex-column layout it replaces: the
* grid's own `gap-8` (32px) supplies the inter-card gap, and headers carry
* margins that top the gap up to the original values - `mb-4` lands the
* header->cards distance at 48px (`cardRowHeaderToGrid`), and the top margins
* land the section->section distance at 120/88/64px (`sectionRhythm`).
*/
interface EnterpriseFeatureGridProps {
/** The four 3-card feature rows, in source order. */
rows: SolutionsCardRowConfig[]
}
/**
* Header top margins recreating `LANDING_SECTION_RHYTHM` (120/88/64px) on top
* of the grid's 32px row gap; `mb-4` recreates `cardRowHeaderToGrid` (48px).
*/
const HEADER_RHYTHM = 'mt-[88px] mb-4 max-lg:mt-14 max-sm:mt-8'
const FIRST_HEADER_RHYTHM = 'mb-4'
/** Two-column-band ordering for the four headers (positions 1, 6, 11, 14). */
const TABLET_HEADER_ORDER = [
'sm:max-lg:order-1',
'sm:max-lg:order-6',
'sm:max-lg:order-11',
'sm:max-lg:order-[14]',
] as const
/**
* Two-column-band ordering for the 12 cards (flat index = rowIndex * 3 +
* cardIndex), interleaved with {@link TABLET_HEADER_ORDER} to produce the
* 4 / 4 / 2 / 2 grouping.
*/
const TABLET_CARD_ORDER = [
'sm:max-lg:order-2',
'sm:max-lg:order-3',
'sm:max-lg:order-4',
'sm:max-lg:order-5',
'sm:max-lg:order-7',
'sm:max-lg:order-8',
'sm:max-lg:order-9',
'sm:max-lg:order-10',
'sm:max-lg:order-12',
'sm:max-lg:order-[13]',
'sm:max-lg:order-[15]',
'sm:max-lg:order-[16]',
] as const
export function EnterpriseFeatureGrid({ rows }: EnterpriseFeatureGridProps) {
return (
<div
className={cn(
'grid grid-cols-3 max-sm:grid-cols-1 max-lg:grid-cols-2',
SOLUTIONS_SPACING.cardGridGap
)}
>
{rows.map((row, rowIndex) => {
const headingId = `solutions-row-${row.id}-heading`
return (
<section
key={row.id}
id={`solutions-row-${row.id}`}
aria-labelledby={headingId}
className='contents'
>
<div
className={cn(
'col-span-full',
rowIndex === 0 ? FIRST_HEADER_RHYTHM : HEADER_RHYTHM,
TABLET_HEADER_ORDER[rowIndex]
)}
>
<SolutionsCardRowHeader row={row} headingId={headingId} variant='feature' />
</div>
{row.cards.map((card, cardIndex) => (
<div
key={`${row.id}-${card.title}`}
className={cn('min-w-0', TABLET_CARD_ORDER[rowIndex * 3 + cardIndex])}
>
<SolutionsCard
card={card}
headingId={`solutions-row-${row.id}-card-${cardIndex}-heading`}
variant='featureTile'
/>
</div>
))}
</section>
)
})}
</div>
)
}
@@ -0,0 +1 @@
export { EnterpriseFeatureGrid } from './enterprise-feature-grid'
@@ -0,0 +1,267 @@
'use client'
import { useEffect, useState } from 'react'
import { ChevronDown, cn } from '@sim/emcn'
import {
ArrowRight,
ArrowUp,
ClipboardList,
Files,
Mic,
Paperclip,
Plus,
ShieldCheck,
Shuffle,
Slash,
Table,
} from '@sim/emcn/icons'
import { ThinkingLoader } from '@/components/ui'
import {
COMPOSER_PLACEHOLDER,
ENTERPRISE_GREETING,
ENTERPRISE_PROMPT,
ENTERPRISE_REPLY,
type EnterpriseLoopPhase,
PROMPT_CHAR_MS,
REPLY_WORD_MS,
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
* throttled background tab catches up instead of stalling mid-reveal.
* Resets to 0 when inactive; jumps straight to `total` under
* `prefers-reduced-motion`.
*/
function useElapsedReveal(active: boolean, stepMs: number, total: number) {
const [revealed, setRevealed] = useState(0)
useEffect(() => {
if (!active) {
setRevealed(0)
return
}
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
let interval: ReturnType<typeof setInterval> | null = null
const run = () => {
const startedAt = performance.now()
interval = setInterval(() => {
const elapsed = performance.now() - startedAt
const n = Math.min(Math.floor(elapsed / stepMs) + 1, total)
setRevealed(n)
if (n >= total && interval) clearInterval(interval)
}, stepMs)
}
const syncMotionPreference = () => {
if (interval) clearInterval(interval)
if (media.matches) {
setRevealed(total)
return
}
run()
}
syncMotionPreference()
media.addEventListener('change', syncMotionPreference)
return () => {
media.removeEventListener('change', syncMotionPreference)
if (interval) clearInterval(interval)
}
}, [active, stepMs, total])
return revealed
}
/** Greyscale leading icons for the suggested-action rows, in row order. */
const ACTION_ICONS = [Table, ShieldCheck, ClipboardList, Files] as const
const Caret = () => (
<span
aria-hidden='true'
className='ml-px inline-block h-[16px] w-px translate-y-[2px] animate-caret-blink bg-[var(--text-primary)]'
/>
)
interface ComposerProps {
/** Rendered in the text region (placeholder span or typed prompt). */
children: React.ReactNode
/** Fills the send disc with the active ink once the prompt has text. */
active: boolean
}
/**
* The Mothership composer chrome - white rounded field, text region on top,
* icon rail beneath (add / attach / skills left, mic + send disc right) -
* matching the homepage loop's composer and the real `UserInput`.
*/
function Composer({ children, active }: ComposerProps) {
return (
<div className='w-full rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 shadow-[0_1px_2px_0_rgba(18,18,18,0.05)]'>
<p className='min-h-[24px] px-1.5 pt-1 text-[15px] text-[var(--text-primary)] leading-[24px]'>
{children}
</p>
<div className='mt-2 flex items-center gap-1.5'>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Plus className='size-[16px] text-[var(--text-icon)]' />
</span>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Paperclip className='size-[16px] text-[var(--text-icon)]' />
</span>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Slash className='size-[16px] text-[var(--text-icon)]' />
</span>
<span className='ml-auto flex items-center gap-1.5'>
<span className='flex size-[28px] items-center justify-center rounded-full'>
<Mic className='size-[16px] text-[var(--text-icon)]' />
</span>
<span
className={cn(
'flex size-[28px] items-center justify-center rounded-full transition-colors duration-200',
active ? 'bg-[#383838]' : 'bg-[#808080]'
)}
>
<ArrowUp className='size-[16px] text-white' />
</span>
</span>
</div>
</div>
)
}
interface EnterpriseHomeStageProps {
/** Current beat, driven by the parent {@link EnterprisePlatformLoop} clock. */
phase: EnterpriseLoopPhase
/** True during the brief fade-out before the cycle restarts. */
fading: boolean
}
/**
* The main pane of the enterprise loop - the real workspace's NEW-CHAT home
* view, replayed: the centered greeting, the composer (placeholder, then the
* enterprise prompt typing out, then the send disc arming), and the
* "Suggested actions" rows beneath. On `dispatch` the home layer exits first
* and the conversation layer (the sent prompt as a user bubble over the goo
* {@link ThinkingLoader}, with the composer docked at the bottom) enters after
* a beat; the loader thinks while the parent's stage pane builds the workflow,
* then the reply streams in word by word on `reply`.
*
* Purely presentational; the clock lives in the parent so the chat and the
* workflow stage animate off one timeline. Both typewriters derive from
* ELAPSED time so throttled background tabs catch up instead of stalling
* mid-sentence.
*/
export function EnterpriseHomeStage({ phase, fading }: 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 visiblePrompt = promptDone ? ENTERPRISE_PROMPT : ENTERPRISE_PROMPT.slice(0, typedChars)
const hasText = visiblePrompt.length > 0
return (
<div
className={cn(
'relative h-full w-full bg-[var(--bg)] transition-opacity duration-300 ease-out',
fading ? 'opacity-0' : 'opacity-100'
)}
>
{/* Home layer - greeting, composer, suggested actions. Exits FIRST on
dispatch (no delay) so the swap never reads as two stacked layers. */}
<div
className={cn(
'absolute inset-0 flex flex-col items-center justify-center px-10 pb-[6vh] transition-opacity duration-200 ease-out',
inConversation ? 'pointer-events-none opacity-0' : 'opacity-100'
)}
>
<p className='mb-7 text-[30px] text-[var(--text-primary)]'>{ENTERPRISE_GREETING}</p>
<div className='w-full max-w-[576px]'>
<Composer active={hasText}>
{hasText ? (
<>
{visiblePrompt}
{isTyping && <Caret />}
</>
) : (
<span className='font-[380] text-[var(--text-muted)]'>{COMPOSER_PLACEHOLDER}</span>
)}
</Composer>
<div className='mt-7'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-2'>
<span className='text-[13px] text-[var(--text-muted)]'>Suggested actions</span>
<ChevronDown className='h-[7px] w-[9px] text-[var(--text-muted)]' />
</span>
<span className='flex items-center gap-1.5'>
<span className='text-[13px] text-[var(--text-muted)]'>Shuffle</span>
<Shuffle className='size-[14px] text-[var(--text-muted)]' />
</span>
</div>
<div className='mt-2 flex flex-col'>
{SUGGESTED_ACTIONS.map((action, i) => {
const Icon = ACTION_ICONS[i]
return (
<span
key={action}
className={cn(
'flex items-center gap-2 border-[var(--border-1)] px-2 py-2',
i > 0 && 'border-t'
)}
>
<Icon className='size-[16px] flex-shrink-0 text-[var(--text-muted)]' />
<span className='flex-1 truncate text-[var(--text-primary)] text-sm'>
{action}
</span>
<ArrowRight className='size-[16px] shrink-0 text-[var(--text-muted)]' />
</span>
)
})}
</div>
</div>
</div>
</div>
{/* Conversation layer - the sent exchange. Enters AFTER the home layer's
200ms exit so the handoff is a choreographed swap, not a crossfade.
The loader thinks through the workflow build, then the reply streams. */}
<div
className={cn(
'absolute inset-0 flex flex-col transition-opacity duration-200 ease-out',
inConversation ? 'opacity-100 [transition-delay:220ms]' : 'pointer-events-none opacity-0'
)}
>
<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}
</div>
{phase === 'dispatch' && (
<ThinkingLoader size={26} phase labelRatio={0.58} className='mt-1' />
)}
<p
className={cn(
'text-[15px] text-[var(--text-primary)] leading-[1.6] transition-opacity duration-200 ease-out',
isReply ? 'opacity-100' : 'opacity-0'
)}
>
{REPLY_WORDS.slice(0, revealedWords).join(' ')}
</p>
</div>
<div className='mx-auto mb-5 w-[calc(100%-40px)] max-w-[600px] shrink-0'>
<Composer active={false}>
<span className='font-[380] text-[var(--text-muted)]'>Send message to Sim</span>
</Composer>
</div>
</div>
</div>
)
}
@@ -0,0 +1,169 @@
'use client'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { HeroWorkflowStage } from '@/app/(landing)/components/hero/components/hero-platform-loop/hero-workflow-stage'
import { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage'
import { EnterpriseSidebar } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
import {
BUILD_STEP_MS,
ENTERPRISE_STAGE_BLOCKS,
ENTERPRISE_STAGE_CANVAS,
ENTERPRISE_STAGE_EDGES,
type EnterpriseLoopPhase,
LOOP_TIMELINE,
RESET_FADE_MS,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
/**
* 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
/**
* The enterprise hero's platform loop - a sibling of the homepage
* `HeroPlatformLoop` that shares its architecture (fixed design-space layer
* scaled to the window via ResizeObserver + `transform: scale`, a parent-owned
* timeline clock driving presentational stages, reduced-motion showing a
* static finished frame) but diverges in content: where the homepage overlays
* a live chat over a baked screenshot, this variant renders the WHOLE interior
* live - the {@link EnterpriseSidebar} (a filled-out Brightwave workspace) and
* the {@link EnterpriseHomeStage} (the real new-chat home view, replaying an
* enterprise prompt) - because its sidebar content differs from the shot's.
*
* 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.
*
* 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)
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()
}, [])
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()
}
}, [])
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>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,147 @@
import { ChevronDown, cn, Home, Library } from '@sim/emcn'
import {
Calendar,
Database,
File,
HelpCircle,
Integration,
MoreHorizontal,
PanelLeft,
Plus,
Search,
Settings,
Table,
} from '@sim/emcn/icons'
import Image from 'next/image'
import {
SIDEBAR_CHATS,
SIDEBAR_WORKFLOWS,
} from '@/app/(landing)/enterprise/components/enterprise-platform-loop/stage-data'
const WORKSPACE_NAV = [
{ label: 'Tables', icon: Table },
{ label: 'Files', icon: File },
{ label: 'Knowledge base', icon: Database },
{ label: 'Scheduled tasks', icon: Calendar },
{ label: 'Logs', icon: Library },
] as const
interface IconRowProps {
icon: React.ComponentType<{ className?: string }>
label: string
active?: boolean
}
/** A sidebar nav row with a leading icon, like the real workspace sidebar. */
function IconRow({ icon: Icon, label, active = false }: IconRowProps) {
return (
<div
className={cn(
'mx-0.5 flex h-[28px] items-center gap-2 rounded-[8px] px-2',
active && 'bg-[var(--surface-active)]'
)}
>
<Icon className='size-[14px] flex-shrink-0 text-[var(--text-icon)]' />
<span className='truncate font-[450] text-[13px] text-[var(--text-body)]'>{label}</span>
</div>
)
}
/** A bare text row - the real sidebar's chat and workflow entries. */
function TextRow({ label }: { label: string }) {
return (
<div className='mx-0.5 flex h-[28px] items-center rounded-[8px] px-2'>
<span className='truncate font-[450] text-[13px] text-[var(--text-body)]'>{label}</span>
</div>
)
}
/** Muted section heading (Chats / Workspace / Workflows). */
function SectionLabel({ label, actions }: { label: string; actions?: boolean }) {
return (
<div className='flex items-center justify-between px-4 pb-1.5'>
<span className='text-[12px] text-[var(--text-icon)]'>{label}</span>
{actions && (
<span className='flex items-center gap-2 text-[var(--text-icon)]'>
<MoreHorizontal className='size-[14px]' />
<Plus className='size-[14px]' />
</span>
)}
</div>
)
}
/**
* The Brightwave workspace sidebar, rendered live (the homepage loop keeps its
* baked-screenshot sidebar; the enterprise loop draws its own so the content
* can read like a large tenured deployment): the workspace header, New chat /
* 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.
*/
export function EnterpriseSidebar() {
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
(borderless `chipVariants()` geometry: h-[30px] rounded-lg px-2 with
mx-0.5, 16px logo, text-sm name, 6x10 chevron) and therefore the
homepage's baked sidebar pixels - logo + name + chevron as a bare
row, panel toggle right-aligned outside it. */}
<div className='flex flex-shrink-0 items-center justify-between px-2'>
<div className='mx-0.5 flex h-[30px] min-w-0 items-center gap-2 rounded-lg px-2'>
{/* The exact Brightwave mark the homepage capture seeds
(`readme-tour-capture` sets `logoUrl: '/landing/rivian-logo.svg'`),
so both platform previews show the same company logo. */}
<Image
src='/landing/rivian-logo.svg'
alt=''
width={16}
height={16}
className='size-[16px] flex-shrink-0 rounded-sm'
/>
<span className='min-w-0 truncate text-[var(--text-body)] text-sm'>Brightwave</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={Search} label='Search' />
<IconRow icon={Integration} label='Integrations' />
</div>
<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) => (
<TextRow key={chat} label={chat} />
))}
</div>
</div>
<div className='mt-3.5 flex flex-shrink-0 flex-col'>
<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} />
))}
</div>
</div>
<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) => (
<TextRow key={workflow} label={workflow} />
))}
</div>
</div>
<div className='flex flex-shrink-0 flex-col gap-0.5 px-2 pt-[9px] pb-2'>
<IconRow icon={HelpCircle} label='Help' />
<IconRow icon={Settings} label='Settings' />
</div>
</div>
)
}
@@ -0,0 +1,3 @@
export { EnterpriseHomeStage } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-home-stage'
export { EnterprisePlatformLoop } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-platform-loop'
export { EnterpriseSidebar } from '@/app/(landing)/enterprise/components/enterprise-platform-loop/enterprise-sidebar'
@@ -0,0 +1,180 @@
import { AgentIcon, ConditionalIcon, MailIcon, StartIcon, TableIcon } from '@/components/icons'
import type { BlockDef } from '@/app/(landing)/components/hero/components/hero-visual/workflow-data'
/**
* Content + timing config for the enterprise hero's platform loop. Kept in one
* place (and phase starts derived, not hardcoded) so later stages - tables,
* files, knowledge base, logs walkthroughs - extend the timeline by appending
* beats here rather than reworking the clock.
*/
/** The new-chat greeting, personalized like the real workspace Home. */
export const ENTERPRISE_GREETING = 'What should we get done, Morgan?'
/** Placeholder shown in the composer before the prompt types out. */
export const COMPOSER_PLACEHOLDER = 'Ask Sim to automate a process.'
/**
* The prompt the loop types - a large company's multi-system operational
* workflow (AP + NetSuite + finance review + audit trail), concise enough to
* type on screen.
*/
export const ENTERPRISE_PROMPT =
'When a vendor invoice lands in AP, match it against the PO in NetSuite, flag exceptions for finance review, and log the approval trail.'
/** Typewriter cadence for the composer prompt. */
export const PROMPT_CHAR_MS = 28
/** Sim's reply, streamed word by word once the workflow finishes building. */
export const ENTERPRISE_REPLY =
"On it. I'll match each AP invoice to its PO in NetSuite, route exceptions to finance review, and log the full approval trail."
/** Word-reveal cadence for the streamed reply. */
export const REPLY_WORD_MS = 55
/**
* Recent chats - enterprise-flavored, so Brightwave reads long-tenured. Four
* entries: together with the five workflows this fills the sidebar's 735px
* design height exactly, without clipping the Workflows section.
*/
export const SIDEBAR_CHATS = [
'Vendor invoice exceptions',
'Q3 access review',
'NetSuite sync errors',
'Supplier onboarding docs',
] as const
/** Deployed workflows - a fuller section than the homepage's three. */
export const SIDEBAR_WORKFLOWS = [
'Invoice exception routing',
'Employee onboarding',
'Vendor risk scoring',
'IT access provisioning',
'Weekly compliance report',
] as const
/** Suggested actions under the composer, mirroring the real Home rows. */
export const SUGGESTED_ACTIONS = [
'Reconcile vendor invoices in NetSuite',
'Triage pending IT access requests',
'Summarize open compliance exceptions',
'Draft the weekly ops readiness report',
] as const
/**
* The workflow the chat "builds" on the stage pane - the invoice-matching flow
* the prompt describes, distilled to what reads at mini-app scale: Start feeds
* the NetSuite PO match, exceptions are flagged, and the flow fans out to
* finance review and the audit log. Same geometry conventions as the homepage
* stage (250px blocks, vertical spine at x=155, terminals fanned at y=580);
* tiles use the platform's grey text ramp - color is reserved for real
* third-party marks, and none of these carry one.
*
* Ordered by build sequence; an edge draws once both endpoints are on canvas.
*/
export const ENTERPRISE_STAGE_BLOCKS: BlockDef[] = [
{
id: 'start',
name: 'Start',
icon: StartIcon,
bgColor: 'var(--text-muted)',
isTrigger: true,
rows: [{ title: 'Inputs', value: '-' }],
x: 155,
y: 12,
},
{
id: 'match',
name: 'Match PO',
icon: AgentIcon,
bgColor: 'var(--text-primary)',
rows: [
{ title: 'Messages', value: '-' },
{ title: 'Model', value: '-' },
],
x: 155,
y: 172,
},
{
id: 'exceptions',
name: 'Flag exceptions',
icon: ConditionalIcon,
bgColor: 'var(--text-secondary)',
rows: [{ title: 'Conditions', value: '-' }],
x: 155,
y: 372,
},
{
id: 'review',
name: 'Finance review',
icon: MailIcon,
bgColor: 'var(--text-body)',
isTerminal: true,
rows: [
{ title: 'To', value: '-' },
{ title: 'Subject', value: '-' },
],
x: 0,
y: 560,
},
{
id: 'audit',
name: 'Audit log',
icon: TableIcon,
bgColor: 'var(--text-muted)',
isTerminal: true,
rows: [
{ title: 'Table', value: '-' },
{ title: 'Operation', value: '-' },
],
x: 310,
y: 560,
},
]
/** Source → target pairs, drawn in order as their endpoints land on canvas. */
export const ENTERPRISE_STAGE_EDGES: ReadonlyArray<readonly [string, string]> = [
['start', 'match'],
['match', 'exceptions'],
['exceptions', 'review'],
['exceptions', 'audit'],
]
/** Design-space bounding box of the layout above. */
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'
/** The idle new-chat view holds this long before typing starts. */
const IDLE_HOLD_MS = 1400
/** Rest on the fully-typed prompt before "send". */
const TYPED_HOLD_MS = 700
/** Thinking runs alone this long before the stage pane slides open. */
const STAGE_OPEN_AFTER_MS = 900
/** First block lands this long after the pane opens. */
const BUILD_START_AFTER_MS = 500
/** Block N (build order) pops in at buildStart + N * BUILD_STEP_MS. */
export const BUILD_STEP_MS = 620
/** The reply starts streaming this long after the last block lands. */
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. Later stages (tables, files, knowledge base, logs)
* slot in after `reply` by extending {@link EnterpriseLoopPhase} and appending
* starts here.
*/
export const LOOP_TIMELINE = (() => {
const typing = IDLE_HOLD_MS
const typed = typing + ENTERPRISE_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 total = reply + REPLY_HOLD_MS
return { typing, typed, dispatch, stageOpen, buildStart, reply, total } as const
})()
@@ -0,0 +1,136 @@
/**
* The AccessControlGraphic's motion, all on one shared 6s cycle: the six
* connector edges draw in with a dash-normalized stroke sweep (every path
* carries `pathLength=1`, so a dasharray/dashoffset of 1 spans the whole
* curve regardless of geometry — the deploy tile's pattern), staggered so
* the grants wire up one after another, quiet edges first and the
* emphasized Engineering → Deploy edge last and slowest. Each edge's
* stagger is baked into its own keyframe percentages rather than an
* `animation-delay`, so every edge resets on the same loop boundary and
* the cycle reads draw-once-then-hold. As the emphasized edge lands, the
* grant node blooms the family's soft ring pulse. Under
* prefers-reduced-motion everything is static: edges fully drawn, no
* pulse.
*/
.edgeDraw {
stroke-dasharray: 1;
stroke-dashoffset: 1;
}
.edgeDraw0 {
animation: access-edge-draw-0 6s ease-in-out infinite;
}
.edgeDraw1 {
animation: access-edge-draw-1 6s ease-in-out infinite;
}
.edgeDraw2 {
animation: access-edge-draw-2 6s ease-in-out infinite;
}
.edgeDraw3 {
animation: access-edge-draw-3 6s ease-in-out infinite;
}
.edgeDraw4 {
animation: access-edge-draw-4 6s ease-in-out infinite;
}
.edgeDrawEmphasized {
animation: access-edge-draw-emphasized 6s ease-in-out infinite;
}
@keyframes access-edge-draw-0 {
0% {
stroke-dashoffset: 1;
}
14%,
100% {
stroke-dashoffset: 0;
}
}
@keyframes access-edge-draw-1 {
0%,
7% {
stroke-dashoffset: 1;
}
21%,
100% {
stroke-dashoffset: 0;
}
}
@keyframes access-edge-draw-2 {
0%,
14% {
stroke-dashoffset: 1;
}
28%,
100% {
stroke-dashoffset: 0;
}
}
@keyframes access-edge-draw-3 {
0%,
21% {
stroke-dashoffset: 1;
}
35%,
100% {
stroke-dashoffset: 0;
}
}
@keyframes access-edge-draw-4 {
0%,
28% {
stroke-dashoffset: 1;
}
42%,
100% {
stroke-dashoffset: 0;
}
}
@keyframes access-edge-draw-emphasized {
0%,
38% {
stroke-dashoffset: 1;
}
56%,
100% {
stroke-dashoffset: 0;
}
}
.grantPulse {
animation: access-grant-pulse 6s ease-out infinite;
}
@keyframes access-grant-pulse {
0%,
54% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 35%, transparent);
}
72% {
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) {
.edgeDraw {
animation: none;
stroke-dashoffset: 0;
}
.grantPulse {
animation: none;
}
}
@@ -0,0 +1,199 @@
import { ChipTag, cn } from '@sim/emcn'
import Image from 'next/image'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/access-control-graphic.module.css'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
/** Fixed pixel canvas the vignette is drawn on, centered inside the shell. */
const CANVAS = { WIDTH: 320, HEIGHT: 250 } as const
/** Vertical anchors: team output ports on top, chip input ports below. */
const PORT_Y = { TEAM: 72, CHIP: 208 } as const
const TEAMS = [
{ avatar: '/landing/team-avatar-1.jpg', name: 'Engineering', x: 64, leftClass: 'left-[64px]' },
{ avatar: '/landing/team-avatar-2.jpg', name: 'Support', x: 160, leftClass: 'left-[160px]' },
{ avatar: '/landing/team-avatar-3.jpg', name: 'Ops', x: 256, leftClass: 'left-[256px]' },
] as const
const CHIPS = [
{ label: 'Build', x: 80, leftClass: 'left-[80px]' },
{ label: 'Review', x: 160, leftClass: 'left-[160px]' },
{ label: 'Deploy', x: 240, leftClass: 'left-[240px]' },
] as const
interface Edge {
/** Team output-port x coordinate. */
from: number
/** Chip input-port x coordinate. */
to: number
/** Whether this is the emphasized live grant (stronger ink). */
emphasized?: boolean
}
/**
* Team → permission grants: Engineering builds, reviews, and deploys
* (Deploy is the live grant); Support builds and reviews; Ops reviews.
*/
const EDGES: readonly Edge[] = [
{ from: 64, to: 80 },
{ from: 64, to: 160 },
{ from: 160, to: 80 },
{ from: 160, to: 160 },
{ from: 256, to: 160 },
{ from: 64, to: 240, emphasized: true },
] as const
/**
* Per-index draw classes for the quiet edges — the stagger order is baked
* into each class's keyframes so all edges share one 6s loop boundary.
* The emphasized edge uses its own dedicated class instead.
*/
const EDGE_DRAW_CLASSES = [
styles.edgeDraw0,
styles.edgeDraw1,
styles.edgeDraw2,
styles.edgeDraw3,
styles.edgeDraw4,
] as const
/** Builds a node-graph edge: vertical tangents easing into both ports. */
function edgePath(edge: Edge): string {
const { from, to } = edge
const bend = 62
return `M ${from} ${PORT_Y.TEAM} C ${from} ${PORT_Y.TEAM + bend} ${to} ${PORT_Y.CHIP - bend} ${to} ${PORT_Y.CHIP}`
}
/**
* Workspace access told as a vertical role graph, with no window framing:
* three gradient-circle team avatars across the top flow down through
* curved node-graph edges — 1px bezier strokes in the deploy tile's faint
* guide-line grey, each bending toward and landing on the specific
* permission chip that team holds — so who-can-do-what reads as directed
* wiring. Scopes narrow across the teams (Engineering builds, reviews, and
* deploys; Support builds and reviews; Ops reviews). Engineering's Deploy
* grant is the emphasized element: its curve carries the stronger
* `--text-secondary` ink into a filled junction node that blooms the row's
* shared 6s ring pulse; the other chips sit behind quiet hollow
* port nodes and mono chips (fills stepped up to `--surface-6` so the
* pills stay legible on the grey ground).
*
* Motion (from `access-control-graphic.module.css`, one shared 6s cycle):
* the edges draw in with a dash-normalized stroke sweep (`pathLength=1`,
* the deploy tile's pattern), staggered so the grants wire up one after
* another — quiet edges first, the emphasized Deploy edge last — then
* hold drawn for the rest of the loop, and the grant node's ring pulse
* blooms as the emphasized edge lands. Under `prefers-reduced-motion` the
* graph renders fully drawn and static.
*
* The avatar assets are grey radial gradients on a black square, so each
* sits in a `rounded-full overflow-hidden` clip with a slight scale-up to
* crop the black canvas past the circle's edge.
*
* 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 fixed-size canvas is `shrink-0`: if it were allowed to
* 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.
*/
export function AccessControlGraphic() {
return (
<FeatureGraphicShell>
<div
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]'>
<svg
className='absolute inset-0'
fill='none'
viewBox={`0 0 ${CANVAS.WIDTH} ${CANVAS.HEIGHT}`}
width={CANVAS.WIDTH}
height={CANVAS.HEIGHT}
>
{EDGES.map((edge, index) => (
<path
key={`${edge.from}-${edge.to}`}
d={edgePath(edge)}
pathLength={1}
className={cn(
styles.edgeDraw,
edge.emphasized ? styles.edgeDrawEmphasized : EDGE_DRAW_CLASSES[index]
)}
stroke={
edge.emphasized
? 'var(--text-secondary)'
: 'color-mix(in srgb, var(--text-muted) 35%, transparent)'
}
strokeWidth='1'
/>
))}
</svg>
{TEAMS.map((team, index) => (
<div
key={team.name}
className={cn(
'-translate-x-1/2 absolute top-2 flex w-24 flex-col items-center gap-1.5',
team.leftClass
)}
>
<span className='relative size-8 overflow-hidden rounded-full shadow-sm'>
<Image
src={team.avatar}
alt=''
width={32}
height={32}
className='size-full scale-110 object-cover'
/>
</span>
<span
className={cn(
'text-caption',
index === 0
? 'font-medium text-[var(--text-primary)]'
: 'text-[var(--text-secondary)]'
)}
>
{team.name}
</span>
</div>
))}
{CHIPS.map((chip) => {
const emphasized = EDGES.some((edge) => edge.emphasized && edge.to === chip.x)
return (
<div
key={chip.label}
className={cn(
'-translate-x-1/2 absolute top-[203px] flex flex-col items-center gap-1.5',
chip.leftClass
)}
>
<span
className={cn(
emphasized
? cn('size-2.5 rounded-full bg-[var(--text-primary)]', styles.grantPulse)
: 'size-2 rounded-full border border-[var(--text-muted)] bg-[var(--surface-3)]'
)}
/>
<ChipTag
variant={emphasized ? 'solid' : 'mono'}
className={cn(!emphasized && 'bg-[var(--surface-6)]')}
>
{chip.label}
</ChipTag>
</div>
)
})}
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,47 @@
/**
* The AuditTrailGraphic's motion: the newest ledger entry stamps in once on
* load (a short settle from above — an append, never a re-append, since the
* record is immutable), and its actor avatar then carries the row's shared
* quiet 6s ring-pulse beat (same bloom as the access tile's live grant and
* the lifecycle tile's live node) to mark the trail as live. Under
* prefers-reduced-motion both are removed and the ledger sits static.
*/
.stampIn {
animation: audit-stamp-in 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.35s backwards;
}
@keyframes audit-stamp-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.sealPulse {
animation: audit-seal-pulse 6s ease-out infinite;
}
@keyframes audit-seal-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 35%, 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) {
.stampIn,
.sealPulse {
animation: none;
}
}
@@ -0,0 +1,165 @@
import { ChipTag, cn } from '@sim/emcn'
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 {
/** Human-readable action label, matching how the audit UI renders `AuditAction` codes. */
action: string
/** Attributed actor shown on the detail line. */
actor: string
/** Resource the action touched. */
resource: string
/** Relative or absolute timestamp, newest first. */
time: string
/** Gradient team avatar attributing the entry to its actor. */
avatar: string
}
/**
* Ledger records newest-first, drawn from the real `AuditAction` codes in
* `packages/audit` (`workflow.deployed`, `permission_group.updated`,
* `credential.accessed`, `workflow.created`) rendered as spaced words the
* way the workspace audit UI formats them, and threading the running
* Support-agent story: Maya ships v3, the Support permission group is
* tuned, a Zendesk credential access is recorded, and the trail reaches
* back to the workflow's creation.
*/
const ENTRIES: readonly AuditEntry[] = [
{
action: 'Workflow deployed',
actor: 'Maya Chen',
resource: 'Support agent v3',
time: 'Now',
avatar: '/landing/team-avatar-1.jpg',
},
{
action: 'Permission group updated',
actor: 'Jordan Lee',
resource: 'Support team',
time: '2 min ago',
avatar: '/landing/team-avatar-2.jpg',
},
{
action: 'Credential accessed',
actor: 'Sam Ortiz',
resource: 'Zendesk OAuth',
time: '26 min ago',
avatar: '/landing/team-avatar-3.jpg',
},
{
action: 'Workflow created',
actor: 'Maya Chen',
resource: 'Support agent',
time: 'Jun 14',
avatar: '/landing/team-avatar-1.jpg',
},
] as const
/** Per-row ink treatments, quieter with age like the lifecycle timeline. */
const ROW_TONES = [
{ action: 'text-[var(--text-primary)]', avatar: 'opacity-100' },
{ action: 'text-[var(--text-secondary)]', avatar: 'opacity-80' },
{ action: 'text-[var(--text-muted)]', avatar: 'opacity-60' },
{ action: 'text-[var(--text-muted)]', avatar: 'opacity-40' },
] as const
/**
* Sim's audit trail told as an append-only ledger rather than a product
* window: a frameless, centered vignette (the access tile's composition,
* which sits beside it in the row) where each record is a plain row —
* gradient actor avatar, the action label in the row's regular sans face
* (`font-medium text-small`, the same treatment the standards tile gives
* its row titles), an "actor · resource" attribution line, and a
* right-aligned timestamp. The newest record is the selected event: it
* sits on a solid white card wearing the build tile's window chrome
* exactly — `--white` fill, 1px `--border-1` hairline, `rounded-xl`,
* `shadow-sm` — while the older records rest directly on the tile,
* quietening with age until a mask gradient dissolves the oldest —
* implying the trail continues into history. A small "Audit log"
* header with an `Append-only` mono ChipTag (fill stepped up to
* `--surface-6` so the pill stays legible on the grey ground) names the
* surface without window chrome.
*
* Motion (from `audit-trail-graphic.module.css`): the newest record stamps
* in once from above — an append, never re-played, since the record is
* immutable — and its avatar then carries the row's shared 6s ring-pulse
* beat (matching the access tile's grant node and the lifecycle tile's
* live node). Both are removed under `prefers-reduced-motion`.
*
* The avatar assets are grey radial gradients on a black square, so each
* sits in a `rounded-full overflow-hidden` clip with a slight scale-up to
* crop the black canvas past the circle's edge.
*
* 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.
*/
export function AuditTrailGraphic() {
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-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)]'>
Append-only
</ChipTag>
</div>
<div className='flex flex-col gap-1.5 [mask-image:linear-gradient(to_bottom,black_55%,transparent_100%)]'>
{ENTRIES.map((entry, index) => {
const tone = ROW_TONES[index]
const newest = index === 0
return (
<div
key={entry.action}
className={cn(
'flex items-center gap-3 px-3 py-2.5',
newest &&
cn(
'rounded-xl border border-[var(--border-1)] bg-[var(--white)] shadow-sm',
styles.stampIn
)
)}
>
<span
className={cn(
'relative size-7 shrink-0 overflow-hidden rounded-full shadow-sm',
tone.avatar,
newest && styles.sealPulse
)}
>
<Image
src={entry.avatar}
alt=''
width={28}
height={28}
className='size-full scale-110 object-cover'
/>
</span>
<span className='min-w-0 flex-1'>
<span className={cn('block truncate font-medium text-small', tone.action)}>
{entry.action}
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
{entry.actor} · {entry.resource}
</span>
</span>
<span className='shrink-0 self-start pt-px text-[var(--text-muted)] text-caption'>
{entry.time}
</span>
</div>
)
})}
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,337 @@
'use client'
import { useEffect, useState } from 'react'
import { cn } from '@sim/emcn'
import { ArrowUp, FolderCode, Mic, Paperclip, Plus, Slash } from '@sim/emcn/icons'
import { ThinkingLoader } from '@/components/ui'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import { FeaturePlatformPanel } from '@/app/(landing)/enterprise/components/feature-graphics/feature-platform-panel'
interface CodeSegment {
text: string
tone?: 'muted' | 'primary'
}
/** The `support-agent.ts` contents, split into tone-colored typewriter segments. */
const CODE_LINES: CodeSegment[][] = [
[
{ text: 'import', tone: 'muted' },
{ text: ' ' },
{ text: '{ agent }', tone: 'primary' },
{ text: ' ' },
{ text: 'from', tone: 'muted' },
{ text: ' ' },
{ text: "'@sim/sdk'", tone: 'primary' },
],
[
{ text: 'const', tone: 'muted' },
{ text: ' ' },
{ text: 'supportAgent', tone: 'primary' },
{ text: ' ' },
{ text: '= await', tone: 'muted' },
{ text: ' ' },
{ text: 'agent', tone: 'primary' },
],
[{ text: ' .workflow({' }],
[
{ text: ' ' },
{ text: 'name:', tone: 'muted' },
{ text: ' ' },
{ text: "'Support agent'", tone: 'primary' },
{ text: ',' },
],
[{ text: ' ' }, { text: 'instructions:', tone: 'muted' }],
[{ text: ' ' }, { text: "'Answer customer questions'", tone: 'primary' }, { text: ',' }],
[
{ text: ' ' },
{ text: 'tools:', tone: 'muted' },
{ text: ' ' },
{ text: '[zendesk, slack]', tone: 'primary' },
{ text: ',' },
],
[{ text: ' })' }],
]
const CODE_LINE_LENGTHS = CODE_LINES.map((line) =>
line.reduce((total, segment) => total + segment.text.length, 0)
)
const CODE_LINE_STARTS = CODE_LINE_LENGTHS.map((_, index) =>
CODE_LINE_LENGTHS.slice(0, index).reduce((total, length) => total + length, 0)
)
const CODE_TOTAL_CHARS = CODE_LINE_LENGTHS.reduce((total, length) => total + length, 0)
const PROMPT = 'Create a support agent that answers customer questions'
const REPLY =
'On it — scaffolding a support agent with Zendesk and Slack that answers customer questions.'
const REPLY_WORDS = REPLY.split(' ')
const CODE_START_MS = 500
const CODE_CHAR_MS = 24
const COMPOSER_IN_MS = 5300
const PROMPT_START_MS = 6000
const PROMPT_CHAR_MS = 55
const SEND_MS = 9600
const CHAT_ENTER_MS = 10100
const REPLY_MS = 11600
const REPLY_WORD_MS = 80
const FADE_MS = 15100
const LOOP_MS = 15700
type BuildMethodsPhase =
| 'idle'
| 'code'
| 'composer'
| 'prompt'
| 'exit'
| 'chat'
| 'reply'
| 'fade'
const SEGMENT_TONE_CLASS = {
muted: 'text-[var(--text-muted)]',
primary: 'text-[var(--text-primary)]',
} as const
/** Renders one code line clipped to the number of characters typed so far. */
function renderCodeLine(segments: CodeSegment[], visibleChars: number) {
const rendered = []
let remaining = visibleChars
for (let index = 0; index < segments.length && remaining > 0; index++) {
const segment = segments[index]
rendered.push(
<span key={index} className={segment.tone && SEGMENT_TONE_CLASS[segment.tone]}>
{segment.text.slice(0, remaining)}
</span>
)
remaining -= segment.text.length
}
return rendered
}
/**
* Decorative loop for the "Build visually or with code" tile: the
* `support-agent.ts` editor types itself out, the platform composer slides up
* and receives a typed prompt, and the send beat swaps the editor for a
* headerless Mothership chat surface — the same agent built from code or from
* a message. All window bodies sit on `--white`, matching the hero surfaces.
*
* Window handoffs are exit-before-enter orchestration, never crossfades: the
* editor fully exits (slides down and fades over 380ms) during the `exit`
* phase, the stage holds empty for a beat, and only then does the chat enter
* (slide-up fade, 420ms). The composer is a pure transform slide from below
* the shell's clipped edge — its opacity never animates, so it never reads as
* a translucent layer over the editor. Panel transitions are disabled on the
* `idle` reset so state snaps back while the scene-level fade has the tile at
* zero opacity. Local timers + CSS transitions only, mirroring the other
* landing loops; under `prefers-reduced-motion` it renders the finished
* editor + composer as a static frame.
*/
export function BuildMethodsGraphic() {
const [phase, setPhase] = useState<BuildMethodsPhase>('idle')
const [typedCodeChars, setTypedCodeChars] = useState(0)
const [typedPromptChars, setTypedPromptChars] = useState(0)
const [revealedReplyWords, setRevealedReplyWords] = useState(0)
const [reducedMotion, setReducedMotion] = useState(false)
useEffect(() => {
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
const timeouts: ReturnType<typeof setTimeout>[] = []
const intervals: ReturnType<typeof setInterval>[] = []
const clearScheduled = () => {
for (const timeout of timeouts) clearTimeout(timeout)
for (const interval of intervals) clearInterval(interval)
timeouts.length = 0
intervals.length = 0
}
const showFinished = () => {
setReducedMotion(true)
setPhase('composer')
setTypedCodeChars(CODE_TOTAL_CHARS)
setTypedPromptChars(0)
setRevealedReplyWords(0)
}
const startCodeTyping = () => {
setPhase('code')
const startedAt = performance.now()
const interval = setInterval(() => {
const elapsed = performance.now() - startedAt
const next = Math.min(Math.floor(elapsed / CODE_CHAR_MS) + 1, CODE_TOTAL_CHARS)
setTypedCodeChars(next)
if (next >= CODE_TOTAL_CHARS) clearInterval(interval)
}, CODE_CHAR_MS)
intervals.push(interval)
}
const startPromptTyping = () => {
setPhase('prompt')
const startedAt = performance.now()
const interval = setInterval(() => {
const elapsed = performance.now() - startedAt
const next = Math.min(Math.floor(elapsed / PROMPT_CHAR_MS) + 1, PROMPT.length)
setTypedPromptChars(next)
if (next >= PROMPT.length) clearInterval(interval)
}, PROMPT_CHAR_MS)
intervals.push(interval)
}
const startReply = () => {
setPhase('reply')
const startedAt = performance.now()
const interval = setInterval(() => {
const elapsed = performance.now() - startedAt
const next = Math.min(Math.floor(elapsed / REPLY_WORD_MS) + 1, REPLY_WORDS.length)
setRevealedReplyWords(next)
if (next >= REPLY_WORDS.length) clearInterval(interval)
}, REPLY_WORD_MS)
intervals.push(interval)
}
const runLoop = () => {
clearScheduled()
setReducedMotion(false)
setPhase('idle')
setTypedCodeChars(0)
setTypedPromptChars(0)
setRevealedReplyWords(0)
timeouts.push(setTimeout(startCodeTyping, CODE_START_MS))
timeouts.push(setTimeout(() => setPhase('composer'), COMPOSER_IN_MS))
timeouts.push(setTimeout(startPromptTyping, PROMPT_START_MS))
timeouts.push(setTimeout(() => setPhase('exit'), SEND_MS))
timeouts.push(setTimeout(() => setPhase('chat'), CHAT_ENTER_MS))
timeouts.push(setTimeout(startReply, REPLY_MS))
timeouts.push(setTimeout(() => setPhase('fade'), FADE_MS))
timeouts.push(setTimeout(runLoop, LOOP_MS))
}
const syncMotionPreference = () => {
clearScheduled()
if (media.matches) {
showFinished()
return
}
runLoop()
}
syncMotionPreference()
media.addEventListener('change', syncMotionPreference)
return () => {
media.removeEventListener('change', syncMotionPreference)
clearScheduled()
}
}, [])
const codeTypingActive = phase === 'code' && typedCodeChars < CODE_TOTAL_CHARS
const composerVisible = reducedMotion || !['idle', 'code'].includes(phase)
const editorExited = !reducedMotion && ['exit', 'chat', 'reply', 'fade'].includes(phase)
const chatOpen = !reducedMotion && ['chat', 'reply', 'fade'].includes(phase)
const promptText = phase === 'prompt' || phase === 'exit' ? PROMPT.slice(0, typedPromptChars) : ''
const replyText = REPLY_WORDS.slice(0, revealedReplyWords).join(' ')
const lastStartedLine = CODE_LINE_STARTS.reduce(
(last, start, index) => (typedCodeChars > start ? index : last),
-1
)
return (
<FeatureGraphicShell>
<div
className={cn(
'absolute inset-0 transition-opacity duration-300 ease-out motion-reduce:transition-none',
phase === 'fade' ? 'opacity-0' : 'opacity-100'
)}
>
<FeaturePlatformPanel
className={cn(
'top-5 bg-[var(--white)] transition-[transform,opacity] duration-[380ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
phase === 'idle' && 'transition-none',
editorExited ? 'translate-y-16 opacity-0' : 'translate-y-0 opacity-100'
)}
icon={FolderCode}
title='support-agent.ts'
>
<div className='min-h-[190px] space-y-2 p-4 font-mono text-caption leading-[1.7]'>
{CODE_LINES.map((line, index) =>
typedCodeChars > CODE_LINE_STARTS[index] ? (
<div key={index} className='flex gap-3'>
<span className='w-3 select-none text-right text-[var(--text-muted)]'>
{index + 1}
</span>
<code>
{renderCodeLine(line, typedCodeChars - CODE_LINE_STARTS[index])}
{codeTypingActive && index === lastStartedLine && (
<span className='ml-px inline-block h-[1.1em] w-px translate-y-[2px] animate-pulse bg-[var(--text-primary)] align-text-bottom' />
)}
</code>
</div>
) : null
)}
</div>
</FeaturePlatformPanel>
<div
aria-hidden='true'
className={cn(
'absolute top-5 right-0 bottom-0 left-0 overflow-hidden rounded-tl-xl border-[var(--border-1)] border-t border-l bg-[var(--white)] shadow-sm transition-[transform,opacity] duration-[420ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
phase === 'idle' && 'transition-none',
chatOpen ? 'translate-y-0 opacity-100' : 'translate-y-6 opacity-0'
)}
>
<div className='flex flex-col gap-3 p-4'>
<div className='max-w-[80%] self-end rounded-lg bg-[var(--surface-3)] px-3 py-2 text-[var(--text-primary)] text-caption leading-[1.5]'>
{PROMPT}
</div>
{phase === 'chat' && <ThinkingLoader size={18} phase labelRatio={0.6} />}
<p
className={cn(
'text-[var(--text-primary)] text-caption leading-[1.6] transition-opacity duration-200 ease-out',
phase === 'reply' || phase === 'fade' ? 'opacity-100' : 'opacity-0'
)}
>
{replyText}
</p>
</div>
</div>
<div
className={cn(
'absolute right-3 bottom-5 left-5 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm transition-transform duration-[450ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',
phase === 'idle' && 'transition-none',
composerVisible ? 'translate-y-0' : 'translate-y-[130%]'
)}
>
<p
className={cn(
'px-1 text-caption',
promptText ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'
)}
>
{promptText || 'Send message to Sim'}
{phase === 'prompt' && typedPromptChars < PROMPT.length && (
<span className='ml-px inline-block h-[1.1em] w-px translate-y-[2px] animate-pulse bg-[var(--text-primary)] align-text-bottom' />
)}
</p>
<div className='mt-2 flex items-center gap-1'>
<span className='flex size-6 items-center justify-center'>
<Plus className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='flex size-6 items-center justify-center'>
<Paperclip className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='flex size-6 items-center justify-center'>
<Slash className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='ml-auto flex size-6 items-center justify-center'>
<Mic className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='flex size-7 items-center justify-center rounded-full bg-[var(--text-primary)]'>
<ArrowUp className='size-[14px] text-[var(--text-inverse)]' />
</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,141 @@
/**
* Shared 4.5s animation timeline for the DeployGraphic vignette: the connector
* line sweep and the browser outline trace. Each connector line is a 1px
* faint grey track (styled in the TSX) clipping an absolutely-positioned
* white sweep that travels top to bottom; the lower line's sweep is delayed
* so the two read as one continuous flow. The moment the lower sweep's
* leading edge lands on the browser's top edge the outline trace takes over:
* four rounded-rect border overlay copies (a top pair and a side pair,
* mirrored left/right) are revealed by animated clip-path inset windows, so
* the white line splits at top-center, draws outward along the top border,
* bends around the exact rounded-corner geometry, and continues down the
* sides while the tail releases from center and follows the head — a
* traveling pulse that runs out and off the outline. Under
* prefers-reduced-motion everything is static: plain faint lines, no sweep,
* no trace.
*
* Timeline (percent of 4.5s): each sweep's full pass spans 22% (~1s); the
* lower sweep is delayed 0.9s, so its leading edge reaches the line bottom at
* 0.9s + 0.5s = 1.395s = 31%, where the trace begins with no gap. Trace head
* runs center → corner over 31→43%, head descends the sides while the tail
* crosses the top over 43→55%, and the tail descends and exits the open
* bottom by 67%; the loop rests until 100%. The top windows keep a 14px-tall
* strip (12px radius + 1px border on each box edge) so the corner arc
* belongs to the top pair; the side pair starts 13px down, exactly where the
* arc ends.
*/
.sweep {
position: absolute;
inset: 0;
background: var(--text-inverse);
transform: translateY(-101%);
animation: deploy-line-sweep 4.5s linear infinite;
}
.sweepLower {
animation-delay: 0.9s;
}
@keyframes deploy-line-sweep {
0% {
transform: translateY(-101%);
}
22% {
transform: translateY(101%);
}
100% {
transform: translateY(101%);
}
}
.traceTopLeft {
clip-path: inset(0 50% calc(100% - 14px) 50%);
animation: deploy-trace-top-left 4.5s linear infinite;
}
.traceTopRight {
clip-path: inset(0 50% calc(100% - 14px) 50%);
animation: deploy-trace-top-right 4.5s linear infinite;
}
.traceSideLeft {
clip-path: inset(13px calc(100% - 14px) calc(100% - 13px) 0);
animation: deploy-trace-side-left 4.5s linear infinite;
}
.traceSideRight {
clip-path: inset(13px 0 calc(100% - 13px) calc(100% - 14px));
animation: deploy-trace-side-right 4.5s linear infinite;
}
@keyframes deploy-trace-top-left {
0%,
31% {
clip-path: inset(0 50% calc(100% - 14px) 50%);
}
43% {
clip-path: inset(0 50% calc(100% - 14px) 0);
}
55%,
100% {
clip-path: inset(0 100% calc(100% - 14px) 0);
}
}
@keyframes deploy-trace-top-right {
0%,
31% {
clip-path: inset(0 50% calc(100% - 14px) 50%);
}
43% {
clip-path: inset(0 0 calc(100% - 14px) 50%);
}
55%,
100% {
clip-path: inset(0 0 calc(100% - 14px) 100%);
}
}
@keyframes deploy-trace-side-left {
0%,
43% {
clip-path: inset(13px calc(100% - 14px) calc(100% - 13px) 0);
}
55% {
clip-path: inset(13px calc(100% - 14px) 0 0);
}
67%,
100% {
clip-path: inset(100% calc(100% - 14px) 0 0);
}
}
@keyframes deploy-trace-side-right {
0%,
43% {
clip-path: inset(13px 0 calc(100% - 13px) calc(100% - 14px));
}
55% {
clip-path: inset(13px 0 0 calc(100% - 14px));
}
67%,
100% {
clip-path: inset(100% 0 0 calc(100% - 14px));
}
}
@media (prefers-reduced-motion: reduce) {
.sweep {
display: none;
animation: none;
}
.traceTopLeft,
.traceTopRight,
.traceSideLeft,
.traceSideRight {
display: none;
animation: none;
}
}
@@ -0,0 +1,144 @@
import type { CSSProperties } from 'react'
import { ChipTag, chipContentLabelClass, chipGeometryClass, cn } from '@sim/emcn'
import { CircleCheck, Lock } from '@sim/emcn/icons'
import { ThinkingLoader } from '@/components/ui'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/deploy-graphic.module.css'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
/**
* The ThinkingLoader's light-grey material (its dark-surface theme from
* `thinking-loader.module.css`), asserted inline because the always-light
* landing would otherwise ink the loader dark — invisible on the dark
* Deploy button.
*/
const DEPLOY_LOADER_INK = {
'--tl-grad-inner': '#a7a7a7',
'--tl-grad-outer': '#d6d6d6',
'--tl-glow': 'rgba(255, 255, 255, 0.9)',
} as CSSProperties
/**
* The moment of a one-click deploy, told top to bottom: the agent being
* shipped, the Deploy button, and a
* minimal dark browser window rising from the bottom edge with the hosted
* endpoint's URL in its address bar — the deployed agent is literally a live
* web address, no infrastructure in between. The window is an outlined shell
* (grey hairline, tile shows through) whose address bar carries the Deploy
* button's `--text-muted` fill, so click and outcome read as one system. A
* single causal vignette instead of a workflow canvas, so the click itself is
* the subject. Faint `--text-muted-inverse` hairlines (mixed toward the dark
* tile background so they read as guides) link agent → button → browser, and
* a white progress sweep (from `deploy-graphic.module.css`) loops down the
* two hairlines in sequence on a shared 4.5s timeline — pill → button, then
* button → browser. The instant the sweep's leading edge lands on the
* browser's top edge, the outline trace takes over with no gap: the white
* line splits at the top-center connection point and draws outward both
* ways along the top border, bends around the rounded corners, and
* continues down the sides while its tail releases from center and follows
* the head — a traveling pulse that runs out and off the outline rather
* than painting it permanently. It is implemented as four rounded-rect
* border overlay copies (top pair + side pair, mirrored) revealed by
* animated `clip-path` inset windows, so the trace follows the exact
* rounded-corner geometry at any width. Under `prefers-reduced-motion`
* everything is static — plain faint lines, no sweep or trace.
*
* The Deploy button's icon is the gooey {@link ThinkingLoader} pinned to
* `relay` — the Return shape (work coming back) — looping its ball pass,
* inked light so it reads on the dark button.
*
* The agent header follows the nested-corner radius rule: the `v3` ChipTag is
* `rounded-md` (6px) and sits 6px (`py-1.5` / `pr-1.5`) from the container
* edge, so the container radius is 6px + 6px = 12px.
*
* 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 browser window is pinned to `h-24` (96px) so its top edge lands on the
* same line as the neighboring build-methods tile's composer (76px tall +
* `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.
*/
export function DeployGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
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>
</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)]'>
<span className={styles.sweep} />
</span>
<span
className={cn(
chipGeometryClass,
'mx-0 mt-2.5 inline-flex h-9 rounded-[10px] bg-[var(--text-muted)] px-3 text-[var(--text-inverse)]'
)}
>
<ThinkingLoader variant='relay' size={18} style={DEPLOY_LOADER_INK} />
<span className={cn(chipContentLabelClass, 'text-[15px] text-current')}>Deploy</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)]'>
<span className={cn(styles.sweep, styles.sweepLower)} />
</span>
<div className='relative h-24 w-full rounded-t-xl border border-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)] border-b-0'>
<span
className={cn(
'-inset-px absolute rounded-t-xl border border-[var(--text-inverse)] border-b-0',
styles.traceTopLeft
)}
/>
<span
className={cn(
'-inset-px absolute rounded-t-xl border border-[var(--text-inverse)] border-b-0',
styles.traceTopRight
)}
/>
<span
className={cn(
'-inset-px absolute rounded-t-xl border border-[var(--text-inverse)] border-b-0',
styles.traceSideLeft
)}
/>
<span
className={cn(
'-inset-px absolute rounded-t-xl border border-[var(--text-inverse)] border-b-0',
styles.traceSideRight
)}
/>
<div className='flex items-center gap-2 px-2.5 pt-2.5 pb-1'>
<span className='flex shrink-0 gap-1'>
<span className='size-2 rounded-full bg-[var(--text-muted-inverse)]' />
<span className='size-2 rounded-full bg-[var(--text-muted-inverse)]' />
<span className='size-2 rounded-full bg-[var(--text-muted-inverse)]' />
</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>
</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
</span>
<span className='shrink-0 text-[var(--text-muted-inverse)] text-caption'>Just now</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,14 @@
import type { ReactNode } from 'react'
interface FeatureGraphicShellProps {
children: ReactNode
}
/** Shared crop canvas for platform-faithful enterprise feature previews. */
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 h-full min-h-[260px]'>{children}</div>
</div>
)
}
@@ -0,0 +1,41 @@
import type { ComponentType, ReactNode, SVGProps } from 'react'
import { cn } from '@sim/emcn'
interface FeaturePlatformPanelProps {
children: ReactNode
className?: string
icon: ComponentType<SVGProps<SVGSVGElement>>
title: string
}
/**
* A cropped workspace surface using the same lightweight header treatment as
* Sim's product views. Individual graphics provide real platform controls as
* the content rather than drawing a separate illustration language. The
* window anchors to the slot's left edge (`left-0`) so it left-aligns with
* the tile's title/description text column, bleeding off the right edge.
*/
export function FeaturePlatformPanel({
children,
className,
icon: Icon,
title,
}: FeaturePlatformPanelProps) {
return (
<div
aria-hidden='true'
className={cn(
'absolute right-0 bottom-0 left-0 overflow-hidden rounded-tl-xl border-[var(--border-1)] border-t border-l bg-[var(--surface-2)] shadow-sm',
className
)}
>
<div className='flex h-12 items-center gap-2 border-[var(--border)] border-b px-4'>
<span className='flex size-6 items-center justify-center rounded-md bg-[var(--surface-5)]'>
<Icon className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='font-medium text-[var(--text-primary)] text-base'>{title}</span>
</div>
{children}
</div>
)
}
@@ -0,0 +1,14 @@
export { AccessControlGraphic } from './access-control-graphic'
export { AuditTrailGraphic } from './audit-trail-graphic'
export { BuildMethodsGraphic } from './build-methods-graphic'
export { DeployGraphic } from './deploy-graphic'
export { FeatureGraphicShell } from './feature-graphic-shell'
export { FeaturePlatformPanel } from './feature-platform-panel'
export { ItPlatformTeamsGraphic } from './it-platform-teams-graphic'
export { LifecycleGraphic } from './lifecycle-graphic'
export { OperationsTeamsGraphic } from './operations-teams-graphic'
export { RollbackGraphic } from './rollback-graphic'
export { RunMonitoringGraphic } from './run-monitoring-graphic'
export { StagingGraphic } from './staging-graphic'
export { StandardsGraphic } from './standards-graphic'
export { TechnicalTeamsGraphic } from './technical-teams-graphic'
@@ -0,0 +1,32 @@
/**
* The ItPlatformTeamsGraphic's only motion: a soft ring pulse blooming
* from the Active policy tag — the family's shared quiet 6s
* state-confirmation beat (the staging tile's Promote button, the
* technical tile's Approved tag). The tag itself is borderless
* (`shadow-none` strips the gray variant's inset ring, matching the
* Approved tag), so the keyframes bloom from a bare pill. Under
* prefers-reduced-motion the pulse is removed and the tag sits static.
*/
.activePulse {
animation: it-platform-active-pulse 6s ease-out infinite;
}
@keyframes it-platform-active-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) {
.activePulse {
animation: none;
}
}
@@ -0,0 +1,120 @@
import { ChipTag, cn } from '@sim/emcn'
import { CircleCheck } from '@sim/emcn/icons'
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 {
/** Enforced control name, one per pillar of the tile's claim. */
label: string
/** Right-aligned enforcement detail. */
detail: string
}
/**
* The three enforced controls mirror the tile's claim word for word —
* access controls (SSO), governance (reviewer approval), and audit
* trails (retention).
*/
const CONTROLS: readonly PolicyControl[] = [
{ label: 'Single sign-on', detail: 'Enforced' },
{ label: 'Reviewer approval', detail: 'Required' },
{ label: 'Audit history', detail: '90 days' },
] as const
/**
* IT governance told as a frameless policy vignette (the audit and
* monitoring tiles' composition): no window chrome — a small "Workspace
* policies" header sits directly on the tile ground, attributed on the
* right to the managing team: a 16px gradient avatar (the access and
* audit tiles' team-avatar treatment) beside a `Managed by IT` mono
* ChipTag, its fill stepped up to `--surface-6` so the pill stays
* legible on the grey ground (the staging and rollback tiles'
* environment-pill treatment). Below it, the tile's highlight: the active Production
* policy lifted onto a white card in the audit tile's exact chrome
* (`--white` fill, 1px `--border-1` hairline, `rounded-xl`, `shadow-sm`)
* pairing the policy name and its scope line with an `Active` tag that
* carries the tile's one motion, the family's shared quiet 6s ring pulse
* (from `it-platform-teams-graphic.module.css`, removed under
* `prefers-reduced-motion`). Under the card, the policy's enforced
* controls read as quiet hairline-ruled rows — a passing circle-check,
* the control name, and a right-aligned enforcement detail — one row per
* pillar of the tile's claim: access controls, governance, audit trails.
*
* The avatar asset is a grey radial gradient on a black square, so it
* sits in a `rounded-full overflow-hidden` clip with a slight scale-up
* to crop the black canvas past the circle's edge.
*
* 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 policy
* name truncates instead of clipping.
*/
export function ItPlatformTeamsGraphic() {
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-3 flex items-center justify-between gap-2'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-base'>
Workspace
</span>
<span className='flex shrink-0 items-center gap-1.5'>
<span className='relative size-4 overflow-hidden rounded-full shadow-sm'>
<Image
src='/landing/team-avatar-3.jpg'
alt=''
width={16}
height={16}
className='size-full scale-110 object-cover'
/>
</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Managed by IT
</ChipTag>
</span>
</div>
<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
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
Applies to every workspace
</span>
</span>
<ChipTag variant='gray' className={cn('shrink-0 shadow-none', styles.activePulse)}>
Active
</ChipTag>
</div>
<div className='mt-1.5 px-3'>
{CONTROLS.map((control, index) => (
<div
key={control.label}
className={cn(
'flex h-9 items-center gap-2',
index > 0 && 'border-[var(--border-1)] border-t'
)}
>
<CircleCheck className='size-[13px] shrink-0 text-[var(--text-icon)]' />
<span className='min-w-0 flex-1 truncate text-[var(--text-secondary)] text-caption'>
{control.label}
</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>
{control.detail}
</span>
</div>
))}
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,30 @@
/**
* The LifecycleGraphic's only motion: a soft ring pulse blooming from the
* live version node every 6s — quiet state confirmation rather than travel,
* keeping this tile stiller than the deploy tile beside it. The spine's
* connector lines are static (styled in the TSX). Under
* prefers-reduced-motion the pulse is removed and the node sits static.
*/
.livePulse {
animation: lifecycle-live-pulse 6s ease-out infinite;
}
@keyframes lifecycle-live-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 35%, 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) {
.livePulse {
animation: none;
}
}
@@ -0,0 +1,115 @@
import { ChipTag, cn } from '@sim/emcn'
import { Clock } from '@sim/emcn/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/lifecycle-graphic.module.css'
/** Older timeline entries below the live/saved pair, quietest first to render. */
const OLDER_VERSIONS = [
{ detail: 'Published Jun 28', label: 'v1' },
{ detail: 'Saved Jun 21', label: 'v0.9' },
{ detail: 'Saved Jun 14', label: 'v0.8' },
] as const
/**
* A version-history timeline housed in an outlined window shell that mirrors
* the build tile's editor window exactly — same slot geometry (`top-5`,
* `left-0` so the window left-aligns with the tile's text column, bled
* right/bottom), same `rounded-tl-xl` radius, same top + left
* hairline `--border-1` edges — but drawn with no fill so the tile shows
* through: the two side-by-side tiles read as the same window in the same
* place, one filled, one outlined. An outlined title bar ("Versions" with a
* `Clock` icon in the build header's `size-6` `rounded-md` icon box — drawn
* as a `--border-1` hairline outline instead of the filled grey — no fill,
* `--border-1` bottom rule at the build header's `h-12` height) crowns the
* window. Inside, a quiet vertical spine of faint
* static 1px connector segments (the light analog of the deploy tile's guide
* lines) links version nodes bottom-up — older versions lower and
* progressively quieter, v3 at the top as the live entry. The live row is a
* white pill card following the nested-corner radius rule (6px ChipTag + 6px
* gap = 12px container), echoing the deploy tile's agent header; older rows
* are bare text rows that fade toward `--text-muted` (the v2 `Saved`
* tag's fill stepped up to `--surface-6` so the pill stays legible on
* the grey ground), and a mask gradient
* dissolves the oldest entries so the timeline reads as continuing into
* history past the window's bottom edge.
*
* The only motion is a soft ring pulse on the live node (from
* `lifecycle-graphic.module.css`), removed under `prefers-reduced-motion`.
*/
export function LifecycleGraphic() {
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'>
<span className='flex size-6 items-center justify-center rounded-md border border-[var(--border-1)]'>
<Clock className='size-[14px] text-[var(--text-icon)]' />
</span>
<span className='font-medium text-[var(--text-primary)] text-base'>Versions</span>
</div>
<div className='flex flex-col p-4 [mask-image:linear-gradient(to_bottom,black_45%,transparent_98%)]'>
<div className='flex items-center gap-3'>
<span className='flex w-2.5 justify-center'>
<span
className={cn('size-2.5 rounded-full bg-[var(--text-primary)]', styles.livePulse)}
/>
</span>
<span className='flex min-w-0 flex-1 items-center gap-2'>
<span className='flex items-center gap-1.5 rounded-[12px] bg-[var(--white)] py-1.5 pr-1.5 pl-2.5 shadow-sm'>
<span className='font-medium text-[var(--text-primary)] text-small'>v3</span>
<ChipTag variant='solid'>Live</ChipTag>
</span>
<span className='truncate text-[var(--text-muted)] text-caption'>
Published today
</span>
</span>
</div>
<div className='flex gap-3'>
<span className='flex w-2.5 justify-center'>
<span className='h-9 w-px bg-[color:color-mix(in_srgb,var(--text-muted)_35%,transparent)]' />
</span>
</div>
<div className='flex items-center gap-3'>
<span className='flex w-2.5 justify-center'>
<span className='size-2 rounded-full border border-[var(--text-muted)] bg-[var(--surface-3)]' />
</span>
<span className='flex min-w-0 flex-1 items-center gap-2'>
<span className='font-medium text-[var(--text-secondary)] text-small'>v2</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Saved
</ChipTag>
<span className='truncate text-[var(--text-muted)] text-caption'>
Published yesterday
</span>
</span>
</div>
{OLDER_VERSIONS.map((version) => (
<div key={version.label} className='flex flex-col'>
<div className='flex gap-3'>
<span className='flex w-2.5 justify-center'>
<span className='h-9 w-px bg-[color:color-mix(in_srgb,var(--text-muted)_35%,transparent)]' />
</span>
</div>
<div className='flex items-center gap-3'>
<span className='flex w-2.5 justify-center'>
<span className='size-2 rounded-full border border-[color:color-mix(in_srgb,var(--text-muted)_60%,transparent)] bg-[var(--surface-3)]' />
</span>
<span className='flex min-w-0 flex-1 items-center gap-2'>
<span className='text-[var(--text-muted)] text-small'>{version.label}</span>
<span className='truncate text-[var(--text-muted)] text-caption'>
{version.detail}
</span>
</span>
</div>
</div>
))}
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,380 @@
/**
* One shared 16s timeline of four 4s route phases, a precomputed shuffle
* so the routing feels varied without true randomness:
*
* phase 0 (0%25%): Zendesk -> Slack
* phase 1 (25%50%): Sheets -> Jira
* phase 2 (50%75%): Gmail -> Salesforce
* phase 3 (75%100%): Zendesk -> Jira
*
* Within each 4s phase: the source tag's white filled state crossfades
* in over its outline (00.5s), a white pulse falls down its wire
* (0.41.2s), dwells in the router while the hub blooms (1.22s),
* travels out to the destination (22.8s), the destination tag's fill
* crossfades in as the route connects (2.73.1s), both hold (to 3.6s),
* then fade back to outlined (to 4s).
*
* Tag fill overlays rest transparent (opacity-0, set inline in the TSX)
* so under prefers-reduced-motion — where every animation here is
* disabled — all six tags render outlined and static.
*/
.pulse {
stroke-dasharray: 0.28 1;
stroke-dashoffset: 0.28;
opacity: 0;
}
.pulseInA {
animation: ops-pulse-in-a 16s linear infinite;
}
@keyframes ops-pulse-in-a {
0%,
2.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
2.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
7.5% {
stroke-dashoffset: -1;
opacity: 1;
}
7.6%,
77.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
77.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
82.5% {
stroke-dashoffset: -1;
opacity: 1;
}
82.6%,
100% {
stroke-dashoffset: -1;
opacity: 0;
}
}
.pulseInB {
animation: ops-pulse-in-b 16s linear infinite;
}
@keyframes ops-pulse-in-b {
0%,
27.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
27.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
32.5% {
stroke-dashoffset: -1;
opacity: 1;
}
32.6%,
100% {
stroke-dashoffset: -1;
opacity: 0;
}
}
.pulseInC {
animation: ops-pulse-in-c 16s linear infinite;
}
@keyframes ops-pulse-in-c {
0%,
52.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
52.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
57.5% {
stroke-dashoffset: -1;
opacity: 1;
}
57.6%,
100% {
stroke-dashoffset: -1;
opacity: 0;
}
}
.pulseOutA {
animation: ops-pulse-out-a 16s linear infinite;
}
@keyframes ops-pulse-out-a {
0%,
12.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
12.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
17.5% {
stroke-dashoffset: -1;
opacity: 1;
}
17.6%,
100% {
stroke-dashoffset: -1;
opacity: 0;
}
}
.pulseOutB {
animation: ops-pulse-out-b 16s linear infinite;
}
@keyframes ops-pulse-out-b {
0%,
37.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
37.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
42.5% {
stroke-dashoffset: -1;
opacity: 1;
}
42.6%,
87.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
87.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
92.5% {
stroke-dashoffset: -1;
opacity: 1;
}
92.6%,
100% {
stroke-dashoffset: -1;
opacity: 0;
}
}
.pulseOutC {
animation: ops-pulse-out-c 16s linear infinite;
}
@keyframes ops-pulse-out-c {
0%,
62.4% {
stroke-dashoffset: 0.28;
opacity: 0;
}
62.5% {
stroke-dashoffset: 0.28;
opacity: 1;
}
67.5% {
stroke-dashoffset: -1;
opacity: 1;
}
67.6%,
100% {
stroke-dashoffset: -1;
opacity: 0;
}
}
.fillSrcA {
animation: ops-fill-src-a 16s linear infinite;
}
@keyframes ops-fill-src-a {
0% {
opacity: 0;
}
3.1% {
opacity: 1;
}
22.5% {
opacity: 1;
}
24.9%,
75% {
opacity: 0;
}
78.1% {
opacity: 1;
}
97.5% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fillSrcB {
animation: ops-fill-src-b 16s linear infinite;
}
@keyframes ops-fill-src-b {
0%,
25% {
opacity: 0;
}
28.1% {
opacity: 1;
}
47.5% {
opacity: 1;
}
49.9%,
100% {
opacity: 0;
}
}
.fillSrcC {
animation: ops-fill-src-c 16s linear infinite;
}
@keyframes ops-fill-src-c {
0%,
50% {
opacity: 0;
}
53.1% {
opacity: 1;
}
72.5% {
opacity: 1;
}
74.9%,
100% {
opacity: 0;
}
}
.fillDstA {
animation: ops-fill-dst-a 16s linear infinite;
}
@keyframes ops-fill-dst-a {
0%,
16.9% {
opacity: 0;
}
19.4% {
opacity: 1;
}
22.5% {
opacity: 1;
}
24.9%,
100% {
opacity: 0;
}
}
.fillDstB {
animation: ops-fill-dst-b 16s linear infinite;
}
@keyframes ops-fill-dst-b {
0%,
41.9% {
opacity: 0;
}
44.4% {
opacity: 1;
}
47.5% {
opacity: 1;
}
49.9%,
91.9% {
opacity: 0;
}
94.4% {
opacity: 1;
}
97.5% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fillDstC {
animation: ops-fill-dst-c 16s linear infinite;
}
@keyframes ops-fill-dst-c {
0%,
66.9% {
opacity: 0;
}
69.4% {
opacity: 1;
}
72.5% {
opacity: 1;
}
74.9%,
100% {
opacity: 0;
}
}
.routerBloom {
animation: ops-router-bloom 4s linear infinite;
}
@keyframes ops-router-bloom {
0%,
30% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-muted-inverse) 45%, transparent);
}
50% {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--text-muted-inverse) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-muted-inverse) 0%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.pulseInA,
.pulseInB,
.pulseInC,
.pulseOutA,
.pulseOutB,
.pulseOutC,
.fillSrcA,
.fillSrcB,
.fillSrcC,
.fillDstA,
.fillDstB,
.fillDstC,
.routerBloom {
animation: none;
}
}
@@ -0,0 +1,299 @@
import type { CSSProperties } from 'react'
import { cn } from '@sim/emcn'
import { ThinkingLoader } from '@/components/ui'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/operations-teams-graphic.module.css'
/**
* Fixed pixel canvas the switchboard is drawn on, centered inside the
* shell. Kept to 280px wide (chips reaching only x 32250) so every chip
* stays inside the tile's visible area even at the narrowest three-up
* tile width (~274px visible at 1024), and 248px tall — the tallest
* canvas that clears the slot's shortest inner box (~257px at 1024 after
* bottom-bleed compensation) so the staggered top pills never clip while
* the wires keep long vertical runs.
*/
const CANVAS = { WIDTH: 280, HEIGHT: 248 } as const
/**
* The ThinkingLoader's light-grey material (its dark-surface theme from
* `thinking-loader.module.css`), asserted inline exactly as the deploy
* tile's Deploy button does — the always-light landing would otherwise
* ink the loader dark, invisible on the dark tile ground showing through
* the outlined router hub.
*/
const ROUTER_LOADER_INK = {
'--tl-grad-inner': '#a7a7a7',
'--tl-grad-outer': '#d6d6d6',
'--tl-glow': 'rgba(255, 255, 255, 0.9)',
} as CSSProperties
interface Port {
/** Tailwind classes positioning the size-2 port dot, centered on the port's canvas x/y. */
dotClass: string
/** Tailwind classes placing the tag's center on the port's canvas x, above/below its dot. */
chipClass: string
/** Tool name rendered as an outlined tag beside the port. */
label: string
/** CSS-module animation class driving this tag's white connect-fill on its scheduled phases. */
fillClass: 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
* sits lowest at the left (x 56), Sheets between at the right (x 224).
*/
const SOURCES: readonly Port[] = [
{
dotClass: 'top-[48px] left-[52px]',
chipClass: 'top-[32px] left-[56px]',
label: 'Gmail',
fillClass: styles.fillSrcC,
},
{
dotClass: 'top-[28px] left-[136px]',
chipClass: 'top-[12px] left-[140px]',
label: 'Zendesk',
fillClass: styles.fillSrcA,
},
{
dotClass: 'top-[38px] left-[220px]',
chipClass: 'top-[22px] left-[224px]',
label: 'Sheets',
fillClass: styles.fillSrcB,
},
] as const
/**
* Outbound destinations mirrored below, staggered the same way: Slack at
* the left (x 56), Salesforce hanging lowest on the center axis (x 140),
* Jira highest at the right (x 224).
*/
const DESTINATIONS: readonly Port[] = [
{
dotClass: 'top-[192px] left-[52px]',
chipClass: 'top-[216px] left-[56px]',
label: 'Slack',
fillClass: styles.fillDstA,
},
{
dotClass: 'top-[212px] left-[136px]',
chipClass: 'top-[236px] left-[140px]',
label: 'Salesforce',
fillClass: styles.fillDstC,
},
{
dotClass: 'top-[202px] left-[220px]',
chipClass: 'top-[226px] left-[224px]',
label: 'Jira',
fillClass: styles.fillDstB,
},
] as const
/**
* Wires from each source port gathering into the router hub's top pole
* (140, 93) — all three terminate at the same point so they read as a
* tied bundle feeding the agent, with vertical tangents at both ends
* (the access tile's edge geometry) and long vertical runs so each curve
* travels before it arrives.
*/
const IN_PATHS = {
gmail: 'M 56 52 C 56 74 140 66 140 93',
zendesk: 'M 140 32 L 140 93',
sheets: 'M 224 42 C 224 70 140 64 140 93',
} as const
/**
* Wires fanning out from the router hub's bottom pole (140, 155) — the
* shared origin splitting to each destination, echoing the deploy tile's
* single pipeline branching.
*/
const OUT_PATHS = {
slack: 'M 140 155 C 140 182 56 174 56 196',
salesforce: 'M 140 155 L 140 216',
jira: 'M 140 155 C 140 184 224 178 224 206',
} as const
/** Faint-ink stroke for the resting wires (the deploy tile's guide-line grey, quieter). */
const QUIET_STROKE = 'color-mix(in srgb, var(--text-muted-inverse) 28%, transparent)'
/** Shared 1px outline ink for the tags, port dots, and router hub ring. */
const OUTLINE_INK = 'border-[color:color-mix(in_srgb,var(--text-muted-inverse)_45%,transparent)]'
/** Shared SVG props for a resting wire. */
const WIRE_PROPS = { stroke: QUIET_STROKE, strokeWidth: '1' } as const
/** Shared SVG props for a traveling white request-pulse overlay on a wire. */
const PULSE_PROPS = {
pathLength: 1,
stroke: 'var(--text-inverse)',
strokeWidth: '1.25',
strokeLinecap: 'round',
} as const
/** An outlined integration tag whose white connect-fill crossfades in on its scheduled phases. */
function PortTag({ port }: { port: Port }) {
return (
<span
className={cn('-translate-x-1/2 -translate-y-1/2 absolute whitespace-nowrap', port.chipClass)}
>
<span
className={cn(
'relative flex h-5 items-center overflow-hidden rounded-md border bg-transparent px-1.5 font-medium text-[var(--text-muted-inverse)] text-caption',
OUTLINE_INK
)}
>
{port.label}
<span
className={cn(
'absolute inset-0 flex items-center bg-[var(--white)] px-1.5 text-[var(--text-primary)] opacity-0',
port.fillClass
)}
>
{port.label}
</span>
</span>
</span>
)
}
/**
* Operations automation told as a vertical switchboard — the access tile's
* top-to-bottom composition (sources above, curved bezier edges flowing
* down to what they feed) in the dark tiles' ink: three ops tools wired in
* across the top (Gmail, Zendesk, Sheets), an outlined circular router hub
* at center — the agent — and three destinations wired out below (Slack,
* Salesforce, Jira). Both rows stagger their tag heights into a loose
* constellation, giving each wire its own long arc instead of ranking the
* tags into flat rows. Every element is drawn in the same outlined
* language: the six tool tags rest as transparent pills with 1px light
* hairline borders and light-grey ink, the hub is a transparent circle
* with the same hairline ring, and the wires are 1px curved SVG paths
* with vertical tangents (the access tile's edge geometry) gathering into
* the hub's top pole and fanning from its bottom pole — a tied bundle in,
* a split out.
*
* Inside the hub, the platform's gooey {@link ThinkingLoader} runs its
* default full morph cycle (no pinned variant, unlike the deploy button's
* `relay`) wearing the deploy button's exact light-grey ink override, so
* the page's two dark-tile loaders read as the same material — the agent
* visibly churning through work between dispatches. The loader stops
* cycling on its own under `prefers-reduced-motion`.
*
* Motion (from `operations-teams-graphic.module.css`, one shared 16s
* timeline of four 4s route phases — Zendesk→Slack, Sheets→Jira,
* Gmail→Salesforce, Zendesk→Jira — a precomputed shuffle so the routing
* feels varied): in each phase the source tag's white filled state
* crossfades in over its outline (ink flipping dark with it), a white
* pulse falls down its wire into the router — which blooms the family's
* ring pulse while the agent classifies — then out the bottom pole to
* the destination tag, whose fill crossfades in as the route connects.
* Both ends hold briefly, fade back to outlined, and the next pair
* begins. Everything is static and outlined under
* `prefers-reduced-motion`.
*
* The feature tile's visual slot bleeds `2rem` right and bottom (`1.5rem`
* under `max-lg`) but not left or top, so the canvas sits inside a
* matching right- and bottom-padded box to land on the tile's visible
* 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.
*/
export function OperationsTeamsGraphic() {
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]'>
<svg
className='absolute inset-0'
fill='none'
viewBox={`0 0 ${CANVAS.WIDTH} ${CANVAS.HEIGHT}`}
width={CANVAS.WIDTH}
height={CANVAS.HEIGHT}
>
<path d={IN_PATHS.gmail} {...WIRE_PROPS} />
<path d={IN_PATHS.zendesk} {...WIRE_PROPS} />
<path d={IN_PATHS.sheets} {...WIRE_PROPS} />
<path d={OUT_PATHS.slack} {...WIRE_PROPS} />
<path d={OUT_PATHS.salesforce} {...WIRE_PROPS} />
<path d={OUT_PATHS.jira} {...WIRE_PROPS} />
<path
d={IN_PATHS.zendesk}
className={cn(styles.pulse, styles.pulseInA)}
{...PULSE_PROPS}
/>
<path
d={IN_PATHS.sheets}
className={cn(styles.pulse, styles.pulseInB)}
{...PULSE_PROPS}
/>
<path
d={IN_PATHS.gmail}
className={cn(styles.pulse, styles.pulseInC)}
{...PULSE_PROPS}
/>
<path
d={OUT_PATHS.slack}
className={cn(styles.pulse, styles.pulseOutA)}
{...PULSE_PROPS}
/>
<path
d={OUT_PATHS.jira}
className={cn(styles.pulse, styles.pulseOutB)}
{...PULSE_PROPS}
/>
<path
d={OUT_PATHS.salesforce}
className={cn(styles.pulse, styles.pulseOutC)}
{...PULSE_PROPS}
/>
</svg>
{SOURCES.map((port) => (
<span key={port.label}>
<span
className={cn(
'absolute size-2 rounded-full border bg-[var(--text-secondary)]',
OUTLINE_INK,
port.dotClass
)}
/>
<PortTag port={port} />
</span>
))}
{DESTINATIONS.map((port) => (
<span key={port.label}>
<span
className={cn(
'absolute size-2 rounded-full border bg-[var(--text-secondary)]',
OUTLINE_INK,
port.dotClass
)}
/>
<PortTag port={port} />
</span>
))}
<div
className={cn(
'absolute top-[93px] left-[109px] flex size-[62px] items-center justify-center rounded-full border',
OUTLINE_INK,
styles.routerBloom
)}
>
<ThinkingLoader size={36} style={ROUTER_LOADER_INK} />
</div>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,123 @@
import { Button, ChipTag } from '@sim/emcn'
import { Undo } from '@sim/emcn/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
/**
* Rollback told as a "return to known-good" gesture 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 and lifecycle windows. A "Version history" title
* bar with a right-aligned `Production` mono ChipTag (its fill stepped up
* to `--surface-6` so the pill stays legible on the grey ground) crowns
* the window at the family's `h-12` header height, ruled by a hairline
* divider.
*
* Inside, a short newest-first version rail runs down the left gutter —
* v4 as the quiet current row (its `Current` tag on the grey-ground
* `--surface-6` pill fill), then a plain vertical hairline connector
* down to v3, the lifecycle timeline's spine language. v3 is the tile's
* highlight: the node fills solid and
* the row lifts onto a white card (`--white` fill, 1px `--border-1`
* hairline, nested `rounded-lg` inside the `rounded-tl-xl` window,
* `shadow-sm`) pairing "v3 · Stable · Maya Chen" with the tile's
* strongest element, the solid Roll back button led by a small `Undo`
* glyph inked in the button's inverse text color. A quieter v2 row
* dissolves through a mask gradient below, implying the history continues
* past the window's bled bottom edge — the lifecycle tile's fade device.
*
* 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` = the bleed plus the usual `1rem` gutter)
* to keep the Production tag and the v3 card's Roll back button fully
* inside the visible tile.
*
* The rail is fully static. The connector hairlines are absolutely
* positioned inside fixed-height spacer rows so they can extend past the
* row box into the adjacent rows' empty gutter space, ending a couple of
* pixels from each node — the lifecycle spine's tight node-to-line
* clearance — without disturbing the flow layout.
*/
export function RollbackGraphic() {
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 justify-between gap-2 border-[var(--border-1)] border-b px-4 pr-12 max-lg:pr-10'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-base'>
Version history
</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Production
</ChipTag>
</div>
<div className='p-4 pr-12 [mask-image:linear-gradient(to_bottom,black_55%,transparent_98%)] max-lg:pr-10'>
<div className='flex items-center gap-3'>
<span className='flex w-2.5 shrink-0 justify-center'>
<span className='size-2 rounded-full border border-[var(--text-muted)] bg-[var(--surface-3)]' />
</span>
<span className='flex min-w-0 flex-1 items-center gap-2'>
<span className='font-medium text-[var(--text-secondary)] text-small'>v4</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Current
</ChipTag>
</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>Published 2h ago</span>
</div>
<div className='flex'>
<span className='relative flex h-[38px] w-2.5 shrink-0 justify-center'>
<span className='-top-[4px] -bottom-[24px] absolute w-px bg-[color:color-mix(in_srgb,var(--text-muted)_35%,transparent)]' />
</span>
</div>
<div className='flex items-center gap-3'>
<span className='flex w-2.5 shrink-0 justify-center'>
<span className='size-2.5 rounded-full bg-[var(--text-primary)]' />
</span>
<div className='flex min-w-0 flex-1 items-center gap-3 rounded-lg border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm'>
<span className='min-w-0 flex-1'>
<span className='flex items-center gap-2'>
<span className='font-medium text-[var(--text-primary)] text-small'>v3</span>
<ChipTag variant='mono'>Stable</ChipTag>
</span>
<span className='mt-0.5 block truncate text-[var(--text-muted)] text-caption'>
Maya Chen · Jun 30
</span>
</span>
<Button
variant='primary'
size='sm'
tabIndex={-1}
className='pointer-events-none shrink-0 gap-1'
>
<Undo className='size-[12px]' />
Roll back
</Button>
</div>
</div>
<div className='flex'>
<span className='relative flex h-[28px] w-2.5 shrink-0 justify-center'>
<span className='-top-[24px] -bottom-[4px] absolute w-px bg-[color:color-mix(in_srgb,var(--text-muted)_35%,transparent)]' />
</span>
</div>
<div className='flex items-center gap-3'>
<span className='flex w-2.5 shrink-0 justify-center'>
<span className='size-2 rounded-full border border-[color:color-mix(in_srgb,var(--text-muted)_60%,transparent)] bg-[var(--surface-3)]' />
</span>
<span className='flex min-w-0 flex-1 items-center gap-2'>
<span className='text-[var(--text-muted)] text-small'>v2</span>
<span className='truncate text-[var(--text-muted)] text-caption'>Saved Jun 24</span>
</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,30 @@
/**
* The RunMonitoringGraphic's only motion: a soft ring pulse blooming from
* the header's live-status dot every 6s — the family's shared quiet
* state-confirmation beat (the lifecycle tile's live node, the staging
* tile's Promote button). Under prefers-reduced-motion the pulse is
* removed and the dot sits static.
*/
.livePulse {
animation: run-monitoring-live-pulse 6s ease-out infinite;
}
@keyframes run-monitoring-live-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-secondary) 35%, 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) {
.livePulse {
animation: none;
}
}
@@ -0,0 +1,128 @@
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 {
/** 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
}
/**
* The Log Details keys the real panel leads with, distilled to tile scale:
* workflow name, shortened run id, trigger, and duration. The workspace
* UI's green Level/Trigger tags become quiet grey mono chips per the
* 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>,
},
] as const
/**
* The workspace's Log Details panel told as a frameless vignette (the
* access and standards tiles' composition): no window chrome — a small
* "Log details" header with a live-status dot (the tile's one emphasized
* motion) sits directly on the tile ground above the panel's key-value
* ledger — Workflow, shortened Run ID, Trigger, and Duration as airy
* rows ruled by quiet 1px `--border-1` hairlines, the real UI's green
* tags quieted to grey mono chips (fills stepped up to `--surface-6` so
* the pills stay legible on the grey ground). The closing "Workflow
* output" section
* lifts its two-line JSON fragment onto the tile's highlight: a white
* card in the audit tile's exact chrome (`--white` fill, 1px `--border-1`
* hairline, rounded, `shadow-sm`), the run's payload presented as the
* artifact worth watching.
*
* The only motion is the soft ring pulse on the live dot (from
* `run-monitoring-graphic.module.css`), the family's shared quiet 6s
* beat, 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
* workflow value and JSON lines truncate instead of clipping.
*/
export function RunMonitoringGraphic() {
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'>
Log details
</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>
</div>
{LOG_FIELDS.map((field, index) => (
<div
key={field.label}
className={cn(
'flex h-9 items-center justify-between gap-3',
index > 0 && 'border-[var(--border-1)] border-t'
)}
>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>{field.label}</span>
{field.value}
</div>
))}
<div className='mt-2'>
<span className='block text-[var(--text-muted)] text-caption'>Workflow output</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)]'>,</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)]'>{' }'}</span>
</div>
</div>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,30 @@
/**
* The StagingGraphic's only motion: a soft ring pulse blooming from the
* Promote button — the same quiet 6s state-confirmation beat as the
* access tile's grant node and the lifecycle tile's live node, keeping
* the family's shared rhythm. Under prefers-reduced-motion the pulse is
* removed and the button sits static.
*/
.promotePulse {
animation: staging-promote-pulse 6s ease-out infinite;
}
@keyframes staging-promote-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) {
.promotePulse {
animation: none;
}
}
@@ -0,0 +1,123 @@
import { Button, ChipTag, cn } from '@sim/emcn'
import { ArrowRight, CircleCheck } from '@sim/emcn/icons'
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/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
/**
* A GitHub-style promotion surface compressed to tile scale, on the
* lifecycle tile's outlined ground: the window is a 1px `--border-1`
* hairline outline with no fill (tile grey showing through,
* `rounded-xl`), topped by a plain title header ("Support agent" + a
* `v4` mono tag on the grey-ground `--surface-6` pill fill). Below,
* three hairline-ruled sections tell the
* merge-area story with an inverted surface hierarchy — the build being
* promoted is the highlight, a white card (`--white` fill, `--border-1`
* hairline, nested `rounded-lg`, `shadow-sm`, echoing the audit tile's
* selected-record card) pairing a short hash chip with its change
* message and a "Maya Chen · 2h ago" attribution line (gradient avatar
* shared with the access and audit tiles); the named check gates sit
* directly on the grey ground as quiet passing rows; and the promotion
* bar pairs `Staging → Live` environment tags (fills stepped up to
* `--surface-6` so the pills stay legible on the grey ground) with the
* tile's strongest element, the solid Promote button.
*
* The only motion is a soft ring pulse on the Promote button (from
* `staging-graphic.module.css`), the family's shared quiet 6s beat,
* removed under `prefers-reduced-motion`.
*
* The avatar asset is a grey radial gradient on a black square, so it
* sits in a `rounded-full overflow-hidden` clip with a slight scale-up to
* crop the black canvas past the circle's edge.
*
* The feature tile's visual slot bleeds `2rem` right and bottom
* (`1.5rem` under `max-lg`) but not left or top, so this centered window
* adds matching right and bottom padding to land on the tile's visible
* center instead of the bled slot's center — keeping comparable air
* above and below the window. The section paddings stay compact
* (`h-10` header, `py-2.5` sections) so the window's intrinsic height
* plus the bottom-bleed compensation fits inside the slot's
* `overflow-hidden` bounds — a taller window would push the top hairline
* and corners past the slot's top clip line and truncate them. The
* window is fluid (`w-full max-w-[312px]`) so it never
* exceeds the compensated slot and both edges stay visible at narrow
* 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.
*/
export function StagingGraphic() {
return (
<FeatureGraphicShell>
<div
aria-hidden='true'
className='absolute inset-0 flex items-center justify-center pr-8 pb-8 max-lg:pr-6 max-lg:pb-6'
>
<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
</span>
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
v4
</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>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-small'>
Tighten refund policy checks
</span>
</span>
<span className='mt-1.5 flex items-center gap-1.5'>
<span className='relative size-4 overflow-hidden rounded-full'>
<Image
src='/landing/team-avatar-1.jpg'
alt=''
width={16}
height={16}
className='size-full scale-110 object-cover'
/>
</span>
<span className='text-[var(--text-muted)] text-caption'>Maya Chen · 2h ago</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) => (
<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>
</span>
))}
</div>
<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
</ChipTag>
<ArrowRight className='size-[12px] shrink-0 text-[var(--text-icon)]' />
<ChipTag variant='mono' className='bg-[var(--surface-6)]'>
Live
</ChipTag>
</span>
<Button
variant='primary'
size='sm'
tabIndex={-1}
className={cn('pointer-events-none ml-auto', styles.promotePulse)}
>
Promote
</Button>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,106 @@
/**
* The StandardsGraphic's only motion: the certification seal blooms the
* row's shared 6s ring pulse — the same quiet state-confirmation beat as
* the lifecycle tile's live node and the access tile's grant node, inked
* with the deploy tile's light `--text-muted-inverse` so it reads on the
* dark `--text-secondary` tile surface. Removed under
* prefers-reduced-motion so the seal sits static.
*/
.sealPulse {
animation: standards-seal-pulse 6s ease-out infinite;
}
@keyframes standards-seal-pulse {
0%,
20% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-muted-inverse) 55%, transparent);
}
36% {
box-shadow: 0 0 0 6px color-mix(in srgb, var(--text-muted-inverse) 0%, transparent);
}
100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--text-muted-inverse) 0%, transparent);
}
}
/**
* Connector draw loop, following the deploy tile's dash vocabulary:
* paths normalized to pathLength=1 draw in, hold, then un-draw forward
* (dashoffset continues to -1 so the line exits in its direction of
* travel) on the same 6s timeline as the seal pulse. The trailing
* connector (seal → Open source) runs the identical keyframes
* phase-shifted by 0.7s so the two lines read as a stagger.
*/
.connector {
stroke-dasharray: 1;
stroke-dashoffset: 1;
animation: standards-connector-draw 6s ease-in-out infinite;
}
.connectorTrailing {
animation-delay: 0.7s;
}
@keyframes standards-connector-draw {
0% {
stroke-dashoffset: 1;
}
16% {
stroke-dashoffset: 0;
}
68% {
stroke-dashoffset: 0;
}
84%,
100% {
stroke-dashoffset: -1;
}
}
/**
* Orbital ring sweeps — the deploy tile's white progress sweep bent around
* the certificate rings: each ring carries a fixed 50° arc stroked with a
* comet gradient (bright at the clockwise leading end, transparent at the
* tail) inside a group that rotates around the seal center for a slow,
* seamless orbit. Rotation replaced the earlier stroke-dash offset loop
* because a dash that wraps the circle's path start renders a seam
* artifact (a stray cap segment at the wrap point); a rotating group has
* no seam. Both orbits share the 14s period; the outer ring runs half a
* revolution out of phase so the two sweeps never align.
*/
.ringSweep {
transform-box: view-box;
transform-origin: 160px 112px;
animation: standards-ring-orbit 14s linear infinite;
}
.ringSweepOuter {
animation-delay: -7s;
}
@keyframes standards-ring-orbit {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.sealPulse {
animation: none;
}
.connector {
animation: none;
stroke-dashoffset: 0;
}
.ringSweep {
display: none;
}
}
@@ -0,0 +1,164 @@
import { ChipTag, cn } from '@sim/emcn'
import { ShieldCheck } from '@sim/emcn/icons'
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics/feature-graphic-shell'
import styles from '@/app/(landing)/enterprise/components/feature-graphics/standards-graphic.module.css'
/**
* Enterprise standards told as a certification seal, with no window
* framing: a shield-check medallion at the center wearing the deploy
* tile's Deploy-button chrome — the same `--text-muted` fill with
* `--text-inverse` ink — so the two dark tiles' hero elements read as
* siblings. It is ringed by two concentric hairlines like a certificate
* mark, with the outer ring dissolving through a mask the way the row's
* older elements fade. The tile's two claims anchor the seal from either
* side over short hairline connectors with port dots (the access tile's
* junction vocabulary): a `SOC 2` chip and an `Open source` chip, whose
* mono `--surface-5` fills read as light pills on the dark ground
* exactly like the deploy tile's ChipTag. Each chip is anchored by its
* connector-facing edge to the connector's endpoint (not centered on a
* fixed x), so both junctions stay flush regardless of label width. A `Verified` pill beneath the seal carries the emphasized
* state on the deploy tile's `--text-muted` chip fill with `--text-inverse`
* ink (ChipTag's solid variant would vanish here — its fill is the tile's
* own `--text-secondary`). The seal blooms the row's shared 6s ring pulse
* (from `standards-graphic.module.css`, removed under
* `prefers-reduced-motion`).
*
* Inks follow the deploy tile's dark-surface palette: hairlines and dots
* mix `--text-muted-inverse` toward transparent (45% on the working
* lines, fainter on the decorative rings), and the port dots fill with
* the tile's `--text-secondary` so the connector reads as passing
* beneath them.
*
* The two connectors are SVG paths normalized to `pathLength=1` so they
* draw in with the page's dash vocabulary (the deploy tile's stagger):
* SOC 2 draws into the seal first, the seal draws out to Open source a
* beat later, both hold, then un-draw forward and loop on the shared 6s
* timeline. Under `prefers-reduced-motion` they render fully drawn with
* no animation.
*
* Each ring also carries a comet arc — the deploy tile's white connector
* sweep made circular: a fixed 50° arc over the hairline, stroked with a
* `userSpaceOnUse` linear gradient that is bright at the clockwise
* leading tip and fades to transparent along the trailing side, inside a
* group that rotates around the seal center (a rotating group rather
* than a dash offset, because a dash wrapping the circle's path start
* renders a stray seam-cap artifact). The two orbits share one 14s
* period but the outer runs phase-shifted half a revolution, so the
* sweeps are never synchronized; the outer sweep sits inside the same
* fade mask as its hairline so it dissolves through the bottom of the
* tile like the ring it traces. Under `prefers-reduced-motion` the
* sweeps are hidden entirely.
*
* The feature tile's visual slot bleeds `2rem` right (`1.5rem` under
* `max-lg`) but not left, so the canvas sits inside a matching
* right-padded box to land on the tile's visible center instead of the
* 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.
*/
export function StandardsGraphic() {
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='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)]' />
<svg
className='absolute inset-0'
fill='none'
viewBox='0 0 320 250'
width={320}
height={250}
>
<defs>
<linearGradient
id='standards-sweep-inner'
gradientUnits='userSpaceOnUse'
x1='235.5'
y1='112'
x2='208.53'
y2='169.84'
>
<stop offset='0' stopColor='var(--text-inverse)' stopOpacity='0' />
<stop offset='1' stopColor='var(--text-inverse)' stopOpacity='0.9' />
</linearGradient>
<linearGradient
id='standards-sweep-outer'
gradientUnits='userSpaceOnUse'
x1='271.5'
y1='112'
x2='231.67'
y2='197.41'
>
<stop offset='0' stopColor='var(--text-inverse)' stopOpacity='0' />
<stop offset='1' stopColor='var(--text-inverse)' stopOpacity='0.9' />
</linearGradient>
</defs>
<g className='[mask-image:linear-gradient(to_bottom,black_30%,transparent_92%)]'>
<g className={cn(styles.ringSweep, styles.ringSweepOuter)}>
<path
d='M 271.5 112 A 111.5 111.5 0 0 1 231.67 197.41'
stroke='url(#standards-sweep-outer)'
strokeWidth='1'
strokeLinecap='round'
/>
</g>
</g>
<g className={styles.ringSweep}>
<path
d='M 235.5 112 A 75.5 75.5 0 0 1 208.53 169.84'
stroke='url(#standards-sweep-inner)'
strokeWidth='1'
strokeLinecap='round'
/>
</g>
<path
d='M 90 112.5 L 122 112.5'
pathLength={1}
className={styles.connector}
stroke='color-mix(in srgb, var(--text-muted-inverse) 45%, transparent)'
strokeWidth='1'
/>
<path
d='M 198 112.5 L 230 112.5'
pathLength={1}
className={cn(styles.connector, styles.connectorTrailing)}
stroke='color-mix(in srgb, var(--text-muted-inverse) 45%, transparent)'
strokeWidth='1'
/>
</svg>
<span className='absolute top-[108px] left-[118px] size-2 rounded-full border border-[color:color-mix(in_srgb,var(--text-muted-inverse)_70%,transparent)] bg-[var(--text-secondary)]' />
<span className='absolute top-[108px] left-[194px] size-2 rounded-full border border-[color:color-mix(in_srgb,var(--text-muted-inverse)_70%,transparent)] bg-[var(--text-secondary)]' />
<div
className={cn(
'absolute top-[74px] left-[122px] flex size-[76px] items-center justify-center rounded-full bg-[var(--text-muted)] shadow-sm',
styles.sealPulse
)}
>
<ShieldCheck className='size-7 text-[var(--text-inverse)]' />
</div>
<span className='-translate-x-full -translate-y-1/2 absolute top-[112px] left-[90px] whitespace-nowrap'>
<ChipTag variant='mono'>SOC 2</ChipTag>
</span>
<span className='-translate-y-1/2 absolute top-[112px] left-[230px] whitespace-nowrap'>
<ChipTag variant='mono'>Open source</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
</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}
@@ -0,0 +1,31 @@
/**
* The TechnicalTeamsGraphic's only motion: a soft ring pulse blooming
* from the Approved tag on the review card — the same quiet 6s
* state-confirmation beat as the staging tile's Promote button and the
* audit tile's newest-entry avatar, keeping the family's shared rhythm.
* Under prefers-reduced-motion the pulse is removed and the tag sits
* static.
*/
.approvedPulse {
animation: technical-approved-pulse 6s ease-out infinite;
}
@keyframes technical-approved-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) {
.approvedPulse {
animation: none;
}
}
@@ -0,0 +1,120 @@
import { ChipTag, cn } from '@sim/emcn'
import { CircleCheck } from '@sim/emcn/icons'
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 {
/** Gutter marker — space for context, `-` for removed, `+` for added. */
marker: ' ' | '-' | '+'
/** The code content after the marker. */
code: string
}
/**
* A one-word behavior change to the same `Support agent` config the
* build tile types out — the surrounding config lines ground the excerpt
* as real code while the `-`/`+` pair stays the focal point.
*/
const DIFF_LINES: readonly DiffLine[] = [
{ marker: ' ', code: " name: 'Support agent'," },
{ marker: ' ', code: ' instructions:' },
{ marker: ' ', code: " 'Answer customer questions'," },
{ marker: ' ', code: ' tools: [zendesk, slack],' },
{ marker: '-', code: " onError: 'retry'," },
{ marker: '+', code: " onError: 'escalate'," },
{ marker: ' ', code: '})' },
] as const
/** Per-marker ink treatments: added strongest, removed and context quiet. */
const MARKER_TONES: Record<DiffLine['marker'], string> = {
' ': 'text-[var(--text-muted)]',
'-': 'text-[var(--text-muted)] opacity-70',
'+': 'text-[var(--text-primary)]',
}
/**
* Technical collaboration told as a distilled code-review vignette, in
* the frameless composition its row-mates share (the IT tile's policy
* ledger, the operations tile's handoff): no window chrome and no header
* — the tile leads straight with the change under review, a seven-line
* mono excerpt of the Support-agent config sitting bare on the tile.
* Context and removed lines read in quiet `--text-muted`, the single
* added line carrying `--text-primary` ink so the edit is the first
* thing read. The review's
* verdict is the tile's one highlight: a white card in the audit tile's
* exact chrome (`--white` fill, 1px `--border-1` hairline, `rounded-xl`,
* `shadow-sm`) pairing the reviewer — gradient avatar (shared with the
* access, audit, and staging tiles), name, and an "Approved these
* changes" attribution line — with an `Approved` tag that carries the
* tile's only motion, the family's shared quiet 6s ring pulse (from
* `technical-teams-graphic.module.css`, removed under
* `prefers-reduced-motion`). A closing hairline-ruled row counts the
* resolved review threads, keeping the together-ness of the claim
* without a second emphasis.
*
* The avatar asset is a grey radial gradient on a black square, so it
* sits in a `rounded-full overflow-hidden` clip with a slight scale-up
* to crop the black canvas past the circle's edge.
*
* 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 — diff lines
* and the reviewer's attribution truncate instead of clipping.
*/
export function TechnicalTeamsGraphic() {
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='px-3 font-mono text-caption leading-[1.8]'>
{DIFF_LINES.map((line) => (
<div
key={`${line.marker}${line.code}`}
className={cn('truncate whitespace-pre', MARKER_TONES[line.marker])}
>
{line.marker} {line.code}
</div>
))}
</div>
<div className='mt-3 flex items-center gap-3 rounded-xl border border-[var(--border-1)] bg-[var(--white)] px-3 py-2.5 shadow-sm'>
<span className='relative size-7 shrink-0 overflow-hidden rounded-full shadow-sm'>
<Image
src='/landing/team-avatar-2.jpg'
alt=''
width={28}
height={28}
className='size-full scale-110 object-cover'
/>
</span>
<span className='min-w-0 flex-1'>
<span className='block truncate font-medium text-[var(--text-primary)] text-small'>
Jordan Lee
</span>
<span className='block truncate text-[var(--text-muted)] text-caption'>
Approved these changes
</span>
</span>
<ChipTag variant='gray' className={cn('shrink-0', styles.approvedPulse)}>
Approved
</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
</span>
<span className='shrink-0 text-[var(--text-muted)] text-caption'>2 of 2</span>
</div>
</div>
</div>
</FeatureGraphicShell>
)
}