chore: pivot to Wordbench — remove Originmain codebase, add PRD
Clears the prior AI design-engineering product and replaces the repo with the Wordbench (agentic WordPress development environment) PRD.
This commit is contained in:
@@ -1,169 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(pnpm --version)",
|
||||
"Bash(pnpm install *)",
|
||||
"Bash(pnpm run *)",
|
||||
"Bash(pnpm approve-builds *)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:www.figma.com)",
|
||||
"mcp__Claude_in_Chrome__tabs_context_mcp",
|
||||
"Bash(curl -s \"https://github.com/features/codespaces\")",
|
||||
"Bash(curl -s \"https://www.figma.com/draw/\")",
|
||||
"Bash(curl -s \"https://www.figma.com/make/\")",
|
||||
"Bash(curl -s \"https://github.githubassets.com/assets/primer-primitives-0f570a01cbe448fb.css\")",
|
||||
"Bash(pnpm --filter @originmain/app run build)",
|
||||
"Bash(pnpm --filter @originmain/app install)",
|
||||
"Bash(pnpm add *)",
|
||||
"Bash(xargs ls *)",
|
||||
"Bash(grep -E \"\\\\.\\(ts|tsx\\)$\")",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:trees.software)",
|
||||
"Bash(pnpm exec *)",
|
||||
"WebFetch(domain:diffs.com)",
|
||||
"WebFetch(domain:diff.com)",
|
||||
"Bash(npm show *)",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\({k:d.get\\(k\\) for k in ['description','main','types','exports','peerDependencies','keywords']}, indent=2\\)\\)\")",
|
||||
"Bash(pnpm --filter @originmain/diff-engine test)",
|
||||
"Bash(npx tsc *)",
|
||||
"Bash(npm test *)",
|
||||
"Skill(claude-api)",
|
||||
"Skill(claude-api:*)",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\({**d.get\\('dependencies',{}\\), **d.get\\('devDependencies',{}\\)}, indent=2\\)\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\('exports', d.get\\('main', ''\\)\\), indent=2\\)\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(d['version']\\)\")",
|
||||
"Bash(node_modules/.bin/tsc --noEmit)",
|
||||
"Bash(echo \"exit: $?\")",
|
||||
"Bash(echo \"agent-bridge: $?\")",
|
||||
"Bash(echo \"integrations: $?\")",
|
||||
"Bash(echo \"agent-bridge exit: $?\")",
|
||||
"Bash(echo \"integrations exit: $?\")",
|
||||
"Bash(echo \"platform exit: $?\")",
|
||||
"Bash(node -e \"const c = require\\('./node_modules/.pnpm/@clerk+nextjs@7.2.7_next@15.3.1_react-dom@19.2.5_react@19.2.5__react@19.2.5__react@19.2.5_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/@clerk/nextjs/dist/cjs/index.js'\\); console.log\\(Object.keys\\(c\\).filter\\(k => /show|signed/i.test\\(k\\)\\)\\)\")",
|
||||
"Bash(echo \"port 3000 status: $?\")",
|
||||
"Bash(curl -s http://localhost:3002)",
|
||||
"Bash(xargs ps *)",
|
||||
"Bash(curl -s http://localhost:3000 -L)",
|
||||
"Bash(pnpm typecheck *)",
|
||||
"Bash(pnpm build *)",
|
||||
"Bash(tee /tmp/build-out.txt)",
|
||||
"Bash(echo \"EXIT:$?\")",
|
||||
"Bash(npx next *)",
|
||||
"Bash(echo \"EXIT_CODE:$?\")",
|
||||
"Bash(node_modules/.bin/next build *)",
|
||||
"Bash(./node_modules/.bin/next build *)",
|
||||
"Bash(../../node_modules/.bin/next build *)",
|
||||
"Bash(/Users/USER/Desktop/originmain/node_modules/.bin/next build *)",
|
||||
"Bash(echo \"BUILD_EXIT:$?\")",
|
||||
"Bash(node /Users/USER/Desktop/originmain/node_modules/.pnpm/next@15.5.15_@playwright+test@1.59.1_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/next/dist/bin/next build)",
|
||||
"Bash(pkill -f \"next build\")",
|
||||
"Bash(pnpm --filter @originmain/app build)",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/linear.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/cursor.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/anthropic.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/typescript.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/nextdotjs.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/react.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg\" -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/visualstudiocode.svg\" -o /dev/null -w \"%{http_code}\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/_data/simple-icons.json\")",
|
||||
"Bash(python3 -c ' *)",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/simple-icons.json\")",
|
||||
"Bash(curl -s \"https://api.github.com/repos/simple-icons/simple-icons/contents/icons\")",
|
||||
"Bash(curl -sI \"https://cdn.simpleicons.org/slack\")",
|
||||
"Bash(curl -sI \"https://cdn.simpleicons.org/visualstudiocode\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/master/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/master/icons/visualstudiocode.svg\")",
|
||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('tag_name',''\\)\\)\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -s \"https://raw.githubusercontent.com/simple-icons/simple-icons/16.18.0/_data/simple-icons.json\")",
|
||||
"Bash(curl -s \"https://registry.npmjs.org/simple-icons/latest\")",
|
||||
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(d.get\\('version',''\\)\\)\")",
|
||||
"Bash(curl -s \"https://unpkg.com/simple-icons/icons/slack.svg\")",
|
||||
"Bash(curl -s \"https://unpkg.com/simple-icons/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons/icons/slack.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons/simple-icons.json\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@16.18.0/\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@16.18.0/?meta\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@13.22.0/icons/slack.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@13.22.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/icons/slack.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@14.0.0/?meta\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@9.0.0/icons/visualstudiocode.svg\")",
|
||||
"Bash(curl -sL \"https://unpkg.com/simple-icons@9.0.0/simple-icons.json\")",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/14.0.0/_data/simple-icons.json\" -o /tmp/si14.json)",
|
||||
"Read(//tmp/**)",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/9.0.0/_data/simple-icons.json\" -o /tmp/si9.json)",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/_data/simple-icons.json\" -o /tmp/si_dev.json)",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/cursor.svg\")",
|
||||
"Bash(curl -s \"https://api.github.com/repos/simple-icons/simple-icons/git/trees/develop?recursive=0\")",
|
||||
"Bash(curl -sL \"https://raw.githubusercontent.com/simple-icons/simple-icons/develop/data/simple-icons.json\" -o /tmp/si_develop.json)",
|
||||
"Bash(pnpm --filter @originmain/agent-bridge exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/integrations exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/multiplayer exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/platform exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/ai-layer exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/design-language exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/origin-graph exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter originmain-app exec tsc --noEmit)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:cdn.simpleicons.org)",
|
||||
"WebFetch(domain:brandfetch.com)",
|
||||
"WebFetch(domain:simpleicons.org)",
|
||||
"WebFetch(domain:www.svgrepo.com)",
|
||||
"WebFetch(domain:worldvectorlogo.com)",
|
||||
"Bash(pnpm --filter @originmain/app exec tsc --noEmit)",
|
||||
"Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/sign-in/\\\\[\\\\[...sign-in\\\\]\\\\])",
|
||||
"Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/sign-up/\\\\[\\\\[...sign-up\\\\]\\\\])",
|
||||
"Bash(python3)",
|
||||
"Bash(mkdir -p \"/Users/USER/Desktop/originmain/packages/app/src/app/workspace/[wid]\")",
|
||||
"Bash(mkdir -p \"/Users/USER/Desktop/originmain/packages/app/src/app/workspace/[wid]/project/new\")",
|
||||
"Bash(mkdir -p \"/Users/USER/Desktop/originmain/packages/app/src/app/workspace/[wid]/project/[pid]\")",
|
||||
"Bash(git -C /Users/USER/Desktop/originmain log --oneline --follow -- packages/origin-graph/migrations/002_artboard_ancestry.sql packages/origin-graph/migrations/003_rls_policies.sql packages/origin-graph/migrations/004_add_notes_to_diffs.sql)",
|
||||
"Bash(git -C /Users/USER/Desktop/originmain diff --stat HEAD)",
|
||||
"Bash(git -C /Users/USER/Desktop/originmain status --short)",
|
||||
"Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/tokens)",
|
||||
"Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/projects/\\\\[pid\\\\])",
|
||||
"Bash(mkdir -p /Users/USER/Desktop/originmain/packages/app/src/app/api/workspace/\\\\[id\\\\]/invite)",
|
||||
"Bash(grep anthropic *)",
|
||||
"Bash(npx --yes npm-check-updates --packageFile /Users/USER/Desktop/originmain/packages/ai-layer/package.json --filter \"@anthropic-ai/sdk\")",
|
||||
"Bash(pnpm --filter @originmain/app tsc --noEmit)",
|
||||
"Bash(pnpm --filter @originmain/ai-layer add @anthropic-ai/sdk@latest)",
|
||||
"Bash(pnpm --filter @originmain/origin-graph test)",
|
||||
"Bash(grep -E \"\\\\.d\\\\.ts$\")",
|
||||
"Bash(pnpm -w ls @anthropic-ai/sdk)",
|
||||
"Bash(grep -v \"\\\\.map$\")",
|
||||
"Bash(perl -pi -e 's/background: '\\\\''#FAFAFA'\\\\''/background: '\\\\''var\\(--page-bg\\)'\\\\''/g' /Users/USER/Desktop/originmain/packages/app/src/app/onboarding/page.tsx /Users/USER/Desktop/originmain/packages/app/src/app/error.tsx)",
|
||||
"Bash(node -e ' *)",
|
||||
"Bash(python3 -c \"import sys,json; [print\\(m['id']\\) for m in json.load\\(sys.stdin\\).get\\('data',[]\\)]\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\({k:d[k] for k in d if k!='content'}, indent=2\\)\\); print\\('TEXT:', next\\(\\(b['text'] for b in d.get\\('content',[]\\) if b['type']=='text'\\),''\\)[:200]\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); text=next\\(\\(b['text'] for b in d.get\\('content',[]\\) if b['type']=='text'\\),''\\); print\\('STATUS:', d.get\\('type','ok'\\)\\); print\\('TEXT:', text[:300]\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('main:', d.get\\('main'\\)\\); print\\('exports:', json.dumps\\(d.get\\('exports',{}\\), indent=2\\)\\); print\\('scripts:', json.dumps\\(d.get\\('scripts',{}\\), indent=2\\)\\)\")",
|
||||
"Bash(curl -s http://localhost:3000/api/ai/query -X POST -H 'Content-Type: application/json' -d '{\"workspace_id\":\"test\",\"question\":\"test\"}')",
|
||||
"Bash(curl -sv http://localhost:3000/api/ai/query -X POST -H 'Content-Type: application/json' -d '{\"workspace_id\":\"test\",\"question\":\"test\"}')",
|
||||
"Bash(curl -s -w '\\\\nHTTP_STATUS:%{http_code}' http://localhost:3000/api/ai/query -X POST -H 'Content-Type: application/json' -d '{\"workspace_id\":\"test\",\"question\":\"test\"}')",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\('dependencies',{}\\), indent=2\\)\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('version:', d.get\\('version'\\)\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('type:', d.get\\('type'\\)\\); print\\('error:', d.get\\('error',{}\\).get\\('message','none'\\)[:200]\\)\")",
|
||||
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); text=next\\(\\(b['text'] for b in d.get\\('content',[]\\) if b['type']=='text'\\),''\\); print\\('STATUS:', d.get\\('type','?'\\)\\); print\\('ERROR:', d.get\\('error',{}\\).get\\('message','none'\\)[:300]\\); print\\('TEXT:', text[:300]\\)\")",
|
||||
"Bash(perl -pi -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.22\\\\\\)'\\\\''/color: T.dim/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.18\\\\\\)'\\\\''/color: T.dim/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.28\\\\\\)'\\\\''/color: T.key/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.32\\\\\\)'\\\\''/color: T.key/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.45\\\\\\)'\\\\''/color: T.fgMuted/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.5\\\\\\)'\\\\''/color: T.fgMuted/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.62\\\\\\)'\\\\''/color: T.fgMuted/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.85\\\\\\)'\\\\''/color: T.fg/g;' -e 's/color: '\\\\''rgba\\\\\\(255,255,255,0\\\\.88\\\\\\)'\\\\''/color: T.fg/g;' /Users/USER/Desktop/originmain/packages/app/src/components/inspector/Inspector.tsx)",
|
||||
"Bash(pnpm --filter @originmain/renderer exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter \"@originmain/origin-graph\" exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter \"@originmain/ai-layer\" exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter \"@originmain/diff-engine\" exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter \"@originmain/renderer\" exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter \"@originmain/app\" exec tsc --noEmit)",
|
||||
"Bash(pnpm --filter \"@originmain/origin-graph\" run test)",
|
||||
"Bash(pnpm update *)",
|
||||
"Bash(pnpm --filter \"@originmain/diff-engine\" run test)",
|
||||
"Bash(pnpm --filter @originmain/app typecheck)",
|
||||
"Bash(pnpm --filter @originmain/ai-layer typecheck)",
|
||||
"Bash(pnpm --filter @originmain/design-language typecheck)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
# Supabase — get from https://supabase.com/dashboard/project/<project>/settings/api
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
|
||||
# Anthropic — https://console.anthropic.com/settings/keys
|
||||
# Never expose this in the browser bundle — server-side only
|
||||
ANTHROPIC_API_KEY=
|
||||
|
||||
# Clerk — https://dashboard.clerk.com
|
||||
# NOTE: the publishable key MUST use the NEXT_PUBLIC_ prefix so Clerk initialises
|
||||
# on the client side. The secret key is server-only and must NOT have the prefix.
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
|
||||
CLERK_SECRET_KEY=
|
||||
|
||||
# Agent Bridge — used to sign/verify workspace tokens for IDE integrations (Cursor, Claude Code).
|
||||
# Generate with: openssl rand -hex 32
|
||||
AGENT_BRIDGE_SECRET=
|
||||
|
||||
# Liveblocks (Phase 3) — https://liveblocks.io/dashboard
|
||||
LIVEBLOCKS_SECRET_KEY=
|
||||
|
||||
# Webhook secrets — set these in each integration's dashboard
|
||||
LINEAR_WEBHOOK_SECRET=
|
||||
SLACK_BOT_TOKEN=
|
||||
SLACK_SIGNING_SECRET=
|
||||
GITHUB_WEBHOOK_SECRET=
|
||||
INTERCOM_WEBHOOK_SECRET=
|
||||
|
||||
# App base URL
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
@@ -1,55 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: Lint · Type · Test · Build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Test
|
||||
run: pnpm run test
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
|
||||
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
|
||||
|
||||
- name: Migration dry-run
|
||||
run: pnpm run migration:dry-run
|
||||
env:
|
||||
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
||||
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
|
||||
@@ -1,57 +0,0 @@
|
||||
name: Preview Deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: preview-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-preview:
|
||||
name: Deploy Vercel Preview
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Deploy to Vercel
|
||||
id: vercel-deploy
|
||||
uses: amondnet/vercel-action@v25
|
||||
with:
|
||||
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
||||
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
working-directory: packages/app
|
||||
|
||||
- name: Post preview URL as PR comment
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PREVIEW_URL: ${{ steps.vercel-deploy.outputs.preview-url }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
with:
|
||||
script: |
|
||||
const previewUrl = process.env.PREVIEW_URL;
|
||||
const sha = process.env.COMMIT_SHA;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `## Preview Deployment\n\n✅ **Preview URL:** ${previewUrl}\n\nDeployed from commit \`${sha}\``
|
||||
});
|
||||
+9
-32
@@ -1,34 +1,11 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Build outputs
|
||||
.next/
|
||||
dist/
|
||||
out/
|
||||
build/
|
||||
|
||||
# Test & coverage
|
||||
coverage/
|
||||
.vitest/
|
||||
|
||||
# Supabase
|
||||
supabase/.branches
|
||||
supabase/.temp
|
||||
|
||||
# Vercel
|
||||
.vercel/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor
|
||||
.vscode/settings.json
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
node_modules/
|
||||
.wp-env/
|
||||
vendor/
|
||||
dist/
|
||||
*.log
|
||||
.idea/
|
||||
*.swp
|
||||
.vscode/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
# Allow esbuild to run its install script (downloads the native binary)
|
||||
build-scripts-allowed=esbuild
|
||||
@@ -1,58 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Originmain are documented here.
|
||||
Format: [version] — date — summary
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Figma-style Design panel** — Inspector Design tab rebuilt from scratch with semantic Figma-parity layout:
|
||||
- **Dimensions block** — W/H stepper inputs always at top, arrow-key ±1 (Shift ±10) while preserving CSS unit
|
||||
- **Fill** — colour-picker swatch (opens native `<input type="color">`) + hex text input + alpha %; hidden when background is transparent
|
||||
- **Text Color** — same colour-swatch treatment for `color` property
|
||||
- **Typography** — font family full-width text input, compact row of Size / Weight / Line-H, letter-spacing + text-align toggle group (L/C/R/J icon buttons)
|
||||
- **Layout** — Display select, flex-direction toggle (→↓←↑), Gap, Align/Justify selects, 4-corner padding grid (↑→↓←)
|
||||
- **Appearance** — Radius + Opacity side-by-side, border colour/width (shown only when border exists), box-shadow text input
|
||||
- All inputs fire `patchStyleEdit` on every change — live preview in the iframe with zero latency
|
||||
- **Live resize handles** — SelectionOverlay handles call `patchStyleEdit` on every `mousemove` during drag, giving real-time iframe feedback
|
||||
- **8 resize handles** — 4 corners (nwse/nesw cursors) + 4 edge midpoints (ns/ew cursors), matching Figma's control points
|
||||
- **Delete component** — "✕ delete" button in the selection label bar + Delete/Backspace keyboard shortcut; sends `REMOVE_ELEMENT` to iframe (sets `display: none`, non-destructive)
|
||||
- **Auto multi-screen layout** — When a live artboard connects, `ROUTES_DISCOVERED` message auto-creates new artboards for every undiscovered route in a horizontal row (80 px gaps). Designers see all app screens on the canvas without manual setup.
|
||||
- **Route discovery in hook** — `discoverRoutes()` scans `<a href>` DOM links + fiber tree for Link/NavLink/RouterLink components; fires on READY and on `popstate`
|
||||
- **Live style patching** — `PATCH_ELEMENT_STYLE` protocol message applies an inline-style override to a fiber node's DOM element in real time. Non-destructive — source files are unchanged; changes are exportable as diffs.
|
||||
- **Element style inspection** — `REQUEST_ELEMENT_STYLES` / `ELEMENT_STYLES` protocol round-trip: hook reads `window.getComputedStyle()` for ~30 CSS properties and posts them back to the host.
|
||||
- **Route-aware artboards** — Each artboard has an optional `route` field (e.g. `/dashboard`). The iframe src becomes `baseUrl + route`. Route is editable inline in the Props tab.
|
||||
- **On-canvas onboarding** — The empty canvas shows a 3-step guide (Press A → run CLI → paste proxy URL) including the exact CLI command.
|
||||
|
||||
### Changed
|
||||
- Inspector default tab changed from **Props** to **Design** — mirrors Figma's inspect-first workflow.
|
||||
- Tab order: **Design → Props → Diff → Graph**.
|
||||
- `selectComponent` store action clears `selectedComponentStyles` on every selection change.
|
||||
- `selectArtboard` store action clears both component selection and styles.
|
||||
|
||||
### Fixed
|
||||
- HTTP 204 with body in `/api/design-language` route — now returns 200.
|
||||
- `tools/list` MCP endpoint now includes `inputSchema` per spec.
|
||||
- `ProjectSettingsForm` role check used wrong constant (`'DEVELOPER'` → `'ENGINEER'`).
|
||||
- Removed dead `INJECT_FIBER_HOOK` message type from renderer protocol.
|
||||
- Zustand v5 compatibility: replaced `subscribe(selector, listener)` (removed in v5) with `useEffect` + selector hook.
|
||||
|
||||
---
|
||||
|
||||
## [0.0.3] — 2026-04-28
|
||||
- CLI proxy published to npm as `@originmain/cli`
|
||||
- README added covering quick start, CLI flags, programmatic API
|
||||
|
||||
## [0.0.2] — 2026-04-27
|
||||
- Live rendering foundation: fiber hook injection, X-Frame-Options stripping, WebSocket passthrough
|
||||
- Canvas artboard system: create, drag, rename, delete
|
||||
- Inspector: Props / Diff / Graph tabs
|
||||
- Live artboard iframe with bidirectional postMessage protocol
|
||||
- Click-to-select components in live render with blue highlight ring
|
||||
- Design token injection via CSS custom properties
|
||||
- Drift report generation via AI layer
|
||||
- Diff export with AI-generated summaries
|
||||
- MCP agent bridge with JSON-RPC 2.0 workspace token auth
|
||||
- Origin Graph query tab in inspector
|
||||
-471
@@ -1,471 +0,0 @@
|
||||
# Originmain — Implementation Handoff
|
||||
|
||||
> Living document. Updated after each layer is completed or substantially advanced.
|
||||
> Reference: `Originmain-Implementation-Guide-v1_0.md` (v1.0, April 2026)
|
||||
|
||||
---
|
||||
|
||||
## Layer Status Summary
|
||||
|
||||
| Layer | Name | Status | Notes |
|
||||
|-------|-----------------------------|--------------|------------------------------------------------------------------|
|
||||
| 0 | Infrastructure & DevOps | ⏳ Deferred | Skipped for now per plan |
|
||||
| 1 | Canvas UI Shell | ✅ Complete | ArtboardTree, CodebaseFileTree, CodeDiffPanel |
|
||||
| 2 | Live Rendering Engine | ✅ Revised | CLI proxy + Live SDK; see RENDERING-ARCHITECTURE.md |
|
||||
| 3 | Visual Editing & Diff Engine| ✅ Complete | Diff engine (49 tests), SelectionOverlay, history store |
|
||||
| 4 | Origin Graph & Data Store | ✅ Complete | 3 migrations, RLS, Zod types, query helpers |
|
||||
| 5 | Design Language Runtime | ✅ Complete | New package, JSON Schema, Zod validator, token pipeline |
|
||||
| 6 | AI Completion Layer | ✅ Complete | Gateway, 5 features, prompt caching, Claude Opus 4.7 |
|
||||
| 7 | Agent Bridge (MCP) | ✅ Complete | 5 tools, HMAC auth, rate limiter, Cursor + Claude Code adapters |
|
||||
| 8 | Multi-Origin Ingestion | ✅ Complete | OriginIngester interface + 4 connectors |
|
||||
| 9 | Multiplayer & Presence | ✅ Foundation | Room schema, adapter interface, cursor palette — no app integration yet |
|
||||
| 10 | Platform & Extensions | ✅ Foundation | Plugin API, enterprise types (SSO/SCIM/audit), white-label theming |
|
||||
|
||||
---
|
||||
|
||||
## Layer 1 — Canvas UI Shell ✅
|
||||
|
||||
**Completed:** 2026-04-25
|
||||
|
||||
### What was built
|
||||
|
||||
#### Diff Engine integration (pre-Layer 1)
|
||||
- **`packages/diff-engine/src/schemas.ts`** — Zod schemas for `ComponentSnapshot` and `ComponentDiff`
|
||||
- **`packages/diff-engine/src/engine.ts`** — Core diff computation, Myers LCS, recursive child diffing
|
||||
- **`packages/diff-engine/src/myers.ts`** — Myers longest-common-subsequence algorithm
|
||||
- **`packages/diff-engine/src/patch.ts`** — Unified patch generation
|
||||
- **`packages/diff-engine/src/serialize.ts`** — Snapshot serialization to text
|
||||
- **49 tests, ~88% coverage, all passing**
|
||||
|
||||
#### Canvas UI Shell components (NEW — additive only)
|
||||
- **`packages/app/src/components/navigator/ArtboardTree.tsx`** — Fluent 2 `Tree`/`TreeItem`/`TreeItemLayout` for workspace artboard hierarchy
|
||||
- **`packages/app/src/components/codebase/CodebaseFileTree.tsx`** — `@pierre/trees` standalone codebase file browser (separate from ArtboardNavigator.tsx)
|
||||
- **`packages/app/src/components/diff/CodeDiffPanel.tsx`** — `@pierre/diffs` `PatchDiff` renderer with unified diff patch string input
|
||||
|
||||
### Critical invariants
|
||||
- `ArtboardNavigator.tsx` must **never** be reversed or have its `@pierre/trees` FileTree removed
|
||||
- `ArtboardTree.tsx` and `CodebaseFileTree.tsx` are **separate** components
|
||||
- All new components are **additive**
|
||||
|
||||
---
|
||||
|
||||
## Layer 2 — Live Rendering Engine ✅ (Revised 2026-04-29)
|
||||
|
||||
**Original:** 2026-04-25 — postMessage protocol, fiber hook, MF config, LiveArtboard iframe
|
||||
**Revised:** 2026-04-29 — Replaced non-functional cross-origin injection with two working modes
|
||||
|
||||
> See **`RENDERING-ARCHITECTURE.md`** for the full design document.
|
||||
|
||||
### Why the original approach was non-functional
|
||||
The original `LiveArtboard.tsx` tried `iframe.contentDocument.createElement('script')` to inject the fiber hook. This fails for cross-origin iframes (the browser blocks DOM access), and the READY handshake had a circular dependency. See `RENDERING-ARCHITECTURE.md` §2 for details.
|
||||
|
||||
### Two rendering modes (both use the same postMessage protocol)
|
||||
|
||||
#### Mode A — CLI Proxy (`@originmain/cli`)
|
||||
For active local development. User runs `npx @originmain/cli dev --target http://localhost:3000` and enters the proxy URL (`http://localhost:4170`) into the artboard.
|
||||
|
||||
The proxy:
|
||||
1. Strips `X-Frame-Options` / CSP headers from responses
|
||||
2. Injects the fiber hook `<script>` into HTML responses (before React loads)
|
||||
3. Passes WebSocket upgrades through for HMR
|
||||
|
||||
#### Mode B — Live SDK (`@originmain/live`)
|
||||
For preview deployments / GitHub integration. User installs `@originmain/live` in their app and imports it before React:
|
||||
```ts
|
||||
import '@originmain/live'; // must be first import
|
||||
import React from 'react';
|
||||
```
|
||||
|
||||
The SDK installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` at module evaluation time and only activates inside an Originmain iframe (detected by `window.name` prefix `om:`).
|
||||
|
||||
### Files created / modified
|
||||
- **`packages/renderer/src/fiber-hook.ts`** — Added `buildProxyFiberHookScript()`: generic fiber hook that reads `window.name` for artboard ID (used by both CLI proxy and SDK). Original `buildFiberHookScript(id)` preserved but deprecated.
|
||||
- **`packages/cli/`** (NEW) — CLI package:
|
||||
- `src/cli.ts` — CLI entry: `originmain dev --target <url> [--port <n>]`
|
||||
- `src/proxy.ts` — HTTP reverse proxy: header stripping, HTML injection, WebSocket passthrough
|
||||
- `src/inject.ts` — HTML injection logic: inserts `<script>` after `<head>`
|
||||
- **`packages/live-sdk/`** (NEW) — SDK package:
|
||||
- `src/hook.ts` — Self-contained fiber hook: installs `__REACT_DEVTOOLS_GLOBAL_HOOK__`, serializes fiber tree, sends postMessage to parent
|
||||
- `src/index.ts` — Side-effect re-export
|
||||
- **`packages/app/src/components/canvas/LiveArtboard.tsx`** — Removed broken `injectFiberHook()`. Added `name={`om:${id}`}` to iframe. READY handler no longer attempts injection.
|
||||
- **`packages/app/src/components/canvas/Artboard.tsx`** — Updated `EmptyArtboardContent` to show proxy URL instructions and correct placeholder.
|
||||
- **`packages/integrations/src/connectors/github.ts`** — Fixed `renderUrl`: now accepts `deploymentUrl` option (from deployment_status webhook) instead of using `pr.html_url` (which is un-iframeable).
|
||||
|
||||
### Key design decisions
|
||||
- **Artboard ID via `window.name`**: `LiveArtboard` sets `name="om:{id}"` on the iframe. The fiber hook reads `window.name` to tag postMessage envelopes. Persists across SPA navigation, supports multiple artboards on one proxy.
|
||||
- **No WebSocket relay needed**: postMessage works cross-origin; the CLI proxy makes the iframe renderable by stripping blocking headers. No additional relay infrastructure required.
|
||||
- **Zero-dep SDK**: `@originmain/live` has no dependencies; it's a ~2 KB side-effect import that's a no-op outside Originmain iframes.
|
||||
|
||||
---
|
||||
|
||||
## Layer 3 — Visual Editing & Diff Engine ✅
|
||||
|
||||
**Completed:** 2026-04-25
|
||||
|
||||
### Files created
|
||||
- **`packages/app/src/store/history.ts`** — Zustand store: `stacks: Record<string, {past, future}>`, `pushEdit()`, `undo()`, `redo()`, `canUndo()`, `canRedo()`, `clearHistory()`. Max 100 entries per artboard.
|
||||
- **`packages/app/src/components/canvas/SelectionOverlay.tsx`** — Transparent `cursor: crosshair` layer; `hitTestFiber()` depth-first Fiber tree search by mouse position + `domRect`; `SelectionHandles` corner drag-to-resize; commits `PropChange[]` via `pushEdit()`
|
||||
|
||||
---
|
||||
|
||||
## Layer 4 — Origin Graph & Data Store ✅
|
||||
|
||||
**Completed:** 2026-04-25
|
||||
|
||||
### Files created
|
||||
- **`packages/origin-graph/migrations/001_initial_schema.sql`** — 5 enum types, 7 tables (workspaces, artboards, origins, intent_diffs, agent_sessions, design_language_files, team_members), FK indexes, `set_updated_at()` trigger
|
||||
- **`packages/origin-graph/migrations/002_artboard_ancestry.sql`** — Recursive CTE materialized view `artboard_ancestry(artboard_id, ancestor_id, depth)`, unique index for `CONCURRENTLY` refresh, trigger on artboard insert/update
|
||||
- **`packages/origin-graph/migrations/003_rls_policies.sql`** — `is_workspace_member(ws_id UUID)` SECURITY DEFINER function, SELECT/INSERT/UPDATE/DELETE policies on all 7 tables
|
||||
- **`packages/origin-graph/src/types.ts`** — Zod schemas for all 7 tables + insert types
|
||||
- **`packages/origin-graph/src/queries.ts`** — `DbClient` generic interface + 13 query helpers (no hard Supabase dependency)
|
||||
|
||||
---
|
||||
|
||||
## Layer 5 — Design Language Runtime ✅
|
||||
|
||||
**Completed:** 2026-04-25
|
||||
|
||||
### New package: `packages/design-language`
|
||||
|
||||
- **`src/schema.ts`** — `DesignLanguageFileBodySchema` with `tokens`, `components`, `screens`, `voice`, `accessibility` subsections
|
||||
- **`src/validator.ts`** — `validateDesignLanguageFile()` returns `ValidationSuccess | ValidationFailure`; `checkComponentConstraints()` for real-time per-edit validation
|
||||
- **`src/tokens.ts`** — `extractColorTokens()`, `tokensToCssVars()`, `dlfToCssVars()`, `dlfSummary()`
|
||||
- **`tsconfig.base.json`** — path alias added: `"@originmain/design-language": ["./packages/design-language/src/index.ts"]`
|
||||
|
||||
---
|
||||
|
||||
## Layer 6 — AI Completion Layer ✅
|
||||
|
||||
**Completed:** 2026-04-25
|
||||
|
||||
### New package: `packages/ai-layer`
|
||||
|
||||
- **`src/client.ts`** — `getClient(): Anthropic`, `MODEL = 'claude-opus-4-7'`
|
||||
- **`src/gateway.ts`** — `AIGateway` class: 60 RPM rate limiter, 3 retries on 429/529/5xx, cost tracking (`computeCost()`). Note: `thinking: {type: 'adaptive'}` pending SDK ≥0.58 upgrade
|
||||
- **`src/prompts/system.ts`** — `buildSystemPrompt({role, dlfJson?})` returns `TextBlockParam[]`; DLF block has `cache_control: {type: 'ephemeral'}` as stable system prompt prefix
|
||||
- **`src/completion-zone.ts`** — `fillCompletionZone()`: 3-retry JSON validation loop, temp 0.3
|
||||
- **`src/diff-summary.ts`** — `generateDiffSummary()`: temp 0.3, 128 tokens max
|
||||
- **`src/artboard-query.ts`** — `queryCrossArtboard()`: temp 0.3
|
||||
- **`src/drift-report.ts`** — `generateDriftReport()`: base64 image, temp 0.3
|
||||
- **`src/agent-qa.ts`** — `answerAgentQuestion()`: temp 0.7
|
||||
|
||||
### SDK note
|
||||
`@anthropic-ai/sdk` upgraded to **v0.91.1** in Session 4. All 5 feature files now use `thinking: {type: 'adaptive'}` via the gateway. `temperature` parameter removed from `GatewayRequest` interface and all feature call sites (Opus 4.7 with adaptive thinking rejects temperature with HTTP 400).
|
||||
|
||||
---
|
||||
|
||||
## Layer 7 — Agent Bridge (MCP) ✅
|
||||
|
||||
**Completed:** 2026-04-26
|
||||
|
||||
### New package: `packages/agent-bridge`
|
||||
|
||||
- **`src/protocol.ts`** — JSON-RPC 2.0 types: `JsonRpcRequest`, `JsonRpcSuccess<T>`, `JsonRpcError`, `MCP_ERROR` constants (`UNAUTHORIZED: -32001`, `RATE_LIMITED: -32002`), `AuthRequest`/`AuthAck` handshake, `textResult()`/`jsonResult<T>()` helpers
|
||||
- **`src/auth.ts`** — `issueWorkspaceToken(workspaceId, agentType)` HMAC-SHA256 base64url token; `verifyWorkspaceToken(token)` with timing-safe comparison, 30-day TTL; uses Node `crypto` built-in
|
||||
- **`src/rate-limiter.ts`** — Sliding window: 100 requests/60-min/workspace; in-process Map of timestamp arrays (replace with Redis for multi-instance)
|
||||
- **`src/tools.ts`** — `ToolContext` interface (db + ai adapters), `McpTool` interface, 5 tools: `get_pending_diffs` (rate-guarded), `get_artboard_context`, `ask_design_agent`, `update_diff_status`, `get_design_language`; `TOOLS` array, `TOOL_MAP`, `getToolList()`
|
||||
- **`src/adapters/cursor.ts`** — `generateCursorConfig()`: generates `.cursorrules` markdown + `cursor_settings.json` MCP server entry with Bearer auth
|
||||
- **`src/adapters/claude-code.ts`** — `generateClaudeCodeConfig()`: generates `CLAUDE.md` section + `.claude/settings.json` MCP server declaration with SSE transport
|
||||
|
||||
### Notes
|
||||
- Added `@types/node` devDep + `"types": ["node"]` in tsconfig for `crypto`/`Buffer`/`process`
|
||||
- `AgentType`: `'CURSOR' | 'CLAUDE_CODE' | 'GENERIC'`
|
||||
|
||||
---
|
||||
|
||||
## Layer 8 — Multi-Origin Ingestion ✅
|
||||
|
||||
**Completed:** 2026-04-26
|
||||
|
||||
### Package: `packages/integrations`
|
||||
|
||||
- **`src/types.ts`** — `OriginIngester<TPayload>` interface: `parsePayload(raw): TPayload` + `ingest(payload): IngestionResult`; `IngestionResult` carries `origin: InsertOrigin`, `artboardTitle`, optional `renderUrl`
|
||||
- **`src/connectors/linear.ts`** — `linearIngester`: Zod-validates Linear Issue webhook payload; maps to `LINEAR_ISSUE` origin; prefers attachment URL for `renderUrl`
|
||||
- **`src/connectors/slack.ts`** — `slackIngester`: Zod-validates Slack `message_posted` event; extracts first image attachment; maps to `SLACK_MESSAGE` origin
|
||||
- **`src/connectors/github.ts`** — `githubIngester`: Zod-validates GitHub `pull_request` webhook (opened/synchronize/reopened/closed); maps to `GIT_COMMIT` origin with PR metadata
|
||||
- **`src/connectors/intercom.ts`** — `intercomIngester`: Zod-validates Intercom `conversation.user.created` webhook; extracts annotated screenshot attachment; maps to `URL` origin with user context
|
||||
|
||||
### Design note
|
||||
Each connector validates untrusted webhook JSON at the boundary with Zod (`parsePayload` throws `ZodError` on invalid input). The `origin` output maps directly to `InsertOrigin` from `@originmain/origin-graph` — no `workspace_id` on origins (that scoping lives on the `artboards` table via `origin_id`).
|
||||
|
||||
---
|
||||
|
||||
## Package Inventory
|
||||
|
||||
| Package | Status | Notes |
|
||||
|----------------------------|-------------|-------------------------------------------------------------|
|
||||
| `packages/app` | Active | Next.js 15 app; Layers 1–3 complete |
|
||||
| `packages/diff-engine` | Complete | 49 tests, ~88% coverage |
|
||||
| `packages/ui` | Active | Theme + FluentProvider |
|
||||
| `packages/renderer` | Complete | Layer 2: postMessage protocol, fiber hook (proxy-compatible), MF config |
|
||||
| `packages/cli` | Complete | Layer 2: CLI proxy for live rendering (`originmain dev`) |
|
||||
| `packages/live-sdk` | Complete | Layer 2: `@originmain/live` SDK for preview deployments |
|
||||
| `packages/origin-graph` | Complete | Layer 4: migrations, RLS, Zod types, query helpers; 45 tests, 100% types.ts coverage |
|
||||
| `packages/design-language` | Complete | Layer 5: DLF schema, validator, token pipeline |
|
||||
| `packages/ai-layer` | Complete | Layer 6: gateway, 5 features, prompt library; SDK v0.91.1, adaptive thinking |
|
||||
| `packages/agent-bridge` | Complete | Layer 7: MCP tools, auth, rate limiter, adapters |
|
||||
| `packages/integrations` | Complete | Layer 8: OriginIngester + 4 connectors |
|
||||
| `packages/multiplayer` | Foundation | Layer 9: room schema, MultiplayerAdapter, cursor palette |
|
||||
| `packages/platform` | Foundation | Layer 10: Plugin API, SSO/SCIM/audit types, theming |
|
||||
| `packages/e2e` | Not started | Playwright E2E; Phase 1 completion target |
|
||||
|
||||
---
|
||||
|
||||
## Key Technical Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Artboard tree | Fluent 2 `Tree`/`TreeItem` | ARIA, Fluent token integration, <100s of nodes |
|
||||
| Codebase file tree | `@pierre/trees` | Virtualization for 5k+ files, git status, search |
|
||||
| Diff renderer | `@pierre/diffs` `PatchDiff` | Shiki syntax highlight, split/unified modes |
|
||||
| Canvas state | Zustand | Lightweight, supports undo/redo middleware |
|
||||
| TS imports in Next.js | `extensionAlias` `.js → .ts` | ESM-compatible workspace packages |
|
||||
| AI model | Claude Opus 4.7 | Max intelligence for design engineering tasks |
|
||||
| Prompt caching | DLF as stable system prompt prefix | ~60% input token cost saving per session |
|
||||
| MCP auth | HMAC-SHA256 workspace token | Stateless, 30-day TTL, timing-safe verify |
|
||||
| Rate limit | In-process sliding window | Simple; swap for Redis in multi-instance deploy |
|
||||
| Webhook validation | Zod at connector boundary | `ZodError` → HTTP 400 before any DB write |
|
||||
| `exactOptionalPropertyTypes` | Conditional spread `...(v !== undefined ? {k: v} : {})` | Required by strict TS config throughout monorepo |
|
||||
|
||||
---
|
||||
|
||||
## Layer 9 — Multiplayer & Presence (Foundation) ✅
|
||||
|
||||
**Completed:** 2026-04-26 (Phase 3 foundation only — no Liveblocks app integration per guide)
|
||||
|
||||
### New package: `packages/multiplayer`
|
||||
|
||||
- **`src/room-schema.ts`** — Liveblocks room schema types:
|
||||
- `UserPresence` — cursor `{x,y}`, `activeArtboardId`, `selectedComponentIds`, `cursorColor`; updated ≤50ms throttle
|
||||
- `ArtboardStorageObject` — CRDT-backed artboard state (viewport, locked, lastModifiedAt)
|
||||
- `WorkspaceStorage` — `artboards: Record<id, ArtboardStorageObject>` + `artboardOrder`
|
||||
- `LiveblocksRoomTypes` — generic type params for `createRoomContext<Presence, Storage, UserMeta, RoomEvent>`
|
||||
- `RoomEvent` — broadcast events: `ARTBOARD_LOCKED`, `DIFF_EXPORTED`, `COMPLETION_ZONE_ACCEPTED`
|
||||
- `workspaceRoomId(workspaceId)` — room ID convention: `"workspace:<uuid>"`
|
||||
- **`src/adapter.ts`** — `MultiplayerAdapter` interface matching Zustand store API surface (presence reads/writes, artboard storage, subscribe); `createLocalAdapter()` stub for Phase 1/2
|
||||
- **`src/cursor-colors.ts`** — `cursorColorForUser(userId)` deterministic 12-colour palette assignment
|
||||
|
||||
### Phase 3 integration note
|
||||
Actual `createClient` / `createRoomContext` calls go in `packages/app/src/lib/liveblocks.ts` (Phase 3). `@liveblocks/client` and `@liveblocks/react` are declared as optional peer deps — not installed yet.
|
||||
|
||||
---
|
||||
|
||||
## Layer 10 — Platform & Extensions (Foundation) ✅
|
||||
|
||||
**Completed:** 2026-04-26 (Phase 4 foundation — types and contracts only)
|
||||
|
||||
### New package: `packages/platform`
|
||||
|
||||
- **`src/plugin-api.ts`** — Plugin API:
|
||||
- `PluginManifest` — reverse-DNS `id`, `version`, `entryUrl`, `bundleHash`, `permissions[]`
|
||||
- `PluginPermission` — 7 scopes: `artboards:read`, `artboards:write`, `diffs:read`, `diffs:export`, `completion-zones:register`, `ingesters:register`, `design-language:read`
|
||||
- `PluginReadAPI` / `PluginWriteAPI` — async APIs exposed to sandboxed plugin via postMessage
|
||||
- `PluginContext` — `read`, `write` (null unless `artboards:write` granted), `notify()`
|
||||
- `CustomCompletionZoneDefinition` — custom zone type with `systemPromptAddendum` + `outputSchema`
|
||||
- `CustomIngesterDefinition` — custom connector `handleWebhook()` interface
|
||||
- `PluginRegistry` — `list / install / uninstall / setEnabled` per workspace
|
||||
- **`src/enterprise.ts`** — Enterprise features:
|
||||
- `SsoConfig` — SAML 2.0: `idpEntityId`, `idpSsoUrl`, `idpCertificate`, `enforced`, `emailDomains`; providers: okta/azure/google-workspace/onelogin/custom
|
||||
- `ScimUser` / `ScimGroup` — SCIM 2.0 normalized objects with `role` mapped to `TeamRole`
|
||||
- `AuditLogEntry` — 30 audit actions across auth/workspace/artboard/diff/AI/plugin/DLF/agent-bridge
|
||||
- `buildAuditEntry()` — convenience constructor with auto-timestamp
|
||||
- **`src/theming.ts`** — White-label theming:
|
||||
- `BrandTokens` — `primaryColor`, `secondaryColor`, `logoUrl`, `productName`, `faviconUrl`
|
||||
- `brandVariantsFromHex(hex)` — derives 16-shade Fluent 2 `BrandVariants` from single hex colour via HSL interpolation (shade10=lightest → shade160=darkest)
|
||||
|
||||
---
|
||||
|
||||
## Known Pending Items
|
||||
|
||||
1. **Redis-backed rate limiter** — `packages/agent-bridge/src/rate-limiter.ts` is in-process only; needs Redis for multi-instance MCP server deployment
|
||||
2. **Layer 0** — Infrastructure (Vercel, Supabase, Render, GitHub Actions) deferred per plan
|
||||
3. **Layer 9 Phase 3** — Wire `MultiplayerAdapter` into app components; add `createClient`/`createRoomContext` in `packages/app/src/lib/liveblocks.ts`; install `@liveblocks/client` + `@liveblocks/react`
|
||||
4. **Layer 10 Phase 4** — Plugin sandbox runtime (iframe + postMessage bridge); SCIM webhook endpoint; SSO provider registration UI; audit log Supabase table + extension
|
||||
5. **`packages/e2e`** — Playwright E2E tests not yet created
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Product Bug Audit (2026-04-26) — Work In Progress
|
||||
|
||||
The following is a second-pass audit of all real product gaps, independent of layer status. These are being worked on in the current session.
|
||||
|
||||
### ✅ Completed — Session 1
|
||||
- `.env.example` fixed: `CLERK_PUBLISHABLE_KEY` → `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`, added `AGENT_BRIDGE_SECRET`, `GITHUB_WEBHOOK_SECRET`, `INTERCOM_WEBHOOK_SECRET`
|
||||
- `project_id` added to `ArtboardSchema` in `types.ts` and `getArtboards` query
|
||||
- `createArtboardMutation` exported from `useArtboards.ts`, used in `Canvas.tsx` artboard tool
|
||||
- `useArtboards` returns `rawArtboards: Artboard[]` (full DB rows) alongside canvas-mapped shapes
|
||||
- Inspector props tab now shows real `metadata_jsonb` fields from selected artboard
|
||||
- ArtboardNavigator shows real artboard names (not hardcoded FILE_PATHS)
|
||||
- Canvas keyboard shortcuts: V/H/A/Z for tools, Escape to cancel
|
||||
- All 4 error boundary files created (global, workspaces, workspace, canvas)
|
||||
- Design Language upload UI in workspace page
|
||||
- Webhook `project_id: null` placeholder added (awaiting full project param support)
|
||||
- `PATCH /api/artboards/[id]` + `DELETE /api/artboards/[id]` + origin-graph helpers
|
||||
- `POST /api/workspace/[id]/tokens` — workspace token issuance for IDE integration
|
||||
- `WorkspaceCard.tsx` + `ProjectCard.tsx` extracted as `'use client'` components → fixed server-side exception on breadcrumb click
|
||||
|
||||
### ✅ Completed — Session 2 (2026-04-26)
|
||||
- **Server-side crash fixed**: `workspaces/page.tsx` and `workspace/[wid]/page.tsx` confirmed using `WorkspaceCard`/`ProjectCard` client components
|
||||
- **Inspector fully rewired**: uses `useDiffs` (DB) + `useHistory` (pending); Export Diff button; `renderUrl` inline editor; live status bar reads `liveArtboardIds`
|
||||
- **Empty canvas state**: DEMO_ARTBOARDS removed; Canvas shows hint when empty
|
||||
- **Zone tool drag preview**: live dashed rectangle + dimension label
|
||||
- **Workspace settings page**: `GET+PATCH /api/workspace/[id]`, rename, IDE token issuance, danger zone
|
||||
- **Settings link** added to workspace page
|
||||
|
||||
### ✅ Completed — Session 3 (2026-04-26)
|
||||
- **Canvas store `artboardFiberRoots`**: new `setFiberRoot(artboardId, root)` — Artboard calls it on every fiber update; Inspector Graph tab reads from it
|
||||
- **Inspector Graph tab**: shows real collapsible `FiberTreeView` from live artboard; shows node count + depth; placeholder when no live render
|
||||
- **Inspector Props tab — component selection**: when a component is clicked via `SelectionOverlay`, fiber props appear in a `↳ ComponentName` section above artboard metadata
|
||||
- **Artboard delete button**: ✕ icon appears in label when artboard is selected; confirm dialog → `DELETE /api/artboards/[id]` → invalidate query
|
||||
- **Artboard drag + rename**: already implemented (drag label to move, double-click to rename inline)
|
||||
- **Navigator delete/rename**: hover on artboard row reveals pencil (rename) and trash (delete) icon buttons; calls `patchArtboard` / `DELETE` API
|
||||
- **Zone prompt popup**: after zone drag ends, `ZonePromptOverlay` floats at zone position; textarea + "Generate ⌘↵" → `POST /api/ai/completion-zone`; shows result; Escape/close to dismiss
|
||||
- **Dead demo code removed**: `ArtboardContent`, `DashboardCard`, `UserProfile`, `NavSidebar`, `DataTable` all deleted from `Artboard.tsx`
|
||||
|
||||
### ✅ Completed — Session 4 (2026-04-26)
|
||||
- **Viewport per-workspace persistence**: `viewport.ts` `restore()` action + `AppChrome.tsx` useEffect saves/restores `panX/panY/zoom` per workspace in localStorage under `originmain:viewport:{workspaceId}`
|
||||
- **Webhook `?project=` param**: `/api/webhooks/[provider]/route.ts` reads `project` query param → `project_id` on created artboard
|
||||
- **Navigator live render badge**: artboard rows show pulsing green dot when artboard ID is in `liveArtboardIds`
|
||||
- **Navigator real graph stats**: `artboardFiberRoots` traversal gives true component counts; live artboard count from `liveArtboardIds.size`
|
||||
- **Cross-Artboard Query UI**: `CrossArtboardQuery` component in Navigator, queries `POST /api/ai/query`
|
||||
- **Drift Report UI**: "↻ Generate drift report" button at bottom of Inspector Props tab; calls `POST /api/ai/drift-report`; shows scrollable pre-formatted result panel
|
||||
- **`@anthropic-ai/sdk` upgraded to v0.91.1**: adaptive thinking wired in gateway; `temperature` removed from interface + all 5 feature files
|
||||
- **Team invitation UI**: `TeamInviteForm` added to `WorkspaceSettingsForm` — Clerk userId + role picker → `POST /api/workspace/[id]/invite`; conflict/error/success feedback
|
||||
- **Project settings page**: `/workspace/[wid]/project/[pid]/settings` — rename, description, app URL, framework selector, type-to-confirm delete; gear icon in AppChrome breadcrumb
|
||||
- **Vitest: diff-engine**: stray test moved to correct location; all 49 tests pass, 88% coverage
|
||||
- **Vitest: origin-graph**: 45 new Zod schema tests in `__tests__/types.test.ts`; 100% `types.ts` coverage
|
||||
|
||||
### ✅ Completed — Session 5 (2026-04-28)
|
||||
|
||||
- **Completion zone payload fixed**: `POST /api/ai/completion-zone` now adapts canvas UI shape `{artboard_id, bounds, prompt}` → `CompletionZoneInput {componentTreeJson, intent}`. Auto-loads active DLF from workspace. Returns `description` + backwards-compat `result`/`completion` keys.
|
||||
- **Cross-artboard query fixed**: `POST /api/ai/query` now adapts navigator UI shape `{workspace_id, question}` → `ArtboardQueryInput {query, artboardsJson}`. Fetches live artboard list from DB. Returns `{results, reasoning, answer}`.
|
||||
- **DELETE /api/workspace/:id**: Owner-only workspace deletion. `deleteWorkspace()` added to `origin-graph/queries.ts`. WorkspaceSettingsForm danger zone now calls the endpoint and redirects to `/workspaces` on success.
|
||||
- **Diff summary wired**: `POST /api/ai/diff-summary` route created. DiffTab export now calls it before `POST /api/diffs` — summary included in the diff payload. Export button shows "Summarising…" → "Exporting…" states.
|
||||
- **Artboard fork**: Fork button (branch icon) added to navigator artboard rows. Creates child artboard with `parent_artboard_id` set, offset 40px right of source, same `renderUrl` + `origin_id` inherited.
|
||||
- **Migration split fixed**: Migrations 002–005 copied to `supabase/migrations/`. `get_diffs_by_status` RPC appended to `origin-graph/migrations/001_initial_schema.sql`.
|
||||
- **Dead code removed**: `packages/app/src/hooks/useDiff.ts` and `packages/app/src/data/artboard-snapshots.ts` deleted (no imports, confirmed safe).
|
||||
- **Dark mode**: `packages/app/src/store/theme.ts` (Zustand persist, key `originmain:theme`). `providers.tsx` reads `useTheme` to pick `originmainLightTheme`/`originmainDarkTheme`. Sun/moon toggle button in AppChrome breadcrumb bar.
|
||||
- **Plugin stub page**: `/workspace/[wid]/plugins` — full page with permissions preview, Phase 4 roadmap note. Linked from workspace page header alongside Settings.
|
||||
- **Agent status badges**: `Artboard.tsx` calls `useDiffs(id)` and renders compact colored chips (draft/reviewed/applied/blocked) bottom-right of each artboard frame when diffs exist.
|
||||
- **TypeScript**: `tsc --noEmit` exits 0 on both `packages/app` and `packages/origin-graph`.
|
||||
|
||||
### ✅ Completed — Session 6 (2026-04-29)
|
||||
|
||||
#### Full-app dark/light theme system
|
||||
|
||||
- **`packages/app/src/store/canvasTheme.ts`** (NEW) — `CanvasTokens` interface (24 typed tokens); `DARK_TOKENS` + `LIGHT_TOKENS` constant sets; `useCanvasTheme()` hook reads `useTheme` store and returns the active token set.
|
||||
- **`packages/app/src/app/globals.css`** — Added 17 CSS variables in `:root` + `[data-theme="dark"]` overrides: `--page-bg/text`, `--card-bg/border/text/muted/subtle/icon-bg/icon-fg`, `--input-bg/border/text`, `--btn-bg/fg`, `--btn-idle-bg/fg/border`.
|
||||
- **`packages/app/src/app/providers.tsx`** — `useEffect` syncs Zustand `mode` → `document.documentElement.setAttribute('data-theme', mode)` so CSS variables cascade to server-rendered pages.
|
||||
- **Canvas editor — all panels now theme-aware** via `useCanvasTheme()`:
|
||||
- `Toolbar.tsx` — all tokens replaced; `Sep`/`TBtn` sub-components accept `T: CanvasTokens` prop
|
||||
- `Canvas.tsx` — `canvasBg`, `dotColor` tokens
|
||||
- `ArtboardNavigator.tsx` — full token conversion; `NavRow`/`IconBtn` accept `T` prop; `treeThemeStyles` reactive; all micro-components (`SectionLabel`, `HSep`, `GraphStat`, `CrossArtboardQuery`) call `useCanvasTheme()` directly
|
||||
- `Inspector.tsx` — all sub-components (`PropsTab`, `Section`, `PropRow`, `GraphTab`, `FiberTreeView`, `GraphNode`, `DiffTab`, `SavedDiffRow`) call `useCanvasTheme()` directly
|
||||
- `AppChrome.tsx` breadcrumb bar — logo, separators, links, settings gear, theme toggle icon all use `CT.*` tokens (no more hardcoded `rgba(255,255,255,*)`)
|
||||
- **Shell pages — CSS variable adoption** (`var(--card-bg)`, `var(--card-text)`, `var(--input-bg)`, `var(--btn-bg)`, etc.):
|
||||
- `ProjectCard.tsx`, `WorkspaceCard.tsx`, `DesignLanguageUpload.tsx`
|
||||
- `ProjectSettingsForm.tsx`, `WorkspaceSettingsForm.tsx` — static `SECTION`/`LABEL`/`INPUT`/`BTN_PRIMARY` const objects updated to use CSS vars
|
||||
- `workspace/[wid]/page.tsx`, `workspace/[wid]/plugins/page.tsx`
|
||||
- `workspace/[wid]/project/new/page.tsx`, `workspaces/new/page.tsx`
|
||||
- `workspace/[wid]/project/[pid]/settings/page.tsx`
|
||||
- `onboarding/page.tsx`, `error.tsx`
|
||||
- **`AppHeader.tsx`** (shell) — converted to `'use client'`; `useTheme` hook; sun/moon toggle button before `UserButton`; breadcrumb colors theme-aware.
|
||||
|
||||
#### AI layer fixes
|
||||
|
||||
- **`claude-opus-4-7` + `thinking: {type: 'adaptive'}`** confirmed correct and restored (earlier session accidentally introduced invalid model name and deprecated `{type:'enabled', budget_tokens:N}` format).
|
||||
- **Defensive JSON stripping** in `artboard-query.ts`, `agent-qa.ts`, `completion-zone.ts`, `drift-report.ts` — strips ` ```json ``` ` code fences before `JSON.parse` to handle model responses that wrap JSON in markdown blocks.
|
||||
- **Stale webpack cache cleared** — `packages/app/.next/cache/webpack/server-development` removed to force recompile of changed `ai-layer` source.
|
||||
|
||||
#### Marketing page (`public/marketing.html`)
|
||||
|
||||
- **Dark mode CSS block** — `[data-theme="dark"]` overrides all CSS variables; sun/moon toggle in desktop nav and mobile drawer; syncs to both `om-theme` (marketing) and `originmain:theme` (Zustand/app) localStorage keys.
|
||||
- **`cmp-card.hero-card` bug fixed** — was `background: var(--fg)` which = near-white in dark mode (white text on white card). Fixed to always-dark `#06060E` with blue `rgba(51,133,255,0.4)` glow border; eyebrow/tags get blue tint; body contrast improved.
|
||||
- **Bottom CTA (`#cta-bottom`) bug fixed** — was `background: var(--bg-inv)` which = light gray in dark mode, making `btn-outline-white` invisible. Fixed to always-dark `#06060E`; headline/subtext use fixed white alphas; gradient intensity slightly increased.
|
||||
|
||||
### ✅ Completed — Session 7 (2026-04-29)
|
||||
|
||||
#### Rendering Architecture Overhaul
|
||||
|
||||
The original Layer 2 rendering system used direct cross-origin iframe script injection, which was non-functional (see RENDERING-ARCHITECTURE.md §2). Replaced with two working modes:
|
||||
|
||||
- **`RENDERING-ARCHITECTURE.md`** (NEW) — Full design document: problem statement, two rendering modes, postMessage protocol, sequence diagrams, edge cases, package reference, migration notes.
|
||||
|
||||
- **`packages/renderer/src/fiber-hook.ts`** — Added `buildProxyFiberHookScript()`: generates a generic fiber hook script that reads `window.name` for artboard ID routing. Exported from package index. Original `buildFiberHookScript(id)` preserved as deprecated.
|
||||
|
||||
- **`packages/cli/`** (NEW) — `@originmain/cli` CLI package:
|
||||
- `src/cli.ts` — Entry: `originmain dev --target <url> [--port <n>]` with argument validation
|
||||
- `src/proxy.ts` — Reverse HTTP proxy: strips X-Frame-Options/CSP, injects fiber hook into HTML, passes WebSocket upgrades for HMR, adds CORS headers
|
||||
- `src/inject.ts` — HTML injection: inserts `<script>` after `<head>` (with `<html>` and prepend fallbacks)
|
||||
- `src/index.ts` — Programmatic API export
|
||||
|
||||
- **`packages/live-sdk/`** (NEW) — `@originmain/live` SDK package:
|
||||
- `src/hook.ts` — Self-contained fiber hook: installs `__REACT_DEVTOOLS_GLOBAL_HOOK__`, wraps existing DevTools handler, serializes fiber tree, sends postMessage with artboard ID from `window.name`. No-op when not in an Originmain iframe.
|
||||
- `src/index.ts` — Side-effect re-export
|
||||
|
||||
- **`packages/app/src/components/canvas/LiveArtboard.tsx`** — Removed broken `injectFiberHook()` helper. Added `name={`om:${id}`}` to iframe for artboard ID routing. READY handler no longer attempts script injection.
|
||||
|
||||
- **`packages/app/src/components/canvas/Artboard.tsx`** — `EmptyArtboardContent` updated: shows CLI proxy command hint, placeholder changed to `:4170`, URL input hint explains both connection modes.
|
||||
|
||||
- **`packages/integrations/src/connectors/github.ts`** — Fixed `renderUrl`: accepts `deploymentUrl` option from caller instead of using `pr.html_url` (which is un-iframeable). Uses `exactOptionalPropertyTypes`-safe conditional spread.
|
||||
|
||||
- **`packages/integrations/src/types.ts`** — `OriginIngester.ingest()` now accepts optional `opts` parameter for connector-specific configuration.
|
||||
|
||||
- **`tsconfig.base.json`** — Added path aliases for `@originmain/cli` and `@originmain/live`.
|
||||
|
||||
- **TypeScript**: Full monorepo `pnpm -r run typecheck` passes — all 12 packages, zero errors.
|
||||
|
||||
### 🔧 Remaining — Lower Priority
|
||||
|
||||
- [ ] **Multiplayer**: Wire `MultiplayerAdapter` into app (install `@liveblocks/client` + `@liveblocks/react`; create `packages/app/src/lib/liveblocks.ts`)
|
||||
- [ ] **`packages/e2e`**: Playwright E2E tests not yet created
|
||||
- [ ] **Redis rate limiter**: Replace in-process rate limiter in `agent-bridge/src/rate-limiter.ts` for multi-instance MCP support
|
||||
- [ ] **Product analytics**: Instrument PostHog for activation/engagement metrics (GTM requirement)
|
||||
- [ ] **Onboarding wizard**: Step-by-step "connect your app" flow from signup to first live artboard
|
||||
- [ ] **GitHub App + deployment_status webhook**: Full OAuth flow to connect GitHub repos + auto-populate `deploymentUrl` from Vercel/Netlify deployment webhooks
|
||||
- [ ] **Marketing page — remaining `section-dark` / `--bg-inv` surfaces**: other sections using `background: var(--bg-inv)` (e.g. `#features-alt`, footer) should be audited for the same light-mode-in-dark-mode inversion issue if the marketing page is expected to fully support theme toggling
|
||||
|
||||
### ✅ Completed — Session 8 (2026-04-30)
|
||||
|
||||
#### Canvas artboard creation fixed
|
||||
- **Bug**: `e.target === e.currentTarget` guard in `Canvas.tsx` was always false because the canvas has a full-size `position:absolute; inset:0; z-index:2` transform layer that intercepts all pointer events. Pressing A + clicking the canvas never created an artboard.
|
||||
- **Fix**: Removed the guard. Added `onMouseDown={(e) => e.stopPropagation()}` to the artboard root div so clicks on existing artboards don't bubble up and trigger creation.
|
||||
|
||||
#### Artboard drag stale-closure fix
|
||||
- **Bug**: `onUp` closed over `dragOffset` state from `useCallback` creation time (always `{dx:0,dy:0}`). Artboards snapped back to original position after drag.
|
||||
- **Fix**: Added `dragOffsetRef` mutable ref. `onMove` updates both state and ref; `onUp` reads only from ref.
|
||||
|
||||
#### Design tab (live CSS inspector + editor)
|
||||
- **`packages/renderer/src/protocol.ts`** — Two new message types:
|
||||
- `REQUEST_ELEMENT_STYLES { nodeId }` (Host → Renderer)
|
||||
- `ELEMENT_STYLES { nodeId, styles }` (Renderer → Host)
|
||||
- `PATCH_ELEMENT_STYLE { nodeId, property, value }` (Host → Renderer)
|
||||
- **`packages/live-sdk/src/hook.ts`** — Three new handlers:
|
||||
- `REQUEST_ELEMENT_STYLES` → reads `window.getComputedStyle(el)` for ~30 curated properties → posts `ELEMENT_STYLES`
|
||||
- `PATCH_ELEMENT_STYLE` → calls `el.style.setProperty(property, value)` on the fiber's DOM element
|
||||
- **`packages/app/src/store/canvas.ts`** — Added `selectedComponentStyles`, `setComponentStyles`, `styleEditEvent` mailbox, `patchStyleEdit`, `clearStyleEdit`.
|
||||
- **`packages/app/src/components/canvas/LiveArtboard.tsx`** — Sends `REQUEST_ELEMENT_STYLES` immediately after `COMPONENT_SELECTED`. Handles `ELEMENT_STYLES` response. Watches Zustand `styleEditEvent` and forwards `PATCH_ELEMENT_STYLE` to the iframe.
|
||||
- **`packages/app/src/components/canvas/Artboard.tsx`** — Wires `onComponentStylesUpdate` → `setComponentStyles`; handles deselect with styles clear.
|
||||
- **`packages/app/src/components/inspector/Inspector.tsx`** — **DESIGN** tab added as first tab. `DesignTab` component shows Typography, Layout, Visual sections. Every property is inline-editable — changes send `PATCH_ELEMENT_STYLE` via the Zustand mailbox for immediate live preview.
|
||||
- **Inspector tab order**: Design → Props → Diff → Graph (was Props → Diff → Graph).
|
||||
- **Default tab**: Design (was Props).
|
||||
|
||||
#### Route-aware artboards (multi-screen workflow)
|
||||
- **`packages/app/src/hooks/useArtboards.ts`** — `CanvasArtboard` gets `route?: string`; `toCanvasArtboard` extracts `metadata_jsonb.route`.
|
||||
- **`packages/app/src/components/canvas/Artboard.tsx`** — `route` prop; `buildSrc(base, route)` helper combines base URL + path. Artboards on the same proxy now show different pages.
|
||||
- **`packages/app/src/components/inspector/Inspector.tsx`** — `route` field in PropsTab Render Target section; editable inline; saved to `metadata_jsonb.route`. `reservedKeys` set expanded to exclude `route` from "extra props" section.
|
||||
|
||||
#### On-canvas onboarding
|
||||
- **`packages/app/src/components/canvas/Canvas.tsx`** — Empty canvas now shows a 3-step guide: ① Press A + click, ② CLI command (with copy-ready code block), ③ Paste proxy URL. Replaces the single-line hint.
|
||||
|
||||
#### CLI published
|
||||
- `@originmain/cli@0.0.3` on npm. README covers quick start, flags, programmatic API, how it works.
|
||||
|
||||
#### Bug fixes (prior sessions)
|
||||
- HTTP 204 with body in `GET /api/design-language` (RFC 7230 §3.3 violation)
|
||||
- `tools/list` MCP endpoint missing `inputSchema` (breaks IDE clients)
|
||||
- `ProjectSettingsForm` wrong role constant + wrong delete-confirm comparison target
|
||||
|
||||
*Last updated: 2026-04-30 — Session 8 complete*
|
||||
Binary file not shown.
@@ -1,317 +0,0 @@
|
||||
**ORIGINMAIN**
|
||||
|
||||
Business Development Document
|
||||
|
||||
*From Idea to Company: The Full Picture*
|
||||
|
||||
Version 1.0 \| April 2026 \| Patrick --- Founder \| Confidential
|
||||
|
||||
**1. Executive Summary**
|
||||
|
||||
Originmain is being built to capture a specific, high-value gap in the \$10B+ design tooling and AI developer tools markets: the persistent, expensive seam between design intent and production code. The design-to-code handoff costs modern product teams an estimated 30-40% of sprint capacity. Existing tools have nibbled at this problem from either side --- better design specs, better AI code generation --- but no tool has attacked the gap itself by making the design surface and the code surface the same thing.
|
||||
|
||||
This document covers the complete business development picture: market sizing, revenue model, funding strategy, team building, competitive moat, and the milestones that define the journey from founding to a sustainable, category-defining business.
|
||||
|
||||
----------------------------- ---------------------------------------------------------------------------------------------------------------
|
||||
**Dimension** **Summary**
|
||||
Market Opportunity \$2.8B serviceable market (design engineering teams on React/Next.js stacks globally)
|
||||
Revenue Model SaaS, team-based pricing (\$49-\$149/mo per workspace), Enterprise (custom)
|
||||
Target Year-1 ARR (post-GA) \$1.2M ARR from 800 paying teams at blended \$125/mo
|
||||
Funding Target (Seed) \$2.5M --- 18 months runway to Phase 3 (public beta)
|
||||
Target Funding (Series A) \$10M --- fuel GTM and enterprise sales post-GA, at Month 18+
|
||||
Team at Launch 6 FTEs: Founder/CEO, 2 Engineers, 1 Design Engineer, 1 AI/Agent Engineer, 1 Growth
|
||||
Moat Origin Graph + Agent Bridge + Design Language Runtime --- a data flywheel no competitor can replicate quickly
|
||||
----------------------------- ---------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**2. Market Opportunity**
|
||||
|
||||
**2.1 Total Addressable Market (TAM)**
|
||||
|
||||
The TAM for Originmain spans three converging markets:
|
||||
|
||||
-------------------------- --------------- ----------------- ----------------------------------------------------------------------------
|
||||
**Market** **2026 Size** **Growth Rate** **Basis**
|
||||
Design Tools \$3.2B 18% CAGR Figma, Sketch, Framer, Adobe XD subscription revenue globally
|
||||
AI Developer Tools \$4.8B 42% CAGR GitHub Copilot, Cursor, Claude Code, Tabnine subscription revenue globally
|
||||
Design-Dev Collaboration \$1.1B 25% CAGR Zeplin, Abstract, Figma Dev Mode, InVision
|
||||
Combined TAM \$9.1B --- ---
|
||||
-------------------------- --------------- ----------------- ----------------------------------------------------------------------------
|
||||
|
||||
**2.2 Serviceable Addressable Market (SAM)**
|
||||
|
||||
Originmain\'s SAM is constrained to teams with: (a) a React or Next.js frontend, (b) at least one person in a design engineering or frontend developer role, and (c) an active design process (using Figma, Stitch, or similar). Based on developer survey data (Stack Overflow 2025, State of JS 2025), approximately 31% of professional frontend teams meet this profile.
|
||||
|
||||
-------------------------------------------------- ---------------------- --------------------------------------------------------------------------
|
||||
**Estimate** **Value** **Method**
|
||||
Professional product teams globally \~4.2M teams GitHub active org count x filter for frontend frameworks
|
||||
Teams using React/Next.js \~1.3M teams 31% of 4.2M (State of JS 2025 data)
|
||||
Teams with design process + design engineer role \~620,000 teams 47% of React teams (surveys indicate design maturity at post-Seed stage)
|
||||
SAM (at \$49-149/mo per team) \$2.8B ARR potential 620,000 x \$375 blended ARPA (annual)
|
||||
-------------------------------------------------- ---------------------- --------------------------------------------------------------------------
|
||||
|
||||
**2.3 Serviceable Obtainable Market (SOM)**
|
||||
|
||||
Realistic market capture in Years 1--3, given current competitive dynamics, team size, and GTM strategy:
|
||||
|
||||
-------------------------- -------------- ----------------------- --------- -------------------------
|
||||
**Year** **Teams** **Blended ARPA** **ARR** **Market Share of SAM**
|
||||
Year 1 (GA to 12 months) 800 teams \$125/mo (\$1,500/yr) \$1.2M 0.13%
|
||||
Year 2 4,000 teams \$150/mo (\$1,800/yr) \$7.2M 0.65%
|
||||
Year 3 15,000 teams \$175/mo (\$2,100/yr) \$31.5M 2.4%
|
||||
-------------------------- -------------- ----------------------- --------- -------------------------
|
||||
|
||||
------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**THESIS** A 2.4% share of our SAM by Year 3 produces a \$31.5M ARR business. This is conservative. The design-to-code tool category is winner-take-most: the tool that becomes the default integration layer between design and AI coding captures disproportionate share.
|
||||
------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**3. Business Model**
|
||||
|
||||
**3.1 Revenue Streams**
|
||||
|
||||
-------------------------------------- ---------------------------------------- ------------------- -------------------------
|
||||
**Revenue Stream** **Type** **Launch Timing** **Year 3 % of Revenue**
|
||||
Pro & Team SaaS subscriptions Recurring SaaS Month 18 (GA) 55%
|
||||
Enterprise contracts (custom) Recurring SaaS + professional services Month 18 (GA) 35%
|
||||
AI usage overages (Completion Zones) Usage-based add-on Month 18 (GA) 7%
|
||||
Agency / white-label licensing Annual licence + rev-share Month 24 3%
|
||||
-------------------------------------- ---------------------------------------- ------------------- -------------------------
|
||||
|
||||
**3.2 Unit Economics Model**
|
||||
|
||||
Target unit economics at steady state (Year 3):
|
||||
|
||||
-------------------------------------- ----------------------- ---------------------------------------------------------------------------
|
||||
**Metric** **Target** **Basis**
|
||||
Gross Margin 82% SaaS with AI API costs passed through at 2x; infra costs \~8% at scale
|
||||
Customer Acquisition Cost (CAC) \$180 blended Community-led growth dominant; paid search + content at scale
|
||||
Annual Contract Value (ACV) --- Pro \$588/yr (\$49/mo) Single workspace, monthly billing
|
||||
Annual Contract Value (ACV) --- Team \$1,788/yr (\$149/mo) Growing teams, annual billing discount applied
|
||||
ACV --- Enterprise \$24,000/yr avg 10-50 seat equivalent, annual contracts, negotiated
|
||||
Customer Lifetime (avg) 3.2 years Low churn expected: deep codebase integration creates high switching cost
|
||||
LTV (blended) \$4,320 Blended ACV \$1,350 x 3.2 years
|
||||
LTV:CAC Ratio 24:1 Target 3:1 minimum; community-led GTM achieves significantly better ratio
|
||||
Payback Period 1.6 months Low CAC + relatively high ACV makes payback fast
|
||||
-------------------------------------- ----------------------- ---------------------------------------------------------------------------
|
||||
|
||||
**3.3 Pricing Rationale**
|
||||
|
||||
Originmain\'s pricing is anchored to the value it displaces, not to the cost of building it. The reference comparison: a 5-person product team spending 30% of sprint time on design-to-code reconciliation at an average \$85/hr blended rate loses \$5,440/month to the problem. Originmain\'s Team plan at \$149/month costs 2.7% of the problem it solves. This is the anchor used in sales conversations.
|
||||
|
||||
**4. Competitive Moat**
|
||||
|
||||
**4.1 Moat Architecture**
|
||||
|
||||
Originmain\'s defensibility is not a single feature --- it is an interlocking system of three compounding moats that become harder to replicate as adoption grows:
|
||||
|
||||
--------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------ -------------------------------------------------------------
|
||||
**Moat** **Description** **Time to Replicate (est.)** **Compounds With**
|
||||
Origin Graph A proprietary data graph of every design decision, its origin, its author, and its implementation outcome. Grows more valuable with every artboard, diff, and agent session added. No competitor has this data. 18--24 months (data moat --- cannot be copied, only accumulated) Agent Bridge feedback loop; Design Language drift detection
|
||||
Agent Bridge Integrations First-party, bidirectional integrations with Cursor and Claude Code. Not a simple \'export to\' --- a two-way channel where coding agents communicate with design agents. Requires deep partnership with AI coding tool vendors. 12--18 months (partnership + protocol depth) Origin Graph enrichment from implementation feedback
|
||||
Design Language Runtime A validated, team-specific runtime that constrains AI completions, visual edits, and drift detection. The longer a team uses it, the richer and more precise it becomes --- a flywheel other tools cannot access. 12 months to build; cannot replicate a specific team\'s DLF data Completion Zone quality; drift detection precision
|
||||
--------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------ -------------------------------------------------------------
|
||||
|
||||
**4.2 Switching Cost Analysis**
|
||||
|
||||
Originmain\'s switching cost is unusually high for a design tool --- because it integrates deeply with both the design process AND the codebase:
|
||||
|
||||
- The Origin Graph contains the team\'s complete design decision history --- irreplaceable once built
|
||||
|
||||
- Design Language Files are custom-built and validated against the team\'s specific codebase --- not portable to other tools
|
||||
|
||||
- Agent Bridge integrations are configured per-team and per-codebase --- require re-setup if moving
|
||||
|
||||
- The team\'s workflow changes: designers stop using Figma, engineers stop receiving static specs. Reverting requires re-establishing the entire prior workflow.
|
||||
|
||||
--------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**MOAT NOTE** The switching cost is not just technical --- it is behavioural. Once a team has built its Origin Graph and Design Language File, those artefacts represent months of team knowledge encoded in the product. The tool becomes infrastructure, not software.
|
||||
--------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**5. Team Building Plan**
|
||||
|
||||
**5.1 Founding Team (Month 0--4)**
|
||||
|
||||
-------------------------------- ------------------------------ -------------------------- -------------------------------------------------------------------------------------------------------------------------
|
||||
**Role** **Hire Type** **Compensation Model** **Critical Criteria**
|
||||
Founder / CEO (Patrick) Founder Equity (founder) Product vision, community building, investor relationships
|
||||
Lead Rendering / Diff Engineer Full-time hire \#1 Salary + 0.5-1.0% equity Expert in React internals, iframe sandboxing, Module Federation. Must have shipped a live-rendering system before.
|
||||
Full-Stack / Data Engineer Full-time hire \#2 Salary + 0.5-1.0% equity PostgreSQL expert; Supabase experience preferred; strong TypeScript skills
|
||||
Design Engineer (UI + DX) Full-time hire \#3 Salary + 0.3-0.7% equity Fluent 2 / Fluent UI experience highly desirable; must ship beautiful, accessible UIs without a designer alongside them
|
||||
AI / Agent Engineer Full-time hire \#4 (Month 3) Salary + 0.3-0.7% equity Claude API + MCP experience; prompt engineering discipline; has built agentic workflows before
|
||||
Growth / Community (Month 4) Part-time or fractional Salary or contract Design engineering community native; writes well; manages GTM execution, not strategy
|
||||
-------------------------------- ------------------------------ -------------------------- -------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**5.2 Phase 2 Hires (Months 5--10)**
|
||||
|
||||
---------------------------------------- ----------------------- -------------------------------------------------------------------------------------------------
|
||||
**Role** **Timing** **Rationale**
|
||||
Second Rendering Engineer Month 6 Rendering Engine is the highest-risk component; needs a second brain
|
||||
Product Designer (Fluent 2 specialist) Month 5 Originmain\'s own UI needs expert design attention as it scales; can\'t rely on engineers alone
|
||||
Developer Relations / Technical Writer Month 8 Closed beta onboarding, documentation, community education at scale
|
||||
Customer Success (first hire) Month 9 (beta launch) High-touch beta onboarding; feeds direct product feedback to engineering
|
||||
---------------------------------------- ----------------------- -------------------------------------------------------------------------------------------------
|
||||
|
||||
**5.3 Series A Team (Month 18+)**
|
||||
|
||||
Post Series A, the team scales to \~20 FTEs across product, engineering, GTM, and enterprise sales. Key additions:
|
||||
|
||||
- VP of Engineering --- to own the technical organisation as it scales beyond the founding team
|
||||
|
||||
- Head of Enterprise Sales --- to drive the enterprise and agency revenue stream
|
||||
|
||||
- 2-3 additional frontend engineers for multiplayer, plugin API, and multi-framework support
|
||||
|
||||
- Data Engineer --- to productise the Origin Graph analytics and drift detection features
|
||||
|
||||
- Marketing Lead --- to own content, brand, and paid channels post-GA
|
||||
|
||||
**6. Funding Strategy**
|
||||
|
||||
**6.1 Funding Stages**
|
||||
|
||||
------------------------- ------------------- ------------------------ --------------------------------------------------------------------------- -------------------------------------------------------------------
|
||||
**Stage** **Target Amount** **Timing** **Use of Funds** **Target Investors**
|
||||
Pre-Seed / Bootstrapped \$150K Month 0 (now) Founder living expenses, domain, minimal infra for 6 months Self-funded / angels
|
||||
Seed Round \$2.5M Month 3--4 Hire founding team (4 FTEs), 18 months runway to Phase 3 / public beta Design tool investors, developer tool funds, AI-native SaaS funds
|
||||
Series A \$10M Month 18--20 (post-GA) Scale GTM, enterprise sales team, infrastructure, international expansion Tier 1 SaaS / developer tools VCs
|
||||
------------------------- ------------------- ------------------------ --------------------------------------------------------------------------- -------------------------------------------------------------------
|
||||
|
||||
**6.2 Seed Round Investor Profile**
|
||||
|
||||
Target seed investors are funds with a strong portfolio in developer tools, design tooling, or AI-native SaaS. The pitch works best for investors who: (a) have personal experience with the design-to-code problem, (b) have already bet on the AI coding tools space and see Originmain as a natural complement, or (c) have a thesis about the \'design layer\' of the AI software development stack.
|
||||
|
||||
----------------------------------- ------------------------------------------------------------------------------ -----------------------------------------------------------------------------------
|
||||
**Investor Type** **Examples** **Why They Fit**
|
||||
Developer tool-focused seed funds Heavybit, Boldstart Ventures, Amplify Partners Deep domain expertise; portfolio companies are Originmain\'s integration partners
|
||||
Design tool investors General Catalyst (Figma), Accel (InVision) Understand the space; see the category shift happening
|
||||
AI-native SaaS funds AIX Ventures, South Park Commons, a16z (SPEEDRUN) Active thesis on AI development tooling; see the coding agent layer as strategic
|
||||
Strategic angels Design engineers and engineering leaders at Figma, Vercel, Linear, Anthropic Domain credibility; potential integration partnerships; community amplification
|
||||
----------------------------------- ------------------------------------------------------------------------------ -----------------------------------------------------------------------------------
|
||||
|
||||
**6.3 Seed Round Milestones**
|
||||
|
||||
The seed round is raised against the following milestone trajectory. Investors should expect to see these milestones hit by the end of the seed period (Month 18):
|
||||
|
||||
--------------------------------------------- ----------------- ------------------------------------------------------------------------
|
||||
**Milestone** **Target Date** **Signals Success**
|
||||
Founding team hired (4 FTEs) Month 3 Team in place; engineering execution underway
|
||||
First Live Artboard rendered from real app Month 4 Core technical thesis validated
|
||||
20 Founding Design Partners active Month 6 Product-market fit early signal; qualitative feedback loop established
|
||||
Waitlist: 2,000+ qualified signups Month 6 GTM resonance; audience built before launch
|
||||
Closed beta: 100 teams, NPS \>= 50 Month 9 Retention signal; users find real workflow value
|
||||
Agent Bridge live with Cursor + Claude Code Month 10 Differentiated technical moat established
|
||||
Public beta live Month 12 Self-serve growth channel open; broader signal of product-market fit
|
||||
1,000 active teams on public beta Month 14 Scale signal ahead of Series A
|
||||
Series A fundraise kick-off Month 18 GA launched; paid plans active; initial revenue data
|
||||
--------------------------------------------- ----------------- ------------------------------------------------------------------------
|
||||
|
||||
**7. Financial Model**
|
||||
|
||||
**7.1 Seed Period Budget (\$2.5M over 18 months)**
|
||||
|
||||
-------------------------------------------------- ---------------------------- ------------------------ ----------------
|
||||
**Category** **Monthly Budget** **18-Month Total** **% of Raise**
|
||||
Salaries (5 FTEs avg \$145K/yr) \$60,400 \$1,087,200 43%
|
||||
Founder compensation \$9,000 \$162,000 6%
|
||||
Infrastructure (Vercel, Supabase, Railway, APIs) \$3,200 \$57,600 2%
|
||||
AI API costs (Anthropic --- beta usage) \$2,000 → \$8,000 \$90,000 (est.) 4%
|
||||
Legal (incorporation, IP, contracts) \$5,000 setup + \$1,000/mo \$23,000 1%
|
||||
Marketing / GTM (pre-launch) \$2,500 \$45,000 2%
|
||||
Marketing / GTM (launch + post) \$8,000 \$96,000 (months 8-18) 4%
|
||||
Travel (investor meetings, conferences) \$1,500 \$27,000 1%
|
||||
Office / co-working (optional, remote-first) \$1,200 \$21,600 1%
|
||||
Buffer / contingency (15%) --- \$375,000 15%
|
||||
TOTAL \$88,800 avg/mo \$2,500,000 (approx) 100%
|
||||
-------------------------------------------------- ---------------------------- ------------------------ ----------------
|
||||
|
||||
**7.2 Revenue Projections (Post-GA)**
|
||||
|
||||
------------------------- ------------------ ---------------------------- ------------- ------------------
|
||||
**Quarter** **Paying Teams** **Blended ARPA (monthly)** **MRR** **ARR Run Rate**
|
||||
Month 18 (GA launch) 100 (early paid) \$95 \$9,500 \$114,000
|
||||
Month 21 (Q1 post-GA) 350 \$110 \$38,500 \$462,000
|
||||
Month 24 (Year 2 start) 800 \$125 \$100,000 \$1,200,000
|
||||
Month 30 (mid Year 2) 2,000 \$140 \$280,000 \$3,360,000
|
||||
Month 36 (Year 3 start) 4,000 \$150 \$600,000 \$7,200,000
|
||||
Month 42 (mid Year 3) 8,000 \$160 \$1,280,000 \$15,360,000
|
||||
Month 48 (Year 4 start) 15,000 \$175 \$2,625,000 \$31,500,000
|
||||
------------------------- ------------------ ---------------------------- ------------- ------------------
|
||||
|
||||
---------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**NOTE** These projections assume: (1) paid conversion from public beta of 8% of active teams; (2) monthly churn of 1.5% (industry average for SaaS with high switching cost is 0.8--2.5%); (3) expansion revenue from Starter → Pro → Team upgrades contributing 30% of net new MRR after Month 24.
|
||||
---------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**8. Business Risks & Mitigations**
|
||||
|
||||
-------------------------------------------------------- ---------------------------- ------------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**Risk** **Likelihood** **Impact** **Mitigation**
|
||||
Figma acquires a competitor and ships live rendering Medium (18 mo horizon) High Accelerate Agent Bridge and Origin Graph moat --- Figma cannot replicate the bidirectional coding agent integration without becoming a code editor, which is not their business model.
|
||||
Slow paid conversion --- users stay on free tier Medium High Gate the most high-value features (Agent Bridge, AI Completions) behind Pro; ensure free tier is genuinely useful but clearly limited. Analyse free-to-paid conversion weekly.
|
||||
Rendering engine reliability issues slow adoption Medium High Invest heavily in rendering fidelity testing from Phase 1; set reliability SLA (99.5% render success rate) as a P0 engineering metric.
|
||||
Google Stitch ships codebase-connected rendering Low-Medium (12 mo horizon) High Our moat is the Agent Bridge + Origin Graph, not rendering alone. Stitch would need to replicate 18 months of product depth to match Originmain at that point.
|
||||
AI API costs exceed projections at scale Medium Medium Implement aggressive prompt caching; use Claude Haiku for low-stakes completions, Sonnet only for high-value tasks; pass overage costs to users via usage-based pricing.
|
||||
Key hire failure (rendering engineer is irreplaceable) Low High Dual-hire strategy: hire 2 engineers with partial overlap in rendering skills so no single engineer is a SPOF. Build extensive documentation from Day 1.
|
||||
MCP protocol maturity stalls Agent Bridge adoption Low-Medium Medium Ship a non-MCP fallback (file export + webhook) that covers 80% of the value; MCP becomes the premium path when it matures.
|
||||
-------------------------------------------------------- ---------------------------- ------------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**9. Strategic Partnerships & Ecosystem**
|
||||
|
||||
**9.1 Tier 1 Strategic Partnerships (Existential Importance)**
|
||||
|
||||
These partnerships directly determine whether Originmain\'s core moat (the Agent Bridge) reaches its full potential:
|
||||
|
||||
------------------------- ---------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------
|
||||
**Partner** **Goal** **Value Exchange** **Approach**
|
||||
Anthropic (Claude Code) Official \'design layer\' partner for Claude Code; deep MCP integration; co-marketing Originmain gives Claude Code a visual context layer it cannot build itself; Anthropic gives Originmain distribution to the Claude Code user base (millions of developers) Direct outreach to Anthropic partnerships team; demonstrate the Agent Bridge integration; propose mutual case study
|
||||
Cursor (Anysphere) First-party Cursor integration; Originmain featured in Cursor\'s integration ecosystem Originmain makes Cursor more powerful for design engineering workflows; Cursor gives Originmain access to its largest-in-class AI coding audience Partner with Cursor\'s ecosystem team; ship a polished Cursor extension before approaching for co-marketing
|
||||
Linear Deep issue-to-artboard ingestion; integration listed in Linear\'s app directory Originmain makes Linear issues directly actionable on the design surface; Linear\'s design engineering audience is a near-perfect ICP match Linear has a published partner programme; apply formally; demo the issue-to-artboard flow
|
||||
------------------------- ---------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**9.2 Tier 2 Ecosystem Partnerships**
|
||||
|
||||
---------------------------- -------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------
|
||||
**Partner** **Goal** **Timing**
|
||||
Microsoft (Fluent UI team) Originmain as a showcase for Fluent 2 in the design engineering space; potential co-marketing Month 6 (after canvas UI is polished)
|
||||
Vercel Featured in Vercel\'s Next.js tooling ecosystem; joint webinars for Next.js design engineering audience Month 8 (when rendering engine is stable for Next.js apps)
|
||||
GitHub Integration in GitHub\'s developer tooling marketplace; Originmain listed as a recommended design-code bridge tool Month 12 (when public beta is live)
|
||||
Frontend Masters / Egghead Originmain featured in design engineering courses; co-created course content Month 14
|
||||
---------------------------- -------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------
|
||||
|
||||
**10. Milestones & Decision Gates**
|
||||
|
||||
The following milestones serve as explicit decision gates. At each gate, the founding team evaluates whether to continue on the current trajectory, pivot a specific element, or accelerate based on the evidence.
|
||||
|
||||
------------------- ------------------------------------------------- ------------------------------------------------------------------- --------------------------------------------------------------- ----------------------------------------------------------------------------------
|
||||
**Gate** **Milestone** **Green Outcome** **Yellow --- Investigate** **Red --- Pivot**
|
||||
Gate 1 (Month 4) First Live Artboard from real app Renders React app reliably; component tree extracted Partial rendering; some components fail Rendering approach must change; consider alternative architecture
|
||||
Gate 2 (Month 6) 20 Founding Partners active; qualitative signal Partners using Originmain weekly; clear \'aha moment\' documented Partners interested but not daily-active Re-examine the core loop; which part of the product drives the most value?
|
||||
Gate 3 (Month 9) Closed beta NPS \>= 50 NPS 50+; clear retention; low churn from beta NPS 35--49; retention moderate NPS \< 35; product has fundamental usability or value gap requiring sprint reset
|
||||
Gate 4 (Month 12) Public beta live; 1,000 active teams in 30 days Self-serve funnel working; referral loop emerging 500--999 active teams; acquisition strong but activation weak \< 500; re-examine onboarding and activation flow
|
||||
Gate 5 (Month 18) GA launch; Series A process begins \$100K MRR; 10+ enterprise conversations; Series A term sheet \$50-100K MRR; investor interest but no term sheet \< \$50K MRR; re-evaluate pricing, segment, or positioning
|
||||
------------------- ------------------------------------------------- ------------------------------------------------------------------- --------------------------------------------------------------- ----------------------------------------------------------------------------------
|
||||
|
||||
**11. Long-Term Vision (Year 5+)**
|
||||
|
||||
Originmain\'s 5-year ambition extends well beyond being a better design tool. The Origin Graph, at scale across thousands of teams, becomes one of the most valuable datasets in the software design industry --- a map of what design decisions are made, how they are implemented, and what outcomes they produce.
|
||||
|
||||
**11.1 The Platform Endgame**
|
||||
|
||||
By Year 5, Originmain aspires to be the platform layer that connects every design decision to every code change across the product development lifecycle. This means:
|
||||
|
||||
- The Origin Graph becomes queryable at industry scale: \'Show me how every B2B SaaS company has handled the onboarding flow over the last 3 years\'
|
||||
|
||||
- Design system drift detection becomes a standard part of the CI/CD pipeline, not a design tool feature
|
||||
|
||||
- The Agent Bridge becomes the standard protocol for design-code communication in the AI coding era --- every AI coding agent queries Originmain for design context before writing code
|
||||
|
||||
- White-label Originmain becomes the standard design-engineering platform for design agencies and digital consultancies globally
|
||||
|
||||
**11.2 Potential Exit Scenarios**
|
||||
|
||||
----------------------------------- ---------------------------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**Scenario** **Acquirer Hypothesis** **Strategic Rationale**
|
||||
Strategic acquisition (Year 5--7) Microsoft, Vercel, Figma, or Anthropic Originmain\'s Origin Graph and Agent Bridge are strategic infrastructure for any company building in the design-dev-AI stack. Microsoft (Fluent 2 alignment + GitHub Copilot complementarity) is the highest-probability strategic acquirer.
|
||||
IPO (Year 8--10) Public market If Originmain reaches \$100M+ ARR with strong retention and a clear platform story, it is an IPO candidate in its own right --- a new category of AI-native design infrastructure.
|
||||
Remain independent N/A The Origin Graph creates a defensible, compounding data asset that may be more valuable held independently than sold. This is the default plan until compelling acquisition terms materialise.
|
||||
----------------------------------- ---------------------------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
*End of Business Development Document --- Originmain v1.0*
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,552 +0,0 @@
|
||||
---
|
||||
name: originmain-frontend
|
||||
description: "Use this skill for all frontend work on Originmain. It synthesises three design systems into a coherent implementation guide: Fluent 2 (@fluentui/react-components v9) for all application chrome and navigation UI, @pierre/diffs for code-level diff rendering in the export panel and Agent Bridge view, and @pierre/trees for the codebase file browser. The skill defines which library owns which surface, how to theme @pierre/* libraries using Fluent 2 tokens, critical constraints for each library, and component-level implementation patterns. Use when creating, editing, or reviewing any Originmain UI component."
|
||||
---
|
||||
|
||||
# Originmain Frontend Design Skill
|
||||
|
||||
This skill governs all frontend work on Originmain. It is the definitive reference for which library owns which surface, how the three systems interoperate, and what constraints must never be violated.
|
||||
|
||||
---
|
||||
|
||||
## The Three-Library Architecture
|
||||
|
||||
Originmain's frontend is built on three libraries that cover distinct responsibilities. Understanding their boundaries is the most important thing in this skill.
|
||||
|
||||
### Library 1: Fluent 2 — Application Chrome & Navigation
|
||||
|
||||
**Package:** `@fluentui/react-components` v9
|
||||
**License:** MIT
|
||||
**Governs:** Everything in the application shell — toolbar, menus, panels, dialogs, inspector, inputs, feedback components, and the artboard navigator sidebar.
|
||||
|
||||
Fluent 2 is not a styling choice. It is the design language contract. Every Originmain-owned UI surface uses Fluent 2 components and Fluent 2 tokens. No exceptions. This ensures that the tool's own interface embodies the same systematic, accessible design language it helps teams produce.
|
||||
|
||||
### Library 2: @pierre/diffs — Code-Level Diff Rendering
|
||||
|
||||
**Package:** `@pierre/diffs`
|
||||
**License:** Apache 2.0
|
||||
**Governs:** The export panel (where a developer previews what code changes will be applied to their codebase) and the Agent Bridge communication view (where the coding agent's status is shown alongside design intent).
|
||||
|
||||
@pierre/diffs does NOT replace the custom AST differ in `packages/diff-engine`. The AST differ computes what changed. @pierre/diffs renders it. These are separate concerns.
|
||||
|
||||
### Library 3: @pierre/trees — Codebase File Browser
|
||||
|
||||
**Package:** `@pierre/trees`
|
||||
**License:** Open source (The Pierre Computer Co.)
|
||||
**Governs:** The codebase file browser panel — the panel that shows the connected application's repository file tree with git status indicators, search, and virtualization.
|
||||
|
||||
@pierre/trees does NOT replace Fluent 2's Tree component. Fluent 2 Tree governs the artboard navigator (workspace folders, artboard groups, project hierarchy). @pierre/trees governs the codebase file browser only.
|
||||
|
||||
---
|
||||
|
||||
## Surface Ownership Map
|
||||
|
||||
This table is the definitive reference. When building any component, check which library owns the surface before writing a line of code.
|
||||
|
||||
| Surface | Library | Why |
|
||||
|---|---|---|
|
||||
| Root layout | Fluent 2 `FluentProvider` | Required wrapper for all Fluent 2 context |
|
||||
| Canvas toolbar | Fluent 2 `Toolbar`, `ToolbarButton` | Chrome component — in-ecosystem |
|
||||
| Top navigation | Fluent 2 `Tab`, `TabList`, `Menu` | Chrome component |
|
||||
| **Artboard navigator** (left panel workspace tree) | Fluent 2 `Tree`, `TreeItem` | Navigates product metadata; small node count; must match chrome |
|
||||
| **Codebase file browser** (repository tree) | `@pierre/trees` `FileTree` | Navigates code; thousands of nodes; needs git status + virtualization |
|
||||
| Inspector panel | Fluent 2 `Card`, `Accordion`, `DataGrid` | Chrome component |
|
||||
| Intent Diff inspector | Fluent 2 `Card`, `Accordion`, `Badge` | Structured semantic data, not code |
|
||||
| **Export panel diff view** | `@pierre/diffs` `MultiFileDiff` | Code-level rendering with Shiki; split or stacked mode |
|
||||
| **Agent Bridge diff view** | `@pierre/diffs` `FileDiff` | Stacked mode; lines of dialogue above diff |
|
||||
| Completion Zone UI | Fluent 2 `Popover`, `Input`, `Button` | In-canvas control — chrome |
|
||||
| Dialogs | Fluent 2 `Dialog`, `Drawer` | Chrome |
|
||||
| Status & feedback | Fluent 2 `Toast`, `MessageBar`, `Spinner` | Chrome |
|
||||
| Presence / multiplayer | Fluent 2 `Avatar`, `PresenceBadge` | Chrome |
|
||||
| Artboard status badges | Fluent 2 `Badge`, `PresenceBadge` | Chrome |
|
||||
| Form inputs | Fluent 2 `Input`, `Combobox`, `SearchBox` | Chrome |
|
||||
|
||||
---
|
||||
|
||||
## Fluent 2 — Rules and Patterns
|
||||
|
||||
### Setup
|
||||
|
||||
```tsx
|
||||
// packages/app/src/app/layout.tsx
|
||||
import { FluentProvider } from '@fluentui/react-components';
|
||||
import { originmainTheme } from '@originmain/ui';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<FluentProvider theme={originmainTheme}>
|
||||
{children}
|
||||
</FluentProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Brand Theme
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/themes/originmain-theme.ts
|
||||
import { createLightTheme, createDarkTheme, type BrandVariants } from '@fluentui/react-components';
|
||||
|
||||
const originmainBrand: BrandVariants = {
|
||||
10: '#f0f4fc',
|
||||
20: '#d8e4f7',
|
||||
30: '#b3c9ef',
|
||||
40: '#8daee7',
|
||||
50: '#6793df',
|
||||
60: '#4178d7',
|
||||
70: '#2563ce',
|
||||
80: '#1655c0', // primary brand colour
|
||||
90: '#0f52ba', // anchor — the Originmain blue
|
||||
100: '#0d4aa8',
|
||||
110: '#0b4096',
|
||||
120: '#083680',
|
||||
130: '#062c6a',
|
||||
140: '#042254',
|
||||
150: '#02183e',
|
||||
160: '#010d28',
|
||||
};
|
||||
|
||||
export const originmainTheme = createLightTheme(originmainBrand);
|
||||
export const originmainDarkTheme = createDarkTheme(originmainBrand);
|
||||
```
|
||||
|
||||
### Styling — Always Use makeStyles
|
||||
|
||||
Never use inline styles for Fluent 2 components except when projecting tokens into third-party libraries (see Theming Bridge below). Use `makeStyles` from `@fluentui/react-components` (Griffel CSS-in-JS).
|
||||
|
||||
```typescript
|
||||
import { makeStyles, tokens } from '@fluentui/react-components';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
panel: {
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
borderRight: `1px solid ${tokens.colorNeutralStroke2}`,
|
||||
padding: tokens.spacingHorizontalM,
|
||||
},
|
||||
heading: {
|
||||
fontSize: tokens.fontSizeBase400,
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
color: tokens.colorNeutralForeground1,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Artboard Navigator (Fluent 2 Tree)
|
||||
|
||||
```tsx
|
||||
import { Tree, TreeItem, TreeItemLayout } from '@fluentui/react-components';
|
||||
|
||||
interface NavigatorNode {
|
||||
id: string;
|
||||
label: string;
|
||||
type: 'workspace' | 'folder' | 'group' | 'artboard';
|
||||
originType?: 'linear' | 'git' | 'feedback' | 'manual';
|
||||
children?: NavigatorNode[];
|
||||
}
|
||||
|
||||
function renderNode(node: NavigatorNode): React.ReactNode {
|
||||
if (node.children?.length) {
|
||||
return (
|
||||
<TreeItem key={node.id} itemType="branch">
|
||||
<TreeItemLayout iconBefore={<FolderIcon />}>{node.label}</TreeItemLayout>
|
||||
<Tree>{node.children.map(renderNode)}</Tree>
|
||||
</TreeItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TreeItem key={node.id} itemType="leaf">
|
||||
<TreeItemLayout iconBefore={<ArtboardIcon />}>{node.label}</TreeItemLayout>
|
||||
</TreeItem>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Critical Fluent 2 Constraints
|
||||
|
||||
- **FluentProvider is required.** All Fluent 2 hooks, tokens, and components silently fail or throw outside it.
|
||||
- **Never use `style` prop for colours.** Use `tokens.*` from `@fluentui/react-components` inside `makeStyles`.
|
||||
- **Never import from `@fluentui/react-components/unstable`.** Unstable APIs are not covered by semver.
|
||||
- **Use `tokens.colorBrandBackground` not `#0F52BA`** for brand colour references in Fluent 2 surfaces.
|
||||
- **Heading hierarchy matters for accessibility.** Fluent 2's `Text` component does not enforce this — you must set the correct `as` prop (`h1`, `h2`, etc.) manually.
|
||||
|
||||
---
|
||||
|
||||
## @pierre/diffs — Rules and Patterns
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
pnpm add @pierre/diffs
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { MultiFileDiff, FileDiff, PatchDiff } from '@pierre/diffs/react';
|
||||
```
|
||||
|
||||
### Component Choices
|
||||
|
||||
| Situation | Component | Notes |
|
||||
|---|---|---|
|
||||
| Multiple files changed | `MultiFileDiff` | Export panel default |
|
||||
| Single file comparison | `FileDiff` | Pass `before` and `after` file content strings |
|
||||
| Raw git patch string | `PatchDiff` | Agent Bridge git-level view |
|
||||
|
||||
### Theming Bridge (Fluent 2 → @pierre/diffs)
|
||||
|
||||
@pierre/diffs is themed via CSS custom properties on its container element. Map Fluent 2 tokens to the properties @pierre/diffs exposes. Always use the `style` prop on the wrapper div — never hardcoded values. Note: `makeStyles` does not support arbitrary CSS custom properties, so this is the correct exception where inline `style` is used.
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/diff/CodeDiffPanel.tsx
|
||||
import { MultiFileDiff } from '@pierre/diffs/react';
|
||||
|
||||
const diffThemeBridge = {
|
||||
'--diff-background': 'var(--colorNeutralBackground1)',
|
||||
'--diff-gutter-background': 'var(--colorNeutralBackground2)',
|
||||
'--diff-added-line': 'var(--colorPaletteGreenBackground1)',
|
||||
'--diff-removed-line': 'var(--colorPaletteRedBackground1)',
|
||||
'--diff-added-gutter': 'var(--colorPaletteGreenBackground2)',
|
||||
'--diff-removed-gutter': 'var(--colorPaletteRedBackground2)',
|
||||
'--diff-border-color': 'var(--colorNeutralStroke2)',
|
||||
'--diff-text-color': 'var(--colorNeutralForeground1)',
|
||||
'--diff-annotation-color': 'var(--colorNeutralForeground3)',
|
||||
'--diff-filename-color': 'var(--colorNeutralForeground2)',
|
||||
'--diff-hunk-background': 'var(--colorNeutralBackground3)',
|
||||
} as React.CSSProperties;
|
||||
|
||||
// Shiki theme selection based on Fluent 2 theme mode
|
||||
function getShikiTheme(isDark: boolean): string {
|
||||
return isDark ? 'github-dark-dimmed' : 'github-light';
|
||||
}
|
||||
|
||||
function CodeDiffPanel({ diff, isDark }: { diff: IntentDiff; isDark: boolean }) {
|
||||
return (
|
||||
<div style={diffThemeBridge}>
|
||||
<MultiFileDiff
|
||||
diffs={mapIntentDiffToFileDiffs(diff)}
|
||||
mode="split"
|
||||
theme={getShikiTheme(isDark)}
|
||||
annotations={buildDiffAnnotations(diff)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Annotation Pattern
|
||||
|
||||
The annotation system is @pierre/diffs' most valuable feature for Originmain. Anchor the `humanSummary` of each `ComponentChange` to the specific line it modifies. This creates the connected "design intent → code line" view that is Originmain's signature export experience.
|
||||
|
||||
```typescript
|
||||
// packages/app/src/components/diff/diff-mappers.ts
|
||||
|
||||
interface DiffAnnotation {
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'info' | 'warning' | 'success';
|
||||
}
|
||||
|
||||
export function buildDiffAnnotations(diff: IntentDiff): DiffAnnotation[] {
|
||||
return diff.changes
|
||||
.filter(change => change.lineNumber !== undefined)
|
||||
.map(change => ({
|
||||
lineNumber: change.lineNumber!,
|
||||
content: change.humanSummary,
|
||||
type: change.changeType === 'removal' ? 'warning' : 'info',
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
### Mode Switching (Split vs Stacked)
|
||||
|
||||
The export panel uses split mode (side-by-side) at widths >= 640px and stacked (unified) mode below. The Agent Bridge communication view always uses stacked mode to maximise vertical information density alongside the chat-style dialogue.
|
||||
|
||||
```tsx
|
||||
const [mode, setMode] = React.useState<'split' | 'stacked'>('split');
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
setMode(entry.contentRect.width >= 640 ? 'split' : 'stacked');
|
||||
});
|
||||
observer.observe(containerRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Critical @pierre/diffs Constraints
|
||||
|
||||
- **Never use @pierre/diffs for the Intent Diff inspector sidebar.** That surface shows semantic component-level changes (Fluent 2 Card/Accordion). @pierre/diffs is for code-level line diffs only.
|
||||
- **Always provide a Shiki theme.** Omitting `theme` falls back to an unstyled view that clashes with the Fluent 2 chrome.
|
||||
- **Annotations only anchor to lines that exist in the rendered diff.** Validate that `lineNumber` is within range before creating an annotation — out-of-range line numbers are silently ignored.
|
||||
- **Shadow DOM is used internally.** Do not attempt to style @pierre/diffs internals with global CSS selectors. The CSS custom properties API is the only supported theming interface.
|
||||
- **Test both light and dark mode.** The Shiki theme must switch when the user toggles Fluent 2's dark mode.
|
||||
|
||||
---
|
||||
|
||||
## @pierre/trees — Rules and Patterns
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
pnpm add @pierre/trees
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { FileTree } from '@pierre/trees';
|
||||
import type { FileTreeNode } from '@pierre/trees';
|
||||
```
|
||||
|
||||
### Component Usage
|
||||
|
||||
```tsx
|
||||
// packages/app/src/components/codebase/CodebaseFileTree.tsx
|
||||
|
||||
interface CodebaseFileTreeProps {
|
||||
nodes: FileTreeNode[];
|
||||
onFileSelect: (path: string) => void;
|
||||
}
|
||||
|
||||
export function CodebaseFileTree({ nodes, onFileSelect }: CodebaseFileTreeProps) {
|
||||
return (
|
||||
<div style={{ ...treesThemeBridge, height: '100%' }}>
|
||||
<FileTree
|
||||
nodes={nodes}
|
||||
gitStatus={true}
|
||||
flattenEmptyDirectories={true}
|
||||
fileTreeSearchMode="filter"
|
||||
onSelect={(node) => {
|
||||
if (node.type === 'file') onFileSelect(node.path);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### FileTreeNode Shape
|
||||
|
||||
```typescript
|
||||
interface FileTreeNode {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
gitStatus?: 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked' | 'ignored';
|
||||
children?: FileTreeNode[];
|
||||
}
|
||||
```
|
||||
|
||||
Build the `FileTreeNode[]` array from the component tree extracted by the renderer (`packages/renderer/src/fiber-injector.ts`). The renderer provides file paths via sourcemaps — use those to construct the tree structure.
|
||||
|
||||
### Theming Bridge (Fluent 2 → @pierre/trees)
|
||||
|
||||
```tsx
|
||||
const treesThemeBridge = {
|
||||
'--tree-background': 'var(--colorNeutralBackground2)',
|
||||
'--tree-item-hover': 'var(--colorNeutralBackground3)',
|
||||
'--tree-item-selected': 'var(--colorBrandBackground2)',
|
||||
'--tree-item-selected-text': 'var(--colorBrandForeground2)',
|
||||
'--tree-text-color': 'var(--colorNeutralForeground1)',
|
||||
'--tree-secondary-text': 'var(--colorNeutralForeground3)',
|
||||
'--tree-border-color': 'var(--colorNeutralStroke2)',
|
||||
'--tree-git-added': 'var(--colorPaletteGreenForeground1)',
|
||||
'--tree-git-modified': 'var(--colorPaletteYellowForeground1)',
|
||||
'--tree-git-deleted': 'var(--colorPaletteRedForeground1)',
|
||||
'--tree-git-renamed': 'var(--colorPaletteBlueForeground2)',
|
||||
'--tree-git-untracked': 'var(--colorNeutralForeground3)',
|
||||
'--tree-git-ignored': 'var(--colorNeutralForegroundDisabled)',
|
||||
} as React.CSSProperties;
|
||||
```
|
||||
|
||||
### fileTreeSearchMode Options
|
||||
|
||||
| Mode | Behaviour | When to use |
|
||||
|---|---|---|
|
||||
| `"filter"` | Non-matching nodes are hidden | Default — cleaner search experience |
|
||||
| `"highlight"` | Non-matching nodes are dimmed | When user needs spatial context |
|
||||
| `"expand"` | Matching nodes' parents expand automatically | When browsing deep paths |
|
||||
|
||||
### Critical @pierre/trees Constraints
|
||||
|
||||
- **@pierre/trees is for the codebase file browser only.** Never use it in the artboard navigator. They serve different data with different requirements.
|
||||
- **Virtualization is automatic.** Do not set a fixed height and then manually manage virtualization. Let @pierre/trees own its scroll container.
|
||||
- **`gitStatus` prop must be boolean `true`**, not an object or function. Git status is read from the `gitStatus` field on each `FileTreeNode`.
|
||||
- **The container div must have a defined height.** Set `height: '100%'` and ensure the parent has a constrained height. A tree inside a flex container with no height constraint will render zero rows.
|
||||
- **Do not render @pierre/trees outside a Fluent 2 FluentProvider.** The theme bridge uses CSS variables emitted by Fluent 2's token system — those variables are undefined outside the provider.
|
||||
- **Search is client-side.** Do not fire API calls on search input. All filtering happens in memory on the node array passed in.
|
||||
|
||||
---
|
||||
|
||||
## Token Quick Reference
|
||||
|
||||
The most commonly needed Fluent 2 design tokens for Originmain surfaces:
|
||||
|
||||
```typescript
|
||||
// Backgrounds
|
||||
tokens.colorNeutralBackground1 // primary surface (white in light / dark in dark)
|
||||
tokens.colorNeutralBackground2 // secondary surface (panels, sidebars)
|
||||
tokens.colorNeutralBackground3 // tertiary surface (hover states, tinted fills)
|
||||
tokens.colorBrandBackground // brand blue fill
|
||||
tokens.colorBrandBackground2 // selected item fill (lighter blue)
|
||||
|
||||
// Foregrounds
|
||||
tokens.colorNeutralForeground1 // primary text
|
||||
tokens.colorNeutralForeground2 // secondary text
|
||||
tokens.colorNeutralForeground3 // tertiary / placeholder text
|
||||
tokens.colorNeutralForegroundDisabled // disabled text
|
||||
tokens.colorBrandForeground1 // brand blue text on neutral backgrounds
|
||||
tokens.colorBrandForeground2 // brand blue text on brand backgrounds
|
||||
|
||||
// Strokes
|
||||
tokens.colorNeutralStroke1 // default border
|
||||
tokens.colorNeutralStroke2 // subtle border
|
||||
tokens.colorBrandStroke1 // brand border / focus ring
|
||||
|
||||
// Status palette (use for git status, diff indicators, validation states)
|
||||
tokens.colorPaletteGreenForeground1 // added / success
|
||||
tokens.colorPaletteGreenBackground1 // added line highlight
|
||||
tokens.colorPaletteYellowForeground1 // modified / warning
|
||||
tokens.colorPaletteYellowBackground1 // modified line highlight
|
||||
tokens.colorPaletteRedForeground1 // deleted / error
|
||||
tokens.colorPaletteRedBackground1 // deleted line highlight
|
||||
tokens.colorPaletteBlueForeground2 // renamed / info
|
||||
|
||||
// Spacing
|
||||
tokens.spacingHorizontalXS // 4px
|
||||
tokens.spacingHorizontalS // 8px
|
||||
tokens.spacingHorizontalM // 12px
|
||||
tokens.spacingHorizontalL // 16px
|
||||
tokens.spacingHorizontalXL // 20px
|
||||
tokens.spacingHorizontalXXL // 24px
|
||||
tokens.spacingVerticalXS // 4px
|
||||
tokens.spacingVerticalS // 8px
|
||||
tokens.spacingVerticalM // 12px
|
||||
tokens.spacingVerticalL // 16px
|
||||
|
||||
// Typography
|
||||
tokens.fontSizeBase200 // 12px
|
||||
tokens.fontSizeBase300 // 14px
|
||||
tokens.fontSizeBase400 // 16px
|
||||
tokens.fontSizeBase500 // 20px
|
||||
tokens.fontWeightRegular // 400
|
||||
tokens.fontWeightMedium // 500
|
||||
tokens.fontWeightSemibold // 600
|
||||
tokens.fontWeightBold // 700
|
||||
|
||||
// Motion
|
||||
tokens.durationFast // 100ms
|
||||
tokens.durationNormal // 200ms
|
||||
tokens.durationSlow // 300ms
|
||||
tokens.curveEasyEase // cubic-bezier(0.33, 0, 0.67, 1)
|
||||
tokens.curveDecelerateMax // cubic-bezier(0, 0, 0, 1) — for panels entering
|
||||
tokens.curveAccelerateMax // cubic-bezier(1, 0, 1, 1) — for panels leaving
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Dark Mode Toggle
|
||||
|
||||
```tsx
|
||||
import { FluentProvider } from '@fluentui/react-components';
|
||||
import { originmainTheme, originmainDarkTheme } from '@originmain/ui';
|
||||
|
||||
function ThemedApp({ isDark, children }: { isDark: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<FluentProvider theme={isDark ? originmainDarkTheme : originmainTheme}>
|
||||
{children}
|
||||
</FluentProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Pass `isDark` into `CodeDiffPanel` and `CodebaseFileTree` so they switch their Shiki theme and CSS variable bridge values accordingly.
|
||||
|
||||
### Design System Violation Badge
|
||||
|
||||
When a visual edit sets a value not found in the team's Design Language File:
|
||||
|
||||
```tsx
|
||||
import { Badge } from '@fluentui/react-components';
|
||||
|
||||
<Badge appearance="filled" color="warning" size="small">
|
||||
Not in design system
|
||||
</Badge>
|
||||
```
|
||||
|
||||
### Artboard Status Badge (Agent Bridge Sync)
|
||||
|
||||
Coding agent implementation status shown on artboard cards:
|
||||
|
||||
```tsx
|
||||
import { PresenceBadge } from '@fluentui/react-components';
|
||||
|
||||
const statusMap: Record<IntentDiff['status'], React.ReactNode> = {
|
||||
implemented: <PresenceBadge status="available" />,
|
||||
blocked: <PresenceBadge status="busy" />,
|
||||
needs_clarification: <PresenceBadge status="away" />,
|
||||
exported: <PresenceBadge status="unknown" />,
|
||||
acknowledged: <PresenceBadge status="unknown" />,
|
||||
draft: null,
|
||||
rejected: <PresenceBadge status="offline" />,
|
||||
};
|
||||
```
|
||||
|
||||
### Empty States
|
||||
|
||||
Every panel that can be empty must show a Fluent 2-styled empty state — never a blank white box:
|
||||
|
||||
```tsx
|
||||
import { Body1, Caption1, makeStyles, tokens } from '@fluentui/react-components';
|
||||
|
||||
const useEmptyStyles = makeStyles({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: tokens.spacingVerticalM,
|
||||
padding: tokens.spacingHorizontalXXL,
|
||||
color: tokens.colorNeutralForeground3,
|
||||
textAlign: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
function EmptyState({ title, description }: { title: string; description: string }) {
|
||||
const styles = useEmptyStyles();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Body1 weight="semibold">{title}</Body1>
|
||||
<Caption1>{description}</Caption1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Checklist
|
||||
|
||||
Every component must satisfy this checklist before merge:
|
||||
|
||||
- [ ] Keyboard navigation works without a mouse (Tab, Arrow keys, Enter, Escape)
|
||||
- [ ] ARIA roles are correct — do not override Fluent 2's built-in roles unnecessarily
|
||||
- [ ] Focus is managed correctly when dialogs open and close (focus trap in `Dialog`, focus restore on close)
|
||||
- [ ] Colour alone is never the only differentiator — use icons or labels alongside colour
|
||||
- [ ] All interactive elements meet WCAG 2.1 AA contrast ratio (4.5:1 for text, 3:1 for UI components)
|
||||
- [ ] All images and icons have `aria-label` or `alt` text (use `aria-hidden="true"` for decorative icons)
|
||||
- [ ] `@pierre/trees` built-in ARIA (`tree`, `treeitem`, `aria-level`, `aria-posinset`, `aria-setsize`) is not suppressed
|
||||
- [ ] `@pierre/diffs` built-in keyboard navigation for the diff view is not suppressed
|
||||
- [ ] No `tabIndex` overrides without explicit justification in the PR description
|
||||
|
||||
---
|
||||
|
||||
*Originmain Frontend Design Skill — v1.0 — April 2026*
|
||||
*Synthesises: Fluent 2 (@fluentui/react-components v9) · @pierre/diffs (diffs.com) · @pierre/trees (trees.software)*
|
||||
Binary file not shown.
@@ -1,297 +0,0 @@
|
||||
**ORIGINMAIN**
|
||||
|
||||
Go-to-Market Strategy
|
||||
|
||||
*Building Momentum While We Build the Product*
|
||||
|
||||
Version 1.0 \| April 2026 \| Patrick --- Product Lead \| Confidential
|
||||
|
||||
**1. The Story We Tell While We Build**
|
||||
|
||||
Originmain\'s GTM strategy begins before the product ships. The design-to-code problem is a felt pain that every product team experiences daily --- which means we can build a genuine audience and community around the problem before the solution is ready. This section defines the narrative we use externally from Day 1, during development.
|
||||
|
||||
-- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
*The handoff is the leak in every modern product team. Designers ship intent. Engineers ship approximations. The gap between them is where quality dies, timelines extend, and frustration compounds. Originmain seals the gap --- not by improving the handoff, but by eliminating it.*
|
||||
-- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**1.1 The Problem Narrative (Share-Ready Language)**
|
||||
|
||||
This is the copy used in all external communications during the development phase --- social posts, early landing page, waitlist emails, and conversations with prospective users:
|
||||
|
||||
-------------- --------------------------------------------------------------------------------
|
||||
**HEADLINE** Your design tool and your codebase have never met. Originmain introduces them.
|
||||
-------------- --------------------------------------------------------------------------------
|
||||
|
||||
Expanded narrative (for blog posts, LinkedIn, community threads):
|
||||
|
||||
-- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
*Every product team we have ever spoken to describes the same experience: a designer produces a beautiful, well-considered interface. An engineer implements their best interpretation of it. The result is close --- but close is not the same. What follows is a cycle of correction, clarification, and compromise that consumes 30-40% of sprint capacity without ever being counted as a formal task. It lives in the margins of Figma comments, in Slack threads, in the quiet resignation of designers who stop fighting for pixel-perfect and engineers who stop asking questions. Originmain was built because we believe this gap is not inevitable. It exists because the tools that design software and the tools that build software have always been separate --- and in that separation, intent is lost. We are building a canvas where your application lives, not just a picture of it. Where a visual change is a code change. Where context, provenance, and human intent travel with every artboard, not just the final export.*
|
||||
-- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**1.2 The Three Problem Hooks**
|
||||
|
||||
Use these condensed problem framings depending on the audience and context:
|
||||
|
||||
--------------------------- ------------------ ----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**Hook** **For Audience** **One-Line**
|
||||
The Handoff Hook Designers \"You spend weeks designing it. It ships looking 70% like your work. Originmain makes the other 30% inevitable.\"
|
||||
The Context Collapse Hook Engineers \"You implement a design with no idea why it was made, who made it, or what user problem it\'s solving. Originmain gives you that context.\"
|
||||
The Blank Canvas Hook Design Engineers \"Every tool gives you a blank page and calls it freedom. Originmain gives you your actual product as the starting point.\"
|
||||
--------------------------- ------------------ ----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**2. Target Audience & Ideal Customer Profile**
|
||||
|
||||
**2.1 Ideal Customer Profile (ICP)**
|
||||
|
||||
The ICP for Originmain\'s initial launch is tight by design. Broad appeal will dilute the message and slow word-of-mouth adoption. We will expand the ICP in subsequent phases as the product matures.
|
||||
|
||||
------------------ -------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**Dimension** **ICP Description**
|
||||
Company Size 15--200 person product companies; 2--8 person product/design teams within larger organisations
|
||||
Industry B2B SaaS, developer tools, fintech, and health tech --- segments where product quality and UI precision drive retention
|
||||
Tech Stack React or Next.js frontend; at least one person on the team who identifies as a Design Engineer, UI Engineer, or senior Frontend Developer with design sensibility
|
||||
Tooling Today Figma for design; Cursor, Claude Code, or Copilot Workspace for AI coding; Linear for project management
|
||||
Pain Level Actively frustrated by the design-to-code gap; has attempted and failed to solve it with Figma Dev Mode, Zeplin, or direct Figma MCP integration
|
||||
Budget Indicator Already paying for Figma Organisation (\$75+/month) and at least one AI coding tool (\$20+/month per developer)
|
||||
------------------ -------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**2.2 Buyer vs. User Map**
|
||||
|
||||
--------------------- ----------------------------- --------------------------------------------------------- ----------------------------------------------------------------
|
||||
**Persona** **Role in Purchase** **Primary Motivation** **Channel to Reach**
|
||||
Design Engineer Champion / Power User End the tool-switching; one surface for design and code Twitter/X, Bluesky, GitHub, design engineering Discord servers
|
||||
Product Designer Power User / Influencer Their visual intent preserved through implementation Figma community, Design Twitter, UX newsletters
|
||||
Engineering Manager Economic Buyer Reduce sprint cycle time on design-to-code tasks LinkedIn, CTO/EM newsletters, Hacker News
|
||||
Product Manager Influencer / Sponsor Faster iteration from user feedback to shipped feature ProductHunt, Product Management Slack groups
|
||||
CTO / VP Eng Final Approver (Enterprise) AI coding ROI; codify design intent in the AI pipeline Direct outreach, conference talks, thought leadership
|
||||
--------------------- ----------------------------- --------------------------------------------------------- ----------------------------------------------------------------
|
||||
|
||||
**3. Positioning & Messaging**
|
||||
|
||||
**3.1 Positioning Statement**
|
||||
|
||||
-------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**POSITION** For design engineering teams who are frustrated by the gap between design intent and production code, Originmain is the AI-native design platform that renders your actual application as an editable canvas --- so that every visual change is a structured code diff, not a request for one. Unlike Figma (which produces static specs) or AI code generators (which start from nothing), Originmain starts from your real codebase and bridges the gap permanently.
|
||||
-------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**3.2 Message Architecture**
|
||||
|
||||
Messages are structured in three tiers: Tier 1 is used everywhere (website hero, social bios, pitch decks). Tier 2 is used in product pages and longer content. Tier 3 is used in deep-dive posts and documentation.
|
||||
|
||||
--------------------------- -------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------
|
||||
**Tier** **Message** **Where Used**
|
||||
Tier 1 --- Core Value Design and code --- finally, the same surface. Website hero, social bios, elevator pitch
|
||||
Tier 1 --- Differentiator Your artboard IS your application. Not a picture of it. Product page above-fold, demo video opener
|
||||
Tier 2 --- Problem The design-to-code handoff consumes 30-40% of every sprint. Originmain eliminates it by making a visual change identical to a code change. Long-form copy, investor decks, email sequences
|
||||
Tier 2 --- Solution Render any screen from your live app. Edit it visually. Export the diff to your coding agent. Done. How It Works section, onboarding copy
|
||||
Tier 3 --- Technical Intent Diffs: component-level structured changes that coding agents (Cursor, Claude Code) can execute directly against your codebase. Technical blog posts, GitHub README, developer documentation
|
||||
--------------------------- -------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------
|
||||
|
||||
**3.3 Competitive Messaging**
|
||||
|
||||
How to position against each major competitive threat:
|
||||
|
||||
-------------------- --------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**Competitor** **Their Claim** **Originmain\'s Counter**
|
||||
Figma \"The collaborative design platform\" Figma designs are pictures of your app. Originmain IS your app. Edits in Figma create new work; edits in Originmain create code.
|
||||
Google Stitch \"Design any product, anywhere\" Stitch generates screens from AI prompts. Originmain renders your actual components. When Stitch generates a button, it\'s a generic button. When Originmain shows you a button, it\'s your Button.tsx.
|
||||
v0 (Vercel) \"Ship polished UIs in seconds\" v0 starts from a chat prompt and builds something new. Originmain starts from your existing product and evolves it --- preserving 100% of your design system and codebase context.
|
||||
Cursor + Figma MCP \"AI coding with design context\" Cursor reads from Figma. Originmain writes to Cursor. The direction of information flow is reversed --- and that reversal is everything.
|
||||
-------------------- --------------------------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**4. Pre-Launch Strategy (Months 1--6)**
|
||||
|
||||
The pre-launch period runs from now until the closed beta. Its goal is to build a waiting list of 2,000+ qualified design engineering teams, establish Originmain as a credible voice in the design-engineering conversation, and recruit 20 founding design partners who will shape the product and provide social proof at launch.
|
||||
|
||||
**4.1 The Problem-First Content Strategy**
|
||||
|
||||
We publish before we have a product to show. All pre-launch content focuses on the problem, not the solution. This positions Originmain as a thoughtful voice in the space and attracts exactly the users who feel this pain most acutely.
|
||||
|
||||
------------------- --------------- --------------------------------------------------------------------------- -----------------------------------------------------
|
||||
**Content Type** **Cadence** **Topic Focus** **Distribution**
|
||||
Long-form article Every 2 weeks The handoff gap, blank canvas problem, design engineering as a discipline Personal LinkedIn, Substack, cross-post to Medium
|
||||
Twitter/X thread 3x per week Behind-the-build updates, problem framing, design system hot takes Patrick\'s personal account (@), Originmain account
|
||||
Bluesky Daily Design engineering community engagement, short observations Originmain account + Patrick personal
|
||||
GitHub Monthly Open-source tooling (Intent Diff schema spec, Design Language File spec) GitHub releases, Hacker News Show HN
|
||||
Video (Loom) Every 3 weeks \"Building in public\" --- raw demos of the product in progress YouTube, embedded in email newsletter
|
||||
Newsletter Every 2 weeks Curated design engineering news + Originmain build update Email (ConvertKit), cross-posted to Substack
|
||||
------------------- --------------- --------------------------------------------------------------------------- -----------------------------------------------------
|
||||
|
||||
**4.2 Founding Design Partner Programme**
|
||||
|
||||
The Founding Design Partner Programme recruits 20 teams who will use Originmain throughout development, provide weekly feedback, and become the product\'s most vocal early advocates at launch. Each Founding Partner receives:
|
||||
|
||||
- Free Originmain Pro access for 12 months post-launch
|
||||
|
||||
- Direct Slack channel with the founding team
|
||||
|
||||
- Monthly 1:1 with Patrick (product lead) for feedback and roadmap input
|
||||
|
||||
- Named acknowledgement in the product (Founding Partner badge and credits page)
|
||||
|
||||
- Co-authorship opportunity on Originmain blog posts and launch content
|
||||
|
||||
Target Partner Profile: 2--8 person design/engineering teams at post-Series A SaaS companies who use React/Next.js, Figma, and at least one AI coding tool. The goal is diversity of team size, industry, and use-case.
|
||||
|
||||
**4.3 Waitlist & Landing Page**
|
||||
|
||||
The waitlist landing page launches in Month 1, before any product is ready to show. It communicates:
|
||||
|
||||
- The problem in vivid, specific terms (not marketing speak)
|
||||
|
||||
- The Originmain vision with a 60-second animated concept video
|
||||
|
||||
- Social proof from the founding team\'s credibility and early partner conversations
|
||||
|
||||
- A simple, frictionless waitlist form (email only, with optional LinkedIn)
|
||||
|
||||
------------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**TARGET** 2,000 qualified signups by end of Month 4. Qualified = job title matches one of the three primary personas (Design Engineer, Product Designer, Frontend Developer). Conversion from landing page visitor to signup: target 15%+.
|
||||
------------ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**5. Closed Beta Launch (Month 8)**
|
||||
|
||||
**5.1 Beta Cohort Selection**
|
||||
|
||||
The closed beta opens to 100 teams, selected from the waitlist and Founding Partner programme. Selection criteria:
|
||||
|
||||
---------------------------------------- ------------ -----------------------------------------------------------------------------------------
|
||||
**Criterion** **Weight** **How Assessed**
|
||||
Pain intensity 30% Application question: describe your current design-to-code workflow and where it breaks
|
||||
Technical fit (React/Next.js) 25% Application question: what is your frontend stack?
|
||||
Team composition (has Design Engineer) 20% LinkedIn verification of applicant\'s role
|
||||
Engagement in pre-launch content 15% Email open rate, social engagement, waitlist source
|
||||
Reference from Founding Partner 10% Direct referral from an existing Founding Partner
|
||||
---------------------------------------- ------------ -----------------------------------------------------------------------------------------
|
||||
|
||||
**5.2 Beta Onboarding Programme**
|
||||
|
||||
Each beta team goes through a structured onboarding sequence designed to reach the product\'s \'aha moment\' (first Live Artboard rendered from their actual app) within 30 minutes of first login:
|
||||
|
||||
- Day 0: Welcome email with Slack invite, setup guide, and 15-minute onboarding call booking
|
||||
|
||||
- Day 1: Onboarding call: screen share, connect their app, render first artboard together
|
||||
|
||||
- Day 7: Check-in email with usage analytics shared back to the team (\"you\'ve rendered X artboards and generated Y diffs\")
|
||||
|
||||
- Day 14: Feedback survey (NPS + 3 open questions: what\'s working, what\'s broken, what\'s missing)
|
||||
|
||||
- Day 30: Full beta retrospective call; product roadmap preview for Phase 3 features
|
||||
|
||||
**5.3 Beta Success Metrics**
|
||||
|
||||
------------------------------------------------------- ---------------------------------------------
|
||||
**Metric** **Target**
|
||||
Teams completing onboarding (first artboard rendered) \>= 85% within 7 days of signup
|
||||
Weekly Active Teams in beta \>= 60% of beta cohort after 30 days
|
||||
Net Promoter Score (NPS) \>= 50 at Day 30 survey
|
||||
Intent Diffs exported per active team per week \>= 5 (indicates real workflow integration)
|
||||
Qualitative testimonials secured for launch \>= 15 from beta teams
|
||||
------------------------------------------------------- ---------------------------------------------
|
||||
|
||||
**6. Public Beta Launch (Month 12)**
|
||||
|
||||
**6.1 Launch Sequence**
|
||||
|
||||
The public beta launch is a coordinated multi-channel event compressed into a 72-hour window. It is designed for maximum surface area, not a single spike.
|
||||
|
||||
---------------- ------------------------------------------------------------------------------------------- --------------------------------------- -------------------------------------------------------
|
||||
**Day** **Action** **Owned By** **Expected Reach**
|
||||
Day -7 Teaser campaign: \"Something is coming for design engineers.\" Countdown on landing page. Marketing Existing waitlist (2,000+), social followers
|
||||
Day -3 Founding Partner embargo lift: partners publish their own case studies and testimonials Founding Partners Each partner\'s network (est. 500--5,000 per partner)
|
||||
Day 0, 12:01am ProductHunt launch (scheduled) Patrick + team upvoting support PH daily users (\~50,000 design/dev audience)
|
||||
Day 0, 9:00am Long-form launch article: \'Why we built Originmain\' Patrick (LinkedIn + Substack) LinkedIn network + Substack subscribers
|
||||
Day 0, 10:00am Demo video release (YouTube, embedded on landing page) Originmain YouTube Organic + waitlist email blast
|
||||
Day 0, 12:00pm Twitter/X thread: the full Originmain story in 20 tweets Patrick personal + Originmain account Twitter Design Engineering community
|
||||
Day 0, 2:00pm Hacker News \'Show HN\' post with technical deep-dive on the Intent Diff architecture Patrick HN developer audience
|
||||
Day 1--3 Designer and engineer newsletter placements (pre-booked) Paid/barter placements Dense (2,500--10,000 per newsletter)
|
||||
Day 3 Founder AMA on Design Engineering Discord (30,000+ members) Patrick Core ICP community
|
||||
---------------- ------------------------------------------------------------------------------------------- --------------------------------------- -------------------------------------------------------
|
||||
|
||||
**6.2 Launch KPIs**
|
||||
|
||||
--------------------------------------------- ----------------------------------- ----------------------------------
|
||||
**KPI** **Target (72 hours post-launch)** **Target (30 days post-launch)**
|
||||
New signups 1,000 5,000
|
||||
ProductHunt rank Top 5 product of the day Featured in PH newsletter
|
||||
Self-serve teams activated (first artboard) 200 1,000
|
||||
MRR from paid conversions \$0 (beta is free) \$25,000
|
||||
Press / media coverage 3 original pieces 10 original pieces
|
||||
Demo video views 5,000 20,000
|
||||
--------------------------------------------- ----------------------------------- ----------------------------------
|
||||
|
||||
**7. Pricing Strategy**
|
||||
|
||||
**7.1 Pricing Philosophy**
|
||||
|
||||
Originmain\'s pricing is designed around the team as the billing unit, not the individual seat --- because the value of the product compounds with every team member who uses it. Pricing should be opaque enough to justify discovery and transparent enough to feel fair. We anchor against the cost of the status quo: a 30% sprint waste on design-to-code reconciliation costs a 10-person team \~\$8,000/month in labour. Our pricing should feel trivially cheap by comparison.
|
||||
|
||||
------------ ------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------
|
||||
**Tier** **Price** **Includes** **Target**
|
||||
Starter Free forever 1 workspace, 3 artboards, 1 app connection, unlimited exports Individual design engineers, students, side projects
|
||||
Pro \$49/mo per workspace Unlimited artboards, 3 app connections, AI Completion Zones (100/mo), Agent Bridge (Cursor + Claude Code) Small product teams (2--10 people)
|
||||
Team \$149/mo per workspace Everything in Pro + unlimited AI completions, all integrations (Linear, Slack, GitHub), multiplayer, cross-artboard querying, priority support Growing product companies (10--50 people)
|
||||
Enterprise Custom Everything in Team + SSO, SCIM, audit logs, white-label, SLA, dedicated onboarding, custom integrations, unlimited app connections Companies 50+ people, agencies, design studios
|
||||
------------ ------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------
|
||||
|
||||
---------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**NOTE** Beta pricing: all closed and public beta users get Team tier free for their full beta period. Paid plans launch with General Availability (Month 18). This removes price as a barrier to adoption and feedback quality during the critical early phase.
|
||||
---------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**8. Channel Strategy**
|
||||
|
||||
**8.1 Owned Channels**
|
||||
|
||||
------------------------------------- --------------------------------------------------------------- --------------------------------------------------------
|
||||
**Channel** **Goal** **KPI**
|
||||
originmain.com (landing/product) Convert visitors to signups and activations Visitor-to-signup: 15%; signup-to-active: 60%
|
||||
Email newsletter (ConvertKit) Nurture pre-launch audience; retain and re-engage post-launch Open rate: 40%+; CTR: 8%+
|
||||
YouTube (product demos) Discovery for bottom-of-funnel search; demo library for sales Demo watch completion: 50%+; 500 subscribers by launch
|
||||
GitHub (Intent Diff spec, DLF spec) Developer credibility; top-of-funnel for technical audience Stars on public repos: 500+ by launch
|
||||
------------------------------------- --------------------------------------------------------------- --------------------------------------------------------
|
||||
|
||||
**8.2 Earned Channels**
|
||||
|
||||
------------------------------ --------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------
|
||||
**Channel** **Strategy** **Target**
|
||||
Design Engineering Twitter/X Participate in the community; don\'t broadcast, converse. Build Patrick\'s personal brand alongside the product brand. 5,000 engaged followers (Patrick) by launch; 1,000 for \@originmain
|
||||
Hacker News Technical deep-dives on the Intent Diff architecture, rendering engine challenges, and AI agent communication. \'Show HN\' at launch. 3 front-page posts pre-launch; launch \'Show HN\' in top 5
|
||||
Bluesky Design Community Growing design engineering presence; early adopter community for technical designers. Active presence; 500+ followers by launch
|
||||
Product Hunt Coordinated launch with Founding Partner support and community upvoting. Top 5 product of the day; \#1 product of the week target
|
||||
Design Engineering Discords Genuine community participation; no spam. Answer questions, share the problem narrative, invite feedback on early concepts. Active presence in 5+ communities pre-launch
|
||||
------------------------------ --------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------
|
||||
|
||||
**8.3 Paid Channels (Post-GA Only)**
|
||||
|
||||
Paid acquisition is deferred until General Availability (Month 18). Pre-GA growth is entirely organic and community-driven. This is intentional: paid acquisition before product-market fit accelerates churn, not growth. The paid channel strategy will be defined based on what the organic data reveals about highest-converting sources.
|
||||
|
||||
**9. Partnership Strategy**
|
||||
|
||||
------------------------------ ------------------------------------------------------ ------------------------------------------------------------------------------------------------ ----------------------
|
||||
**Partner Type** **Target Partners** **Integration Value** **Timing**
|
||||
AI Coding Tools Cursor, Anthropic (Claude Code) First-party Agent Bridge adapters; co-marketing as the \'design layer\' for their coding tools Phase 2 --- Month 6+
|
||||
Design Community Figma Community plugins, Fluent UI team at Microsoft Import path from Figma; Microsoft design ecosystem alignment Phase 1-2
|
||||
Project Management Linear (primary), Jira (secondary) Deep issue-to-artboard ingestion; co-marketing to Linear\'s design engineering audience Phase 2
|
||||
Design Engineering Education Frontend Masters, Egghead.io, Scrimba Course integrations; Originmain as the design tool in design engineering curricula Phase 3
|
||||
Design Agencies Top 10 React-focused digital agencies White-label access; agency case studies; enterprise sales referrals Phase 4
|
||||
------------------------------ ------------------------------------------------------ ------------------------------------------------------------------------------------------------ ----------------------
|
||||
|
||||
**10. GTM Metrics Dashboard**
|
||||
|
||||
The following metrics are tracked weekly during pre-launch and daily during launch week. All metrics are available in PostHog (product analytics) and a shared Notion dashboard visible to the full founding team.
|
||||
|
||||
--------------------- ------------------------------------------- ------------------- ------------------------------------------------------
|
||||
**Phase** **Metric** **Target** **Green / Yellow / Red Thresholds**
|
||||
Pre-launch Waitlist signups (cumulative) 2,000 by Month 4 G: on pace / Y: 20% behind pace / R: 40% behind pace
|
||||
Pre-launch Founding Partners onboarded 20 by Month 6 G: 20+ / Y: 15-19 / R: \<15
|
||||
Pre-launch Newsletter subscribers 1,000 by Month 4 G: 1,000+ / Y: 700-999 / R: \<700
|
||||
Pre-launch LinkedIn article impressions (cumulative) 50,000 by Month 6 G: 50K+ / Y: 30-50K / R: \<30K
|
||||
Beta Beta teams activated (first artboard) \>85% in 7 days G: 85%+ / Y: 70-84% / R: \<70%
|
||||
Beta Weekly active beta teams \>60% at Day 30 G: 60%+ / Y: 45-59% / R: \<45%
|
||||
Beta NPS (Day 30 survey) \>=50 G: 50+ / Y: 35-49 / R: \<35
|
||||
Launch Day-1 signups 1,000 G: 1,000+ / Y: 600-999 / R: \<600
|
||||
Launch ProductHunt rank (end of day) Top 5 G: Top 5 / Y: Top 10 / R: Below 10
|
||||
30-days post-launch Teams actively using product 1,000 G: 1,000+ / Y: 700-999 / R: \<700
|
||||
--------------------- ------------------------------------------- ------------------- ------------------------------------------------------
|
||||
|
||||
*End of Go-to-Market Strategy --- Originmain v1.0*
|
||||
Binary file not shown.
@@ -1,551 +0,0 @@
|
||||
**ORIGINMAIN**
|
||||
|
||||
Implementation Guide
|
||||
|
||||
*AI-Native Design Engineering Platform*
|
||||
|
||||
Version 1.0 \| April 2026 \| Patrick --- Product Lead \| Confidential
|
||||
|
||||
**1. Executive Overview**
|
||||
|
||||
This Implementation Guide translates the Originmain PRD into a concrete, layer-by-layer engineering blueprint. It is the primary technical reference for every developer who will build, integrate, test, or deploy the product. It covers the full stack --- from infrastructure provisioning to AI pipeline design --- and is structured as a progressive sequence of implementation layers, each of which must be stable before the next begins.
|
||||
|
||||
The guide follows Originmain\'s phased roadmap (Phases 1--4 from the PRD) and maps each feature requirement to a specific layer, technology choice, and implementation approach. Where trade-offs exist, the rationale for the selected approach is documented explicitly.
|
||||
|
||||
--------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**PRINCIPLE** Every architectural decision in this guide is subordinate to one constraint: the design surface and the code diff must be the same continuous action. Any trade-off that creates a perceptible gap between a visual edit and a code change is unacceptable.
|
||||
--------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
----------- ------------------------------ ------------------------------------------------ -----------
|
||||
**Layer** **Name** **Primary Technology** **Phase**
|
||||
0 Infrastructure & DevOps Vercel, Supabase, Render, GitHub Actions 1
|
||||
1 Canvas UI Shell React 19, Fluent UI v9, Griffel 1
|
||||
2 Live Rendering Engine Sandboxed iframe, Module Federation, Webpack 5 1
|
||||
3 Visual Editing & Diff Engine Custom AST, TypeScript, react-fiber-utils 1
|
||||
4 Origin Graph & Data Store PostgreSQL, pg\_graphql, Supabase Realtime 1
|
||||
5 Design Language Runtime JSON Schema, Zod, custom validator 1
|
||||
6 AI Completion Layer Claude Sonnet 4 API, Anthropic SDK 2
|
||||
7 Agent Bridge (MCP) Model Context Protocol, WebSocket, tRPC 2
|
||||
8 Multi-Origin Ingestion Linear SDK, Slack API, GitHub REST 2
|
||||
9 Multiplayer & Presence Liveblocks, CRDT, Yjs 3
|
||||
10 Platform & Extensions Plugin API, SSO, SCIM, audit logs 4
|
||||
----------- ------------------------------ ------------------------------------------------ -----------
|
||||
|
||||
**2. Development Prerequisites**
|
||||
|
||||
**2.1 Required Team Roles**
|
||||
|
||||
Before a single line is written, the following roles must be staffed. The product has a high degree of complexity in its rendering and diffing subsystems; under-resourcing these areas is the leading cause of scope collapse in similar products.
|
||||
|
||||
--------------------------------- ----------- ------------------------------------------------------------------
|
||||
**Role** **Count** **Critical Responsibility**
|
||||
Lead Frontend / Design Engineer 1 Canvas UI, component palette, Fluent 2 integration
|
||||
Rendering Engine Engineer 1--2 Sandboxed iframe renderer, Module Federation, hot reload
|
||||
Diff Engine Engineer 1 AST diffing, Intent Diff schema, component-level change tracking
|
||||
Backend / Data Engineer 1 PostgreSQL schema, pg\_graphql, Origin Graph, Supabase setup
|
||||
AI / Agent Engineer 1 Claude API integration, Completion Zones, Agent Bridge (MCP)
|
||||
DevOps / Platform Engineer 0.5 CI/CD, infra provisioning, environment management
|
||||
Product Designer (Fluent 2) 1 Originmain\'s own UI, design token system, interaction design
|
||||
--------------------------------- ----------- ------------------------------------------------------------------
|
||||
|
||||
**2.2 Local Development Environment**
|
||||
|
||||
- Node.js 22 LTS (use nvm for version management)
|
||||
|
||||
- pnpm 9+ as the package manager (workspaces enabled for monorepo)
|
||||
|
||||
- Docker Desktop for local PostgreSQL and Redis
|
||||
|
||||
- Supabase CLI for local database branching and migrations
|
||||
|
||||
- Vercel CLI for local preview deployments
|
||||
|
||||
- GitHub CLI (gh) for PR automation and Actions triggers
|
||||
|
||||
**2.3 Repository Structure**
|
||||
|
||||
Originmain is a pnpm monorepo with the following top-level packages:
|
||||
|
||||
----------------- -------------------------- -----------------------------------------------------------------------
|
||||
**Package** **Path** **Description**
|
||||
app packages/app Main canvas application (React 19, Next.js 15 App Router)
|
||||
renderer packages/renderer Sandboxed iframe rendering engine and Module Federation host
|
||||
diff-engine packages/diff-engine AST differ, Intent Diff schema, change tracker
|
||||
origin-graph packages/origin-graph PostgreSQL schema, Supabase migrations, pg\_graphql resolvers
|
||||
ai-layer packages/ai-layer Claude API client, Completion Zone processor, prompt library
|
||||
agent-bridge packages/agent-bridge MCP server, WebSocket protocol, coding agent adapters
|
||||
design-language packages/design-language JSON Schema validator, token pipeline, guidance file runtime
|
||||
ui packages/ui Shared Fluent 2 component wrappers and Originmain-specific components
|
||||
integrations packages/integrations Linear, Slack, GitHub ingestion connectors
|
||||
e2e packages/e2e Playwright end-to-end test suites
|
||||
----------------- -------------------------- -----------------------------------------------------------------------
|
||||
|
||||
**3. Layer 0 --- Infrastructure & DevOps**
|
||||
|
||||
**3.1 Cloud Architecture**
|
||||
|
||||
Originmain\'s infrastructure is built on three primary cloud providers, selected for complementary strengths: Vercel for edge-first frontend delivery, Supabase for managed PostgreSQL with real-time and GraphQL, and Render for long-running backend services (Agent Bridge, rendering workers).
|
||||
|
||||
----------------------- --------------------- -------------------------------------------------- -----------------------
|
||||
**Service** **Provider** **Purpose** **Tier (Launch)**
|
||||
Frontend App Vercel (Pro) Next.js 15 app, edge functions, ISR Pro --- \$20/mo
|
||||
Primary Database Supabase (Pro) PostgreSQL, Origin Graph, auth, realtime Pro --- \$25/mo
|
||||
Redis Cache Upstash Redis Session cache, rate limiting, queue Pay-per-use
|
||||
Agent Bridge Server Render Long-running MCP WebSocket server Hobby --- \$5/mo
|
||||
Object Storage Supabase Storage Artboard screenshots, design language files Included in Pro
|
||||
CDN / Edge Vercel Edge Network Static assets, API edge caching Included in Pro
|
||||
Email (Transactional) Resend Auth emails, notifications Free tier
|
||||
Error Monitoring Sentry Frontend and backend error tracking Team --- \$26/mo
|
||||
Analytics PostHog Product analytics, session replay, feature flags Free tier (1M events)
|
||||
----------------------- --------------------- -------------------------------------------------- -----------------------
|
||||
|
||||
**3.2 CI/CD Pipeline**
|
||||
|
||||
Every pull request triggers a full pipeline via GitHub Actions. The pipeline is defined as code in .github/workflows/ and enforces quality gates that cannot be bypassed. No code reaches production without passing all gates.
|
||||
|
||||
1. Lint & type-check (ESLint, TypeScript strict mode) --- must pass with zero errors
|
||||
|
||||
2. Unit tests (Vitest) --- must maintain 80%+ coverage on diff-engine and origin-graph packages
|
||||
|
||||
3. Build verification --- all packages must build cleanly
|
||||
|
||||
4. Visual regression (Playwright + Percy) --- no unreviewed visual diffs
|
||||
|
||||
5. Supabase migration dry-run --- schema changes validated against production snapshot
|
||||
|
||||
6. Preview deployment to Vercel --- every PR gets a unique preview URL
|
||||
|
||||
7. Production deployment on merge to main --- gated by all above steps
|
||||
|
||||
**3.3 Environment Strategy**
|
||||
|
||||
Three environments run in parallel: local (developer machine), staging (auto-deployed from main branch), and production. Each environment has an isolated Supabase project and Vercel deployment. Staging uses production data snapshots (anonymized) refreshed weekly.
|
||||
|
||||
-------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**CRITICAL** The sandboxed rendering iframe communicates with the host app via postMessage. CSP headers must be configured precisely: the frame\'s origin must be explicitly whitelisted in the host app\'s Content-Security-Policy. A misconfigured CSP is the single most common rendering failure mode.
|
||||
-------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**4. Layer 1 --- Canvas UI Shell**
|
||||
|
||||
**4.1 Framework & Technology**
|
||||
|
||||
The canvas application is built with Next.js 15 (App Router) and React 19. The infinite canvas is implemented using a combination of CSS transforms and a custom viewport state manager --- NOT a third-party canvas library like react-flow or Konva, which lack the semantic structure required for component-level interaction.
|
||||
|
||||
------------------- -------------------------------- ----------------------------------------------------------
|
||||
**Concern** **Technology** **Rationale**
|
||||
Framework Next.js 15 App Router RSC for meta-layer content, client components for canvas
|
||||
Component Library \@fluentui/react-components v9 PRD mandate; Fluent 2 is the design system
|
||||
CSS-in-JS Griffel (via Fluent 2) Fluent 2\'s native styling system; zero runtime overhead
|
||||
Canvas Primitives Custom React + CSS transform Full control over hit testing, selection, and Z-order
|
||||
State Management Zustand + Immer Lightweight, performant; supports undo/redo middleware
|
||||
Data Fetching TanStack Query v5 Cache management, optimistic updates, background sync
|
||||
Routing Next.js App Router Workspace / artboard URL structure
|
||||
Auth Clerk Team-aware auth, org management, RBAC
|
||||
------------------- -------------------------------- ----------------------------------------------------------
|
||||
|
||||
**4.2 Canvas Architecture**
|
||||
|
||||
The canvas is a full-viewport React tree with three stacked layers, managed by absolute positioning and pointer-events toggling:
|
||||
|
||||
- Background Layer: Grid, guides, rulers --- purely decorative, rendered on a canvas element for performance
|
||||
|
||||
- Artboard Layer: The collection of Live Artboards, each an absolutely positioned iframe wrapper with overlay controls
|
||||
|
||||
- UI Chrome Layer: Toolbars, inspectors, panels --- all Fluent 2 components, rendered above the canvas
|
||||
|
||||
Viewport state (pan offset, zoom level) is managed in a single Zustand store and applied via a CSS transform matrix to the Artboard Layer. This approach enables smooth 60fps panning and zooming without re-rendering the artboard content.
|
||||
|
||||
**4.3 Fluent 2 Integration**
|
||||
|
||||
Originmain\'s own UI uses the following Fluent 2 pattern as its root structure. This must be established before any other UI work begins:
|
||||
|
||||
// packages/app/src/app/layout.tsx
|
||||
|
||||
import { FluentProvider, webLightTheme } from \'\@fluentui/react-components\';
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
|
||||
return \<FluentProvider theme={webLightTheme}\>{children}\</FluentProvider\>;
|
||||
|
||||
}
|
||||
|
||||
---------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**NOTE** Custom brand theming is achieved via createLightTheme(brandVariants) where brandVariants maps Originmain\'s \#0F52BA palette to the 16 BrandVariants slots. Store the custom theme in packages/ui/src/themes/originmain-theme.ts.
|
||||
---------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**4.4 Codebase File Browser (\@pierre/trees)**
|
||||
|
||||
Originmain exposes two tree-based navigation surfaces with different requirements, and they are served by different libraries. This distinction is architectural, not cosmetic.
|
||||
|
||||
**Artboard Navigator (Fluent 2 Tree + TreeItem).** The left-panel tree that shows workspace hierarchy, artboard groups, and project folders. This surface is UX-first: it navigates product metadata, never grows beyond a few hundred nodes, and must visually match the Fluent 2 design language that governs the rest of the chrome. Fluent 2's Tree and TreeItem components are the correct choice here --- they carry built-in ARIA roles, keyboard navigation, and Fluent token integration at zero additional cost. Implementation path: packages/app/src/components/navigator/ArtboardTree.tsx.
|
||||
|
||||
**Codebase File Browser (\@pierre/trees).** When Originmain connects to a React or Next.js repository, a separate file browser panel renders the connected codebase's file tree. This surface has categorically different requirements: real codebases contain thousands of files, git status indicators are essential (added, modified, deleted, renamed, untracked, ignored), path-aware search is required, and virtualization is not optional --- it is the baseline. \@pierre/trees (Apache 2.0, from The Pierre Computer Co.) is built precisely for this use case. It renders trees of 5,000+ files instantly via automatic row virtualization (only visible rows mount), shows git status badges natively via the gitStatus prop, supports three fileTreeSearchMode options for filtering, and collapses single-child directory chains via the flattenEmptyDirectories option. Its ARIA compliance (tree, treeitem, aria-level, aria-posinset, aria-setsize) matches Fluent 2's accessibility standards. Implementation path: packages/app/src/components/codebase/CodebaseFileTree.tsx.
|
||||
|
||||
Install: pnpm add \@pierre/trees. The library ships a React component as its primary API. Pass the file tree data structure (nodes with name, path, type, children, and optional gitStatus fields), and the component handles all rendering, virtualization, and keyboard interaction. Theming is applied via CSS custom properties on the container element, which allows Fluent 2 token values to be projected into the tree's visual layer without adopting the Pierre design language wholesale.
|
||||
|
||||
**5. Layer 2 --- Live Rendering Engine**
|
||||
|
||||
**5.1 Architecture Overview**
|
||||
|
||||
The Rendering Engine is the technical heart of Originmain and its most differentiated component. It renders a connected application\'s actual React component tree inside a sandboxed iframe, preserving full interactivity, state, and design token fidelity. This is fundamentally different from screenshot-based rendering (which produces a static image) or Figma-style rendering (which draws components from a spec, not the actual code).
|
||||
|
||||
**5.2 Module Federation Setup**
|
||||
|
||||
The rendering approach uses Webpack 5 Module Federation to share the connected application\'s components into the Originmain renderer without bundling them directly. The connected app exposes a Module Federation remote; the Originmain renderer host imports from it at runtime.
|
||||
|
||||
---------- ----------------------- -------------------------------------------------------------------------------------------------
|
||||
**Role** **Party** **Configuration**
|
||||
Remote Connected Application Exposes component routes via ModuleFederationPlugin; runs on localhost:3001 (dev) or CDN (prod)
|
||||
Host Originmain Renderer Consumes remote components; renders them in sandboxed iframes with injected design context
|
||||
Shared React, React-DOM Singleton shared to prevent duplicate React instances across host and remote
|
||||
---------- ----------------------- -------------------------------------------------------------------------------------------------
|
||||
|
||||
**5.3 Sandboxing Model**
|
||||
|
||||
Each Live Artboard renders inside an iframe with a carefully scoped sandbox attribute. The sandboxing serves two purposes: security isolation (the rendered app cannot access the host\'s DOM or cookies) and visual isolation (the rendered app\'s global CSS cannot leak into the canvas chrome).
|
||||
|
||||
- iframe sandbox attribute: allow-scripts allow-same-origin allow-forms
|
||||
|
||||
- Communication protocol: window.postMessage with structured message types defined in packages/renderer/src/protocol.ts
|
||||
|
||||
- Component tree extraction: a lightweight React DevTools hook injected into the iframe reads the Fiber tree and posts it to the host via the protocol
|
||||
|
||||
- Design token injection: Originmain injects a script before the remote app initialises that patches the Fluent 2 FluentProvider with the team\'s custom brand theme
|
||||
|
||||
**5.4 Rendering Triggers**
|
||||
|
||||
A Live Artboard can be created from any of the following origins, each handled by a distinct ingestion path:
|
||||
|
||||
----------------- ------------------------------------------------------------- ------------------------------------------------------
|
||||
**Origin Type** **Ingestion Path** **Metadata Captured**
|
||||
App Route URL User pastes URL; renderer navigates iframe to route Route path, component tree, design tokens, timestamp
|
||||
Linear Issue Linear SDK fetches issue; linked screenshot or URL rendered Issue ID, title, reporter, linked PR/commit
|
||||
Slack Message Slack API fetches message; screenshot or URL rendered Channel, author, timestamp, thread context
|
||||
Git Commit Hash GitHub API fetches commit; checks out and renders that tree Commit SHA, author, branch, diff from HEAD
|
||||
Manual Fork User duplicates an existing artboard Parent artboard ID, fork timestamp, author
|
||||
----------------- ------------------------------------------------------------- ------------------------------------------------------
|
||||
|
||||
**6. Layer 3 --- Visual Editing & Diff Engine**
|
||||
|
||||
**6.1 Intent Diff Schema**
|
||||
|
||||
The Intent Diff is the core data structure of Originmain. It is the contract between the design surface and the code agent. Unlike a pixel diff (which compares images) or a DOM diff (which compares HTML), an Intent Diff operates at the component level --- it describes changes in terms of the React component tree, props, and design tokens.
|
||||
|
||||
The Intent Diff schema (TypeScript):
|
||||
|
||||
interface IntentDiff {
|
||||
|
||||
artboardId: string;
|
||||
|
||||
timestamp: ISODateString;
|
||||
|
||||
author: UserId;
|
||||
|
||||
changes: ComponentChange\[\];
|
||||
|
||||
summary: string; // AI-generated natural language summary
|
||||
|
||||
beforeScreenshot: StorageUrl;
|
||||
|
||||
afterScreenshot: StorageUrl;
|
||||
|
||||
}
|
||||
|
||||
**6.2 Visual Editing Implementation**
|
||||
|
||||
Visual editing is implemented as an overlay system: a transparent interaction layer sits above the rendered iframe and intercepts mouse events. When a user clicks a component, the renderer identifies it via the Fiber tree map and renders selection handles. Drag operations are translated into prop changes (position, size, padding) and appended to the current Intent Diff.
|
||||
|
||||
-------------------- --------------------------------------------------------------------- ----------------------------------------------
|
||||
**Edit Operation** **Component-Level Change Produced** **Coding Agent Target**
|
||||
Resize component width/height prop or className token change Inline style or Tailwind/Fluent token update
|
||||
Reposition element CSS position/margin/padding change Layout class or inline style update
|
||||
Swap component Component reference change (e.g. SecondaryButton -\> PrimaryButton) Import statement + JSX element replacement
|
||||
Edit text children prop text node change JSX text node replacement
|
||||
Change colour Design token reassignment (colorBrandBackground, etc.) Token variable update in theme file
|
||||
Add/remove element Insertion/deletion in component subtree JSX child addition or removal
|
||||
-------------------- --------------------------------------------------------------------- ----------------------------------------------
|
||||
|
||||
**6.3 Undo/Redo & Branch Exploration**
|
||||
|
||||
Undo/redo is implemented via a Zustand middleware that maintains a linear history stack per artboard. Branch exploration (fork an artboard and try both directions) is implemented at the data model level: forking creates a new artboard record in the Origin Graph with a parent reference, both sharing the same rendering origin but accumulating independent change histories.
|
||||
|
||||
**6.4 Code-Level Diff Rendering (\@pierre/diffs)**
|
||||
|
||||
The Diff Engine (Section 6.1) computes Intent Diffs at the AST and component-prop level --- this is the logic layer and is custom TypeScript. A separate concern is the visual rendering of code-level diffs in two specific surfaces: the export panel (where a developer previews what code changes will be applied to their codebase before accepting), and the Agent Bridge communication view (where the coding agent's implementation status is shown alongside the design intent). For these surfaces, \@pierre/diffs (Apache 2.0, from The Pierre Computer Co.) is the designated renderer.
|
||||
|
||||
\@pierre/diffs is built on Shiki for syntax highlighting, uses CSS Grid and Shadow DOM for layout (resulting in fewer DOM nodes and faster paint than DOM-heavy alternatives), and ships three React components: MultiFileDiff (multiple changed files in one panel), PatchDiff (a git patch string rendered directly), and FileDiff (two arbitrary file contents compared). All three share a common prop set for configuration, annotations, and styling. The annotation framework is the critical capability for Originmain: it allows line-level context to be injected inline --- specifically, the natural-language summary phrases from the Intent Diff can be anchored to the exact lines they describe, giving developers a directly connected view of design intent and code change simultaneously.
|
||||
|
||||
The library supports both stacked (unified) and split (side-by-side) rendering modes. The export panel uses split mode by default (showing the before state in one column and the proposed after state in the other) and falls back to stacked on narrower drawer widths. The Agent Bridge communication view uses stacked mode to prioritise vertical information density alongside the chat-style agent dialogue.
|
||||
|
||||
Theming: \@pierre/diffs uses Shiki themes for syntax colouring. Map Fluent 2's webLightTheme and webDarkTheme to appropriate Shiki themes (github-light and github-dark-dimmed are the recommended defaults) and override the diff gutter and background colours via CSS custom properties on the container element to match Fluent 2 surface tokens (colorNeutralBackground1, colorNeutralBackground2). This produces a visually coherent diff panel without requiring the Pierre colour palette. Install: pnpm add \@pierre/diffs. Primary implementation path: packages/app/src/components/diff/CodeDiffPanel.tsx.
|
||||
|
||||
**7. Layer 4 --- Origin Graph & Data Store**
|
||||
|
||||
**7.1 PostgreSQL Schema**
|
||||
|
||||
The Origin Graph is stored in PostgreSQL (Supabase) as a directed acyclic graph. The core entities are Workspaces, Artboards, Origins, Diffs, and Agents. All tables use UUIDs as primary keys and include created\_at / updated\_at timestamps.
|
||||
|
||||
------------------------- ---------------------------------------------------------------------------- ----------------------------------------------------------------------------
|
||||
**Table** **Key Columns** **Description**
|
||||
workspaces id, name, owner\_id, plan, settings\_jsonb Top-level tenant boundary; one per team
|
||||
artboards id, workspace\_id, name, origin\_id, parent\_artboard\_id, metadata\_jsonb The core canvas unit; tracks lineage via parent\_artboard\_id
|
||||
origins id, type (ENUM), source\_ref, source\_metadata\_jsonb Typed origin record: GIT\_COMMIT, LINEAR\_ISSUE, SLACK\_MESSAGE, URL, FORK
|
||||
intent\_diffs id, artboard\_id, author\_id, changes\_jsonb, summary, status (ENUM) Each saved change set; status: DRAFT, EXPORTED, IMPLEMENTED, BLOCKED
|
||||
agent\_sessions id, artboard\_id, diff\_id, agent\_type, messages\_jsonb, status Bidirectional Agent Bridge conversation log
|
||||
design\_language\_files id, workspace\_id, name, schema\_jsonb, version Uploaded design language JSON/YAML, validated and stored
|
||||
team\_members id, workspace\_id, user\_id, role (ENUM) Role: OWNER, DESIGNER, ENGINEER, PM, VIEWER
|
||||
------------------------- ---------------------------------------------------------------------------- ----------------------------------------------------------------------------
|
||||
|
||||
**7.2 pg\_graphql API**
|
||||
|
||||
Supabase\'s pg\_graphql extension auto-generates a GraphQL API from the PostgreSQL schema. Originmain uses this as the primary data API for the frontend, supplemented by tRPC endpoints for write-heavy operations (diff creation, agent sessions) that require complex server-side logic.
|
||||
|
||||
---------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**PERF** The most expensive query is the Origin Graph traversal (finding all ancestors or descendants of an artboard). Pre-compute and cache ancestry paths in a materialised view (artboard\_ancestry) updated by a trigger on INSERT to artboards. This avoids recursive CTEs at query time.
|
||||
---------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**8. Layer 5 --- Design Language Runtime**
|
||||
|
||||
**8.1 Design Language File Format**
|
||||
|
||||
A Design Language File is a JSON document that defines a team\'s design system rules. It is validated against a JSON Schema on upload and stored in the design\_language\_files table. The runtime loads it into memory for use by the AI layer, visual editing constraints, and Completion Zone validation.
|
||||
|
||||
The file structure:
|
||||
|
||||
- tokens: Colour, typography, spacing, and motion tokens (maps to Fluent 2 token names where applicable)
|
||||
|
||||
- components: Per-component usage rules (allowed props, forbidden variants, required ARIA attributes)
|
||||
|
||||
- screens: Screen-level rules (which components are allowed, mandatory sections, layout constraints)
|
||||
|
||||
- voice: Tone-of-voice rules for AI-generated text content
|
||||
|
||||
- accessibility: Global WCAG requirements and custom accessibility rules
|
||||
|
||||
**8.2 Validation Pipeline**
|
||||
|
||||
Every AI completion and every visual edit that changes a design token is validated against the active Design Language File in real time. Violations are shown as inline annotations on the artboard --- not blocking errors, but visible warnings that require explicit acknowledgement before exporting a diff.
|
||||
|
||||
**9. Layer 6 --- AI Completion Layer**
|
||||
|
||||
**9.1 Claude API Integration**
|
||||
|
||||
The AI layer is built on Claude Sonnet 4 via the Anthropic SDK. All AI features route through a single AI gateway service (packages/ai-layer) which manages API key rotation, rate limiting, cost tracking, and prompt versioning. No AI calls are made directly from the frontend.
|
||||
|
||||
---------------------------- ----------------- ------------------------------------------------------------ -----------------------------------------------
|
||||
**AI Feature** **Model** **Input** **Output**
|
||||
Completion Zone fill Claude Sonnet 4 Zone context, design language file, surrounding components Structured component tree or content JSON
|
||||
Diff summary generation Claude Sonnet 4 Raw component-level changes (JSON) Natural language summary for human review
|
||||
Cross-artboard query Claude Sonnet 4 Natural language query + Origin Graph metadata Filtered artboard list with reasoning
|
||||
Design system drift report Claude Sonnet 4 Live app screenshot + design language file Drift violations with corrective Intent Diffs
|
||||
Agent Bridge Q&A Claude Sonnet 4 Coding agent question + artboard context Design agent answer with visual reference
|
||||
---------------------------- ----------------- ------------------------------------------------------------ -----------------------------------------------
|
||||
|
||||
**9.2 Completion Zone Implementation**
|
||||
|
||||
A Completion Zone is a React component rendered in the UI chrome layer over the relevant region of an artboard. The zone UI (Fluent 2 Card with intent selector and submit button) communicates with the AI layer via tRPC. The AI response is rendered as a proposed overlay on the artboard; the designer accepts, modifies, or rejects it before it is committed to the Intent Diff history.
|
||||
|
||||
**9.3 Prompt Engineering Guidelines**
|
||||
|
||||
- Every prompt includes: the team\'s design language file as a system constraint, the component tree context of the target region, and before/after screenshots of the artboard
|
||||
|
||||
- Prompts are versioned in packages/ai-layer/src/prompts/ and tested with an evaluation harness before deployment
|
||||
|
||||
- Temperature is set to 0.3 for completion tasks (deterministic quality) and 0.7 for generative variation tasks (creative alternatives)
|
||||
|
||||
- All AI outputs are validated against the Design Language File schema before being presented to the user; invalid outputs are silently regenerated up to 3 times before surfacing an error
|
||||
|
||||
**10. Layer 7 --- Agent Bridge (MCP)**
|
||||
|
||||
**10.1 Protocol Architecture**
|
||||
|
||||
The Agent Bridge is an MCP (Model Context Protocol) server that exposes Originmain\'s design context to external coding agents. It runs as a long-lived WebSocket server (Render) alongside a REST endpoint for polling-based agents. The bridge is bidirectional: it pushes diffs to coding agents and receives implementation status updates in return.
|
||||
|
||||
**10.2 MCP Server Tools**
|
||||
|
||||
The Originmain MCP server exposes the following tools to connected coding agents:
|
||||
|
||||
------------------------ ---------------------------------------- ----------------------------------------------------------------------------------------
|
||||
**MCP Tool** **Input** **Output**
|
||||
get\_pending\_diffs workspace\_id, artboard\_id (optional) Array of IntentDiff objects with status EXPORTED
|
||||
get\_artboard\_context artboard\_id Full artboard metadata, component tree, design language file, before/after screenshots
|
||||
ask\_design\_agent diff\_id, question: string Claude-generated answer from the design agent with visual reference
|
||||
update\_diff\_status diff\_id, status, notes Acknowledges implementation; updates diff record in Origin Graph
|
||||
get\_design\_language workspace\_id The team\'s active design language file for local validation
|
||||
------------------------ ---------------------------------------- ----------------------------------------------------------------------------------------
|
||||
|
||||
**10.3 Coding Agent Adapters**
|
||||
|
||||
Phase 2 ships with first-party adapters for Cursor and Claude Code. Each adapter wraps the MCP protocol in the coding agent\'s native integration format:
|
||||
|
||||
- Cursor: .cursorrules file + MCP server config in cursor\_settings.json; adapter translates IntentDiff to Cursor\'s \'edit plan\' format
|
||||
|
||||
- Claude Code: CLAUDE.md auto-generation + MCP server declaration; adapter streams IntentDiff as a structured task to the Claude Code session
|
||||
|
||||
- Generic: Raw MCP JSON-RPC over WebSocket for custom integrations
|
||||
|
||||
**11. Layers 8--10 --- Integrations, Multiplayer & Platform**
|
||||
|
||||
**11.1 Layer 8: Multi-Origin Ingestion**
|
||||
|
||||
Each integration is an isolated connector in packages/integrations that implements the OriginIngester interface: ingest(source) =\> Origin. Connectors run as serverless functions (Vercel Edge Functions) to minimise latency on the ingestion path.
|
||||
|
||||
----------------- ------------------- ----------------------------------------------------------------------------------------------------------------
|
||||
**Integration** **SDK / API** **Ingestion Flow**
|
||||
Linear \@linear/sdk v2 Webhook on issue update -\> fetch issue + attachments -\> render linked URL or screenshot as artboard
|
||||
Slack \@slack/bolt v4 Event API on message\_posted -\> parse URL or attachment -\> render as artboard with Slack thread context
|
||||
GitHub \@octokit/rest Webhook on PR open/push -\> render affected routes at commit SHA -\> diff against base branch artboard
|
||||
Intercom Intercom REST API Inbound webhook on user report -\> extract annotated screenshot -\> create artboard with user context metadata
|
||||
----------------- ------------------- ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**11.2 Layer 9: Multiplayer & Presence**
|
||||
|
||||
Real-time multiplayer is implemented using Liveblocks, which provides CRDT-based shared state, presence, and conflict resolution. The Liveblocks room maps 1:1 to a Workspace. Each artboard is a Liveblocks Storage object. Presence (cursor positions, active artboard, selection state) uses Liveblocks Presence API with a 50ms update throttle.
|
||||
|
||||
--------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
**IMPORTANT** Multiplayer is a Phase 3 feature. Design the data model and state management for eventual multiplayer from Phase 1 (use Zustand stores that can be swapped for Liveblocks storage without API changes), but do not integrate Liveblocks until Phase 3 to avoid complexity creep.
|
||||
--------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**11.3 Layer 10: Platform & Extensions**
|
||||
|
||||
The Plugin API (Phase 4) is a sandboxed JavaScript execution environment (based on the Figma Plugin API model) that allows third parties to:
|
||||
|
||||
- Read artboard metadata and Intent Diffs via a read-only API
|
||||
|
||||
- Write new artboards and origins via a write API (requires approval)
|
||||
|
||||
- Register custom Completion Zone types with custom AI prompts
|
||||
|
||||
- Add custom ingestion connectors beyond the first-party set
|
||||
|
||||
Enterprise features (Phase 4) include: SSO via SAML 2.0 (Clerk Enterprise), SCIM user provisioning, workspace-level audit logs (Supabase audit log extension), and white-label theming via the Fluent 2 createLightTheme API.
|
||||
|
||||
**12. Testing Strategy**
|
||||
|
||||
------------------------- ------------------- ----------------------------------------- -------------------------------------------
|
||||
**Layer** **Test Type** **Tooling** **Coverage Target**
|
||||
Diff Engine Unit Vitest 95% --- this is the most critical package
|
||||
Origin Graph Integration Vitest + Supabase local 80% on all query paths
|
||||
Design Language Runtime Unit Vitest 90% on validator logic
|
||||
AI Layer Eval harness Custom eval framework + Anthropic evals 80% acceptance rate on completions
|
||||
Agent Bridge Contract Pact (consumer-driven contracts) All MCP tools covered
|
||||
Canvas UI Component Storybook + Chromatic All Fluent 2 wrapper components
|
||||
Rendering Engine Visual regression Playwright + Percy All route renders covered
|
||||
Full product E2E Playwright 10 critical user journeys
|
||||
------------------------- ------------------- ----------------------------------------- -------------------------------------------
|
||||
|
||||
**12.1 The 10 Critical E2E Journeys**
|
||||
|
||||
8. Connect an app and render its first Live Artboard from a route URL
|
||||
|
||||
9. Make a visual edit and verify the correct Intent Diff is generated
|
||||
|
||||
10. Export a diff to Cursor via the Agent Bridge and verify receipt
|
||||
|
||||
11. Upload a Design Language File and verify AI completion respects its constraints
|
||||
|
||||
12. Ingest a Linear issue and render its linked screen as an artboard
|
||||
|
||||
13. Fork an artboard and verify independent change histories
|
||||
|
||||
14. Use the Cross-artboard Query to find all artboards matching a natural language filter
|
||||
|
||||
15. Complete a Completion Zone and accept the AI-generated content
|
||||
|
||||
16. Verify multiplayer presence shows correct cursor positions for two users
|
||||
|
||||
17. Generate a Design System Drift report and verify corrective diffs are accurate
|
||||
|
||||
**13. Security Considerations**
|
||||
|
||||
------------------------------------------ -----------------------------------------------------------------------------------------------------------------------------------
|
||||
**Risk** **Mitigation**
|
||||
Sandboxed iframe XSS Strict CSP, sandbox attribute, postMessage origin validation, no allow-same-origin + allow-scripts combined for untrusted content
|
||||
Design Language File injection JSON Schema validation on upload, Zod runtime parsing, no eval() of file contents
|
||||
AI prompt injection via artboard content Sanitise all user-generated content before including in AI prompts; use Anthropic\'s system prompt boundary strictly
|
||||
API key exposure All Anthropic API calls server-side only; Clerk JWT required on all tRPC routes; environment variables never in client bundle
|
||||
Origin Graph data leakage Row-level security (RLS) in Supabase on all tables; workspace\_id checked on every query; Clerk JWT verified server-side
|
||||
Agent Bridge abuse MCP connections require a signed workspace token; rate-limited to 100 diff exports per hour per workspace; all sessions logged
|
||||
Module Federation supply chain Pin remote versions; validate module hashes on load; disallow dynamic remote URLs from untrusted sources
|
||||
------------------------------------------ -----------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
**14. Phase-by-Phase Build Plan**
|
||||
|
||||
**Phase 1: Foundation (Months 1--4)**
|
||||
|
||||
Deliverables at the end of Phase 1 constitute the Minimum Viable Product for internal use:
|
||||
|
||||
---------- ---------------------------------------------------- ---------------------------------------------------------------------------
|
||||
**Week** **Milestone** **Acceptance Criteria**
|
||||
1--2 Monorepo scaffold, CI/CD, Supabase setup All packages build; GitHub Actions pipeline green; Supabase local running
|
||||
3--4 Fluent 2 canvas shell (Layout, Toolbar, Inspector) Canvas chrome renders; pan/zoom works; Fluent 2 tokens applied
|
||||
5--7 Rendering Engine v1 (iframe + Module Federation) Connected Next.js app renders inside canvas artboard
|
||||
8--10 Visual editing + Diff Engine v1 Click-to-select, drag-to-resize; Intent Diff generated on every edit
|
||||
11--12 Origin Graph v1 (PostgreSQL + pg\_graphql) Artboard CRUD; metadata stored; basic query API working
|
||||
13--14 Design Language File upload + validation File uploaded; AI and editing constrained to its rules
|
||||
15--16 Diff export (JSON + NL summary) Intent Diff exported to clipboard and file; summary generated by Claude
|
||||
---------- ---------------------------------------------------- ---------------------------------------------------------------------------
|
||||
|
||||
**Phase 2: Intelligence (Months 5--8)**
|
||||
|
||||
---------- ---------------------------------------------------- ------------------------------------------------------------------------
|
||||
**Week** **Milestone** **Acceptance Criteria**
|
||||
17--18 AI Completion Zones v1 Designer draws zone; Claude generates completion; accepted or rejected
|
||||
19--21 Agent Bridge v1 (MCP server + Cursor adapter) Cursor receives IntentDiff; status update flows back to artboard
|
||||
22--23 Linear + Slack ingestion Linear issue renders as artboard; Slack screenshot renders as artboard
|
||||
24--26 Claude Code adapter + bidirectional Q&A Claude Code receives diff; design agent answers coding agent questions
|
||||
27--28 Closed beta preparation: onboarding, docs, support 100 design engineering teams onboarded to closed beta
|
||||
---------- ---------------------------------------------------- ------------------------------------------------------------------------
|
||||
|
||||
**Phase 3: Scale (Months 9--12)**
|
||||
|
||||
---------- ------------------------------------------ ------------------------------------------------------------------------------
|
||||
**Week** **Milestone** **Acceptance Criteria**
|
||||
29--32 Liveblocks multiplayer integration Two users edit simultaneously; presence cursors visible; no data conflicts
|
||||
33--35 Cross-artboard natural language querying Query returns correct artboard set; Origin Graph traversal \< 200ms
|
||||
36--38 Design system drift detection Drift report generated; corrective diffs exported; coding agent applies them
|
||||
39--40 Vue + Svelte rendering support Vue and Svelte routes render as Live Artboards
|
||||
41--44 Public beta preparation and launch Public beta live; self-serve onboarding; pricing page active
|
||||
---------- ------------------------------------------ ------------------------------------------------------------------------------
|
||||
|
||||
**15. Performance Targets**
|
||||
|
||||
-------------------------------- -------------------- ----------------------------------------------------------------------
|
||||
**Operation** **Target** **Measurement**
|
||||
Artboard initial render (cold) \< 3 seconds Playwright performance.timing from navigation to iframe loaded
|
||||
Artboard re-render after edit \< 500ms Time from mouse-up on drag to updated visual feedback
|
||||
Intent Diff generation \< 100ms Time from edit commit to diff appearing in inspector panel
|
||||
Diff export to coding agent \< 2 seconds Time from export button click to coding agent receiving payload
|
||||
AI Completion Zone fill \< 8 seconds (P90) Time from zone submission to completion rendered in artboard
|
||||
Canvas pan/zoom 60fps sustained Chrome DevTools frame rate monitor during continuous pan gesture
|
||||
Origin Graph query (complex) \< 500ms p95 query time in production (measured via Supabase Query Analytics)
|
||||
Cross-artboard NL query \< 5 seconds Time from query submission to results rendered
|
||||
-------------------------------- -------------------- ----------------------------------------------------------------------
|
||||
|
||||
**Appendix: Key Dependencies**
|
||||
|
||||
------------------------------- -------------- --------------------------------------------------------------
|
||||
**Package** **Version** **Purpose**
|
||||
react 19.x Core UI framework
|
||||
next 15.x App framework, routing, RSC
|
||||
\@fluentui/react-components 9.x Design system and component library
|
||||
\@griffel/react 1.x CSS-in-JS (included with Fluent 2)
|
||||
zustand 5.x Canvas state management
|
||||
\@tanstack/react-query 5.x Server state and caching
|
||||
\@trpc/server + \@trpc/client 11.x Type-safe API layer
|
||||
\@anthropic-ai/sdk 0.x (latest) Claude API client
|
||||
\@modelcontextprotocol/sdk latest MCP server and client
|
||||
\@supabase/supabase-js 2.x Database, auth, storage, realtime
|
||||
\@liveblocks/client 2.x Multiplayer CRDT (Phase 3)
|
||||
\@linear/sdk 2.x Linear integration
|
||||
\@slack/bolt 4.x Slack integration
|
||||
\@octokit/rest 20.x GitHub integration
|
||||
zod 3.x Runtime schema validation
|
||||
vitest 2.x Unit and integration testing
|
||||
playwright 1.x E2E and visual regression testing
|
||||
yjs 13.x CRDT underlying Liveblocks (Phase 3)
|
||||
\@pierre/diffs 1.x (latest) Code-level diff rendering (export panel & Agent Bridge view)
|
||||
\@pierre/trees latest Codebase file browser (git status, virtualization, search)
|
||||
------------------------------- -------------- --------------------------------------------------------------
|
||||
|
||||
*End of Implementation Guide --- Originmain v1.0*
|
||||
@@ -1,456 +0,0 @@
|
||||
|
||||
|
||||
PRODUCT REQUIREMENTS DOCUMENT
|
||||
|
||||
**Originmain**
|
||||
|
||||
The AI-Native Design Engineering Platform
|
||||
|
||||
*Where design intent becomes production code, on a single canvas.*
|
||||
|
||||
| Version | 1.0 |
|
||||
| :---- | :---- |
|
||||
| **Date** | April 20, 2026 |
|
||||
| **Author** | Patrick (Product Lead) |
|
||||
| **Status** | Draft — Internal Review |
|
||||
| **Design System** | Microsoft Fluent 2 (@fluentui/react-components v9) |
|
||||
| **Classification** | Confidential |
|
||||
|
||||
# **Table of Contents**
|
||||
|
||||
**1\. Executive Summary**
|
||||
|
||||
**2\. Problem Statement**
|
||||
|
||||
**3\. Vision & Opportunity**
|
||||
|
||||
**4\. Target Users & Personas**
|
||||
|
||||
**5\. Product Principles**
|
||||
|
||||
**6\. Core Concepts & Mental Model**
|
||||
|
||||
**7\. Feature Requirements (P0–P2)**
|
||||
|
||||
**8\. Architecture & System Design**
|
||||
|
||||
**9\. Design System: Fluent 2 Integration**
|
||||
|
||||
**10\. Competitive Landscape**
|
||||
|
||||
**11\. Metrics & Success Criteria**
|
||||
|
||||
**12\. Phased Roadmap**
|
||||
|
||||
**13\. Risks & Mitigations**
|
||||
|
||||
**14\. Appendices**
|
||||
|
||||
# **1\. Executive Summary**
|
||||
|
||||
Originmain is an AI-native design engineering platform that collapses the distance between design intent and production code into a single, continuous workflow. It is a canvas where every artboard is a live, editable render of your actual application — not a static mockup — and every visual change produces a structured diff that a coding agent can execute against your real codebase.
|
||||
|
||||
The product synthesises two converging insights. The first, articulated by practitioners building modern product teams, is that the ideal design tool is a canvas where any view of your application can be rendered, explored, duplicated, and modified visually — where user feedback from tools like Linear can be ingested as a starting-point screen, where design language files guide AI-assisted completion, where every artboard carries metadata about its origin, author, and change history, and where edits export as executable plans for coding agents. The second insight, drawn from the concept of **designing for creation**, is that the most powerful creative tools do not present a blank canvas — they provide structured starting points, contextual scaffolding, and opinionated defaults that transform the intimidating emptiness of possibility into a launchpad for exploration. Originmain embeds this philosophy at every layer: you never start from nothing.
|
||||
|
||||
Originmain is built entirely on Microsoft’s Fluent 2 Design System, using @fluentui/react-components v9 as its component foundation. This is not merely a styling decision — it ensures that the tool’s own interface embodies the same systematic, accessible, and cross-platform design language it helps teams produce.
|
||||
|
||||
# **2\. Problem Statement**
|
||||
|
||||
**The 2026 design-to-code landscape is fragmented across three broken seams:**
|
||||
|
||||
## **2.1 The Handoff Gap**
|
||||
|
||||
Design tools produce static mockups. Code tools produce running software. The translation between them — the handoff — destroys intent. Pixel-perfect Figma files become “close enough” implementations, creating technical debt from day one. Industry reports show that even with modern AI-assisted design-to-code tools, the handoff between design and code still breaks, with teams spending 30–40% of sprint cycles on design-to-code reconciliation.
|
||||
|
||||
## **2.2 The Context Collapse**
|
||||
|
||||
Current tools silo context. A designer’s artboard knows nothing about the user feedback that prompted it, the engineer who will implement it, or the design system constraints that should govern it. Metadata — who created a view, what changed, when, and why — lives in Jira tickets, Slack threads, and people’s heads, not in the design artifact itself.
|
||||
|
||||
## **2.3 The Blank Canvas Problem**
|
||||
|
||||
Most creation tools start you at zero. A blank canvas is freedom, but it is also friction. Studies and practitioner experience confirm that adding even a single structured prompt to a blank canvas jumpstarts creative output. The most powerful tools do not offer infinite possibility — they offer curated starting points that channel creative energy. As one design leader observed, most people don’t want a blank canvas; they want a format, a prompt, a structure. If the tool doesn’t offer that scaffolding, the feed will.
|
||||
|
||||
# **3\. Vision & Opportunity**
|
||||
|
||||
**Originmain occupies the space where three previously separate categories converge:**
|
||||
|
||||
| Category | Current Tools | Originmain Approach |
|
||||
| :---- | :---- | :---- |
|
||||
| Visual Design | Figma, Stitch, Figma Make | Canvas renders live app views, not static frames |
|
||||
| AI Code Generation | v0, Bolt, Lovable, Cursor | Diffs export as structured plans for coding agents |
|
||||
| Design-Dev Collaboration | Zeplin, Figma Dev Mode, MCP | Metadata, provenance, and change history are native to every artboard |
|
||||
|
||||
**The opportunity is to build the first tool where a design change IS a code change — not a request for one.**
|
||||
|
||||
# **4\. Target Users & Personas**
|
||||
|
||||
## **4.1 Primary: The Design Engineer**
|
||||
|
||||
A hybrid practitioner who thinks in components and ships in code. They are frustrated by the false separation between design tools and code editors. They want a single surface where visual exploration and code-backed iteration are the same action. Role titles include Design Engineer, UI Engineer, Creative Technologist, and Frontend Developer with design sensibility.
|
||||
|
||||
## **4.2 Secondary: The Product Designer**
|
||||
|
||||
A designer who creates high-fidelity mockups but lacks the engineering skill to implement them. They want their visual intent to be preserved faithfully through implementation without learning to code. They are tired of handing off Figma files and seeing the result diverge from their vision.
|
||||
|
||||
## **4.3 Tertiary: The Product Manager**
|
||||
|
||||
A PM who receives user feedback (via Linear, Jira, Intercom, or direct user reports) and needs to communicate the required changes to design and engineering. They want to render the screen a user is complaining about, annotate it, and have that annotation flow directly into the build pipeline.
|
||||
|
||||
# **5\. Product Principles**
|
||||
|
||||
## **5.1 Never Start from Nothing**
|
||||
|
||||
Every interaction begins with structured context. A new artboard is pre-populated from a live app render, a user feedback screenshot, a design system template, or a team’s most recent work. The blank canvas is not the default — it is the exception. This principle is core to Originmain’s identity and draws directly from the insight that constraints catalyse creativity, while pure openness creates paralysis.
|
||||
|
||||
## **5.2 Design IS Code, Code IS Design**
|
||||
|
||||
There is no handoff because there is no gap. Every visual element on the canvas maps to a component in the codebase. Every visual edit produces a structured diff. The design tool and the code editor are the same surface viewed through different lenses.
|
||||
|
||||
## **5.3 Metadata is a First-Class Citizen**
|
||||
|
||||
Every artboard carries provenance: its origin (which screen, which commit, which user report), its author, its change history, and its relationship to other artboards. This metadata is queryable across the entire team’s workspace, enabling questions like “show me every artboard that originated from a user complaint in the last sprint.”
|
||||
|
||||
## **5.4 AI Fills, Humans Direct**
|
||||
|
||||
AI is a completion engine, not a replacement engine. Designers define zones, boundaries, and intent. AI completes lists, generates data-realistic content, fills layout regions, and suggests alternatives — always within the constraints the human has established. The tool has a point of view: it provides opinionated defaults informed by the team’s design language, product guidance files, and historical patterns.
|
||||
|
||||
## **5.5 Agents Communicate, Not Just Execute**
|
||||
|
||||
The design tool’s AI agents maintain check-ins with the coding agent. They do not simply hand over a spec — they communicate the nuances of the design: the spatial relationships, the motion intent, the edge cases, the “this should feel fast” qualitative judgements that a static spec cannot convey.
|
||||
|
||||
# **6\. Core Concepts & Mental Model**
|
||||
|
||||
## **6.1 The Live Artboard**
|
||||
|
||||
The fundamental unit of Originmain is the Live Artboard — a visual surface that renders a real view from your application. Unlike a Figma frame, a Live Artboard is aware of the component tree, state, props, and routing context of the screen it represents. You can fork an artboard, make visual changes, and those changes are tracked as a structured diff against the source.
|
||||
|
||||
## **6.2 The Origin Graph**
|
||||
|
||||
Every Live Artboard is a node in the Origin Graph, a directed acyclic graph that traces the provenance of every design decision. An artboard’s origin might be a Git commit, a Linear issue, a user feedback screenshot, or another artboard. The graph enables questions like: “Where did this design decision come from?” “Who changed this and when?” “Which user feedback led to this artboard?”
|
||||
|
||||
## **6.3 The Intent Diff**
|
||||
|
||||
When a designer modifies a Live Artboard, Originmain produces an Intent Diff — a structured representation of what changed, expressed at the component level (not the pixel level). An Intent Diff might say: “Replace the secondary button with a primary button in the checkout footer; increase the padding of the card container by 8px; add a loading skeleton to the product list.” This diff is the contract between design and code.
|
||||
|
||||
## **6.4 The Completion Zone**
|
||||
|
||||
A Completion Zone is a region on the canvas that the designer marks as “AI should fill this.” It might be a data table that needs realistic rows, a navigation menu that needs items generated from the sitemap, or an empty layout region that should be completed in a style consistent with the surrounding design. Completion Zones respect the team’s design language files and product guidance.
|
||||
|
||||
## **6.5 The Agent Bridge**
|
||||
|
||||
The Agent Bridge is the communication layer between Originmain’s design agents and external coding agents (Cursor, Claude Code, Copilot Workspace, or custom agents). The bridge does not simply export a spec — it maintains a bidirectional channel where the design agent can answer the coding agent’s questions, resolve ambiguities, and verify that the implementation matches intent.
|
||||
|
||||
# **7\. Feature Requirements**
|
||||
|
||||
Features are classified by priority: P0 (launch-blocking), P1 (launch-critical, can ship within 30 days post-launch), and P2 (strategic, scheduled for subsequent releases).
|
||||
|
||||
## **7.1 P0 — Launch Blocking**
|
||||
|
||||
### **7.1.1 Live App Rendering Engine**
|
||||
|
||||
* Render any route/view from a connected React or Next.js application as a Live Artboard on the canvas.
|
||||
|
||||
* Support importing component trees via an MCP server or direct codebase integration.
|
||||
|
||||
* Preserve component hierarchy, props, state, and design tokens in the rendered artboard.
|
||||
|
||||
* Enable rendering from external triggers: Linear issues, Slack messages, or pasted URLs.
|
||||
|
||||
### **7.1.2 Visual Editing with Structured Diffs**
|
||||
|
||||
* Allow direct visual manipulation of rendered components: resizing, repositioning, recolouring, swapping components, editing text content.
|
||||
|
||||
* Track every edit as a component-level Intent Diff, not a pixel-level change.
|
||||
|
||||
* Display Intent Diffs in the inspector sidebar as structured, human-readable component-level change summaries (rendered via Fluent 2 Card and Accordion components).
|
||||
|
||||
* Render code-level diffs in the export panel and Agent Bridge view using @pierre/diffs — stacked (unified) or split (side-by-side) mode, with Intent Diff annotation phrases anchored to the specific lines they describe.
|
||||
|
||||
* Support undo/redo with full history and branch-based exploration (fork an artboard, try both directions).
|
||||
|
||||
### **7.1.3 Design Language & Guidance Files**
|
||||
|
||||
* Allow teams to upload design language files (JSON/YAML) defining colour palettes, typography scales, spacing systems, component usage rules, and tone-of-voice guidelines.
|
||||
|
||||
* AI agents use these files as hard constraints when completing zones or suggesting alternatives.
|
||||
|
||||
* Build out a design language file/storybook document from a github repo
|
||||
|
||||
* Product guidance files define screen-level rules: which components are allowed on which screen types, mandatory accessibility patterns, and layout constraints.
|
||||
|
||||
### **7.1.4 Artboard Metadata & Origin Tracking**
|
||||
|
||||
* Every artboard stores: origin source, creator identity, creation timestamp, full change history, linked issues/tickets, and relationships to other artboards.
|
||||
|
||||
* Metadata is queryable via a search/filter interface: “Show me all artboards created by Sarah this week” or “Show me artboards linked to Linear issue LIN-4521.”
|
||||
|
||||
### **7.1.5 Diff Export to Coding Agents**
|
||||
|
||||
* Export Intent Diffs as structured plans (JSON \+ natural-language summary) compatible with Cursor, Claude Code, and GitHub Copilot Workspace.
|
||||
|
||||
* Include component references, file paths, prop changes, and visual context (before/after screenshots).
|
||||
|
||||
* Support MCP-based export for real-time streaming to connected coding agents.
|
||||
|
||||
### **7.1.6 Fluent 2 Component Library**
|
||||
|
||||
* Ship with the full @fluentui/react-components v9 library pre-loaded as the default component palette.
|
||||
|
||||
* All Originmain’s own UI surfaces are built using Fluent 2 components: FluentProvider, Button, Input, Dialog, Menu, Card, Tree, DataGrid, Toolbar, Tab, Badge, Avatar, Tooltip, and all 60+ components.
|
||||
|
||||
* Theming via Fluent 2 token system: webLightTheme, webDarkTheme, and custom brand themes via createLightTheme/createDarkTheme.
|
||||
|
||||
## **7.2 P1 — Launch Critical (Within 30 Days)**
|
||||
|
||||
### **7.2.1 AI Completion Zones**
|
||||
|
||||
* Designer draws a region and assigns an intent: “fill this table with realistic customer data,” “complete this navigation from the sitemap,” “generate 3 layout variations for this hero section.”
|
||||
|
||||
* AI respects design language files and product guidance.
|
||||
|
||||
* Support image-based completion: “use this screenshot as reference for the visual style of this zone.”
|
||||
|
||||
### **7.2.2 Agent Bridge (Design ↔ Code)**
|
||||
|
||||
* Bidirectional communication protocol between Originmain design agents and external coding agents.
|
||||
|
||||
* Design agent responds to coding agent queries: “What should this component look like in the error state?” “Is 16px or 24px padding intended here?”
|
||||
|
||||
* Coding agent reports implementation status back to the artboard: “Implemented,” “Blocked,” “Needs clarification.”
|
||||
|
||||
* Status sync visible on the canvas as component-level badges.
|
||||
|
||||
### **7.2.3 Multi-Origin Ingestion**
|
||||
|
||||
* Linear: Import issues as artboard starting points, with issue context embedded in metadata.
|
||||
|
||||
* Slack: Render a screen from a shared screenshot or URL, attributed to the sender.
|
||||
|
||||
* User Feedback Platforms: Ingest annotated screenshots from Intercom, Hotjar, or FullStory as artboard origins.
|
||||
|
||||
* Git: Render the UI at any commit hash to compare visual changes over time.
|
||||
|
||||
## **7.3 P2 — Strategic**
|
||||
|
||||
### **7.3.1 Cross-Artboard Querying**
|
||||
|
||||
* Natural-language queries across the entire team’s artboard workspace: “Which screens have changed the most in the last quarter?” “Show me every use of the legacy button component.”
|
||||
|
||||
* Powered by the Origin Graph and structured metadata.
|
||||
|
||||
### **7.3.2 Multi-Framework Support**
|
||||
|
||||
* Extend the rendering engine beyond React/Next.js to Vue, Svelte, Angular, and Flutter web.
|
||||
|
||||
* Framework-specific diff exporters that produce idiomatic code plans for each framework.
|
||||
|
||||
### **7.3.3 Collaborative Multiplayer Canvas**
|
||||
|
||||
* Real-time collaborative editing with presence indicators, cursors, and conflict resolution.
|
||||
|
||||
* Role-based permissions: designers edit visuals, engineers view diffs, PMs annotate and comment.
|
||||
|
||||
### **7.3.4 Design System Drift Detection**
|
||||
|
||||
* Continuously compare the live application against the design language files.
|
||||
|
||||
* Flag components that have drifted from the design system: wrong tokens, deprecated components, inconsistent spacing.
|
||||
|
||||
* Generate corrective Intent Diffs that a coding agent can apply to bring the codebase back into compliance.
|
||||
|
||||
# **8\. Architecture & System Design**
|
||||
|
||||
## **8.1 High-Level Architecture**
|
||||
|
||||
| Layer | Technology | Responsibility |
|
||||
| :---- | :---- | :---- |
|
||||
| Canvas UI | React 19 \+ Fluent UI v9 \+ Griffel CSS-in-JS | Infinite canvas, component palette, artboard rendering, visual editing tools |
|
||||
| Artboard Navigator | Fluent 2 Tree \+ TreeItem | Left-panel workspace and artboard hierarchy — in-ecosystem, Fluent-token-native |
|
||||
| Codebase File Browser | @pierre/trees (Apache 2.0) | Connected-repo file tree — git status badges, automatic virtualization, path search |
|
||||
| Rendering Engine | Sandboxed iframe \+ Module Federation | Safely render connected app components with full interactivity |
|
||||
| Diff Engine | Custom AST differ (TypeScript) | Compute component-level Intent Diffs from visual edits |
|
||||
| Diff Renderer | @pierre/diffs (Apache 2.0) | Code-level diff display in export panel and Agent Bridge view — Shiki-powered, annotatable |
|
||||
| Origin Graph | PostgreSQL \+ pg\_graphql | Store and query artboard provenance, metadata, and relationships |
|
||||
| AI Layer | Claude Sonnet 4 via Anthropic API | Completion Zones, design guidance enforcement, natural-language querying |
|
||||
| Agent Bridge | MCP (Model Context Protocol) | Bidirectional communication with external coding agents |
|
||||
| Integrations | Linear SDK, Slack API, Git providers | Multi-origin ingestion and status sync |
|
||||
| Auth & Collab | Clerk \+ Liveblocks | Authentication, multiplayer, presence, and permissions |
|
||||
|
||||
## **8.2 Data Flow**
|
||||
|
||||
The core data flow follows five stages:
|
||||
|
||||
* **Ingest:** A screen origin is received (app route, Linear issue, user screenshot, Git commit). The Rendering Engine produces a Live Artboard with full component-tree metadata.
|
||||
|
||||
* **Edit:** The designer modifies the artboard visually. Each edit is captured by the Diff Engine as a component-level Intent Diff and written to the Origin Graph.
|
||||
|
||||
* **Complete:** AI fills Completion Zones using the team’s design language files and the Claude API. Completions are tracked as diffs with AI attribution.
|
||||
|
||||
* **Export:** Intent Diffs are packaged as a structured plan (JSON \+ natural language) and sent to a coding agent via the Agent Bridge (MCP protocol).
|
||||
|
||||
* **Verify:** The coding agent reports implementation status back through the Agent Bridge. The artboard updates to show build progress, and the design agent answers clarifying questions.
|
||||
|
||||
# **9\. Design System: Fluent 2 Integration**
|
||||
|
||||
Originmain is built on Fluent 2 at every layer. This section defines how Fluent 2 is used in the product’s own interface and how it enables the design-to-code workflow.
|
||||
|
||||
## **9.1 Originmain’s Own UI**
|
||||
|
||||
* Root: FluentProvider wraps the entire application with webLightTheme (default) and webDarkTheme (toggled via settings). Custom brand theme available for white-labelling.
|
||||
|
||||
* Canvas Chrome: Toolbar, Menu, MenuButton, and Tab components for tool selection, view switching, and workspace navigation.
|
||||
|
||||
* Artboard Inspector: Card, Accordion, and DataGrid for displaying metadata, diffs, and component properties.
|
||||
|
||||
* Dialogs & Modals: Dialog, DialogSurface, and Drawer for settings, export flows, and agent communication.
|
||||
|
||||
* Input Surfaces: Input, Textarea, Combobox, Dropdown, and SearchBox for querying, filtering, and naming.
|
||||
|
||||
* Status & Feedback: Badge, Toast, Spinner, ProgressBar, and MessageBar for agent status, build progress, and system notifications.
|
||||
|
||||
* Navigation (workspace): Tree and TreeItem (Fluent 2) for the artboard navigator sidebar — workspace folders, artboard groups, and project hierarchy. Breadcrumb and Nav for top-level workspace routing.
|
||||
|
||||
* Navigation (codebase): @pierre/trees for the codebase file browser panel — renders the connected application's repository file tree with automatic virtualization (suitable for monorepos with thousands of files), git status badges (added, modified, deleted, renamed, untracked, ignored), path/filename search, and single-child directory chain flattening. Theming is projected via CSS custom properties mapped to Fluent 2 surface tokens.
|
||||
|
||||
## **9.2 Design Token Pipeline**
|
||||
|
||||
Fluent 2’s token system (colorBrandBackground, fontSizeBase300, spacingHorizontalM, etc.) is the backbone of Originmain’s theming. Teams can create custom brand themes using createLightTheme and createDarkTheme, which map to Fluent 2’s BrandVariants palette. These tokens flow through the rendering engine so that artboards produced by Originmain are automatically Fluent 2-compliant.
|
||||
|
||||
## **9.3 Accessibility**
|
||||
|
||||
Fluent 2 components ship with built-in WCAG 2.1 AA compliance: keyboard navigation, screen reader support, high-contrast mode, and focus management. Originmain inherits these capabilities without additional work. The Completion Zone AI is further constrained to produce only layouts that pass automated accessibility checks (colour contrast, touch target size, heading hierarchy).
|
||||
|
||||
# **10\. Competitive Landscape**
|
||||
|
||||
| Tool | What It Does Well | What Originmain Does Differently |
|
||||
| :---- | :---- | :---- |
|
||||
| Figma \+ Figma Make | Industry-standard design tool; AI-assisted layout generation; massive plugin ecosystem | Originmain artboards are live app renders, not static frames; edits produce code diffs, not design specs |
|
||||
| Google Stitch | Multi-screen AI generation; infinite canvas; voice input; free; code export to 7 frameworks | Originmain starts from YOUR app’s actual components, not generic AI-generated screens; diffs map to your codebase |
|
||||
| v0 (Vercel) | Production-quality React component generation; Git-native workflows; Next.js ecosystem | Originmain is a visual canvas first, not a chat-first code generator; supports bidirectional design-code sync |
|
||||
| Lovable / Bolt | Full-stack app generation from prompts; deployment included | Originmain is for teams with existing codebases, not greenfield apps; design intent preservation is the priority |
|
||||
| Cursor \+ MCP | AI-powered code editor with design context via Figma MCP; reads design tokens | Originmain IS the design surface that generates the context; it doesn’t read from a separate design tool |
|
||||
| Anima | Figma-to-code conversion; design-aware AI; playground for prototyping | Originmain eliminates Figma as a dependency; the artboard is the source of truth, not a Figma file |
|
||||
|
||||
**Originmain’s core differentiation: it is the only tool where the design surface, the code diff, the metadata graph, and the AI completion engine are a single integrated system — not a pipeline of separate tools stitched together with exports and plugins.**
|
||||
|
||||
# **11\. Metrics & Success Criteria**
|
||||
|
||||
| Metric | Target (6 months post-launch) | Measurement |
|
||||
| :---- | :---- | :---- |
|
||||
| Design-to-Code Cycle Time | ≤50% reduction vs. Figma \+ handoff baseline | Time from first artboard creation to PR merge, measured via Git integration |
|
||||
| Intent Fidelity Score | ≥90% of Intent Diffs implemented without manual correction | Automated comparison of exported diff vs. committed code changes |
|
||||
| Blank Canvas Bypass Rate | ≥80% of new artboards start from a structured origin (not blank) | Origin Graph analytics: proportion of artboards with non-null origin |
|
||||
| Agent Bridge Resolution Rate | ≥70% of coding agent queries resolved by design agent without human intervention | Agent Bridge conversation logs |
|
||||
| Completion Zone Acceptance Rate | ≥75% of AI completions accepted without modification | Edit history on Completion Zone outputs |
|
||||
| Weekly Active Teams | 500+ teams in closed beta | Auth \+ workspace analytics |
|
||||
|
||||
# **12\. Phased Roadmap**
|
||||
|
||||
## **Phase 1: Foundation (Months 1–4)**
|
||||
|
||||
* Canvas infrastructure: infinite canvas with pan/zoom, artboard CRUD, and Fluent 2-based chrome.
|
||||
|
||||
* Rendering Engine v1: render React/Next.js routes as Live Artboards via sandboxed iframes.
|
||||
|
||||
* Visual editing: direct manipulation of rendered components with Intent Diff generation.
|
||||
|
||||
* Design language file upload and enforcement.
|
||||
|
||||
* Artboard metadata and Origin Graph (PostgreSQL backend).
|
||||
|
||||
* Diff export to clipboard and file (JSON \+ natural language summary).
|
||||
|
||||
## **Phase 2: Intelligence (Months 5–8)**
|
||||
|
||||
* AI Completion Zones powered by Claude Sonnet 4\.
|
||||
|
||||
* Agent Bridge v1: MCP-based export to Cursor and Claude Code.
|
||||
|
||||
* Multi-origin ingestion: Linear, Slack, Git commit rendering.
|
||||
|
||||
* Agent check-in protocol: design agent communicates nuances to coding agent.
|
||||
|
||||
* Closed beta launch with 100 design engineering teams.
|
||||
|
||||
## **Phase 3: Scale (Months 9–12)**
|
||||
|
||||
* Collaborative multiplayer canvas (Liveblocks integration).
|
||||
|
||||
* Cross-artboard querying via natural language.
|
||||
|
||||
* Design system drift detection.
|
||||
|
||||
* Multi-framework rendering (Vue, Svelte).
|
||||
|
||||
* Public beta launch.
|
||||
|
||||
## **Phase 4: Platform (Months 13–18)**
|
||||
|
||||
* Plugin/extension API for custom integrations.
|
||||
|
||||
* Enterprise features: SSO, audit logs, workspace administration.
|
||||
|
||||
* White-label theming for agencies and design studios.
|
||||
|
||||
* Flutter web and Angular rendering support.
|
||||
|
||||
* General availability.
|
||||
|
||||
# **13\. Risks & Mitigations**
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| :---- | :---- | :---- |
|
||||
| Rendering fidelity: sandboxed rendering may not perfectly match production environments | High — trust in the tool depends on visual accuracy | Use Module Federation for component sharing; automated visual regression testing against production screenshots |
|
||||
| AI hallucination in Completion Zones | Medium — incorrect completions erode trust | Hard-constrain AI output to design language files; human approval gate before completion is committed; confidence scoring |
|
||||
| MCP protocol maturity: Agent Bridge depends on MCP ecosystem adoption | Medium — limited coding agent compatibility | Ship with first-party Cursor and Claude Code adapters; abstract over protocol layer to support future alternatives |
|
||||
| Fluent 2 dependency: tightly coupling to Fluent 2 may limit appeal for non-Microsoft teams | Medium — addressable market concern | Originmain’s own UI uses Fluent 2, but the rendering engine is design-system agnostic; teams render THEIR components, not Fluent components |
|
||||
| Competitive response from Figma/Google | High — both have AI canvas initiatives | Originmain’s moat is the bidirectional Agent Bridge and Origin Graph — features that require deep codebase integration, not just AI generation |
|
||||
| Team adoption inertia: designers resistant to leaving Figma | High — user acquisition challenge | Figma import tool (convert Figma frames to Live Artboards); gradual adoption path where Originmain reads from Figma via MCP before fully replacing it |
|
||||
|
||||
# **14\. Appendices**
|
||||
|
||||
## **Appendix A: Glossary**
|
||||
|
||||
| Term | Definition |
|
||||
| :---- | :---- |
|
||||
| Live Artboard | A canvas surface that renders a live view from a connected application, carrying full component-tree metadata |
|
||||
| Origin Graph | A directed acyclic graph tracking the provenance of every artboard and design decision |
|
||||
| Intent Diff | A structured, component-level representation of design changes, expressed as props/layout/component mutations |
|
||||
| Completion Zone | A designer-defined region on the canvas where AI generates or completes content within design-system constraints |
|
||||
| Agent Bridge | The MCP-based bidirectional communication layer between Originmain’s design agents and external coding agents |
|
||||
| Design Language File | A JSON/YAML configuration defining a team’s design system rules: tokens, component usage, accessibility requirements |
|
||||
| Product Guidance File | A configuration defining screen-level design rules: which components are permitted on which screen types |
|
||||
|
||||
## **Appendix B: Key References**
|
||||
|
||||
* Microsoft Fluent 2 Design System: https://fluent2.microsoft.design
|
||||
|
||||
* Fluent UI React Components v9: https://react.fluentui.dev
|
||||
|
||||
* Model Context Protocol (MCP): https://modelcontextprotocol.io
|
||||
|
||||
* Daniel Bezos, “Designing for Creation” (2025): Principles on structured starting points and the blank-canvas problem
|
||||
|
||||
* Figma MCP Server and AI Agent Canvas: Design-code bridging via MCP (April 2026\)
|
||||
|
||||
* Google Stitch 2.0 (March 2026): AI-native infinite canvas with multi-screen generation
|
||||
|
||||
* NxCode Vibe Design Tools Report (2026): Industry analysis of AI design tool adoption
|
||||
|
||||
## **Appendix C: Fluent 2 Component Inventory for Originmain UI**
|
||||
|
||||
The following Fluent 2 components are used in Originmain’s own interface:
|
||||
|
||||
| Surface | Components | Library |
|
||||
| :---- | :---- | :---- |
|
||||
| Canvas Chrome | Toolbar, ToolbarButton, Menu, MenuButton, MenuList, MenuItem, Tab, TabList, Divider | Fluent 2 |
|
||||
| Artboard Inspector | Card, CardHeader, Accordion, AccordionItem, DataGrid, DataGridRow, DataGridCell, Badge | Fluent 2 |
|
||||
| Dialogs & Overlays | Dialog, DialogSurface, DialogTitle, DialogBody, DialogActions, Drawer, Popover, Tooltip | Fluent 2 |
|
||||
| Inputs & Search | Input, Textarea, Combobox, Dropdown, Option, SearchBox, Switch, Checkbox, RadioGroup, Slider | Fluent 2 |
|
||||
| Artboard Navigator | Tree, TreeItem, Breadcrumb, BreadcrumbItem, Nav, NavItem, Link | Fluent 2 |
|
||||
| Status & Feedback | Toast, Toaster, MessageBar, Spinner, ProgressBar, Skeleton, PresenceBadge | Fluent 2 |
|
||||
| Data & Layout | Table, TableHeader, TableRow, TableCell, Avatar, AvatarGroup, CounterBadge, Tag, InteractionTag | Fluent 2 |
|
||||
| Codebase File Browser | FileTree (root component), git status indicators, search, virtualized list | @pierre/trees |
|
||||
| Export Panel / Agent Bridge | MultiFileDiff, FileDiff (split and stacked modes), line annotations | @pierre/diffs |
|
||||
|
||||
*End of Document*
|
||||
@@ -0,0 +1,425 @@
|
||||
# Product Requirements Document
|
||||
|
||||
**Wordbench**
|
||||
|
||||
The Agentic WordPress Development Environment
|
||||
|
||||
*Cursor for how software gets built. WordPress for how the web gets published. Wordbench is both.*
|
||||
|
||||
| Field | Value |
|
||||
| :--- | :--- |
|
||||
| **Version** | 1.0 |
|
||||
| **Date** | August 12, 2026 |
|
||||
| **Author** | Patrick (Product Lead) |
|
||||
| **Status** | Draft — Internal |
|
||||
| **Repo** | SinachPat/originmain (pivoting; rename TBD) |
|
||||
| **Classification** | Confidential |
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Wordbench is an agentic development environment purpose-built for WordPress. It takes everything that makes Cursor transformative for general software engineering — repo-aware chat, multi-file agents, inline edit, terminal control, diffs you can trust, project rules, and tool use — and rebuilds it around how WordPress actually works: themes, plugins, blocks, hooks, the template hierarchy, WP-CLI, MySQL state, staging/production, and the plugin/theme ecosystem.
|
||||
|
||||
Today, WordPress builders bounce between Local/DDEV, VS Code/PhpStorm, a browser admin, WP-CLI in a separate terminal, phpMyAdmin, and ChatGPT tabs that do not know their `functions.php` from a mu-plugin. Agents that are great at TypeScript are naïve about `add_action`, `block.json`, or why a change in `theme.json` did not appear on the front end.
|
||||
|
||||
Wordbench collapses that into one environment where:
|
||||
|
||||
- The **site is a first-class runtime** (local WordPress you can boot, browse, and reset), not just a folder of files.
|
||||
- The agent understands **WordPress semantics** (hooks, template hierarchy, CPT/taxonomies, block editor, WooCommerce patterns), not only PHP/JS syntax.
|
||||
- Changes are proposed as reviewable diffs against theme/plugin code **and** as intentional WP-CLI / database operations when state must change.
|
||||
- Design-to-build for blocks and FSE themes is native: edit `theme.json`, register blocks, sync patterns, preview in an embedded site frame.
|
||||
- Shipping means **environments** (local → staging → production), not only `git push`.
|
||||
|
||||
**The opportunity:** be the default professional environment for anyone who builds with WordPress in an agent-native world — agencies, product teams on WP headless/hybrid, plugin vendors, and ambitious solo builders.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem Statement
|
||||
|
||||
### 2.1 Cursor is general; WordPress is a platform
|
||||
|
||||
General coding agents treat a WordPress project like any PHP app. They miss:
|
||||
|
||||
- The split between **code** (themes/plugins) and **content/state** (posts, options, post meta, transients).
|
||||
- **Load order** and hook timing (`plugins_loaded` vs `init` vs `wp_enqueue_scripts`).
|
||||
- **Child themes**, template hierarchy, and block theme / classic theme dual world.
|
||||
- **WP-CLI** as the real automation API.
|
||||
- **Multisite**, capability checks, and nonce/auth patterns.
|
||||
- **Gutenberg** block development (`block.json`, `render.php`, editor APIs).
|
||||
|
||||
Result: plausible patches that break production, enqueue assets wrong, or bypass APIs the platform already provides.
|
||||
|
||||
### 2.2 The WordPress toolchain is fragmented
|
||||
|
||||
A typical build involves:
|
||||
|
||||
| Concern | Today's tool |
|
||||
| :--- | :--- |
|
||||
| Edit code | VS Code / PhpStorm |
|
||||
| Run site | Local WP, DDEV, Lando, wp-env, Docker |
|
||||
| Admin / content | wp-admin in browser |
|
||||
| Automate | WP-CLI in another terminal |
|
||||
| DB | phpMyAdmin / TablePlus |
|
||||
| AI | Cursor/ChatGPT without site context |
|
||||
| Deploy | FTP, rsync, Git push + host pipelines, ManageWP |
|
||||
|
||||
Context dies at every boundary. The agent cannot open wp-admin, inspect the REST schema of *this* site, or verify a block in the editor.
|
||||
|
||||
### 2.3 Agencies sell velocity; the stack sells friction
|
||||
|
||||
WordPress agencies and product studios compete on turnaround. Their bottleneck is not "writing PHP" — it is environment spin-up, safe changes across code+DB, regression checks on real themes, and handoff between design, content, and engineering. Existing AI features in hosts (e.g. copy helpers) do not replace an engineering environment.
|
||||
|
||||
### 2.4 Security and blast radius
|
||||
|
||||
WordPress sites are high-value attack surfaces. Unconstrained agents that edit `wp-config.php`, install plugins from arbitrary zips, or run SQL are dangerous. Wordbench must make **safe-by-default agent permissions** a product feature, not an afterthought.
|
||||
|
||||
---
|
||||
|
||||
## 3. Vision & Opportunity
|
||||
|
||||
**Vision:** Open a WordPress project in Wordbench the way you open a repo in Cursor — and the site boots, the agent already knows your theme/plugin map, and "add a pricing block that matches our patterns and ship it to staging" is one agent run with diffs + CLI + preview.
|
||||
|
||||
Wordbench occupies the intersection of:
|
||||
|
||||
| Category | Examples | Wordbench stance |
|
||||
| :--- | :--- | :--- |
|
||||
| AI code editors | Cursor, Windsurf, Copilot Workspace | Same agent UX ceiling; WP-native tools & runtime |
|
||||
| Local WP environments | Local, DDEV, wp-env | Environment is embedded and agent-controllable |
|
||||
| WP-specific IDEs / studios | Limited / aging | Modern agent-first, block-era native |
|
||||
| Host AI / site copilots | Host dashboards | Dev environment, not marketing copy widgets |
|
||||
|
||||
**Differentiation:** Wordbench is not "ChatGPT in wp-admin." It is not "Cursor with a WordPress system prompt." It is an **environment** where code, runtime, CLI, database introspection, and preview share one agent loop.
|
||||
|
||||
---
|
||||
|
||||
## 4. Target Users & Personas
|
||||
|
||||
### 4.1 Primary — Agency WordPress Engineer
|
||||
|
||||
Builds custom themes/plugins for clients weekly. Lives in deadlines. Wants faster scaffolding, safer refactors, and less context switching. Comfortable with Git; allergic to FTP surprises.
|
||||
|
||||
### 4.2 Primary — Plugin / Block Product Developer
|
||||
|
||||
Ships commercial or open-source plugins and block libraries. Needs scaffolding, test runs, wp-env, WPCS, and release hygiene. Wants agents that respect plugin headers, text domains, and `@wordpress/scripts` build pipelines.
|
||||
|
||||
### 4.3 Secondary — Technical Founder / Solo Builder
|
||||
|
||||
Uses WordPress as the product surface (LMS, membership, WooCommerce). Wants Cursor-like leverage without hiring a full WP team.
|
||||
|
||||
### 4.4 Secondary — Design Engineer on Block Themes
|
||||
|
||||
Works in `theme.json`, patterns, and template parts. Needs visual preview + structured edits, not only raw PHP.
|
||||
|
||||
### 4.5 Tertiary — Technical PM / Solutions Architect
|
||||
|
||||
Scopes client builds, reviews agent diffs, cares about migration plans and staging verification.
|
||||
|
||||
**Non-goals for v1 users:** pure content editors who only need AI to write posts inside wp-admin (different product).
|
||||
|
||||
---
|
||||
|
||||
## 5. Product Principles
|
||||
|
||||
1. **Site is a runtime, not a folder** — If the agent cannot boot, browse, and assert against a real WordPress, it is guessing.
|
||||
2. **Code and state are both first-class** — File diffs *and* explicit data/CLI operations; never silent DB mutation.
|
||||
3. **WordPress semantics over generic PHP** — Prefer platform APIs, hooks, and block patterns the way a senior WP engineer would.
|
||||
4. **Safe by default** — Capability-scoped tools; production gated; secrets redacted; destructive ops require confirmation.
|
||||
5. **Preview is proof** — Agent claims are backed by screenshots, HTTP assertions, or editor canvas checks when possible.
|
||||
6. **Git is the system of record for code** — Content/state migrations are exported as replayable WP-CLI scripts or migration packages, not tribal knowledge.
|
||||
7. **Feels like Cursor** — Chat, inline edit, multi-file agent, rules, terminal, diffs — familiar UX, deeper WP tools underneath.
|
||||
|
||||
---
|
||||
|
||||
## 6. Core Concepts & Mental Model
|
||||
|
||||
### 6.1 Workspace = Project + Site
|
||||
|
||||
A Wordbench **Workspace** binds:
|
||||
|
||||
- A Git repo (theme, plugin, monorepo of plugins, or Bedrock/WordPress Composer layout)
|
||||
- A **Site runtime** (wp-env, Docker, or imported Local/DDEV)
|
||||
- Environment config (local/staging/production endpoints & credentials vault)
|
||||
|
||||
### 6.2 The WordPress Knowledge Graph
|
||||
|
||||
Indexed understanding of:
|
||||
|
||||
- Themes / child themes / active theme
|
||||
- Plugins (active, mu-plugins, drop-ins)
|
||||
- Registered CPTs, taxonomies, REST routes
|
||||
- Hook map (approximate: from code analysis)
|
||||
- Block library (`block.json` inventory)
|
||||
- `theme.json` tokens and style variations
|
||||
- Template hierarchy resolution for key routes
|
||||
|
||||
### 6.3 The Agent Loop (WP-native)
|
||||
|
||||
Plan → edit files → run WP-CLI / tests → refresh preview → read logs → revise. Tools are WordPress-aware (see §7).
|
||||
|
||||
### 6.4 Intent Diffs for State
|
||||
|
||||
When a task requires DB/content changes (create CPT seed data, flip an option, import a pattern), Wordbench proposes a **State Diff**: WP-CLI commands and/or a migration script the user can apply, review, and commit — not a hidden phpMyAdmin edit.
|
||||
|
||||
### 6.5 Rules & Playbooks
|
||||
|
||||
Cursor Rules equivalent: `WORDPRESS.md` / `.wordbench/rules` covering coding standards, agency patterns, banned functions (`extract`, direct SQL without `$wpdb->prepare`), and deploy checklists. **Playbooks** are reusable agent workflows ("scaffold a dynamic block", "migrate classic → block theme section").
|
||||
|
||||
### 6.6 Environments
|
||||
|
||||
Local (full control) → Staging (restricted agent: sync down, careful push) → Production (read + observe by default; write only with explicit break-glass).
|
||||
|
||||
---
|
||||
|
||||
## 7. Feature Requirements
|
||||
|
||||
Priority: **P0** launch-blocking, **P1** within ~30–60 days of launch, **P2** strategic.
|
||||
|
||||
### 7.1 P0 — Launch Blocking
|
||||
|
||||
#### 7.1.1 Cursor-class Editor UX
|
||||
- Repo-aware chat with @-context (files, symbols, docs)
|
||||
- Inline edit / Cmd-K style rewrite
|
||||
- Multi-file agent mode with applyable diffs
|
||||
- Terminal panel
|
||||
- Git status, diff review, commit message assist
|
||||
- Project rules (`.wordbench/rules`, `WORDPRESS.md`)
|
||||
|
||||
#### 7.1.2 WordPress Project Intelligence
|
||||
- Auto-detect project shape: classic theme, block theme, single plugin, `wp-content` checkout, Bedrock/Composer
|
||||
- Map active theme/plugins when site is connected
|
||||
- PHP + JS (block) language support with WP stubs / wordpress-stubs integration
|
||||
- Awareness of template hierarchy and `block.json` packages
|
||||
|
||||
#### 7.1.3 Embedded Local Site Runtime
|
||||
- One-click (or one-command) local site via **wp-env** (default) and documented Docker compose escape hatch
|
||||
- Start/stop/reset site from the IDE
|
||||
- Embedded browser preview (front end + wp-admin)
|
||||
- Log tail (PHP error log, web server; Query Monitor integration later)
|
||||
|
||||
#### 7.1.4 WP-CLI Tooling for Agents
|
||||
- First-class WP-CLI runner tool with allowlist
|
||||
- Common recipes: scaffold plugin/theme/block, rewrite flush, cache flush, plugin activate, db export/import (local only by default)
|
||||
- Command preview before execute for destructive commands
|
||||
|
||||
#### 7.1.5 Safe Apply & Permissions
|
||||
- Tool permission tiers: Read FS, Edit FS, Run WP-CLI (safe), Run WP-CLI (destructive), SQL read, SQL write, Network install
|
||||
- Production write disabled by default
|
||||
- Secret scanning on apply; redact `.env` / `wp-config` secrets in agent context by default
|
||||
|
||||
#### 7.1.6 Preview Verification
|
||||
- After agent edits, optional "Verify" step: load URL(s), capture screenshot, basic console/network error sniff
|
||||
- Block editor open-and-insert check for block tasks (P0 lightweight: open site editor / post editor URL)
|
||||
|
||||
#### 7.1.7 Scaffolding Playbooks
|
||||
- New plugin (headers, text domain, composer/phpunit optional)
|
||||
- New dynamic / static block (`@wordpress/scripts`)
|
||||
- Child theme
|
||||
- CPT + REST + basic admin UI (generate + wire)
|
||||
|
||||
### 7.2 P1 — Launch Critical Follow-on
|
||||
|
||||
#### 7.2.1 Database & Options Introspection
|
||||
- Read-only schema explorer and options search
|
||||
- "Explain this options row" with link back to plugin code when possible
|
||||
- State Diff generator for option/post meta changes
|
||||
|
||||
#### 7.2.2 Block & FSE Workshop
|
||||
- `theme.json` structured editor + agent tools
|
||||
- Pattern library sync (export/import patterns as files)
|
||||
- Visual select-an-element → jump to template part / block source (best-effort)
|
||||
|
||||
#### 7.2.3 Quality Gates
|
||||
- PHPCS with WordPress Coding Standards
|
||||
- PHPUnit / Playwright (or wp-env e2e) hooks
|
||||
- Agent must run configured gates before "done" on PRs
|
||||
|
||||
#### 7.2.4 Staging Connectors
|
||||
- Pull DB/media from staging (explicit, logged)
|
||||
- Deploy code via GitHub Actions / host APIs (extendable)
|
||||
- Never pull production DB without double confirm + scrubbing warnings
|
||||
|
||||
#### 7.2.5 Hook & REST Introspection
|
||||
- List REST routes from living site
|
||||
- Static analysis assist for `add_action` / `add_filter` registrations
|
||||
|
||||
#### 7.2.6 Migration from Existing Tools
|
||||
- Import Local WP / DDEV project paths
|
||||
- Open existing wp-env repos with zero config
|
||||
|
||||
### 7.3 P2 — Strategic
|
||||
|
||||
#### 7.3.1 WooCommerce-aware agent skills
|
||||
#### 7.3.2 Multisite management tools
|
||||
#### 7.3.3 Host marketplace integrations (WP Cloud, SpinupWP, Rocket.net, etc.)
|
||||
#### 7.3.4 Collaborative multiplayer workspaces for agencies
|
||||
#### 7.3.5 Autonomous "site maintenance agent" (updates, conflict triage) with strict sandboxes
|
||||
#### 7.3.6 Design intake (Figma → block theme patterns) as an optional bridge
|
||||
#### 7.3.7 Headless WP (Faust/Next) hybrid workspaces
|
||||
|
||||
---
|
||||
|
||||
## 8. Architecture & System Design
|
||||
|
||||
### 8.1 High-level
|
||||
|
||||
| Layer | Responsibility |
|
||||
| :--- | :--- |
|
||||
| **Editor shell** | Cursor-like IDE UX (files, chat, agent, terminal, git) |
|
||||
| **WP language services** | PHP/JS + WP stubs, `block.json`, `theme.json` schemas |
|
||||
| **Site runtime manager** | wp-env/Docker lifecycle, ports, credentials |
|
||||
| **Agent tool bus** | File tools, WP-CLI, HTTP preview, DB read, linters |
|
||||
| **Knowledge index** | Codebase index + WP graph (plugins, blocks, routes) |
|
||||
| **Policy engine** | Permissions, environment gates, secret redaction |
|
||||
| **Preview** | Embedded browser + screenshot/verify services |
|
||||
| **Connectors** | GitHub, staging hosts, optional design tools |
|
||||
|
||||
### 8.2 Default local stack
|
||||
- **wp-env** for reproducibility and CI parity
|
||||
- SQLite integration option for ultralight demos (secondary); MySQL remains default for fidelity
|
||||
- Node LTS for `@wordpress/scripts` block builds
|
||||
|
||||
### 8.3 Agent tools (initial set)
|
||||
- `fs.read` / `fs.write` / `fs.apply_patch`
|
||||
- `wpcli.run` (categorized)
|
||||
- `site.browse` / `site.screenshot`
|
||||
- `site.request` (front/REST)
|
||||
- `db.query` (read-only default)
|
||||
- `lint.phpcs` / `test.phpunit`
|
||||
- `index.search` / `wp.graph.lookup`
|
||||
|
||||
### 8.4 Data flow (happy path)
|
||||
1. User: "Add an FAQ accordion block to the theme and show it on /pricing."
|
||||
2. Agent reads theme type, block build setup, existing patterns.
|
||||
3. Scaffolds block, registers it, adds pattern or template reference.
|
||||
4. Runs build + WP-CLI flush as needed.
|
||||
5. Opens /pricing + editor insert path; screenshots.
|
||||
6. Presents file diffs + optional State Diff; user approves.
|
||||
|
||||
---
|
||||
|
||||
## 9. Design System & UX Notes
|
||||
|
||||
- Dark-first IDE aesthetic (Cursor-familiar), dense but calm
|
||||
- **Site status bar**: environment, WP version, active theme, agent permission mode
|
||||
- **Diff view**: Files tab + State tab
|
||||
- **Preview pane**: dockable; device widths; logged-in as role selector (Admin/Editor/Customer)
|
||||
- Accessibility: full keyboard for chat/agent; don't trap focus in preview iframe without escape
|
||||
|
||||
---
|
||||
|
||||
## 10. Competitive Landscape
|
||||
|
||||
| Product | Strength | Gap vs Wordbench |
|
||||
| :--- | :--- | :--- |
|
||||
| Cursor | Best-in-class agentic coding | Not WP-runtime aware |
|
||||
| VS Code + Copilot | Ubiquitous | Same fragmentation; weak WP semantics |
|
||||
| PhpStorm | Deep PHP | Not agent-native; no WP site loop |
|
||||
| Local WP | Easy site spin-up | Not a coding agent environment |
|
||||
| wp-env / DDEV | Solid runtimes | CLI-centric, no agent UX |
|
||||
| Host AI features | Convenient for content | Not engineering environments |
|
||||
| Specialized WP AI plugins | In-admin helpers | No real IDE / Git / agent loop |
|
||||
|
||||
**Moat:** the closed loop of **WP knowledge graph + controllable runtime + policy-aware tools + verify-via-preview**, trained into playbooks agencies repeat daily.
|
||||
|
||||
---
|
||||
|
||||
## 11. Metrics & Success Criteria
|
||||
|
||||
| Metric | 6-month target | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| Time to first local site preview from new workspace | ≤ 10 minutes p50 | Including deps |
|
||||
| Agent task success (user accepts without major rework) | ≥ 60% on P0 playbooks | Scaffold block, child theme, CPT |
|
||||
| Verify step catches regressions before accept | ≥ 30% of failed tasks | Leading indicator of loop value |
|
||||
| Weekly active paying seats | TBD after pricing | Agency team seats primary |
|
||||
| Retention (trial → weekly habit by week 4) | ≥ 40% | Habit formation |
|
||||
|
||||
Qualitative bar: senior WP engineers say "it doesn't do dumb WordPress things."
|
||||
|
||||
---
|
||||
|
||||
## 12. Phased Roadmap
|
||||
|
||||
### Phase 0 — Pivot & Spec (now)
|
||||
- Clear old Originmain codebase
|
||||
- PRD + positioning + name finalization (**Wordbench** working title)
|
||||
- Technical spike: wp-env control plane + agent tool bus
|
||||
|
||||
### Phase 1 — Foundation (Weeks 1–8)
|
||||
- Editor shell (reuse strategy TBD: VS Code OSS vs cloud remote)
|
||||
- Project open + WP detection
|
||||
- wp-env lifecycle + preview iframe
|
||||
- Chat + diffs + rules
|
||||
- WP-CLI tool + permission engine
|
||||
- P0 playbooks (plugin, block, child theme)
|
||||
|
||||
### Phase 2 — Intelligence (Weeks 9–16)
|
||||
- WP knowledge graph v1
|
||||
- PHPCS/WPCS + tests in agent loop
|
||||
- State Diffs + DB introspection (read)
|
||||
- Staging pull (careful)
|
||||
- Closed alpha with 10–20 agencies/plugin teams
|
||||
|
||||
### Phase 3 — Professional (Weeks 17–28)
|
||||
- Block/FSE workshop
|
||||
- Role-based preview
|
||||
- Host deploy connectors
|
||||
- Team rules / shared playbooks
|
||||
- Paid beta
|
||||
|
||||
### Phase 4 — Platform
|
||||
- WooCommerce skills, multisite, maintenance agents, marketplace
|
||||
|
||||
---
|
||||
|
||||
## 13. Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| :--- | :--- | :--- |
|
||||
| Building a full IDE is enormous | High | Start from VS Code OSS or equivalent shell; buy don't rebuild editor chrome |
|
||||
| wp-env/Docker reliability on Win/macOS | High | First-class diagnostics; support DDEV import early |
|
||||
| Agent damages sites | High | Permission tiers; local-default; production lock; State Diffs |
|
||||
| "Just use Cursor + prompts" objection | Medium | Runtime+verify loop demos; playbooks that general agents fail |
|
||||
| WordPress PHP legacy complexity | Medium | Stubs, WPCS, curated playbooks; don't claim magic on 10-year themes day one |
|
||||
| Naming/repo still "originmain" | Low | Rename product + repo after PRD buy-in |
|
||||
| WordPress trademark in marketing | Medium | Follow WordPress Foundation trademark policy |
|
||||
|
||||
---
|
||||
|
||||
## 14. Open Questions (resolve in Phase 0)
|
||||
|
||||
1. **Shell strategy:** VS Code OSS fork vs cloud-only web IDE vs thin client over remote VMs?
|
||||
2. **Distribution:** Desktop-first (agency reality) vs browser-first?
|
||||
3. **Final name** (Wordbench is working title) and whether to rename the GitHub repo.
|
||||
4. **Pricing:** per seat vs per agency workspace vs usage on hosted runtimes.
|
||||
5. **How much of Bedrock/Trellis/Roots stack to support on day one?**
|
||||
6. **Model routing:** bring-your-own key vs hosted; default model mix.
|
||||
|
||||
---
|
||||
|
||||
## 15. Appendices
|
||||
|
||||
### A. Glossary
|
||||
- **State Diff** — Reviewable proposal of WP-CLI/SQL/content mutations
|
||||
- **Playbook** — Reusable agent workflow with tools + checks
|
||||
- **WP Knowledge Graph** — Indexed map of theme/plugin/block/hook/REST reality
|
||||
- **FSE** — Full Site Editing (block themes)
|
||||
- **wp-env** — Official local environment tooling from WordPress / `@wordpress/env`
|
||||
|
||||
### B. P0 Playbook sketches
|
||||
1. **Scaffold dynamic block** — detect build system → create-block or equivalent → register → build → verify insert in editor → diff
|
||||
2. **Child theme** — scaffold → enqueue parent → template override → screenshot home
|
||||
3. **CPT** — register → flush rewrites → seed via WP-CLI → REST check → diff
|
||||
|
||||
### C. Explicit non-goals (v1)
|
||||
- Replacing wp-admin for content authors
|
||||
- Automatic production hotfixes without human approval
|
||||
- Full visual page builder competing with Elementor (enhance code-era WP first)
|
||||
- Supporting every classic page builder's proprietary shortcodes equally on day one
|
||||
|
||||
### D. Positioning one-liner
|
||||
**Wordbench is Cursor for WordPress — an agentic environment where your code, WP-CLI, database, and live site share one brain.**
|
||||
|
||||
---
|
||||
|
||||
*End of PRD v1.0 — Wordbench*
|
||||
@@ -0,0 +1,13 @@
|
||||
# Wordbench
|
||||
|
||||
**Working title.** Agentic WordPress development environment — Cursor-depth tooling, WordPress-native runtime.
|
||||
|
||||
> This repository previously contained **Originmain** (AI design-engineering canvas). That codebase was removed in the product pivot of August 2026.
|
||||
|
||||
## Start here
|
||||
|
||||
- Full product spec: [PRD.md](./PRD.md)
|
||||
|
||||
## Status
|
||||
|
||||
Spec / Phase 0. Implementation has not started.
|
||||
@@ -1,512 +0,0 @@
|
||||
# Rendering Architecture
|
||||
|
||||
> Definitive design document for Originmain's two live-rendering modes.
|
||||
> Created 2026-04-29. Supersedes the original Layer 2 approach (direct
|
||||
> cross-origin iframe script injection, which is non-functional — see
|
||||
> "Why the Previous Approach Failed" below).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#1-overview)
|
||||
2. [Why the Previous Approach Failed](#2-why-the-previous-approach-failed)
|
||||
3. [Architecture Overview](#3-architecture-overview)
|
||||
4. [Mode A — CLI Proxy (Live Development)](#4-mode-a--cli-proxy-live-development)
|
||||
5. [Mode B — Live SDK (Preview Deployments / GitHub)](#5-mode-b--live-sdk-preview-deployments--github)
|
||||
6. [PostMessage Protocol (Unchanged)](#6-postmessage-protocol-unchanged)
|
||||
7. [Artboard ID Routing via `window.name`](#7-artboard-id-routing-via-windowname)
|
||||
8. [Sequence Diagrams](#8-sequence-diagrams)
|
||||
9. [Edge Cases](#9-edge-cases)
|
||||
10. [Package Reference](#10-package-reference)
|
||||
11. [Migration Notes](#11-migration-notes)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Originmain needs to render a user's **running React application** inside
|
||||
an artboard iframe on the canvas, while simultaneously extracting the
|
||||
React **fiber tree** (component names, props, DOM rects) so the
|
||||
Inspector, SelectionOverlay, and Diff Engine can operate on live data.
|
||||
|
||||
Two rendering modes serve different workflows:
|
||||
|
||||
| Mode | When to use | How the app loads | Who installs the fiber hook |
|
||||
|------|-------------|-------------------|----------------------------|
|
||||
| **A — CLI Proxy** | Active local development | User's dev server proxied through `@originmain/cli` | The proxy injects it into every HTML response |
|
||||
| **B — Live SDK** | Preview deployments, CI, async review | Vercel/Netlify preview URL or any hosted app | `@originmain/live` npm package (imported before React) |
|
||||
|
||||
Both modes use the **same postMessage protocol** (`packages/renderer/src/protocol.ts`)
|
||||
and the **same `LiveArtboard.tsx`** component. The only difference is
|
||||
who is responsible for installing the fiber hook before React loads.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why the Previous Approach Failed
|
||||
|
||||
The original Layer 2 implementation attempted direct cross-origin script
|
||||
injection via `iframe.contentDocument`. This fails for three independent
|
||||
reasons, any one of which is fatal:
|
||||
|
||||
### 2.1 Cross-Origin DOM Access Is Blocked
|
||||
|
||||
`LiveArtboard.tsx` called `iframe.contentDocument.createElement('script')`.
|
||||
When the iframe's origin (`http://localhost:3000`) differs from the
|
||||
host (`http://localhost:3001` or `https://app.originmain.com`), the
|
||||
browser returns `null` for `contentDocument`. The try/catch in
|
||||
`injectFiberHook()` silently swallowed this error.
|
||||
|
||||
### 2.2 Circular READY Handshake
|
||||
|
||||
The `READY` message was generated by the injected fiber hook script
|
||||
itself (line 106 in `fiber-hook.ts`). But the host waited for `READY`
|
||||
before injecting the script. Neither side could proceed first.
|
||||
|
||||
### 2.3 Hook Must Precede React Module Evaluation
|
||||
|
||||
`__REACT_DEVTOOLS_GLOBAL_HOOK__` must exist before React's module body
|
||||
runs. React checks for this object exactly once, at import time. Any
|
||||
script injected after React has loaded is too late — React will never
|
||||
register with a hook installed after the fact.
|
||||
|
||||
### 2.4 X-Frame-Options / CSP Blocking
|
||||
|
||||
Many apps (including GitHub pages) send `X-Frame-Options: DENY` or
|
||||
`Content-Security-Policy: frame-ancestors 'none'`. The iframe renders
|
||||
blank. No browser-side workaround exists.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Originmain Canvas (host) │
|
||||
│ │
|
||||
│ ┌──────────────┐ postMessage ┌──────────────────────┐ │
|
||||
│ │ LiveArtboard │ ◄──────────────── │ iframe │ │
|
||||
│ │ (listens) │ READY │ name="om:{id}" │ │
|
||||
│ │ │ FIBER_TREE_UPDATE │ │ │
|
||||
│ │ │ COMPONENT_SELECTED │ ┌────────────────┐ │ │
|
||||
│ └──────────────┘ │ │ Fiber Hook │ │ │
|
||||
│ │ │ │ (reads │ │ │
|
||||
│ ▼ │ │ window.name) │ │ │
|
||||
│ ┌──────────┐ │ └────────────────┘ │ │
|
||||
│ │Inspector │ │ ▲ │ │
|
||||
│ │Overlay │ │ │ installed │ │
|
||||
│ │DiffEngine│ │ ┌──┴─────────┐ │ │
|
||||
│ └──────────┘ │ │ Mode A: │ │ │
|
||||
│ │ │ CLI Proxy │ │ │
|
||||
│ │ │ Mode B: │ │ │
|
||||
│ │ │ Live SDK │ │ │
|
||||
│ │ └────────────┘ │ │
|
||||
│ └──────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Key invariant: **the fiber hook is always installed before React loads**.
|
||||
The two modes differ only in *who* ensures this.
|
||||
|
||||
---
|
||||
|
||||
## 4. Mode A — CLI Proxy (Live Development)
|
||||
|
||||
### 4.1 User Experience
|
||||
|
||||
```bash
|
||||
# Terminal 1 — user's dev server (as usual)
|
||||
npm run dev # → http://localhost:3000
|
||||
|
||||
# Terminal 2 — Originmain proxy
|
||||
npx @originmain/cli dev --target http://localhost:3000
|
||||
# → Proxy listening on http://localhost:4170
|
||||
# → Paste this URL into your Originmain artboard
|
||||
```
|
||||
|
||||
The user enters `http://localhost:4170` as the artboard's render URL
|
||||
in Originmain. The iframe loads through the proxy.
|
||||
|
||||
### 4.2 What the Proxy Does
|
||||
|
||||
For every HTTP request:
|
||||
|
||||
1. **Forward** the request to the target dev server (`localhost:3000`)
|
||||
2. **Strip** response headers that block iframing:
|
||||
- `X-Frame-Options` (any value)
|
||||
- `Content-Security-Policy` `frame-ancestors` directive
|
||||
3. **For HTML responses only** (Content-Type contains `text/html`):
|
||||
- Strip `Accept-Encoding` from the outgoing request so the target
|
||||
sends uncompressed HTML (avoids decompressing gzip/brotli)
|
||||
- Buffer the full response body
|
||||
- Inject the fiber hook `<script>` tag immediately after `<head>`
|
||||
(or after `<html>`, or at document start as fallback)
|
||||
- Update `Content-Length` to match the modified body
|
||||
4. **For non-HTML responses**: stream through unchanged (only headers stripped)
|
||||
|
||||
### 4.3 WebSocket Passthrough (HMR)
|
||||
|
||||
Modern dev servers (Next.js, Vite, Webpack) use WebSocket for Hot
|
||||
Module Replacement. The proxy listens for HTTP `Upgrade` requests and
|
||||
opens a raw TCP tunnel to the target server, passing bytes
|
||||
bidirectionally. HMR continues working normally.
|
||||
|
||||
### 4.4 CORS Headers
|
||||
|
||||
The proxy adds permissive CORS headers to all responses:
|
||||
```
|
||||
Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Methods: *
|
||||
Access-Control-Allow-Headers: *
|
||||
```
|
||||
This prevents issues when the user's app makes absolute-URL API calls
|
||||
that resolve to `localhost:3000` from an iframe at `localhost:4170`.
|
||||
|
||||
### 4.5 The Injected Script
|
||||
|
||||
The proxy injects a self-contained `<script>` block that:
|
||||
|
||||
1. Checks `window.parent !== window` (only runs when iframed)
|
||||
2. Reads `window.name` for the artboard ID (set by `LiveArtboard`)
|
||||
3. Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` if not present
|
||||
4. Wraps `onCommitFiberRoot` to serialize the fiber tree on every commit
|
||||
5. Sends `READY` via `postMessage` to the parent
|
||||
6. On each React commit, sends `FIBER_TREE_UPDATE` via `postMessage`
|
||||
|
||||
The script is ~90 lines, has zero dependencies, and is generated once
|
||||
at proxy startup by `buildProxyFiberHookScript()` in
|
||||
`packages/renderer/src/fiber-hook.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Mode B — Live SDK (Preview Deployments / GitHub)
|
||||
|
||||
### 5.1 User Experience
|
||||
|
||||
```bash
|
||||
npm install @originmain/live
|
||||
```
|
||||
|
||||
```ts
|
||||
// MUST be the very first import in your app entry point
|
||||
// (before React, before anything else)
|
||||
import '@originmain/live';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
// ... rest of app
|
||||
```
|
||||
|
||||
The SDK is a zero-dependency side-effect import. At module evaluation
|
||||
time (before React's module body runs), it installs the fiber hook.
|
||||
|
||||
### 5.2 When It Activates
|
||||
|
||||
The SDK only activates when **all** of these are true:
|
||||
|
||||
- `window.parent !== window` (the app is inside an iframe)
|
||||
- `window.name` starts with `om:` (the iframe was created by Originmain)
|
||||
|
||||
When running standalone (not iframed, or iframed by something other
|
||||
than Originmain), the SDK is a complete no-op: no global hook
|
||||
installed, no messages sent, zero runtime cost.
|
||||
|
||||
### 5.3 GitHub Integration Flow
|
||||
|
||||
```
|
||||
1. User connects GitHub repo to Originmain workspace
|
||||
2. User installs @originmain/live in their app
|
||||
3. User pushes code → Vercel/Netlify deploys preview
|
||||
4. GitHub sends deployment_status webhook → Originmain
|
||||
5. Originmain creates artboard with preview deployment URL
|
||||
6. Canvas iframes the preview URL
|
||||
7. @originmain/live (bundled in preview) detects Originmain iframe
|
||||
8. Fiber data flows via postMessage → full inspection works
|
||||
```
|
||||
|
||||
### 5.4 Why Preview URLs Work Without a Proxy
|
||||
|
||||
Vercel and Netlify preview deployments do **not** set
|
||||
`X-Frame-Options` or `frame-ancestors` by default. They are designed
|
||||
to be embeddable (Vercel's own preview comments embed them). This
|
||||
means the iframe renders correctly without header stripping.
|
||||
|
||||
If a user's deployment provider does block iframing, they should use
|
||||
Mode A (CLI proxy) instead, or configure their provider's headers.
|
||||
|
||||
### 5.5 Production Opt-Out
|
||||
|
||||
The SDK should not ship to production. Recommended patterns:
|
||||
|
||||
```ts
|
||||
// Option 1: conditional import (Vite / Webpack)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await import('@originmain/live');
|
||||
}
|
||||
|
||||
// Option 2: separate entry point
|
||||
// dev.tsx imports @originmain/live, then imports main.tsx
|
||||
// prod.tsx imports main.tsx directly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. PostMessage Protocol (Unchanged)
|
||||
|
||||
The protocol from `packages/renderer/src/protocol.ts` is unchanged.
|
||||
Both modes use the same envelope format:
|
||||
|
||||
### Host → Renderer (Originmain → iframe)
|
||||
|
||||
| Type | Payload | Purpose |
|
||||
|------|---------|---------|
|
||||
| `SET_DESIGN_TOKENS` | `{ tokens: Record<string, string> }` | Push DLF tokens as CSS variables |
|
||||
| `NAVIGATE` | `{ path: string }` | Navigate to a route within the app |
|
||||
| `SELECT_COMPONENT` | `{ nodeId: string }` | Highlight a component |
|
||||
| `DESELECT` | — | Clear selection |
|
||||
| `INJECT_FIBER_HOOK` | — | Legacy; no longer used |
|
||||
|
||||
### Renderer → Host (iframe → Originmain)
|
||||
|
||||
| Type | Payload | Purpose |
|
||||
|------|---------|---------|
|
||||
| `READY` | — | Fiber hook installed, app loaded |
|
||||
| `FIBER_TREE_UPDATE` | `{ root: FiberNode }` | Full serialized fiber tree |
|
||||
| `COMPONENT_SELECTED` | `{ nodeId, rect }` | User clicked a component in the iframe |
|
||||
| `COMPONENT_DESELECTED` | — | Selection cleared |
|
||||
| `ERROR` | `{ message: string }` | Hook or serialization error |
|
||||
|
||||
### Envelope Format
|
||||
|
||||
```ts
|
||||
// Renderer → Host
|
||||
{
|
||||
source: 'originmain-renderer', // discriminant
|
||||
artboardId: string, // from window.name
|
||||
message: RendererMessage
|
||||
}
|
||||
|
||||
// Host → Renderer
|
||||
{
|
||||
source: 'originmain-host', // discriminant
|
||||
artboardId: string,
|
||||
message: HostMessage
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Artboard ID Routing via `window.name`
|
||||
|
||||
Each `<iframe>` element gets a `name` attribute set by `LiveArtboard`:
|
||||
|
||||
```tsx
|
||||
<iframe name={`om:${artboardId}`} src={proxyOrPreviewUrl} ... />
|
||||
```
|
||||
|
||||
The fiber hook (whether injected by the proxy or by the SDK) reads
|
||||
`window.name` at initialization:
|
||||
|
||||
```js
|
||||
var artboardId = '';
|
||||
if (window.name && window.name.indexOf('om:') === 0) {
|
||||
artboardId = window.name.slice(3);
|
||||
}
|
||||
if (!artboardId) return; // not an Originmain iframe — do nothing
|
||||
```
|
||||
|
||||
This design has three advantages over the previous baked-in ID approach:
|
||||
|
||||
1. **Multiple artboards can share one proxy** — each iframe has a
|
||||
unique name, but all load through the same `localhost:4170` proxy.
|
||||
2. **SPA navigation preserves the ID** — `window.name` persists
|
||||
across navigations within the same browsing context.
|
||||
3. **The proxy is artboard-agnostic** — it injects one generic script
|
||||
for all requests. No per-artboard configuration needed.
|
||||
|
||||
---
|
||||
|
||||
## 8. Sequence Diagrams
|
||||
|
||||
### Mode A — CLI Proxy
|
||||
|
||||
```
|
||||
User's Dev Server CLI Proxy (:4170) Originmain Canvas
|
||||
│ │ │
|
||||
│ │ iframe src=:4170/ │
|
||||
│ │ ◄──────────────────────── │
|
||||
│ GET / │ │
|
||||
│ ◄──────────────────── │ │
|
||||
│ │ │
|
||||
│ 200 OK (HTML) │ │
|
||||
│ ──────────────────── ▶ │ │
|
||||
│ │ │
|
||||
│ strip X-Frame-Options │
|
||||
│ inject <script> after <head> │
|
||||
│ update Content-Length │
|
||||
│ │ │
|
||||
│ │ 200 OK (modified HTML) │
|
||||
│ │ ─────────────────────────▶ │
|
||||
│ │ │
|
||||
│ │ postMessage: READY │
|
||||
│ │ ─────────────────────────▶ │
|
||||
│ │ │
|
||||
│ │ (React loads, commits) │
|
||||
│ │ │
|
||||
│ │ postMessage: │
|
||||
│ │ FIBER_TREE_UPDATE │
|
||||
│ │ ─────────────────────────▶ │
|
||||
│ │ │
|
||||
│ │ Inspector / Overlay / │
|
||||
│ │ DiffEngine now have │
|
||||
│ │ live fiber data │
|
||||
```
|
||||
|
||||
### Mode B — Live SDK
|
||||
|
||||
```
|
||||
User's App (preview URL) Originmain Canvas
|
||||
│ │
|
||||
│ iframe src=preview-url.vercel.app │
|
||||
│ ◄─────────────────────────────────────── │
|
||||
│ │
|
||||
│ @originmain/live evaluates (side effect) │
|
||||
│ → installs __REACT_DEVTOOLS_GLOBAL_HOOK__ │
|
||||
│ → reads window.name → artboard ID │
|
||||
│ → sends postMessage: READY │
|
||||
│ ──────────────────────────────────────────▶ │
|
||||
│ │
|
||||
│ React evaluates → registers with hook │
|
||||
│ React renders → onCommitFiberRoot fires │
|
||||
│ │
|
||||
│ postMessage: FIBER_TREE_UPDATE │
|
||||
│ ──────────────────────────────────────────▶ │
|
||||
│ │
|
||||
│ (identical from here on) │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Edge Cases
|
||||
|
||||
### 9.1 HMR / Hot Module Replacement
|
||||
|
||||
The CLI proxy passes WebSocket `Upgrade` requests through to the
|
||||
target server via raw TCP socket tunneling. The user's HMR continues
|
||||
working. When a hot update changes the component tree, React commits
|
||||
again, the fiber hook fires, and the canvas receives an updated tree.
|
||||
|
||||
### 9.2 Server-Side Rendering (SSR)
|
||||
|
||||
The proxy receives the server-rendered HTML. The fiber hook script is
|
||||
injected before any application scripts. When React hydrates the SSR
|
||||
output, it finds the hook already installed and registers. Hydration
|
||||
commits trigger `onCommitFiberRoot` just like client-side renders.
|
||||
|
||||
### 9.3 React DevTools Coexistence
|
||||
|
||||
If the user has the React DevTools browser extension installed, it
|
||||
will have already set `__REACT_DEVTOOLS_GLOBAL_HOOK__`. The fiber
|
||||
hook checks for an existing hook and wraps the existing
|
||||
`onCommitFiberRoot` rather than replacing it. Both Originmain and
|
||||
React DevTools receive fiber commits independently.
|
||||
|
||||
### 9.4 Non-React Applications
|
||||
|
||||
If the proxied app does not use React, the hook installs but
|
||||
`onCommitFiberRoot` never fires. The `READY` message is still sent.
|
||||
The artboard displays the app visually, but the Inspector shows no
|
||||
component tree. This is by design — Originmain supports visual
|
||||
review of any web app, with React-specific features only activating
|
||||
when React is detected.
|
||||
|
||||
### 9.5 Multiple Artboards, One Proxy
|
||||
|
||||
Several artboards can point to the same proxy URL with different
|
||||
routes (e.g., `localhost:4170/dashboard` and `localhost:4170/settings`).
|
||||
Each iframe has a unique `window.name`, so fiber updates are correctly
|
||||
routed to their respective artboards.
|
||||
|
||||
### 9.6 SPA Navigation Inside the Iframe
|
||||
|
||||
SPA routers change the URL without a full page reload. React does not
|
||||
unmount — it re-renders. The fiber hook, already installed, continues
|
||||
receiving commits. `window.name` persists across SPA navigations.
|
||||
|
||||
### 9.7 Full Page Navigation Inside the Iframe
|
||||
|
||||
If the user's app performs a full page navigation (e.g., `<a href>`
|
||||
without client-side routing), the browser requests the new page from
|
||||
the proxy. The proxy injects the fiber hook into the new HTML response.
|
||||
`window.name` is preserved by the browser across same-frame
|
||||
navigations. The new page's React instance finds the hook and
|
||||
registers. A new `READY` message fires.
|
||||
|
||||
### 9.8 Compressed Responses
|
||||
|
||||
The proxy strips `Accept-Encoding` from outgoing requests to the
|
||||
target, causing the target to respond with uncompressed HTML. This
|
||||
avoids the complexity of decompressing gzip/brotli/deflate before
|
||||
injection. The performance impact is negligible over localhost.
|
||||
|
||||
### 9.9 Chunked Transfer-Encoding
|
||||
|
||||
The proxy buffers the full HTML response before injecting the script.
|
||||
For streaming SSR responses, this adds latency equal to the full
|
||||
document size. This is acceptable for a development tool.
|
||||
|
||||
---
|
||||
|
||||
## 10. Package Reference
|
||||
|
||||
### `@originmain/cli` (`packages/cli`)
|
||||
|
||||
| Export / Command | Description |
|
||||
|------------------|-------------|
|
||||
| `originmain dev --target <url> [--port <n>]` | Start the reverse proxy |
|
||||
| `--target` | Required. The user's dev server URL |
|
||||
| `--port` | Proxy listen port (default: 4170) |
|
||||
|
||||
Dependencies: `@originmain/renderer` (for `buildProxyFiberHookScript`).
|
||||
Node.js built-ins only for proxy (`node:http`, `node:net`, `node:url`).
|
||||
|
||||
### `@originmain/live` (`packages/live-sdk`)
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `import '@originmain/live'` | Side-effect import — installs fiber hook |
|
||||
|
||||
Zero dependencies. ~2 KB minified. No-op when not in an Originmain iframe.
|
||||
|
||||
### `@originmain/renderer` (`packages/renderer`) — updated
|
||||
|
||||
| Export | Description |
|
||||
|--------|-------------|
|
||||
| `buildProxyFiberHookScript()` | Returns the generic fiber hook script (reads `window.name`) |
|
||||
| `buildFiberHookScript(id)` | **Deprecated.** Legacy baked-in ID version |
|
||||
| (all other exports) | Unchanged |
|
||||
|
||||
---
|
||||
|
||||
## 11. Migration Notes
|
||||
|
||||
### LiveArtboard.tsx Changes
|
||||
|
||||
1. **Add** `name={`om:${id}`}` to the `<iframe>` element
|
||||
2. **Remove** the `injectFiberHook()` call from the `READY` handler
|
||||
3. **Keep** the `postMessage` listener — the protocol is identical
|
||||
|
||||
### Artboard.tsx Changes
|
||||
|
||||
1. **Update** `EmptyArtboardContent` to show proxy instructions
|
||||
2. **No changes** to fiber handling, SelectionOverlay, or diff logic
|
||||
|
||||
### GitHub Connector Fix
|
||||
|
||||
`packages/integrations/src/connectors/github.ts` must set `renderUrl`
|
||||
to the **deployment preview URL** (from a `deployment_status` webhook),
|
||||
not `pr.html_url` (which is the GitHub PR page and blocks iframing).
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-04-29*
|
||||
@@ -1,488 +0,0 @@
|
||||
# Originmain SDK Architecture
|
||||
### From Proxy to SDK — System Design & Implementation Progress
|
||||
|
||||
> **Living document.** Updated every time a piece of this architecture is implemented.
|
||||
> Supersedes `RENDERING-ARCHITECTURE.md` (proxy-era, now deprecated).
|
||||
>
|
||||
> Last updated: 2026-05-13
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Why We Moved Away From the Proxy](#1-why-we-moved-away-from-the-proxy)
|
||||
2. [Target Architecture](#2-target-architecture)
|
||||
3. [System Components](#3-system-components)
|
||||
4. [Wire Protocol](#4-wire-protocol)
|
||||
5. [Implementation Progress](#5-implementation-progress)
|
||||
6. [Package Map](#6-package-map)
|
||||
7. [Data Flow — Step by Step](#7-data-flow--step-by-step)
|
||||
8. [Local Dev Problem & Tunnel Strategy](#8-local-dev-problem--tunnel-strategy)
|
||||
9. [Edge Cases](#9-edge-cases)
|
||||
10. [What Remains To Be Built](#10-what-remains-to-be-built)
|
||||
|
||||
---
|
||||
|
||||
## 1. Why We Moved Away From the Proxy
|
||||
|
||||
The original approach (`@originmain/cli`) ran a local HTTP reverse proxy that injected a
|
||||
`<script>` tag into every HTML response. This failed in practice for four compounding reasons:
|
||||
|
||||
| Failure | Root Cause | Fatal? |
|
||||
|---|---|---|
|
||||
| Script never executed | React 19 `hydrateRoot(document, ...)` reconciles the entire `<head>` and removes any `<script>` tag not present in React's VDOM | ✅ Yes |
|
||||
| `window.name` always empty | Chrome 88+ strips `window.name` on cross-origin iframe loads (Spectre mitigation) — the SDK used this as its artboard ID source | ✅ Yes |
|
||||
| Inline script blocked | CSPs without `'unsafe-inline'` blocked the injected `<script>…</script>` content | ✅ Yes |
|
||||
| Hook installed too late | `__REACT_DEVTOOLS_GLOBAL_HOOK__` must exist **before** React's module body evaluates. Injected scripts run after HTML parses — after React is already loaded | ✅ Yes |
|
||||
|
||||
**The only reliable fix**: the fiber hook must be installed by the app itself, not by an external injector. That means it ships as an npm package the developer imports **before React** in their app entry point.
|
||||
|
||||
---
|
||||
|
||||
## 2. Target Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ORIGINMAIN CLOUD (originmain.com) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────┐ ┌──────────────────────────────────┐ │
|
||||
│ │ Canvas (Next.js app) │◄───►│ WebSocket Bridge │ │
|
||||
│ │ • Artboards / iframes │ │ /api/sdk/[projectId] │ │
|
||||
│ │ • Inspector / Editor │ │ • auth via SDK token │ │
|
||||
│ │ • File diff viewer │ │ • routes messages to user │ │
|
||||
│ └─────────────────────────────┘ └──────────────────────────────────┘ │
|
||||
│ ▲ │
|
||||
└────────────────────────────────────────────────────────│───────────────────┘
|
||||
│ WSS
|
||||
│ (wss://originmain.com/api/sdk/[projectId])
|
||||
┌────────────────────────────────────────────────────────│───────────────────┐
|
||||
│ USER MACHINE (localhost / staging) │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────┐ ┌────────────────────────────────────┐ │
|
||||
│ │ Next.js dev server │◄───►│ @originmain/dev (SDK) │ │
|
||||
│ │ (next dev / Vercel) │ │ • client runtime in browser │ │
|
||||
│ │ │ │ • server runtime in Node │ │
|
||||
│ │ │ │ • direct React fiber access │ │
|
||||
│ │ Components.tsx ◄────────┼─────┤ • file-write capability │ │
|
||||
│ │ │ │ • opens WSS to cloud canvas │ │
|
||||
│ └──────────────────────────┘ └────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ next.config.js — wrapped with @originmain/next plugin │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Key invariant**: the fiber hook lives *inside* the user's app bundle.
|
||||
No proxy, no injection, no external script — the user's `import '@originmain/live'`
|
||||
is what instruments React.
|
||||
|
||||
---
|
||||
|
||||
## 3. System Components
|
||||
|
||||
### 3.1 `@originmain/live` (client runtime) — `packages/live-sdk`
|
||||
|
||||
The browser-side SDK. A side-effect import that:
|
||||
|
||||
- Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` **before React evaluates** (module-load time)
|
||||
- Resolves the artboard ID from three sources in priority order:
|
||||
1. URL fragment: `location.hash` contains `#__om_artboard=<id>` ← primary, cross-origin safe
|
||||
2. `window.name`: starts with `om:` (same-origin iframes only)
|
||||
3. postMessage handshake: sends `{ __om_init_request: true }` to parent, awaits reply
|
||||
- Runs the full postMessage protocol (see §4) once the artboard ID is known
|
||||
- Is a **complete no-op** outside an Originmain artboard iframe (zero runtime cost in production)
|
||||
|
||||
### 3.2 `@originmain/next` (build plugin) — `packages/next`
|
||||
|
||||
A `withOriginmain(nextConfig)` wrapper for `next.config.ts` that:
|
||||
|
||||
- Prepends `@originmain/live` to every client-side webpack entry point
|
||||
- Ensures the fiber hook import runs **before any other module** in the bundle
|
||||
- Skips the server-side bundle (fiber hook is browser-only)
|
||||
- Is idempotent (safe to wrap twice)
|
||||
- Delegates to any existing `webpack` customisation in the user's config
|
||||
|
||||
### 3.3 `@originmain/dev` (full SDK) — `packages/dev` ❌ NOT BUILT YET
|
||||
|
||||
The full SDK intended for local development. Will combine:
|
||||
|
||||
- Everything in `@originmain/live` (client runtime, fiber hook)
|
||||
- A **server runtime** that runs inside the Next.js dev server process
|
||||
- A **WebSocket client** that connects outbound to the cloud canvas bridge
|
||||
- **File-write capability**: receives design panel edits from the canvas and applies them to source files (`.tsx`, `.ts`, CSS modules)
|
||||
|
||||
### 3.4 WebSocket Bridge — `packages/app/src/app/api/sdk/[projectId]/` ❌ NOT BUILT YET
|
||||
|
||||
A Next.js API route on the cloud canvas that:
|
||||
|
||||
- Accepts an inbound WSS connection from `@originmain/dev`
|
||||
- Authenticates via a project-scoped SDK token (issued in project settings)
|
||||
- Routes messages bidirectionally:
|
||||
- SDK → Canvas: `FIBER_TREE_UPDATE`, `ELEMENT_STYLES`, `ROUTES_DISCOVERED`, etc.
|
||||
- Canvas → SDK: `PATCH_ELEMENT_STYLE`, `REQUEST_ELEMENT_STYLES`, `CAPTURE_SNAPSHOT`, etc.
|
||||
- Maintains one WebSocket connection per active project session
|
||||
|
||||
### 3.5 Canvas (Next.js app) — `packages/app`
|
||||
|
||||
Originmain's cloud editor. Current transport: **postMessage via iframe**.
|
||||
Future transport: **WebSocket via bridge** (for `@originmain/dev` local dev).
|
||||
|
||||
Key components:
|
||||
- `LiveArtboard.tsx` — manages the `<iframe>`, postMessage listener, message dispatch
|
||||
- `DesignTab.tsx` — design property sections (Frame, Layout, Fill, Typography, etc.)
|
||||
- `Inspector.tsx` — tab container (Design / Props / Code / Diff / Graph)
|
||||
- `Artboard.tsx` — artboard frame, selection overlay, static-page detection
|
||||
- `Canvas.tsx` — infinite canvas, zoom/pan, artboard layout, onboarding overlay
|
||||
|
||||
---
|
||||
|
||||
## 4. Wire Protocol
|
||||
|
||||
Defined in `packages/renderer/src/protocol.ts`. Both the postMessage transport
|
||||
(current) and the WebSocket transport (planned) use the same message shapes.
|
||||
|
||||
### 4.1 Canvas → App (Host → Renderer)
|
||||
|
||||
| Message | Payload | Purpose |
|
||||
|---|---|---|
|
||||
| `SET_DESIGN_TOKENS` | `{ tokens: Record<string, string> }` | Push CSS custom property values |
|
||||
| `NAVIGATE` | `{ path: string }` | SPA navigation inside the iframe |
|
||||
| `SELECT_COMPONENT` | `{ nodeId: string }` | Show highlight ring on component |
|
||||
| `DESELECT` | — | Remove highlight ring |
|
||||
| `REQUEST_ELEMENT_STYLES` | `{ nodeId: string }` | Ask for computed CSS properties |
|
||||
| `PATCH_ELEMENT_STYLE` | `{ nodeId, property, value }` | Apply one inline CSS override |
|
||||
| `PATCH_CHILDREN_STYLE` | `{ parentNodeId, selector, property, value }` | Patch CSS on all matching children |
|
||||
| `REMOVE_ELEMENT` | `{ nodeId: string }` | Set `display:none` on element |
|
||||
| `CAPTURE_THUMBNAIL` | — | Capture full-page JPEG via html2canvas |
|
||||
| `CAPTURE_SNAPSHOT` | `{ nodeId: string }` | Capture selected element PNG |
|
||||
| `CANCEL_SNAPSHOT` | — | Abort in-flight snapshot |
|
||||
|
||||
### 4.2 App → Canvas (Renderer → Host)
|
||||
|
||||
| Message | Payload | Purpose |
|
||||
|---|---|---|
|
||||
| `READY` | `{ rootFontSizePx?: number }` | Hook active, React detected |
|
||||
| `FIBER_TREE_UPDATE` | `{ root: FiberNode }` | Full serialized component tree |
|
||||
| `COMPONENT_SELECTED` | `{ nodeId, rect }` | User clicked a component |
|
||||
| `COMPONENT_DESELECTED` | — | User clicked empty space |
|
||||
| `ELEMENT_STYLES` | `{ nodeId, styles, hasDirectText, hasParagraphChildren }` | Computed CSS response |
|
||||
| `ROUTES_DISCOVERED` | `{ routes: Array<{ path, label }> }` | App's navigation routes |
|
||||
| `THUMBNAIL_READY` | `{ dataUrl: string \| null }` | JPEG data URL or null |
|
||||
| `SNAPSHOT_READY` | `{ dataUrl: string \| null, nodeId: string }` | PNG data URL or null |
|
||||
| `ERROR` | `{ message: string }` | Hook or serialization error |
|
||||
|
||||
### 4.3 Envelope Format (postMessage)
|
||||
|
||||
```ts
|
||||
// Canvas → App
|
||||
{ source: 'originmain-host', artboardId: string, message: HostMessage }
|
||||
|
||||
// App → Canvas
|
||||
{ source: 'originmain-renderer', artboardId: string, message: RendererMessage }
|
||||
```
|
||||
|
||||
### 4.4 Artboard ID Resolution (how the SDK finds its artboard)
|
||||
|
||||
The canvas appends the artboard ID to the iframe's `src` as a URL fragment:
|
||||
|
||||
```tsx
|
||||
// LiveArtboard.tsx
|
||||
src={url + '#__om_artboard=' + encodeURIComponent(id)}
|
||||
```
|
||||
|
||||
The SDK reads it in priority order:
|
||||
```
|
||||
1. location.hash → /__om_artboard=abc123/ (primary, cross-origin safe)
|
||||
2. window.name → "om:abc123" (same-origin iframes)
|
||||
3. postMessage handshake (async fallback, 10s timeout)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Implementation Progress
|
||||
|
||||
### ✅ Done
|
||||
|
||||
| Component | File(s) | Notes |
|
||||
|---|---|---|
|
||||
| `@originmain/live` client runtime | `packages/live-sdk/src/hook.ts` | Full rewrite. URL fragment guard. Full protocol parity. html2canvas support. |
|
||||
| `@originmain/live` build step | `packages/live-sdk/build.mjs` | esbuild → `dist/index.js` (9.4kb browser ESM bundle, minified). |
|
||||
| `@originmain/next` build plugin | `packages/next/src/index.ts` | `withOriginmain()` — webpack entry prepend. Idempotent. |
|
||||
| `@originmain/next` build step | `packages/next/build.mjs` | esbuild → ESM + CJS; tsc → `dist/index.d.ts` type declarations. |
|
||||
| Root `sdk:build` script | `package.json` | `pnpm sdk:build` builds both SDK packages in order. |
|
||||
| SDK token auth | `packages/app/src/lib/sdk-auth.ts` | `issueSdkToken()` / `verifySdkToken()` — HMAC-SHA256, 90-day TTL, project-scoped. |
|
||||
| SDK bridge registry | `packages/app/src/lib/sdk-bridge-registry.ts` | In-process SSE sink Map — pushToCanvas / pushToSdk. |
|
||||
| SDK bridge fiber events route | `packages/app/src/app/api/sdk/[projectId]/route.ts` | GET (canvas SSE) + POST (SDK pushes fiber data). |
|
||||
| SDK bridge commands route | `packages/app/src/app/api/sdk/[projectId]/commands/route.ts` | GET (SDK SSE) + POST (canvas pushes commands). |
|
||||
| SDK token issuance route | `packages/app/src/app/api/sdk/token/route.ts` | POST: canvas issues a project-scoped token for `@originmain/dev`. |
|
||||
| LiveArtboard postMessage handshake | `packages/app/src/components/canvas/LiveArtboard.tsx` | Responds to `__om_init_request` from SDK |
|
||||
| LiveArtboard URL fragment injection | `packages/app/src/components/canvas/LiveArtboard.tsx` | Appends `#__om_artboard=<id>` to all iframe src URLs |
|
||||
| Style refresh after design panel edit | `packages/app/src/components/canvas/LiveArtboard.tsx` | 120ms debounced `REQUEST_ELEMENT_STYLES` after queue drains |
|
||||
| Design panel → history tracking | `packages/app/src/components/inspector/DesignTab.tsx` | `pushEdit()` on every `patch()` call — feeds Diff tab |
|
||||
| Design panel → optimistic styles | `packages/app/src/components/inspector/DesignTab.tsx` | `setComponentStyles()` optimistic update — inputs reflect change immediately |
|
||||
| Canvas onboarding UI | `packages/app/src/components/canvas/Canvas.tsx` | Updated from proxy instructions to SDK install instructions |
|
||||
| Static page banner | `packages/app/src/components/canvas/Artboard.tsx` | Now says "add `import '@originmain/live'`" instead of confusing message |
|
||||
| `tsconfig.base.json` path alias | `tsconfig.base.json` | `@originmain/next` added to paths |
|
||||
|
||||
### 🚧 In Progress / Partially Done
|
||||
|
||||
| Component | File(s) | Status | Blocker |
|
||||
|---|---|---|---|
|
||||
| `@originmain/live` publishing | `packages/live-sdk/package.json` | Build complete. `"private": false`. **Not yet `npm publish`-ed** | Run `pnpm sdk:build && cd packages/live-sdk && npm publish` |
|
||||
| `@originmain/next` publishing | `packages/next/package.json` | Build complete. `"private": false`. **Not yet `npm publish`-ed** | Same (depends on live being published first) |
|
||||
| CLI proxy deprecation | `packages/cli/` | Still exists and still works | Can delete once SDK is published and users migrate |
|
||||
| SDK bridge persistent pub/sub | `packages/app/src/lib/sdk-bridge-registry.ts` | In-process Maps (works for local dev / single server) | For Vercel serverless: replace with Supabase Realtime or Redis |
|
||||
|
||||
### ❌ Not Built
|
||||
|
||||
| Component | Target File(s) | Priority | Depends On |
|
||||
|---|---|---|---|
|
||||
| WebSocket bridge API route | `packages/app/src/app/api/sdk/[projectId]/route.ts` | HIGH | — |
|
||||
| `@originmain/dev` package | `packages/dev/` | HIGH | WebSocket bridge |
|
||||
| SDK server runtime (Node.js) | `packages/dev/src/server.ts` | HIGH | WebSocket bridge |
|
||||
| File-write capability | `packages/dev/src/file-writer.ts` | HIGH | Server runtime |
|
||||
| SDK token issuance in canvas | `packages/app/src/app/api/sdk/token/route.ts` | HIGH | WebSocket bridge |
|
||||
| Local dev tunnel (localhost → WSS) | TBD | MEDIUM | `@originmain/dev` |
|
||||
| `IsolationFrame.tsx` SDK migration | `packages/app/src/components/canvas/IsolationFrame.tsx` | LOW | `@originmain/dev` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Package Map
|
||||
|
||||
```
|
||||
packages/
|
||||
├── app/ @originmain/app — Cloud canvas (Next.js, Vercel)
|
||||
├── live-sdk/ @originmain/live — Client SDK: browser fiber hook ✅ DONE
|
||||
├── next/ @originmain/next — Next.js build plugin ✅ DONE
|
||||
├── dev/ @originmain/dev — Full SDK (client + server) ❌ NOT BUILT
|
||||
├── renderer/ @originmain/renderer — Protocol types, message shapes
|
||||
├── cli/ @originmain/cli — Legacy proxy (deprecated, not deleted yet)
|
||||
├── diff-engine/ @originmain/diff-engine
|
||||
├── design-language/ @originmain/design-language
|
||||
├── agent-bridge/ @originmain/agent-bridge
|
||||
├── ai-layer/ @originmain/ai-layer
|
||||
├── platform/ @originmain/platform
|
||||
├── multiplayer/ @originmain/multiplayer
|
||||
├── origin-graph/ @originmain/origin-graph
|
||||
├── ui/ @originmain/ui
|
||||
└── e2e/ (test suite)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Data Flow — Step by Step
|
||||
|
||||
### 7.1 Current Flow (postMessage via iframe)
|
||||
|
||||
```
|
||||
1. User adds @originmain/live to their Next.js app
|
||||
2. User wraps next.config.ts with withOriginmain()
|
||||
3. User deploys to Vercel — @originmain/live is in the bundle
|
||||
|
||||
4. Canvas: user pastes Vercel URL → artboard created in DB
|
||||
5. Canvas: LiveArtboard renders <iframe src="https://app.vercel.app/route#__om_artboard=abc123">
|
||||
6. Browser: iframe loads app from Vercel
|
||||
|
||||
7. App bundle: @originmain/live runs (module-level side effect, before React)
|
||||
→ installs __REACT_DEVTOOLS_GLOBAL_HOOK__
|
||||
→ reads location.hash → finds __om_artboard=abc123
|
||||
→ calls startMainLoop("abc123")
|
||||
|
||||
8. React evaluates → sees hook → registers onCommitFiberRoot
|
||||
|
||||
9. App renders → React commits
|
||||
→ onCommitFiberRoot fires
|
||||
→ SDK serializes fiber tree
|
||||
→ postMessage({ source: 'originmain-renderer', artboardId: 'abc123',
|
||||
message: { type: 'FIBER_TREE_UPDATE', root: ... } })
|
||||
|
||||
10. Canvas: LiveArtboard.handleMessage receives FIBER_TREE_UPDATE
|
||||
→ setFiberRoot(artboardId, root) [Zustand]
|
||||
→ SelectionOverlay renders component hit-test overlay
|
||||
→ Inspector shows component tree (Graph tab)
|
||||
|
||||
11. User clicks a component in the iframe
|
||||
→ SDK: click handler → getFiberKey(el) → walk fiber.return chain
|
||||
→ postMessage COMPONENT_SELECTED { nodeId, rect }
|
||||
|
||||
12. Canvas: receives COMPONENT_SELECTED
|
||||
→ selectComponent(nodeId) [Zustand]
|
||||
→ LiveArtboard sends REQUEST_ELEMENT_STYLES { nodeId }
|
||||
|
||||
13. SDK: respondWithStyles() → getComputedStyle(el) → all CSS properties
|
||||
→ postMessage ELEMENT_STYLES { nodeId, styles, hasDirectText, hasParagraphChildren }
|
||||
|
||||
14. Canvas: setComponentStyles(styles) [Zustand]
|
||||
→ DesignTab re-renders with actual computed values
|
||||
→ Frame/Layout/Fill/Typography sections show live data
|
||||
|
||||
15. User edits width in Frame section: 200px → 250px
|
||||
[DesignTab.patch('width', '250px')]
|
||||
→ patchStyleEdit(artboardId, nodeId, 'width', '250px') → styleEditQueue
|
||||
→ setComponentStyles({ ...styles, width: '250px' }) → optimistic panel update
|
||||
→ pushEdit({ key: 'width', before: '200px', after: '250px', ... }) → history
|
||||
|
||||
16. LiveArtboard: styleEditQueue effect drains
|
||||
→ postMessage PATCH_ELEMENT_STYLE { nodeId, property: 'width', value: '250px' }
|
||||
→ starts 120ms debounce timer
|
||||
|
||||
17. SDK: patchElementStyle(nodeId, 'width', '250px')
|
||||
→ el.style.setProperty('width', '250px')
|
||||
→ iframe visually updates ✅
|
||||
|
||||
18. 120ms later: LiveArtboard sends REQUEST_ELEMENT_STYLES
|
||||
→ SDK: getComputedStyle now returns '250px' (inline style overrides)
|
||||
→ ELEMENT_STYLES response → panel shows accurate computed value
|
||||
|
||||
19. Diff tab: pendingChanges = [{ key: 'width', before: '200px', after: '250px' }]
|
||||
→ "Export diff →" button active
|
||||
→ Code tab shows generated file patch
|
||||
→ "Send to Agent" sends diff to AI for code application
|
||||
```
|
||||
|
||||
### 7.2 Target Flow (WebSocket — not built yet)
|
||||
|
||||
Same as above, except steps 5–9 use WebSocket instead of postMessage:
|
||||
|
||||
```
|
||||
5. Canvas: LiveArtboard connects to wss://originmain.com/api/sdk/abc123
|
||||
6. @originmain/dev (server runtime): connects to same WSS URL with SDK token
|
||||
7. Bridge authenticates token, pairs the two connections
|
||||
8. All messages flow through the bridge instead of postMessage
|
||||
→ This unlocks local dev (no Vercel deploy required)
|
||||
→ This enables file-write (SDK has Node.js fs access)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Local Dev Problem & Tunnel Strategy
|
||||
|
||||
### The Problem
|
||||
|
||||
The cloud canvas (`https://originmain.com`) is served over HTTPS. Browsers
|
||||
enforce **mixed content policy**: an HTTPS page cannot iframe `http://localhost:3000`.
|
||||
This means the current SDK approach (iframe + postMessage) **only works for deployed apps**
|
||||
(Vercel, Netlify, etc.) — not for local development.
|
||||
|
||||
### Current Workaround (v1)
|
||||
|
||||
Users must deploy to Vercel (or any HTTPS host) to use the canvas.
|
||||
Vercel's preview deployments are free and instant (`vercel deploy --prod` is optional).
|
||||
|
||||
### Planned Solution (`@originmain/dev` + WebSocket bridge)
|
||||
|
||||
When the WebSocket bridge exists:
|
||||
- `@originmain/dev` runs a Node.js server alongside `next dev`
|
||||
- It connects **outbound** from the user's machine via WSS to the canvas bridge
|
||||
- Outbound WSS from localhost → cloud is always allowed (no mixed content issue)
|
||||
- The canvas receives fiber data via the bridge, not via the iframe
|
||||
- The iframe can be replaced with a screenshot stream or kept pointing to a tunnel URL
|
||||
|
||||
### Tunnel Options (if iframe is still needed for visual rendering)
|
||||
|
||||
| Option | Effort | Notes |
|
||||
|---|---|---|
|
||||
| Vercel deploy (v1 recommendation) | Lowest | Works today. No extra tooling. |
|
||||
| `cloudflared tunnel` (user-run) | Low | `npx cloudflared tunnel --url http://localhost:3000` |
|
||||
| Originmain-managed tunnel | High | Requires Originmain to run tunnel infrastructure |
|
||||
|
||||
---
|
||||
|
||||
## 9. Edge Cases
|
||||
|
||||
### 9.1 React DevTools Coexistence
|
||||
|
||||
React DevTools extension installs its own `__REACT_DEVTOOLS_GLOBAL_HOOK__` first.
|
||||
The SDK detects the existing hook and wraps `onCommitFiberRoot` — both DevTools and
|
||||
Originmain receive fiber commits. Neither overwrites the other.
|
||||
|
||||
### 9.2 HMR (Hot Module Replacement)
|
||||
|
||||
When webpack/turbopack pushes a hot update, React re-renders affected components.
|
||||
`onCommitFiberRoot` fires again → SDK sends a new `FIBER_TREE_UPDATE` → canvas updates.
|
||||
The artboard ID is stable across hot updates (URL fragment persists).
|
||||
|
||||
### 9.3 SPA Navigation
|
||||
|
||||
SPA routers change the URL without a full page reload. React re-renders.
|
||||
`onCommitFiberRoot` fires → new `FIBER_TREE_UPDATE`. Route discovery re-runs via
|
||||
a `popstate` listener. The SDK sends updated `ROUTES_DISCOVERED`.
|
||||
|
||||
### 9.4 React 19 Server Components
|
||||
|
||||
Server Components don't produce fiber nodes in the client tree — they're rendered
|
||||
to RSC payload and hydrated as static DOM. The SDK correctly skips these
|
||||
(they have no `type` function). Only client components appear in the tree.
|
||||
|
||||
### 9.5 Non-React Pages (Static HTML)
|
||||
|
||||
If the app serves a page with no React (e.g., a static landing page),
|
||||
`onCommitFiberRoot` never fires. The `READY` message is still sent.
|
||||
After 8 seconds with no `FIBER_TREE_UPDATE`, `LiveArtboard` calls
|
||||
`onStaticPageDetected()` → the "No React detected" banner appears.
|
||||
|
||||
### 9.6 Multiple Artboards, Same App
|
||||
|
||||
Multiple artboards can iframe the same app at different routes. Each gets a
|
||||
unique `#__om_artboard=<id>` in its URL. The SDK is a module singleton — it runs
|
||||
once per page load — and binds to the single artboard ID from the URL fragment.
|
||||
Each iframe is a separate browsing context with its own SDK instance.
|
||||
|
||||
### 9.7 CSP (`script-src 'self'`)
|
||||
|
||||
Since the SDK ships in the app's own bundle (not injected externally), CSP
|
||||
`script-src 'self'` does not block it. The SDK is part of the same origin as the app.
|
||||
|
||||
---
|
||||
|
||||
## 10. What Remains To Be Built
|
||||
|
||||
Priority order for next implementation sprint:
|
||||
|
||||
### Priority 1 — Publish packages (unblocks users) ✅ BUILD COMPLETE
|
||||
|
||||
- [x] Add esbuild build step for `@originmain/live` → `packages/live-sdk/build.mjs`
|
||||
- [x] Add esbuild + tsc build step for `@originmain/next` → `packages/next/build.mjs`
|
||||
- [x] Update exports in both `package.json` files to point to `dist/`
|
||||
- [x] Add `@originmain/live` as workspace dependency of `@originmain/next`
|
||||
- [x] Root `pnpm sdk:build` script for one-command build
|
||||
- [ ] `npm publish` `@originmain/live` — ready to publish, command: `cd packages/live-sdk && npm publish`
|
||||
- [ ] `npm publish` `@originmain/next` — depends on live being published first
|
||||
|
||||
### Priority 2 — WebSocket Bridge (unblocks local dev) ✅ DONE (SSE transport)
|
||||
|
||||
> Implemented as SSE (bidirectional via two channels) rather than raw WebSocket
|
||||
> to be compatible with Next.js App Router and Vercel's serverless runtime.
|
||||
> Functionally equivalent — real WebSocket can replace SSE in a future iteration
|
||||
> without changing the client API.
|
||||
|
||||
- [x] SDK token auth (`packages/app/src/lib/sdk-auth.ts`)
|
||||
- [x] In-process SSE registry (`packages/app/src/lib/sdk-bridge-registry.ts`)
|
||||
- [x] Fiber events channel: `GET` (canvas SSE) + `POST` (SDK → bridge) — `packages/app/src/app/api/sdk/[projectId]/route.ts`
|
||||
- [x] Commands channel: `GET` (SDK SSE) + `POST` (canvas → bridge) — `packages/app/src/app/api/sdk/[projectId]/commands/route.ts`
|
||||
- [x] Token issuance: `POST /api/sdk/token` — `packages/app/src/app/api/sdk/token/route.ts`
|
||||
|
||||
### Priority 3 — `@originmain/dev` package (file-write, local dev)
|
||||
|
||||
- [ ] `packages/dev/src/client.ts` — re-export `@originmain/live`
|
||||
- [ ] `packages/dev/src/server.ts` — Node.js WebSocket client, connects to bridge
|
||||
- [ ] `packages/dev/src/file-writer.ts` — applies design panel patches to source files
|
||||
- Parse `PATCH_ELEMENT_STYLE` messages → locate source file via `callSite`
|
||||
- Rewrite Tailwind classes / CSS modules / inline styles
|
||||
- [ ] `packages/next/src/index.ts` — extend `withOriginmain()` to also start the server runtime
|
||||
|
||||
### Priority 4 — SDK token issuance
|
||||
|
||||
- [ ] Project settings UI: "Generate SDK token" button
|
||||
- [ ] `packages/app/src/app/api/sdk/token/route.ts` — create/rotate tokens
|
||||
- [ ] Store tokens in Supabase (scoped to project, revocable)
|
||||
|
||||
### Priority 5 — IsolationFrame migration
|
||||
|
||||
- [ ] Migrate `IsolationFrame.tsx` from proxy URL to SDK-based approach
|
||||
- Currently still requires the CLI proxy to serve `/__om_isolation__/*`
|
||||
|
||||
---
|
||||
|
||||
*Document owner: engineering*
|
||||
*Last updated: 2026-05-13*
|
||||
*Next review: when WebSocket bridge is implemented*
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "originmain",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "pnpm -r run typecheck",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"test": "vitest run --coverage",
|
||||
"build": "pnpm --filter @originmain/app run build",
|
||||
"sdk:build": "pnpm --filter @originmain/live run build && pnpm --filter @originmain/next run build",
|
||||
"dev": "pnpm --filter @originmain/app run dev",
|
||||
"cli:build": "pnpm --filter @originmain/cli run build",
|
||||
"cli:dev": "node packages/cli/dist/cli.js dev",
|
||||
"migration:dry-run": "supabase db diff --schema public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"eslint": "^9.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"pnpm": ">=9"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "@originmain/agent-bridge",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@originmain/diff-engine": "workspace:*",
|
||||
"@originmain/origin-graph": "workspace:*",
|
||||
"@originmain/ai-layer": "workspace:*",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { TOOLS } from '../tools.js';
|
||||
|
||||
// ── Claude Code adapter ───────────────────────────────────────────────────────
|
||||
// Generates two artifacts for Claude Code MCP integration:
|
||||
// 1. CLAUDE.md section — tool reference injected into the project memory
|
||||
// 2. MCP server declaration — JSON block for .claude/settings.json
|
||||
//
|
||||
// Claude Code discovers MCP servers from .claude/settings.json and injects
|
||||
// CLAUDE.md into every session's system context automatically.
|
||||
|
||||
export interface ClaudeCodeAdapterOptions {
|
||||
/** MCP WebSocket endpoint URL */
|
||||
mcpServerUrl: string;
|
||||
/** Signed workspace token (from issueWorkspaceToken) */
|
||||
workspaceToken: string;
|
||||
workspaceName: string;
|
||||
/** Optional project name shown in CLAUDE.md header */
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export interface ClaudeCodeAdapterOutput {
|
||||
claudeMdSection: string;
|
||||
mcpServerDeclaration: ClaudeCodeMcpServer;
|
||||
}
|
||||
|
||||
export interface ClaudeCodeMcpServer {
|
||||
name: string;
|
||||
type: 'sse' | 'stdio';
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export function generateClaudeCodeConfig(opts: ClaudeCodeAdapterOptions): ClaudeCodeAdapterOutput {
|
||||
const projectLabel = opts.projectName ?? opts.workspaceName;
|
||||
const toolLines = TOOLS.map(t => `- \`${t.name}\`: ${t.description}`).join('\n');
|
||||
|
||||
const claudeMdSection = `## Origin Design Agent Bridge — ${projectLabel}
|
||||
|
||||
This project is connected to an Origin MCP server that exposes design-to-code tools.
|
||||
The server is pre-configured in \`.claude/settings.json\`.
|
||||
|
||||
### Available MCP Tools
|
||||
${toolLines}
|
||||
|
||||
### How intents arrive
|
||||
The Origin canvas pushes design intent diffs in two ways:
|
||||
|
||||
1. **SSE push (primary):** The MCP server sends an \`INTENT_RECEIVED\` event over the SSE
|
||||
connection automatically when the designer exports a change. You do not need to poll.
|
||||
|
||||
2. **Poll (fallback):** Call \`push_intent\` with \`{ workspace_id }\` to drain any pending
|
||||
intents. Use this at session start or when you suspect a missed push.
|
||||
|
||||
### Implementation Workflow
|
||||
1. Wait for an \`INTENT_RECEIVED\` SSE event, or call \`push_intent\` to fetch pending intents.
|
||||
2. Each intent includes a \`component.name\` — call \`resolve_component\` with that name to locate
|
||||
the source file and line number (e.g. \`{ component_name: "DashboardCard" }\`).
|
||||
3. Apply the change to the source file. When \`codeDiff\` is present and \`confidence\` is
|
||||
\`"exact"\`, apply it verbatim. When \`"approximate"\`, use it as a guide and refine as needed.
|
||||
4. After applying, call \`update_diff_status\` with \`status: "IMPLEMENTED"\` and the \`intentId\`.
|
||||
5. If the diff cannot be applied for any reason, call \`update_diff_status\` with \`status: "BLOCKED"\`
|
||||
and a \`reason\` string describing why (e.g. "Component not found in file", "File is read-only",
|
||||
"Diff conflicts with current file state"). The designer will see this reason in the Origin canvas.
|
||||
|
||||
### Important: Always close the loop with update_diff_status
|
||||
Every intent received — whether via SSE push or \`push_intent\` poll — MUST be closed with
|
||||
\`update_diff_status\` using either \`IMPLEMENTED\` or \`BLOCKED\`. An intent left in \`EXPORTED\`
|
||||
state will be retried on the next session.
|
||||
|
||||
### Design Language
|
||||
When token keys are present in the intent changes (\`tokenKey\` field), write \`var(--token-name)\`
|
||||
instead of the raw value so the component stays in sync with the design system.
|
||||
|
||||
### Rate Limit
|
||||
100 diff exports/hour per workspace. If you hit the limit, wait before retrying.
|
||||
`;
|
||||
|
||||
const mcpServerDeclaration: ClaudeCodeMcpServer = {
|
||||
name: 'origin',
|
||||
type: 'sse',
|
||||
url: opts.mcpServerUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.workspaceToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
return { claudeMdSection, mcpServerDeclaration };
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { TOOLS } from '../tools.js';
|
||||
|
||||
// ── Cursor adapter ────────────────────────────────────────────────────────────
|
||||
// Generates the two files Cursor reads to discover MCP tools:
|
||||
// 1. .cursorrules — natural-language context injected into every prompt
|
||||
// 2. cursor_settings.json — MCP server registration (added to .cursor/settings)
|
||||
//
|
||||
// Both are returned as strings; the caller writes them to the workspace root.
|
||||
|
||||
export interface CursorAdapterOptions {
|
||||
/** MCP WebSocket endpoint URL */
|
||||
mcpServerUrl: string;
|
||||
/** Signed workspace token (from issueWorkspaceToken) */
|
||||
workspaceToken: string;
|
||||
workspaceName: string;
|
||||
}
|
||||
|
||||
export interface CursorAdapterOutput {
|
||||
cursorrules: string;
|
||||
cursorSettings: CursorSettings;
|
||||
}
|
||||
|
||||
interface McpServerEntry {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CursorSettings {
|
||||
mcpServers: Record<string, McpServerEntry>;
|
||||
}
|
||||
|
||||
export function generateCursorConfig(opts: CursorAdapterOptions): CursorAdapterOutput {
|
||||
const toolDescriptions = TOOLS.map(t => `- **${t.name}**: ${t.description}`).join('\n');
|
||||
|
||||
const cursorrules = `# Origin — Design-to-Code Agent Bridge
|
||||
# Workspace: ${opts.workspaceName}
|
||||
#
|
||||
# You have access to the following Origin MCP tools via the connected MCP server.
|
||||
# Use them to fetch design diffs, query artboard context, and report implementation status.
|
||||
#
|
||||
## Available Tools
|
||||
${toolDescriptions}
|
||||
|
||||
## Workflow
|
||||
1. Call \`get_pending_diffs\` to retrieve EXPORTED IntentDiffs awaiting implementation.
|
||||
2. Call \`get_artboard_context\` to load component tree, design language, and screenshots.
|
||||
3. Implement the diff. Use \`ask_design_agent\` if design intent is unclear.
|
||||
4. Call \`update_diff_status\` with IMPLEMENTED or BLOCKED when done.
|
||||
|
||||
## Design Language
|
||||
Always validate tokens/colors/spacing against the workspace Design Language File.
|
||||
Call \`get_design_language\` once per session to cache the active DLF locally.
|
||||
|
||||
## Rate Limits
|
||||
100 diff exports per hour per workspace. The server returns HTTP 429 when exceeded.
|
||||
`;
|
||||
|
||||
const cursorSettings: CursorSettings = {
|
||||
mcpServers: {
|
||||
origin: {
|
||||
url: opts.mcpServerUrl,
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.workspaceToken}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { cursorrules, cursorSettings };
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
|
||||
// ── Workspace token ───────────────────────────────────────────────────────────
|
||||
// Format: base64(workspaceId:agentType:timestamp:hmac)
|
||||
// HMAC-SHA256 signed with AGENT_BRIDGE_SECRET (server-side env var).
|
||||
// Tokens expire after 30 days.
|
||||
|
||||
const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
|
||||
export type AgentType = 'CURSOR' | 'CLAUDE_CODE' | 'GENERIC';
|
||||
|
||||
export interface WorkspaceToken {
|
||||
workspaceId: string;
|
||||
agentType: AgentType;
|
||||
issuedAt: number;
|
||||
}
|
||||
|
||||
function getSecret(): string {
|
||||
const secret = process.env.AGENT_BRIDGE_SECRET;
|
||||
if (!secret) throw new Error('AGENT_BRIDGE_SECRET is not set');
|
||||
return secret;
|
||||
}
|
||||
|
||||
function sign(payload: string, secret: string): string {
|
||||
return createHmac('sha256', secret).update(payload).digest('hex');
|
||||
}
|
||||
|
||||
export function issueWorkspaceToken(workspaceId: string, agentType: AgentType): string {
|
||||
const issuedAt = Date.now();
|
||||
const payload = `${workspaceId}:${agentType}:${issuedAt}`;
|
||||
const hmac = sign(payload, getSecret());
|
||||
return Buffer.from(`${payload}:${hmac}`).toString('base64url');
|
||||
}
|
||||
|
||||
export function verifyWorkspaceToken(token: string): WorkspaceToken | null {
|
||||
// Decode base64url — this is the only step that can throw for reasons outside
|
||||
// our control (malformed input). All other failures are deterministic logic.
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = Buffer.from(token, 'base64url').toString('utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = decoded.split(':');
|
||||
if (parts.length !== 4) return null;
|
||||
|
||||
const [workspaceId, agentType, issuedAtStr, providedHmac] = parts;
|
||||
if (!workspaceId || !agentType || !issuedAtStr || !providedHmac) return null;
|
||||
|
||||
const issuedAt = parseInt(issuedAtStr, 10);
|
||||
if (isNaN(issuedAt) || Date.now() - issuedAt > TOKEN_TTL_MS) return null;
|
||||
|
||||
const payload = `${workspaceId}:${agentType}:${issuedAtStr}`;
|
||||
// getSecret() intentionally throws if AGENT_BRIDGE_SECRET is unset —
|
||||
// that's a server misconfiguration, not an invalid token.
|
||||
const expectedHmac = sign(payload, getSecret());
|
||||
|
||||
const expected = Buffer.from(expectedHmac, 'hex');
|
||||
const provided = Buffer.from(providedHmac, 'hex');
|
||||
if (expected.length !== provided.length) return null;
|
||||
if (!timingSafeEqual(expected, provided)) return null;
|
||||
|
||||
const validAgentTypes: AgentType[] = ['CURSOR', 'CLAUDE_CODE', 'GENERIC'];
|
||||
if (!validAgentTypes.includes(agentType as AgentType)) return null;
|
||||
|
||||
return { workspaceId, agentType: agentType as AgentType, issuedAt };
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
export type { JsonRpcRequest, JsonRpcSuccess, JsonRpcError, JsonRpcResponse, AuthRequest, AuthAck, ToolResult, IntentChange, IntentMessage, IntentReceivedPush } from './protocol.js';
|
||||
export { MCP_ERROR, textResult, jsonResult } from './protocol.js';
|
||||
|
||||
export type { WorkspaceToken, AgentType } from './auth.js';
|
||||
export { issueWorkspaceToken, verifyWorkspaceToken } from './auth.js';
|
||||
|
||||
export type { RateLimitResult } from './rate-limiter.js';
|
||||
export { checkRateLimit, getRateLimitStatus } from './rate-limiter.js';
|
||||
|
||||
export type { McpTool, ToolContext } from './tools.js';
|
||||
export { TOOLS, TOOL_MAP, getToolList, dispatchTool, storePendingIntent, drainPendingIntents, registerIndexer, getIndexerUrl, heartbeatIndexer, registerSseClient } from './tools.js';
|
||||
|
||||
export type { CursorAdapterOptions, CursorAdapterOutput } from './adapters/cursor.js';
|
||||
export { generateCursorConfig } from './adapters/cursor.js';
|
||||
|
||||
export type { ClaudeCodeAdapterOptions, ClaudeCodeAdapterOutput, ClaudeCodeMcpServer } from './adapters/claude-code.js';
|
||||
export { generateClaudeCodeConfig } from './adapters/claude-code.js';
|
||||
@@ -1,130 +0,0 @@
|
||||
// ── MCP / JSON-RPC 2.0 protocol types ────────────────────────────────────────
|
||||
// The Agent Bridge implements MCP over JSON-RPC 2.0, transported via WebSocket
|
||||
// (long-running) or HTTP POST (polling-based agents).
|
||||
|
||||
export interface JsonRpcRequest {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
export interface JsonRpcSuccess<T = unknown> {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number;
|
||||
result: T;
|
||||
}
|
||||
|
||||
export interface JsonRpcError {
|
||||
jsonrpc: '2.0';
|
||||
id: string | number | null;
|
||||
error: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export type JsonRpcResponse<T = unknown> = JsonRpcSuccess<T> | JsonRpcError;
|
||||
|
||||
// ── MCP error codes ───────────────────────────────────────────────────────────
|
||||
export const MCP_ERROR = {
|
||||
PARSE_ERROR: -32700,
|
||||
INVALID_REQUEST: -32600,
|
||||
METHOD_NOT_FOUND: -32601,
|
||||
INVALID_PARAMS: -32602,
|
||||
INTERNAL_ERROR: -32603,
|
||||
// Application-level codes
|
||||
UNAUTHORIZED: -32001,
|
||||
RATE_LIMITED: -32002,
|
||||
NOT_FOUND: -32003,
|
||||
} as const;
|
||||
|
||||
// ── Origmain-specific envelope ────────────────────────────────────────────────
|
||||
// Every connection must present a signed workspace token in the first message.
|
||||
|
||||
export interface AuthRequest {
|
||||
type: 'auth';
|
||||
workspaceToken: string;
|
||||
}
|
||||
|
||||
export interface AuthAck {
|
||||
type: 'auth_ack';
|
||||
workspaceId: string;
|
||||
agentType: 'CURSOR' | 'CLAUDE_CODE' | 'GENERIC';
|
||||
}
|
||||
|
||||
// ── Intent types — Phase 5 §8.4 ──────────────────────────────────────────────
|
||||
// These describe the design changes the canvas wants the agent to apply to source.
|
||||
|
||||
export interface IntentChange {
|
||||
type: 'style' | 'prop' | 'layout' | 'remove';
|
||||
cssProperty?: string;
|
||||
propName?: string;
|
||||
from?: unknown;
|
||||
to?: unknown;
|
||||
/** CSS custom property key when the value maps to a design token (Phase 6). */
|
||||
tokenKey?: string;
|
||||
confidence: 'exact' | 'approximate';
|
||||
}
|
||||
|
||||
export interface IntentMessage {
|
||||
intentId: string;
|
||||
component: {
|
||||
name: string;
|
||||
/** Fiber path ID — used for diff correlation. */
|
||||
nodeId: string;
|
||||
/** Call-site location: "src/app/dashboard/page.tsx:34" */
|
||||
callSite?: string;
|
||||
definitionFile?: string;
|
||||
definitionLine?: number;
|
||||
/** Current runtime props — context for the agent. */
|
||||
props: Record<string, unknown>;
|
||||
/** From AST indexer (optional — indexer may not be running). */
|
||||
propsSchema?: Array<{ name: string; type: string; optional: boolean }>;
|
||||
};
|
||||
changes: IntentChange[];
|
||||
/**
|
||||
* Ready-to-apply code diff (when confidence is 'exact', apply verbatim;
|
||||
* when 'approximate', use as a guide and refine).
|
||||
*/
|
||||
codeDiff?: {
|
||||
file: string;
|
||||
originalContent: string;
|
||||
patchedContent: string;
|
||||
confidence: 'exact' | 'approximate';
|
||||
};
|
||||
/** Before/after visual snapshots (base64 data URLs). */
|
||||
snapshot?: {
|
||||
before: string;
|
||||
after?: string;
|
||||
};
|
||||
/** Design language context when tokens are loaded (Phase 6). */
|
||||
designLanguage?: {
|
||||
tokensUsed: string[];
|
||||
palette: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Server → agent push (sent over WebSocket, no JSON-RPC id — not a request).
|
||||
* Emitted immediately after `push_intent` stores a new intent.
|
||||
*/
|
||||
export interface IntentReceivedPush {
|
||||
type: 'INTENT_RECEIVED';
|
||||
intent: IntentMessage;
|
||||
}
|
||||
|
||||
// ── Tool result types ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface ToolResult<T> {
|
||||
content: { type: 'text'; text: string } | { type: 'json'; json: T };
|
||||
}
|
||||
|
||||
export function textResult(text: string): ToolResult<never> {
|
||||
return { content: { type: 'text', text } };
|
||||
}
|
||||
|
||||
export function jsonResult<T>(json: T): ToolResult<T> {
|
||||
return { content: { type: 'json', json } };
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
// ── Per-workspace rate limiter ────────────────────────────────────────────────
|
||||
// Enforces: 100 diff exports per hour per workspace.
|
||||
// Uses a sliding window implemented with an in-process timestamp array.
|
||||
// For multi-instance deployments, replace with a Redis-backed implementation.
|
||||
|
||||
export interface RateLimitResult {
|
||||
allowed: boolean;
|
||||
remaining: number;
|
||||
resetAt: number;
|
||||
}
|
||||
|
||||
const WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
||||
const MAX_REQUESTS = 100;
|
||||
|
||||
const windows = new Map<string, number[]>();
|
||||
|
||||
export function checkRateLimit(workspaceId: string): RateLimitResult {
|
||||
const now = Date.now();
|
||||
const cutoff = now - WINDOW_MS;
|
||||
|
||||
const prev = (windows.get(workspaceId) ?? []).filter(t => t > cutoff);
|
||||
const allowed = prev.length < MAX_REQUESTS;
|
||||
|
||||
// Only persist the new timestamp when the request is allowed.
|
||||
// Read the allowed decision from `prev.length` (before push) so concurrent
|
||||
// synchronous callers in the same event-loop tick all see the same baseline.
|
||||
if (allowed) {
|
||||
windows.set(workspaceId, [...prev, now]);
|
||||
}
|
||||
|
||||
const timestamps = allowed ? [...prev, now] : prev;
|
||||
const oldest = timestamps[0];
|
||||
const resetAt = oldest !== undefined ? oldest + WINDOW_MS : now + WINDOW_MS;
|
||||
|
||||
return {
|
||||
allowed,
|
||||
remaining: Math.max(0, MAX_REQUESTS - timestamps.length),
|
||||
resetAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRateLimitStatus(workspaceId: string): RateLimitResult {
|
||||
const now = Date.now();
|
||||
const cutoff = now - WINDOW_MS;
|
||||
const timestamps = (windows.get(workspaceId) ?? []).filter(t => t > cutoff);
|
||||
const oldest = timestamps[0];
|
||||
return {
|
||||
allowed: timestamps.length < MAX_REQUESTS,
|
||||
remaining: Math.max(0, MAX_REQUESTS - timestamps.length),
|
||||
resetAt: oldest !== undefined ? oldest + WINDOW_MS : now + WINDOW_MS,
|
||||
};
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
/**
|
||||
* tools.ts — Phase 5
|
||||
*
|
||||
* Defines the MCP tools exposed by the Agent Bridge to connected IDE agents
|
||||
* (Cursor, Claude Code, etc.). Each tool is a JSON Schema descriptor plus a
|
||||
* handler that runs inside the MCP server request loop.
|
||||
*
|
||||
* Tools:
|
||||
* push_intent — Canvas pushes a style intent diff to the agent
|
||||
* resolve_component — Agent queries the component source location by fiber ID
|
||||
*
|
||||
* spec: SOURCE-AWARE-CANVAS.md Phase 5 §9 "Agent Bridge"
|
||||
*/
|
||||
|
||||
import { textResult, jsonResult } from './protocol.js';
|
||||
import type { ToolResult, IntentMessage, IntentChange, IntentReceivedPush } from './protocol.js';
|
||||
|
||||
// ── Tool descriptor shape (MCP tools/list schema) ─────────────────────────────
|
||||
|
||||
export interface McpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// ── Tool execution context (injected by the MCP server request loop) ──────────
|
||||
|
||||
export interface ToolContext {
|
||||
/** Verified workspace ID from the authenticated token. */
|
||||
workspaceId: string;
|
||||
/** The raw parsed params from the JSON-RPC request. */
|
||||
params: unknown;
|
||||
/**
|
||||
* Optional Supabase server-client for tools that need DB writes.
|
||||
* Only provided when the route handler calls createServerClient() — tools
|
||||
* that don't need it (push_intent, resolve_component) ignore it.
|
||||
*/
|
||||
db?: {
|
||||
from: (table: string) => unknown;
|
||||
};
|
||||
}
|
||||
|
||||
// ── In-process intent store ───────────────────────────────────────────────────
|
||||
// Intents are pushed here by the /api/intent Next.js route (via push_intent),
|
||||
// and drained by connected agents through polling or INTENT_RECEIVED push.
|
||||
//
|
||||
// For production multi-instance deployments, replace with a Redis pub/sub or
|
||||
// Supabase Realtime channel.
|
||||
|
||||
interface IntentRecord {
|
||||
intentId: string;
|
||||
workspaceId: string;
|
||||
artboardId: string;
|
||||
componentName: string;
|
||||
patchJson: string;
|
||||
strategy: string;
|
||||
summary: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
const pendingIntents = new Map<string, IntentRecord[]>();
|
||||
|
||||
/**
|
||||
* Store an intent pushed by the canvas (called from the /api/intent route).
|
||||
*
|
||||
* @param supplyIntentId Pass the Supabase UUID when you've already created the
|
||||
* DB row — the in-memory ID must match so that the agent's
|
||||
* `update_diff_status` tool can call `.eq('id', intentId)`.
|
||||
* If omitted, a local opaque ID is generated (dev/offline mode).
|
||||
* Returns the intentId actually stored.
|
||||
*/
|
||||
export function storePendingIntent(
|
||||
workspaceId: string,
|
||||
intent: Omit<IntentRecord, 'intentId' | 'workspaceId' | 'createdAt'>,
|
||||
supplyIntentId?: string,
|
||||
): string {
|
||||
const intentId = supplyIntentId ?? `intent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
const record: IntentRecord = {
|
||||
intentId,
|
||||
workspaceId,
|
||||
...intent,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
const queue = pendingIntents.get(workspaceId) ?? [];
|
||||
queue.push(record);
|
||||
pendingIntents.set(workspaceId, queue);
|
||||
|
||||
// Push INTENT_RECEIVED to any SSE-connected agents for this workspace.
|
||||
const push: IntentReceivedPush = {
|
||||
type: 'INTENT_RECEIVED',
|
||||
intent: recordToIntentMessage(record),
|
||||
};
|
||||
pushToSseClients(workspaceId, `data: ${JSON.stringify(push)}\n\n`);
|
||||
|
||||
return intentId;
|
||||
}
|
||||
|
||||
// ── SSE client registry ───────────────────────────────────────────────────────
|
||||
// Maps workspaceId → set of send callbacks registered by connected SSE clients.
|
||||
//
|
||||
// Same in-process design as pendingIntents / indexerRegistry — for production
|
||||
// multi-instance deployments, replace with Redis pub/sub or Supabase Realtime.
|
||||
|
||||
type SseSendFn = (chunk: string) => void;
|
||||
|
||||
const sseClients = new Map<string, Set<SseSendFn>>();
|
||||
|
||||
/**
|
||||
* Register an SSE send callback for a workspace.
|
||||
* Returns an unsubscribe function — call it when the SSE connection closes.
|
||||
*/
|
||||
export function registerSseClient(workspaceId: string, send: SseSendFn): () => void {
|
||||
let set = sseClients.get(workspaceId);
|
||||
if (!set) { set = new Set(); sseClients.set(workspaceId, set); }
|
||||
set.add(send);
|
||||
return () => {
|
||||
set!.delete(send);
|
||||
if (set!.size === 0) sseClients.delete(workspaceId);
|
||||
};
|
||||
}
|
||||
|
||||
/** Broadcast a raw SSE chunk to all connected clients for a workspace. */
|
||||
function pushToSseClients(workspaceId: string, chunk: string): void {
|
||||
const clients = sseClients.get(workspaceId);
|
||||
if (!clients) return;
|
||||
for (const send of clients) {
|
||||
try { send(chunk); } catch { /* client disconnected — will be deregistered on abort */ }
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert an IntentRecord to the IntentMessage shape for SSE / push_intent JSON response. */
|
||||
function recordToIntentMessage(record: IntentRecord): IntentMessage {
|
||||
let changes: IntentChange[] = [];
|
||||
try {
|
||||
const parsed = JSON.parse(record.patchJson) as unknown;
|
||||
const patches: Array<{ property?: string; value?: string; previousValue?: string }> =
|
||||
Array.isArray(parsed) ? parsed : ((parsed as { patches?: unknown[] }).patches ?? []);
|
||||
changes = patches.map((p) => ({
|
||||
type: 'style' as const,
|
||||
cssProperty: p.property ?? '',
|
||||
from: p.previousValue,
|
||||
to: p.value ?? '',
|
||||
confidence: 'approximate' as const,
|
||||
}));
|
||||
} catch { /* malformed patchJson — send empty changes list */ }
|
||||
|
||||
return {
|
||||
intentId: record.intentId,
|
||||
component: {
|
||||
name: record.componentName,
|
||||
// nodeId not stored in IntentRecord — agent uses component_name with
|
||||
// resolve_component instead of a fiber-ID lookup.
|
||||
nodeId: '',
|
||||
props: {},
|
||||
},
|
||||
changes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain all pending intents for a workspace (called by push_intent tool handler
|
||||
* or by the agent when polling).
|
||||
*/
|
||||
export function drainPendingIntents(workspaceId: string): IntentRecord[] {
|
||||
const queue = pendingIntents.get(workspaceId) ?? [];
|
||||
pendingIntents.delete(workspaceId);
|
||||
return queue;
|
||||
}
|
||||
|
||||
// ── CLI indexer registry ──────────────────────────────────────────────────────
|
||||
// Maps workspaceId → { url, expiresAt } (registered via /register-indexer).
|
||||
//
|
||||
// TTL policy (spec Phase 5 §8.3):
|
||||
// • Default TTL: 300 s (5 min)
|
||||
// • CLI sends heartbeat POST every 120 s to refresh the TTL
|
||||
// • Registration is considered expired after 360 s (3× heartbeat interval)
|
||||
//
|
||||
// The GC sweep runs on every registration and lookup to avoid a timer leak
|
||||
// in serverless/edge environments where setInterval may not fire.
|
||||
|
||||
interface IndexerEntry {
|
||||
url: string;
|
||||
expiresAt: number; // ms since epoch
|
||||
}
|
||||
|
||||
const indexerRegistry = new Map<string, IndexerEntry>();
|
||||
|
||||
/** Evict all entries whose TTL has expired. */
|
||||
function gcIndexerRegistry(): void {
|
||||
const now = Date.now();
|
||||
for (const [wid, entry] of indexerRegistry) {
|
||||
if (entry.expiresAt < now) indexerRegistry.delete(wid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register (or refresh) a CLI indexer for a workspace.
|
||||
* Security: the caller MUST validate that `url` is a localhost URL before
|
||||
* calling this function (enforced by the /register-indexer API route).
|
||||
*
|
||||
* @param workspaceId Verified workspace ID
|
||||
* @param url Indexer URL — must be localhost (caller-validated)
|
||||
* @param ttlSeconds TTL in seconds (default: 300 s / 5 min)
|
||||
*/
|
||||
export function registerIndexer(workspaceId: string, url: string, ttlSeconds = 300): void {
|
||||
gcIndexerRegistry();
|
||||
indexerRegistry.set(workspaceId, { url, expiresAt: Date.now() + ttlSeconds * 1000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered indexer URL for a workspace, or undefined if not
|
||||
* registered or if the TTL has expired.
|
||||
*/
|
||||
export function getIndexerUrl(workspaceId: string): string | undefined {
|
||||
gcIndexerRegistry();
|
||||
const entry = indexerRegistry.get(workspaceId);
|
||||
if (!entry) return undefined;
|
||||
if (entry.expiresAt < Date.now()) {
|
||||
indexerRegistry.delete(workspaceId);
|
||||
return undefined;
|
||||
}
|
||||
return entry.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the TTL for an already-registered indexer (heartbeat endpoint).
|
||||
* Returns false if no entry exists for the workspace (caller should 404).
|
||||
*/
|
||||
export function heartbeatIndexer(workspaceId: string, ttlSeconds = 300): boolean {
|
||||
const entry = indexerRegistry.get(workspaceId);
|
||||
if (!entry || entry.expiresAt < Date.now()) return false;
|
||||
entry.expiresAt = Date.now() + ttlSeconds * 1000;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Tool definitions ──────────────────────────────────────────────────────────
|
||||
|
||||
const PUSH_INTENT_TOOL: McpTool = {
|
||||
name: 'push_intent',
|
||||
description:
|
||||
'Receive a design intent diff from the Origin canvas. The diff describes style or prop changes ' +
|
||||
'made by the designer that should be applied to the source code. Call this when Origin notifies ' +
|
||||
'you of pending changes. Returns the list of pending intents for this workspace.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
workspace_id: {
|
||||
type: 'string',
|
||||
description: 'The workspace ID (must match the authenticated token).',
|
||||
},
|
||||
},
|
||||
required: ['workspace_id'],
|
||||
},
|
||||
};
|
||||
|
||||
const RESOLVE_COMPONENT_TOOL: McpTool = {
|
||||
name: 'resolve_component',
|
||||
description:
|
||||
'Resolve the source file location for a React component by name. ' +
|
||||
'Proxies to the CLI indexer running in the workspace and returns the file path, ' +
|
||||
'line number, props schema, and design tokens used by the component.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
component_name: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The display name of the component to look up (e.g. "DashboardCard"). ' +
|
||||
'Case-sensitive; use the name exactly as it appears in the INTENT_RECEIVED message.',
|
||||
},
|
||||
},
|
||||
required: ['component_name'],
|
||||
},
|
||||
};
|
||||
|
||||
const UPDATE_DIFF_STATUS_TOOL: McpTool = {
|
||||
name: 'update_diff_status',
|
||||
description:
|
||||
'Update the status of a design intent diff after attempting to apply it. ' +
|
||||
'Call with status "IMPLEMENTED" after successfully applying the diff to the source code. ' +
|
||||
'Call with status "BLOCKED" and a reason string if the diff could not be applied — ' +
|
||||
'the reason is shown to the designer so they can resolve the conflict manually. ' +
|
||||
'Valid statuses: "IMPLEMENTED" | "BLOCKED".',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
intent_id: {
|
||||
type: 'string',
|
||||
description: 'The intentId received in the INTENT_RECEIVED message or push_intent response.',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['IMPLEMENTED', 'BLOCKED'],
|
||||
description: '"IMPLEMENTED" if the diff was applied successfully; "BLOCKED" if it could not be applied.',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Required when status is "BLOCKED". Describe why the diff could not be applied — ' +
|
||||
'e.g. "Component not found in file", "File is read-only", "Diff conflicts with current state".',
|
||||
},
|
||||
},
|
||||
required: ['intent_id', 'status'],
|
||||
},
|
||||
};
|
||||
|
||||
// ── Tool map (name → descriptor) ──────────────────────────────────────────────
|
||||
|
||||
export const TOOL_MAP: Record<string, McpTool> = {
|
||||
push_intent: PUSH_INTENT_TOOL,
|
||||
resolve_component: RESOLVE_COMPONENT_TOOL,
|
||||
update_diff_status: UPDATE_DIFF_STATUS_TOOL,
|
||||
};
|
||||
|
||||
export const TOOLS: McpTool[] = Object.values(TOOL_MAP);
|
||||
|
||||
export function getToolList(): McpTool[] {
|
||||
return TOOLS;
|
||||
}
|
||||
|
||||
// ── Tool handlers ─────────────────────────────────────────────────────────────
|
||||
|
||||
type Params = Record<string, unknown>;
|
||||
|
||||
async function handlePushIntent(ctx: ToolContext): Promise<ToolResult<unknown>> {
|
||||
const params = ctx.params as Params;
|
||||
const wid = typeof params['workspace_id'] === 'string' ? params['workspace_id'] : ctx.workspaceId;
|
||||
|
||||
if (wid !== ctx.workspaceId) {
|
||||
return textResult('Error: workspace_id does not match authenticated workspace.');
|
||||
}
|
||||
|
||||
const records = drainPendingIntents(wid);
|
||||
|
||||
if (records.length === 0) {
|
||||
return jsonResult({ intents: [], message: 'No pending design intents for this workspace.' });
|
||||
}
|
||||
|
||||
// Return the full IntentMessage array so the agent can act on each intent
|
||||
// without a separate round-trip. The same data is pushed proactively over
|
||||
// SSE as INTENT_RECEIVED — this poll path exists as a fallback and for
|
||||
// agents that do not maintain a persistent SSE connection.
|
||||
const intents = records.map(recordToIntentMessage);
|
||||
return jsonResult({ intents });
|
||||
}
|
||||
|
||||
async function handleResolveComponent(ctx: ToolContext): Promise<ToolResult<unknown>> {
|
||||
const params = ctx.params as Params;
|
||||
const componentName = typeof params['component_name'] === 'string' ? params['component_name'] : null;
|
||||
|
||||
if (!componentName) {
|
||||
return textResult('Error: component_name is required.');
|
||||
}
|
||||
|
||||
// Check if a CLI indexer is registered for this workspace.
|
||||
const indexerUrl = getIndexerUrl(ctx.workspaceId);
|
||||
if (!indexerUrl) {
|
||||
return textResult(
|
||||
`No CLI indexer registered for workspace ${ctx.workspaceId}. ` +
|
||||
'Run "npx @originmain/cli dev" to enable component resolution.',
|
||||
);
|
||||
}
|
||||
|
||||
// Proxy to GET /components?name=<componentName> on the CLI indexer.
|
||||
// The indexer returns ComponentEntry[] — we return the first exact or best match.
|
||||
try {
|
||||
const url = new URL('/components', indexerUrl);
|
||||
url.searchParams.set('name', componentName);
|
||||
|
||||
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(5000) });
|
||||
|
||||
if (!res.ok) {
|
||||
return textResult(`Indexer returned ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const entries = (await res.json()) as Array<{
|
||||
name: string;
|
||||
definitionFile: string;
|
||||
relativeFile: string;
|
||||
lineNumber: number;
|
||||
props: Array<{ name: string; type: string; optional: boolean }>;
|
||||
tokensUsed: string[];
|
||||
}>;
|
||||
|
||||
if (!entries.length) {
|
||||
return textResult(`Component "${componentName}" not found in index. Make sure the CLI indexer has indexed this file.`);
|
||||
}
|
||||
|
||||
// Prefer exact name match; fall back to first fuzzy result.
|
||||
const match = entries.find((e) => e.name === componentName) ?? entries[0]!;
|
||||
|
||||
return jsonResult({
|
||||
name: match.name,
|
||||
definitionFile: match.definitionFile,
|
||||
relativeFile: match.relativeFile,
|
||||
lineNumber: match.lineNumber,
|
||||
props: match.props,
|
||||
tokensUsed: match.tokensUsed,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return textResult(`Failed to contact CLI indexer: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateDiffStatus(ctx: ToolContext): Promise<ToolResult<unknown>> {
|
||||
const params = ctx.params as Params;
|
||||
const intentId = typeof params['intent_id'] === 'string' ? params['intent_id'] : null;
|
||||
const status = typeof params['status'] === 'string' ? params['status'] : null;
|
||||
const reason = typeof params['reason'] === 'string' ? params['reason'] : null;
|
||||
|
||||
if (!intentId || !status) {
|
||||
return textResult('Error: intent_id and status are required.');
|
||||
}
|
||||
|
||||
if (status !== 'IMPLEMENTED' && status !== 'BLOCKED') {
|
||||
return textResult('Error: status must be "IMPLEMENTED" or "BLOCKED".');
|
||||
}
|
||||
|
||||
if (status === 'BLOCKED' && !reason) {
|
||||
return textResult('Error: reason is required when status is "BLOCKED".');
|
||||
}
|
||||
|
||||
if (!ctx.db) {
|
||||
// Fallback: no DB client available — just acknowledge.
|
||||
return textResult(`update_diff_status acknowledged: ${intentId} → ${status}${reason ? ` (${reason})` : ''}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const updatePayload: Record<string, unknown> = {
|
||||
status,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
if (reason) updatePayload['blocked_reason'] = reason;
|
||||
|
||||
// db is typed minimally; cast to any so the Supabase query chain compiles.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { data, error } = await ((ctx.db.from('intent_diffs') as any)
|
||||
.update(updatePayload)
|
||||
.eq('id', intentId)
|
||||
.select()
|
||||
.single() as Promise<{ data: { id: string; status: string } | null; error: { message: string } | null }>);
|
||||
|
||||
if (error) return textResult(`Error updating diff status: ${error.message}`);
|
||||
if (!data) return textResult(`Error: intent ${intentId} not found.`);
|
||||
|
||||
return textResult(
|
||||
`Intent ${intentId} status updated to ${status}${reason ? `: ${reason}` : ''}.`,
|
||||
);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return textResult(`Failed to update diff status: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dispatch ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function dispatchTool(name: string, ctx: ToolContext): Promise<ToolResult<unknown>> {
|
||||
switch (name) {
|
||||
case 'push_intent':
|
||||
return handlePushIntent(ctx);
|
||||
case 'resolve_component':
|
||||
return handleResolveComponent(ctx);
|
||||
case 'update_diff_status':
|
||||
return handleUpdateDiffStatus(ctx);
|
||||
default:
|
||||
return textResult(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "@originmain/ai-layer",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.91.1",
|
||||
"@originmain/design-language": "workspace:*",
|
||||
"@originmain/diff-engine": "workspace:*",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
// ── Model constants ───────────────────────────────────────────────────────────
|
||||
|
||||
// Spec Layer 6: "Claude Sonnet 4 API is the only AI provider."
|
||||
// claude-sonnet-4-6 supports per-call temperature (0.1 / 0.2 / 0.3 per prompt)
|
||||
// which the spec requires for diff-summary, completion-zone, and agent-query.
|
||||
export const MODEL = 'claude-sonnet-4-6' as const;
|
||||
|
||||
// ── Singleton client ──────────────────────────────────────────────────────────
|
||||
// The client is created once and shared. API key is injected from the server
|
||||
// environment — never exposed to the client bundle.
|
||||
|
||||
let _client: Anthropic | null = null;
|
||||
|
||||
export function getClient(): Anthropic {
|
||||
if (!_client) {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) throw new Error('ANTHROPIC_API_KEY is not set');
|
||||
_client = new Anthropic({ apiKey });
|
||||
}
|
||||
return _client;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildAgentQueryMessages } from '../prompts/agent-query.prompt.js';
|
||||
|
||||
export interface AgentQAInput {
|
||||
/** Question from the coding agent (e.g. Cursor or Claude Code) */
|
||||
question: string;
|
||||
/** The diff ID this question is about */
|
||||
diffId: string;
|
||||
/** Full artboard context as JSON */
|
||||
artboardContextJson: string;
|
||||
/** Active DLF */
|
||||
dlfJson?: string;
|
||||
/** Before-state screenshot as base64 data URL (spec Layer 6.3-R3) */
|
||||
beforeScreenshotBase64?: string;
|
||||
/** After-state screenshot as base64 data URL (spec Layer 6.3-R3) */
|
||||
afterScreenshotBase64?: string;
|
||||
}
|
||||
|
||||
export interface AgentQAOutput {
|
||||
answer: string;
|
||||
/** Optional visual reference description (e.g. "see padding token in section 3") */
|
||||
visualReference?: string;
|
||||
}
|
||||
|
||||
export async function answerAgentQuestion(
|
||||
gateway: AIGateway,
|
||||
input: AgentQAInput
|
||||
): Promise<AgentQAOutput> {
|
||||
const { system, userContent, maxTokens } = buildAgentQueryMessages(input);
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
// userContent is ContentBlockParam[] — may include image blocks for screenshots
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.1, // spec Layer 6.3: 0.1 for agent-query (factual, authoritative)
|
||||
});
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(response.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim());
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Design agent returned unparseable JSON: ${String(err)}. ` +
|
||||
`Raw response (first 300 chars): ${response.text.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
|
||||
const output = parsed as AgentQAOutput;
|
||||
if (!output.answer || typeof output.answer !== 'string') {
|
||||
throw new Error(
|
||||
`Design agent response missing required "answer" field. ` +
|
||||
`Raw response (first 300 chars): ${response.text.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildArtboardQueryMessages } from '../prompts/artboard-query.prompt.js';
|
||||
|
||||
export interface ArtboardQueryInput {
|
||||
/** Natural language query from the user */
|
||||
query: string;
|
||||
/** Array of artboard metadata objects as JSON */
|
||||
artboardsJson: string;
|
||||
}
|
||||
|
||||
export interface ArtboardQueryResult {
|
||||
artboardId: string;
|
||||
relevanceScore: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface ArtboardQueryOutput {
|
||||
results: ArtboardQueryResult[];
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export async function queryCrossArtboard(
|
||||
gateway: AIGateway,
|
||||
input: ArtboardQueryInput
|
||||
): Promise<ArtboardQueryOutput> {
|
||||
const { system, userContent, maxTokens } = buildArtboardQueryMessages(input);
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.1, // spec Layer 10.2: 0.1 for workspace queries (factual)
|
||||
});
|
||||
|
||||
// Returning empty results on parse failure is indistinguishable from "no match".
|
||||
// Throw so the UI can show an error rather than a misleading empty state.
|
||||
// Strip markdown code fences that the model occasionally adds despite instructions.
|
||||
const rawText = response.text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(rawText);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Artboard query returned unparseable JSON: ${String(err)}. ` +
|
||||
`Raw response (first 300 chars): ${response.text.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
|
||||
// Runtime guard: ensure the parsed value has the expected shape before casting.
|
||||
// Without this, a malformed AI response (e.g. { results: null }) would let
|
||||
// callers hit a TypeError on `.results.map()` instead of a clear error message.
|
||||
if (
|
||||
typeof parsed !== 'object' ||
|
||||
parsed === null ||
|
||||
!Array.isArray((parsed as Record<string, unknown>)['results'])
|
||||
) {
|
||||
throw new Error(
|
||||
`Artboard query response missing "results" array. ` +
|
||||
`Raw response (first 300 chars): ${response.text.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
|
||||
return parsed as ArtboardQueryOutput;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { AIGateway } from '../gateway.js';
|
||||
import { buildCompletionZoneMessages } from '../prompts/completion-zone.prompt.js';
|
||||
import type { GatewayResponse } from '../gateway.js';
|
||||
|
||||
// ── Output schema (spec Layer 6.3-R2: validated with Zod before accepting) ────
|
||||
// The model MUST return exactly these two fields. We validate the shape before
|
||||
// returning to callers so a malformed AI response never reaches the canvas store.
|
||||
|
||||
const CompletionZoneResultSchema = z.object({
|
||||
proposedTree: z.unknown(), // arbitrary component tree — shape validated downstream
|
||||
description: z.string().min(1), // non-empty natural-language summary
|
||||
});
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CompletionZoneInput {
|
||||
/** JSON of the surrounding component tree */
|
||||
componentTreeJson: string;
|
||||
/** User intent / what the zone should be filled with */
|
||||
intent: string;
|
||||
/** Active Design Language File as JSON string */
|
||||
dlfJson?: string;
|
||||
/** Before screenshot as base64 data URL (optional) */
|
||||
screenshotBase64?: string;
|
||||
/** Workspace ID for per-workspace cost attribution (spec Layer 6) */
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export interface CompletionZoneOutput {
|
||||
/** Proposed component tree changes as structured JSON — Zod-validated shape */
|
||||
proposedTree: unknown;
|
||||
/** Natural language description of the proposed change */
|
||||
description: string;
|
||||
raw: GatewayResponse;
|
||||
}
|
||||
|
||||
// ── Feature ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fillCompletionZone(
|
||||
gateway: AIGateway,
|
||||
input: CompletionZoneInput,
|
||||
maxRetries = 3
|
||||
): Promise<CompletionZoneOutput> {
|
||||
const { system, userContent, maxTokens } = buildCompletionZoneMessages(input);
|
||||
|
||||
// Retry up to maxRetries times on invalid JSON or Zod-invalid output
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.3, // spec Layer 6.3: 0.3 for completion-zone (creative latitude)
|
||||
...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId } : {}),
|
||||
});
|
||||
|
||||
// Strip markdown fences the model occasionally adds despite instructions
|
||||
const raw = response.text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim();
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (parseErr) {
|
||||
lastError = new Error(
|
||||
`AI returned invalid JSON on attempt ${attempt + 1}: ${String(parseErr)}. ` +
|
||||
`Raw response (first 200 chars): ${response.text.slice(0, 200)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate shape with Zod before accepting (spec Layer 6.3-R2)
|
||||
const validated = CompletionZoneResultSchema.safeParse(parsed);
|
||||
if (!validated.success) {
|
||||
lastError = new Error(
|
||||
`AI response failed schema validation on attempt ${attempt + 1}: ` +
|
||||
validated.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ') +
|
||||
`. Raw response (first 200 chars): ${response.text.slice(0, 200)}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
proposedTree: validated.data.proposedTree,
|
||||
description: validated.data.description,
|
||||
raw: response,
|
||||
};
|
||||
}
|
||||
|
||||
throw lastError ?? new Error('Completion zone fill failed after retries');
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildDiffSummaryMessages, buildAggregateSummaryMessages } from '../prompts/diff-summary.prompt.js';
|
||||
|
||||
export interface DiffSummaryInput {
|
||||
/** Serialized component-level changes (JSON) */
|
||||
changesJson: string;
|
||||
/** Component name */
|
||||
componentName: string;
|
||||
/** Active DLF as JSON string — passed through for rule-violation annotation */
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
export interface DiffSummaryOutput {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export async function generateDiffSummary(
|
||||
gateway: AIGateway,
|
||||
input: DiffSummaryInput
|
||||
): Promise<DiffSummaryOutput> {
|
||||
const { system, userContent, maxTokens } = buildDiffSummaryMessages(input);
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.2, // spec Layer 6.3: 0.2 for diff-summary (consistent output)
|
||||
});
|
||||
|
||||
return { summary: response.text.trim() };
|
||||
}
|
||||
|
||||
// ── Aggregate summary ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface AggregateSummaryInput {
|
||||
/** All changes in the diff as serialized JSON */
|
||||
changesJson: string;
|
||||
/** Human-readable artboard name, e.g. "Homepage Hero" */
|
||||
artboardName: string;
|
||||
/** Active DLF as JSON string — used to flag rule violations in the summary */
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
export interface AggregateSummaryOutput {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a 2-3 sentence summary of ALL changes across a single diff
|
||||
* (artboard-level granularity, suitable for the diff card header and PR body).
|
||||
* Uses buildAggregateSummaryMessages — the session-level counterpart of
|
||||
* buildDiffSummaryMessages (component-level).
|
||||
*/
|
||||
export async function generateAggregateSummary(
|
||||
gateway: AIGateway,
|
||||
input: AggregateSummaryInput,
|
||||
): Promise<AggregateSummaryOutput> {
|
||||
const { system, userContent, maxTokens } = buildAggregateSummaryMessages({
|
||||
changesJson: input.changesJson,
|
||||
artboardName: input.artboardName,
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens,
|
||||
temperature: 0.2, // same as per-component summary — deterministic aggregate
|
||||
});
|
||||
|
||||
return { summary: response.text.trim() };
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { AIGateway } from '../gateway.js';
|
||||
import { buildSystemPrompt } from '../prompts/system.js';
|
||||
|
||||
export interface DriftReportInput {
|
||||
/** Screenshot of the live app as base64 data URL (optional — text-only analysis if absent) */
|
||||
screenshotBase64?: string;
|
||||
/** Active Design Language File as JSON string (optional — generic advice if absent) */
|
||||
dlfJson?: string;
|
||||
/** Human-readable artboard metadata to give the model context (name, size, origin, etc.) */
|
||||
artboardContext?: string;
|
||||
}
|
||||
|
||||
export interface DriftViolation {
|
||||
component: string;
|
||||
property: string;
|
||||
currentValue: string;
|
||||
expectedValue: string;
|
||||
severity: 'critical' | 'warning';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface DriftReportOutput {
|
||||
violations: DriftViolation[];
|
||||
summary: string;
|
||||
violationCount: number;
|
||||
}
|
||||
|
||||
export async function generateDriftReport(
|
||||
gateway: AIGateway,
|
||||
input: DriftReportInput
|
||||
): Promise<DriftReportOutput> {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a design system compliance auditor',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
type ContentBlock =
|
||||
| { type: 'image'; source: { type: 'base64'; media_type: 'image/png'; data: string } }
|
||||
| { type: 'text'; text: string };
|
||||
|
||||
const userContent: ContentBlock[] = [];
|
||||
|
||||
if (input.screenshotBase64) {
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/png',
|
||||
data: input.screenshotBase64.replace(/^data:image\/\w+;base64,/, ''),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const analysisInstructions = input.screenshotBase64
|
||||
? 'Compare this screenshot against the design language file provided in your system context.'
|
||||
: `Analyse the following artboard for design system drift:\n\n${input.artboardContext ?? '(no artboard context provided)'}`;
|
||||
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text: `${analysisInstructions} Identify all design system drift violations.\n\nReturn a JSON object:\n{\n "violations": [{"component":"...", "property":"...", "currentValue":"...", "expectedValue":"...", "severity":"critical|warning", "description":"..."}],\n "summary": "...",\n "violationCount": N\n}\n\nRespond ONLY with valid JSON.`,
|
||||
});
|
||||
|
||||
const response = await gateway.complete({
|
||||
system,
|
||||
messages: [{ role: 'user', content: userContent }],
|
||||
maxTokens: 4096,
|
||||
});
|
||||
|
||||
// Returning empty violations on parse failure would be a false-negative in a
|
||||
// compliance feature. Throw so the caller can surface the error clearly.
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(response.text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim());
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Drift report returned unparseable JSON: ${String(err)}. ` +
|
||||
`Raw response (first 300 chars): ${response.text.slice(0, 300)}`
|
||||
);
|
||||
}
|
||||
|
||||
return parsed as DriftReportOutput;
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { getClient, MODEL } from './client.js';
|
||||
import { generateDiffSummary, generateAggregateSummary } from './features/diff-summary.js';
|
||||
import { fillCompletionZone } from './features/completion-zone.js';
|
||||
import { queryCrossArtboard } from './features/artboard-query.js';
|
||||
import { answerAgentQuestion } from './features/agent-qa.js';
|
||||
import type { DiffSummaryInput, DiffSummaryOutput, AggregateSummaryInput, AggregateSummaryOutput } from './features/diff-summary.js';
|
||||
import type { CompletionZoneInput, CompletionZoneOutput } from './features/completion-zone.js';
|
||||
import type { ArtboardQueryInput, ArtboardQueryOutput } from './features/artboard-query.js';
|
||||
import type { AgentQAInput, AgentQAOutput } from './features/agent-qa.js';
|
||||
|
||||
// ── Gateway config ────────────────────────────────────────────────────────────
|
||||
|
||||
interface GatewayConfig {
|
||||
/** Max requests per minute (default: 60) */
|
||||
rpmLimit?: number;
|
||||
/** Max retries on transient errors (default: 3) */
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
// ── Cost tracking ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface RequestCost {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheReadTokens: number;
|
||||
cacheWriteTokens: number;
|
||||
/** Estimated cost in USD cents */
|
||||
estimatedCentsCost: number;
|
||||
}
|
||||
|
||||
// Spec Layer 6: claude-sonnet-4-6 pricing (used for cost attribution logging)
|
||||
const SONNET_4_INPUT_COST_PER_M = 300; // $3.00 / 1M input tokens
|
||||
const SONNET_4_OUTPUT_COST_PER_M = 1500; // $15.00 / 1M output tokens
|
||||
const SONNET_4_CACHE_READ_PER_M = 30; // $0.30 / 1M cache-read tokens
|
||||
|
||||
function computeCost(usage: { input_tokens: number; output_tokens: number; cache_read_input_tokens?: number | null; cache_creation_input_tokens?: number | null }): RequestCost {
|
||||
const inputTokens = usage.input_tokens;
|
||||
const outputTokens = usage.output_tokens;
|
||||
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
||||
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
||||
|
||||
const billableInput = inputTokens - cacheReadTokens;
|
||||
const estimatedCentsCost = Math.round(
|
||||
(billableInput / 1_000_000) * SONNET_4_INPUT_COST_PER_M +
|
||||
(outputTokens / 1_000_000) * SONNET_4_OUTPUT_COST_PER_M +
|
||||
(cacheReadTokens / 1_000_000) * SONNET_4_CACHE_READ_PER_M
|
||||
);
|
||||
|
||||
return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, estimatedCentsCost };
|
||||
}
|
||||
|
||||
// ── Rate limiter ──────────────────────────────────────────────────────────────
|
||||
|
||||
class RateLimiter {
|
||||
private readonly rpm: number;
|
||||
private timestamps: number[] = [];
|
||||
|
||||
constructor(rpm: number) { this.rpm = rpm; }
|
||||
|
||||
async acquire(): Promise<void> {
|
||||
const now = Date.now();
|
||||
this.timestamps = this.timestamps.filter(t => now - t < 60_000);
|
||||
if (this.timestamps.length >= this.rpm) {
|
||||
const oldest = this.timestamps[0];
|
||||
if (oldest !== undefined) {
|
||||
const wait = 60_000 - (now - oldest);
|
||||
if (wait > 0) await sleep(wait);
|
||||
}
|
||||
}
|
||||
this.timestamps.push(Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// ── Gateway ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface GatewayRequest {
|
||||
messages: Anthropic.Messages.MessageParam[];
|
||||
system?: Anthropic.Messages.TextBlockParam[];
|
||||
maxTokens?: number;
|
||||
/**
|
||||
* Sampling temperature — spec Layer 6.3 per-prompt values:
|
||||
* diff-summary: 0.2 (consistent, low-creativity summaries)
|
||||
* completion-zone: 0.3 (slight creative latitude for UI generation)
|
||||
* agent-query: 0.1 (factual/authoritative answers)
|
||||
* Defaults to 0.2 if omitted. Range [0, 1].
|
||||
*/
|
||||
temperature?: number;
|
||||
/**
|
||||
* Workspace ID for per-workspace cost attribution (spec Layer 6.2-R2).
|
||||
* Logged on every request so cost can be aggregated per workspace.
|
||||
*/
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export interface GatewayResponse {
|
||||
content: Anthropic.Messages.ContentBlock[];
|
||||
text: string;
|
||||
cost: RequestCost;
|
||||
}
|
||||
|
||||
export class AIGateway {
|
||||
private readonly client: Anthropic;
|
||||
private readonly rateLimiter: RateLimiter;
|
||||
private readonly maxRetries: number;
|
||||
private totalCost = 0; // cumulative cents
|
||||
|
||||
constructor(config: GatewayConfig = {}) {
|
||||
this.client = getClient();
|
||||
this.rateLimiter = new RateLimiter(config.rpmLimit ?? 60);
|
||||
this.maxRetries = config.maxRetries ?? 3;
|
||||
}
|
||||
|
||||
async complete(req: GatewayRequest): Promise<GatewayResponse> {
|
||||
await this.rateLimiter.acquire();
|
||||
|
||||
const requestId = `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
const startMs = Date.now();
|
||||
|
||||
let lastError: Error | null = null;
|
||||
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: MODEL,
|
||||
max_tokens: req.maxTokens ?? 4096,
|
||||
temperature: req.temperature ?? 0.2,
|
||||
...(req.system !== undefined ? { system: req.system } : {}),
|
||||
messages: req.messages,
|
||||
});
|
||||
|
||||
const text = response.content
|
||||
.filter((b): b is Anthropic.Messages.TextBlock => b.type === 'text')
|
||||
.map(b => b.text)
|
||||
.join('');
|
||||
|
||||
const cost = computeCost(response.usage);
|
||||
const latencyMs = Date.now() - startMs;
|
||||
this.totalCost += cost.estimatedCentsCost;
|
||||
|
||||
// ── Structured request log (spec Layer 6.2-R4) ──────────────────────
|
||||
// Every AI call is logged with: requestId, model, token counts, cost
|
||||
// estimate, latency, and workspace ID for per-workspace attribution.
|
||||
console.log(JSON.stringify({
|
||||
level: 'info',
|
||||
event: 'ai_request',
|
||||
requestId,
|
||||
model: MODEL,
|
||||
workspaceId: req.workspaceId ?? null,
|
||||
inputTokens: cost.inputTokens,
|
||||
outputTokens: cost.outputTokens,
|
||||
cacheReadTokens: cost.cacheReadTokens,
|
||||
cacheWriteTokens: cost.cacheWriteTokens,
|
||||
estimatedCents: cost.estimatedCentsCost,
|
||||
latencyMs,
|
||||
attempt,
|
||||
}));
|
||||
|
||||
return { content: response.content, text, cost };
|
||||
} catch (err) {
|
||||
lastError = err instanceof Error ? err : new Error(String(err));
|
||||
const latencyMs = Date.now() - startMs;
|
||||
|
||||
// Log every failed attempt for observability
|
||||
console.error(JSON.stringify({
|
||||
level: 'error',
|
||||
event: 'ai_request_error',
|
||||
requestId,
|
||||
model: MODEL,
|
||||
workspaceId: req.workspaceId ?? null,
|
||||
attempt,
|
||||
latencyMs,
|
||||
error: lastError.message,
|
||||
}));
|
||||
|
||||
// Retry on 429, 529, 5xx, and network errors (no HTTP status).
|
||||
// Break immediately on client errors (4xx that aren't rate-limits).
|
||||
if (err instanceof Anthropic.APIError) {
|
||||
const { status } = err;
|
||||
if (status !== 429 && status !== 529 && status < 500) break;
|
||||
}
|
||||
// Non-APIError (network timeout, DNS failure, etc.) → always retry.
|
||||
// Cap at 30s so a long retry sequence doesn't wedge the server forever.
|
||||
const delayMs = Math.min(2 ** attempt * 1000, 30_000);
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new Error('AI gateway request failed');
|
||||
}
|
||||
|
||||
/** Cumulative estimated cost in cents since this gateway was instantiated. */
|
||||
getCumulativeCost(): number {
|
||||
return this.totalCost;
|
||||
}
|
||||
|
||||
// ── Spec Layer 5 convenience methods ─────────────────────────────────────────
|
||||
// Thin wrappers that delegate to the standalone feature functions, making the
|
||||
// gateway usable as a single dependency injection point across the app layer.
|
||||
|
||||
generateDiffSummary(input: DiffSummaryInput): Promise<DiffSummaryOutput> {
|
||||
return generateDiffSummary(this, input);
|
||||
}
|
||||
|
||||
generateAggregateSummary(input: AggregateSummaryInput): Promise<AggregateSummaryOutput> {
|
||||
return generateAggregateSummary(this, input);
|
||||
}
|
||||
|
||||
fillCompletionZone(input: CompletionZoneInput): Promise<CompletionZoneOutput> {
|
||||
return fillCompletionZone(this, input);
|
||||
}
|
||||
|
||||
queryArtboards(input: ArtboardQueryInput): Promise<ArtboardQueryOutput> {
|
||||
return queryCrossArtboard(this, input);
|
||||
}
|
||||
|
||||
answerAgentQuery(input: AgentQAInput): Promise<AgentQAOutput> {
|
||||
return answerAgentQuestion(this, input);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export { getClient, MODEL } from './client.js';
|
||||
export { AIGateway } from './gateway.js';
|
||||
export type { GatewayRequest, GatewayResponse, RequestCost } from './gateway.js';
|
||||
|
||||
// ── Versioned prompt builders ─────────────────────────────────────────────────
|
||||
export { DIFF_SUMMARY_PROMPT_VERSION, buildDiffSummaryMessages, buildAggregateSummaryMessages } from './prompts/diff-summary.prompt.js';
|
||||
export type { DiffSummaryPromptInput, AggregateSummaryPromptInput } from './prompts/diff-summary.prompt.js';
|
||||
|
||||
export { COMPLETION_ZONE_PROMPT_VERSION, buildCompletionZoneMessages } from './prompts/completion-zone.prompt.js';
|
||||
export type { CompletionZonePromptInput } from './prompts/completion-zone.prompt.js';
|
||||
|
||||
export { ARTBOARD_QUERY_PROMPT_VERSION, buildArtboardQueryMessages } from './prompts/artboard-query.prompt.js';
|
||||
export type { ArtboardQueryPromptInput } from './prompts/artboard-query.prompt.js';
|
||||
|
||||
export { AGENT_QUERY_PROMPT_VERSION, buildAgentQueryMessages } from './prompts/agent-query.prompt.js';
|
||||
export type { AgentQueryPromptInput } from './prompts/agent-query.prompt.js';
|
||||
|
||||
export { fillCompletionZone } from './features/completion-zone.js';
|
||||
export type { CompletionZoneInput, CompletionZoneOutput } from './features/completion-zone.js';
|
||||
|
||||
export { generateDiffSummary, generateAggregateSummary } from './features/diff-summary.js';
|
||||
export type { DiffSummaryInput, DiffSummaryOutput, AggregateSummaryInput, AggregateSummaryOutput } from './features/diff-summary.js';
|
||||
|
||||
export { queryCrossArtboard } from './features/artboard-query.js';
|
||||
export type { ArtboardQueryInput, ArtboardQueryOutput, ArtboardQueryResult } from './features/artboard-query.js';
|
||||
|
||||
export { generateDriftReport } from './features/drift-report.js';
|
||||
export type { DriftReportInput, DriftReportOutput, DriftViolation } from './features/drift-report.js';
|
||||
|
||||
export { answerAgentQuestion } from './features/agent-qa.js';
|
||||
export type { AgentQAInput, AgentQAOutput } from './features/agent-qa.js';
|
||||
@@ -1,99 +0,0 @@
|
||||
// ── Agent Query Prompt ────────────────────────────────────────────────────────
|
||||
// Versioned prompt for answering coding-agent questions about design diffs.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 1024 — JSON with answer + optional visual reference
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) + optional DLF constraint block (cached)
|
||||
// User: [before screenshot] + [after screenshot] + artboard context +
|
||||
// diff ID + agent question (spec Layer 6.3-R3: screenshots required)
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const AGENT_QUERY_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface AgentQueryPromptInput {
|
||||
/** Question from the coding agent (e.g. Cursor or Claude Code) */
|
||||
question: string;
|
||||
/** The diff ID this question is about */
|
||||
diffId: string;
|
||||
/** Full artboard context (IntentDiff + artboard metadata) as JSON */
|
||||
artboardContextJson: string;
|
||||
/** Active DLF as JSON string (optional — cached in system when present) */
|
||||
dlfJson?: string;
|
||||
/**
|
||||
* Before-state screenshot as base64 data URL (spec Layer 6.3-R3).
|
||||
* Gives the model visual ground truth of what the artboard looked like
|
||||
* before the diff was applied.
|
||||
*/
|
||||
beforeScreenshotBase64?: string;
|
||||
/**
|
||||
* After-state screenshot as base64 data URL (spec Layer 6.3-R3).
|
||||
* Gives the model the proposed post-diff visual state.
|
||||
*/
|
||||
afterScreenshotBase64?: string;
|
||||
}
|
||||
|
||||
function stripDataUrl(dataUrl: string): string {
|
||||
return dataUrl.replace(/^data:image\/\w+;base64,/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user content blocks for a coding-agent design question.
|
||||
* When screenshots are provided they precede the text context so the model can
|
||||
* visually ground its answer before reading the structured JSON.
|
||||
* Max 1024 tokens — the answer must be precise and actionable.
|
||||
*/
|
||||
export function buildAgentQueryMessages(input: AgentQueryPromptInput): {
|
||||
system: Anthropic.Messages.TextBlockParam[];
|
||||
userContent: Anthropic.Messages.ContentBlockParam[];
|
||||
maxTokens: number;
|
||||
} {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a design agent answering questions from a coding agent implementing a design diff',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const userContent: Anthropic.Messages.ContentBlockParam[] = [];
|
||||
|
||||
// Screenshots first — visual context before text, per multimodal best practice.
|
||||
if (input.beforeScreenshotBase64) {
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text: 'Before state (artboard before this diff was applied):',
|
||||
});
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: stripDataUrl(input.beforeScreenshotBase64) },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
if (input.afterScreenshotBase64) {
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text: 'After state (proposed artboard after this diff is applied):',
|
||||
});
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: stripDataUrl(input.afterScreenshotBase64) },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
userContent.push({
|
||||
type: 'text',
|
||||
text:
|
||||
`<artboard_context>\n${input.artboardContextJson}\n</artboard_context>\n\n` +
|
||||
`<diff_id>${input.diffId}</diff_id>\n\n` +
|
||||
`<question>\n${input.question}\n</question>\n\n` +
|
||||
`Answer the coding agent's question concisely and precisely. ` +
|
||||
`If the answer references a specific design token, component, or visual region, ` +
|
||||
`include a "visualReference" field. Return JSON:\n` +
|
||||
`{\n "answer": "...",\n "visualReference": "..." (optional)\n}\n\n` +
|
||||
`Respond ONLY with valid JSON.`,
|
||||
});
|
||||
|
||||
return { system, userContent, maxTokens: 1024 };
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// ── Artboard Query Prompt ─────────────────────────────────────────────────────
|
||||
// Versioned prompt for cross-artboard design intent search.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 1024 — output is a JSON results array ranked by relevance
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) — no DLF needed for cross-artboard search
|
||||
// User: artboard metadata array + query string (not cached — varies per call)
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const ARTBOARD_QUERY_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface ArtboardQueryPromptInput {
|
||||
/** Natural language query from the user */
|
||||
query: string;
|
||||
/** Array of artboard metadata objects serialized as JSON */
|
||||
artboardsJson: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user message pair for a cross-artboard relevance search.
|
||||
* Only artboards with relevanceScore > 0.3 should be returned by the model.
|
||||
*/
|
||||
export function buildArtboardQueryMessages(input: ArtboardQueryPromptInput): {
|
||||
system: Anthropic.Messages.TextBlockParam[];
|
||||
userContent: string;
|
||||
maxTokens: number;
|
||||
} {
|
||||
const system = buildSystemPrompt({ role: 'a search agent filtering artboards by design intent' });
|
||||
|
||||
const userContent =
|
||||
`<artboards>\n${input.artboardsJson}\n</artboards>\n\n` +
|
||||
`<query>\n${input.query}\n</query>\n\n` +
|
||||
`Return a JSON object:\n` +
|
||||
`{\n` +
|
||||
` "results": [{"artboardId": "...", "relevanceScore": 0-1, "reason": "..."}],\n` +
|
||||
` "reasoning": "brief explanation"\n` +
|
||||
`}\n\n` +
|
||||
`Only include artboards with relevanceScore > 0.3. Respond ONLY with valid JSON.`;
|
||||
|
||||
return { system, userContent, maxTokens: 1024 };
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// ── Completion Zone Prompt ────────────────────────────────────────────────────
|
||||
// Versioned prompt for filling AI Completion Zones.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 4096 — output is a structured JSON tree
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) + DLF as a hard constraint block (cached)
|
||||
// User: component tree context + optional screenshot + zone intent string
|
||||
//
|
||||
// The DLF is placed first (before the component tree) so it is cached across
|
||||
// all completion zone calls in the same workspace session. The model must treat
|
||||
// the DLF as a hard constraint: any proposed component must exist in the DLF's
|
||||
// component list, and all props must pass the DLF's allowedProps rules.
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const COMPLETION_ZONE_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface CompletionZonePromptInput {
|
||||
componentTreeJson: string;
|
||||
intent: string;
|
||||
dlfJson?: string;
|
||||
screenshotBase64?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full message array for a completion zone generation request.
|
||||
* Returns a multi-part user message that includes an optional image block.
|
||||
*/
|
||||
export function buildCompletionZoneMessages(input: CompletionZonePromptInput): {
|
||||
system: Anthropic.Messages.TextBlockParam[];
|
||||
userContent: Anthropic.Messages.ContentBlockParam[];
|
||||
maxTokens: number;
|
||||
} {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a Completion Zone design agent that generates UI component trees to fill empty design regions',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const textBlock: Anthropic.Messages.TextBlockParam = {
|
||||
type: 'text',
|
||||
text:
|
||||
`<component_tree>\n${input.componentTreeJson}\n</component_tree>\n\n` +
|
||||
`<intent>\n${input.intent}\n</intent>\n\n` +
|
||||
(input.dlfJson
|
||||
? `The DLF in the system prompt is a HARD CONSTRAINT. Only use components and prop values defined there.\n\n`
|
||||
: '') +
|
||||
`Return a JSON object with exactly two fields:\n` +
|
||||
`- "proposedTree": the updated component tree matching the intent (same shape as the input tree)\n` +
|
||||
`- "description": one sentence describing what was generated and why\n\n` +
|
||||
`Respond ONLY with valid JSON. No markdown, no prose outside the JSON.`,
|
||||
};
|
||||
|
||||
const userContent: Anthropic.Messages.ContentBlockParam[] = [];
|
||||
|
||||
// Screenshot goes first if provided (visual context before textual context).
|
||||
if (input.screenshotBase64) {
|
||||
const base64Data = input.screenshotBase64.replace(/^data:image\/\w+;base64,/, '');
|
||||
userContent.push({
|
||||
type: 'image',
|
||||
source: { type: 'base64', media_type: 'image/png', data: base64Data },
|
||||
} as Anthropic.Messages.ImageBlockParam);
|
||||
}
|
||||
|
||||
userContent.push(textBlock);
|
||||
|
||||
return { system, userContent, maxTokens: 4096 };
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
// ── Diff Summary Prompt ───────────────────────────────────────────────────────
|
||||
// Versioned prompt for generating human-readable summaries of ComponentChange
|
||||
// records and aggregate IntentDiff records.
|
||||
//
|
||||
// Version: 1.0
|
||||
// Model: claude-sonnet-4-6 (spec Layer 6: "Claude Sonnet 4 API is the only AI provider")
|
||||
// Tokens: max 300 per change summary, 500 for aggregate summary
|
||||
//
|
||||
// Structure:
|
||||
// System: stable role (cached) + optional DLF constraint block (cached)
|
||||
// User: component changes + output instruction (not cached — varies per call)
|
||||
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import { buildSystemPrompt } from './system.js';
|
||||
|
||||
export const DIFF_SUMMARY_PROMPT_VERSION = '1.0';
|
||||
|
||||
export interface DiffSummaryPromptInput {
|
||||
changesJson: string;
|
||||
componentName: string;
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
export interface AggregateSummaryPromptInput {
|
||||
changesJson: string;
|
||||
artboardName: string;
|
||||
dlfJson?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user message pair for a single-component change summary.
|
||||
* Max 300 tokens — one tight sentence for PR review readability.
|
||||
*/
|
||||
export function buildDiffSummaryMessages(
|
||||
input: DiffSummaryPromptInput,
|
||||
): { system: Anthropic.Messages.TextBlockParam[]; userContent: string; maxTokens: number } {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a technical writer summarizing UI component changes for pull request reviewers',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const userContent =
|
||||
`Summarize the following changes to "${input.componentName}" in one concise sentence ` +
|
||||
`(max 20 words). Focus on the user-visible impact, not technical prop names. ` +
|
||||
`If the change violates a design system rule, note it.\n\n` +
|
||||
`<changes>\n${input.changesJson}\n</changes>\n\n` +
|
||||
`Respond with ONLY the summary sentence.`;
|
||||
|
||||
return { system, userContent, maxTokens: 300 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system + user message pair for an aggregate IntentDiff summary.
|
||||
* Max 500 tokens — a short paragraph describing all changes together.
|
||||
*/
|
||||
export function buildAggregateSummaryMessages(
|
||||
input: AggregateSummaryPromptInput,
|
||||
): { system: Anthropic.Messages.TextBlockParam[]; userContent: string; maxTokens: number } {
|
||||
const system = buildSystemPrompt({
|
||||
role: 'a technical writer summarizing a design session for engineering handoff',
|
||||
...(input.dlfJson !== undefined ? { dlfJson: input.dlfJson } : {}),
|
||||
});
|
||||
|
||||
const userContent =
|
||||
`Write a 2-3 sentence summary of the following design changes made to the "${input.artboardName}" artboard. ` +
|
||||
`Describe the combined intent — what changed and why — in terms a developer reading a PR can understand. ` +
|
||||
`Do not list individual prop changes; synthesize them.\n\n` +
|
||||
`<changes>\n${input.changesJson}\n</changes>\n\n` +
|
||||
`Respond with ONLY the summary paragraph.`;
|
||||
|
||||
return { system, userContent, maxTokens: 500 };
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
// ── System prompt builder ─────────────────────────────────────────────────────
|
||||
// The DLF is placed as the FIRST cache breakpoint — it's stable across all
|
||||
// requests in a workspace session, so cache hit rate is very high.
|
||||
// Variable content (artboard context, user question) comes AFTER the breakpoints.
|
||||
|
||||
export function buildSystemPrompt(opts: {
|
||||
role: string;
|
||||
dlfJson?: string;
|
||||
}): Anthropic.Messages.TextBlockParam[] {
|
||||
const blocks: Anthropic.Messages.TextBlockParam[] = [];
|
||||
|
||||
// First block: stable role description — cached
|
||||
blocks.push({
|
||||
type: 'text',
|
||||
text: `You are ${opts.role}. You work within Originmain, an AI-native design engineering platform. Your outputs are used directly by designers and engineers to implement product changes. Be precise, structured, and always respect the design system constraints provided.`,
|
||||
cache_control: { type: 'ephemeral' },
|
||||
});
|
||||
|
||||
// Second block: DLF if provided — cached (stable per workspace)
|
||||
if (opts.dlfJson) {
|
||||
blocks.push({
|
||||
type: 'text',
|
||||
text: `<design_language_file>\n${opts.dlfJson}\n</design_language_file>`,
|
||||
cache_control: { type: 'ephemeral' },
|
||||
});
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
# ── Clerk (authentication) ────────────────────────────────────────────────────
|
||||
# https://dashboard.clerk.com → API Keys
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||
CLERK_SECRET_KEY=sk_test_...
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||
# Force redirect after OAuth (GitHub, Google) — overrides any redirect_url param
|
||||
# "Force" always wins; "Fallback" only fires when no redirect_url param exists.
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL=/workspaces
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/onboarding
|
||||
|
||||
# ── Supabase ──────────────────────────────────────────────────────────────────
|
||||
# https://supabase.com/dashboard → Project Settings → API
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://<project-ref>.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
||||
|
||||
# ── Anthropic ─────────────────────────────────────────────────────────────────
|
||||
# https://console.anthropic.com/keys
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# ── Agent Bridge ─────────────────────────────────────────────────────────────
|
||||
# Random secret used to sign workspace tokens issued to Cursor / Claude Code.
|
||||
# Generate with: openssl rand -hex 32
|
||||
AGENT_BRIDGE_SECRET=
|
||||
|
||||
# ── Webhook signing secrets (optional — skip verification in dev if absent) ──
|
||||
# Linear: Settings → API → Webhooks → signing secret
|
||||
LINEAR_WEBHOOK_SECRET=
|
||||
# Slack: App → Basic Information → Signing Secret
|
||||
SLACK_SIGNING_SECRET=
|
||||
# GitHub: Repository → Settings → Webhooks → secret
|
||||
GITHUB_WEBHOOK_SECRET=
|
||||
# Intercom: Developer Hub → Webhooks → client secret
|
||||
INTERCOM_WEBHOOK_SECRET=
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: [
|
||||
'@originmain/agent-bridge',
|
||||
'@originmain/ai-layer',
|
||||
'@originmain/design-language',
|
||||
'@originmain/diff-engine',
|
||||
'@originmain/integrations',
|
||||
'@originmain/multiplayer',
|
||||
'@originmain/origin-graph',
|
||||
'@originmain/platform',
|
||||
'@originmain/renderer',
|
||||
'@originmain/ui',
|
||||
'@fluentui/react-components',
|
||||
],
|
||||
experimental: {
|
||||
optimizePackageImports: ['@fluentui/react-components'],
|
||||
},
|
||||
webpack(config) {
|
||||
config.resolve.extensionAlias = {
|
||||
'.js': ['.ts', '.tsx', '.js'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
};
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
// afterFiles runs after the filesystem check — static HTML files in
|
||||
// public/features/ are served when the clean URL is requested
|
||||
// (e.g. /features/live-artboard → /features/live-artboard.html)
|
||||
afterFiles: [
|
||||
{ source: '/features/:slug', destination: '/features/:slug.html' },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "@originmain/app",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.56.0",
|
||||
"@clerk/nextjs": "^7.2.7",
|
||||
"@fluentui/react-components": "^9.54.0",
|
||||
"@fluentui/react-icons": "^2.0.0",
|
||||
"@originmain/agent-bridge": "workspace:*",
|
||||
"@originmain/ai-layer": "workspace:*",
|
||||
"@originmain/design-language": "workspace:*",
|
||||
"@originmain/diff-engine": "workspace:*",
|
||||
"@originmain/integrations": "workspace:*",
|
||||
"@originmain/multiplayer": "workspace:*",
|
||||
"@originmain/origin-graph": "workspace:*",
|
||||
"@originmain/platform": "workspace:*",
|
||||
"@originmain/renderer": "workspace:*",
|
||||
"@originmain/ui": "workspace:*",
|
||||
"@pierre/diffs": "^1.1.19",
|
||||
"@pierre/trees": "1.0.0-beta.3",
|
||||
"@supabase/supabase-js": "^2.0.0",
|
||||
"@tanstack/react-query": "^5.62.0",
|
||||
"@tanstack/react-virtual": "^3.13.24",
|
||||
"@trpc/client": "^11.17.0",
|
||||
"@trpc/react-query": "^11.17.0",
|
||||
"@trpc/server": "^11.17.0",
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"zod": "^3.25.76",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
@@ -1,869 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agent Bridge — Originmain</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#FFFFFF;--bg-subtle:#FAFAFA;--bg-muted:#F4F4F5;--bg-strong:#EBEBEB;--bg-inv:#0A0A0A;--bg-inv-2:#141414;--bg-inv-3:#1C1C1C;--fg:#0A0A0A;--fg-2:#52525B;--fg-3:#A1A1AA;--fg-4:#D4D4D8;--fg-inv:rgba(255,255,255,0.92);--fg-inv-2:rgba(255,255,255,0.52);--fg-inv-3:rgba(255,255,255,0.28);--blue:#0066FF;--blue-dark:#0050CC;--blue-light:#3385FF;--blue-soft:rgba(0,102,255,0.07);--blue-mid:rgba(0,102,255,0.15);--purple:#7C3AED;--teal:#0891B2;--amber:#F59E0B;--amber-dark:#D97706;--amber-soft:rgba(245,158,11,0.07);--amber-mid:rgba(245,158,11,0.18);--border:rgba(0,0,0,0.07);--border-2:rgba(0,0,0,0.12);--border-inv:rgba(255,255,255,0.08);--border-inv-2:rgba(255,255,255,0.14);--r-1:4px;--r-2:8px;--r-3:12px;--r-4:16px;--r-5:20px;--r-6:24px;--r-full:9999px;--sh-xs:0 1px 2px rgba(0,0,0,0.05);--sh-sm:0 1px 3px rgba(0,0,0,0.07),0 1px 2px rgba(0,0,0,0.05);--sh-md:0 4px 8px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04);--sh-lg:0 10px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.05);--sh-xl:0 20px 40px rgba(0,0,0,0.09),0 8px 16px rgba(0,0,0,0.05);--sh-2xl:0 32px 64px rgba(0,0,0,0.11),0 16px 32px rgba(0,0,0,0.06);--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--mono:'JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||||
html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.6;overflow-x:hidden;}
|
||||
img,svg{display:block;max-width:100%;}
|
||||
a{color:inherit;text-decoration:none;}
|
||||
button{font-family:inherit;cursor:pointer;border:none;background:none;}
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:0 40px;}
|
||||
.section{padding:112px 0;}
|
||||
.section-muted{background:var(--bg-subtle);}
|
||||
.section-dark{background:var(--bg-inv);}
|
||||
.d1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;line-height:1.02;letter-spacing:-0.045em;color:var(--fg);}
|
||||
.d2{font-size:clamp(1.875rem,3.5vw,2.75rem);font-weight:800;line-height:1.1;letter-spacing:-0.038em;color:var(--fg);}
|
||||
.d3{font-size:clamp(1.25rem,2.5vw,1.625rem);font-weight:700;line-height:1.25;letter-spacing:-0.028em;color:var(--fg);}
|
||||
.subtitle{font-size:clamp(1rem,1.8vw,1.125rem);font-weight:400;line-height:1.75;color:var(--fg-2);}
|
||||
.label{font-family:var(--mono);font-size:0.6875rem;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--fg-3);}
|
||||
.label-amber{color:var(--amber);}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font);font-size:0.9375rem;font-weight:600;padding:11px 22px;border-radius:var(--r-2);transition:transform 0.12s cubic-bezier(.16,1,.3,1),opacity 0.12s,box-shadow 0.12s;white-space:nowrap;letter-spacing:-0.01em;position:relative;overflow:hidden;}
|
||||
.btn::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,0);transition:background 0.12s;}
|
||||
.btn:hover::after{background:rgba(255,255,255,0.06);}
|
||||
.btn:active{transform:scale(0.98)!important;}
|
||||
.btn-primary{background:var(--fg);color:var(--bg);box-shadow:var(--sh-sm);}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:var(--sh-md);}
|
||||
.btn-secondary{background:transparent;color:var(--fg);border:1px solid var(--border-2);}
|
||||
.btn-secondary:hover{background:var(--bg-muted);transform:translateY(-1px);}
|
||||
.btn-amber{background:var(--amber);color:#fff;box-shadow:0 4px 12px rgba(245,158,11,0.35);}
|
||||
.btn-amber:hover{background:var(--amber-dark);transform:translateY(-1px);}
|
||||
.btn-lg{font-size:1rem;padding:13px 28px;}
|
||||
.btn-white{background:#fff;color:#0A0A0A;box-shadow:0 4px 12px rgba(255,255,255,0.15);}
|
||||
.btn-white:hover{background:#F5F5F5;transform:translateY(-1px);}
|
||||
.btn-outline-white{background:transparent;color:rgba(255,255,255,0.72);border:1px solid rgba(255,255,255,0.18);}
|
||||
.btn-outline-white:hover{background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.9);transform:translateY(-1px);}
|
||||
.divider{height:1px;background:var(--border);}
|
||||
.section-head{text-align:center;max-width:640px;margin:0 auto 72px;}
|
||||
.section-head .label{display:block;margin-bottom:16px;}
|
||||
.section-head .d2{margin-bottom:18px;}
|
||||
|
||||
/* NAV */
|
||||
#nav{position:fixed;top:0;left:0;right:0;z-index:200;height:60px;display:flex;align-items:center;padding:0 40px;transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;}
|
||||
#nav.scrolled{background:rgba(255,255,255,0.85);backdrop-filter:blur(20px) saturate(1.6);-webkit-backdrop-filter:blur(20px) saturate(1.6);border-bottom:1px solid var(--border);box-shadow:var(--sh-xs);}
|
||||
.nav-inner{max-width:1120px;margin:0 auto;width:100%;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
|
||||
.nav-logo{justify-self:start;font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:var(--fg);display:flex;align-items:center;gap:2px;}
|
||||
.nav-logo span{color:var(--blue);}
|
||||
.nav-links{justify-self:center;display:flex;align-items:center;gap:0;list-style:none;}
|
||||
.nav-links a{font-size:0.9375rem;font-weight:500;color:var(--fg-2);padding:6px 14px;border-radius:var(--r-1);transition:color 0.12s,background 0.12s;letter-spacing:-0.01em;}
|
||||
.nav-links a:hover{color:var(--fg);}
|
||||
.nav-actions{justify-self:end;display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* Dropdown */
|
||||
.nav-dropdown{position:relative;}
|
||||
.nav-dropdown>a{display:flex;align-items:center;gap:4px;}
|
||||
.nav-dd-caret{transition:transform 0.15s;}
|
||||
/* caret rotation handled via JS .open class */
|
||||
.nav-dropdown-menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-4px);background:rgba(255,255,255,0.98);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--r-3);box-shadow:var(--sh-xl);padding:8px;min-width:240px;opacity:0;visibility:hidden;transition:opacity 0.15s,transform 0.15s,visibility 0.15s;pointer-events:none;}
|
||||
.nav-dropdown-menu.open{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:all;}
|
||||
.nav-dropdown.open .nav-dd-caret{transform:rotate(180deg);}
|
||||
/* Mobile sub-nav */
|
||||
.nav-mobile-sub{display:none;padding:2px 0 6px 14px;flex-direction:column;}
|
||||
.nav-mobile-sub.open{display:flex;}
|
||||
.nav-mobile-sub a{font-size:0.875rem;color:rgba(255,255,255,0.4);padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.06);letter-spacing:-0.01em;transition:color 0.1s;}
|
||||
.nav-mobile-sub a:last-child{border-bottom:none;}
|
||||
.nav-mobile-sub a:hover{color:rgba(255,255,255,0.8);}
|
||||
/* Dark hero: white nav before scrolling */
|
||||
#nav:not(.scrolled) .nav-logo{color:rgba(255,255,255,0.92);}
|
||||
#nav:not(.scrolled) .nav-logo span{color:#3385FF;}
|
||||
#nav:not(.scrolled) .nav-links a{color:rgba(255,255,255,0.5);}
|
||||
#nav:not(.scrolled) .nav-links a:hover{color:rgba(255,255,255,0.9);}
|
||||
#nav:not(.scrolled) .nav-actions .btn-secondary{color:rgba(255,255,255,0.65);border-color:rgba(255,255,255,0.2);background:transparent;}
|
||||
#nav:not(.scrolled) .nav-burger span{background:rgba(255,255,255,0.8);}
|
||||
.nav-dd-item{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:var(--r-2);transition:background 0.1s;cursor:pointer;}
|
||||
.nav-dd-item:hover{background:var(--bg-muted);}
|
||||
.nav-dd-item.active{background:var(--amber-soft);}
|
||||
.nav-dd-icon{width:30px;height:30px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:13px;color:var(--blue);}
|
||||
.nav-dd-icon.amber{background:var(--amber-soft);border-color:var(--amber-mid);}
|
||||
.nav-dd-text{display:flex;flex-direction:column;}
|
||||
.nav-dd-name{font-size:0.8125rem;font-weight:600;color:var(--fg);letter-spacing:-0.01em;}
|
||||
.nav-dd-desc{font-size:0.6875rem;color:var(--fg-3);margin-top:1px;line-height:1.4;}
|
||||
.nav-dd-sep{height:1px;background:var(--border);margin:4px 0;}
|
||||
.nav-burger{display:none;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:36px;height:36px;background:none;border:none;cursor:pointer;padding:4px;border-radius:var(--r-2);}
|
||||
.nav-burger span{display:block;width:20px;height:2px;background:var(--fg);border-radius:2px;transition:transform 0.22s cubic-bezier(0.16,1,0.3,1),opacity 0.18s;transform-origin:center;}
|
||||
.nav-burger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
|
||||
.nav-burger.open span:nth-child(2){opacity:0;transform:scaleX(0);}
|
||||
.nav-burger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
|
||||
.nav-mobile{display:none;position:absolute;top:60px;left:0;right:0;background:rgba(255,255,255,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,0.07);padding:12px 24px 24px;flex-direction:column;gap:2px;}
|
||||
.nav-mobile.open{display:flex;}
|
||||
.nav-mobile ul{list-style:none;display:flex;flex-direction:column;}
|
||||
.nav-mobile ul li a{display:block;font-size:1rem;font-weight:500;color:var(--fg-2);padding:11px 4px;border-bottom:1px solid var(--border);letter-spacing:-0.01em;}
|
||||
.nav-mobile ul li:last-child a{border-bottom:none;}
|
||||
.nav-mobile-ctas{display:flex;gap:8px;margin-top:16px;}
|
||||
.nav-mobile-ctas .btn{flex:1;justify-content:center;padding:10px 12px;font-size:.875rem;}
|
||||
|
||||
/* PAGE HERO */
|
||||
#page-hero{padding:140px 40px 100px;background:var(--bg-inv);position:relative;overflow:hidden;text-align:center;}
|
||||
#page-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(245,158,11,0.1),transparent 60%),radial-gradient(ellipse 40% 40% at 80% 70%,rgba(124,58,237,0.07),transparent 50%);pointer-events:none;}
|
||||
.page-hero-inner{position:relative;max-width:800px;margin:0 auto;}
|
||||
.page-hero-inner .label{color:rgba(255,255,255,0.3);margin-bottom:20px;display:block;}
|
||||
.page-hero-inner .d1{color:var(--fg-inv);margin-bottom:22px;}
|
||||
.page-hero-inner .subtitle{color:var(--fg-inv-2);max-width:620px;margin:0 auto 44px;}
|
||||
.page-hero-ctas{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* PRODUCT VISUAL */
|
||||
.pv-shell{width:100%;max-width:960px;margin:72px auto 0;border-radius:16px 16px 0 0;overflow:hidden;box-shadow:0 48px 96px rgba(0,0,0,0.45),0 0 0 1px rgba(255,255,255,0.06);border-bottom:none;position:relative;}
|
||||
.pv-chrome{background:#1C1C22;height:40px;display:flex;align-items:center;padding:0 14px;gap:7px;border-bottom:1px solid rgba(255,255,255,0.05);}
|
||||
.pv-dot{width:11px;height:11px;border-radius:50%;}
|
||||
.pv-dot.r{background:#FF5F57;}
|
||||
.pv-dot.y{background:#FFBD2E;}
|
||||
.pv-dot.g{background:#28C840;}
|
||||
.pv-url{flex:1;margin:0 12px;background:rgba(255,255,255,0.06);border-radius:5px;height:25px;display:flex;align-items:center;padding:0 10px;font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.3);max-width:360px;}
|
||||
.pv-canvas{background:#0D0D11;height:400px;position:relative;display:flex;align-items:stretch;}
|
||||
.pv-grid{position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,0.04) 1px,transparent 1px);background-size:24px 24px;pointer-events:none;}
|
||||
|
||||
/* Bridge split panels */
|
||||
.bridge-panel-left{flex:1;border-right:1px solid rgba(255,255,255,0.07);display:flex;flex-direction:column;overflow:hidden;position:relative;}
|
||||
.bridge-panel-right{flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative;}
|
||||
.bridge-panel-header{padding:8px 14px;border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;gap:8px;background:rgba(255,255,255,0.02);}
|
||||
.bridge-panel-title{font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.28);letter-spacing:0.08em;text-transform:uppercase;}
|
||||
.bridge-panel-badge{font-family:var(--mono);font-size:0.5rem;letter-spacing:0.06em;text-transform:uppercase;padding:2px 6px;border-radius:var(--r-full);}
|
||||
.bridge-panel-badge.cursor{background:rgba(124,58,237,0.18);color:#C4A1FF;border:1px solid rgba(124,58,237,0.25);}
|
||||
.bridge-panel-badge.canvas{background:rgba(245,158,11,0.14);color:#FCD34D;border:1px solid rgba(245,158,11,0.22);}
|
||||
.bridge-panel-body{flex:1;padding:16px;overflow:hidden;}
|
||||
|
||||
/* Bridge center connector */
|
||||
.bridge-connector{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:10;display:flex;flex-direction:column;align-items:center;gap:0;}
|
||||
.bridge-icon{width:40px;height:40px;border-radius:50%;background:#0A0A0A;border:1px solid rgba(245,158,11,0.4);display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 0 0 4px rgba(245,158,11,0.08),0 0 20px rgba(245,158,11,0.15);position:relative;z-index:2;}
|
||||
.bridge-rings{position:absolute;width:40px;height:40px;top:28px;}
|
||||
.bridge-pulse{position:absolute;inset:0;border-radius:50%;border:1px solid rgba(245,158,11,0.3);animation:bridge-ring 2s ease-out infinite;}
|
||||
.bridge-pulse-2{animation-delay:0.7s;}
|
||||
@keyframes bridge-ring{0%{transform:scale(1);opacity:0.6;}100%{transform:scale(2.4);opacity:0;}}
|
||||
.bridge-line-top{width:1px;height:22px;background:linear-gradient(to top,rgba(245,158,11,0.55),transparent);position:relative;margin-bottom:2px;}
|
||||
.bridge-line-top::before{content:'';position:absolute;top:-1px;left:-3px;border-left:3.5px solid transparent;border-right:3.5px solid transparent;border-bottom:5px solid rgba(245,158,11,0.55);}
|
||||
.bridge-line-bottom{width:1px;height:22px;background:linear-gradient(to bottom,rgba(245,158,11,0.55),transparent);position:relative;margin-top:2px;}
|
||||
.bridge-line-bottom::after{content:'';position:absolute;bottom:-1px;left:-3px;border-left:3.5px solid transparent;border-right:3.5px solid transparent;border-top:5px solid rgba(245,158,11,0.55);}
|
||||
|
||||
/* Editor simulation */
|
||||
.editor-line{display:flex;align-items:baseline;gap:10px;font-family:var(--mono);font-size:0.5875rem;line-height:1.9;white-space:nowrap;}
|
||||
.editor-ln{color:rgba(255,255,255,0.15);min-width:18px;text-align:right;user-select:none;flex-shrink:0;}
|
||||
.editor-code .kw{color:#C4A1FF;}
|
||||
.editor-code .str{color:#7DDBA0;}
|
||||
.editor-code .cm{color:rgba(255,255,255,0.28);}
|
||||
.editor-code .fn{color:#7EB8FF;}
|
||||
.editor-code .prop{color:#FFBA7B;}
|
||||
.editor-code .num{color:#F9A875;}
|
||||
.editor-suggestion{margin:5px 0 5px 28px;background:rgba(124,58,237,0.12);border:1px solid rgba(124,58,237,0.22);border-radius:6px;padding:8px 10px;display:flex;flex-direction:column;gap:4px;}
|
||||
.editor-suggestion-label{font-family:var(--mono);font-size:0.4375rem;color:#C4A1FF;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:2px;}
|
||||
.editor-suggestion-diff{font-family:var(--mono);font-size:0.5625rem;display:flex;flex-direction:column;gap:2px;}
|
||||
.editor-diff-del{color:#F87171;background:rgba(248,113,113,0.07);padding:1px 4px;border-radius:2px;}
|
||||
.editor-diff-add{color:#34D399;background:rgba(52,211,153,0.07);padding:1px 4px;border-radius:2px;}
|
||||
.editor-suggestion-actions{display:flex;gap:6px;margin-top:4px;}
|
||||
.editor-action-btn{font-family:var(--mono);font-size:0.4375rem;letter-spacing:0.05em;padding:3px 8px;border-radius:3px;cursor:pointer;}
|
||||
.editor-action-btn.accept{background:rgba(52,211,153,0.18);color:#34D399;border:1px solid rgba(52,211,153,0.25);}
|
||||
.editor-action-btn.reject{background:rgba(255,255,255,0.05);color:rgba(255,255,255,0.35);border:1px solid rgba(255,255,255,0.1);}
|
||||
|
||||
/* Diff cards */
|
||||
.diff-card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:10px;padding:12px 14px;margin-bottom:8px;}
|
||||
.diff-card-header{display:flex;align-items:center;gap:8px;margin-bottom:9px;}
|
||||
.diff-card-icon{width:22px;height:22px;border-radius:4px;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;}
|
||||
.diff-card-title{font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.7);font-weight:600;}
|
||||
.diff-card-agent{margin-left:auto;font-family:var(--mono);font-size:0.4rem;letter-spacing:0.06em;text-transform:uppercase;padding:2px 6px;border-radius:var(--r-full);background:rgba(245,158,11,0.1);color:#FCD34D;border:1px solid rgba(245,158,11,0.2);white-space:nowrap;}
|
||||
.diff-row{display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.02);margin-bottom:3px;}
|
||||
.diff-row-prop{font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.3);}
|
||||
.diff-row-vals{display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:0.5rem;}
|
||||
.diff-val-old{color:#F87171;text-decoration:line-through;opacity:0.7;}
|
||||
.diff-val-arrow{color:rgba(255,255,255,0.2);}
|
||||
.diff-val-new{color:#34D399;}
|
||||
.diff-actions{display:flex;gap:6px;margin-top:7px;}
|
||||
.diff-btn{font-family:var(--mono);font-size:0.4375rem;letter-spacing:0.05em;padding:4px 10px;border-radius:4px;cursor:pointer;}
|
||||
.diff-btn.accept{background:rgba(52,211,153,0.15);color:#34D399;border:1px solid rgba(52,211,153,0.22);}
|
||||
.diff-btn.reject{background:rgba(255,255,255,0.04);color:rgba(255,255,255,0.32);border:1px solid rgba(255,255,255,0.09);}
|
||||
|
||||
/* HOW IT WORKS */
|
||||
.hiw-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:64px;position:relative;}
|
||||
.hiw-steps::before{content:'';position:absolute;top:28px;left:12.5%;right:12.5%;height:1px;background:linear-gradient(90deg,transparent,var(--border-2) 20%,var(--border-2) 80%,transparent);}
|
||||
.hiw-step{display:flex;flex-direction:column;align-items:center;text-align:center;padding:0 20px;position:relative;z-index:1;}
|
||||
.hiw-num{width:56px;height:56px;border-radius:50%;background:var(--bg);border:1px solid var(--border-2);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--amber);margin-bottom:20px;box-shadow:var(--sh-sm);}
|
||||
.hiw-step h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.hiw-step p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;}
|
||||
.hiw-step code{font-family:var(--mono);font-size:0.78em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;}
|
||||
|
||||
/* FEATURE SPLIT */
|
||||
.feat-split{display:grid;grid-template-columns:1fr 1fr;gap:96px;align-items:center;max-width:1120px;margin:0 auto;}
|
||||
.feat-split.rev{direction:rtl;}
|
||||
.feat-split.rev>*{direction:ltr;}
|
||||
.feat-copy .label{display:block;margin-bottom:16px;}
|
||||
.feat-copy .d3{margin-bottom:16px;}
|
||||
.feat-copy .subtitle{margin-bottom:28px;}
|
||||
.feat-pts{list-style:none;display:flex;flex-direction:column;gap:12px;margin-bottom:32px;}
|
||||
.feat-pts li{display:flex;align-items:flex-start;gap:10px;font-size:0.9375rem;color:var(--fg-2);line-height:1.55;}
|
||||
.feat-pt-icon{width:20px;height:20px;border-radius:3px;background:var(--amber-soft);border:1px solid var(--amber-mid);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;font-size:0.625rem;color:var(--amber);}
|
||||
.feat-vis{border-radius:var(--r-5);overflow:hidden;box-shadow:var(--sh-2xl);border:1px solid var(--border);aspect-ratio:4/3;position:relative;}
|
||||
.feat-vis-dark{background:#0D0D11;border-color:rgba(255,255,255,0.07);}
|
||||
.feat-vis-light{background:var(--bg-muted);}
|
||||
|
||||
/* CODE BLOCK */
|
||||
.code-block{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);padding:24px;font-family:var(--mono);font-size:0.75rem;line-height:1.75;color:rgba(255,255,255,0.75);overflow-x:auto;}
|
||||
.code-block .kw{color:#C4A1FF;}
|
||||
.code-block .str{color:#7DDBA0;}
|
||||
.code-block .cm{color:rgba(255,255,255,0.28);}
|
||||
.code-block .fn{color:#7EB8FF;}
|
||||
.code-block .nm{color:#FFBA7B;}
|
||||
.code-block .ty{color:#FCD34D;}
|
||||
|
||||
/* TECH SECTION */
|
||||
.tech-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:56px;}
|
||||
.tech-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:32px 28px;transition:box-shadow 0.2s;}
|
||||
.tech-card:hover{box-shadow:var(--sh-lg);}
|
||||
.tech-icon{width:44px;height:44px;background:var(--amber-soft);border:1px solid var(--amber-mid);border-radius:var(--r-2);display:flex;align-items:center;justify-content:center;font-size:1.25rem;margin-bottom:18px;}
|
||||
.tech-card h3{font-size:1rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.tech-card p{font-size:0.875rem;color:var(--fg-2);line-height:1.65;}
|
||||
.tech-tag{display:inline-block;margin-top:14px;font-family:var(--mono);font-size:0.625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--amber);background:var(--amber-soft);border:1px solid var(--amber-mid);padding:3px 9px;border-radius:var(--r-full);}
|
||||
|
||||
/* RELATED */
|
||||
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:48px;}
|
||||
.related-card{border:1px solid var(--border);border-radius:var(--r-4);padding:24px;transition:box-shadow 0.15s,border-color 0.15s;display:flex;flex-direction:column;gap:10px;}
|
||||
.related-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.related-icon{font-size:1.25rem;}
|
||||
.related-card h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;}
|
||||
.related-card p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;flex:1;}
|
||||
.related-link{font-size:0.8125rem;font-weight:600;color:var(--amber);}
|
||||
|
||||
/* CTA */
|
||||
#cta-bottom{background:var(--bg-inv);padding:120px 40px;position:relative;overflow:hidden;text-align:center;}
|
||||
#cta-bottom::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 30% 50%,rgba(245,158,11,0.08),transparent 60%),radial-gradient(ellipse 60% 80% at 70% 50%,rgba(124,58,237,0.08),transparent 60%);pointer-events:none;}
|
||||
.cta-inner{position:relative;max-width:680px;margin:0 auto;}
|
||||
.cta-headline{font-size:clamp(2rem,4.5vw,3.5rem);font-weight:900;letter-spacing:-0.045em;color:var(--fg-inv);line-height:1.05;margin-bottom:18px;}
|
||||
.cta-sub{font-size:1.0625rem;color:var(--fg-inv-2);margin-bottom:44px;line-height:1.65;}
|
||||
.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* FOOTER */
|
||||
footer{background:#08080C;border-top:1px solid rgba(255,255,255,0.06);padding:72px 40px 40px;}
|
||||
.ft-inner{max-width:1120px;margin:0 auto;}
|
||||
.ft-top{display:grid;grid-template-columns:260px repeat(4,1fr);gap:48px;padding-bottom:56px;border-bottom:1px solid rgba(255,255,255,0.07);margin-bottom:36px;}
|
||||
.ft-brand-logo{font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:rgba(255,255,255,0.92);margin-bottom:8px;}
|
||||
.ft-brand-logo span{color:#3385FF;}
|
||||
.ft-brand-tag{font-size:0.875rem;color:rgba(255,255,255,0.3);line-height:1.65;max-width:200px;}
|
||||
.ft-col h4{font-size:0.875rem;font-weight:600;color:rgba(255,255,255,0.7);margin-bottom:16px;letter-spacing:-0.01em;}
|
||||
.ft-col ul{list-style:none;display:flex;flex-direction:column;gap:10px;}
|
||||
.ft-col a{font-size:0.875rem;color:rgba(255,255,255,0.32);transition:color 0.14s;letter-spacing:-0.01em;}
|
||||
.ft-col a:hover{color:rgba(255,255,255,0.65);}
|
||||
.ft-bottom{display:flex;justify-content:space-between;align-items:center;}
|
||||
.ft-copy{font-size:0.8125rem;color:rgba(255,255,255,0.22);}
|
||||
|
||||
/* REVEAL */
|
||||
.reveal{opacity:0;transform:translateY(22px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1);}
|
||||
.reveal.in{opacity:1;transform:none;}
|
||||
.stagger>*{opacity:0;transform:translateY(16px);transition:opacity 0.55s cubic-bezier(0.16,1,0.3,1),transform 0.55s cubic-bezier(0.16,1,0.3,1);}
|
||||
.stagger.in>*:nth-child(1){opacity:1;transform:none;transition-delay:0s;}
|
||||
.stagger.in>*:nth-child(2){opacity:1;transform:none;transition-delay:0.07s;}
|
||||
.stagger.in>*:nth-child(3){opacity:1;transform:none;transition-delay:0.14s;}
|
||||
.stagger.in>*:nth-child(4){opacity:1;transform:none;transition-delay:0.21s;}
|
||||
.stagger.in>*:nth-child(5){opacity:1;transform:none;transition-delay:0.28s;}
|
||||
.stagger.in>*:nth-child(6){opacity:1;transform:none;transition-delay:0.35s;}
|
||||
|
||||
@keyframes live-pulse{0%,100%{box-shadow:0 0 0 2px rgba(245,158,11,0.2);}50%{box-shadow:0 0 0 4px rgba(245,158,11,0.08);}}
|
||||
|
||||
@media(max-width:1024px){.feat-split{grid-template-columns:1fr;gap:56px;}.feat-split.rev{direction:ltr;}.hiw-steps{grid-template-columns:1fr 1fr;gap:40px;}.hiw-steps::before{display:none;}.tech-grid{grid-template-columns:1fr 1fr;}.related-grid{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:768px){.wrap{padding:0 24px;}#nav{padding:0 24px;}.nav-inner{display:flex;justify-content:space-between;}.nav-links,.nav-actions .btn{display:none;}.nav-burger{display:flex;}#page-hero{padding:120px 24px 80px;}.pv-shell{margin-top:48px;}.hiw-steps{grid-template-columns:1fr;}.tech-grid,.related-grid{grid-template-columns:1fr;}.ft-top{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:480px){.ft-top{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo">Origin<span>main</span></a>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-dropdown">
|
||||
<a href="/#features" style="display:flex;align-items:center;gap:4px;">
|
||||
Product
|
||||
<svg class="nav-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a class="nav-dd-item" href="/features/live-artboard.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Live Artboard</span><span class="nav-dd-desc">Your app rendered live in the canvas</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/origin-graph.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Origin Graph</span><span class="nav-dd-desc">Every component, indexed and queryable</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/intent-diff.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Intent Diff</span><span class="nav-dd-desc">Ship component-level diffs, not redlines</span></div>
|
||||
</a>
|
||||
<div class="nav-dd-sep"></div>
|
||||
<a class="nav-dd-item" href="/features/completion-zone.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Completion Zone</span><span class="nav-dd-desc">AI-filled design, constrained to your system</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item active" href="/features/agent-bridge.html">
|
||||
<div class="nav-dd-icon amber"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Agent Bridge</span><span class="nav-dd-desc">MCP channel to Cursor, Claude Code & more</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/integrations.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Integrations</span><span class="nav-dd-desc">Linear, Slack, GitHub, Figma & more</span></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<a href="#" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-mobile" id="nav-mobile">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/#features" id="mob-product-toggle" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Product
|
||||
<svg id="mob-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none" style="transition:transform 0.15s;"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-mobile-sub" id="nav-mobile-sub">
|
||||
<a href="/features/live-artboard.html">Live Artboard</a>
|
||||
<a href="/features/origin-graph.html">Origin Graph</a>
|
||||
<a href="/features/intent-diff.html">Intent Diff</a>
|
||||
<a href="/features/completion-zone.html">Completion Zone</a>
|
||||
<a href="/features/agent-bridge.html">Agent Bridge</a>
|
||||
<a href="/features/integrations.html">Integrations</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="#" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section id="page-hero">
|
||||
<div class="page-hero-inner">
|
||||
<span class="label reveal">⇄ Agent Bridge</span>
|
||||
<h1 class="d1 reveal">MCP channel to<br>Cursor, Claude Code<br>& more.</h1>
|
||||
<p class="subtitle reveal">Agent Bridge exposes your entire Origin Graph through the Model Context Protocol — so Cursor, Claude Code, and any MCP-compatible agent can read your component tree, run diffs, and propose changes without leaving the editor.</p>
|
||||
<div class="page-hero-ctas reveal">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="#how-it-works" class="btn btn-outline-white btn-lg">See how it works ↓</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product visual: split editor / canvas bridge -->
|
||||
<div class="pv-shell reveal">
|
||||
<div class="pv-chrome">
|
||||
<div class="pv-dot r"></div>
|
||||
<div class="pv-dot y"></div>
|
||||
<div class="pv-dot g"></div>
|
||||
<div class="pv-url">🔒 app.originmain.io/canvas — Agent Bridge active</div>
|
||||
</div>
|
||||
<div class="pv-canvas">
|
||||
<div class="pv-grid"></div>
|
||||
|
||||
<!-- Left panel: VS Code-style editor -->
|
||||
<div class="bridge-panel-left">
|
||||
<div class="bridge-panel-header">
|
||||
<span class="bridge-panel-title">Editor</span>
|
||||
<span class="bridge-panel-badge cursor">Cursor</span>
|
||||
</div>
|
||||
<div class="bridge-panel-body">
|
||||
<div class="editor-line"><span class="editor-ln">11</span><span class="editor-code"><span class="kw">const</span> <span class="fn">CardRoot</span> = <span class="fn">styled</span>.<span class="fn">div</span>`</span></div>
|
||||
<div class="editor-line"><span class="editor-ln">12</span><span class="editor-code" style="padding-left:16px;"><span class="prop">display</span>: <span class="str">flex</span>;</span></div>
|
||||
<div class="editor-line"><span class="editor-ln">13</span><span class="editor-code" style="padding-left:16px;"><span class="prop">flex-direction</span>: <span class="str">column</span>;</span></div>
|
||||
<div class="editor-line"><span class="editor-ln">14</span><span class="editor-code" style="padding-left:16px;"><span class="prop">border-radius</span>: <span class="str">var(--radius-md)</span>;</span></div>
|
||||
<div class="editor-line"><span class="editor-ln">15</span><span class="editor-code" style="padding-left:16px;"><span class="prop">padding</span>: <span class="num">16px</span>;</span></div>
|
||||
<!-- Inline Cursor suggestion -->
|
||||
<div class="editor-suggestion">
|
||||
<div class="editor-suggestion-label">Cursor suggestion · via Agent Bridge</div>
|
||||
<div class="editor-suggestion-diff">
|
||||
<span class="editor-diff-del">− padding: 16px;</span>
|
||||
<span class="editor-diff-add">+ padding: 24px;</span>
|
||||
</div>
|
||||
<div class="editor-suggestion-actions">
|
||||
<button class="editor-action-btn accept">Accept</button>
|
||||
<button class="editor-action-btn reject">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-line"><span class="editor-ln">16</span><span class="editor-code" style="padding-left:16px;"><span class="prop">overflow</span>: <span class="str">hidden</span>;</span></div>
|
||||
<div class="editor-line"><span class="editor-ln">17</span><span class="editor-code"><span class="str">`</span>;</span></div>
|
||||
<div class="editor-line" style="margin-top:6px;"><span class="editor-ln">18</span><span class="editor-code"></span></div>
|
||||
<div class="editor-line"><span class="editor-ln">19</span><span class="editor-code"><span class="cm">// diff_components() — origin graph context loaded</span></span></div>
|
||||
<div class="editor-line"><span class="editor-ln">20</span><span class="editor-code"><span class="cm">// 3 affected children: CardHeader, CardBody, CardFooter</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center bridge connector -->
|
||||
<div class="bridge-connector">
|
||||
<div class="bridge-line-top"></div>
|
||||
<div style="position:relative;display:flex;align-items:center;justify-content:center;">
|
||||
<div class="bridge-rings">
|
||||
<div class="bridge-pulse"></div>
|
||||
<div class="bridge-pulse bridge-pulse-2"></div>
|
||||
</div>
|
||||
<div class="bridge-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
</div>
|
||||
<div class="bridge-line-bottom"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel: Canvas intent diff -->
|
||||
<div class="bridge-panel-right">
|
||||
<div class="bridge-panel-header">
|
||||
<span class="bridge-panel-title">Canvas</span>
|
||||
<span class="bridge-panel-badge canvas">Intent Diff</span>
|
||||
</div>
|
||||
<div class="bridge-panel-body">
|
||||
<!-- Primary diff card -->
|
||||
<div class="diff-card">
|
||||
<div class="diff-card-header">
|
||||
<div class="diff-card-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<span class="diff-card-title">CardRoot</span>
|
||||
<span class="diff-card-agent">Proposed by: Cursor</span>
|
||||
</div>
|
||||
<div class="diff-row">
|
||||
<span class="diff-row-prop">padding</span>
|
||||
<div class="diff-row-vals">
|
||||
<span class="diff-val-old">16px</span>
|
||||
<span class="diff-val-arrow">→</span>
|
||||
<span class="diff-val-new">24px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diff-actions">
|
||||
<button class="diff-btn accept">Accept diff</button>
|
||||
<button class="diff-btn reject">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Secondary pending diff -->
|
||||
<div class="diff-card" style="opacity:0.5;">
|
||||
<div class="diff-card-header">
|
||||
<div class="diff-card-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<span class="diff-card-title">CardHeader</span>
|
||||
<span class="diff-card-agent">Proposed by: Cursor</span>
|
||||
</div>
|
||||
<div class="diff-row">
|
||||
<span class="diff-row-prop">gap</span>
|
||||
<div class="diff-row-vals">
|
||||
<span class="diff-val-old">8px</span>
|
||||
<span class="diff-val-arrow">→</span>
|
||||
<span class="diff-val-new">12px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MCP status line -->
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-top:6px;">
|
||||
<div style="width:6px;height:6px;border-radius:50%;background:#F59E0B;animation:live-pulse 2s ease-in-out infinite;"></div>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.25);letter-spacing:0.06em;text-transform:uppercase;">MCP connected · 2 pending diffs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section id="how-it-works" class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Protocol</span>
|
||||
<h2 class="d2">From handshake to diff<br>in four steps.</h2>
|
||||
<p class="subtitle">A JSON-RPC 2.0 MCP server sits between your AI agent and the Origin Graph. The agent calls tools. The bridge executes them. Proposed changes come back to you for review.</p>
|
||||
</div>
|
||||
<div class="hiw-steps stagger">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Agent connects</h3>
|
||||
<p>Add the Agent Bridge MCP endpoint to your Cursor or Claude Code settings. The MCP handshake authenticates via <code>AGENT_BRIDGE_SECRET</code> and announces available tool definitions.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Tool call made</h3>
|
||||
<p>Your agent issues a JSON-RPC call — e.g. <code>diff_components({ before, after })</code>. The bridge validates the input schema with Zod before any execution begins.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>Bridge executes</h3>
|
||||
<p>Agent Bridge queries the live Origin Graph, runs the diff engine against your real component tree, and streams structured results back to the agent.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">04</div>
|
||||
<h3>Result returned</h3>
|
||||
<p>The agent receives a structured JSON response. Any proposed change is simultaneously queued as an Intent Diff in your canvas — ready for human review before anything applies.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DEEP DIVE 1: Real tools, not hallucinations -->
|
||||
<div class="section" style="padding:100px 40px;">
|
||||
<div class="feat-split reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-amber">MCP tool definitions</span>
|
||||
<h2 class="d3">Real tools,<br>not hallucinations.</h2>
|
||||
<p class="subtitle">Every agent tool is backed by a live query against your Origin Graph. The agent gets your actual component tree, real prop values, and real token relationships — not documentation that may have drifted from your codebase.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></span>Tool inputs are defined as JSON Schema and validated with Zod on every call. Malformed inputs never reach the graph.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span><code style="font-family:var(--mono);font-size:0.875em;background:var(--bg-muted);padding:1px 5px;border-radius:3px;">query_graph</code> runs structured traversals — find components by name, trace token usage, map dependency chains.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span><code style="font-family:var(--mono);font-size:0.875em;background:var(--bg-muted);padding:1px 5px;border-radius:3px;">answer_question</code> gives agents plain-language answers grounded in the real graph, not training data.</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-primary">Read the MCP docs</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:20px 18px;overflow:hidden;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:12px;">MCP tool definitions</div>
|
||||
|
||||
<!-- get_component -->
|
||||
<div style="margin-bottom:10px;padding:9px 11px;background:rgba(245,158,11,0.04);border:1px solid rgba(245,158,11,0.1);border-radius:7px;">
|
||||
<div style="font-family:var(--mono);font-size:0.5625rem;color:#FCD34D;font-weight:600;margin-bottom:4px;">get_component</div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.28);line-height:1.75;">
|
||||
<div><span style="color:#C4A1FF;">in:</span> <span style="color:#7DDBA0;">{ name: string, includeChildren?: boolean }</span></div>
|
||||
<div><span style="color:#C4A1FF;">out:</span> <span style="color:#7DDBA0;">{ node: ComponentNode, props: PropSchema, children: ComponentNode[] }</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- diff_components -->
|
||||
<div style="margin-bottom:10px;padding:9px 11px;background:rgba(245,158,11,0.04);border:1px solid rgba(245,158,11,0.1);border-radius:7px;">
|
||||
<div style="font-family:var(--mono);font-size:0.5625rem;color:#FCD34D;font-weight:600;margin-bottom:4px;">diff_components</div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.28);line-height:1.75;">
|
||||
<div><span style="color:#C4A1FF;">in:</span> <span style="color:#7DDBA0;">{ before: ComponentSnapshot, after: ComponentSnapshot }</span></div>
|
||||
<div><span style="color:#C4A1FF;">out:</span> <span style="color:#7DDBA0;">{ diff: IntentDiff[], affectedCount: number }</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- query_graph -->
|
||||
<div style="margin-bottom:10px;padding:9px 11px;background:rgba(245,158,11,0.04);border:1px solid rgba(245,158,11,0.1);border-radius:7px;">
|
||||
<div style="font-family:var(--mono);font-size:0.5625rem;color:#FCD34D;font-weight:600;margin-bottom:4px;">query_graph</div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.28);line-height:1.75;">
|
||||
<div><span style="color:#C4A1FF;">in:</span> <span style="color:#7DDBA0;">{ query: string, depth?: number, filters?: GraphFilter[] }</span></div>
|
||||
<div><span style="color:#C4A1FF;">out:</span> <span style="color:#7DDBA0;">{ nodes: GraphNode[], edges: GraphEdge[], total: number }</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- answer_question -->
|
||||
<div style="padding:9px 11px;background:rgba(245,158,11,0.04);border:1px solid rgba(245,158,11,0.1);border-radius:7px;">
|
||||
<div style="font-family:var(--mono);font-size:0.5625rem;color:#FCD34D;font-weight:600;margin-bottom:4px;">answer_question</div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.28);line-height:1.75;">
|
||||
<div><span style="color:#C4A1FF;">in:</span> <span style="color:#7DDBA0;">{ question: string, context?: string[] }</span></div>
|
||||
<div><span style="color:#C4A1FF;">out:</span> <span style="color:#7DDBA0;">{ answer: string, sources: GraphNode[], confidence: number }</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEEP DIVE 2: Auditable agent actions -->
|
||||
<div class="section section-muted" style="padding:100px 40px;">
|
||||
<div class="feat-split rev reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-amber">Audit trail</span>
|
||||
<h2 class="d3">Auditable agent<br>actions. Always.</h2>
|
||||
<p class="subtitle">Every change an agent proposes flows through the exact same Intent Diff review pipeline as a human edit. Nothing is applied silently. Every agent action is stamped, attributable, and reversible.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Agent-proposed diffs appear in your canvas labeled with the source agent — Cursor, Claude Code, or any MCP-compatible client.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Accept or reject individual diffs. The agent cannot bypass the review queue or apply changes directly.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></span>Every action is written to an append-only audit log with timestamp, agent identity, tool called, and full input/output payload.</li>
|
||||
</ul>
|
||||
<a href="/features/intent-diff.html" class="btn btn-primary">Explore Intent Diff</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:20px 18px;display:flex;flex-direction:column;gap:10px;overflow:hidden;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:2px;">Intent Diff — agent queue</div>
|
||||
|
||||
<!-- Agent diff card -->
|
||||
<div style="background:rgba(245,158,11,0.05);border:1px solid rgba(245,158,11,0.14);border-radius:9px;padding:11px 13px;">
|
||||
<div style="display:flex;align-items:center;gap:7px;margin-bottom:8px;">
|
||||
<div style="width:22px;height:22px;border-radius:4px;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.2);display:flex;align-items:center;justify-content:center;font-size:9px;flex-shrink:0;"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.72);font-weight:600;">CardRoot</span>
|
||||
<span style="margin-left:auto;font-family:var(--mono);font-size:0.4rem;letter-spacing:0.05em;text-transform:uppercase;padding:2px 6px;border-radius:99px;background:rgba(245,158,11,0.1);color:#FCD34D;border:1px solid rgba(245,158,11,0.18);white-space:nowrap;">Proposed by: Claude Code</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.02);margin-bottom:3px;">
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.3);">padding</span>
|
||||
<div style="display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:0.5rem;">
|
||||
<span style="color:#F87171;text-decoration:line-through;opacity:0.7;">16px</span>
|
||||
<span style="color:rgba(255,255,255,0.2);">→</span>
|
||||
<span style="color:#34D399;">24px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.02);margin-bottom:7px;">
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.3);">gap</span>
|
||||
<div style="display:flex;align-items:center;gap:5px;font-family:var(--mono);font-size:0.5rem;">
|
||||
<span style="color:#F87171;text-decoration:line-through;opacity:0.7;">8px</span>
|
||||
<span style="color:rgba(255,255,255,0.2);">→</span>
|
||||
<span style="color:#34D399;">12px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button style="font-family:var(--mono);font-size:0.4375rem;letter-spacing:0.05em;padding:4px 10px;border-radius:4px;background:rgba(52,211,153,0.15);color:#34D399;border:1px solid rgba(52,211,153,0.22);cursor:pointer;">Accept diff</button>
|
||||
<button style="font-family:var(--mono);font-size:0.4375rem;letter-spacing:0.05em;padding:4px 10px;border-radius:4px;background:rgba(255,255,255,0.04);color:rgba(255,255,255,0.32);border:1px solid rgba(255,255,255,0.09);cursor:pointer;">Reject</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit log -->
|
||||
<div style="background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.07);border-radius:9px;padding:11px 13px;">
|
||||
<div style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.2);letter-spacing:0.08em;text-transform:uppercase;margin-bottom:8px;">Audit log</div>
|
||||
<div style="display:flex;flex-direction:column;gap:5px;">
|
||||
<div style="display:flex;align-items:center;gap:7px;">
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.18);min-width:52px;">14:02:31</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:#FCD34D;">claude-code</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.3);">diff_components</span>
|
||||
<span style="margin-left:auto;font-family:var(--mono);font-size:0.4375rem;color:#34D399;">queued</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:7px;">
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.18);min-width:52px;">14:02:28</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:#C4A1FF;">cursor</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.3);">query_graph</span>
|
||||
<span style="margin-left:auto;font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.28);">ok</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:7px;">
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.18);min-width:52px;">14:02:21</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:#C4A1FF;">cursor</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.3);">get_component</span>
|
||||
<span style="margin-left:auto;font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.28);">ok</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:7px;">
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.18);min-width:52px;">14:02:09</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:#C4A1FF;">cursor</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.3);">answer_question</span>
|
||||
<span style="margin-left:auto;font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.28);">ok</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CODE BLOCK: Full tool schema reference -->
|
||||
<section class="section section-muted" style="padding:80px 40px;">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Developer reference</span>
|
||||
<h2 class="d2">The full tool schema.</h2>
|
||||
<p class="subtitle">Agent Bridge speaks standard JSON-RPC 2.0. Drop the endpoint into any MCP-compatible client and tool definitions auto-discover on connect.</p>
|
||||
</div>
|
||||
<div class="code-block reveal">
|
||||
<span class="cm">// Agent Bridge — MCP server tool manifest</span>
|
||||
<span class="cm">// Add to Cursor: Settings → MCP → Add Server → paste endpoint + secret</span>
|
||||
|
||||
<span class="kw">const</span> <span class="nm">tools</span> = [
|
||||
{
|
||||
<span class="nm">name</span>: <span class="str">"get_component"</span>,
|
||||
<span class="nm">description</span>: <span class="str">"Fetch a component node from the Origin Graph by name or ID"</span>,
|
||||
<span class="nm">inputSchema</span>: {
|
||||
<span class="nm">type</span>: <span class="str">"object"</span>,
|
||||
<span class="nm">properties</span>: {
|
||||
<span class="nm">name</span>: { <span class="nm">type</span>: <span class="str">"string"</span>, <span class="nm">description</span>: <span class="str">"Component display name"</span> },
|
||||
<span class="nm">includeChildren</span>: { <span class="nm">type</span>: <span class="str">"boolean"</span>, <span class="nm">default</span>: <span class="kw">false</span> }
|
||||
},
|
||||
<span class="nm">required</span>: [<span class="str">"name"</span>]
|
||||
}
|
||||
},
|
||||
{
|
||||
<span class="nm">name</span>: <span class="str">"diff_components"</span>,
|
||||
<span class="nm">description</span>: <span class="str">"Run a structural diff between two component snapshots"</span>,
|
||||
<span class="nm">inputSchema</span>: {
|
||||
<span class="nm">type</span>: <span class="str">"object"</span>,
|
||||
<span class="nm">properties</span>: {
|
||||
<span class="nm">before</span>: { <span class="nm">type</span>: <span class="str">"object"</span>, <span class="nm">description</span>: <span class="str">"ComponentSnapshot before change"</span> },
|
||||
<span class="nm">after</span>: { <span class="nm">type</span>: <span class="str">"object"</span>, <span class="nm">description</span>: <span class="str">"ComponentSnapshot after change"</span> }
|
||||
},
|
||||
<span class="nm">required</span>: [<span class="str">"before"</span>, <span class="str">"after"</span>]
|
||||
}
|
||||
},
|
||||
{
|
||||
<span class="nm">name</span>: <span class="str">"query_graph"</span>,
|
||||
<span class="nm">description</span>: <span class="str">"Traverse the Origin Graph — find components, tokens, and relationships"</span>,
|
||||
<span class="nm">inputSchema</span>: {
|
||||
<span class="nm">type</span>: <span class="str">"object"</span>,
|
||||
<span class="nm">properties</span>: {
|
||||
<span class="nm">query</span>: { <span class="nm">type</span>: <span class="str">"string"</span> },
|
||||
<span class="nm">depth</span>: { <span class="nm">type</span>: <span class="str">"number"</span>, <span class="nm">default</span>: <span class="ty">2</span> },
|
||||
<span class="nm">filters</span>: { <span class="nm">type</span>: <span class="str">"array"</span>, <span class="nm">items</span>: { <span class="nm">type</span>: <span class="str">"string"</span> } }
|
||||
},
|
||||
<span class="nm">required</span>: [<span class="str">"query"</span>]
|
||||
}
|
||||
},
|
||||
{
|
||||
<span class="nm">name</span>: <span class="str">"answer_question"</span>,
|
||||
<span class="nm">description</span>: <span class="str">"Ask a natural-language question grounded in the live Origin Graph"</span>,
|
||||
<span class="nm">inputSchema</span>: {
|
||||
<span class="nm">type</span>: <span class="str">"object"</span>,
|
||||
<span class="nm">properties</span>: {
|
||||
<span class="nm">question</span>: { <span class="nm">type</span>: <span class="str">"string"</span> },
|
||||
<span class="nm">context</span>: { <span class="nm">type</span>: <span class="str">"array"</span>, <span class="nm">items</span>: { <span class="nm">type</span>: <span class="str">"string"</span> } }
|
||||
},
|
||||
<span class="nm">required</span>: [<span class="str">"question"</span>]
|
||||
}
|
||||
}
|
||||
];
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TECH CARDS -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Technical foundation</span>
|
||||
<h2 class="d2">Built on open protocols.</h2>
|
||||
<p class="subtitle">Agent Bridge is a first-class MCP server. No proprietary SDKs, no vendor lock-in. If your agent speaks MCP, it works.</p>
|
||||
</div>
|
||||
<div class="tech-grid stagger">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">📡</div>
|
||||
<h3>JSON-RPC 2.0</h3>
|
||||
<p>Every tool call and response follows the JSON-RPC 2.0 specification exactly — standard request/response envelopes, structured error codes, and batch request support out of the box.</p>
|
||||
<span class="tech-tag">Protocol</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<h3>MCP protocol</h3>
|
||||
<p>Agent Bridge registers as a full MCP server — tool discovery, capability negotiation, and streaming all follow the Model Context Protocol spec. Works with Cursor, Claude Code, and any future MCP client.</p>
|
||||
<span class="tech-tag">Standard</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🛡</div>
|
||||
<h3>Tool schema validation</h3>
|
||||
<p>Every inbound tool call is validated against a Zod schema before any execution. Malformed inputs are rejected with structured JSON-RPC error responses — the graph is never reached by bad data.</p>
|
||||
<span class="tech-tag">Zod</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🔑</div>
|
||||
<h3>AGENT_BRIDGE_SECRET auth</h3>
|
||||
<p>Authentication is token-based via a pre-shared <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">AGENT_BRIDGE_SECRET</code>. Set it once in your MCP client config — all subsequent calls are bearer-authenticated automatically.</p>
|
||||
<span class="tech-tag">Security</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⚡</div>
|
||||
<h3>Streaming responses</h3>
|
||||
<p>Large graph queries and diff results stream back incrementally as NDJSON. Your agent receives the first nodes immediately — no waiting for the full response payload to accumulate server-side.</p>
|
||||
<span class="tech-tag">Performance</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">📋</div>
|
||||
<h3>Audit log for agent actions</h3>
|
||||
<p>Every agent action is persisted to an append-only audit log — agent identity, tool called, inputs, outputs, and timestamp. Queryable from the canvas and exportable to JSON at any time.</p>
|
||||
<span class="tech-tag">Compliance</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RELATED FEATURES -->
|
||||
<section class="section section-muted">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Connected features</span>
|
||||
<h2 class="d2">Agent Bridge is<br>one half of the loop.</h2>
|
||||
<p class="subtitle">Agents read from the Origin Graph and write back through Intent Diff. The whole system is the pipeline.</p>
|
||||
</div>
|
||||
<div class="related-grid stagger">
|
||||
<a class="related-card" href="/features/origin-graph.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3>Origin Graph</h3>
|
||||
<p>The queryable index that Agent Bridge reads from. Every component, token, and relationship in your codebase — live, traversable, and always in sync with your real code.</p>
|
||||
<span class="related-link">Explore Origin Graph →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/intent-diff.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<h3>Intent Diff</h3>
|
||||
<p>Every change an agent proposes enters the same Intent Diff review pipeline as a human edit. Nothing gets applied without your explicit sign-off — every agent action is auditable.</p>
|
||||
<span class="related-link">Explore Intent Diff →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/completion-zone.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<h3>Completion Zone</h3>
|
||||
<p>Mark a region in the canvas and let AI fill it using your design language constraints. Pairs with Agent Bridge for end-to-end agentic design-to-code sessions.</p>
|
||||
<span class="related-link">Explore Completion Zone →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta-bottom">
|
||||
<div class="cta-inner">
|
||||
<h2 class="cta-headline">Give your agents<br>real context.</h2>
|
||||
<p class="cta-sub">Connect Cursor or Claude Code to your live Origin Graph in under five minutes. No hallucinated docs — just your actual codebase, queryable from any MCP agent.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="/" class="btn btn-outline-white btn-lg">Back to overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="ft-inner">
|
||||
<div class="ft-top">
|
||||
<div>
|
||||
<div class="ft-brand-logo">Origin<span>main</span></div>
|
||||
<p class="ft-brand-tag">Where design intent becomes production code.</p>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="/features/live-artboard.html">Live Artboard</a></li>
|
||||
<li><a href="/features/origin-graph.html">Origin Graph</a></li>
|
||||
<li><a href="/features/intent-diff.html">Intent Diff</a></li>
|
||||
<li><a href="/features/completion-zone.html">Completion Zone</a></li>
|
||||
<li><a href="/features/agent-bridge.html">Agent Bridge</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Integrations</h4>
|
||||
<ul>
|
||||
<li><a href="/features/integrations.html">All integrations</a></li>
|
||||
<li><a href="#">GitHub</a></li>
|
||||
<li><a href="#">Linear</a></li>
|
||||
<li><a href="#">Slack</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Developers</h4>
|
||||
<ul>
|
||||
<li><a href="#">Docs</a></li>
|
||||
<li><a href="#">API Reference</a></li>
|
||||
<li><a href="#">MCP Protocol</a></li>
|
||||
<li><a href="#">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft-bottom">
|
||||
<span class="ft-copy">© 2025 Originmain. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Nav scroll
|
||||
const nav=document.getElementById('nav');
|
||||
window.addEventListener('scroll',()=>{nav.classList.toggle('scrolled',window.scrollY>20);});
|
||||
// Dropdown: delay-based hover (desktop) + click (mobile/touch)
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd=>{const menu=dd.querySelector('.nav-dropdown-menu');let t;const open=()=>{clearTimeout(t);dd.classList.add('open');menu.classList.add('open');};const close=()=>{t=setTimeout(()=>{dd.classList.remove('open');menu.classList.remove('open');},150);};dd.addEventListener('mouseenter',open);dd.addEventListener('mouseleave',close);menu.addEventListener('mouseenter',open);menu.addEventListener('mouseleave',close);dd.querySelector('a').addEventListener('click',e=>{if(window.matchMedia('(hover:none)').matches||window.innerWidth<=1024){e.preventDefault();const o=menu.classList.toggle('open');dd.classList.toggle('open',o);}});});
|
||||
// Mobile product sub-accordion
|
||||
const mpt=document.getElementById('mob-product-toggle'),mns=document.getElementById('nav-mobile-sub'),mdc=document.getElementById('mob-dd-caret');if(mpt&&mns){mpt.addEventListener('click',e=>{e.preventDefault();const o=mns.classList.toggle('open');if(mdc)mdc.style.transform=o?'rotate(180deg)':'';});}
|
||||
// Burger
|
||||
const burger=document.getElementById('nav-burger'),mob=document.getElementById('nav-mobile');
|
||||
burger.addEventListener('click',()=>{const o=mob.classList.toggle('open');burger.classList.toggle('open',o);burger.setAttribute('aria-expanded',o);});
|
||||
// Reveal
|
||||
const io=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target);}});},{threshold:0.12});
|
||||
document.querySelectorAll('.reveal,.stagger').forEach(el=>io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,805 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Completion Zone — Originmain</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#FFFFFF;--bg-subtle:#FAFAFA;--bg-muted:#F4F4F5;--bg-strong:#EBEBEB;--bg-inv:#0A0A0A;--bg-inv-2:#141414;--bg-inv-3:#1C1C1C;--fg:#0A0A0A;--fg-2:#52525B;--fg-3:#A1A1AA;--fg-4:#D4D4D8;--fg-inv:rgba(255,255,255,0.92);--fg-inv-2:rgba(255,255,255,0.52);--fg-inv-3:rgba(255,255,255,0.28);--blue:#0066FF;--blue-dark:#0050CC;--blue-light:#3385FF;--blue-soft:rgba(0,102,255,0.07);--blue-mid:rgba(0,102,255,0.15);--green:#10B981;--green-dark:#059669;--green-light:#34D399;--green-soft:rgba(16,185,129,0.07);--green-mid:rgba(16,185,129,0.18);--purple:#7C3AED;--teal:#0891B2;--border:rgba(0,0,0,0.07);--border-2:rgba(0,0,0,0.12);--border-inv:rgba(255,255,255,0.08);--border-inv-2:rgba(255,255,255,0.14);--r-1:4px;--r-2:8px;--r-3:12px;--r-4:16px;--r-5:20px;--r-6:24px;--r-full:9999px;--sh-xs:0 1px 2px rgba(0,0,0,0.05);--sh-sm:0 1px 3px rgba(0,0,0,0.07),0 1px 2px rgba(0,0,0,0.05);--sh-md:0 4px 8px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04);--sh-lg:0 10px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.05);--sh-xl:0 20px 40px rgba(0,0,0,0.09),0 8px 16px rgba(0,0,0,0.05);--sh-2xl:0 32px 64px rgba(0,0,0,0.11),0 16px 32px rgba(0,0,0,0.06);--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--mono:'JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||||
html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.6;overflow-x:hidden;}
|
||||
img,svg{display:block;max-width:100%;}
|
||||
a{color:inherit;text-decoration:none;}
|
||||
button{font-family:inherit;cursor:pointer;border:none;background:none;}
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:0 40px;}
|
||||
.section{padding:112px 0;}
|
||||
.section-muted{background:var(--bg-subtle);}
|
||||
.section-dark{background:var(--bg-inv);}
|
||||
.d1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;line-height:1.02;letter-spacing:-0.045em;color:var(--fg);}
|
||||
.d2{font-size:clamp(1.875rem,3.5vw,2.75rem);font-weight:800;line-height:1.1;letter-spacing:-0.038em;color:var(--fg);}
|
||||
.d3{font-size:clamp(1.25rem,2.5vw,1.625rem);font-weight:700;line-height:1.25;letter-spacing:-0.028em;color:var(--fg);}
|
||||
.subtitle{font-size:clamp(1rem,1.8vw,1.125rem);font-weight:400;line-height:1.75;color:var(--fg-2);}
|
||||
.label{font-family:var(--mono);font-size:0.6875rem;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--fg-3);}
|
||||
.label-green{color:var(--green);}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font);font-size:0.9375rem;font-weight:600;padding:11px 22px;border-radius:var(--r-2);transition:transform 0.12s cubic-bezier(.16,1,.3,1),opacity 0.12s,box-shadow 0.12s;white-space:nowrap;letter-spacing:-0.01em;position:relative;overflow:hidden;}
|
||||
.btn::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,0);transition:background 0.12s;}
|
||||
.btn:hover::after{background:rgba(255,255,255,0.06);}
|
||||
.btn:active{transform:scale(0.98)!important;}
|
||||
.btn-primary{background:var(--fg);color:var(--bg);box-shadow:var(--sh-sm);}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:var(--sh-md);}
|
||||
.btn-secondary{background:transparent;color:var(--fg);border:1px solid var(--border-2);}
|
||||
.btn-secondary:hover{background:var(--bg-muted);transform:translateY(-1px);}
|
||||
.btn-green{background:var(--green);color:#fff;box-shadow:0 4px 12px rgba(16,185,129,0.35);}
|
||||
.btn-green:hover{background:var(--green-dark);transform:translateY(-1px);}
|
||||
.btn-lg{font-size:1rem;padding:13px 28px;}
|
||||
.btn-white{background:#fff;color:#0A0A0A;box-shadow:0 4px 12px rgba(255,255,255,0.15);}
|
||||
.btn-white:hover{background:#F5F5F5;transform:translateY(-1px);}
|
||||
.btn-outline-white{background:transparent;color:rgba(255,255,255,0.72);border:1px solid rgba(255,255,255,0.18);}
|
||||
.btn-outline-white:hover{background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.9);transform:translateY(-1px);}
|
||||
.divider{height:1px;background:var(--border);}
|
||||
.section-head{text-align:center;max-width:640px;margin:0 auto 72px;}
|
||||
.section-head .label{display:block;margin-bottom:16px;}
|
||||
.section-head .d2{margin-bottom:18px;}
|
||||
|
||||
/* NAV */
|
||||
#nav{position:fixed;top:0;left:0;right:0;z-index:200;height:60px;display:flex;align-items:center;padding:0 40px;transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;}
|
||||
#nav.scrolled{background:rgba(255,255,255,0.85);backdrop-filter:blur(20px) saturate(1.6);-webkit-backdrop-filter:blur(20px) saturate(1.6);border-bottom:1px solid var(--border);box-shadow:var(--sh-xs);}
|
||||
.nav-inner{max-width:1120px;margin:0 auto;width:100%;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
|
||||
.nav-logo{justify-self:start;font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:var(--fg);display:flex;align-items:center;gap:2px;}
|
||||
.nav-logo span{color:var(--blue);}
|
||||
.nav-links{justify-self:center;display:flex;align-items:center;gap:0;list-style:none;}
|
||||
.nav-links a{font-size:0.9375rem;font-weight:500;color:var(--fg-2);padding:6px 14px;border-radius:var(--r-1);transition:color 0.12s,background 0.12s;letter-spacing:-0.01em;}
|
||||
.nav-links a:hover{color:var(--fg);}
|
||||
.nav-actions{justify-self:end;display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* Dropdown */
|
||||
.nav-dropdown{position:relative;}
|
||||
.nav-dropdown>a{display:flex;align-items:center;gap:4px;}
|
||||
.nav-dd-caret{transition:transform 0.15s;}
|
||||
/* caret rotation handled via JS .open class */
|
||||
.nav-dropdown-menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-4px);background:rgba(255,255,255,0.98);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--r-3);box-shadow:var(--sh-xl);padding:8px;min-width:240px;opacity:0;visibility:hidden;transition:opacity 0.15s,transform 0.15s,visibility 0.15s;pointer-events:none;}
|
||||
.nav-dropdown-menu.open{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:all;}
|
||||
.nav-dropdown.open .nav-dd-caret{transform:rotate(180deg);}
|
||||
/* Mobile sub-nav */
|
||||
.nav-mobile-sub{display:none;padding:2px 0 6px 14px;flex-direction:column;}
|
||||
.nav-mobile-sub.open{display:flex;}
|
||||
.nav-mobile-sub a{font-size:0.875rem;color:rgba(255,255,255,0.4);padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.06);letter-spacing:-0.01em;transition:color 0.1s;}
|
||||
.nav-mobile-sub a:last-child{border-bottom:none;}
|
||||
.nav-mobile-sub a:hover{color:rgba(255,255,255,0.8);}
|
||||
/* Dark hero: white nav before scrolling */
|
||||
#nav:not(.scrolled) .nav-logo{color:rgba(255,255,255,0.92);}
|
||||
#nav:not(.scrolled) .nav-logo span{color:#3385FF;}
|
||||
#nav:not(.scrolled) .nav-links a{color:rgba(255,255,255,0.5);}
|
||||
#nav:not(.scrolled) .nav-links a:hover{color:rgba(255,255,255,0.9);}
|
||||
#nav:not(.scrolled) .nav-actions .btn-secondary{color:rgba(255,255,255,0.65);border-color:rgba(255,255,255,0.2);background:transparent;}
|
||||
#nav:not(.scrolled) .nav-burger span{background:rgba(255,255,255,0.8);}
|
||||
.nav-dd-item{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:var(--r-2);transition:background 0.1s;cursor:pointer;}
|
||||
.nav-dd-item:hover{background:var(--bg-muted);}
|
||||
.nav-dd-item.active{background:var(--green-soft);}
|
||||
.nav-dd-icon{width:30px;height:30px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:13px;color:var(--blue);}
|
||||
.nav-dd-icon.green{background:var(--green-soft);border-color:var(--green-mid);}
|
||||
.nav-dd-text{display:flex;flex-direction:column;}
|
||||
.nav-dd-name{font-size:0.8125rem;font-weight:600;color:var(--fg);letter-spacing:-0.01em;}
|
||||
.nav-dd-desc{font-size:0.6875rem;color:var(--fg-3);margin-top:1px;line-height:1.4;}
|
||||
.nav-dd-sep{height:1px;background:var(--border);margin:4px 0;}
|
||||
.nav-burger{display:none;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:36px;height:36px;background:none;border:none;cursor:pointer;padding:4px;border-radius:var(--r-2);}
|
||||
.nav-burger span{display:block;width:20px;height:2px;background:var(--fg);border-radius:2px;transition:transform 0.22s cubic-bezier(0.16,1,0.3,1),opacity 0.18s;transform-origin:center;}
|
||||
.nav-burger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
|
||||
.nav-burger.open span:nth-child(2){opacity:0;transform:scaleX(0);}
|
||||
.nav-burger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
|
||||
.nav-mobile{display:none;position:absolute;top:60px;left:0;right:0;background:rgba(255,255,255,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,0.07);padding:12px 24px 24px;flex-direction:column;gap:2px;}
|
||||
.nav-mobile.open{display:flex;}
|
||||
.nav-mobile ul{list-style:none;display:flex;flex-direction:column;}
|
||||
.nav-mobile ul li a{display:block;font-size:1rem;font-weight:500;color:var(--fg-2);padding:11px 4px;border-bottom:1px solid var(--border);letter-spacing:-0.01em;}
|
||||
.nav-mobile ul li:last-child a{border-bottom:none;}
|
||||
.nav-mobile-ctas{display:flex;gap:8px;margin-top:16px;}
|
||||
.nav-mobile-ctas .btn{flex:1;justify-content:center;padding:10px 12px;font-size:.875rem;}
|
||||
|
||||
/* PAGE HERO */
|
||||
#page-hero{padding:140px 40px 100px;background:var(--bg-inv);position:relative;overflow:hidden;text-align:center;}
|
||||
#page-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(16,185,129,0.13),transparent 60%),radial-gradient(ellipse 40% 40% at 80% 70%,rgba(52,211,153,0.07),transparent 50%);pointer-events:none;}
|
||||
.page-hero-inner{position:relative;max-width:800px;margin:0 auto;}
|
||||
.page-hero-inner .label{color:rgba(255,255,255,0.3);margin-bottom:20px;display:block;}
|
||||
.page-hero-inner .d1{color:var(--fg-inv);margin-bottom:22px;}
|
||||
.page-hero-inner .subtitle{color:var(--fg-inv-2);max-width:600px;margin:0 auto 44px;}
|
||||
.page-hero-ctas{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* PRODUCT VISUAL */
|
||||
.pv-shell{width:100%;max-width:960px;margin:72px auto 0;border-radius:16px 16px 0 0;overflow:hidden;box-shadow:0 48px 96px rgba(0,0,0,0.45),0 0 0 1px rgba(255,255,255,0.06);border-bottom:none;position:relative;}
|
||||
.pv-chrome{background:#1C1C22;height:40px;display:flex;align-items:center;padding:0 14px;gap:7px;border-bottom:1px solid rgba(255,255,255,0.05);}
|
||||
.pv-dot{width:11px;height:11px;border-radius:50%;}
|
||||
.pv-dot.r{background:#FF5F57;}
|
||||
.pv-dot.y{background:#FFBD2E;}
|
||||
.pv-dot.g{background:#28C840;}
|
||||
.pv-url{flex:1;margin:0 12px;background:rgba(255,255,255,0.06);border-radius:5px;height:25px;display:flex;align-items:center;padding:0 10px;font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.3);max-width:320px;}
|
||||
.pv-canvas{background:#0D0D11;height:400px;position:relative;display:flex;align-items:center;justify-content:center;}
|
||||
.pv-grid{position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,0.055) 1px,transparent 1px);background-size:24px 24px;pointer-events:none;}
|
||||
|
||||
/* COMPLETION BADGE */
|
||||
.pv-cz-badge{position:absolute;top:16px;right:16px;display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:6px;background:rgba(16,185,129,0.12);border:1px solid rgba(16,185,129,0.25);}
|
||||
.pv-cz-dot{width:6px;height:6px;border-radius:50%;background:#10B981;box-shadow:0 0 0 2px rgba(16,185,129,0.25);animation:cz-pulse 2.4s ease-in-out infinite;}
|
||||
@keyframes cz-pulse{0%,100%{box-shadow:0 0 0 2px rgba(16,185,129,0.25);}50%{box-shadow:0 0 0 5px rgba(16,185,129,0.08);}}
|
||||
.pv-cz-txt{font-family:var(--mono);font-size:0.5625rem;color:#10B981;font-weight:600;}
|
||||
|
||||
/* Dashed placeholder shimmer */
|
||||
@keyframes shimmer-drift{0%{background-position:-200% 0;}100%{background-position:200% 0;}}
|
||||
.cz-placeholder{border:1.5px dashed rgba(16,185,129,0.45);border-radius:8px;background:linear-gradient(90deg,rgba(16,185,129,0.03) 0%,rgba(16,185,129,0.09) 40%,rgba(52,211,153,0.12) 50%,rgba(16,185,129,0.09) 60%,rgba(16,185,129,0.03) 100%);background-size:200% 100%;animation:shimmer-drift 2.8s ease-in-out infinite;display:flex;align-items:center;justify-content:center;box-shadow:0 0 18px rgba(16,185,129,0.1),inset 0 0 10px rgba(16,185,129,0.04);}
|
||||
.cz-q{font-family:var(--mono);font-size:1.125rem;color:rgba(16,185,129,0.55);font-weight:700;}
|
||||
|
||||
/* HOW IT WORKS */
|
||||
.hiw-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:64px;position:relative;}
|
||||
.hiw-steps::before{content:'';position:absolute;top:28px;left:12.5%;right:12.5%;height:1px;background:linear-gradient(90deg,transparent,var(--border-2) 20%,var(--border-2) 80%,transparent);}
|
||||
.hiw-step{display:flex;flex-direction:column;align-items:center;text-align:center;padding:0 20px;position:relative;z-index:1;}
|
||||
.hiw-num{width:56px;height:56px;border-radius:50%;background:var(--bg);border:1px solid var(--border-2);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--green);margin-bottom:20px;box-shadow:var(--sh-sm);}
|
||||
.hiw-step h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.hiw-step p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;}
|
||||
.hiw-step-tag{display:inline-block;margin-top:10px;font-family:var(--mono);font-size:0.5625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--green);background:var(--green-soft);border:1px solid var(--green-mid);padding:2px 8px;border-radius:var(--r-full);}
|
||||
|
||||
/* FEATURE SPLIT */
|
||||
.feat-split{display:grid;grid-template-columns:1fr 1fr;gap:96px;align-items:center;max-width:1120px;margin:0 auto;}
|
||||
.feat-split.rev{direction:rtl;}
|
||||
.feat-split.rev>*{direction:ltr;}
|
||||
.feat-copy .label{display:block;margin-bottom:16px;}
|
||||
.feat-copy .d3{margin-bottom:16px;}
|
||||
.feat-copy .subtitle{margin-bottom:28px;}
|
||||
.feat-pts{list-style:none;display:flex;flex-direction:column;gap:12px;margin-bottom:32px;}
|
||||
.feat-pts li{display:flex;align-items:flex-start;gap:10px;font-size:0.9375rem;color:var(--fg-2);line-height:1.55;}
|
||||
.feat-pt-icon{width:20px;height:20px;border-radius:3px;background:var(--green-soft);border:1px solid var(--green-mid);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;font-size:0.625rem;color:var(--green);}
|
||||
|
||||
/* Feature visual */
|
||||
.feat-vis{border-radius:var(--r-5);overflow:hidden;box-shadow:var(--sh-2xl);border:1px solid var(--border);aspect-ratio:4/3;position:relative;}
|
||||
.feat-vis-dark{background:#0D0D11;border-color:rgba(255,255,255,0.07);}
|
||||
.feat-vis-light{background:var(--bg-muted);}
|
||||
|
||||
/* COMPARISON VISUAL */
|
||||
.cz-compare{display:grid;grid-template-columns:1fr 1fr;height:100%;}
|
||||
.cz-compare-col{padding:20px 18px;display:flex;flex-direction:column;gap:10px;}
|
||||
.cz-compare-col.bad{border-right:1px solid rgba(255,255,255,0.05);}
|
||||
.cz-col-head{display:flex;align-items:center;gap:7px;margin-bottom:4px;}
|
||||
.cz-col-badge{font-family:var(--mono);font-size:0.5rem;letter-spacing:0.1em;text-transform:uppercase;padding:3px 8px;border-radius:var(--r-full);font-weight:600;}
|
||||
.cz-col-badge.bad{background:rgba(239,68,68,0.1);color:#F87171;border:1px solid rgba(239,68,68,0.2);}
|
||||
.cz-col-badge.good{background:rgba(16,185,129,0.1);color:#34D399;border:1px solid rgba(16,185,129,0.22);}
|
||||
.cz-row{display:flex;align-items:flex-start;gap:7px;padding:7px 9px;border-radius:6px;}
|
||||
.cz-row.bad-row{background:rgba(239,68,68,0.05);}
|
||||
.cz-row.good-row{background:rgba(16,185,129,0.05);}
|
||||
.cz-row-icon{font-size:0.625rem;flex-shrink:0;margin-top:1px;}
|
||||
.cz-row-text{font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.55);line-height:1.55;}
|
||||
.cz-row-text strong{display:block;font-weight:600;margin-bottom:1px;}
|
||||
.cz-row-text strong.bad-txt{color:#F87171;}
|
||||
.cz-row-text strong.good-txt{color:#34D399;}
|
||||
|
||||
/* TECH SECTION */
|
||||
.tech-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:56px;}
|
||||
.tech-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:32px 28px;transition:box-shadow 0.2s;}
|
||||
.tech-card:hover{box-shadow:var(--sh-lg);}
|
||||
.tech-icon{width:44px;height:44px;background:var(--green-soft);border:1px solid var(--green-mid);border-radius:var(--r-2);display:flex;align-items:center;justify-content:center;font-size:1.25rem;margin-bottom:18px;}
|
||||
.tech-card h3{font-size:1rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.tech-card p{font-size:0.875rem;color:var(--fg-2);line-height:1.65;}
|
||||
.tech-tag{display:inline-block;margin-top:14px;font-family:var(--mono);font-size:0.625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--green);background:var(--green-soft);border:1px solid var(--green-mid);padding:3px 9px;border-radius:var(--r-full);}
|
||||
|
||||
/* CODE BLOCK */
|
||||
.code-block{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);padding:24px;font-family:var(--mono);font-size:0.75rem;line-height:1.75;color:rgba(255,255,255,0.75);overflow-x:auto;}
|
||||
.code-block .kw{color:#7EB8FF;}
|
||||
.code-block .str{color:#7DDBA0;}
|
||||
.code-block .cm{color:rgba(255,255,255,0.28);}
|
||||
.code-block .fn{color:#FFBA7B;}
|
||||
.code-block .nm{color:#D4A0FF;}
|
||||
.code-block .grn{color:#34D399;}
|
||||
|
||||
/* RELATED */
|
||||
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:48px;}
|
||||
.related-card{border:1px solid var(--border);border-radius:var(--r-4);padding:24px;transition:box-shadow 0.15s,border-color 0.15s;display:flex;flex-direction:column;gap:10px;}
|
||||
.related-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.related-icon{font-size:1.25rem;}
|
||||
.related-card h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;}
|
||||
.related-card p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;flex:1;}
|
||||
.related-link{font-size:0.8125rem;font-weight:600;color:var(--green);}
|
||||
|
||||
/* CTA */
|
||||
#cta-bottom{background:var(--bg-inv);padding:120px 40px;position:relative;overflow:hidden;text-align:center;}
|
||||
#cta-bottom::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 30% 50%,rgba(16,185,129,0.1),transparent 60%),radial-gradient(ellipse 60% 80% at 70% 50%,rgba(52,211,153,0.07),transparent 60%);pointer-events:none;}
|
||||
.cta-inner{position:relative;max-width:680px;margin:0 auto;}
|
||||
.cta-headline{font-size:clamp(2rem,4.5vw,3.5rem);font-weight:900;letter-spacing:-0.045em;color:var(--fg-inv);line-height:1.05;margin-bottom:18px;}
|
||||
.cta-sub{font-size:1.0625rem;color:var(--fg-inv-2);margin-bottom:44px;line-height:1.65;}
|
||||
.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* FOOTER */
|
||||
footer{background:#08080C;border-top:1px solid rgba(255,255,255,0.06);padding:72px 40px 40px;}
|
||||
.ft-inner{max-width:1120px;margin:0 auto;}
|
||||
.ft-top{display:grid;grid-template-columns:260px repeat(4,1fr);gap:48px;padding-bottom:56px;border-bottom:1px solid rgba(255,255,255,0.07);margin-bottom:36px;}
|
||||
.ft-brand-logo{font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:rgba(255,255,255,0.92);margin-bottom:8px;}
|
||||
.ft-brand-logo span{color:#3385FF;}
|
||||
.ft-brand-tag{font-size:0.875rem;color:rgba(255,255,255,0.3);line-height:1.65;max-width:200px;}
|
||||
.ft-col h4{font-size:0.875rem;font-weight:600;color:rgba(255,255,255,0.7);margin-bottom:16px;letter-spacing:-0.01em;}
|
||||
.ft-col ul{list-style:none;display:flex;flex-direction:column;gap:10px;}
|
||||
.ft-col a{font-size:0.875rem;color:rgba(255,255,255,0.32);transition:color 0.14s;letter-spacing:-0.01em;}
|
||||
.ft-col a:hover{color:rgba(255,255,255,0.65);}
|
||||
.ft-bottom{display:flex;justify-content:space-between;align-items:center;}
|
||||
.ft-copy{font-size:0.8125rem;color:rgba(255,255,255,0.22);}
|
||||
|
||||
/* REVEAL */
|
||||
.reveal{opacity:0;transform:translateY(22px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1);}
|
||||
.reveal.in{opacity:1;transform:none;}
|
||||
.stagger>*{opacity:0;transform:translateY(16px);transition:opacity 0.55s cubic-bezier(0.16,1,0.3,1),transform 0.55s cubic-bezier(0.16,1,0.3,1);}
|
||||
.stagger.in>*:nth-child(1){opacity:1;transform:none;transition-delay:0s;}
|
||||
.stagger.in>*:nth-child(2){opacity:1;transform:none;transition-delay:0.07s;}
|
||||
.stagger.in>*:nth-child(3){opacity:1;transform:none;transition-delay:0.14s;}
|
||||
.stagger.in>*:nth-child(4){opacity:1;transform:none;transition-delay:0.21s;}
|
||||
.stagger.in>*:nth-child(5){opacity:1;transform:none;transition-delay:0.28s;}
|
||||
.stagger.in>*:nth-child(6){opacity:1;transform:none;transition-delay:0.35s;}
|
||||
|
||||
@media(max-width:1024px){.feat-split{grid-template-columns:1fr;gap:56px;}.feat-split.rev{direction:ltr;}.hiw-steps{grid-template-columns:1fr 1fr;gap:40px;}.hiw-steps::before{display:none;}.tech-grid{grid-template-columns:1fr 1fr;}.related-grid{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:768px){.wrap{padding:0 24px;}#nav{padding:0 24px;}.nav-inner{display:flex;justify-content:space-between;}.nav-links,.nav-actions .btn{display:none;}.nav-burger{display:flex;}#page-hero{padding:120px 24px 80px;}.pv-shell{margin-top:48px;}.hiw-steps{grid-template-columns:1fr;}.tech-grid,.related-grid{grid-template-columns:1fr;}.ft-top{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:480px){.ft-top{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo">Origin<span>main</span></a>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-dropdown">
|
||||
<a href="/#features" style="display:flex;align-items:center;gap:4px;">
|
||||
Product
|
||||
<svg class="nav-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a class="nav-dd-item" href="/features/live-artboard.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Live Artboard</span><span class="nav-dd-desc">Your app rendered live in the canvas</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/origin-graph.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Origin Graph</span><span class="nav-dd-desc">Every component, indexed and queryable</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/intent-diff.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Intent Diff</span><span class="nav-dd-desc">Ship component-level diffs, not redlines</span></div>
|
||||
</a>
|
||||
<div class="nav-dd-sep"></div>
|
||||
<a class="nav-dd-item active" href="/features/completion-zone.html">
|
||||
<div class="nav-dd-icon green"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Completion Zone</span><span class="nav-dd-desc">AI-filled design, constrained to your system</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/agent-bridge.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Agent Bridge</span><span class="nav-dd-desc">MCP channel to Cursor, Claude Code & more</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/integrations.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Integrations</span><span class="nav-dd-desc">Linear, Slack, GitHub, Figma & more</span></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<a href="#" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-mobile" id="nav-mobile">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/#features" id="mob-product-toggle" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Product
|
||||
<svg id="mob-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none" style="transition:transform 0.15s;"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-mobile-sub" id="nav-mobile-sub">
|
||||
<a href="/features/live-artboard.html">Live Artboard</a>
|
||||
<a href="/features/origin-graph.html">Origin Graph</a>
|
||||
<a href="/features/intent-diff.html">Intent Diff</a>
|
||||
<a href="/features/completion-zone.html">Completion Zone</a>
|
||||
<a href="/features/agent-bridge.html">Agent Bridge</a>
|
||||
<a href="/features/integrations.html">Integrations</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="#" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section id="page-hero">
|
||||
<div class="page-hero-inner">
|
||||
<span class="label reveal">◈ Completion Zone</span>
|
||||
<h1 class="d1 reveal">AI-filled design,<br>constrained to your system.</h1>
|
||||
<p class="subtitle reveal">Describe a component in plain language. Completion Zone generates it using only tokens, variants, and patterns that already exist in your design system — no hallucinated styles, no drift.</p>
|
||||
<div class="page-hero-ctas reveal">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="#how-it-works" class="btn btn-outline-white btn-lg">See how it works ↓</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product visual: before / after canvas -->
|
||||
<div class="pv-shell reveal">
|
||||
<div class="pv-chrome">
|
||||
<div class="pv-dot r"></div>
|
||||
<div class="pv-dot y"></div>
|
||||
<div class="pv-dot g"></div>
|
||||
<div class="pv-url">🔒 app.originmain.io/canvas — Completion Zone</div>
|
||||
</div>
|
||||
<div class="pv-canvas">
|
||||
<div class="pv-grid"></div>
|
||||
<div class="pv-cz-badge"><div class="pv-cz-dot"></div><span class="pv-cz-txt">Constrained generation</span></div>
|
||||
|
||||
<!-- BEFORE: partial card with dashed placeholder -->
|
||||
<div style="position:absolute;left:76px;top:36px;width:256px;">
|
||||
<div style="font-family:var(--mono);font-size:0.475rem;color:rgba(16,185,129,0.55);margin-bottom:5px;">MetricsCard — completing…</div>
|
||||
<div style="background:#1A1A22;border-radius:10px;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,0.6);border:1px solid rgba(255,255,255,0.07);">
|
||||
<div style="padding:14px 14px 0;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<span style="font-size:0.625rem;font-weight:700;color:rgba(255,255,255,0.8);letter-spacing:-0.01em;">Weekly Overview</span>
|
||||
<span style="font-size:0.475rem;background:rgba(16,185,129,0.12);color:#34D399;padding:2px 6px;border-radius:99px;font-weight:600;border:1px solid rgba(16,185,129,0.2);">Live</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
|
||||
<div style="background:rgba(255,255,255,0.04);border-radius:5px;padding:8px 9px;border:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size:0.45rem;color:rgba(255,255,255,0.3);margin-bottom:2px;font-family:var(--mono);">Sessions</div>
|
||||
<div style="font-size:0.875rem;font-weight:800;color:rgba(255,255,255,0.88);letter-spacing:-0.04em;">4,821</div>
|
||||
<div style="font-size:0.45rem;color:#34D399;margin-top:1px;">↑ 12%</div>
|
||||
</div>
|
||||
<div style="background:rgba(255,255,255,0.04);border-radius:5px;padding:8px 9px;border:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size:0.45rem;color:rgba(255,255,255,0.3);margin-bottom:2px;font-family:var(--mono);">Conversions</div>
|
||||
<div style="font-size:0.875rem;font-weight:800;color:rgba(255,255,255,0.88);letter-spacing:-0.04em;">9.4%</div>
|
||||
<div style="font-size:0.45rem;color:#34D399;margin-top:1px;">↑ 2.1%</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dashed shimmer placeholder -->
|
||||
<div class="cz-placeholder" style="height:48px;border-radius:6px;margin-bottom:12px;position:relative;">
|
||||
<span class="cz-q">?</span>
|
||||
<span style="position:absolute;top:-8px;right:7px;font-family:var(--mono);font-size:0.425rem;color:rgba(16,185,129,0.55);background:#0D0D11;padding:0 4px;">metric badge</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:0 14px 12px;">
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span style="font-size:0.45rem;color:rgba(255,255,255,0.3);font-family:var(--mono);">Goal progress</span><span style="font-size:0.45rem;color:rgba(255,255,255,0.5);font-family:var(--mono);">68%</span></div>
|
||||
<div style="height:2.5px;background:rgba(255,255,255,0.06);border-radius:99px;overflow:hidden;"><div style="height:100%;width:68%;background:linear-gradient(90deg,#10B981,#34D399);border-radius:99px;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Prompt input -->
|
||||
<div style="margin-top:8px;background:#111118;border:1px solid rgba(16,185,129,0.3);border-radius:7px;padding:7px 10px;display:flex;align-items:center;gap:7px;box-shadow:0 0 0 3px rgba(16,185,129,0.06);">
|
||||
<span style="font-size:0.475rem;color:rgba(16,185,129,0.7);"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.55);flex:1;">Add a metric badge to the top-right corner</span>
|
||||
<span style="display:inline-block;width:1.5px;height:10px;background:#10B981;border-radius:1px;animation:cz-pulse 1s ease-in-out infinite;"></span>
|
||||
</div>
|
||||
<div style="display:flex;gap:5px;margin-top:5px;padding:0 1px;align-items:center;">
|
||||
<div style="font-family:var(--mono);font-size:0.45rem;color:rgba(255,255,255,0.22);">Matching:</div>
|
||||
<div style="font-family:var(--mono);font-size:0.45rem;color:rgba(16,185,129,0.65);background:rgba(16,185,129,0.07);padding:1px 5px;border-radius:3px;border:1px solid rgba(16,185,129,0.15);">MetricBadge</div>
|
||||
<div style="font-family:var(--mono);font-size:0.45rem;color:rgba(255,255,255,0.22);">·</div>
|
||||
<div style="font-family:var(--mono);font-size:0.45rem;color:rgba(16,185,129,0.65);background:rgba(16,185,129,0.07);padding:1px 5px;border-radius:3px;border:1px solid rgba(16,185,129,0.15);">color.success</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:3px;z-index:10;">
|
||||
<div style="font-family:var(--mono);font-size:0.45rem;color:rgba(16,185,129,0.45);white-space:nowrap;">constrained</div>
|
||||
<div style="font-size:1.125rem;color:rgba(16,185,129,0.65);">→</div>
|
||||
<div style="font-family:var(--mono);font-size:0.45rem;color:rgba(16,185,129,0.45);white-space:nowrap;">generation</div>
|
||||
</div>
|
||||
|
||||
<!-- AFTER: completed card -->
|
||||
<div style="position:absolute;right:76px;top:36px;width:244px;">
|
||||
<div style="font-family:var(--mono);font-size:0.475rem;color:rgba(16,185,129,0.55);margin-bottom:5px;">MetricsCard — completed</div>
|
||||
<div style="background:#1A1A22;border-radius:10px;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,0.6),0 0 0 1.5px rgba(16,185,129,0.28);border:1px solid rgba(16,185,129,0.18);">
|
||||
<div style="padding:14px 14px 0;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
|
||||
<span style="font-size:0.625rem;font-weight:700;color:rgba(255,255,255,0.8);letter-spacing:-0.01em;">Weekly Overview</span>
|
||||
<span style="font-size:0.475rem;background:rgba(16,185,129,0.12);color:#34D399;padding:2px 6px;border-radius:99px;font-weight:600;border:1px solid rgba(16,185,129,0.2);">Live</span>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px;">
|
||||
<div style="background:rgba(255,255,255,0.04);border-radius:5px;padding:8px 9px;border:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size:0.45rem;color:rgba(255,255,255,0.3);margin-bottom:2px;font-family:var(--mono);">Sessions</div>
|
||||
<div style="font-size:0.875rem;font-weight:800;color:rgba(255,255,255,0.88);letter-spacing:-0.04em;">4,821</div>
|
||||
<div style="font-size:0.45rem;color:#34D399;margin-top:1px;">↑ 12%</div>
|
||||
</div>
|
||||
<div style="background:rgba(255,255,255,0.04);border-radius:5px;padding:8px 9px;border:1px solid rgba(255,255,255,0.06);">
|
||||
<div style="font-size:0.45rem;color:rgba(255,255,255,0.3);margin-bottom:2px;font-family:var(--mono);">Conversions</div>
|
||||
<div style="font-size:0.875rem;font-weight:800;color:rgba(255,255,255,0.88);letter-spacing:-0.04em;">9.4%</div>
|
||||
<div style="font-size:0.45rem;color:#34D399;margin-top:1px;">↑ 2.1%</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filled metric badge component -->
|
||||
<div style="background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.22);border-radius:6px;padding:8px 11px;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 0 10px rgba(16,185,129,0.07);">
|
||||
<div>
|
||||
<div style="font-size:0.425rem;font-family:var(--mono);color:rgba(255,255,255,0.3);margin-bottom:1px;">Avg. session</div>
|
||||
<div style="font-size:0.8125rem;font-weight:800;color:#34D399;letter-spacing:-0.04em;">3m 42s</div>
|
||||
</div>
|
||||
<div style="font-size:0.45rem;background:rgba(16,185,129,0.15);color:#34D399;padding:2px 7px;border-radius:99px;font-weight:700;border:1px solid rgba(16,185,129,0.25);">↑ 8%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:0 14px 12px;">
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:4px;"><span style="font-size:0.45rem;color:rgba(255,255,255,0.3);font-family:var(--mono);">Goal progress</span><span style="font-size:0.45rem;color:rgba(255,255,255,0.5);font-family:var(--mono);">68%</span></div>
|
||||
<div style="height:2.5px;background:rgba(255,255,255,0.06);border-radius:99px;overflow:hidden;"><div style="height:100%;width:68%;background:linear-gradient(90deg,#10B981,#34D399);border-radius:99px;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:5px;margin-top:6px;padding:0 1px;">
|
||||
<span style="font-family:var(--mono);font-size:0.425rem;color:rgba(16,185,129,0.65);">✓ MetricBadge variant="success" · color.success.600 · spacing.md</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section id="how-it-works" class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Architecture</span>
|
||||
<h2 class="d2">From prompt to placed component in four steps.</h2>
|
||||
<p class="subtitle">Completion Zone is not a generic AI. Every generation pass is anchored to the full context of your Origin Graph — so the output is always on-system.</p>
|
||||
</div>
|
||||
<div class="hiw-steps stagger">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Prompt captured</h3>
|
||||
<p>"Add a stats row below the chart" — your plain-language intent is parsed and the target artboard region is identified.</p>
|
||||
<span class="hiw-step-tag">Natural language</span>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Graph context injected</h3>
|
||||
<p>Relevant components, design tokens, spacing scales, and color palettes are fetched from your Origin Graph and assembled into the model context.</p>
|
||||
<span class="hiw-step-tag">Origin Graph</span>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>AI generates</h3>
|
||||
<p>Claude constrains its output strictly to the graph context — only existing variants, only tokens in your scale. No new styles are ever invented.</p>
|
||||
<span class="hiw-step-tag">Constrained output</span>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">04</div>
|
||||
<h3>Component placed</h3>
|
||||
<p>The generated component is written into the artboard, the live canvas updates, and the result is emitted as an Intent Diff ready to ship.</p>
|
||||
<span class="hiw-step-tag">Live Artboard</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DEEP DIVE 1: Not generation — completion -->
|
||||
<div class="section" style="padding:100px 40px;">
|
||||
<div class="feat-split reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-green">Key differentiator</span>
|
||||
<h2 class="d3">Not generation —<br>completion.</h2>
|
||||
<p class="subtitle">Generic AI tools invent. Completion Zone finishes. The difference is the constraint layer: every output token is validated against your existing design system before anything touches the canvas.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Zero design drift — the AI only uses tokens that already exist in your system, never creating new ones.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Only variants that exist — if your Button has three variants, the AI can only choose from those three.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Only spacings from your scale — no magic numbers, no arbitrary pixel values slipping through.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Generates real code, not images — the output is a live component on your artboard, not a static screenshot.</li>
|
||||
</ul>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
|
||||
<!-- Two-column comparison visual -->
|
||||
<div class="feat-vis feat-vis-dark reveal" style="overflow:hidden;">
|
||||
<div class="cz-compare" style="height:100%;">
|
||||
<!-- Generic AI -->
|
||||
<div class="cz-compare-col bad">
|
||||
<div class="cz-col-head">
|
||||
<span class="cz-col-badge bad">Generic AI</span>
|
||||
</div>
|
||||
<div style="font-family:var(--mono);font-size:0.475rem;color:rgba(255,255,255,0.2);margin-bottom:5px;letter-spacing:0.06em;text-transform:uppercase;">Output</div>
|
||||
<div class="cz-row bad-row">
|
||||
<span class="cz-row-icon" style="color:#F87171;">✗</span>
|
||||
<div class="cz-row-text"><strong class="bad-txt">Arbitrary styles</strong>background: #2D3748 · padding: 14px 18px</div>
|
||||
</div>
|
||||
<div class="cz-row bad-row">
|
||||
<span class="cz-row-icon" style="color:#F87171;">✗</span>
|
||||
<div class="cz-row-text"><strong class="bad-txt">New tokens introduced</strong>--new-color-7: #1a202c</div>
|
||||
</div>
|
||||
<div class="cz-row bad-row">
|
||||
<span class="cz-row-icon" style="color:#F87171;">✗</span>
|
||||
<div class="cz-row-text"><strong class="bad-txt">Inconsistent variants</strong>Button size="xl" — not in your system</div>
|
||||
</div>
|
||||
<div class="cz-row bad-row">
|
||||
<span class="cz-row-icon" style="color:#F87171;">✗</span>
|
||||
<div class="cz-row-text"><strong class="bad-txt">Magic numbers</strong>border-radius: 7px · margin: 11px</div>
|
||||
</div>
|
||||
<div class="cz-row bad-row">
|
||||
<span class="cz-row-icon" style="color:#F87171;">✗</span>
|
||||
<div class="cz-row-text"><strong class="bad-txt">Hallucinated components</strong>StatsWidget — not in your library</div>
|
||||
</div>
|
||||
<div style="margin-top:auto;padding:8px 9px;background:rgba(239,68,68,0.06);border:1px solid rgba(239,68,68,0.12);border-radius:5px;">
|
||||
<div style="font-family:var(--mono);font-size:0.475rem;color:rgba(239,68,68,0.6);">Result: design drift. Manual cleanup required.</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Completion Zone -->
|
||||
<div class="cz-compare-col good">
|
||||
<div class="cz-col-head">
|
||||
<span class="cz-col-badge good">◈ Completion Zone</span>
|
||||
</div>
|
||||
<div style="font-family:var(--mono);font-size:0.475rem;color:rgba(255,255,255,0.2);margin-bottom:5px;letter-spacing:0.06em;text-transform:uppercase;">Output</div>
|
||||
<div class="cz-row good-row">
|
||||
<span class="cz-row-icon" style="color:#34D399;">✓</span>
|
||||
<div class="cz-row-text"><strong class="good-txt">Existing tokens only</strong>color.success.600 · spacing.md</div>
|
||||
</div>
|
||||
<div class="cz-row good-row">
|
||||
<span class="cz-row-icon" style="color:#34D399;">✓</span>
|
||||
<div class="cz-row-text"><strong class="good-txt">Zero new tokens</strong>Only what's already in your Origin Graph</div>
|
||||
</div>
|
||||
<div class="cz-row good-row">
|
||||
<span class="cz-row-icon" style="color:#34D399;">✓</span>
|
||||
<div class="cz-row-text"><strong class="good-txt">Constrained variants</strong>Button variant="primary" size="md" — verified</div>
|
||||
</div>
|
||||
<div class="cz-row good-row">
|
||||
<span class="cz-row-icon" style="color:#34D399;">✓</span>
|
||||
<div class="cz-row-text"><strong class="good-txt">Scale-only spacings</strong>All values drawn from your spacing scale</div>
|
||||
</div>
|
||||
<div class="cz-row good-row">
|
||||
<span class="cz-row-icon" style="color:#34D399;">✓</span>
|
||||
<div class="cz-row-text"><strong class="good-txt">Verified components</strong>MetricBadge — confirmed in your library</div>
|
||||
</div>
|
||||
<div style="margin-top:auto;padding:8px 9px;background:rgba(16,185,129,0.06);border:1px solid rgba(16,185,129,0.14);border-radius:5px;">
|
||||
<div style="font-family:var(--mono);font-size:0.475rem;color:rgba(16,185,129,0.7);">Result: zero drift. Ships as-is.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEEP DIVE 2: Full graph context -->
|
||||
<div class="section section-muted" style="padding:100px 40px;">
|
||||
<div class="feat-split rev reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-green">Full graph context</span>
|
||||
<h2 class="d3">The AI knows<br>your entire system.</h2>
|
||||
<p class="subtitle">Before Claude generates a single character, it receives the complete context of your Origin Graph — every token, every component name, every spacing value. The constraint is baked into the prompt, not tacked on afterward.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>All design tokens — color palettes, typography scales, spacing systems — injected verbatim into the system prompt.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>All component names, accepted props, and valid variant combinations from your live component library.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>The current artboard snapshot — surrounding components inform what fits, stylistically and spatially.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Accepted completions feed back into the graph, making every future suggestion smarter over time.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Pseudo-code panel -->
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:18px 16px;overflow:hidden;display:flex;flex-direction:column;">
|
||||
<div style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.2);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:10px;border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:7px;">Context assembly — completion_zone.ts</div>
|
||||
<div style="flex:1;overflow:hidden;font-family:var(--mono);font-size:0.5875rem;line-height:1.85;color:rgba(255,255,255,0.72);background:#111118;border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:14px 16px;overflow-y:auto;">
|
||||
<span style="color:rgba(255,255,255,0.28);">// 1. Fetch full Origin Graph context</span>
|
||||
<span style="color:#7EB8FF;">const</span> <span style="color:#D4A0FF;">graph</span> = <span style="color:#7EB8FF;">await</span> <span style="color:#FFBA7B;">originGraph</span>.<span style="color:#FFBA7B;">getContext</span>({
|
||||
tokens: <span style="color:#7DDBA0;">true</span>, <span style="color:rgba(255,255,255,0.28);">// all design tokens</span>
|
||||
components: <span style="color:#7DDBA0;">true</span>, <span style="color:rgba(255,255,255,0.28);">// all names + props</span>
|
||||
variants: <span style="color:#7DDBA0;">true</span>, <span style="color:rgba(255,255,255,0.28);">// valid combos only</span>
|
||||
snapshot: <span style="color:#D4A0FF;">artboard</span>.<span style="color:#FFBA7B;">snapshot</span>(),
|
||||
});
|
||||
|
||||
<span style="color:rgba(255,255,255,0.28);">// 2. Build constrained system prompt</span>
|
||||
<span style="color:#7EB8FF;">const</span> <span style="color:#D4A0FF;">systemPrompt</span> = <span style="color:#FFBA7B;">buildPrompt</span>({
|
||||
instruction: <span style="color:#7DDBA0;">"Only use tokens in this list. "</span>
|
||||
+ <span style="color:#7DDBA0;">"Never invent new tokens or variants."</span>,
|
||||
tokens: <span style="color:#D4A0FF;">graph</span>.<span style="color:#D4A0FF;">tokens</span>,
|
||||
components: <span style="color:#D4A0FF;">graph</span>.<span style="color:#D4A0FF;">components</span>,
|
||||
artboard: <span style="color:#D4A0FF;">graph</span>.<span style="color:#D4A0FF;">snapshot</span>,
|
||||
});
|
||||
|
||||
<span style="color:rgba(255,255,255,0.28);">// 3. Call Claude with full context</span>
|
||||
<span style="color:#7EB8FF;">const</span> <span style="color:#D4A0FF;">result</span> = <span style="color:#7EB8FF;">await</span> <span style="color:#FFBA7B;">claude</span>.<span style="color:#FFBA7B;">complete</span>({
|
||||
system: <span style="color:#D4A0FF;">systemPrompt</span>,
|
||||
prompt: <span style="color:#D4A0FF;">userIntent</span>,
|
||||
model: <span style="color:#7DDBA0;">"claude-sonnet-4-6"</span>,
|
||||
});
|
||||
|
||||
<span style="color:rgba(255,255,255,0.28);">// 4. Validate against graph</span>
|
||||
<span style="color:#7EB8FF;">const</span> <span style="color:#D4A0FF;">validated</span> = <span style="color:#FFBA7B;">validateAgainstGraph</span>(
|
||||
<span style="color:#D4A0FF;">result</span>.<span style="color:#D4A0FF;">component</span>, <span style="color:#D4A0FF;">graph</span>
|
||||
);
|
||||
|
||||
<span style="color:rgba(255,255,255,0.28);">// 5. Place + record feedback loop</span>
|
||||
<span style="color:#7EB8FF;">if</span> (<span style="color:#D4A0FF;">validated</span>.<span style="color:#D4A0FF;">ok</span>) {
|
||||
<span style="color:#D4A0FF;">artboard</span>.<span style="color:#FFBA7B;">place</span>(<span style="color:#D4A0FF;">validated</span>.<span style="color:#D4A0FF;">component</span>);
|
||||
<span style="color:#34D399;">feedbackLoop</span>.<span style="color:#FFBA7B;">accept</span>(<span style="color:#D4A0FF;">validated</span>);
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SELLING POINTS BAND -->
|
||||
<section style="padding:72px 0;background:var(--bg-inv);">
|
||||
<div class="wrap">
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:rgba(255,255,255,0.06);border-radius:var(--r-4);overflow:hidden;" class="stagger">
|
||||
<div style="background:var(--bg-inv-2);padding:32px 28px;">
|
||||
<div style="font-size:1.5rem;margin-bottom:14px;"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<h3 style="font-size:0.9375rem;font-weight:700;color:var(--fg-inv);letter-spacing:-0.02em;margin-bottom:8px;">Zero design drift</h3>
|
||||
<p style="font-size:0.8125rem;color:var(--fg-inv-2);line-height:1.65;">Every token, every spacing value, every color reference is pulled from your existing scale. Nothing new is ever introduced into your system.</p>
|
||||
</div>
|
||||
<div style="background:var(--bg-inv-2);padding:32px 28px;">
|
||||
<div style="font-size:1.5rem;margin-bottom:14px;">⬛</div>
|
||||
<h3 style="font-size:0.9375rem;font-weight:700;color:var(--fg-inv);letter-spacing:-0.02em;margin-bottom:8px;">Generates real code</h3>
|
||||
<p style="font-size:0.8125rem;color:var(--fg-inv-2);line-height:1.65;">The output is a live component placed on your artboard — actual JSX using your real component library, not a static image or an approximation.</p>
|
||||
</div>
|
||||
<div style="background:var(--bg-inv-2);padding:32px 28px;">
|
||||
<div style="font-size:1.5rem;margin-bottom:14px;">⟳</div>
|
||||
<h3 style="font-size:0.9375rem;font-weight:700;color:var(--fg-inv);letter-spacing:-0.02em;margin-bottom:8px;">Learns from your patterns</h3>
|
||||
<p style="font-size:0.8125rem;color:var(--fg-inv-2);line-height:1.65;">Every accepted completion feeds back into the Origin Graph. The more you build, the better Completion Zone understands your conventions.</p>
|
||||
</div>
|
||||
<div style="background:var(--bg-inv-2);padding:32px 28px;">
|
||||
<div style="font-size:1.5rem;margin-bottom:14px;"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3 style="font-size:0.9375rem;font-weight:700;color:var(--fg-inv);letter-spacing:-0.02em;margin-bottom:8px;">Respects constraints</h3>
|
||||
<p style="font-size:0.8125rem;color:var(--fg-inv-2);line-height:1.65;">Only variants that exist in your library. Only spacings from your defined scale. Hard constraints enforced in the prompt, not soft suggestions.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TECHNICAL SECTION -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Technical foundation</span>
|
||||
<h2 class="d2">Engineered for design systems.</h2>
|
||||
<p class="subtitle">Completion Zone is not a thin wrapper around an LLM. It's a constraint engine built from the ground up for the specific demands of systematic design at scale.</p>
|
||||
</div>
|
||||
<div class="tech-grid stagger">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⚡</div>
|
||||
<h3>Claude API + context injection</h3>
|
||||
<p>Claude powers generation, but the constraint work happens before and after the API call. The system prompt is fully assembled from your Origin Graph on every request — no stale cached context, no drift over time.</p>
|
||||
<span class="tech-tag">Claude API</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<h3>Token-constrained generation</h3>
|
||||
<p>A validation layer sits between the LLM output and the canvas. Every proposed token value is checked against your design token registry. Invalid tokens are flagged or substituted with the nearest valid match from your scale.</p>
|
||||
<span class="tech-tag">Constraint layer</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3>Graph context retrieval</h3>
|
||||
<p>The Origin Graph is queried with a relevance function to fetch the most pertinent subset of your component library for each prompt — keeping the context window focused and the generation latency low.</p>
|
||||
<span class="tech-tag">Origin Graph</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<h3>Component snapshot diffing</h3>
|
||||
<p>Before placing a completion on the canvas, Originmain diffs it against the current artboard state. Only the minimal set of changes is applied, producing a clean Intent Diff entry with zero noise or redundancy.</p>
|
||||
<span class="tech-tag">Intent Diff</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">✦</div>
|
||||
<h3>Prompt engineering for design systems</h3>
|
||||
<p>The system prompt template communicates your design system's grammar to the model — token naming conventions, component composition patterns, spatial reasoning cues, and variant inheritance rules.</p>
|
||||
<span class="tech-tag">Prompt design</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⟳</div>
|
||||
<h3>Feedback loop</h3>
|
||||
<p>Accepted completions are written back into the Origin Graph as accepted pattern examples. Over time, these examples shift the model's priors toward your team's specific conventions without any fine-tuning or retraining.</p>
|
||||
<span class="tech-tag">Adaptive</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RELATED FEATURES -->
|
||||
<section class="section section-muted">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">What powers it</span>
|
||||
<h2 class="d2">Completion Zone runs on the full stack.</h2>
|
||||
<p class="subtitle">Every piece of Originmain contributes to making completions possible — and every completion makes the rest of the platform smarter.</p>
|
||||
</div>
|
||||
<div class="related-grid stagger">
|
||||
<a class="related-card" href="/features/origin-graph.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3>Origin Graph</h3>
|
||||
<p>The indexed knowledge base of every component, token, and relationship in your design system. Completion Zone pulls its constraints directly from the graph on every generation request.</p>
|
||||
<span class="related-link">Explore Origin Graph →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/live-artboard.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Live Artboard</h3>
|
||||
<p>Completed components are placed directly onto the live artboard — rendered as real code in a real iframe, not as a static preview. What you see on the canvas is exactly what ships.</p>
|
||||
<span class="related-link">Explore Live Artboard →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/agent-bridge.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<h3>Agent Bridge</h3>
|
||||
<p>Trigger completions from inside Cursor or Claude Code via the MCP channel. Your editor prompt invokes the same constraint engine, placing components directly from your IDE without switching context.</p>
|
||||
<span class="related-link">Explore Agent Bridge →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta-bottom">
|
||||
<div class="cta-inner">
|
||||
<h2 class="cta-headline">Fill the gaps in your<br>design system.</h2>
|
||||
<p class="cta-sub">Early access is free. Connect your Origin Graph and run your first completion in under two minutes.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="/" class="btn btn-outline-white btn-lg">Back to overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="ft-inner">
|
||||
<div class="ft-top">
|
||||
<div>
|
||||
<div class="ft-brand-logo">Origin<span>main</span></div>
|
||||
<p class="ft-brand-tag">Where design intent becomes production code.</p>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="/features/live-artboard.html">Live Artboard</a></li>
|
||||
<li><a href="/features/origin-graph.html">Origin Graph</a></li>
|
||||
<li><a href="/features/intent-diff.html">Intent Diff</a></li>
|
||||
<li><a href="/features/completion-zone.html">Completion Zone</a></li>
|
||||
<li><a href="/features/agent-bridge.html">Agent Bridge</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Integrations</h4>
|
||||
<ul>
|
||||
<li><a href="/features/integrations.html">All integrations</a></li>
|
||||
<li><a href="#">GitHub</a></li>
|
||||
<li><a href="#">Linear</a></li>
|
||||
<li><a href="#">Slack</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Developers</h4>
|
||||
<ul>
|
||||
<li><a href="#">Docs</a></li>
|
||||
<li><a href="#">API Reference</a></li>
|
||||
<li><a href="#">MCP Protocol</a></li>
|
||||
<li><a href="#">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft-bottom">
|
||||
<span class="ft-copy">© 2025 Originmain. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Nav scroll
|
||||
const nav=document.getElementById('nav');
|
||||
window.addEventListener('scroll',()=>{nav.classList.toggle('scrolled',window.scrollY>20);});
|
||||
// Dropdown: delay-based hover (desktop) + click (mobile/touch)
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd=>{const menu=dd.querySelector('.nav-dropdown-menu');let t;const open=()=>{clearTimeout(t);dd.classList.add('open');menu.classList.add('open');};const close=()=>{t=setTimeout(()=>{dd.classList.remove('open');menu.classList.remove('open');},150);};dd.addEventListener('mouseenter',open);dd.addEventListener('mouseleave',close);menu.addEventListener('mouseenter',open);menu.addEventListener('mouseleave',close);dd.querySelector('a').addEventListener('click',e=>{if(window.matchMedia('(hover:none)').matches||window.innerWidth<=1024){e.preventDefault();const o=menu.classList.toggle('open');dd.classList.toggle('open',o);}});});
|
||||
// Mobile product sub-accordion
|
||||
const mpt=document.getElementById('mob-product-toggle'),mns=document.getElementById('nav-mobile-sub'),mdc=document.getElementById('mob-dd-caret');if(mpt&&mns){mpt.addEventListener('click',e=>{e.preventDefault();const o=mns.classList.toggle('open');if(mdc)mdc.style.transform=o?'rotate(180deg)':'';});}
|
||||
// Burger
|
||||
const burger=document.getElementById('nav-burger'),mob=document.getElementById('nav-mobile');
|
||||
burger.addEventListener('click',()=>{const o=mob.classList.toggle('open');burger.classList.toggle('open',o);burger.setAttribute('aria-expanded',o);});
|
||||
// Reveal
|
||||
const io=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target);}});},{threshold:0.12});
|
||||
document.querySelectorAll('.reveal,.stagger').forEach(el=>io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,833 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Integrations — Originmain</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#FFFFFF;--bg-subtle:#FAFAFA;--bg-muted:#F4F4F5;--bg-strong:#EBEBEB;--bg-inv:#0A0A0A;--bg-inv-2:#141414;--bg-inv-3:#1C1C1C;--fg:#0A0A0A;--fg-2:#52525B;--fg-3:#A1A1AA;--fg-4:#D4D4D8;--fg-inv:rgba(255,255,255,0.92);--fg-inv-2:rgba(255,255,255,0.52);--fg-inv-3:rgba(255,255,255,0.28);--blue:#0066FF;--blue-dark:#0050CC;--blue-light:#3385FF;--blue-soft:rgba(0,102,255,0.07);--blue-mid:rgba(0,102,255,0.15);--purple:#7C3AED;--teal:#0891B2;--border:rgba(0,0,0,0.07);--border-2:rgba(0,0,0,0.12);--border-inv:rgba(255,255,255,0.08);--border-inv-2:rgba(255,255,255,0.14);--r-1:4px;--r-2:8px;--r-3:12px;--r-4:16px;--r-5:20px;--r-6:24px;--r-full:9999px;--sh-xs:0 1px 2px rgba(0,0,0,0.05);--sh-sm:0 1px 3px rgba(0,0,0,0.07),0 1px 2px rgba(0,0,0,0.05);--sh-md:0 4px 8px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04);--sh-lg:0 10px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.05);--sh-xl:0 20px 40px rgba(0,0,0,0.09),0 8px 16px rgba(0,0,0,0.05);--sh-2xl:0 32px 64px rgba(0,0,0,0.11),0 16px 32px rgba(0,0,0,0.06);--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--mono:'JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||||
html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.6;overflow-x:hidden;}
|
||||
img,svg{display:block;max-width:100%;}
|
||||
a{color:inherit;text-decoration:none;}
|
||||
button{font-family:inherit;cursor:pointer;border:none;background:none;}
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:0 40px;}
|
||||
.section{padding:112px 0;}
|
||||
.section-muted{background:var(--bg-subtle);}
|
||||
.section-dark{background:var(--bg-inv);}
|
||||
.d1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;line-height:1.02;letter-spacing:-0.045em;color:var(--fg);}
|
||||
.d2{font-size:clamp(1.875rem,3.5vw,2.75rem);font-weight:800;line-height:1.1;letter-spacing:-0.038em;color:var(--fg);}
|
||||
.d3{font-size:clamp(1.25rem,2.5vw,1.625rem);font-weight:700;line-height:1.25;letter-spacing:-0.028em;color:var(--fg);}
|
||||
.subtitle{font-size:clamp(1rem,1.8vw,1.125rem);font-weight:400;line-height:1.75;color:var(--fg-2);}
|
||||
.label{font-family:var(--mono);font-size:0.6875rem;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--fg-3);}
|
||||
.label-blue{color:var(--blue);}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font);font-size:0.9375rem;font-weight:600;padding:11px 22px;border-radius:var(--r-2);transition:transform 0.12s cubic-bezier(.16,1,.3,1),opacity 0.12s,box-shadow 0.12s;white-space:nowrap;letter-spacing:-0.01em;position:relative;overflow:hidden;}
|
||||
.btn::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,0);transition:background 0.12s;}
|
||||
.btn:hover::after{background:rgba(255,255,255,0.06);}
|
||||
.btn:active{transform:scale(0.98)!important;}
|
||||
.btn-primary{background:var(--fg);color:var(--bg);box-shadow:var(--sh-sm);}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:var(--sh-md);}
|
||||
.btn-secondary{background:transparent;color:var(--fg);border:1px solid var(--border-2);}
|
||||
.btn-secondary:hover{background:var(--bg-muted);transform:translateY(-1px);}
|
||||
.btn-blue{background:var(--blue);color:#fff;box-shadow:0 4px 12px rgba(0,102,255,0.3);}
|
||||
.btn-blue:hover{background:var(--blue-dark);transform:translateY(-1px);}
|
||||
.btn-lg{font-size:1rem;padding:13px 28px;}
|
||||
.btn-white{background:#fff;color:#0A0A0A;box-shadow:0 4px 12px rgba(255,255,255,0.15);}
|
||||
.btn-white:hover{background:#F5F5F5;transform:translateY(-1px);}
|
||||
.btn-outline-white{background:transparent;color:rgba(255,255,255,0.72);border:1px solid rgba(255,255,255,0.18);}
|
||||
.btn-outline-white:hover{background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.9);transform:translateY(-1px);}
|
||||
.divider{height:1px;background:var(--border);}
|
||||
.section-head{text-align:center;max-width:640px;margin:0 auto 72px;}
|
||||
.section-head .label{display:block;margin-bottom:16px;}
|
||||
.section-head .d2{margin-bottom:18px;}
|
||||
|
||||
/* NAV */
|
||||
#nav{position:fixed;top:0;left:0;right:0;z-index:200;height:60px;display:flex;align-items:center;padding:0 40px;transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;}
|
||||
#nav.scrolled{background:rgba(255,255,255,0.85);backdrop-filter:blur(20px) saturate(1.6);-webkit-backdrop-filter:blur(20px) saturate(1.6);border-bottom:1px solid var(--border);box-shadow:var(--sh-xs);}
|
||||
.nav-inner{max-width:1120px;margin:0 auto;width:100%;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
|
||||
.nav-logo{justify-self:start;font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:var(--fg);display:flex;align-items:center;gap:2px;}
|
||||
.nav-logo span{color:var(--blue);}
|
||||
.nav-links{justify-self:center;display:flex;align-items:center;gap:0;list-style:none;}
|
||||
.nav-links a{font-size:0.9375rem;font-weight:500;color:var(--fg-2);padding:6px 14px;border-radius:var(--r-1);transition:color 0.12s,background 0.12s;letter-spacing:-0.01em;}
|
||||
.nav-links a:hover{color:var(--fg);}
|
||||
.nav-links a.active{color:var(--blue);}
|
||||
.nav-actions{justify-self:end;display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* Dropdown */
|
||||
.nav-dropdown{position:relative;}
|
||||
.nav-dropdown>a{display:flex;align-items:center;gap:4px;}
|
||||
.nav-dd-caret{transition:transform 0.15s;}
|
||||
/* caret rotation handled via JS .open class */
|
||||
.nav-dropdown-menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-4px);background:rgba(255,255,255,0.98);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--r-3);box-shadow:var(--sh-xl);padding:8px;min-width:240px;opacity:0;visibility:hidden;transition:opacity 0.15s,transform 0.15s,visibility 0.15s;pointer-events:none;}
|
||||
.nav-dropdown-menu.open{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:all;}
|
||||
.nav-dropdown.open .nav-dd-caret{transform:rotate(180deg);}
|
||||
/* Mobile sub-nav */
|
||||
.nav-mobile-sub{display:none;padding:2px 0 6px 14px;flex-direction:column;}
|
||||
.nav-mobile-sub.open{display:flex;}
|
||||
.nav-mobile-sub a{font-size:0.875rem;color:rgba(255,255,255,0.4);padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.06);letter-spacing:-0.01em;transition:color 0.1s;}
|
||||
.nav-mobile-sub a:last-child{border-bottom:none;}
|
||||
.nav-mobile-sub a:hover{color:rgba(255,255,255,0.8);}
|
||||
/* Dark hero: white nav before scrolling */
|
||||
#nav:not(.scrolled) .nav-logo{color:rgba(255,255,255,0.92);}
|
||||
#nav:not(.scrolled) .nav-logo span{color:#3385FF;}
|
||||
#nav:not(.scrolled) .nav-links a{color:rgba(255,255,255,0.5);}
|
||||
#nav:not(.scrolled) .nav-links a:hover{color:rgba(255,255,255,0.9);}
|
||||
#nav:not(.scrolled) .nav-actions .btn-secondary{color:rgba(255,255,255,0.65);border-color:rgba(255,255,255,0.2);background:transparent;}
|
||||
#nav:not(.scrolled) .nav-burger span{background:rgba(255,255,255,0.8);}
|
||||
.nav-dd-item{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:var(--r-2);transition:background 0.1s;cursor:pointer;}
|
||||
.nav-dd-item:hover{background:var(--bg-muted);}
|
||||
.nav-dd-item.current{background:var(--blue-soft);}
|
||||
.nav-dd-icon{width:30px;height:30px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:13px;color:var(--blue);}
|
||||
.nav-dd-text{display:flex;flex-direction:column;}
|
||||
.nav-dd-name{font-size:0.8125rem;font-weight:600;color:var(--fg);letter-spacing:-0.01em;}
|
||||
.nav-dd-desc{font-size:0.6875rem;color:var(--fg-3);margin-top:1px;line-height:1.4;}
|
||||
.nav-dd-sep{height:1px;background:var(--border);margin:4px 0;}
|
||||
.nav-burger{display:none;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:36px;height:36px;background:none;border:none;cursor:pointer;padding:4px;border-radius:var(--r-2);}
|
||||
.nav-burger span{display:block;width:20px;height:2px;background:var(--fg);border-radius:2px;transition:transform 0.22s cubic-bezier(0.16,1,0.3,1),opacity 0.18s;transform-origin:center;}
|
||||
.nav-burger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
|
||||
.nav-burger.open span:nth-child(2){opacity:0;transform:scaleX(0);}
|
||||
.nav-burger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
|
||||
.nav-mobile{display:none;position:absolute;top:60px;left:0;right:0;background:rgba(255,255,255,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,0.07);padding:12px 24px 24px;flex-direction:column;gap:2px;}
|
||||
.nav-mobile.open{display:flex;}
|
||||
.nav-mobile ul{list-style:none;display:flex;flex-direction:column;}
|
||||
.nav-mobile ul li a{display:block;font-size:1rem;font-weight:500;color:var(--fg-2);padding:11px 4px;border-bottom:1px solid var(--border);letter-spacing:-0.01em;}
|
||||
.nav-mobile ul li:last-child a{border-bottom:none;}
|
||||
.nav-mobile-ctas{display:flex;gap:8px;margin-top:16px;}
|
||||
.nav-mobile-ctas .btn{flex:1;justify-content:center;padding:10px 12px;font-size:.875rem;}
|
||||
|
||||
/* PAGE HERO */
|
||||
#page-hero{padding:140px 40px 100px;background:var(--bg-inv);position:relative;overflow:hidden;text-align:center;}
|
||||
#page-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(0,102,255,0.14),transparent 60%),radial-gradient(ellipse 40% 40% at 80% 70%,rgba(0,102,255,0.06),transparent 50%),radial-gradient(ellipse 50% 50% at 15% 60%,rgba(124,58,237,0.06),transparent 50%);pointer-events:none;}
|
||||
.page-hero-inner{position:relative;max-width:800px;margin:0 auto;}
|
||||
.page-hero-inner .label{color:rgba(255,255,255,0.3);margin-bottom:20px;display:block;}
|
||||
.page-hero-inner .d1{color:var(--fg-inv);margin-bottom:22px;}
|
||||
.page-hero-inner .subtitle{color:var(--fg-inv-2);max-width:620px;margin:0 auto 44px;}
|
||||
.page-hero-ctas{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* INTEGRATION BENTO GRID */
|
||||
.int-bento{width:100%;max-width:840px;margin:72px auto 0;position:relative;}
|
||||
.int-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;}
|
||||
.int-card{background:#111118;border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:22px 20px 18px;display:flex;flex-direction:column;align-items:flex-start;gap:10px;position:relative;overflow:hidden;transition:border-color 0.2s,transform 0.2s;}
|
||||
.int-card::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,0.03),transparent 60%);pointer-events:none;}
|
||||
.int-card:hover{border-color:rgba(255,255,255,0.14);transform:translateY(-2px);}
|
||||
.int-logo{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;}
|
||||
.int-name{font-size:0.875rem;font-weight:700;color:rgba(255,255,255,0.88);letter-spacing:-0.02em;}
|
||||
.int-desc{font-size:0.6875rem;color:rgba(255,255,255,0.35);line-height:1.5;flex:1;}
|
||||
.int-status{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:0.5625rem;color:#10B981;font-weight:600;background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.2);padding:3px 8px;border-radius:var(--r-full);margin-top:4px;}
|
||||
.int-status-dot{width:5px;height:5px;border-radius:50%;background:#10B981;animation:live-pulse 2s ease-in-out infinite;}
|
||||
@keyframes live-pulse{0%,100%{box-shadow:0 0 0 2px rgba(16,185,129,0.25);}50%{box-shadow:0 0 0 4px rgba(16,185,129,0.1);}}
|
||||
|
||||
/* GitHub brand */
|
||||
.int-card.github .int-logo{background:#21262D;}
|
||||
/* Figma brand */
|
||||
.int-card.figma .int-logo{background:#1E1E2E;}
|
||||
/* Linear brand */
|
||||
.int-card.linear .int-logo{background:#0D0D1A;}
|
||||
/* Slack brand */
|
||||
.int-card.slack .int-logo{background:#1A1A2E;}
|
||||
/* VSCode brand */
|
||||
.int-card.vscode .int-logo{background:#0D1117;}
|
||||
/* Cursor brand */
|
||||
.int-card.cursor .int-logo{background:#111118;}
|
||||
/* TypeScript brand */
|
||||
.int-card.typescript .int-logo{background:#3178c6;}
|
||||
/* React brand */
|
||||
.int-card.react .int-logo{background:#0D1A1A;}
|
||||
|
||||
/* HOW IT WORKS SECTION */
|
||||
.hiw-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:64px;position:relative;}
|
||||
.hiw-steps::before{content:'';position:absolute;top:28px;left:12.5%;right:12.5%;height:1px;background:linear-gradient(90deg,transparent,var(--border-2) 20%,var(--border-2) 80%,transparent);}
|
||||
.hiw-step{display:flex;flex-direction:column;align-items:center;text-align:center;padding:0 20px;position:relative;z-index:1;}
|
||||
.hiw-num{width:56px;height:56px;border-radius:50%;background:var(--bg);border:1px solid var(--border-2);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--blue);margin-bottom:20px;box-shadow:var(--sh-sm);}
|
||||
.hiw-step h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.hiw-step p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;}
|
||||
|
||||
/* FEATURE SPLIT */
|
||||
.feat-split{display:grid;grid-template-columns:1fr 1fr;gap:96px;align-items:center;max-width:1120px;margin:0 auto;}
|
||||
.feat-split.rev{direction:rtl;}
|
||||
.feat-split.rev>*{direction:ltr;}
|
||||
.feat-copy .label{display:block;margin-bottom:16px;}
|
||||
.feat-copy .d3{margin-bottom:16px;}
|
||||
.feat-copy .subtitle{margin-bottom:28px;}
|
||||
.feat-pts{list-style:none;display:flex;flex-direction:column;gap:12px;margin-bottom:32px;}
|
||||
.feat-pts li{display:flex;align-items:flex-start;gap:10px;font-size:0.9375rem;color:var(--fg-2);line-height:1.55;}
|
||||
.feat-pt-icon{width:20px;height:20px;border-radius:3px;background:var(--blue-soft);border:1px solid var(--blue-mid);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;font-size:0.625rem;color:var(--blue);}
|
||||
|
||||
/* Feature visual */
|
||||
.feat-vis{border-radius:var(--r-5);overflow:hidden;box-shadow:var(--sh-2xl);border:1px solid var(--border);aspect-ratio:4/3;position:relative;}
|
||||
.feat-vis-dark{background:#0D0D11;border-color:rgba(255,255,255,0.07);}
|
||||
.feat-vis-light{background:var(--bg-muted);}
|
||||
|
||||
/* TECH SECTION */
|
||||
.tech-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:56px;}
|
||||
.tech-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:32px 28px;transition:box-shadow 0.2s;}
|
||||
.tech-card:hover{box-shadow:var(--sh-lg);}
|
||||
.tech-icon{width:44px;height:44px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:var(--r-2);display:flex;align-items:center;justify-content:center;font-size:1.25rem;margin-bottom:18px;}
|
||||
.tech-card h3{font-size:1rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.tech-card p{font-size:0.875rem;color:var(--fg-2);line-height:1.65;}
|
||||
.tech-tag{display:inline-block;margin-top:14px;font-family:var(--mono);font-size:0.625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--blue);background:var(--blue-soft);border:1px solid var(--blue-mid);padding:3px 9px;border-radius:var(--r-full);}
|
||||
|
||||
/* CODE BLOCK */
|
||||
.code-block{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);padding:24px;font-family:var(--mono);font-size:0.75rem;line-height:1.75;color:rgba(255,255,255,0.75);overflow-x:auto;}
|
||||
.code-block .kw{color:#7EB8FF;}
|
||||
.code-block .str{color:#7DDBA0;}
|
||||
.code-block .cm{color:rgba(255,255,255,0.28);}
|
||||
.code-block .fn{color:#FFBA7B;}
|
||||
.code-block .nm{color:#D4A0FF;}
|
||||
.code-block .pu{color:rgba(255,255,255,0.5);}
|
||||
|
||||
/* STATUS GRID */
|
||||
.status-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:56px;}
|
||||
.status-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:24px 20px;display:flex;align-items:center;gap:14px;transition:box-shadow 0.15s,border-color 0.15s;}
|
||||
.status-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.status-logo-wrap{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;}
|
||||
.status-info{flex:1;min-width:0;}
|
||||
.status-info h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:2px;}
|
||||
.status-info p{font-size:0.75rem;color:var(--fg-3);}
|
||||
.status-badge{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:0.5625rem;color:#10B981;font-weight:600;background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.18);padding:3px 8px;border-radius:var(--r-full);margin-top:6px;white-space:nowrap;}
|
||||
.status-badge-dot{width:5px;height:5px;border-radius:50%;background:#10B981;}
|
||||
|
||||
/* RELATED */
|
||||
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:48px;}
|
||||
.related-card{border:1px solid var(--border);border-radius:var(--r-4);padding:24px;transition:box-shadow 0.15s,border-color 0.15s;display:flex;flex-direction:column;gap:10px;}
|
||||
.related-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.related-icon{font-size:1.25rem;}
|
||||
.related-card h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;}
|
||||
.related-card p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;flex:1;}
|
||||
.related-link{font-size:0.8125rem;font-weight:600;color:var(--blue);}
|
||||
|
||||
/* CTA */
|
||||
#cta-bottom{background:var(--bg-inv);padding:120px 40px;position:relative;overflow:hidden;text-align:center;}
|
||||
#cta-bottom::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 30% 50%,rgba(0,102,255,0.12),transparent 60%),radial-gradient(ellipse 60% 80% at 70% 50%,rgba(0,102,255,0.08),transparent 60%);pointer-events:none;}
|
||||
.cta-inner{position:relative;max-width:680px;margin:0 auto;}
|
||||
.cta-headline{font-size:clamp(2rem,4.5vw,3.5rem);font-weight:900;letter-spacing:-0.045em;color:var(--fg-inv);line-height:1.05;margin-bottom:18px;}
|
||||
.cta-sub{font-size:1.0625rem;color:var(--fg-inv-2);margin-bottom:44px;line-height:1.65;}
|
||||
.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* FOOTER */
|
||||
footer{background:#08080C;border-top:1px solid rgba(255,255,255,0.06);padding:72px 40px 40px;}
|
||||
.ft-inner{max-width:1120px;margin:0 auto;}
|
||||
.ft-top{display:grid;grid-template-columns:260px repeat(4,1fr);gap:48px;padding-bottom:56px;border-bottom:1px solid rgba(255,255,255,0.07);margin-bottom:36px;}
|
||||
.ft-brand-logo{font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:rgba(255,255,255,0.92);margin-bottom:8px;}
|
||||
.ft-brand-logo span{color:#3385FF;}
|
||||
.ft-brand-tag{font-size:0.875rem;color:rgba(255,255,255,0.3);line-height:1.65;max-width:200px;}
|
||||
.ft-col h4{font-size:0.875rem;font-weight:600;color:rgba(255,255,255,0.7);margin-bottom:16px;letter-spacing:-0.01em;}
|
||||
.ft-col ul{list-style:none;display:flex;flex-direction:column;gap:10px;}
|
||||
.ft-col a{font-size:0.875rem;color:rgba(255,255,255,0.32);transition:color 0.14s;letter-spacing:-0.01em;}
|
||||
.ft-col a:hover{color:rgba(255,255,255,0.65);}
|
||||
.ft-bottom{display:flex;justify-content:space-between;align-items:center;}
|
||||
.ft-copy{font-size:0.8125rem;color:rgba(255,255,255,0.22);}
|
||||
|
||||
/* REVEAL */
|
||||
.reveal{opacity:0;transform:translateY(22px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1);}
|
||||
.reveal.in{opacity:1;transform:none;}
|
||||
.stagger>*{opacity:0;transform:translateY(16px);transition:opacity 0.55s cubic-bezier(0.16,1,0.3,1),transform 0.55s cubic-bezier(0.16,1,0.3,1);}
|
||||
.stagger.in>*:nth-child(1){opacity:1;transform:none;transition-delay:0s;}
|
||||
.stagger.in>*:nth-child(2){opacity:1;transform:none;transition-delay:0.07s;}
|
||||
.stagger.in>*:nth-child(3){opacity:1;transform:none;transition-delay:0.14s;}
|
||||
.stagger.in>*:nth-child(4){opacity:1;transform:none;transition-delay:0.21s;}
|
||||
.stagger.in>*:nth-child(5){opacity:1;transform:none;transition-delay:0.28s;}
|
||||
.stagger.in>*:nth-child(6){opacity:1;transform:none;transition-delay:0.35s;}
|
||||
.stagger.in>*:nth-child(7){opacity:1;transform:none;transition-delay:0.42s;}
|
||||
.stagger.in>*:nth-child(8){opacity:1;transform:none;transition-delay:0.49s;}
|
||||
|
||||
@media(max-width:1024px){.feat-split{grid-template-columns:1fr;gap:56px;}.feat-split.rev{direction:ltr;}.hiw-steps{grid-template-columns:1fr 1fr;gap:40px;}.hiw-steps::before{display:none;}.tech-grid{grid-template-columns:1fr 1fr;}.related-grid{grid-template-columns:1fr 1fr;}.status-grid{grid-template-columns:1fr 1fr;}.int-grid{grid-template-columns:repeat(2,1fr);}}
|
||||
@media(max-width:768px){.wrap{padding:0 24px;}#nav{padding:0 24px;}.nav-inner{display:flex;justify-content:space-between;}.nav-links,.nav-actions .btn{display:none;}.nav-burger{display:flex;}#page-hero{padding:120px 24px 80px;}.int-bento{margin-top:48px;}.hiw-steps{grid-template-columns:1fr;}.tech-grid,.related-grid,.status-grid{grid-template-columns:1fr;}.ft-top{grid-template-columns:1fr 1fr;}.int-grid{grid-template-columns:repeat(2,1fr);}}
|
||||
@media(max-width:480px){.ft-top{grid-template-columns:1fr;}.int-grid{grid-template-columns:1fr 1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo">Origin<span>main</span></a>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-dropdown">
|
||||
<a href="/#features" style="display:flex;align-items:center;gap:4px;">
|
||||
Product
|
||||
<svg class="nav-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a class="nav-dd-item" href="/features/live-artboard.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Live Artboard</span><span class="nav-dd-desc">Your app rendered live in the canvas</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/origin-graph.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Origin Graph</span><span class="nav-dd-desc">Every component, indexed and queryable</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/intent-diff.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Intent Diff</span><span class="nav-dd-desc">Ship component-level diffs, not redlines</span></div>
|
||||
</a>
|
||||
<div class="nav-dd-sep"></div>
|
||||
<a class="nav-dd-item" href="/features/completion-zone.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Completion Zone</span><span class="nav-dd-desc">AI-filled design, constrained to your system</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/agent-bridge.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Agent Bridge</span><span class="nav-dd-desc">MCP channel to Cursor, Claude Code & more</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item current" href="/features/integrations.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Integrations</span><span class="nav-dd-desc">Linear, Slack, GitHub, Figma & more</span></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html" class="active">🔗 Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<a href="#" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-mobile" id="nav-mobile">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/#features" id="mob-product-toggle" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Product
|
||||
<svg id="mob-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none" style="transition:transform 0.15s;"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-mobile-sub" id="nav-mobile-sub">
|
||||
<a href="/features/live-artboard.html">Live Artboard</a>
|
||||
<a href="/features/origin-graph.html">Origin Graph</a>
|
||||
<a href="/features/intent-diff.html">Intent Diff</a>
|
||||
<a href="/features/completion-zone.html">Completion Zone</a>
|
||||
<a href="/features/agent-bridge.html">Agent Bridge</a>
|
||||
<a href="/features/integrations.html">Integrations</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">🔗 Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="#" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section id="page-hero">
|
||||
<div class="page-hero-inner">
|
||||
<span class="label reveal">Integrations</span>
|
||||
<h1 class="d1 reveal">Connect your<br>entire workflow.</h1>
|
||||
<p class="subtitle reveal">Originmain integrates with GitHub, Figma, Linear, Slack, VS Code, Cursor, TypeScript, and React — so every part of your design-to-code pipeline is connected, without changing how you work.</p>
|
||||
<div class="page-hero-ctas reveal">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="#how-it-works" class="btn btn-outline-white btn-lg">See how it works ↓</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration bento grid -->
|
||||
<div class="int-bento reveal">
|
||||
<div class="int-grid">
|
||||
<!-- GitHub -->
|
||||
<div class="int-card github">
|
||||
<div class="int-logo">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="rgba(255,255,255,.85)"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.168 6.839 9.49.5.092.682-.217.682-.482 0-.237-.009-.866-.013-1.7-2.782.604-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12c0-5.523-4.477-10-10-10z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">GitHub</div>
|
||||
<div class="int-desc">PR reviews trigger automatic diffs. Every merge computes a full component-level diff.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- Figma -->
|
||||
<div class="int-card figma">
|
||||
<div class="int-logo">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M8 2h4v6H8a3 3 0 010-6z" fill="#F24E1E"/><path d="M12 2h4a3 3 0 010 6h-4V2z" fill="#FF7262"/><path d="M8 8h4v6H8a3 3 0 010-6z" fill="#A259FF"/><path d="M12 11a3 3 0 116 0 3 3 0 01-6 0z" fill="#1ABCFE"/><path d="M8 14h4v4a3 3 0 11-4-4z" fill="#0ACF83"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">Figma</div>
|
||||
<div class="int-desc">Design tokens, components & layouts synced live into the graph.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- Linear -->
|
||||
<div class="int-card linear">
|
||||
<div class="int-logo">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#5e6ad2"><path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">Linear</div>
|
||||
<div class="int-desc">Issue tracking tied to component changes. Auto-creates issues when drift is detected.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- Slack -->
|
||||
<div class="int-card slack">
|
||||
<div class="int-logo">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M5.042 15.165a2.528 2.528 0 01-2.52 2.52 2.528 2.528 0 01-2.521-2.52 2.528 2.528 0 012.52-2.52h2.521v2.52z" fill="#E01E5A"/><path d="M6.313 15.165a2.528 2.528 0 012.521-2.52 2.528 2.528 0 012.52 2.52v6.313A2.528 2.528 0 018.834 24a2.528 2.528 0 01-2.521-2.522v-6.313z" fill="#E01E5A"/><path d="M8.834 5.042a2.528 2.528 0 01-2.521-2.52A2.528 2.528 0 018.834 0a2.528 2.528 0 012.52 2.522v2.52H8.834z" fill="#36C5F0"/><path d="M8.834 6.313a2.528 2.528 0 012.52 2.521 2.528 2.528 0 01-2.52 2.52H2.522A2.528 2.528 0 010 8.834a2.528 2.528 0 012.522-2.521h6.312z" fill="#36C5F0"/><path d="M18.956 8.834a2.528 2.528 0 012.522-2.521A2.528 2.528 0 0124 8.834a2.528 2.528 0 01-2.522 2.52h-2.522V8.834z" fill="#2EB67D"/><path d="M17.688 8.834a2.528 2.528 0 01-2.522 2.52 2.528 2.528 0 01-2.521-2.52V2.522A2.528 2.528 0 0115.166 0a2.528 2.528 0 012.522 2.522v6.312z" fill="#2EB67D"/><path d="M15.166 18.956a2.528 2.528 0 012.522 2.522A2.528 2.528 0 0115.166 24a2.528 2.528 0 01-2.521-2.522v-2.522h2.521z" fill="#ECB22E"/><path d="M15.166 17.688a2.528 2.528 0 01-2.521-2.522 2.528 2.528 0 012.521-2.521h6.313A2.528 2.528 0 0124 15.166a2.528 2.528 0 01-2.521 2.522h-6.313z" fill="#ECB22E"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">Slack</div>
|
||||
<div class="int-desc">Deploy alerts, review pings, and diff summaries posted to your channels.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- VS Code -->
|
||||
<div class="int-card vscode">
|
||||
<div class="int-logo">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M23.15 2.587L18.21.21a1.494 1.494 0 00-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 00-1.276.057L.327 7.261A1 1 0 00.326 8.74L3.899 12 .326 15.26a1 1 0 00.001 1.479L1.65 17.94a.999.999 0 001.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 001.704.29l4.942-2.377A1.5 1.5 0 0024 19.923V4.077a1.5 1.5 0 00-.85-1.49zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" fill="#007ACC"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">VS Code</div>
|
||||
<div class="int-desc">Extension with inline diff annotations. See component history without leaving your editor.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- Cursor -->
|
||||
<div class="int-card cursor">
|
||||
<div class="int-logo">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="rgba(255,255,255,.88)"><path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">Cursor</div>
|
||||
<div class="int-desc">Origin diffs surface as Cursor suggestions. Accept/reject via the Agent Bridge.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- TypeScript -->
|
||||
<div class="int-card typescript">
|
||||
<div class="int-logo">
|
||||
<svg viewBox="0 0 18 18" width="18" height="18"><rect x="0.5" y="4.5" width="8.5" height="1.8" rx="0.5" fill="white"/><rect x="3.75" y="4.5" width="1.9" height="9.2" rx="0.5" fill="white"/><path d="M15.8 7.2c-.5-.9-1.4-1.4-2.5-1.4-1.4 0-2.3.7-2.3 1.9 0 1 .9 1.6 2.1 2 1.3.4 2.3 1.1 2.3 2.3 0 1.3-1.1 2.1-2.6 2.1-1.1 0-2-.4-2.6-1.3" stroke="white" stroke-width="1.7" stroke-linecap="round" fill="none"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">TypeScript</div>
|
||||
<div class="int-desc">Type-safe component contracts. Prop inference from your TypeScript types.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
<!-- React -->
|
||||
<div class="int-card react">
|
||||
<div class="int-logo">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none"><ellipse cx="12" cy="12" rx="10" ry="4" stroke="#61dafb" stroke-width="1.3"/><ellipse cx="12" cy="12" rx="10" ry="4" stroke="#61dafb" stroke-width="1.3" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="10" ry="4" stroke="#61dafb" stroke-width="1.3" transform="rotate(120 12 12)"/><circle cx="12" cy="12" r="2" fill="#61dafb"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="int-name">React</div>
|
||||
<div class="int-desc">Component tree, hooks, and context all mapped to the graph. Full React Fiber integration.</div>
|
||||
</div>
|
||||
<span class="int-status"><span class="int-status-dot"></span>Live</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section id="how-it-works" class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">How it works</span>
|
||||
<h2 class="d2">From connect to notify in four steps.</h2>
|
||||
<p class="subtitle">Every integration follows the same secure pipeline — events flow in, diffs are computed, and your team is alerted in the tools they already use.</p>
|
||||
</div>
|
||||
<div class="hiw-steps stagger">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Connect</h3>
|
||||
<p>Authorize via OAuth or register a webhook. Originmain handles token refresh and credential storage with zero manual config.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Sync</h3>
|
||||
<p>Events flow into Originmain in real time. Each payload is verified with HMAC signatures before any processing begins.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>Diff</h3>
|
||||
<p>Changes are automatically fed into the Intent Diff engine. Component-level diffs are computed and attributed to the triggering event.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">04</div>
|
||||
<h3>Notify</h3>
|
||||
<p>Your team is alerted where they work — a Slack message, a PR comment, a Linear issue update, or an inline editor annotation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DEEP DIVE: GITHUB -->
|
||||
<div class="section" style="padding:100px 40px;">
|
||||
<div class="feat-split reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-blue">GitHub integration</span>
|
||||
<h2 class="d3">Every merge computes<br>a full component diff.</h2>
|
||||
<p class="subtitle">When a PR is opened, Originmain receives the push event and immediately kicks off a component-level diff across the entire affected subtree. The result is posted back to the PR as a structured comment — no manual review step.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon">⚡</span>PR events trigger automatic diff computation the moment they are received.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>Branch tracking keeps the Origin Graph in sync with every push — not just merges.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></span>Originmain posts a structured diff comment directly onto the GitHub PR for inline review.</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-blue">Connect GitHub</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:20px;display:flex;flex-direction:column;gap:0;overflow:hidden;">
|
||||
<!-- Webhook payload -->
|
||||
<div style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.22);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:8px;">Incoming webhook payload</div>
|
||||
<div class="code-block" style="padding:14px;font-size:0.625rem;line-height:1.65;border-radius:8px;flex-shrink:0;">
|
||||
<span class="pu">{</span>
|
||||
<span class="kw">"event"</span><span class="pu">:</span> <span class="str">"pull_request"</span><span class="pu">,</span>
|
||||
<span class="kw">"action"</span><span class="pu">:</span> <span class="str">"opened"</span><span class="pu">,</span>
|
||||
<span class="kw">"number"</span><span class="pu">:</span> <span class="nm">142</span><span class="pu">,</span>
|
||||
<span class="kw">"head"</span><span class="pu">: {</span>
|
||||
<span class="kw">"sha"</span><span class="pu">:</span> <span class="str">"a4f3c2d"</span><span class="pu">,</span>
|
||||
<span class="kw">"ref"</span><span class="pu">:</span> <span class="str">"feat/dashboard-v2"</span>
|
||||
<span class="pu">},</span>
|
||||
<span class="kw">"x-hub-signature-256"</span><span class="pu">:</span> <span class="str">"sha256=…"</span>
|
||||
<span class="pu">}</span></div>
|
||||
<!-- Arrow -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;padding:8px 0;gap:8px;">
|
||||
<div style="flex:1;height:1px;background:rgba(255,255,255,0.07);"></div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);display:flex;align-items:center;gap:5px;">
|
||||
<span style="width:16px;height:16px;background:rgba(16,185,129,0.12);border:1px solid rgba(16,185,129,0.2);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:0.4rem;color:#10B981;">✓</span>
|
||||
HMAC verified · diff triggered
|
||||
</div>
|
||||
<div style="flex:1;height:1px;background:rgba(255,255,255,0.07);"></div>
|
||||
</div>
|
||||
<!-- Diff result -->
|
||||
<div style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.22);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:8px;">Computed component diff</div>
|
||||
<div style="background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:8px;padding:14px;font-family:var(--mono);font-size:0.625rem;line-height:1.65;flex-shrink:0;">
|
||||
<div style="color:#FFBA7B;margin-bottom:6px;">~ DashboardHeader</div>
|
||||
<div style="color:rgba(255,255,255,0.35);padding-left:10px;margin-bottom:3px;"> <span style="color:#F87171;">- title: "Overview"</span></div>
|
||||
<div style="color:rgba(255,255,255,0.35);padding-left:10px;margin-bottom:8px;"> <span style="color:#4ADE80;">+ title: "Dashboard v2"</span></div>
|
||||
<div style="color:#7EB8FF;margin-bottom:6px;">+ MetricCard (new)</div>
|
||||
<div style="color:rgba(255,255,255,0.35);padding-left:10px;margin-bottom:3px;"> <span style="color:#4ADE80;">+ metric="weekly_active"</span></div>
|
||||
<div style="color:rgba(255,255,255,0.35);padding-left:10px;"> <span style="color:#4ADE80;">+ format="number"</span></div>
|
||||
</div>
|
||||
<!-- Arrow to PR comment -->
|
||||
<div style="display:flex;align-items:center;justify-content:center;padding:8px 0;gap:8px;">
|
||||
<div style="flex:1;height:1px;background:rgba(255,255,255,0.07);"></div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);">posted to PR #142</div>
|
||||
<div style="flex:1;height:1px;background:rgba(255,255,255,0.07);"></div>
|
||||
</div>
|
||||
<!-- PR comment mock -->
|
||||
<div style="background:#161B22;border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:10px;flex-shrink:0;">
|
||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
||||
<div style="width:14px;height:14px;border-radius:50%;background:linear-gradient(135deg,#0066FF,#7C3AED);"></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.5);font-weight:600;">originmain-bot</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.2);">commented just now</span>
|
||||
</div>
|
||||
<div style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.65);">Origin Diff — 2 components changed, 1 added</div>
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.35);margin-top:3px;">View full diff on Originmain →</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEEP DIVE: FIGMA -->
|
||||
<div class="section section-muted" style="padding:100px 40px;">
|
||||
<div class="feat-split rev reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-blue">Figma integration</span>
|
||||
<h2 class="d3">Design changes update<br>the graph automatically.</h2>
|
||||
<p class="subtitle">Connect your Figma file and Originmain subscribes to every change. Design tokens, component definitions, and layout compositions are extracted and mapped directly into the Origin Graph — no manual export, no hand-off step.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Design tokens (color, spacing, typography) are extracted and kept in sync as you iterate in Figma.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Component library updates propagate to every artboard that references them — instantly.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Layout changes generate Intent Diffs that can be handed directly to the Agent Bridge.</li>
|
||||
</ul>
|
||||
<a href="#" class="btn btn-blue">Connect Figma</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-light reveal">
|
||||
<div style="position:absolute;inset:0;padding:24px;display:flex;flex-direction:column;gap:12px;overflow:hidden;">
|
||||
<!-- Token sync panel -->
|
||||
<div style="font-size:0.6875rem;font-weight:700;color:#0A0A0A;letter-spacing:-0.02em;margin-bottom:4px;">Figma → Origin Graph</div>
|
||||
<!-- Token rows -->
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
<div style="background:#fff;border:1px solid rgba(0,0,0,0.07);border-radius:8px;padding:10px 12px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 3px rgba(0,0,0,0.05);">
|
||||
<div style="width:22px;height:22px;border-radius:5px;background:#0066FF;flex-shrink:0;"></div>
|
||||
<div style="flex:1;"><div style="font-size:0.6875rem;font-weight:600;color:#0A0A0A;">color/brand/primary</div><div style="font-family:monospace;font-size:0.5625rem;color:#A1A1AA;">#0066FF</div></div>
|
||||
<div style="font-family:monospace;font-size:0.5rem;color:#10B981;background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);padding:2px 6px;border-radius:99px;">synced</div>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px solid rgba(0,0,0,0.07);border-radius:8px;padding:10px 12px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 3px rgba(0,0,0,0.05);">
|
||||
<div style="width:22px;height:22px;border-radius:5px;background:#F4F4F5;border:1px solid rgba(0,0,0,0.07);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:0.6875rem;">T</div>
|
||||
<div style="flex:1;"><div style="font-size:0.6875rem;font-weight:600;color:#0A0A0A;">typography/heading/xl</div><div style="font-family:monospace;font-size:0.5625rem;color:#A1A1AA;">900 / 4.5rem / -0.045em</div></div>
|
||||
<div style="font-family:monospace;font-size:0.5rem;color:#10B981;background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);padding:2px 6px;border-radius:99px;">synced</div>
|
||||
</div>
|
||||
<div style="background:#fff;border:1px solid rgba(0,0,0,0.07);border-radius:8px;padding:10px 12px;display:flex;align-items:center;gap:10px;box-shadow:0 1px 3px rgba(0,0,0,0.05);">
|
||||
<div style="width:22px;height:22px;border-radius:5px;background:#F4F4F5;border:1px solid rgba(0,0,0,0.07);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:0.5rem;">sp</div>
|
||||
<div style="flex:1;"><div style="font-size:0.6875rem;font-weight:600;color:#0A0A0A;">spacing/lg</div><div style="font-family:monospace;font-size:0.5625rem;color:#A1A1AA;">32px</div></div>
|
||||
<div style="font-family:monospace;font-size:0.5rem;color:#F59E0B;background:rgba(245,158,11,0.08);border:1px solid rgba(245,158,11,0.2);padding:2px 6px;border-radius:99px;">updating</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Component library -->
|
||||
<div style="margin-top:4px;background:#fff;border:1px solid rgba(0,0,0,0.07);border-radius:8px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05);">
|
||||
<div style="font-size:0.6875rem;font-weight:700;color:#0A0A0A;margin-bottom:8px;">Component library — 48 components</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;">
|
||||
<div style="background:#F4F4F5;border-radius:6px;height:28px;display:flex;align-items:center;justify-content:center;font-size:0.5rem;color:#52525B;font-weight:600;">Button</div>
|
||||
<div style="background:#F4F4F5;border-radius:6px;height:28px;display:flex;align-items:center;justify-content:center;font-size:0.5rem;color:#52525B;font-weight:600;">Card</div>
|
||||
<div style="background:#F4F4F5;border-radius:6px;height:28px;display:flex;align-items:center;justify-content:center;font-size:0.5rem;color:#52525B;font-weight:600;">Input</div>
|
||||
<div style="background:rgba(0,102,255,0.07);border:1px solid rgba(0,102,255,0.15);border-radius:6px;height:28px;display:flex;align-items:center;justify-content:center;font-size:0.5rem;color:#0066FF;font-weight:600;">Badge ↑</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SETUP IN MINUTES: TECH SECTION -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Setup in minutes</span>
|
||||
<h2 class="d2">Built on secure, open standards.</h2>
|
||||
<p class="subtitle">Every integration uses platform-native authentication and cryptographic verification. No credentials stored in plaintext, no polling — only push-based, event-driven architecture.</p>
|
||||
</div>
|
||||
<div class="tech-grid stagger">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🔐</div>
|
||||
<h3>HMAC verification</h3>
|
||||
<p>Every incoming webhook is verified with HMAC-SHA256 before processing. The shared secret is stored encrypted in Originmain's key vault — never in environment variables or config files.</p>
|
||||
<span class="tech-tag">Security</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">✍️</div>
|
||||
<h3>Webhook signatures</h3>
|
||||
<p>Each platform has its own signature header — <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">x-hub-signature-256</code> for GitHub, <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">x-figma-signature</code> for Figma. Originmain verifies each using the platform's prescribed algorithm.</p>
|
||||
<span class="tech-tag">Protocol</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">📨</div>
|
||||
<h3>Event types</h3>
|
||||
<p>Subscribe only to the events you need. Originmain exposes a per-integration event filter — receive <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">pull_request.opened</code> and <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">push</code> from GitHub, or <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">FILE_UPDATE</code> from Figma.</p>
|
||||
<span class="tech-tag">Granularity</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🔑</div>
|
||||
<h3>OAuth flows</h3>
|
||||
<p>GitHub, Figma, Linear, and Slack all use standard OAuth 2.0 with PKCE. Tokens are refreshed automatically — you authorize once and Originmain handles the rest without user intervention.</p>
|
||||
<span class="tech-tag">Authentication</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⏱</div>
|
||||
<h3>API rate limits</h3>
|
||||
<p>Originmain's integration layer respects every platform's rate limit with adaptive back-off and request queuing. High-volume repos with hundreds of daily PRs process without dropped events.</p>
|
||||
<span class="tech-tag">Reliability</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">📋</div>
|
||||
<h3>Audit logs</h3>
|
||||
<p>Every event received, every diff triggered, every notification sent is recorded with a timestamp and payload hash. Full audit trail available to workspace admins via the Originmain dashboard.</p>
|
||||
<span class="tech-tag">Compliance</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- INTEGRATION STATUS GRID -->
|
||||
<section class="section section-muted">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Integration status</span>
|
||||
<h2 class="d2">All integrations, all live.</h2>
|
||||
<p class="subtitle">Every integration ships fully operational on day one. Connect GitHub and Figma in under two minutes, then add Linear, Slack, and your editor tools from the dashboard.</p>
|
||||
</div>
|
||||
<div class="status-grid stagger">
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#21262D;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="rgba(255,255,255,.85)"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.418 2.865 8.168 6.839 9.49.5.092.682-.217.682-.482 0-.237-.009-.866-.013-1.7-2.782.604-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12c0-5.523-4.477-10-10-10z"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>GitHub</h3>
|
||||
<p>PR diffs & branch sync</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#1E1E2E;">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M8 2h4v6H8a3 3 0 010-6z" fill="#F24E1E"/><path d="M12 2h4a3 3 0 010 6h-4V2z" fill="#FF7262"/><path d="M8 8h4v6H8a3 3 0 010-6z" fill="#A259FF"/><path d="M12 11a3 3 0 116 0 3 3 0 01-6 0z" fill="#1ABCFE"/><path d="M8 14h4v4a3 3 0 11-4-4z" fill="#0ACF83"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>Figma</h3>
|
||||
<p>Token & component sync</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#0D0D1A;">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="#5e6ad2"><path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>Linear</h3>
|
||||
<p>Issue & milestone sync</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#1A1A2E;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M5.042 15.165a2.528 2.528 0 01-2.52 2.52 2.528 2.528 0 01-2.521-2.52 2.528 2.528 0 012.52-2.52h2.521v2.52z" fill="#E01E5A"/><path d="M6.313 15.165a2.528 2.528 0 012.521-2.52 2.528 2.528 0 012.52 2.52v6.313A2.528 2.528 0 018.834 24a2.528 2.528 0 01-2.521-2.522v-6.313z" fill="#E01E5A"/><path d="M8.834 5.042a2.528 2.528 0 01-2.521-2.52A2.528 2.528 0 018.834 0a2.528 2.528 0 012.52 2.522v2.52H8.834z" fill="#36C5F0"/><path d="M8.834 6.313a2.528 2.528 0 012.52 2.521 2.528 2.528 0 01-2.52 2.52H2.522A2.528 2.528 0 010 8.834a2.528 2.528 0 012.522-2.521h6.312z" fill="#36C5F0"/><path d="M18.956 8.834a2.528 2.528 0 012.522-2.521A2.528 2.528 0 0124 8.834a2.528 2.528 0 01-2.522 2.52h-2.522V8.834z" fill="#2EB67D"/><path d="M17.688 8.834a2.528 2.528 0 01-2.522 2.52 2.528 2.528 0 01-2.521-2.52V2.522A2.528 2.528 0 0115.166 0a2.528 2.528 0 012.522 2.522v6.312z" fill="#2EB67D"/><path d="M15.166 18.956a2.528 2.528 0 012.522 2.522A2.528 2.528 0 0115.166 24a2.528 2.528 0 01-2.521-2.522v-2.522h2.521z" fill="#ECB22E"/><path d="M15.166 17.688a2.528 2.528 0 01-2.521-2.522 2.528 2.528 0 012.521-2.521h6.313A2.528 2.528 0 0124 15.166a2.528 2.528 0 01-2.521 2.522h-6.313z" fill="#ECB22E"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>Slack</h3>
|
||||
<p>Alerts & diff summaries</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#0D1117;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M23.15 2.587L18.21.21a1.494 1.494 0 00-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 00-1.276.057L.327 7.261A1 1 0 00.326 8.74L3.899 12 .326 15.26a1 1 0 00.001 1.479L1.65 17.94a.999.999 0 001.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 001.704.29l4.942-2.377A1.5 1.5 0 0024 19.923V4.077a1.5 1.5 0 00-.85-1.49zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" fill="#007ACC"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>VS Code</h3>
|
||||
<p>Inline diff annotations</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#111118;border:1px solid rgba(255,255,255,0.08);">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="rgba(255,255,255,.88)"><path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>Cursor</h3>
|
||||
<p>Agent Bridge suggestions</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#3178c6;">
|
||||
<svg viewBox="0 0 18 18" width="18" height="18"><rect x="0.5" y="4.5" width="8.5" height="1.8" rx="0.5" fill="white"/><rect x="3.75" y="4.5" width="1.9" height="9.2" rx="0.5" fill="white"/><path d="M15.8 7.2c-.5-.9-1.4-1.4-2.5-1.4-1.4 0-2.3.7-2.3 1.9 0 1 .9 1.6 2.1 2 1.3.4 2.3 1.1 2.3 2.3 0 1.3-1.1 2.1-2.6 2.1-1.1 0-2-.4-2.6-1.3" stroke="white" stroke-width="1.7" stroke-linecap="round" fill="none"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>TypeScript</h3>
|
||||
<p>Type-safe prop contracts</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-logo-wrap" style="background:#0D1A1A;">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none"><ellipse cx="12" cy="12" rx="10" ry="4" stroke="#61dafb" stroke-width="1.3"/><ellipse cx="12" cy="12" rx="10" ry="4" stroke="#61dafb" stroke-width="1.3" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="10" ry="4" stroke="#61dafb" stroke-width="1.3" transform="rotate(120 12 12)"/><circle cx="12" cy="12" r="2" fill="#61dafb"/></svg>
|
||||
</div>
|
||||
<div class="status-info">
|
||||
<h3>React</h3>
|
||||
<p>Full Fiber tree mapping</p>
|
||||
<span class="status-badge"><span class="status-badge-dot"></span>Operational</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RELATED FEATURES -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Goes deeper with</span>
|
||||
<h2 class="d2">Integrations power every feature.</h2>
|
||||
<p class="subtitle">The integration layer is the connective tissue of the whole platform — every feature benefits from a connected workflow.</p>
|
||||
</div>
|
||||
<div class="related-grid stagger">
|
||||
<a class="related-card" href="/features/agent-bridge.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<h3>Agent Bridge</h3>
|
||||
<p>Cursor and Claude Code receive Origin diffs directly through the Agent Bridge MCP channel. Integrations are what keep the bridge fed with up-to-date component context.</p>
|
||||
<span class="related-link">Explore Agent Bridge →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/intent-diff.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<h3>Intent Diff</h3>
|
||||
<p>GitHub pushes, Figma changes, and Linear milestones all trigger Intent Diff computations. Every integration event has a diff attached — automatically, with no manual review step.</p>
|
||||
<span class="related-link">Explore Intent Diff →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/origin-graph.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3>Origin Graph</h3>
|
||||
<p>Figma tokens, TypeScript types, and React Fiber data all flow into the Origin Graph through the integration layer, keeping your component index complete and current at all times.</p>
|
||||
<span class="related-link">Explore Origin Graph →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta-bottom">
|
||||
<div class="cta-inner">
|
||||
<h2 class="cta-headline">Your whole stack,<br>finally connected.</h2>
|
||||
<p class="cta-sub">Connect GitHub and Figma in under two minutes. Every other integration is one click from the dashboard.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="/" class="btn btn-outline-white btn-lg">Back to overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="ft-inner">
|
||||
<div class="ft-top">
|
||||
<div>
|
||||
<div class="ft-brand-logo">Origin<span>main</span></div>
|
||||
<p class="ft-brand-tag">Where design intent becomes production code.</p>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="/features/live-artboard.html">Live Artboard</a></li>
|
||||
<li><a href="/features/origin-graph.html">Origin Graph</a></li>
|
||||
<li><a href="/features/intent-diff.html">Intent Diff</a></li>
|
||||
<li><a href="/features/completion-zone.html">Completion Zone</a></li>
|
||||
<li><a href="/features/agent-bridge.html">Agent Bridge</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Integrations</h4>
|
||||
<ul>
|
||||
<li><a href="/features/integrations.html">All integrations</a></li>
|
||||
<li><a href="#">GitHub</a></li>
|
||||
<li><a href="#">Linear</a></li>
|
||||
<li><a href="#">Slack</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Developers</h4>
|
||||
<ul>
|
||||
<li><a href="#">Docs</a></li>
|
||||
<li><a href="#">API Reference</a></li>
|
||||
<li><a href="#">MCP Protocol</a></li>
|
||||
<li><a href="#">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft-bottom">
|
||||
<span class="ft-copy">© 2025 Originmain. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Nav scroll
|
||||
const nav=document.getElementById('nav');
|
||||
window.addEventListener('scroll',()=>{nav.classList.toggle('scrolled',window.scrollY>20);});
|
||||
// Dropdown: delay-based hover (desktop) + click (mobile/touch)
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd=>{const menu=dd.querySelector('.nav-dropdown-menu');let t;const open=()=>{clearTimeout(t);dd.classList.add('open');menu.classList.add('open');};const close=()=>{t=setTimeout(()=>{dd.classList.remove('open');menu.classList.remove('open');},150);};dd.addEventListener('mouseenter',open);dd.addEventListener('mouseleave',close);menu.addEventListener('mouseenter',open);menu.addEventListener('mouseleave',close);dd.querySelector('a').addEventListener('click',e=>{if(window.matchMedia('(hover:none)').matches||window.innerWidth<=1024){e.preventDefault();const o=menu.classList.toggle('open');dd.classList.toggle('open',o);}});});
|
||||
// Mobile product sub-accordion
|
||||
const mpt=document.getElementById('mob-product-toggle'),mns=document.getElementById('nav-mobile-sub'),mdc=document.getElementById('mob-dd-caret');if(mpt&&mns){mpt.addEventListener('click',e=>{e.preventDefault();const o=mns.classList.toggle('open');if(mdc)mdc.style.transform=o?'rotate(180deg)':'';});}
|
||||
// Burger
|
||||
const burger=document.getElementById('nav-burger'),mob=document.getElementById('nav-mobile');
|
||||
burger.addEventListener('click',()=>{const o=mob.classList.toggle('open');burger.classList.toggle('open',o);burger.setAttribute('aria-expanded',o);});
|
||||
// Reveal
|
||||
const io=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target);}});},{threshold:0.12});
|
||||
document.querySelectorAll('.reveal,.stagger').forEach(el=>io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,740 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Intent Diff — Originmain</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#FFFFFF;--bg-subtle:#FAFAFA;--bg-muted:#F4F4F5;--bg-strong:#EBEBEB;--bg-inv:#0A0A0A;--bg-inv-2:#141414;--bg-inv-3:#1C1C1C;--fg:#0A0A0A;--fg-2:#52525B;--fg-3:#A1A1AA;--fg-4:#D4D4D8;--fg-inv:rgba(255,255,255,0.92);--fg-inv-2:rgba(255,255,255,0.52);--fg-inv-3:rgba(255,255,255,0.28);--blue:#0066FF;--blue-dark:#0050CC;--blue-light:#3385FF;--blue-soft:rgba(0,102,255,0.07);--blue-mid:rgba(0,102,255,0.15);--purple:#7C3AED;--purple-dark:#6D28D9;--purple-soft:rgba(124,58,237,0.07);--purple-mid:rgba(124,58,237,0.15);--teal:#0891B2;--border:rgba(0,0,0,0.07);--border-2:rgba(0,0,0,0.12);--border-inv:rgba(255,255,255,0.08);--border-inv-2:rgba(255,255,255,0.14);--r-1:4px;--r-2:8px;--r-3:12px;--r-4:16px;--r-5:20px;--r-6:24px;--r-full:9999px;--sh-xs:0 1px 2px rgba(0,0,0,0.05);--sh-sm:0 1px 3px rgba(0,0,0,0.07),0 1px 2px rgba(0,0,0,0.05);--sh-md:0 4px 8px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04);--sh-lg:0 10px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.05);--sh-xl:0 20px 40px rgba(0,0,0,0.09),0 8px 16px rgba(0,0,0,0.05);--sh-2xl:0 32px 64px rgba(0,0,0,0.11),0 16px 32px rgba(0,0,0,0.06);--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--mono:'JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||||
html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.6;overflow-x:hidden;}
|
||||
img,svg{display:block;max-width:100%;}
|
||||
a{color:inherit;text-decoration:none;}
|
||||
button{font-family:inherit;cursor:pointer;border:none;background:none;}
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:0 40px;}
|
||||
.section{padding:112px 0;}
|
||||
.section-muted{background:var(--bg-subtle);}
|
||||
.section-dark{background:var(--bg-inv);}
|
||||
.d1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;line-height:1.02;letter-spacing:-0.045em;color:var(--fg);}
|
||||
.d2{font-size:clamp(1.875rem,3.5vw,2.75rem);font-weight:800;line-height:1.1;letter-spacing:-0.038em;color:var(--fg);}
|
||||
.d3{font-size:clamp(1.25rem,2.5vw,1.625rem);font-weight:700;line-height:1.25;letter-spacing:-0.028em;color:var(--fg);}
|
||||
.subtitle{font-size:clamp(1rem,1.8vw,1.125rem);font-weight:400;line-height:1.75;color:var(--fg-2);}
|
||||
.label{font-family:var(--mono);font-size:0.6875rem;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--fg-3);}
|
||||
.label-purple{color:var(--purple);}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font);font-size:0.9375rem;font-weight:600;padding:11px 22px;border-radius:var(--r-2);transition:transform 0.12s cubic-bezier(.16,1,.3,1),opacity 0.12s,box-shadow 0.12s;white-space:nowrap;letter-spacing:-0.01em;position:relative;overflow:hidden;}
|
||||
.btn::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,0);transition:background 0.12s;}
|
||||
.btn:hover::after{background:rgba(255,255,255,0.06);}
|
||||
.btn:active{transform:scale(0.98)!important;}
|
||||
.btn-primary{background:var(--fg);color:var(--bg);box-shadow:var(--sh-sm);}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:var(--sh-md);}
|
||||
.btn-secondary{background:transparent;color:var(--fg);border:1px solid var(--border-2);}
|
||||
.btn-secondary:hover{background:var(--bg-muted);transform:translateY(-1px);}
|
||||
.btn-purple{background:var(--purple);color:#fff;box-shadow:0 4px 12px rgba(124,58,237,0.3);}
|
||||
.btn-purple:hover{background:var(--purple-dark);transform:translateY(-1px);}
|
||||
.btn-lg{font-size:1rem;padding:13px 28px;}
|
||||
.btn-white{background:#fff;color:#0A0A0A;box-shadow:0 4px 12px rgba(255,255,255,0.15);}
|
||||
.btn-white:hover{background:#F5F5F5;transform:translateY(-1px);}
|
||||
.btn-outline-white{background:transparent;color:rgba(255,255,255,0.72);border:1px solid rgba(255,255,255,0.18);}
|
||||
.btn-outline-white:hover{background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.9);transform:translateY(-1px);}
|
||||
.divider{height:1px;background:var(--border);}
|
||||
.section-head{text-align:center;max-width:640px;margin:0 auto 72px;}
|
||||
.section-head .label{display:block;margin-bottom:16px;}
|
||||
.section-head .d2{margin-bottom:18px;}
|
||||
|
||||
/* NAV */
|
||||
#nav{position:fixed;top:0;left:0;right:0;z-index:200;height:60px;display:flex;align-items:center;padding:0 40px;transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;}
|
||||
#nav.scrolled{background:rgba(255,255,255,0.85);backdrop-filter:blur(20px) saturate(1.6);-webkit-backdrop-filter:blur(20px) saturate(1.6);border-bottom:1px solid var(--border);box-shadow:var(--sh-xs);}
|
||||
.nav-inner{max-width:1120px;margin:0 auto;width:100%;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
|
||||
.nav-logo{justify-self:start;font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:var(--fg);display:flex;align-items:center;gap:2px;}
|
||||
.nav-logo span{color:var(--blue);}
|
||||
.nav-links{justify-self:center;display:flex;align-items:center;gap:0;list-style:none;}
|
||||
.nav-links a{font-size:0.9375rem;font-weight:500;color:var(--fg-2);padding:6px 14px;border-radius:var(--r-1);transition:color 0.12s,background 0.12s;letter-spacing:-0.01em;}
|
||||
.nav-links a:hover{color:var(--fg);}
|
||||
.nav-actions{justify-self:end;display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* Dropdown */
|
||||
.nav-dropdown{position:relative;}
|
||||
.nav-dropdown>a{display:flex;align-items:center;gap:4px;}
|
||||
.nav-dd-caret{transition:transform 0.15s;}
|
||||
/* caret rotation handled via JS .open class */
|
||||
.nav-dropdown-menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-4px);background:rgba(255,255,255,0.98);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--r-3);box-shadow:var(--sh-xl);padding:8px;min-width:240px;opacity:0;visibility:hidden;transition:opacity 0.15s,transform 0.15s,visibility 0.15s;pointer-events:none;}
|
||||
.nav-dropdown-menu.open{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:all;}
|
||||
.nav-dropdown.open .nav-dd-caret{transform:rotate(180deg);}
|
||||
/* Mobile sub-nav */
|
||||
.nav-mobile-sub{display:none;padding:2px 0 6px 14px;flex-direction:column;}
|
||||
.nav-mobile-sub.open{display:flex;}
|
||||
.nav-mobile-sub a{font-size:0.875rem;color:rgba(255,255,255,0.4);padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.06);letter-spacing:-0.01em;transition:color 0.1s;}
|
||||
.nav-mobile-sub a:last-child{border-bottom:none;}
|
||||
.nav-mobile-sub a:hover{color:rgba(255,255,255,0.8);}
|
||||
/* Dark hero: white nav before scrolling */
|
||||
#nav:not(.scrolled) .nav-logo{color:rgba(255,255,255,0.92);}
|
||||
#nav:not(.scrolled) .nav-logo span{color:#3385FF;}
|
||||
#nav:not(.scrolled) .nav-links a{color:rgba(255,255,255,0.5);}
|
||||
#nav:not(.scrolled) .nav-links a:hover{color:rgba(255,255,255,0.9);}
|
||||
#nav:not(.scrolled) .nav-actions .btn-secondary{color:rgba(255,255,255,0.65);border-color:rgba(255,255,255,0.2);background:transparent;}
|
||||
#nav:not(.scrolled) .nav-burger span{background:rgba(255,255,255,0.8);}
|
||||
.nav-dd-item{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:var(--r-2);transition:background 0.1s;cursor:pointer;}
|
||||
.nav-dd-item:hover{background:var(--bg-muted);}
|
||||
.nav-dd-icon{width:30px;height:30px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:13px;color:var(--blue);}
|
||||
.nav-dd-icon.purple{background:var(--purple-soft);border-color:var(--purple-mid);}
|
||||
.nav-dd-text{display:flex;flex-direction:column;}
|
||||
.nav-dd-name{font-size:0.8125rem;font-weight:600;color:var(--fg);letter-spacing:-0.01em;}
|
||||
.nav-dd-desc{font-size:0.6875rem;color:var(--fg-3);margin-top:1px;line-height:1.4;}
|
||||
.nav-dd-sep{height:1px;background:var(--border);margin:4px 0;}
|
||||
.nav-burger{display:none;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:36px;height:36px;background:none;border:none;cursor:pointer;padding:4px;border-radius:var(--r-2);}
|
||||
.nav-burger span{display:block;width:20px;height:2px;background:var(--fg);border-radius:2px;transition:transform 0.22s cubic-bezier(0.16,1,0.3,1),opacity 0.18s;transform-origin:center;}
|
||||
.nav-burger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
|
||||
.nav-burger.open span:nth-child(2){opacity:0;transform:scaleX(0);}
|
||||
.nav-burger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
|
||||
.nav-mobile{display:none;position:absolute;top:60px;left:0;right:0;background:rgba(255,255,255,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,0.07);padding:12px 24px 24px;flex-direction:column;gap:2px;}
|
||||
.nav-mobile.open{display:flex;}
|
||||
.nav-mobile ul{list-style:none;display:flex;flex-direction:column;}
|
||||
.nav-mobile ul li a{display:block;font-size:1rem;font-weight:500;color:var(--fg-2);padding:11px 4px;border-bottom:1px solid var(--border);letter-spacing:-0.01em;}
|
||||
.nav-mobile ul li:last-child a{border-bottom:none;}
|
||||
.nav-mobile-ctas{display:flex;gap:8px;margin-top:16px;}
|
||||
.nav-mobile-ctas .btn{flex:1;justify-content:center;padding:10px 12px;font-size:.875rem;}
|
||||
|
||||
/* PAGE HERO */
|
||||
#page-hero{padding:140px 40px 100px;background:var(--bg-inv);position:relative;overflow:hidden;text-align:center;}
|
||||
#page-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(124,58,237,0.14),transparent 60%),radial-gradient(ellipse 40% 40% at 80% 70%,rgba(0,102,255,0.07),transparent 50%);pointer-events:none;}
|
||||
.page-hero-inner{position:relative;max-width:800px;margin:0 auto;}
|
||||
.page-hero-inner .label{color:rgba(255,255,255,0.3);margin-bottom:20px;display:block;}
|
||||
.page-hero-inner .d1{color:var(--fg-inv);margin-bottom:22px;}
|
||||
.page-hero-inner .subtitle{color:var(--fg-inv-2);max-width:600px;margin:0 auto 44px;}
|
||||
.page-hero-ctas{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* PRODUCT VISUAL */
|
||||
.pv-shell{width:100%;max-width:960px;margin:72px auto 0;border-radius:16px 16px 0 0;overflow:hidden;box-shadow:0 48px 96px rgba(0,0,0,0.45),0 0 0 1px rgba(255,255,255,0.06);border-bottom:none;position:relative;}
|
||||
.pv-chrome{background:#1C1C22;height:40px;display:flex;align-items:center;padding:0 14px;gap:7px;border-bottom:1px solid rgba(255,255,255,0.05);}
|
||||
.pv-dot{width:11px;height:11px;border-radius:50%;}
|
||||
.pv-dot.r{background:#FF5F57;}
|
||||
.pv-dot.y{background:#FFBD2E;}
|
||||
.pv-dot.g{background:#28C840;}
|
||||
.pv-url{flex:1;margin:0 12px;background:rgba(255,255,255,0.06);border-radius:5px;height:25px;display:flex;align-items:center;padding:0 10px;font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.3);max-width:380px;}
|
||||
.pv-tab-bar{display:flex;align-items:center;gap:2px;margin-left:auto;}
|
||||
.pv-tab{font-family:var(--mono);font-size:0.5625rem;padding:3px 10px;border-radius:4px;color:rgba(255,255,255,0.35);cursor:default;letter-spacing:0.04em;}
|
||||
.pv-tab.active{background:rgba(124,58,237,0.22);color:rgba(167,139,250,0.9);border:1px solid rgba(124,58,237,0.28);}
|
||||
|
||||
/* SPLIT DIFF PANELS */
|
||||
.diff-canvas{background:#0D0D11;min-height:380px;display:flex;align-items:stretch;}
|
||||
.diff-panel{flex:1;display:flex;flex-direction:column;min-width:0;}
|
||||
.diff-panel-head{height:36px;display:flex;align-items:center;padding:0 16px;border-bottom:1px solid rgba(255,255,255,0.06);}
|
||||
.diff-panel.before .diff-panel-head{background:#1A0A0A;border-right:1px solid rgba(255,255,255,0.04);}
|
||||
.diff-panel.after .diff-panel-head{background:#0A1A0A;}
|
||||
.diff-panel-label{font-family:var(--mono);font-size:0.5625rem;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;}
|
||||
.diff-panel.before .diff-panel-label{color:rgba(248,113,113,0.8);}
|
||||
.diff-panel.after .diff-panel-label{color:rgba(74,222,128,0.8);}
|
||||
.diff-panel-file{font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.2);margin-left:10px;}
|
||||
.diff-lines{flex:1;padding:12px 0;overflow:hidden;}
|
||||
.diff-divider{width:1px;background:rgba(255,255,255,0.05);flex-shrink:0;}
|
||||
.dl{display:flex;align-items:baseline;padding:0 16px;min-height:22px;font-family:var(--mono);font-size:0.6rem;line-height:1.8;}
|
||||
.dl-num{width:28px;flex-shrink:0;color:rgba(255,255,255,0.18);text-align:right;margin-right:12px;font-size:0.55rem;user-select:none;}
|
||||
.dl-sign{width:12px;flex-shrink:0;font-weight:700;font-size:0.6rem;}
|
||||
.dl-text{flex:1;min-width:0;}
|
||||
.dl.ctx .dl-sign{color:rgba(255,255,255,0.15);}
|
||||
.dl.ctx .dl-text{color:rgba(255,255,255,0.38);}
|
||||
.dl.rem{background:rgba(220,38,38,0.1);}
|
||||
.dl.rem .dl-sign{color:#F87171;}
|
||||
.dl.rem .dl-text{color:rgba(248,113,113,0.9);}
|
||||
.dl.add{background:rgba(22,163,74,0.1);}
|
||||
.dl.add .dl-sign{color:#4ADE80;}
|
||||
.dl.add .dl-text{color:rgba(74,222,128,0.9);}
|
||||
.dl.hunk{background:rgba(124,58,237,0.08);border-top:1px solid rgba(124,58,237,0.15);border-bottom:1px solid rgba(124,58,237,0.15);}
|
||||
.dl.hunk .dl-text{color:rgba(167,139,250,0.6);font-size:0.5625rem;}
|
||||
.prop-key{color:rgba(255,255,255,0.5);}
|
||||
.prop-val-rem{color:#FCA5A5;}
|
||||
.prop-val-add{color:#86EFAC;}
|
||||
.prop-val-str{color:#FCD34D;}
|
||||
.prop-val-num{color:#93C5FD;}
|
||||
|
||||
/* HOW IT WORKS */
|
||||
.hiw-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:64px;position:relative;}
|
||||
.hiw-steps::before{content:'';position:absolute;top:28px;left:12.5%;right:12.5%;height:1px;background:linear-gradient(90deg,transparent,var(--border-2) 20%,var(--border-2) 80%,transparent);}
|
||||
.hiw-step{display:flex;flex-direction:column;align-items:center;text-align:center;padding:0 20px;position:relative;z-index:1;}
|
||||
.hiw-num{width:56px;height:56px;border-radius:50%;background:var(--bg);border:1px solid var(--border-2);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--purple);margin-bottom:20px;box-shadow:var(--sh-sm);}
|
||||
.hiw-step h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.hiw-step p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;}
|
||||
|
||||
/* FEATURE SPLIT */
|
||||
.feat-split{display:grid;grid-template-columns:1fr 1fr;gap:96px;align-items:center;max-width:1120px;margin:0 auto;}
|
||||
.feat-split.rev{direction:rtl;}
|
||||
.feat-split.rev>*{direction:ltr;}
|
||||
.feat-copy .label{display:block;margin-bottom:16px;}
|
||||
.feat-copy .d3{margin-bottom:16px;}
|
||||
.feat-copy .subtitle{margin-bottom:28px;}
|
||||
.feat-pts{list-style:none;display:flex;flex-direction:column;gap:12px;margin-bottom:32px;}
|
||||
.feat-pts li{display:flex;align-items:flex-start;gap:10px;font-size:0.9375rem;color:var(--fg-2);line-height:1.55;}
|
||||
.feat-pt-icon{width:20px;height:20px;border-radius:3px;background:var(--purple-soft);border:1px solid var(--purple-mid);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;font-size:0.625rem;color:var(--purple);}
|
||||
|
||||
/* Feature visual */
|
||||
.feat-vis{border-radius:var(--r-5);overflow:hidden;box-shadow:var(--sh-2xl);border:1px solid var(--border);aspect-ratio:4/3;position:relative;}
|
||||
.feat-vis-dark{background:#0D0D11;border-color:rgba(255,255,255,0.07);}
|
||||
.feat-vis-light{background:var(--bg-muted);}
|
||||
|
||||
/* TECH SECTION */
|
||||
.tech-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:56px;}
|
||||
.tech-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:32px 28px;transition:box-shadow 0.2s;}
|
||||
.tech-card:hover{box-shadow:var(--sh-lg);}
|
||||
.tech-icon{width:44px;height:44px;background:var(--purple-soft);border:1px solid var(--purple-mid);border-radius:var(--r-2);display:flex;align-items:center;justify-content:center;font-size:1.25rem;margin-bottom:18px;}
|
||||
.tech-card h3{font-size:1rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.tech-card p{font-size:0.875rem;color:var(--fg-2);line-height:1.65;}
|
||||
.tech-tag{display:inline-block;margin-top:14px;font-family:var(--mono);font-size:0.625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--purple);background:var(--purple-soft);border:1px solid var(--purple-mid);padding:3px 9px;border-radius:var(--r-full);}
|
||||
|
||||
/* CODE BLOCK */
|
||||
.code-block{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);padding:24px;font-family:var(--mono);font-size:0.75rem;line-height:1.75;color:rgba(255,255,255,0.75);overflow-x:auto;}
|
||||
.code-block .kw{color:#7EB8FF;}
|
||||
.code-block .str{color:#7DDBA0;}
|
||||
.code-block .cm{color:rgba(255,255,255,0.28);}
|
||||
.code-block .fn{color:#FFBA7B;}
|
||||
.code-block .nm{color:#D4A0FF;}
|
||||
.code-block .rem{color:#F87171;}
|
||||
.code-block .add{color:#4ADE80;}
|
||||
.code-block .hdr{color:#A78BFA;}
|
||||
.code-block .ctx{color:rgba(255,255,255,0.38);}
|
||||
|
||||
/* ACCEPT/REJECT PANEL */
|
||||
.ar-panel{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);overflow:hidden;}
|
||||
.ar-panel-head{padding:14px 18px;border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:space-between;}
|
||||
.ar-panel-title{font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.35);letter-spacing:0.08em;text-transform:uppercase;}
|
||||
.ar-panel-badge{font-family:var(--mono);font-size:0.5625rem;color:rgba(167,139,250,0.8);background:rgba(124,58,237,0.12);border:1px solid rgba(124,58,237,0.2);padding:2px 8px;border-radius:var(--r-full);}
|
||||
.ar-row{display:flex;align-items:flex-start;gap:12px;padding:12px 18px;border-bottom:1px solid rgba(255,255,255,0.04);}
|
||||
.ar-row:last-child{border-bottom:none;}
|
||||
.ar-checkbox{width:16px;height:16px;border-radius:3px;border:1.5px solid rgba(255,255,255,0.18);flex-shrink:0;margin-top:2px;display:flex;align-items:center;justify-content:center;font-size:0.5rem;}
|
||||
.ar-checkbox.checked{background:rgba(74,222,128,0.15);border-color:rgba(74,222,128,0.5);color:#4ADE80;}
|
||||
.ar-checkbox.unchecked{background:rgba(248,113,113,0.08);border-color:rgba(248,113,113,0.3);}
|
||||
.ar-prop{flex:1;}
|
||||
.ar-prop-name{font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.45);margin-bottom:4px;}
|
||||
.ar-prop-change{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
|
||||
.ar-val-old{font-family:var(--mono);font-size:0.625rem;color:#FCA5A5;background:rgba(220,38,38,0.1);padding:1px 6px;border-radius:3px;text-decoration:line-through;text-decoration-color:rgba(248,113,113,0.4);}
|
||||
.ar-val-arrow{font-size:0.5625rem;color:rgba(255,255,255,0.2);}
|
||||
.ar-val-new{font-family:var(--mono);font-size:0.625rem;color:#86EFAC;background:rgba(22,163,74,0.1);padding:1px 6px;border-radius:3px;}
|
||||
.ar-actions{display:flex;gap:4px;flex-shrink:0;margin-top:2px;}
|
||||
.ar-btn{font-family:var(--mono);font-size:0.5rem;padding:3px 8px;border-radius:3px;font-weight:600;letter-spacing:0.04em;cursor:default;}
|
||||
.ar-btn-accept{background:rgba(22,163,74,0.12);color:#4ADE80;border:1px solid rgba(74,222,128,0.2);}
|
||||
.ar-btn-reject{background:rgba(220,38,38,0.08);color:#F87171;border:1px solid rgba(248,113,113,0.2);}
|
||||
|
||||
/* RELATED */
|
||||
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:48px;}
|
||||
.related-card{border:1px solid var(--border);border-radius:var(--r-4);padding:24px;transition:box-shadow 0.15s,border-color 0.15s;display:flex;flex-direction:column;gap:10px;}
|
||||
.related-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.related-icon{font-size:1.25rem;}
|
||||
.related-card h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;}
|
||||
.related-card p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;flex:1;}
|
||||
.related-link{font-size:0.8125rem;font-weight:600;color:var(--purple);}
|
||||
|
||||
/* CTA */
|
||||
#cta-bottom{background:var(--bg-inv);padding:120px 40px;position:relative;overflow:hidden;text-align:center;}
|
||||
#cta-bottom::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 30% 50%,rgba(124,58,237,0.14),transparent 60%),radial-gradient(ellipse 60% 80% at 70% 50%,rgba(0,102,255,0.08),transparent 60%);pointer-events:none;}
|
||||
.cta-inner{position:relative;max-width:680px;margin:0 auto;}
|
||||
.cta-headline{font-size:clamp(2rem,4.5vw,3.5rem);font-weight:900;letter-spacing:-0.045em;color:var(--fg-inv);line-height:1.05;margin-bottom:18px;}
|
||||
.cta-sub{font-size:1.0625rem;color:var(--fg-inv-2);margin-bottom:44px;line-height:1.65;}
|
||||
.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* FOOTER */
|
||||
footer{background:#08080C;border-top:1px solid rgba(255,255,255,0.06);padding:72px 40px 40px;}
|
||||
.ft-inner{max-width:1120px;margin:0 auto;}
|
||||
.ft-top{display:grid;grid-template-columns:260px repeat(4,1fr);gap:48px;padding-bottom:56px;border-bottom:1px solid rgba(255,255,255,0.07);margin-bottom:36px;}
|
||||
.ft-brand-logo{font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:rgba(255,255,255,0.92);margin-bottom:8px;}
|
||||
.ft-brand-logo span{color:#3385FF;}
|
||||
.ft-brand-tag{font-size:0.875rem;color:rgba(255,255,255,0.3);line-height:1.65;max-width:200px;}
|
||||
.ft-col h4{font-size:0.875rem;font-weight:600;color:rgba(255,255,255,0.7);margin-bottom:16px;letter-spacing:-0.01em;}
|
||||
.ft-col ul{list-style:none;display:flex;flex-direction:column;gap:10px;}
|
||||
.ft-col a{font-size:0.875rem;color:rgba(255,255,255,0.32);transition:color 0.14s;letter-spacing:-0.01em;}
|
||||
.ft-col a:hover{color:rgba(255,255,255,0.65);}
|
||||
.ft-bottom{display:flex;justify-content:space-between;align-items:center;}
|
||||
.ft-copy{font-size:0.8125rem;color:rgba(255,255,255,0.22);}
|
||||
|
||||
/* REVEAL */
|
||||
.reveal{opacity:0;transform:translateY(22px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1);}
|
||||
.reveal.in{opacity:1;transform:none;}
|
||||
.stagger>*{opacity:0;transform:translateY(16px);transition:opacity 0.55s cubic-bezier(0.16,1,0.3,1),transform 0.55s cubic-bezier(0.16,1,0.3,1);}
|
||||
.stagger.in>*:nth-child(1){opacity:1;transform:none;transition-delay:0s;}
|
||||
.stagger.in>*:nth-child(2){opacity:1;transform:none;transition-delay:0.07s;}
|
||||
.stagger.in>*:nth-child(3){opacity:1;transform:none;transition-delay:0.14s;}
|
||||
.stagger.in>*:nth-child(4){opacity:1;transform:none;transition-delay:0.21s;}
|
||||
.stagger.in>*:nth-child(5){opacity:1;transform:none;transition-delay:0.28s;}
|
||||
.stagger.in>*:nth-child(6){opacity:1;transform:none;transition-delay:0.35s;}
|
||||
|
||||
@media(max-width:1024px){.feat-split{grid-template-columns:1fr;gap:56px;}.feat-split.rev{direction:ltr;}.hiw-steps{grid-template-columns:1fr 1fr;gap:40px;}.hiw-steps::before{display:none;}.tech-grid{grid-template-columns:1fr 1fr;}.related-grid{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:768px){.wrap{padding:0 24px;}#nav{padding:0 24px;}.nav-inner{display:flex;justify-content:space-between;}.nav-links,.nav-actions .btn{display:none;}.nav-burger{display:flex;}#page-hero{padding:120px 24px 80px;}.pv-shell{margin-top:48px;}.diff-canvas{flex-direction:column;}.diff-divider{width:100%;height:1px;}.hiw-steps{grid-template-columns:1fr;}.tech-grid,.related-grid{grid-template-columns:1fr;}.ft-top{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:480px){.ft-top{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo">Origin<span>main</span></a>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-dropdown">
|
||||
<a href="/#features" style="display:flex;align-items:center;gap:4px;">
|
||||
Product
|
||||
<svg class="nav-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a class="nav-dd-item" href="/features/live-artboard.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Live Artboard</span><span class="nav-dd-desc">Your app rendered live in the canvas</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/origin-graph.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Origin Graph</span><span class="nav-dd-desc">Every component, indexed and queryable</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/intent-diff.html">
|
||||
<div class="nav-dd-icon purple"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Intent Diff</span><span class="nav-dd-desc">Ship component-level diffs, not redlines</span></div>
|
||||
</a>
|
||||
<div class="nav-dd-sep"></div>
|
||||
<a class="nav-dd-item" href="/features/completion-zone.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Completion Zone</span><span class="nav-dd-desc">AI-filled design, constrained to your system</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/agent-bridge.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Agent Bridge</span><span class="nav-dd-desc">MCP channel to Cursor, Claude Code & more</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/integrations.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Integrations</span><span class="nav-dd-desc">Linear, Slack, GitHub, Figma & more</span></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<a href="#" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-mobile" id="nav-mobile">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/#features" id="mob-product-toggle" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Product
|
||||
<svg id="mob-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none" style="transition:transform 0.15s;"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-mobile-sub" id="nav-mobile-sub">
|
||||
<a href="/features/live-artboard.html">Live Artboard</a>
|
||||
<a href="/features/origin-graph.html">Origin Graph</a>
|
||||
<a href="/features/intent-diff.html">Intent Diff</a>
|
||||
<a href="/features/completion-zone.html">Completion Zone</a>
|
||||
<a href="/features/agent-bridge.html">Agent Bridge</a>
|
||||
<a href="/features/integrations.html">Integrations</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="#" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section id="page-hero">
|
||||
<div class="page-hero-inner">
|
||||
<span class="label reveal">△ Intent Diff</span>
|
||||
<h1 class="d1 reveal">Ship component-level<br>diffs, not redlines.</h1>
|
||||
<p class="subtitle reveal">Every design change is expressed as a structured diff of props, styles, and tree — not a screenshot comparison. Reviewers see exactly what changed and why.</p>
|
||||
<div class="page-hero-ctas reveal">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="#how-it-works" class="btn btn-outline-white btn-lg">See how it works ↓</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Split diff product visual -->
|
||||
<div class="pv-shell reveal">
|
||||
<div class="pv-chrome">
|
||||
<div class="pv-dot r"></div>
|
||||
<div class="pv-dot y"></div>
|
||||
<div class="pv-dot g"></div>
|
||||
<div class="pv-url">🔒 app.originmain.io/diff/Button@pr-412</div>
|
||||
<div class="pv-tab-bar">
|
||||
<span class="pv-tab active">Split</span>
|
||||
<span class="pv-tab">Unified</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diff-canvas">
|
||||
|
||||
<!-- Before panel -->
|
||||
<div class="diff-panel before">
|
||||
<div class="diff-panel-head">
|
||||
<span class="diff-panel-label">Before</span>
|
||||
<span class="diff-panel-file">a/Button.tsx</span>
|
||||
</div>
|
||||
<div class="diff-lines">
|
||||
<div class="dl hunk"><span class="dl-num"></span><span class="dl-sign"></span><span class="dl-text">@@ -1,10 +1,10 @@ Button</span></div>
|
||||
<div class="dl ctx"><span class="dl-num">1</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">component:</span> <span class="prop-val-str">"Button"</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">2</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">variant:</span> <span class="prop-val-str">"primary"</span></span></div>
|
||||
<div class="dl rem"><span class="dl-num">3</span><span class="dl-sign">−</span><span class="dl-text"><span class="prop-key">padding:</span> <span class="prop-val-rem">16px</span></span></div>
|
||||
<div class="dl rem"><span class="dl-num">4</span><span class="dl-sign">−</span><span class="dl-text"><span class="prop-key">color:</span> <span class="prop-val-rem">#333333</span></span></div>
|
||||
<div class="dl rem"><span class="dl-num">5</span><span class="dl-sign">−</span><span class="dl-text"><span class="prop-key">background:</span> <span class="prop-val-rem">#F5F5F5</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">6</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">fontSize:</span> <span class="prop-val-num">14</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">7</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">fontWeight:</span> <span class="prop-val-num">500</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">8</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">height:</span> <span class="prop-val-num">36</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">9</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">disabled:</span> <span class="prop-val-num">false</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center divider -->
|
||||
<div class="diff-divider"></div>
|
||||
|
||||
<!-- After panel -->
|
||||
<div class="diff-panel after">
|
||||
<div class="diff-panel-head">
|
||||
<span class="diff-panel-label">After</span>
|
||||
<span class="diff-panel-file">b/Button.tsx</span>
|
||||
</div>
|
||||
<div class="diff-lines">
|
||||
<div class="dl hunk"><span class="dl-num"></span><span class="dl-sign"></span><span class="dl-text">@@ -1,10 +1,11 @@ Button</span></div>
|
||||
<div class="dl ctx"><span class="dl-num">1</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">component:</span> <span class="prop-val-str">"Button"</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">2</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">variant:</span> <span class="prop-val-str">"primary"</span></span></div>
|
||||
<div class="dl add"><span class="dl-num">3</span><span class="dl-sign">+</span><span class="dl-text"><span class="prop-key">padding:</span> <span class="prop-val-add">24px</span></span></div>
|
||||
<div class="dl add"><span class="dl-num">4</span><span class="dl-sign">+</span><span class="dl-text"><span class="prop-key">color:</span> <span class="prop-val-add">#0066FF</span></span></div>
|
||||
<div class="dl add"><span class="dl-num">5</span><span class="dl-sign">+</span><span class="dl-text"><span class="prop-key">background:</span> <span class="prop-val-add">#EBF2FF</span></span></div>
|
||||
<div class="dl add"><span class="dl-num">6</span><span class="dl-sign">+</span><span class="dl-text"><span class="prop-key">borderRadius:</span> <span class="prop-val-add">8px</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">7</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">fontSize:</span> <span class="prop-val-num">14</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">8</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">fontWeight:</span> <span class="prop-val-num">500</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">9</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">height:</span> <span class="prop-val-num">36</span></span></div>
|
||||
<div class="dl ctx"><span class="dl-num">10</span><span class="dl-sign"> </span><span class="dl-text"><span class="prop-key">disabled:</span> <span class="prop-val-num">false</span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section id="how-it-works" class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Architecture</span>
|
||||
<h2 class="d2">From snapshot to diff in four steps.</h2>
|
||||
<p class="subtitle">Intent Diff runs a Myers/LCS algorithm on serialized component trees — not on pixels. Every merge triggers a full, machine-readable patch automatically.</p>
|
||||
</div>
|
||||
<div class="hiw-steps stagger">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Snapshot captured</h3>
|
||||
<p>The component tree is serialized to a structured document of props and styles, validated against a Zod schema before being stored as the diff baseline.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Myers algorithm runs</h3>
|
||||
<p>The LCS-based Myers diff algorithm compares the serialized lines of the before and after snapshots, finding the minimum edit script between them.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>Unified patch generated</h3>
|
||||
<p>A standard unified patch string is emitted — <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">--- before / +++ after</code> with hunk headers and context lines on each side.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">04</div>
|
||||
<h3>Diff rendered</h3>
|
||||
<p>The patch is passed to <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">@pierre/diffs</code> and displayed in Split or Unified view for reviewers to inspect, accept, or reject individual prop changes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DEEP DIVE 1: Structured diff -->
|
||||
<div class="section" style="padding:100px 40px;">
|
||||
<div class="feat-split reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-purple">Structured, not visual</span>
|
||||
<h2 class="d3">The diff runs on the<br>component AST, not pixels.</h2>
|
||||
<p class="subtitle">Screenshot comparison tools tell you something looks different. Intent Diff tells you exactly which prop changed, from what value to what value. Every diff is a first-class data structure — queryable, storable, and consumable by CI/CD pipelines without any manual interpretation.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Diffs are computed on Zod-validated component snapshots, never on rendered images.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Output is a standard unified patch string — parseable by any diff tooling in your stack.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>CI/CD can consume the patch directly — block merges, post PR comments, or trigger alerts on specific prop changes.</li>
|
||||
</ul>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:24px;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:14px;">Unified patch — Button.tsx</div>
|
||||
<div style="background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:8px;padding:16px;font-family:var(--mono);font-size:0.6rem;line-height:1.9;flex:1;overflow:hidden;">
|
||||
<div style="color:#A78BFA;">--- a/Button</div>
|
||||
<div style="color:#A78BFA;">+++ b/Button</div>
|
||||
<div style="color:#A78BFA;margin-bottom:4px;">@@ -3,5 +3,6 @@ Button</div>
|
||||
<div style="color:rgba(255,255,255,0.35);"> component: "Button"</div>
|
||||
<div style="color:rgba(255,255,255,0.35);"> variant: "primary"</div>
|
||||
<div style="color:#F87171;background:rgba(220,38,38,0.1);padding:1px 0;">- padding: 16px</div>
|
||||
<div style="color:#4ADE80;background:rgba(22,163,74,0.1);padding:1px 0;">+ padding: 24px</div>
|
||||
<div style="color:#F87171;background:rgba(220,38,38,0.1);padding:1px 0;">- color: #333333</div>
|
||||
<div style="color:#4ADE80;background:rgba(22,163,74,0.1);padding:1px 0;">+ color: #0066FF</div>
|
||||
<div style="color:#F87171;background:rgba(220,38,38,0.1);padding:1px 0;">- background: #F5F5F5</div>
|
||||
<div style="color:#4ADE80;background:rgba(22,163,74,0.1);padding:1px 0;">+ background: #EBF2FF</div>
|
||||
<div style="color:#4ADE80;background:rgba(22,163,74,0.1);padding:1px 0;">+ borderRadius: 8px</div>
|
||||
<div style="color:rgba(255,255,255,0.35);"> fontSize: 14</div>
|
||||
<div style="color:rgba(255,255,255,0.35);"> fontWeight: 500</div>
|
||||
</div>
|
||||
<div style="margin-top:12px;display:flex;gap:16px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="width:8px;height:8px;border-radius:2px;background:rgba(220,38,38,0.45);flex-shrink:0;"></span><span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);">3 removals</span></div>
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="width:8px;height:8px;border-radius:2px;background:rgba(22,163,74,0.45);flex-shrink:0;"></span><span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);">4 additions</span></div>
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="width:8px;height:8px;border-radius:2px;background:rgba(124,58,237,0.45);flex-shrink:0;"></span><span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);">1 hunk</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEEP DIVE 2: Accept / Reject -->
|
||||
<div class="section section-muted" style="padding:100px 40px;">
|
||||
<div class="feat-split rev reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-purple">Granular review</span>
|
||||
<h2 class="d3">Accept or reject<br>individual prop changes.</h2>
|
||||
<p class="subtitle">Reviewers are not forced to approve or reject an entire diff. Each prop change is an independent item — cherry-pick what ships and what goes back, with a full audit trail recorded per decision. No more all-or-nothing PR approvals on design work.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Check a single prop to accept it without touching adjacent changes in the same diff.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Rejected changes surface as inline comments on the originating artboard in real time.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Every accepted change is recorded in the merge commit metadata — zero ambiguity in history.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:24px;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:14px;">Diff inspector — Button@pr-412</div>
|
||||
<div class="ar-panel">
|
||||
<div class="ar-panel-head">
|
||||
<span class="ar-panel-title">Prop changes · 4 total</span>
|
||||
<span class="ar-panel-badge">2 accepted · 1 rejected · 1 pending</span>
|
||||
</div>
|
||||
<div class="ar-row">
|
||||
<div class="ar-checkbox checked">✓</div>
|
||||
<div class="ar-prop">
|
||||
<div class="ar-prop-name">padding</div>
|
||||
<div class="ar-prop-change">
|
||||
<span class="ar-val-old">16px</span>
|
||||
<span class="ar-val-arrow">→</span>
|
||||
<span class="ar-val-new">24px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-actions">
|
||||
<span class="ar-btn ar-btn-accept">Accept</span>
|
||||
<span class="ar-btn ar-btn-reject">Reject</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-row">
|
||||
<div class="ar-checkbox unchecked"></div>
|
||||
<div class="ar-prop">
|
||||
<div class="ar-prop-name">color</div>
|
||||
<div class="ar-prop-change">
|
||||
<span class="ar-val-old">#333333</span>
|
||||
<span class="ar-val-arrow">→</span>
|
||||
<span class="ar-val-new">#0066FF</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-actions">
|
||||
<span class="ar-btn ar-btn-accept">Accept</span>
|
||||
<span class="ar-btn ar-btn-reject">Reject</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-row">
|
||||
<div class="ar-checkbox checked">✓</div>
|
||||
<div class="ar-prop">
|
||||
<div class="ar-prop-name">background</div>
|
||||
<div class="ar-prop-change">
|
||||
<span class="ar-val-old">#F5F5F5</span>
|
||||
<span class="ar-val-arrow">→</span>
|
||||
<span class="ar-val-new">#EBF2FF</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-actions">
|
||||
<span class="ar-btn ar-btn-accept">Accept</span>
|
||||
<span class="ar-btn ar-btn-reject">Reject</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-row">
|
||||
<div class="ar-checkbox"></div>
|
||||
<div class="ar-prop">
|
||||
<div class="ar-prop-name">borderRadius <span style="font-family:var(--mono);font-size:0.5rem;color:rgba(167,139,250,0.65);background:rgba(124,58,237,0.1);padding:1px 5px;border-radius:3px;margin-left:4px;">new</span></div>
|
||||
<div class="ar-prop-change">
|
||||
<span class="ar-val-new">8px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ar-actions">
|
||||
<span class="ar-btn ar-btn-accept">Accept</span>
|
||||
<span class="ar-btn ar-btn-reject">Reject</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:10px;font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.18);text-align:right;">Reviewed by SinachPat · just now</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TECHNICAL SECTION -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Technical foundation</span>
|
||||
<h2 class="d2">Built on proven algorithms.</h2>
|
||||
<p class="subtitle">Intent Diff applies battle-tested diff theory — the same algorithm that powers Git — to component trees, not text files. Structured input means structured, machine-readable output.</p>
|
||||
</div>
|
||||
<div class="tech-grid stagger">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon" style="font-size:1rem;font-weight:800;font-family:var(--mono);">ND</div>
|
||||
<h3>Myers algorithm</h3>
|
||||
<p>The O(ND) Myers diff algorithm finds the shortest edit script between two sequences. The same algorithm used in <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">git diff</code> — applied here to serialized component prop lines, not source code.</p>
|
||||
<span class="tech-tag">Core algorithm</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon" style="font-size:1rem;font-weight:700;font-family:var(--mono);">LCS</div>
|
||||
<h3>LCS backtracking</h3>
|
||||
<p>After the edit graph is constructed, longest-common-subsequence backtracking produces the minimum set of additions and removals — minimizing diff noise and keeping context stable across rebases.</p>
|
||||
<span class="tech-tag">Edit script</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon" style="font-size:1rem;font-weight:700;font-family:var(--mono);">±</div>
|
||||
<h3>Unified patch format</h3>
|
||||
<p>Output is a standard unified patch string with <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">---</code> and <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">+++</code> headers, <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">@@</code> hunk ranges, and three lines of context. Parseable by any standard tooling out of the box.</p>
|
||||
<span class="tech-tag">Output format</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon" style="font-size:0.875rem;font-weight:700;font-family:var(--mono);letter-spacing:-0.04em;">⊟⊠</div>
|
||||
<h3>Split / Unified views</h3>
|
||||
<p>The patch is rendered by <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">@pierre/diffs</code> in two modes: Split (side-by-side panels) for detailed prop comparison, and Unified (stacked) for a linear review flow.</p>
|
||||
<span class="tech-tag">Rendering</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🗂</div>
|
||||
<h3>Zod schemas for snapshots</h3>
|
||||
<p>Every component snapshot is validated at write time against a shared Zod schema. Type-safe serialization means the diff algorithm always operates on well-formed input — no silent failures or undefined prop shapes.</p>
|
||||
<span class="tech-tag">Type safety</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⚙</div>
|
||||
<h3>CI/CD integration</h3>
|
||||
<p>The unified patch is available as a JSON payload on every merge event. Pipe it into GitHub Actions, Linear, or your own webhook to block PRs on token violations or notify designers of overridden changes.</p>
|
||||
<span class="tech-tag">Automation</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RELATED FEATURES -->
|
||||
<section class="section section-muted">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">What connects here</span>
|
||||
<h2 class="d2">Intent Diff sits at the center.</h2>
|
||||
<p class="subtitle">Every artboard edit creates a diff. Every diff is queryable by the graph. Every diff is shippable through the bridge.</p>
|
||||
</div>
|
||||
<div class="related-grid stagger">
|
||||
<a class="related-card" href="/features/live-artboard.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Live Artboard</h3>
|
||||
<p>Every prop change made in a Live Artboard is recorded as an Intent Diff entry automatically — edits accumulate into a reviewable, shippable patch without any extra steps from the designer.</p>
|
||||
<span class="related-link">Explore Live Artboard →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/origin-graph.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3>Origin Graph</h3>
|
||||
<p>Diffs are indexed into the Origin Graph, making it queryable which components changed most frequently, which props are most volatile, and who last touched any given node in the tree.</p>
|
||||
<span class="related-link">Explore Origin Graph →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/agent-bridge.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<h3>Agent Bridge</h3>
|
||||
<p>The Agent Bridge exposes diffs as MCP tool responses. Ask Cursor or Claude Code to apply the accepted diff for any component and the agent writes the prop changes directly into your codebase.</p>
|
||||
<span class="related-link">Explore Agent Bridge →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta-bottom">
|
||||
<div class="cta-inner">
|
||||
<h2 class="cta-headline">No more redline hell.<br>Ship the diff.</h2>
|
||||
<p class="cta-sub">Every design change expressed as a structured, machine-readable, component-level diff. Early access is free.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="/" class="btn btn-outline-white btn-lg">Back to overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="ft-inner">
|
||||
<div class="ft-top">
|
||||
<div>
|
||||
<div class="ft-brand-logo">Origin<span>main</span></div>
|
||||
<p class="ft-brand-tag">Where design intent becomes production code.</p>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="/features/live-artboard.html">Live Artboard</a></li>
|
||||
<li><a href="/features/origin-graph.html">Origin Graph</a></li>
|
||||
<li><a href="/features/intent-diff.html">Intent Diff</a></li>
|
||||
<li><a href="/features/completion-zone.html">Completion Zone</a></li>
|
||||
<li><a href="/features/agent-bridge.html">Agent Bridge</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Integrations</h4>
|
||||
<ul>
|
||||
<li><a href="/features/integrations.html">All integrations</a></li>
|
||||
<li><a href="#">GitHub</a></li>
|
||||
<li><a href="#">Linear</a></li>
|
||||
<li><a href="#">Slack</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Developers</h4>
|
||||
<ul>
|
||||
<li><a href="#">Docs</a></li>
|
||||
<li><a href="#">API Reference</a></li>
|
||||
<li><a href="#">MCP Protocol</a></li>
|
||||
<li><a href="#">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft-bottom">
|
||||
<span class="ft-copy">© 2025 Originmain. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Nav scroll
|
||||
const nav=document.getElementById('nav');
|
||||
window.addEventListener('scroll',()=>{nav.classList.toggle('scrolled',window.scrollY>20);});
|
||||
// Dropdown: delay-based hover (desktop) + click (mobile/touch)
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd=>{const menu=dd.querySelector('.nav-dropdown-menu');let t;const open=()=>{clearTimeout(t);dd.classList.add('open');menu.classList.add('open');};const close=()=>{t=setTimeout(()=>{dd.classList.remove('open');menu.classList.remove('open');},150);};dd.addEventListener('mouseenter',open);dd.addEventListener('mouseleave',close);menu.addEventListener('mouseenter',open);menu.addEventListener('mouseleave',close);dd.querySelector('a').addEventListener('click',e=>{if(window.matchMedia('(hover:none)').matches||window.innerWidth<=1024){e.preventDefault();const o=menu.classList.toggle('open');dd.classList.toggle('open',o);}});});
|
||||
// Mobile product sub-accordion
|
||||
const mpt=document.getElementById('mob-product-toggle'),mns=document.getElementById('nav-mobile-sub'),mdc=document.getElementById('mob-dd-caret');if(mpt&&mns){mpt.addEventListener('click',e=>{e.preventDefault();const o=mns.classList.toggle('open');if(mdc)mdc.style.transform=o?'rotate(180deg)':'';});}
|
||||
// Burger
|
||||
const burger=document.getElementById('nav-burger'),mob=document.getElementById('nav-mobile');
|
||||
burger.addEventListener('click',()=>{const o=mob.classList.toggle('open');burger.classList.toggle('open',o);burger.setAttribute('aria-expanded',o);});
|
||||
// Reveal
|
||||
const io=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target);}});},{threshold:0.12});
|
||||
document.querySelectorAll('.reveal,.stagger').forEach(el=>io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,652 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Live Artboard — Originmain</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#FFFFFF;--bg-subtle:#FAFAFA;--bg-muted:#F4F4F5;--bg-strong:#EBEBEB;--bg-inv:#0A0A0A;--bg-inv-2:#141414;--bg-inv-3:#1C1C1C;--fg:#0A0A0A;--fg-2:#52525B;--fg-3:#A1A1AA;--fg-4:#D4D4D8;--fg-inv:rgba(255,255,255,0.92);--fg-inv-2:rgba(255,255,255,0.52);--fg-inv-3:rgba(255,255,255,0.28);--blue:#0066FF;--blue-dark:#0050CC;--blue-light:#3385FF;--blue-soft:rgba(0,102,255,0.07);--blue-mid:rgba(0,102,255,0.15);--purple:#7C3AED;--teal:#0891B2;--border:rgba(0,0,0,0.07);--border-2:rgba(0,0,0,0.12);--border-inv:rgba(255,255,255,0.08);--border-inv-2:rgba(255,255,255,0.14);--r-1:4px;--r-2:8px;--r-3:12px;--r-4:16px;--r-5:20px;--r-6:24px;--r-full:9999px;--sh-xs:0 1px 2px rgba(0,0,0,0.05);--sh-sm:0 1px 3px rgba(0,0,0,0.07),0 1px 2px rgba(0,0,0,0.05);--sh-md:0 4px 8px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04);--sh-lg:0 10px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.05);--sh-xl:0 20px 40px rgba(0,0,0,0.09),0 8px 16px rgba(0,0,0,0.05);--sh-2xl:0 32px 64px rgba(0,0,0,0.11),0 16px 32px rgba(0,0,0,0.06);--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--mono:'JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||||
html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.6;overflow-x:hidden;}
|
||||
img,svg{display:block;max-width:100%;}
|
||||
a{color:inherit;text-decoration:none;}
|
||||
button{font-family:inherit;cursor:pointer;border:none;background:none;}
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:0 40px;}
|
||||
.section{padding:112px 0;}
|
||||
.section-muted{background:var(--bg-subtle);}
|
||||
.section-dark{background:var(--bg-inv);}
|
||||
.d1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;line-height:1.02;letter-spacing:-0.045em;color:var(--fg);}
|
||||
.d2{font-size:clamp(1.875rem,3.5vw,2.75rem);font-weight:800;line-height:1.1;letter-spacing:-0.038em;color:var(--fg);}
|
||||
.d3{font-size:clamp(1.25rem,2.5vw,1.625rem);font-weight:700;line-height:1.25;letter-spacing:-0.028em;color:var(--fg);}
|
||||
.subtitle{font-size:clamp(1rem,1.8vw,1.125rem);font-weight:400;line-height:1.75;color:var(--fg-2);}
|
||||
.label{font-family:var(--mono);font-size:0.6875rem;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--fg-3);}
|
||||
.label-blue{color:var(--blue);}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font);font-size:0.9375rem;font-weight:600;padding:11px 22px;border-radius:var(--r-2);transition:transform 0.12s cubic-bezier(.16,1,.3,1),opacity 0.12s,box-shadow 0.12s;white-space:nowrap;letter-spacing:-0.01em;position:relative;overflow:hidden;}
|
||||
.btn::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,0);transition:background 0.12s;}
|
||||
.btn:hover::after{background:rgba(255,255,255,0.06);}
|
||||
.btn:active{transform:scale(0.98)!important;}
|
||||
.btn-primary{background:var(--fg);color:var(--bg);box-shadow:var(--sh-sm);}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:var(--sh-md);}
|
||||
.btn-secondary{background:transparent;color:var(--fg);border:1px solid var(--border-2);}
|
||||
.btn-secondary:hover{background:var(--bg-muted);transform:translateY(-1px);}
|
||||
.btn-blue{background:var(--blue);color:#fff;box-shadow:0 4px 12px rgba(0,102,255,0.3);}
|
||||
.btn-blue:hover{background:var(--blue-dark);transform:translateY(-1px);}
|
||||
.btn-lg{font-size:1rem;padding:13px 28px;}
|
||||
.btn-white{background:#fff;color:#0A0A0A;box-shadow:0 4px 12px rgba(255,255,255,0.15);}
|
||||
.btn-white:hover{background:#F5F5F5;transform:translateY(-1px);}
|
||||
.btn-outline-white{background:transparent;color:rgba(255,255,255,0.72);border:1px solid rgba(255,255,255,0.18);}
|
||||
.btn-outline-white:hover{background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.9);transform:translateY(-1px);}
|
||||
.divider{height:1px;background:var(--border);}
|
||||
.section-head{text-align:center;max-width:640px;margin:0 auto 72px;}
|
||||
.section-head .label{display:block;margin-bottom:16px;}
|
||||
.section-head .d2{margin-bottom:18px;}
|
||||
|
||||
/* NAV */
|
||||
#nav{position:fixed;top:0;left:0;right:0;z-index:200;height:60px;display:flex;align-items:center;padding:0 40px;transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;}
|
||||
#nav.scrolled{background:rgba(255,255,255,0.85);backdrop-filter:blur(20px) saturate(1.6);-webkit-backdrop-filter:blur(20px) saturate(1.6);border-bottom:1px solid var(--border);box-shadow:var(--sh-xs);}
|
||||
.nav-inner{max-width:1120px;margin:0 auto;width:100%;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
|
||||
.nav-logo{justify-self:start;font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:var(--fg);display:flex;align-items:center;gap:2px;}
|
||||
.nav-logo span{color:var(--blue);}
|
||||
.nav-links{justify-self:center;display:flex;align-items:center;gap:0;list-style:none;}
|
||||
.nav-links a{font-size:0.9375rem;font-weight:500;color:var(--fg-2);padding:6px 14px;border-radius:var(--r-1);transition:color 0.12s,background 0.12s;letter-spacing:-0.01em;}
|
||||
.nav-links a:hover{color:var(--fg);}
|
||||
.nav-actions{justify-self:end;display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* Dropdown */
|
||||
.nav-dropdown{position:relative;}
|
||||
.nav-dropdown>a{display:flex;align-items:center;gap:4px;}
|
||||
.nav-dd-caret{transition:transform 0.15s;}
|
||||
/* caret rotation handled via JS .open class */
|
||||
.nav-dropdown-menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-4px);background:rgba(255,255,255,0.98);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--r-3);box-shadow:var(--sh-xl);padding:8px;min-width:240px;opacity:0;visibility:hidden;transition:opacity 0.15s,transform 0.15s,visibility 0.15s;pointer-events:none;}
|
||||
.nav-dropdown-menu.open{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:all;}
|
||||
.nav-dropdown.open .nav-dd-caret{transform:rotate(180deg);}
|
||||
/* Mobile sub-nav */
|
||||
.nav-mobile-sub{display:none;padding:2px 0 6px 14px;flex-direction:column;}
|
||||
.nav-mobile-sub.open{display:flex;}
|
||||
.nav-mobile-sub a{font-size:0.875rem;color:rgba(255,255,255,0.4);padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.06);letter-spacing:-0.01em;transition:color 0.1s;}
|
||||
.nav-mobile-sub a:last-child{border-bottom:none;}
|
||||
.nav-mobile-sub a:hover{color:rgba(255,255,255,0.8);}
|
||||
/* Dark hero: white nav before scrolling */
|
||||
#nav:not(.scrolled) .nav-logo{color:rgba(255,255,255,0.92);}
|
||||
#nav:not(.scrolled) .nav-logo span{color:#3385FF;}
|
||||
#nav:not(.scrolled) .nav-links a{color:rgba(255,255,255,0.5);}
|
||||
#nav:not(.scrolled) .nav-links a:hover{color:rgba(255,255,255,0.9);}
|
||||
#nav:not(.scrolled) .nav-actions .btn-secondary{color:rgba(255,255,255,0.65);border-color:rgba(255,255,255,0.2);background:transparent;}
|
||||
#nav:not(.scrolled) .nav-burger span{background:rgba(255,255,255,0.8);}
|
||||
.nav-dd-item{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:var(--r-2);transition:background 0.1s;cursor:pointer;}
|
||||
.nav-dd-item:hover{background:var(--bg-muted);}
|
||||
.nav-dd-icon{width:30px;height:30px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:13px;color:var(--blue);}
|
||||
.nav-dd-text{display:flex;flex-direction:column;}
|
||||
.nav-dd-name{font-size:0.8125rem;font-weight:600;color:var(--fg);letter-spacing:-0.01em;}
|
||||
.nav-dd-desc{font-size:0.6875rem;color:var(--fg-3);margin-top:1px;line-height:1.4;}
|
||||
.nav-dd-sep{height:1px;background:var(--border);margin:4px 0;}
|
||||
.nav-burger{display:none;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:36px;height:36px;background:none;border:none;cursor:pointer;padding:4px;border-radius:var(--r-2);}
|
||||
.nav-burger span{display:block;width:20px;height:2px;background:var(--fg);border-radius:2px;transition:transform 0.22s cubic-bezier(0.16,1,0.3,1),opacity 0.18s;transform-origin:center;}
|
||||
.nav-burger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
|
||||
.nav-burger.open span:nth-child(2){opacity:0;transform:scaleX(0);}
|
||||
.nav-burger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
|
||||
.nav-mobile{display:none;position:absolute;top:60px;left:0;right:0;background:rgba(255,255,255,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,0.07);padding:12px 24px 24px;flex-direction:column;gap:2px;}
|
||||
.nav-mobile.open{display:flex;}
|
||||
.nav-mobile ul{list-style:none;display:flex;flex-direction:column;}
|
||||
.nav-mobile ul li a{display:block;font-size:1rem;font-weight:500;color:var(--fg-2);padding:11px 4px;border-bottom:1px solid var(--border);letter-spacing:-0.01em;}
|
||||
.nav-mobile ul li:last-child a{border-bottom:none;}
|
||||
.nav-mobile-ctas{display:flex;gap:8px;margin-top:16px;}
|
||||
.nav-mobile-ctas .btn{flex:1;justify-content:center;padding:10px 12px;font-size:.875rem;}
|
||||
|
||||
/* PAGE HERO */
|
||||
#page-hero{padding:140px 40px 100px;background:var(--bg-inv);position:relative;overflow:hidden;text-align:center;}
|
||||
#page-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(51,133,255,0.12),transparent 60%),radial-gradient(ellipse 40% 40% at 80% 70%,rgba(124,58,237,0.07),transparent 50%);pointer-events:none;}
|
||||
.page-hero-inner{position:relative;max-width:800px;margin:0 auto;}
|
||||
.page-hero-inner .label{color:rgba(255,255,255,0.3);margin-bottom:20px;display:block;}
|
||||
.page-hero-inner .d1{color:var(--fg-inv);margin-bottom:22px;}
|
||||
.page-hero-inner .subtitle{color:var(--fg-inv-2);max-width:580px;margin:0 auto 44px;}
|
||||
.page-hero-ctas{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* PRODUCT VISUAL */
|
||||
.pv-shell{width:100%;max-width:960px;margin:72px auto 0;border-radius:16px 16px 0 0;overflow:hidden;box-shadow:0 48px 96px rgba(0,0,0,0.45),0 0 0 1px rgba(255,255,255,0.06);border-bottom:none;position:relative;}
|
||||
.pv-chrome{background:#1C1C22;height:40px;display:flex;align-items:center;padding:0 14px;gap:7px;border-bottom:1px solid rgba(255,255,255,0.05);}
|
||||
.pv-dot{width:11px;height:11px;border-radius:50%;}
|
||||
.pv-dot.r{background:#FF5F57;}
|
||||
.pv-dot.y{background:#FFBD2E;}
|
||||
.pv-dot.g{background:#28C840;}
|
||||
.pv-url{flex:1;margin:0 12px;background:rgba(255,255,255,0.06);border-radius:5px;height:25px;display:flex;align-items:center;padding:0 10px;font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.3);max-width:320px;}
|
||||
.pv-canvas{background:#0D0D11;height:380px;position:relative;display:flex;align-items:center;justify-content:center;}
|
||||
.pv-grid{position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,0.055) 1px,transparent 1px);background-size:24px 24px;pointer-events:none;}
|
||||
|
||||
/* ARTBOARD VISUAL */
|
||||
.pv-ab{position:absolute;background:#fff;border-radius:10px;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,0.6);}
|
||||
.pv-ab.sel{box-shadow:0 0 0 2px #3385FF,0 8px 40px rgba(0,0,0,0.6);}
|
||||
.pv-ab-lbl{position:absolute;top:-22px;left:0;font-family:var(--mono);font-size:0.5rem;color:#3385FF;white-space:nowrap;}
|
||||
.pv-handle{position:absolute;width:7px;height:7px;background:#fff;border:2px solid #3385FF;border-radius:1.5px;z-index:10;}
|
||||
.pv-h-tl{top:-3px;left:-3px;}
|
||||
.pv-h-tr{top:-3px;right:-3px;}
|
||||
.pv-h-bl{bottom:-3px;left:-3px;}
|
||||
.pv-h-br{bottom:-3px;right:-3px;}
|
||||
|
||||
/* LIVE BADGE */
|
||||
.pv-live-badge{position:absolute;top:16px;right:16px;display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:6px;background:rgba(16,185,129,0.12);border:1px solid rgba(16,185,129,0.25);}
|
||||
.pv-live-dot{width:6px;height:6px;border-radius:50%;background:#10B981;box-shadow:0 0 0 2px rgba(16,185,129,0.25);animation:live-pulse 2s ease-in-out infinite;}
|
||||
@keyframes live-pulse{0%,100%{box-shadow:0 0 0 2px rgba(16,185,129,0.25);}50%{box-shadow:0 0 0 4px rgba(16,185,129,0.1);}}
|
||||
.pv-live-txt{font-family:var(--mono);font-size:0.5625rem;color:#10B981;font-weight:600;}
|
||||
|
||||
/* Fiber overlay */
|
||||
.pv-fiber{position:absolute;pointer-events:none;z-index:5;}
|
||||
.pv-fiber-box{position:absolute;border:1px solid rgba(51,133,255,0.5);border-radius:2px;}
|
||||
.pv-fiber-lbl{position:absolute;background:rgba(51,133,255,0.9);color:#fff;font-family:var(--mono);font-size:0.4rem;padding:1px 4px;border-radius:2px;white-space:nowrap;top:-14px;left:0;}
|
||||
|
||||
/* HOW IT WORKS SECTION */
|
||||
.hiw-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:64px;position:relative;}
|
||||
.hiw-steps::before{content:'';position:absolute;top:28px;left:12.5%;right:12.5%;height:1px;background:linear-gradient(90deg,transparent,var(--border-2) 20%,var(--border-2) 80%,transparent);}
|
||||
.hiw-step{display:flex;flex-direction:column;align-items:center;text-align:center;padding:0 20px;position:relative;z-index:1;}
|
||||
.hiw-num{width:56px;height:56px;border-radius:50%;background:var(--bg);border:1px solid var(--border-2);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--blue);margin-bottom:20px;box-shadow:var(--sh-sm);}
|
||||
.hiw-step h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.hiw-step p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;}
|
||||
|
||||
/* FEATURE SPLIT */
|
||||
.feat-split{display:grid;grid-template-columns:1fr 1fr;gap:96px;align-items:center;max-width:1120px;margin:0 auto;}
|
||||
.feat-split.rev{direction:rtl;}
|
||||
.feat-split.rev>*{direction:ltr;}
|
||||
.feat-copy .label{display:block;margin-bottom:16px;}
|
||||
.feat-copy .d3{margin-bottom:16px;}
|
||||
.feat-copy .subtitle{margin-bottom:28px;}
|
||||
.feat-pts{list-style:none;display:flex;flex-direction:column;gap:12px;margin-bottom:32px;}
|
||||
.feat-pts li{display:flex;align-items:flex-start;gap:10px;font-size:0.9375rem;color:var(--fg-2);line-height:1.55;}
|
||||
.feat-pt-icon{width:20px;height:20px;border-radius:3px;background:var(--blue-soft);border:1px solid var(--blue-mid);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;font-size:0.625rem;color:var(--blue);}
|
||||
|
||||
/* Feature visual */
|
||||
.feat-vis{border-radius:var(--r-5);overflow:hidden;box-shadow:var(--sh-2xl);border:1px solid var(--border);aspect-ratio:4/3;position:relative;}
|
||||
.feat-vis-dark{background:#0D0D11;border-color:rgba(255,255,255,0.07);}
|
||||
.feat-vis-light{background:var(--bg-muted);}
|
||||
|
||||
/* TECH SECTION */
|
||||
.tech-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:56px;}
|
||||
.tech-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:32px 28px;transition:box-shadow 0.2s;}
|
||||
.tech-card:hover{box-shadow:var(--sh-lg);}
|
||||
.tech-icon{width:44px;height:44px;background:var(--blue-soft);border:1px solid var(--blue-mid);border-radius:var(--r-2);display:flex;align-items:center;justify-content:center;font-size:1.25rem;margin-bottom:18px;}
|
||||
.tech-card h3{font-size:1rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.tech-card p{font-size:0.875rem;color:var(--fg-2);line-height:1.65;}
|
||||
.tech-tag{display:inline-block;margin-top:14px;font-family:var(--mono);font-size:0.625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--blue);background:var(--blue-soft);border:1px solid var(--blue-mid);padding:3px 9px;border-radius:var(--r-full);}
|
||||
|
||||
/* CODE BLOCK */
|
||||
.code-block{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);padding:24px;font-family:var(--mono);font-size:0.75rem;line-height:1.75;color:rgba(255,255,255,0.75);overflow-x:auto;}
|
||||
.code-block .kw{color:#7EB8FF;}
|
||||
.code-block .str{color:#7DDBA0;}
|
||||
.code-block .cm{color:rgba(255,255,255,0.28);}
|
||||
.code-block .fn{color:#FFBA7B;}
|
||||
.code-block .nm{color:#D4A0FF;}
|
||||
|
||||
/* RELATED */
|
||||
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:48px;}
|
||||
.related-card{border:1px solid var(--border);border-radius:var(--r-4);padding:24px;transition:box-shadow 0.15s,border-color 0.15s;display:flex;flex-direction:column;gap:10px;}
|
||||
.related-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.related-icon{font-size:1.25rem;}
|
||||
.related-card h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;}
|
||||
.related-card p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;flex:1;}
|
||||
.related-link{font-size:0.8125rem;font-weight:600;color:var(--blue);}
|
||||
|
||||
/* CTA */
|
||||
#cta-bottom{background:var(--bg-inv);padding:120px 40px;position:relative;overflow:hidden;text-align:center;}
|
||||
#cta-bottom::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 30% 50%,rgba(124,58,237,0.12),transparent 60%),radial-gradient(ellipse 60% 80% at 70% 50%,rgba(0,102,255,0.1),transparent 60%);pointer-events:none;}
|
||||
.cta-inner{position:relative;max-width:680px;margin:0 auto;}
|
||||
.cta-headline{font-size:clamp(2rem,4.5vw,3.5rem);font-weight:900;letter-spacing:-0.045em;color:var(--fg-inv);line-height:1.05;margin-bottom:18px;}
|
||||
.cta-sub{font-size:1.0625rem;color:var(--fg-inv-2);margin-bottom:44px;line-height:1.65;}
|
||||
.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* FOOTER */
|
||||
footer{background:#08080C;border-top:1px solid rgba(255,255,255,0.06);padding:72px 40px 40px;}
|
||||
.ft-inner{max-width:1120px;margin:0 auto;}
|
||||
.ft-top{display:grid;grid-template-columns:260px repeat(4,1fr);gap:48px;padding-bottom:56px;border-bottom:1px solid rgba(255,255,255,0.07);margin-bottom:36px;}
|
||||
.ft-brand-logo{font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:rgba(255,255,255,0.92);margin-bottom:8px;}
|
||||
.ft-brand-logo span{color:#3385FF;}
|
||||
.ft-brand-tag{font-size:0.875rem;color:rgba(255,255,255,0.3);line-height:1.65;max-width:200px;}
|
||||
.ft-col h4{font-size:0.875rem;font-weight:600;color:rgba(255,255,255,0.7);margin-bottom:16px;letter-spacing:-0.01em;}
|
||||
.ft-col ul{list-style:none;display:flex;flex-direction:column;gap:10px;}
|
||||
.ft-col a{font-size:0.875rem;color:rgba(255,255,255,0.32);transition:color 0.14s;letter-spacing:-0.01em;}
|
||||
.ft-col a:hover{color:rgba(255,255,255,0.65);}
|
||||
.ft-bottom{display:flex;justify-content:space-between;align-items:center;}
|
||||
.ft-copy{font-size:0.8125rem;color:rgba(255,255,255,0.22);}
|
||||
|
||||
/* REVEAL */
|
||||
.reveal{opacity:0;transform:translateY(22px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1);}
|
||||
.reveal.in{opacity:1;transform:none;}
|
||||
.stagger>*{opacity:0;transform:translateY(16px);transition:opacity 0.55s cubic-bezier(0.16,1,0.3,1),transform 0.55s cubic-bezier(0.16,1,0.3,1);}
|
||||
.stagger.in>*:nth-child(1){opacity:1;transform:none;transition-delay:0s;}
|
||||
.stagger.in>*:nth-child(2){opacity:1;transform:none;transition-delay:0.07s;}
|
||||
.stagger.in>*:nth-child(3){opacity:1;transform:none;transition-delay:0.14s;}
|
||||
.stagger.in>*:nth-child(4){opacity:1;transform:none;transition-delay:0.21s;}
|
||||
|
||||
@media(max-width:1024px){.feat-split{grid-template-columns:1fr;gap:56px;}.feat-split.rev{direction:ltr;}.hiw-steps{grid-template-columns:1fr 1fr;gap:40px;}.hiw-steps::before{display:none;}.tech-grid{grid-template-columns:1fr 1fr;}.related-grid{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:768px){.wrap{padding:0 24px;}#nav{padding:0 24px;}.nav-inner{display:flex;justify-content:space-between;}.nav-links,.nav-actions .btn{display:none;}.nav-burger{display:flex;}#page-hero{padding:120px 24px 80px;}.pv-shell{margin-top:48px;}.hiw-steps{grid-template-columns:1fr;}.tech-grid,.related-grid{grid-template-columns:1fr;}.ft-top{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:480px){.ft-top{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo">Origin<span>main</span></a>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-dropdown">
|
||||
<a href="/#features" style="display:flex;align-items:center;gap:4px;">
|
||||
Product
|
||||
<svg class="nav-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a class="nav-dd-item" href="/features/live-artboard.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Live Artboard</span><span class="nav-dd-desc">Your app rendered live in the canvas</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/origin-graph.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Origin Graph</span><span class="nav-dd-desc">Every component, indexed and queryable</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/intent-diff.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Intent Diff</span><span class="nav-dd-desc">Ship component-level diffs, not redlines</span></div>
|
||||
</a>
|
||||
<div class="nav-dd-sep"></div>
|
||||
<a class="nav-dd-item" href="/features/completion-zone.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Completion Zone</span><span class="nav-dd-desc">AI-filled design, constrained to your system</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/agent-bridge.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Agent Bridge</span><span class="nav-dd-desc">MCP channel to Cursor, Claude Code & more</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/integrations.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Integrations</span><span class="nav-dd-desc">Linear, Slack, GitHub, Figma & more</span></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<a href="#" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-mobile" id="nav-mobile">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/#features" id="mob-product-toggle" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Product
|
||||
<svg id="mob-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none" style="transition:transform 0.15s;"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-mobile-sub" id="nav-mobile-sub">
|
||||
<a href="/features/live-artboard.html">Live Artboard</a>
|
||||
<a href="/features/origin-graph.html">Origin Graph</a>
|
||||
<a href="/features/intent-diff.html">Intent Diff</a>
|
||||
<a href="/features/completion-zone.html">Completion Zone</a>
|
||||
<a href="/features/agent-bridge.html">Agent Bridge</a>
|
||||
<a href="/features/integrations.html">Integrations</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="#" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section id="page-hero">
|
||||
<div class="page-hero-inner">
|
||||
<span class="label reveal">Live Artboard</span>
|
||||
<h1 class="d1 reveal">Your app,<br>live on the canvas.</h1>
|
||||
<p class="subtitle reveal">Not a mockup. Not a screenshot. Every artboard is a real iframe render of your actual component tree — running your real code, connected to your real data.</p>
|
||||
<div class="page-hero-ctas reveal">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="#how-it-works" class="btn btn-outline-white btn-lg">See how it works ↓</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Product visual -->
|
||||
<div class="pv-shell reveal">
|
||||
<div class="pv-chrome">
|
||||
<div class="pv-dot r"></div>
|
||||
<div class="pv-dot y"></div>
|
||||
<div class="pv-dot g"></div>
|
||||
<div class="pv-url">🔒 app.originmain.io/canvas</div>
|
||||
</div>
|
||||
<div class="pv-canvas">
|
||||
<div class="pv-grid"></div>
|
||||
<div class="pv-live-badge"><div class="pv-live-dot"></div><span class="pv-live-txt">Live render</span></div>
|
||||
|
||||
<!-- Selected artboard -->
|
||||
<div class="pv-ab sel" style="top:48px;left:80px;width:220px;">
|
||||
<div class="pv-ab-lbl">DashboardCard — live</div>
|
||||
<div class="pv-handle pv-h-tl"></div><div class="pv-handle pv-h-tr"></div>
|
||||
<div class="pv-handle pv-h-bl"></div><div class="pv-handle pv-h-br"></div>
|
||||
<!-- Fiber overlay boxes -->
|
||||
<div class="pv-fiber" style="inset:0;">
|
||||
<div class="pv-fiber-box" style="top:14px;left:14px;right:14px;height:28px;">
|
||||
<span class="pv-fiber-lbl">Card</span>
|
||||
</div>
|
||||
<div class="pv-fiber-box" style="top:56px;left:14px;width:80px;height:16px;">
|
||||
<span class="pv-fiber-lbl">Title</span>
|
||||
</div>
|
||||
<div class="pv-fiber-box" style="top:56px;right:14px;width:40px;height:16px;"></div>
|
||||
</div>
|
||||
<!-- Card content -->
|
||||
<div style="padding:18px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
|
||||
<span style="font-size:0.6875rem;font-weight:600;color:#111;letter-spacing:-0.01em;">Revenue Overview</span>
|
||||
<span style="font-size:0.5625rem;background:#ECFDF5;color:#059669;padding:2px 6px;border-radius:99px;font-weight:600;">Live</span>
|
||||
</div>
|
||||
<div style="font-size:1.375rem;font-weight:800;color:#0A0A0A;letter-spacing:-0.045em;line-height:1;margin-bottom:4px;">$12,450</div>
|
||||
<div style="font-size:0.625rem;color:#059669;font-weight:600;margin-bottom:12px;">↑ +2.4%</div>
|
||||
<div style="height:2px;background:#F0F0F0;border-radius:99px;overflow:hidden;margin-bottom:12px;"><div style="height:100%;width:68%;background:#0066FF;border-radius:99px;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Second artboard -->
|
||||
<div class="pv-ab" style="top:48px;right:80px;width:160px;">
|
||||
<div class="pv-ab-lbl">UserProfile</div>
|
||||
<div style="padding:16px;text-align:center;">
|
||||
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,#7C3AED,#0066FF);margin:0 auto 8px;"></div>
|
||||
<div style="font-size:0.6875rem;font-weight:700;color:#0A0A0A;margin-bottom:2px;">Sarah Chen</div>
|
||||
<div style="font-size:0.5625rem;color:#A1A1AA;">Design Engineer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inspector peek -->
|
||||
<div style="position:absolute;right:16px;top:168px;width:156px;background:#111115;border:1px solid rgba(255,255,255,0.07);border-radius:8px;padding:12px;font-family:monospace;">
|
||||
<div style="font-size:0.4375rem;color:rgba(255,255,255,0.25);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:8px;">Props — Card</div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:5px;"><span style="font-size:0.5625rem;color:rgba(255,255,255,0.35);">title</span><span style="font-size:0.5625rem;color:#7DDBA0;">"Revenue…"</span></div>
|
||||
<div style="display:flex;justify-content:space-between;margin-bottom:5px;"><span style="font-size:0.5625rem;color:rgba(255,255,255,0.35);">value</span><span style="font-size:0.5625rem;color:#7EB8FF;">12450</span></div>
|
||||
<div style="display:flex;justify-content:space-between;"><span style="font-size:0.5625rem;color:rgba(255,255,255,0.35);">live</span><span style="font-size:0.5625rem;color:#FFBA7B;">true</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section id="how-it-works" class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Architecture</span>
|
||||
<h2 class="d2">From URL to live canvas in four steps.</h2>
|
||||
<p class="subtitle">A lightweight runtime bridge connects your running app to the Originmain canvas with no build step required.</p>
|
||||
</div>
|
||||
<div class="hiw-steps stagger">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Open any route</h3>
|
||||
<p>Paste any URL from your running app. Originmain opens it as an iframe-backed artboard in the canvas.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Fiber hook injects</h3>
|
||||
<p>A lightweight script injects into the iframe and hooks into React's fiber tree — reading every component's props, state, and DOM rect.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>Canvas receives tree</h3>
|
||||
<p>The fiber tree streams to the host via postMessage. The canvas renders selection overlays and prop inspectors in real time.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">04</div>
|
||||
<h3>Edits write back</h3>
|
||||
<p>Click any component. Change a prop in the inspector. The iframe re-renders instantly — no round-trips, no rebuild.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DEEP DIVE 1: Real components -->
|
||||
<div class="section" style="padding:100px 40px;">
|
||||
<div class="feat-split reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-blue">Real renders</span>
|
||||
<h2 class="d3">Not a mockup.<br>The actual component tree.</h2>
|
||||
<p class="subtitle">Every artboard runs your actual React components in a sandboxed iframe. Your real styles, your real tokens, your real data — all visible and editable from the canvas.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Open any route from your dev server, staging URL, or production app.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>The full fiber tree is mapped — every component, hook, and context value.</li>
|
||||
<li><span class="feat-pt-icon">⚡</span>Hot module reload propagates instantly. The canvas always reflects the current build.</li>
|
||||
</ul>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:24px;display:flex;flex-direction:column;gap:8px;overflow:hidden;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:4px;">Fiber tree — DashboardCard</div>
|
||||
<!-- Tree nodes -->
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="color:rgba(51,133,255,0.6);font-family:monospace;font-size:0.6rem;">▶</span><span style="font-family:var(--mono);font-size:0.625rem;color:#7EB8FF;">DashboardCard</span><span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);margin-left:auto;">×1</span></div>
|
||||
<div style="padding-left:16px;display:flex;flex-direction:column;gap:6px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="color:rgba(255,255,255,0.2);font-size:0.6rem;">▶</span><span style="font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.65);">Card</span><span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);margin-left:auto;">elevation="md"</span></div>
|
||||
<div style="padding-left:14px;display:flex;flex-direction:column;gap:5px;">
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.5);">CardHeader</span></div>
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.5);">MetricDisplay</span><span style="font-family:var(--mono);font-size:0.5rem;color:#FFBA7B;margin-left:auto;">value=12450</span></div>
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.5);">ProgressBar</span><span style="font-family:var(--mono);font-size:0.5rem;color:#7EB8FF;margin-left:auto;">value=68</span></div>
|
||||
<div style="display:flex;align-items:center;gap:6px;"><span style="font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.5);">TagList</span><span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.2);margin-left:auto;">×3 items</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:auto;padding:12px;background:rgba(51,133,255,0.08);border:1px solid rgba(51,133,255,0.15);border-radius:6px;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);margin-bottom:6px;">SELECTED — MetricDisplay</div>
|
||||
<div style="display:flex;justify-content:space-between;"><span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.4);">value</span><span style="font-family:var(--mono);font-size:0.5625rem;color:#FFBA7B;">12450</span></div>
|
||||
<div style="display:flex;justify-content:space-between;margin-top:4px;"><span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.4);">currency</span><span style="font-family:var(--mono);font-size:0.5625rem;color:#7DDBA0;">"USD"</span></div>
|
||||
<div style="display:flex;justify-content:space-between;margin-top:4px;"><span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.4);">format</span><span style="font-family:var(--mono);font-size:0.5625rem;color:#7DDBA0;">"compact"</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEEP DIVE 2: Interaction -->
|
||||
<div class="section section-muted" style="padding:100px 40px;">
|
||||
<div class="feat-split rev reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-blue">Interaction model</span>
|
||||
<h2 class="d3">Click. Edit. See it.<br>No context switch.</h2>
|
||||
<p class="subtitle">Click any component in the canvas — the inspector populates with its live props. Change a value and the iframe re-renders in under 16ms. All edits accumulate into an Intent Diff automatically.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></span>Selection handles show the exact DOM rect reported by the fiber hook.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></span>Every prop change is recorded as an entry in the artboard's undo stack.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></span>Cmd+Z / Cmd+Shift+Z for full undo/redo — applied live to the iframe.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-light reveal" style="overflow:visible;">
|
||||
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:32px;">
|
||||
<div style="width:100%;max-width:260px;position:relative;">
|
||||
<!-- Artboard frame -->
|
||||
<div style="background:#fff;border-radius:10px;padding:20px;box-shadow:0 8px 32px rgba(0,0,0,0.12);border:2px solid #3385FF;position:relative;">
|
||||
<!-- Selection handle -->
|
||||
<div style="position:absolute;top:-4px;left:-4px;width:8px;height:8px;background:#fff;border:2px solid #3385FF;border-radius:2px;"></div>
|
||||
<div style="position:absolute;top:-4px;right:-4px;width:8px;height:8px;background:#fff;border:2px solid #3385FF;border-radius:2px;"></div>
|
||||
<div style="position:absolute;bottom:-4px;left:-4px;width:8px;height:8px;background:#fff;border:2px solid #3385FF;border-radius:2px;"></div>
|
||||
<div style="position:absolute;bottom:-4px;right:-4px;width:8px;height:8px;background:#fff;border:2px solid #3385FF;border-radius:2px;"></div>
|
||||
<!-- Content -->
|
||||
<div style="font-size:0.6875rem;font-weight:700;color:#0A0A0A;margin-bottom:12px;">Revenue Overview</div>
|
||||
<div style="font-size:1.25rem;font-weight:800;color:#0A0A0A;letter-spacing:-0.04em;">$12,450</div>
|
||||
<div style="font-size:0.5625rem;color:#059669;margin-top:3px;margin-bottom:12px;">↑ +2.4%</div>
|
||||
<div style="height:2px;background:#F0F0F0;border-radius:99px;overflow:hidden;"><div style="height:100%;width:68%;background:#0066FF;border-radius:99px;"></div></div>
|
||||
</div>
|
||||
<!-- Inspector popup -->
|
||||
<div style="position:absolute;right:-130px;top:0;width:120px;background:#111115;border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:10px;box-shadow:0 8px 24px rgba(0,0,0,0.4);">
|
||||
<div style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.25);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:7px;">Props</div>
|
||||
<div style="margin-bottom:5px;"><div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.35);">borderRadius</div><div style="font-family:var(--mono);font-size:0.5625rem;color:#FFBA7B;margin-top:1px;">12</div></div>
|
||||
<div style="margin-bottom:5px;"><div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.35);">accentColor</div><div style="font-family:var(--mono);font-size:0.5625rem;color:#7DDBA0;margin-top:1px;">"#0066FF"</div></div>
|
||||
<div style="margin-bottom:5px;"><div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.35);">elevation</div><div style="font-family:var(--mono);font-size:0.5625rem;color:#7DDBA0;margin-top:1px;">"medium"</div></div>
|
||||
</div>
|
||||
<!-- Component label -->
|
||||
<div style="position:absolute;top:-22px;left:0;font-family:var(--mono);font-size:0.5rem;color:#3385FF;">ProgressBar</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TECHNICAL SECTION -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Technical foundation</span>
|
||||
<h2 class="d2">Built on open standards.</h2>
|
||||
<p class="subtitle">No proprietary runtime. No custom compiler. Live Artboard works with any React app — any version, any bundler, any framework.</p>
|
||||
</div>
|
||||
<div class="tech-grid stagger">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⬛</div>
|
||||
<h3>iframe sandboxing</h3>
|
||||
<p>Each artboard runs in a sandboxed iframe with <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">allow-scripts allow-same-origin</code>. Your app runs in full isolation — Originmain never modifies your code.</p>
|
||||
<span class="tech-tag">Security</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⚛</div>
|
||||
<h3>React Fiber hook</h3>
|
||||
<p>A lightweight script hooks into React's internal fiber tree via <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">__REACT_DEVTOOLS_GLOBAL_HOOK__</code> — the same API used by React DevTools. Works with React 16.8+.</p>
|
||||
<span class="tech-tag">Compatibility</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">📡</div>
|
||||
<h3>postMessage protocol</h3>
|
||||
<p>Fiber tree updates stream from the iframe to the host canvas via a typed postMessage protocol. Messages are validated with a shared artboard ID so multiple artboards never cross-contaminate.</p>
|
||||
<span class="tech-tag">Protocol</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🎯</div>
|
||||
<h3>DOM rect tracking</h3>
|
||||
<p>Every component's bounding rect is captured on mount and update. This drives the selection overlay, hover highlights, and resize handles — all pixel-accurate to the live render.</p>
|
||||
<span class="tech-tag">Precision</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">♻️</div>
|
||||
<h3>Zero build step</h3>
|
||||
<p>No Babel plugin, no webpack loader, no custom babel config. Paste any URL — dev server, staging, or production — and the artboard renders immediately.</p>
|
||||
<span class="tech-tag">Zero friction</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🔒</div>
|
||||
<h3>Cross-origin safe</h3>
|
||||
<p>When the iframe is cross-origin, the renderer can include the hook script directly via a small npm package. Same protocol, same canvas experience — no compromise on security.</p>
|
||||
<span class="tech-tag">Flexibility</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RELATED FEATURES -->
|
||||
<section class="section section-muted">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">What's next</span>
|
||||
<h2 class="d2">Live Artboard powers everything.</h2>
|
||||
<p class="subtitle">The real-time component tree is the foundation for every other Originmain feature.</p>
|
||||
</div>
|
||||
<div class="related-grid stagger">
|
||||
<a class="related-card" href="/features/origin-graph.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<h3>Origin Graph</h3>
|
||||
<p>The fiber tree feeds directly into the Origin Graph — building a queryable index of every component, token, and relationship in your app.</p>
|
||||
<span class="related-link">Explore Origin Graph →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/intent-diff.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<h3>Intent Diff</h3>
|
||||
<p>Every visual edit in a live artboard generates an Intent Diff — a machine-readable, component-level description of exactly what changed.</p>
|
||||
<span class="related-link">Explore Intent Diff →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/completion-zone.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<h3>Completion Zone</h3>
|
||||
<p>Mark any region of the artboard. AI fills it using your design language constraints, then renders the result live in the same artboard.</p>
|
||||
<span class="related-link">Explore Completion Zone →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta-bottom">
|
||||
<div class="cta-inner">
|
||||
<h2 class="cta-headline">See your app<br>live on the canvas.</h2>
|
||||
<p class="cta-sub">Early access is free. Connect your first artboard in under two minutes.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="/" class="btn btn-outline-white btn-lg">Back to overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="ft-inner">
|
||||
<div class="ft-top">
|
||||
<div>
|
||||
<div class="ft-brand-logo">Origin<span>main</span></div>
|
||||
<p class="ft-brand-tag">Where design intent becomes production code.</p>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="/features/live-artboard.html">Live Artboard</a></li>
|
||||
<li><a href="/features/origin-graph.html">Origin Graph</a></li>
|
||||
<li><a href="/features/intent-diff.html">Intent Diff</a></li>
|
||||
<li><a href="/features/completion-zone.html">Completion Zone</a></li>
|
||||
<li><a href="/features/agent-bridge.html">Agent Bridge</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Integrations</h4>
|
||||
<ul>
|
||||
<li><a href="/features/integrations.html">All integrations</a></li>
|
||||
<li><a href="#">GitHub</a></li>
|
||||
<li><a href="#">Linear</a></li>
|
||||
<li><a href="#">Slack</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Developers</h4>
|
||||
<ul>
|
||||
<li><a href="#">Docs</a></li>
|
||||
<li><a href="#">API Reference</a></li>
|
||||
<li><a href="#">MCP Protocol</a></li>
|
||||
<li><a href="#">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft-bottom">
|
||||
<span class="ft-copy">© 2025 Originmain. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Nav scroll
|
||||
const nav=document.getElementById('nav');
|
||||
window.addEventListener('scroll',()=>{nav.classList.toggle('scrolled',window.scrollY>20);});
|
||||
// Dropdown: delay-based hover (desktop) + click (mobile/touch)
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd=>{const menu=dd.querySelector('.nav-dropdown-menu');let t;const open=()=>{clearTimeout(t);dd.classList.add('open');menu.classList.add('open');};const close=()=>{t=setTimeout(()=>{dd.classList.remove('open');menu.classList.remove('open');},150);};dd.addEventListener('mouseenter',open);dd.addEventListener('mouseleave',close);menu.addEventListener('mouseenter',open);menu.addEventListener('mouseleave',close);dd.querySelector('a').addEventListener('click',e=>{if(window.matchMedia('(hover:none)').matches||window.innerWidth<=1024){e.preventDefault();const o=menu.classList.toggle('open');dd.classList.toggle('open',o);}});});
|
||||
// Mobile product sub-accordion
|
||||
const mpt=document.getElementById('mob-product-toggle'),mns=document.getElementById('nav-mobile-sub'),mdc=document.getElementById('mob-dd-caret');if(mpt&&mns){mpt.addEventListener('click',e=>{e.preventDefault();const o=mns.classList.toggle('open');if(mdc)mdc.style.transform=o?'rotate(180deg)':'';});}
|
||||
// Burger
|
||||
const burger=document.getElementById('nav-burger'),mob=document.getElementById('nav-mobile');
|
||||
burger.addEventListener('click',()=>{const o=mob.classList.toggle('open');burger.classList.toggle('open',o);burger.setAttribute('aria-expanded',o);});
|
||||
// Reveal
|
||||
const io=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target);}});},{threshold:0.12});
|
||||
document.querySelectorAll('.reveal,.stagger').forEach(el=>io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,774 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Origin Graph — Originmain</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--bg:#FFFFFF;--bg-subtle:#FAFAFA;--bg-muted:#F4F4F5;--bg-strong:#EBEBEB;--bg-inv:#0A0A0A;--bg-inv-2:#141414;--bg-inv-3:#1C1C1C;--fg:#0A0A0A;--fg-2:#52525B;--fg-3:#A1A1AA;--fg-4:#D4D4D8;--fg-inv:rgba(255,255,255,0.92);--fg-inv-2:rgba(255,255,255,0.52);--fg-inv-3:rgba(255,255,255,0.28);--blue:#0066FF;--blue-dark:#0050CC;--blue-light:#3385FF;--blue-soft:rgba(0,102,255,0.07);--blue-mid:rgba(0,102,255,0.15);--purple:#7C3AED;--teal:#0891B2;--teal-dark:#0E7490;--teal-soft:rgba(8,145,178,0.07);--teal-mid:rgba(8,145,178,0.18);--border:rgba(0,0,0,0.07);--border-2:rgba(0,0,0,0.12);--border-inv:rgba(255,255,255,0.08);--border-inv-2:rgba(255,255,255,0.14);--r-1:4px;--r-2:8px;--r-3:12px;--r-4:16px;--r-5:20px;--r-6:24px;--r-full:9999px;--sh-xs:0 1px 2px rgba(0,0,0,0.05);--sh-sm:0 1px 3px rgba(0,0,0,0.07),0 1px 2px rgba(0,0,0,0.05);--sh-md:0 4px 8px rgba(0,0,0,0.06),0 2px 4px rgba(0,0,0,0.04);--sh-lg:0 10px 24px rgba(0,0,0,0.08),0 4px 8px rgba(0,0,0,0.05);--sh-xl:0 20px 40px rgba(0,0,0,0.09),0 8px 16px rgba(0,0,0,0.05);--sh-2xl:0 32px 64px rgba(0,0,0,0.11),0 16px 32px rgba(0,0,0,0.06);--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;--mono:'JetBrains Mono','SF Mono',ui-monospace,'Cascadia Code',monospace;}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
||||
html{font-size:16px;scroll-behavior:smooth;-webkit-font-smoothing:antialiased;}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.6;overflow-x:hidden;}
|
||||
img,svg{display:block;max-width:100%;}
|
||||
a{color:inherit;text-decoration:none;}
|
||||
button{font-family:inherit;cursor:pointer;border:none;background:none;}
|
||||
.wrap{max-width:1120px;margin:0 auto;padding:0 40px;}
|
||||
.section{padding:112px 0;}
|
||||
.section-muted{background:var(--bg-subtle);}
|
||||
.section-dark{background:var(--bg-inv);}
|
||||
.d1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:900;line-height:1.02;letter-spacing:-0.045em;color:var(--fg);}
|
||||
.d2{font-size:clamp(1.875rem,3.5vw,2.75rem);font-weight:800;line-height:1.1;letter-spacing:-0.038em;color:var(--fg);}
|
||||
.d3{font-size:clamp(1.25rem,2.5vw,1.625rem);font-weight:700;line-height:1.25;letter-spacing:-0.028em;color:var(--fg);}
|
||||
.subtitle{font-size:clamp(1rem,1.8vw,1.125rem);font-weight:400;line-height:1.75;color:var(--fg-2);}
|
||||
.label{font-family:var(--mono);font-size:0.6875rem;font-weight:500;letter-spacing:0.1em;text-transform:uppercase;color:var(--fg-3);}
|
||||
.label-teal{color:var(--teal);}
|
||||
.btn{display:inline-flex;align-items:center;gap:8px;font-family:var(--font);font-size:0.9375rem;font-weight:600;padding:11px 22px;border-radius:var(--r-2);transition:transform 0.12s cubic-bezier(.16,1,.3,1),opacity 0.12s,box-shadow 0.12s;white-space:nowrap;letter-spacing:-0.01em;position:relative;overflow:hidden;}
|
||||
.btn::after{content:'';position:absolute;inset:0;background:rgba(255,255,255,0);transition:background 0.12s;}
|
||||
.btn:hover::after{background:rgba(255,255,255,0.06);}
|
||||
.btn:active{transform:scale(0.98)!important;}
|
||||
.btn-primary{background:var(--fg);color:var(--bg);box-shadow:var(--sh-sm);}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:var(--sh-md);}
|
||||
.btn-secondary{background:transparent;color:var(--fg);border:1px solid var(--border-2);}
|
||||
.btn-secondary:hover{background:var(--bg-muted);transform:translateY(-1px);}
|
||||
.btn-teal{background:var(--teal);color:#fff;box-shadow:0 4px 12px rgba(8,145,178,0.3);}
|
||||
.btn-teal:hover{background:var(--teal-dark);transform:translateY(-1px);}
|
||||
.btn-lg{font-size:1rem;padding:13px 28px;}
|
||||
.btn-white{background:#fff;color:#0A0A0A;box-shadow:0 4px 12px rgba(255,255,255,0.15);}
|
||||
.btn-white:hover{background:#F5F5F5;transform:translateY(-1px);}
|
||||
.btn-outline-white{background:transparent;color:rgba(255,255,255,0.72);border:1px solid rgba(255,255,255,0.18);}
|
||||
.btn-outline-white:hover{background:rgba(255,255,255,0.06);color:rgba(255,255,255,0.9);transform:translateY(-1px);}
|
||||
.divider{height:1px;background:var(--border);}
|
||||
.section-head{text-align:center;max-width:640px;margin:0 auto 72px;}
|
||||
.section-head .label{display:block;margin-bottom:16px;}
|
||||
.section-head .d2{margin-bottom:18px;}
|
||||
|
||||
/* NAV */
|
||||
#nav{position:fixed;top:0;left:0;right:0;z-index:200;height:60px;display:flex;align-items:center;padding:0 40px;transition:background 0.2s,border-color 0.2s,box-shadow 0.2s;}
|
||||
#nav.scrolled{background:rgba(255,255,255,0.85);backdrop-filter:blur(20px) saturate(1.6);-webkit-backdrop-filter:blur(20px) saturate(1.6);border-bottom:1px solid var(--border);box-shadow:var(--sh-xs);}
|
||||
.nav-inner{max-width:1120px;margin:0 auto;width:100%;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;}
|
||||
.nav-logo{justify-self:start;font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:var(--fg);display:flex;align-items:center;gap:2px;}
|
||||
.nav-logo span{color:var(--teal);}
|
||||
.nav-links{justify-self:center;display:flex;align-items:center;gap:0;list-style:none;}
|
||||
.nav-links a{font-size:0.9375rem;font-weight:500;color:var(--fg-2);padding:6px 14px;border-radius:var(--r-1);transition:color 0.12s,background 0.12s;letter-spacing:-0.01em;}
|
||||
.nav-links a:hover{color:var(--fg);}
|
||||
.nav-actions{justify-self:end;display:flex;align-items:center;gap:8px;}
|
||||
|
||||
/* Dropdown */
|
||||
.nav-dropdown{position:relative;}
|
||||
.nav-dropdown>a{display:flex;align-items:center;gap:4px;}
|
||||
.nav-dd-caret{transition:transform 0.15s;}
|
||||
/* caret rotation handled via JS .open class */
|
||||
.nav-dropdown-menu{position:absolute;top:calc(100% + 10px);left:50%;transform:translateX(-50%) translateY(-4px);background:rgba(255,255,255,0.98);backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--r-3);box-shadow:var(--sh-xl);padding:8px;min-width:240px;opacity:0;visibility:hidden;transition:opacity 0.15s,transform 0.15s,visibility 0.15s;pointer-events:none;}
|
||||
.nav-dropdown-menu.open{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:all;}
|
||||
.nav-dropdown.open .nav-dd-caret{transform:rotate(180deg);}
|
||||
/* Mobile sub-nav */
|
||||
.nav-mobile-sub{display:none;padding:2px 0 6px 14px;flex-direction:column;}
|
||||
.nav-mobile-sub.open{display:flex;}
|
||||
.nav-mobile-sub a{font-size:0.875rem;color:rgba(255,255,255,0.4);padding:8px 4px;border-bottom:1px solid rgba(255,255,255,0.06);letter-spacing:-0.01em;transition:color 0.1s;}
|
||||
.nav-mobile-sub a:last-child{border-bottom:none;}
|
||||
.nav-mobile-sub a:hover{color:rgba(255,255,255,0.8);}
|
||||
/* Dark hero: white nav before scrolling */
|
||||
#nav:not(.scrolled) .nav-logo{color:rgba(255,255,255,0.92);}
|
||||
#nav:not(.scrolled) .nav-logo span{color:#3385FF;}
|
||||
#nav:not(.scrolled) .nav-links a{color:rgba(255,255,255,0.5);}
|
||||
#nav:not(.scrolled) .nav-links a:hover{color:rgba(255,255,255,0.9);}
|
||||
#nav:not(.scrolled) .nav-actions .btn-secondary{color:rgba(255,255,255,0.65);border-color:rgba(255,255,255,0.2);background:transparent;}
|
||||
#nav:not(.scrolled) .nav-burger span{background:rgba(255,255,255,0.8);}
|
||||
.nav-dd-item{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:var(--r-2);transition:background 0.1s;cursor:pointer;}
|
||||
.nav-dd-item:hover{background:var(--bg-muted);}
|
||||
.nav-dd-item.active{background:var(--teal-soft);}
|
||||
.nav-dd-icon{width:30px;height:30px;background:var(--teal-soft);border:1px solid var(--teal-mid);border-radius:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:13px;color:var(--blue);}
|
||||
.nav-dd-icon.blue-icon{background:var(--blue-soft);border-color:var(--blue-mid);}
|
||||
.nav-dd-text{display:flex;flex-direction:column;}
|
||||
.nav-dd-name{font-size:0.8125rem;font-weight:600;color:var(--fg);letter-spacing:-0.01em;}
|
||||
.nav-dd-desc{font-size:0.6875rem;color:var(--fg-3);margin-top:1px;line-height:1.4;}
|
||||
.nav-dd-sep{height:1px;background:var(--border);margin:4px 0;}
|
||||
.nav-burger{display:none;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:36px;height:36px;background:none;border:none;cursor:pointer;padding:4px;border-radius:var(--r-2);}
|
||||
.nav-burger span{display:block;width:20px;height:2px;background:var(--fg);border-radius:2px;transition:transform 0.22s cubic-bezier(0.16,1,0.3,1),opacity 0.18s;transform-origin:center;}
|
||||
.nav-burger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
|
||||
.nav-burger.open span:nth-child(2){opacity:0;transform:scaleX(0);}
|
||||
.nav-burger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}
|
||||
.nav-mobile{display:none;position:absolute;top:60px;left:0;right:0;background:rgba(255,255,255,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);box-shadow:0 8px 32px rgba(0,0,0,0.07);padding:12px 24px 24px;flex-direction:column;gap:2px;}
|
||||
.nav-mobile.open{display:flex;}
|
||||
.nav-mobile ul{list-style:none;display:flex;flex-direction:column;}
|
||||
.nav-mobile ul li a{display:block;font-size:1rem;font-weight:500;color:var(--fg-2);padding:11px 4px;border-bottom:1px solid var(--border);letter-spacing:-0.01em;}
|
||||
.nav-mobile ul li:last-child a{border-bottom:none;}
|
||||
.nav-mobile-ctas{display:flex;gap:8px;margin-top:16px;}
|
||||
.nav-mobile-ctas .btn{flex:1;justify-content:center;padding:10px 12px;font-size:.875rem;}
|
||||
|
||||
/* PAGE HERO */
|
||||
#page-hero{padding:140px 40px 100px;background:var(--bg-inv);position:relative;overflow:hidden;text-align:center;}
|
||||
#page-hero::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(8,145,178,0.14),transparent 60%),radial-gradient(ellipse 40% 40% at 80% 70%,rgba(8,145,178,0.07),transparent 50%),radial-gradient(ellipse 50% 50% at 20% 80%,rgba(124,58,237,0.06),transparent 50%);pointer-events:none;}
|
||||
.page-hero-inner{position:relative;max-width:800px;margin:0 auto;}
|
||||
.page-hero-inner .label{color:rgba(255,255,255,0.3);margin-bottom:20px;display:block;}
|
||||
.page-hero-inner .d1{color:var(--fg-inv);margin-bottom:22px;}
|
||||
.page-hero-inner .subtitle{color:var(--fg-inv-2);max-width:600px;margin:0 auto 44px;}
|
||||
.page-hero-ctas{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* PRODUCT VISUAL */
|
||||
.pv-shell{width:100%;max-width:960px;margin:72px auto 0;border-radius:16px 16px 0 0;overflow:hidden;box-shadow:0 48px 96px rgba(0,0,0,0.45),0 0 0 1px rgba(255,255,255,0.06);border-bottom:none;position:relative;}
|
||||
.pv-chrome{background:#1C1C22;height:40px;display:flex;align-items:center;padding:0 14px;gap:7px;border-bottom:1px solid rgba(255,255,255,0.05);}
|
||||
.pv-dot{width:11px;height:11px;border-radius:50%;}
|
||||
.pv-dot.r{background:#FF5F57;}
|
||||
.pv-dot.y{background:#FFBD2E;}
|
||||
.pv-dot.g{background:#28C840;}
|
||||
.pv-url{flex:1;margin:0 12px;background:rgba(255,255,255,0.06);border-radius:5px;height:25px;display:flex;align-items:center;padding:0 10px;font-family:var(--mono);font-size:0.625rem;color:rgba(255,255,255,0.3);max-width:320px;}
|
||||
.pv-canvas{background:#0D0D11;height:400px;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;}
|
||||
.pv-grid{position:absolute;inset:0;background-image:radial-gradient(circle,rgba(255,255,255,0.042) 1px,transparent 1px);background-size:24px 24px;pointer-events:none;}
|
||||
|
||||
/* Graph SVG */
|
||||
.graph-svg{position:absolute;inset:0;width:100%;height:100%;}
|
||||
|
||||
/* Node info panel */
|
||||
.graph-panel{position:absolute;right:20px;top:50%;transform:translateY(-50%);width:148px;background:#111118;border:1px solid rgba(8,145,178,0.22);border-radius:10px;padding:14px;box-shadow:0 8px 32px rgba(0,0,0,0.5);}
|
||||
.graph-panel-title{font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.25);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:10px;}
|
||||
.graph-panel-name{font-family:var(--mono);font-size:0.6875rem;font-weight:600;color:#0891B2;margin-bottom:10px;}
|
||||
.graph-stat{display:flex;justify-content:space-between;margin-bottom:6px;}
|
||||
.graph-stat-label{font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.32);}
|
||||
.graph-stat-val{font-family:var(--mono);font-size:0.5625rem;font-weight:600;color:rgba(255,255,255,0.7);}
|
||||
.graph-stat-val.teal{color:#0891B2;}
|
||||
.graph-sep{height:1px;background:rgba(255,255,255,0.06);margin:10px 0;}
|
||||
.graph-edge-legend{display:flex;flex-direction:column;gap:5px;margin-top:4px;}
|
||||
.graph-edge-item{display:flex;align-items:center;gap:6px;}
|
||||
.graph-edge-line{width:16px;height:1px;flex-shrink:0;}
|
||||
.graph-edge-line.usage{background:rgba(255,255,255,0.3);}
|
||||
.graph-edge-line.inherit{background-image:repeating-linear-gradient(90deg,rgba(124,58,237,0.7) 0,rgba(124,58,237,0.7) 4px,transparent 4px,transparent 7px);}
|
||||
.graph-edge-line.token{background-image:repeating-linear-gradient(90deg,rgba(8,145,178,0.7) 0,rgba(8,145,178,0.7) 3px,transparent 3px,transparent 5px);}
|
||||
.graph-edge-text{font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.3);}
|
||||
|
||||
/* HOW IT WORKS */
|
||||
.hiw-steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:64px;position:relative;}
|
||||
.hiw-steps::before{content:'';position:absolute;top:28px;left:12.5%;right:12.5%;height:1px;background:linear-gradient(90deg,transparent,var(--border-2) 20%,var(--border-2) 80%,transparent);}
|
||||
.hiw-step{display:flex;flex-direction:column;align-items:center;text-align:center;padding:0 20px;position:relative;z-index:1;}
|
||||
.hiw-num{width:56px;height:56px;border-radius:50%;background:var(--bg);border:1px solid var(--border-2);display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--teal);margin-bottom:20px;box-shadow:var(--sh-sm);}
|
||||
.hiw-step h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.hiw-step p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;}
|
||||
|
||||
/* FEATURE SPLIT */
|
||||
.feat-split{display:grid;grid-template-columns:1fr 1fr;gap:96px;align-items:center;max-width:1120px;margin:0 auto;}
|
||||
.feat-split.rev{direction:rtl;}
|
||||
.feat-split.rev>*{direction:ltr;}
|
||||
.feat-copy .label{display:block;margin-bottom:16px;}
|
||||
.feat-copy .d3{margin-bottom:16px;}
|
||||
.feat-copy .subtitle{margin-bottom:28px;}
|
||||
.feat-pts{list-style:none;display:flex;flex-direction:column;gap:12px;margin-bottom:32px;}
|
||||
.feat-pts li{display:flex;align-items:flex-start;gap:10px;font-size:0.9375rem;color:var(--fg-2);line-height:1.55;}
|
||||
.feat-pt-icon{width:20px;height:20px;border-radius:3px;background:var(--teal-soft);border:1px solid var(--teal-mid);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px;font-size:0.625rem;color:var(--teal);}
|
||||
|
||||
/* Feature visual */
|
||||
.feat-vis{border-radius:var(--r-5);overflow:hidden;box-shadow:var(--sh-2xl);border:1px solid var(--border);aspect-ratio:4/3;position:relative;}
|
||||
.feat-vis-dark{background:#0D0D11;border-color:rgba(255,255,255,0.07);}
|
||||
.feat-vis-light{background:var(--bg-muted);}
|
||||
|
||||
/* TECH SECTION */
|
||||
.tech-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:56px;}
|
||||
.tech-card{background:var(--bg);border:1px solid var(--border);border-radius:var(--r-4);padding:32px 28px;transition:box-shadow 0.2s;}
|
||||
.tech-card:hover{box-shadow:var(--sh-lg);}
|
||||
.tech-icon{width:44px;height:44px;background:var(--teal-soft);border:1px solid var(--teal-mid);border-radius:var(--r-2);display:flex;align-items:center;justify-content:center;font-size:1.25rem;margin-bottom:18px;}
|
||||
.tech-card h3{font-size:1rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;margin-bottom:8px;}
|
||||
.tech-card p{font-size:0.875rem;color:var(--fg-2);line-height:1.65;}
|
||||
.tech-tag{display:inline-block;margin-top:14px;font-family:var(--mono);font-size:0.625rem;letter-spacing:0.08em;text-transform:uppercase;color:var(--teal);background:var(--teal-soft);border:1px solid var(--teal-mid);padding:3px 9px;border-radius:var(--r-full);}
|
||||
|
||||
/* CODE BLOCK */
|
||||
.code-block{background:#111118;border:1px solid rgba(255,255,255,0.07);border-radius:var(--r-3);padding:24px;font-family:var(--mono);font-size:0.75rem;line-height:1.75;color:rgba(255,255,255,0.75);overflow-x:auto;}
|
||||
.code-block .kw{color:#7EB8FF;}
|
||||
.code-block .str{color:#7DDBA0;}
|
||||
.code-block .cm{color:rgba(255,255,255,0.28);}
|
||||
.code-block .fn{color:#FFBA7B;}
|
||||
.code-block .nm{color:#D4A0FF;}
|
||||
.code-block .tl{color:#5FCDD9;}
|
||||
|
||||
/* RELATED */
|
||||
.related-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:48px;}
|
||||
.related-card{border:1px solid var(--border);border-radius:var(--r-4);padding:24px;transition:box-shadow 0.15s,border-color 0.15s;display:flex;flex-direction:column;gap:10px;}
|
||||
.related-card:hover{box-shadow:var(--sh-md);border-color:var(--border-2);}
|
||||
.related-icon{font-size:1.25rem;}
|
||||
.related-card h3{font-size:0.9375rem;font-weight:700;color:var(--fg);letter-spacing:-0.02em;}
|
||||
.related-card p{font-size:0.8125rem;color:var(--fg-2);line-height:1.6;flex:1;}
|
||||
.related-link{font-size:0.8125rem;font-weight:600;color:var(--teal);}
|
||||
|
||||
/* CTA */
|
||||
#cta-bottom{background:var(--bg-inv);padding:120px 40px;position:relative;overflow:hidden;text-align:center;}
|
||||
#cta-bottom::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 30% 50%,rgba(8,145,178,0.1),transparent 60%),radial-gradient(ellipse 60% 80% at 70% 50%,rgba(8,145,178,0.07),transparent 60%);pointer-events:none;}
|
||||
.cta-inner{position:relative;max-width:680px;margin:0 auto;}
|
||||
.cta-headline{font-size:clamp(2rem,4.5vw,3.5rem);font-weight:900;letter-spacing:-0.045em;color:var(--fg-inv);line-height:1.05;margin-bottom:18px;}
|
||||
.cta-sub{font-size:1.0625rem;color:var(--fg-inv-2);margin-bottom:44px;line-height:1.65;}
|
||||
.cta-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
|
||||
|
||||
/* FOOTER */
|
||||
footer{background:#08080C;border-top:1px solid rgba(255,255,255,0.06);padding:72px 40px 40px;}
|
||||
.ft-inner{max-width:1120px;margin:0 auto;}
|
||||
.ft-top{display:grid;grid-template-columns:260px repeat(4,1fr);gap:48px;padding-bottom:56px;border-bottom:1px solid rgba(255,255,255,0.07);margin-bottom:36px;}
|
||||
.ft-brand-logo{font-size:1rem;font-weight:800;letter-spacing:-0.04em;color:rgba(255,255,255,0.92);margin-bottom:8px;}
|
||||
.ft-brand-logo span{color:#0891B2;}
|
||||
.ft-brand-tag{font-size:0.875rem;color:rgba(255,255,255,0.3);line-height:1.65;max-width:200px;}
|
||||
.ft-col h4{font-size:0.875rem;font-weight:600;color:rgba(255,255,255,0.7);margin-bottom:16px;letter-spacing:-0.01em;}
|
||||
.ft-col ul{list-style:none;display:flex;flex-direction:column;gap:10px;}
|
||||
.ft-col a{font-size:0.875rem;color:rgba(255,255,255,0.32);transition:color 0.14s;letter-spacing:-0.01em;}
|
||||
.ft-col a:hover{color:rgba(255,255,255,0.65);}
|
||||
.ft-bottom{display:flex;justify-content:space-between;align-items:center;}
|
||||
.ft-copy{font-size:0.8125rem;color:rgba(255,255,255,0.22);}
|
||||
|
||||
/* REVEAL */
|
||||
.reveal{opacity:0;transform:translateY(22px);transition:opacity 0.7s cubic-bezier(0.16,1,0.3,1),transform 0.7s cubic-bezier(0.16,1,0.3,1);}
|
||||
.reveal.in{opacity:1;transform:none;}
|
||||
.stagger>*{opacity:0;transform:translateY(16px);transition:opacity 0.55s cubic-bezier(0.16,1,0.3,1),transform 0.55s cubic-bezier(0.16,1,0.3,1);}
|
||||
.stagger.in>*:nth-child(1){opacity:1;transform:none;transition-delay:0s;}
|
||||
.stagger.in>*:nth-child(2){opacity:1;transform:none;transition-delay:0.07s;}
|
||||
.stagger.in>*:nth-child(3){opacity:1;transform:none;transition-delay:0.14s;}
|
||||
.stagger.in>*:nth-child(4){opacity:1;transform:none;transition-delay:0.21s;}
|
||||
.stagger.in>*:nth-child(5){opacity:1;transform:none;transition-delay:0.28s;}
|
||||
.stagger.in>*:nth-child(6){opacity:1;transform:none;transition-delay:0.35s;}
|
||||
|
||||
@media(max-width:1024px){.feat-split{grid-template-columns:1fr;gap:56px;}.feat-split.rev{direction:ltr;}.hiw-steps{grid-template-columns:1fr 1fr;gap:40px;}.hiw-steps::before{display:none;}.tech-grid{grid-template-columns:1fr 1fr;}.related-grid{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:768px){.wrap{padding:0 24px;}#nav{padding:0 24px;}.nav-inner{display:flex;justify-content:space-between;}.nav-links,.nav-actions .btn{display:none;}.nav-burger{display:flex;}#page-hero{padding:120px 24px 80px;}.pv-shell{margin-top:48px;}.hiw-steps{grid-template-columns:1fr;}.tech-grid,.related-grid{grid-template-columns:1fr;}.ft-top{grid-template-columns:1fr 1fr;}}
|
||||
@media(max-width:480px){.ft-top{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- NAV -->
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo">Origin<span>main</span></a>
|
||||
<ul class="nav-links">
|
||||
<li class="nav-dropdown">
|
||||
<a href="/#features" style="display:flex;align-items:center;gap:4px;">
|
||||
Product
|
||||
<svg class="nav-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-dropdown-menu">
|
||||
<a class="nav-dd-item" href="/features/live-artboard.html">
|
||||
<div class="nav-dd-icon blue-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Live Artboard</span><span class="nav-dd-desc">Your app rendered live in the canvas</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item active" href="/features/origin-graph.html">
|
||||
<div class="nav-dd-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">◉ Origin Graph</span><span class="nav-dd-desc">Every component, indexed and queryable</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/intent-diff.html">
|
||||
<div class="nav-dd-icon blue-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Intent Diff</span><span class="nav-dd-desc">Ship component-level diffs, not redlines</span></div>
|
||||
</a>
|
||||
<div class="nav-dd-sep"></div>
|
||||
<a class="nav-dd-item" href="/features/completion-zone.html">
|
||||
<div class="nav-dd-icon blue-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Completion Zone</span><span class="nav-dd-desc">AI-filled design, constrained to your system</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/agent-bridge.html">
|
||||
<div class="nav-dd-icon blue-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><path d="M1.5 5h11M9.5 2.5l3 2.5-3 2.5"/><path d="M12.5 9h-11M4.5 6.5L1.5 9l3 2.5"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Agent Bridge</span><span class="nav-dd-desc">MCP channel to Cursor, Claude Code & more</span></div>
|
||||
</a>
|
||||
<a class="nav-dd-item" href="/features/integrations.html">
|
||||
<div class="nav-dd-icon blue-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<div class="nav-dd-text"><span class="nav-dd-name">Integrations</span><span class="nav-dd-desc">Linear, Slack, GitHub, Figma & more</span></div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-actions">
|
||||
<a href="#" class="btn btn-secondary" style="padding:7px 16px;font-size:.875rem">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary" style="padding:8px 18px;font-size:.875rem">Get Started</a>
|
||||
<button class="nav-burger" id="nav-burger" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-mobile" id="nav-mobile">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/#features" id="mob-product-toggle" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
Product
|
||||
<svg id="mob-dd-caret" width="10" height="6" viewBox="0 0 10 6" fill="none" style="transition:transform 0.15s;"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</a>
|
||||
<div class="nav-mobile-sub" id="nav-mobile-sub">
|
||||
<a href="/features/live-artboard.html">Live Artboard</a>
|
||||
<a href="/features/origin-graph.html">Origin Graph</a>
|
||||
<a href="/features/intent-diff.html">Intent Diff</a>
|
||||
<a href="/features/completion-zone.html">Completion Zone</a>
|
||||
<a href="/features/agent-bridge.html">Agent Bridge</a>
|
||||
<a href="/features/integrations.html">Integrations</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/features/integrations.html">Integrations</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Docs</a></li>
|
||||
</ul>
|
||||
<div class="nav-mobile-ctas">
|
||||
<a href="#" class="btn btn-secondary">Log in</a>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<section id="page-hero">
|
||||
<div class="page-hero-inner">
|
||||
<span class="label reveal">◉ Origin Graph</span>
|
||||
<h1 class="d1 reveal">Every component,<br>indexed and queryable.</h1>
|
||||
<p class="subtitle reveal">The Origin Graph is a living knowledge graph of your entire component ecosystem — relationships, history, usage, and design tokens — all queryable in real time.</p>
|
||||
<div class="page-hero-ctas reveal">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="#how-it-works" class="btn btn-outline-white btn-lg">See how it works ↓</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Graph product visual -->
|
||||
<div class="pv-shell reveal">
|
||||
<div class="pv-chrome">
|
||||
<div class="pv-dot r"></div>
|
||||
<div class="pv-dot y"></div>
|
||||
<div class="pv-dot g"></div>
|
||||
<div class="pv-url">🔒 app.originmain.io/graph</div>
|
||||
</div>
|
||||
<div class="pv-canvas">
|
||||
<div class="pv-grid"></div>
|
||||
|
||||
<!-- SVG graph visualization -->
|
||||
<svg class="graph-svg" viewBox="0 0 960 400" preserveAspectRatio="xMidYMid meet">
|
||||
<defs>
|
||||
<marker id="arr-usage" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
|
||||
<path d="M0,1 L5,3 L0,5" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="arr-token" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
|
||||
<path d="M0,1 L5,3 L0,5" fill="none" stroke="rgba(8,145,178,0.5)" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="arr-inherit" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
|
||||
<path d="M0,1 L5,3 L0,5" fill="none" stroke="rgba(124,58,237,0.5)" stroke-width="1"/>
|
||||
</marker>
|
||||
<filter id="glow-teal" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur"/>
|
||||
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Usage edges (solid faint white) -->
|
||||
<!-- NavBar → Button -->
|
||||
<line stroke="rgba(255,255,255,0.12)" stroke-width="1" x1="285" y1="128" x2="404" y2="192" marker-end="url(#arr-usage)"/>
|
||||
<!-- DashboardCard → Card -->
|
||||
<line stroke="rgba(255,255,255,0.12)" stroke-width="1" x1="490" y1="180" x2="578" y2="130" marker-end="url(#arr-usage)"/>
|
||||
<!-- DashboardCard → Button -->
|
||||
<line stroke="rgba(255,255,255,0.12)" stroke-width="1" x1="462" y1="192" x2="436" y2="192" marker-end="url(#arr-usage)"/>
|
||||
<!-- Card → Input -->
|
||||
<line stroke="rgba(255,255,255,0.12)" stroke-width="1" x1="606" y1="148" x2="634" y2="256" marker-end="url(#arr-usage)"/>
|
||||
<!-- NavBar → Input -->
|
||||
<line stroke="rgba(255,255,255,0.08)" stroke-width="1" x1="308" y1="135" x2="618" y2="265" marker-end="url(#arr-usage)"/>
|
||||
|
||||
<!-- Inheritance edge (dashed purple) -->
|
||||
<!-- DashboardCard extends Card -->
|
||||
<line stroke="rgba(124,58,237,0.35)" stroke-width="1.5" stroke-dasharray="5 3" x1="510" y1="178" x2="582" y2="142" marker-end="url(#arr-inherit)"/>
|
||||
|
||||
<!-- Token edges (dashed teal) -->
|
||||
<!-- --color-primary → Button -->
|
||||
<line stroke="rgba(8,145,178,0.3)" stroke-width="1" stroke-dasharray="3 3" x1="400" y1="302" x2="418" y2="212" marker-end="url(#arr-token)"/>
|
||||
<!-- --color-primary → Card -->
|
||||
<line stroke="rgba(8,145,178,0.3)" stroke-width="1" stroke-dasharray="3 3" x1="412" y1="294" x2="578" y2="148" marker-end="url(#arr-token)"/>
|
||||
<!-- --color-primary → DashboardCard -->
|
||||
<line stroke="rgba(8,145,178,0.3)" stroke-width="1" stroke-dasharray="3 3" x1="404" y1="290" x2="472" y2="218" marker-end="url(#arr-token)"/>
|
||||
<!-- --color-primary → Input -->
|
||||
<line stroke="rgba(8,145,178,0.25)" stroke-width="1" stroke-dasharray="3 3" x1="418" y1="298" x2="636" y2="278" marker-end="url(#arr-token)"/>
|
||||
<!-- --color-primary → NavBar -->
|
||||
<line stroke="rgba(8,145,178,0.25)" stroke-width="1" stroke-dasharray="3 3" x1="382" y1="290" x2="282" y2="148" marker-end="url(#arr-token)"/>
|
||||
|
||||
<!-- Node: NavBar -->
|
||||
<g transform="translate(262,120)">
|
||||
<circle r="32" fill="#191920" stroke="rgba(255,255,255,0.12)" stroke-width="1.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="9" fill="rgba(255,255,255,0.65)">NavBar</text>
|
||||
</g>
|
||||
|
||||
<!-- Node: Button -->
|
||||
<g transform="translate(420,200)">
|
||||
<circle r="30" fill="#191920" stroke="rgba(255,255,255,0.12)" stroke-width="1.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="9" fill="rgba(255,255,255,0.65)">Button</text>
|
||||
</g>
|
||||
|
||||
<!-- Node: DashboardCard — selected, blue ring, glow -->
|
||||
<g transform="translate(490,200)" filter="url(#glow-teal)">
|
||||
<circle r="44" fill="none" stroke="rgba(8,145,178,0.12)" stroke-width="1"/>
|
||||
<circle r="36" fill="#0D1E26" stroke="#0891B2" stroke-width="2"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="8" fill="#0891B2" font-weight="600">Dashboard</text>
|
||||
<text y="12" text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="8" fill="#0891B2" font-weight="600">Card</text>
|
||||
</g>
|
||||
|
||||
<!-- Selected label tooltip -->
|
||||
<g transform="translate(490,148)">
|
||||
<rect x="-52" y="-11" width="104" height="18" rx="4" fill="rgba(8,145,178,0.12)" stroke="rgba(8,145,178,0.28)" stroke-width="1"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="7" fill="rgba(8,145,178,0.85)">DashboardCard — selected</text>
|
||||
</g>
|
||||
|
||||
<!-- Node: Card -->
|
||||
<g transform="translate(600,120)">
|
||||
<circle r="30" fill="#191920" stroke="rgba(255,255,255,0.12)" stroke-width="1.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="9" fill="rgba(255,255,255,0.65)">Card</text>
|
||||
</g>
|
||||
|
||||
<!-- Node: Input -->
|
||||
<g transform="translate(648,280)">
|
||||
<circle r="26" fill="#191920" stroke="rgba(255,255,255,0.12)" stroke-width="1.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="9" fill="rgba(255,255,255,0.65)">Input</text>
|
||||
</g>
|
||||
|
||||
<!-- Node: --color-primary (token, teal circle) -->
|
||||
<g transform="translate(392,316)">
|
||||
<circle r="24" fill="#0A1A20" stroke="rgba(8,145,178,0.5)" stroke-width="1.5"/>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="rgba(8,145,178,0.85)" y="-5">--color</text>
|
||||
<text text-anchor="middle" dominant-baseline="central" font-family="'JetBrains Mono',monospace" font-size="6.5" fill="rgba(8,145,178,0.85)" y="6">-primary</text>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
|
||||
<!-- Node info panel -->
|
||||
<div class="graph-panel">
|
||||
<div class="graph-panel-title">Node Inspector</div>
|
||||
<div class="graph-panel-name">DashboardCard</div>
|
||||
<div class="graph-stat">
|
||||
<span class="graph-stat-label">uses</span>
|
||||
<span class="graph-stat-val teal">4</span>
|
||||
</div>
|
||||
<div class="graph-stat">
|
||||
<span class="graph-stat-label">used by</span>
|
||||
<span class="graph-stat-val">2</span>
|
||||
</div>
|
||||
<div class="graph-stat">
|
||||
<span class="graph-stat-label">tokens consumed</span>
|
||||
<span class="graph-stat-val">12</span>
|
||||
</div>
|
||||
<div class="graph-sep"></div>
|
||||
<div class="graph-panel-title" style="margin-bottom:6px;">Edge types</div>
|
||||
<div class="graph-edge-legend">
|
||||
<div class="graph-edge-item">
|
||||
<div class="graph-edge-line usage"></div>
|
||||
<span class="graph-edge-text">Usage</span>
|
||||
</div>
|
||||
<div class="graph-edge-item">
|
||||
<div class="graph-edge-line inherit"></div>
|
||||
<span class="graph-edge-text">Inheritance</span>
|
||||
</div>
|
||||
<div class="graph-edge-item">
|
||||
<div class="graph-edge-line token"></div>
|
||||
<span class="graph-edge-text">Token link</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section id="how-it-works" class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Architecture</span>
|
||||
<h2 class="d2">From source file to queryable graph<br>in four steps.</h2>
|
||||
<p class="subtitle">A two-pass pipeline — static analysis plus runtime capture — builds a continuously updated graph with zero manual annotation required.</p>
|
||||
</div>
|
||||
<div class="hiw-steps stagger">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Static analysis</h3>
|
||||
<p>An AST walker scans every component file, extracting imports, prop signatures, and design-token references without running your code.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Runtime capture</h3>
|
||||
<p>A React Fiber hook captures live props and state at render time, adding real-world usage data that static analysis alone can't see.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>Graph construction</h3>
|
||||
<p>Nodes and typed edges — usage, inheritance, token links — are written to Supabase, forming a queryable graph that updates incrementally.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">04</div>
|
||||
<h3>Query interface</h3>
|
||||
<p>A REST API and MCP tool set expose the graph to AI agents, the diff engine, and the Inspector — answering natural-language queries in real time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DEEP DIVE 1: Impact analysis -->
|
||||
<div class="section" style="padding:100px 40px;">
|
||||
<div class="feat-split reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-teal">Impact analysis, instant</span>
|
||||
<h2 class="d3">Change a token.<br>See all 47 components affected — instantly.</h2>
|
||||
<p class="subtitle">Because every design-token link is a first-class edge in the graph, changing <code style="font-family:var(--mono);font-size:0.85em;background:var(--bg-muted);padding:2px 6px;border-radius:4px;">--color-primary</code> immediately highlights every component downstream. No Storybook trawl. No grep. Just the answer.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>Token edges are resolved at index time — the graph knows before you even ask.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>Affected nodes are ranked by depth: direct consumers first, transitive next.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>The result feeds directly into Intent Diff, so every PR ships with a complete blast-radius audit.</li>
|
||||
</ul>
|
||||
<a href="/sign-up" class="btn btn-primary">Get Started</a>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:28px;display:flex;flex-direction:column;overflow:hidden;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:16px;">Token blast radius — --color-primary</div>
|
||||
|
||||
<!-- Token source row -->
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;">
|
||||
<div style="width:28px;height:28px;border-radius:6px;background:rgba(8,145,178,0.15);border:1px solid rgba(8,145,178,0.4);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><circle cx="6" cy="6" r="4" stroke="#0891B2" stroke-width="1.5"/></svg>
|
||||
</div>
|
||||
<span style="font-family:var(--mono);font-size:0.625rem;color:#0891B2;font-weight:600;">--color-primary</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.22);margin-left:auto;white-space:nowrap;">#0066FF → #0891B2</span>
|
||||
</div>
|
||||
|
||||
<!-- Affected component list -->
|
||||
<div style="display:flex;flex-direction:column;gap:7px;padding-left:14px;border-left:1px solid rgba(8,145,178,0.2);">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:5px;height:1px;background:rgba(8,145,178,0.4);flex-shrink:0;margin-left:-17px;"></div>
|
||||
<div style="width:20px;height:20px;border-radius:4px;background:rgba(8,145,178,0.1);border:1px solid rgba(8,145,178,0.22);display:flex;align-items:center;justify-content:center;flex-shrink:0;"><div style="width:8px;height:8px;border-radius:50%;background:rgba(8,145,178,0.6);"></div></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.65);">Button</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(8,145,178,0.8);margin-left:auto;background:rgba(8,145,178,0.08);padding:1px 6px;border-radius:3px;">direct</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:5px;height:1px;background:rgba(8,145,178,0.4);flex-shrink:0;margin-left:-17px;"></div>
|
||||
<div style="width:20px;height:20px;border-radius:4px;background:rgba(8,145,178,0.1);border:1px solid rgba(8,145,178,0.22);display:flex;align-items:center;justify-content:center;flex-shrink:0;"><div style="width:8px;height:8px;border-radius:50%;background:rgba(8,145,178,0.6);"></div></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.65);">Card</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(8,145,178,0.8);margin-left:auto;background:rgba(8,145,178,0.08);padding:1px 6px;border-radius:3px;">direct</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:5px;height:1px;background:rgba(8,145,178,0.4);flex-shrink:0;margin-left:-17px;"></div>
|
||||
<div style="width:20px;height:20px;border-radius:4px;background:rgba(8,145,178,0.1);border:1px solid rgba(8,145,178,0.22);display:flex;align-items:center;justify-content:center;flex-shrink:0;"><div style="width:8px;height:8px;border-radius:50%;background:rgba(8,145,178,0.6);"></div></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.65);">NavBar</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(8,145,178,0.8);margin-left:auto;background:rgba(8,145,178,0.08);padding:1px 6px;border-radius:3px;">direct</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:5px;height:1px;background:rgba(255,255,255,0.1);flex-shrink:0;margin-left:-17px;"></div>
|
||||
<div style="width:20px;height:20px;border-radius:4px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;flex-shrink:0;"><div style="width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.18);"></div></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.4);">DashboardCard</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.25);margin-left:auto;background:rgba(255,255,255,0.04);padding:1px 6px;border-radius:3px;">via Card</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="width:5px;height:1px;background:rgba(255,255,255,0.1);flex-shrink:0;margin-left:-17px;"></div>
|
||||
<div style="width:20px;height:20px;border-radius:4px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;flex-shrink:0;"><div style="width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.18);"></div></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5625rem;color:rgba(255,255,255,0.4);">Input</span>
|
||||
<span style="font-family:var(--mono);font-size:0.4375rem;color:rgba(255,255,255,0.25);margin-left:auto;background:rgba(255,255,255,0.04);padding:1px 6px;border-radius:3px;">via Button</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;opacity:0.45;">
|
||||
<div style="width:5px;height:1px;background:rgba(255,255,255,0.1);flex-shrink:0;margin-left:-17px;"></div>
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.3);">+ 42 more components…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary bar -->
|
||||
<div style="margin-top:auto;padding:10px 12px;background:rgba(8,145,178,0.07);border:1px solid rgba(8,145,178,0.14);border-radius:6px;display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.3);">Total affected</span>
|
||||
<span style="font-family:var(--mono);font-size:0.75rem;font-weight:700;color:#0891B2;">47 components</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEEP DIVE 2: Graph as API -->
|
||||
<div class="section section-muted" style="padding:100px 40px;">
|
||||
<div class="feat-split rev reveal">
|
||||
<div class="feat-copy">
|
||||
<span class="label label-teal">The graph as API</span>
|
||||
<h2 class="d3">Not just a visualization.<br>A queryable API.</h2>
|
||||
<p class="subtitle">Every node in the graph is a structured JSON object accessible via REST or MCP tools. AI agents, CI pipelines, and custom tooling can query your entire component ecosystem programmatically — no scraping, no docs to keep in sync.</p>
|
||||
<ul class="feat-pts">
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>REST endpoints return nodes, edges, and subgraphs with fully typed results.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>MCP tools let AI agents query the graph via natural language through Agent Bridge.</li>
|
||||
<li><span class="feat-pt-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><circle cx="7" cy="2.5" r="1.5" fill="currentColor"/><circle cx="2.5" cy="11" r="1.5" fill="currentColor"/><circle cx="11.5" cy="11" r="1.5" fill="currentColor"/><path d="M6.2 3.8L3.2 9.6M7.8 3.8l3 5.8M3.8 11h6.4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg></span>Completion Zone AI pulls the relevant graph neighborhood as context automatically — no manual prompt engineering.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="feat-vis feat-vis-dark reveal">
|
||||
<div style="position:absolute;inset:0;padding:28px;overflow:auto;">
|
||||
<div style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.25);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:14px;">GET /graph/nodes/Button</div>
|
||||
<div class="code-block" style="font-size:0.6875rem;padding:18px;margin:0;line-height:1.7;">
|
||||
<span class="kw">{</span>
|
||||
<span class="str">"id"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="str">"Button"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"type"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="str">"component"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"file"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="str">"src/ui/Button.tsx"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"props"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="kw">{</span>
|
||||
<span class="str">"label"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="nm">"string"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"variant"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="nm">"primary | secondary"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"disabled"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="nm">"boolean"</span>
|
||||
<span class="kw">}</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"usedBy"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="kw">[</span><span class="str">"Card"</span><span style="color:rgba(255,255,255,0.45)">, </span><span class="str">"NavBar"</span><span class="kw">]</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"uses"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="kw">[]</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"tokens"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="kw">[</span>
|
||||
<span class="tl">"--color-primary"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="tl">"--radius-sm"</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="tl">"--font-weight-semibold"</span>
|
||||
<span class="kw">]</span><span style="color:rgba(255,255,255,0.45)">,</span>
|
||||
<span class="str">"updatedAt"</span><span style="color:rgba(255,255,255,0.45)">: </span><span class="str">"2025-04-25T14:22:07Z"</span>
|
||||
<span class="kw">}</span>
|
||||
</div>
|
||||
<div style="margin-top:12px;padding:8px 12px;background:rgba(8,145,178,0.07);border:1px solid rgba(8,145,178,0.14);border-radius:6px;display:flex;align-items:center;gap:6px;">
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(8,145,178,0.8);">MCP →</span>
|
||||
<span style="font-family:var(--mono);font-size:0.5rem;color:rgba(255,255,255,0.35);">graph.query("What will break if I rename Button?")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TECH SECTION -->
|
||||
<section class="section">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">Technical foundation</span>
|
||||
<h2 class="d2">Six layers that keep<br>the graph always current.</h2>
|
||||
<p class="subtitle">Origin Graph is designed to stay accurate as your codebase moves — incremental, typed, and accessible to any tooling that speaks REST or MCP.</p>
|
||||
</div>
|
||||
<div class="tech-grid stagger">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🌳</div>
|
||||
<h3>AST walker</h3>
|
||||
<p>A Babel-based AST walker traverses every component file on save, extracting imports, JSX usage, and design-token references — without executing any code.</p>
|
||||
<span class="tech-tag">Static analysis</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⚛</div>
|
||||
<h3>React Fiber runtime</h3>
|
||||
<p>A Fiber hook captures live props and state during rendering, adding real-world usage data — actual prop values in production — that static analysis cannot infer.</p>
|
||||
<span class="tech-tag">Runtime</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">🗄</div>
|
||||
<h3>Supabase graph store</h3>
|
||||
<p>Nodes and edges are persisted in Supabase Postgres with a graph-friendly schema. Postgres views and RPC functions serve subgraph queries with sub-10ms latency.</p>
|
||||
<span class="tech-tag">Persistence</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"><path d="M5.5 8.5l3-3"/><path d="M3.8 6.8L2.8 7.8A2.5 2.5 0 006.3 11l1-1"/><path d="M10.2 7.2l1-1A2.5 2.5 0 007.7 3l-1 1"/></svg></div>
|
||||
<h3>Typed edges</h3>
|
||||
<p>Three edge types — <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">usage</code>, <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">inheritance</code>, <code style="font-family:var(--mono);font-size:0.8em;background:var(--bg-muted);padding:1px 4px;border-radius:3px;">token</code> — are stored with directionality and metadata, making impact analysis a simple graph traversal rather than a heuristic search.</p>
|
||||
<span class="tech-tag">Data model</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">💬</div>
|
||||
<h3>Natural language queries</h3>
|
||||
<p>Agent Bridge exposes the graph as an MCP tool. Ask "What will break if I rename Card?" in plain English from Cursor, Claude Code, or any MCP-compatible editor.</p>
|
||||
<span class="tech-tag">Agent Bridge</span>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">⚡</div>
|
||||
<h3>Incremental updates</h3>
|
||||
<p>Only the files touched since the last commit are re-indexed. No full rebuild. A project with 1,000 components updates in under two seconds on a file save.</p>
|
||||
<span class="tech-tag">Performance</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- RELATED FEATURES -->
|
||||
<section class="section section-muted">
|
||||
<div class="wrap">
|
||||
<div class="section-head reveal">
|
||||
<span class="label">What connects here</span>
|
||||
<h2 class="d2">Origin Graph powers<br>everything downstream.</h2>
|
||||
<p class="subtitle">The graph is infrastructure. Every Originmain feature that needs to understand your codebase queries it first.</p>
|
||||
</div>
|
||||
<div class="related-grid stagger">
|
||||
<a class="related-card" href="/features/live-artboard.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="2.5" width="11" height="9" rx="1.5" stroke="currentColor" stroke-width="1.3"/><path d="M5 6.5l1.8 2 1.2-1.6 1.4 2" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h3>Live Artboard</h3>
|
||||
<p>The Fiber hook that powers Live Artboard is the same one feeding runtime usage data into Origin Graph. Both features share a single, lightweight injection.</p>
|
||||
<span class="related-link">Explore Live Artboard →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/intent-diff.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1.5" y="4" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="6" y="2.5" width="6.5" height="7.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg></div>
|
||||
<h3>Intent Diff</h3>
|
||||
<p>When Intent Diff calculates the blast radius of a change, it queries Origin Graph. The complete token and usage graph makes the diff semantically complete.</p>
|
||||
<span class="related-link">Explore Intent Diff →</span>
|
||||
</a>
|
||||
<a class="related-card" href="/features/completion-zone.html">
|
||||
<div class="related-icon"><svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"><path d="M7 1.5L8.1 5.4 12 6.5l-3.9 1.1L7 11.5 5.9 7.6 2 6.5l3.9-1.1z"/><circle cx="11" cy="2.5" r="1" fill="currentColor" opacity="0.5"/></svg></div>
|
||||
<h3>Completion Zone</h3>
|
||||
<p>Completion Zone AI pulls the relevant graph neighborhood as context before generating. Your design system constraints arrive as graph data — not a prompt you maintain.</p>
|
||||
<span class="related-link">Explore Completion Zone →</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta-bottom">
|
||||
<div class="cta-inner">
|
||||
<h2 class="cta-headline">Your component graph,<br>always up to date.</h2>
|
||||
<p class="cta-sub">Early access is free. Connect your repo and the first full graph index runs in under a minute.</p>
|
||||
<div class="cta-actions">
|
||||
<a href="/sign-up" class="btn btn-white btn-lg">Get Started Free</a>
|
||||
<a href="/" class="btn btn-outline-white btn-lg">Back to overview</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="ft-inner">
|
||||
<div class="ft-top">
|
||||
<div>
|
||||
<div class="ft-brand-logo">Origin<span>main</span></div>
|
||||
<p class="ft-brand-tag">Where design intent becomes production code.</p>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Product</h4>
|
||||
<ul>
|
||||
<li><a href="/features/live-artboard.html">Live Artboard</a></li>
|
||||
<li><a href="/features/origin-graph.html">Origin Graph</a></li>
|
||||
<li><a href="/features/intent-diff.html">Intent Diff</a></li>
|
||||
<li><a href="/features/completion-zone.html">Completion Zone</a></li>
|
||||
<li><a href="/features/agent-bridge.html">Agent Bridge</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Integrations</h4>
|
||||
<ul>
|
||||
<li><a href="/features/integrations.html">All integrations</a></li>
|
||||
<li><a href="#">GitHub</a></li>
|
||||
<li><a href="#">Linear</a></li>
|
||||
<li><a href="#">Slack</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Developers</h4>
|
||||
<ul>
|
||||
<li><a href="#">Docs</a></li>
|
||||
<li><a href="#">API Reference</a></li>
|
||||
<li><a href="#">MCP Protocol</a></li>
|
||||
<li><a href="#">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ft-col">
|
||||
<h4>Company</h4>
|
||||
<ul>
|
||||
<li><a href="#">About</a></li>
|
||||
<li><a href="/#pricing">Pricing</a></li>
|
||||
<li><a href="#">Blog</a></li>
|
||||
<li><a href="#">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ft-bottom">
|
||||
<span class="ft-copy">© 2025 Originmain. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Nav scroll
|
||||
const nav=document.getElementById('nav');
|
||||
window.addEventListener('scroll',()=>{nav.classList.toggle('scrolled',window.scrollY>20);});
|
||||
// Dropdown: delay-based hover (desktop) + click (mobile/touch)
|
||||
document.querySelectorAll('.nav-dropdown').forEach(dd=>{const menu=dd.querySelector('.nav-dropdown-menu');let t;const open=()=>{clearTimeout(t);dd.classList.add('open');menu.classList.add('open');};const close=()=>{t=setTimeout(()=>{dd.classList.remove('open');menu.classList.remove('open');},150);};dd.addEventListener('mouseenter',open);dd.addEventListener('mouseleave',close);menu.addEventListener('mouseenter',open);menu.addEventListener('mouseleave',close);dd.querySelector('a').addEventListener('click',e=>{if(window.matchMedia('(hover:none)').matches||window.innerWidth<=1024){e.preventDefault();const o=menu.classList.toggle('open');dd.classList.toggle('open',o);}});});
|
||||
// Mobile product sub-accordion
|
||||
const mpt=document.getElementById('mob-product-toggle'),mns=document.getElementById('nav-mobile-sub'),mdc=document.getElementById('mob-dd-caret');if(mpt&&mns){mpt.addEventListener('click',e=>{e.preventDefault();const o=mns.classList.toggle('open');if(mdc)mdc.style.transform=o?'rotate(180deg)':'';});}
|
||||
// Burger
|
||||
const burger=document.getElementById('nav-burger'),mob=document.getElementById('nav-mobile');
|
||||
burger.addEventListener('click',()=>{const o=mob.classList.toggle('open');burger.classList.toggle('open',o);burger.setAttribute('aria-expanded',o);});
|
||||
// Reveal
|
||||
const io=new IntersectionObserver(entries=>{entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target);}});},{threshold:0.12});
|
||||
document.querySelectorAll('.reveal,.stagger').forEach(el=>io.observe(el));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,118 +0,0 @@
|
||||
// POST /api/agent-bridge/register-indexer
|
||||
// POST /api/agent-bridge/register-indexer/heartbeat (same handler, path checked below)
|
||||
//
|
||||
// Called by the CLI's `originmain dev` command to register the local AST indexer
|
||||
// so the Agent Bridge can proxy component-resolution requests to it.
|
||||
//
|
||||
// Security (spec Phase 5 §8.3):
|
||||
// • Bearer auth: same workspace-token mechanism as the main MCP endpoint.
|
||||
// • indexerUrl MUST be localhost / 127.0.0.1 — external URLs are rejected to
|
||||
// prevent the Agent Bridge from being used as an SSRF relay.
|
||||
// • TTL: 300 s default; heartbeat POST refreshes it every 120 s.
|
||||
// • Agent Bridge evicts registrations with no heartbeat after 360 s.
|
||||
//
|
||||
// Heartbeat endpoint: POST /api/agent-bridge/register-indexer/heartbeat
|
||||
// Body: { workspaceToken: string }
|
||||
// Returns 200 on success, 404 if the workspace has no active registration.
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { verifyWorkspaceToken, registerIndexer, heartbeatIndexer } from '@originmain/agent-bridge';
|
||||
|
||||
const TTL_SECONDS = 300; // 5 minutes per spec
|
||||
|
||||
/**
|
||||
* Returns true when `url` resolves to the local machine (localhost / loopback).
|
||||
* We block any non-localhost indexerUrl to prevent SSRF.
|
||||
*/
|
||||
function isLocalhostUrl(raw: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// ── Auth ────────────────────────────────────────────────────────────────────
|
||||
// Accept the workspace token either in the Authorization header (CLI standard)
|
||||
// or in the request body as `workspaceToken` (heartbeat convenience).
|
||||
let token: string | null = null;
|
||||
const authHeader = req.headers.get('authorization') ?? '';
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.slice(7);
|
||||
}
|
||||
|
||||
let body: Record<string, unknown> = {};
|
||||
try {
|
||||
body = (await req.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
// Body is optional for heartbeat (token may come from header only)
|
||||
}
|
||||
|
||||
if (!token && typeof body['workspaceToken'] === 'string') {
|
||||
token = body['workspaceToken'];
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing Bearer token or workspaceToken in body' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceToken = verifyWorkspaceToken(token);
|
||||
if (!workspaceToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid or expired workspace token' },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Heartbeat path ──────────────────────────────────────────────────────────
|
||||
// The CLI sends a heartbeat POST to the same URL with no indexerUrl in the body.
|
||||
// We detect this by the absence of indexerUrl and refresh the TTL instead.
|
||||
if (!body['indexerUrl']) {
|
||||
const refreshed = heartbeatIndexer(workspaceToken.workspaceId, TTL_SECONDS);
|
||||
if (!refreshed) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No active registration for this workspace — re-register first' },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
action: 'heartbeat',
|
||||
workspaceId: workspaceToken.workspaceId,
|
||||
ttlSeconds: TTL_SECONDS,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Registration path ───────────────────────────────────────────────────────
|
||||
const indexerUrl = typeof body['indexerUrl'] === 'string' ? body['indexerUrl'] : null;
|
||||
|
||||
if (!indexerUrl) {
|
||||
return NextResponse.json({ error: '`indexerUrl` is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Security: only localhost URLs are allowed — prevent SSRF
|
||||
if (!isLocalhostUrl(indexerUrl)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'indexerUrl must be a localhost URL (e.g. http://localhost:4171)' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const ttl = typeof body['ttl'] === 'number' ? Math.min(body['ttl'], 600) : TTL_SECONDS;
|
||||
|
||||
registerIndexer(workspaceToken.workspaceId, indexerUrl, ttl);
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
action: 'registered',
|
||||
workspaceId: workspaceToken.workspaceId,
|
||||
indexerUrl,
|
||||
ttlSeconds: ttl,
|
||||
heartbeatIntervalSeconds: 120,
|
||||
});
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
// GET /api/agent-bridge — SSE stream; server pushes INTENT_RECEIVED events
|
||||
// POST /api/agent-bridge — JSON-RPC 2.0; agent calls tools (push_intent, resolve_component, …)
|
||||
//
|
||||
// Authentication: Bearer <workspace_token> (HMAC-SHA256, issued by issueWorkspaceToken).
|
||||
// Both methods share the same auth scheme.
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { verifyWorkspaceToken, TOOL_MAP, getToolList, dispatchTool, registerSseClient } from '@originmain/agent-bridge';
|
||||
import type { JsonRpcRequest, ToolContext } from '@originmain/agent-bridge';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
// ── GET — SSE stream for real-time INTENT_RECEIVED push ─────────────────────
|
||||
// The agent connects once and holds this connection open. When the canvas
|
||||
// exports a style intent (via POST /api/intent), storePendingIntent() fires
|
||||
// pushToSseClients() which delivers the event immediately over this stream.
|
||||
//
|
||||
// Deployment note: this is a long-lived streaming response. In serverless
|
||||
// environments (Vercel Hobby / Functions) connections are cut at the platform
|
||||
// timeout (~30 s). For persistent SSE, deploy with Vercel Fluid Compute,
|
||||
// a Node.js server, or replace the in-process client registry with a
|
||||
// Supabase Realtime / Redis pub-sub channel.
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────
|
||||
const authHeader = req.headers.get('authorization') ?? '';
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
|
||||
if (!token) {
|
||||
return new NextResponse('Missing Bearer token', { status: 401 });
|
||||
}
|
||||
|
||||
const workspaceToken = verifyWorkspaceToken(token);
|
||||
if (!workspaceToken) {
|
||||
return new NextResponse('Invalid or expired token', { status: 401 });
|
||||
}
|
||||
|
||||
const { workspaceId } = workspaceToken;
|
||||
const enc = new TextEncoder();
|
||||
|
||||
// ── SSE stream ────────────────────────────────────────────────────────────
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send the mandatory SSE preamble so the client knows the connection is live.
|
||||
controller.enqueue(enc.encode(': connected\n\n'));
|
||||
|
||||
// Register callback — fires every time storePendingIntent() is called for
|
||||
// this workspace (i.e., the canvas exported a new style intent).
|
||||
const unsubscribe = registerSseClient(workspaceId, (chunk: string) => {
|
||||
try {
|
||||
controller.enqueue(enc.encode(chunk));
|
||||
} catch {
|
||||
// Controller may be closed if the client disconnected between the
|
||||
// callback being fired and the enqueue call.
|
||||
}
|
||||
});
|
||||
|
||||
// Keep-alive comment every 25 s — prevents proxies from closing the
|
||||
// connection after a 30 s idle timeout while the agent waits for intents.
|
||||
const keepAliveTimer = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(enc.encode(`: keep-alive\n\n`));
|
||||
} catch {
|
||||
clearInterval(keepAliveTimer);
|
||||
}
|
||||
}, 25_000);
|
||||
|
||||
// Clean up when the client disconnects (connection abort or close).
|
||||
req.signal.addEventListener('abort', () => {
|
||||
clearInterval(keepAliveTimer);
|
||||
unsubscribe();
|
||||
try { controller.close(); } catch { /* already closed */ }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no', // Nginx: disable response buffering
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// ── Auth ────────────────────────────────────────────────────────────────────
|
||||
const authHeader = req.headers.get('authorization') ?? '';
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Missing Bearer token' } },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceToken = verifyWorkspaceToken(token);
|
||||
if (!workspaceToken) {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Invalid or expired token' } },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Parse JSON-RPC body ─────────────────────────────────────────────────────
|
||||
let body: JsonRpcRequest;
|
||||
try {
|
||||
body = (await req.json()) as JsonRpcRequest;
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// tools/list — meta-endpoint; return available tools without executing one.
|
||||
// MCP spec requires each entry to include inputSchema so clients can
|
||||
// construct valid tool calls without out-of-band documentation.
|
||||
if (body.method === 'tools/list') {
|
||||
return NextResponse.json({ jsonrpc: '2.0', id: body.id, result: { tools: getToolList() } });
|
||||
}
|
||||
|
||||
// ── Dispatch ────────────────────────────────────────────────────────────────
|
||||
if (!TOOL_MAP[body.method]) {
|
||||
return NextResponse.json({
|
||||
jsonrpc: '2.0',
|
||||
id: body.id,
|
||||
error: { code: -32601, message: `Method not found: ${body.method}` },
|
||||
});
|
||||
}
|
||||
|
||||
const ctx: ToolContext = {
|
||||
workspaceId: workspaceToken.workspaceId,
|
||||
params: body.params ?? {},
|
||||
// Pass the server-side Supabase client for tools that need DB writes
|
||||
// (e.g. update_diff_status writes blocked_reason to intent_diffs).
|
||||
db: serverClient() as unknown as NonNullable<ToolContext['db']>,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await dispatchTool(body.method, ctx);
|
||||
return NextResponse.json({ jsonrpc: '2.0', id: body.id, result });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Internal error';
|
||||
return NextResponse.json({
|
||||
jsonrpc: '2.0',
|
||||
id: body.id,
|
||||
error: { code: -32603, message },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// POST /api/ai/completion-zone
|
||||
// Fills a design completion zone given a component tree and user intent.
|
||||
//
|
||||
// Accepts two shapes:
|
||||
// Canvas UI payload: { artboard_id, bounds, prompt, componentTreeJson? }
|
||||
// Canonical payload: { componentTreeJson, intent, dlfJson?, screenshotBase64? }
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, fillCompletionZone } from '@originmain/ai-layer';
|
||||
import type { CompletionZoneInput } from '@originmain/ai-layer';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getActiveDesignLanguageFile } from '@originmain/origin-graph';
|
||||
|
||||
// ── Canvas UI payload shape ───────────────────────────────────────────────────
|
||||
interface CanvasPayload {
|
||||
artboard_id?: string;
|
||||
bounds?: { x: number; y: number; width: number; height: number };
|
||||
prompt?: string;
|
||||
componentTreeJson?: string;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const raw = (await req.json()) as CanvasPayload & Partial<CompletionZoneInput>;
|
||||
|
||||
// ── Adapt canvas UI payload → CompletionZoneInput ────────────────────────
|
||||
let input: CompletionZoneInput;
|
||||
|
||||
if (raw.intent !== undefined) {
|
||||
// Already canonical shape — pass through
|
||||
input = raw as CompletionZoneInput;
|
||||
} else {
|
||||
// Canvas UI shape: {artboard_id, bounds, prompt, componentTreeJson?}
|
||||
if (!raw.prompt?.trim()) {
|
||||
return NextResponse.json({ error: 'prompt is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build a context string from bounds + artboard metadata if available
|
||||
const boundsLabel = raw.bounds
|
||||
? `Canvas region: x=${raw.bounds.x}, y=${raw.bounds.y}, width=${raw.bounds.width}, height=${raw.bounds.height}`
|
||||
: '';
|
||||
|
||||
// Try to load the active DLF for this workspace (best-effort — skip on error)
|
||||
let dlfJson: string | undefined;
|
||||
if (raw.artboard_id) {
|
||||
try {
|
||||
const db = serverClient();
|
||||
// Resolve workspace_id from artboard
|
||||
const { data: ab } = await db
|
||||
.from('artboards')
|
||||
.select('workspace_id')
|
||||
.eq('id', raw.artboard_id)
|
||||
.single();
|
||||
if (ab) {
|
||||
const dlf = await getActiveDesignLanguageFile(db, (ab as { workspace_id: string }).workspace_id);
|
||||
if (dlf) dlfJson = JSON.stringify(dlf.schema_jsonb);
|
||||
}
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
input = {
|
||||
componentTreeJson: raw.componentTreeJson ?? boundsLabel ?? '{}',
|
||||
intent: raw.prompt.trim(),
|
||||
...(dlfJson !== undefined ? { dlfJson } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await fillCompletionZone(gateway, input);
|
||||
return NextResponse.json({
|
||||
proposedTree: result.proposedTree,
|
||||
description: result.description,
|
||||
// Also include result/completion for backwards-compat with any old clients
|
||||
result: result.description,
|
||||
completion: result.description,
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// POST /api/ai/diff-summary
|
||||
// Generates a one-sentence natural-language summary of a set of component changes.
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, generateDiffSummary } from '@originmain/ai-layer';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as { changesJson?: string; componentName?: string };
|
||||
|
||||
if (!body.changesJson) {
|
||||
return NextResponse.json({ error: 'changesJson is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await generateDiffSummary(gateway, {
|
||||
changesJson: body.changesJson,
|
||||
componentName: body.componentName ?? 'Component',
|
||||
});
|
||||
return NextResponse.json({ summary: result.summary });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
// POST /api/ai/drift-report
|
||||
// Accepts { artboard_id } — fetches artboard metadata + workspace DLF from DB,
|
||||
// then calls generateDriftReport. Screenshots are optional; text-only analysis runs
|
||||
// when the artboard has no renderUrl or the screenshot is not provided by the client.
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, generateDriftReport } from '@originmain/ai-layer';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const callerEmail = user.primaryEmailAddress?.emailAddress;
|
||||
if (!callerEmail) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
artboard_id?: string;
|
||||
/** Optional — client may pass a base64 screenshot captured from the live iframe */
|
||||
screenshot_base64?: string;
|
||||
};
|
||||
|
||||
if (!body.artboard_id) {
|
||||
return NextResponse.json({ error: 'artboard_id is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
// Fetch artboard + verify the caller is a workspace member
|
||||
const { data: artboard } = await db
|
||||
.from('artboards')
|
||||
.select('id, name, workspace_id, project_id, metadata_jsonb')
|
||||
.eq('id', body.artboard_id)
|
||||
.single() as unknown as {
|
||||
data: {
|
||||
id: string;
|
||||
name: string;
|
||||
workspace_id: string;
|
||||
project_id: string | null;
|
||||
metadata_jsonb: Record<string, unknown>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
if (!artboard) {
|
||||
return NextResponse.json({ error: 'Artboard not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auth gate: require workspace membership
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', artboard.workspace_id)
|
||||
.eq('email', callerEmail)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!member) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Fetch the active Design Language for this workspace (Phase 6 table).
|
||||
// raw_json holds the original uploaded token file; pass it to generateDriftReport
|
||||
// for context. Absence of a design language is non-fatal — analysis runs without it.
|
||||
const { data: dl } = await db
|
||||
.from('design_languages')
|
||||
.select('raw_json, name')
|
||||
.eq('workspace_id', artboard.workspace_id)
|
||||
.limit(1)
|
||||
.single() as unknown as { data: { raw_json: unknown; name: string } | null };
|
||||
|
||||
const meta = artboard.metadata_jsonb;
|
||||
const artboardContext = [
|
||||
`Artboard: ${artboard.name}`,
|
||||
`Size: ${meta['width'] ?? '?'} × ${meta['height'] ?? '?'}`,
|
||||
...(meta['renderUrl'] ? [`Render URL: ${String(meta['renderUrl'])}`] : []),
|
||||
...(artboard.project_id ? [`Project ID: ${artboard.project_id}`] : []),
|
||||
].join('\n');
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await generateDriftReport(gateway, {
|
||||
artboardContext,
|
||||
...(dl ? { dlfJson: JSON.stringify(dl.raw_json) } : {}),
|
||||
...(body.screenshot_base64 ? { screenshotBase64: body.screenshot_base64 } : {}),
|
||||
});
|
||||
return NextResponse.json(result);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// POST /api/ai/query
|
||||
// Cross-artboard search: finds artboards matching a natural language query.
|
||||
//
|
||||
// Accepts two shapes:
|
||||
// Navigator UI payload: { workspace_id, question }
|
||||
// Canonical payload: { query, artboardsJson }
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { AIGateway, queryCrossArtboard } from '@originmain/ai-layer';
|
||||
import type { ArtboardQueryInput } from '@originmain/ai-layer';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getArtboards } from '@originmain/origin-graph';
|
||||
|
||||
interface NavigatorPayload {
|
||||
workspace_id?: string;
|
||||
question?: string;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const raw = (await req.json()) as NavigatorPayload & Partial<ArtboardQueryInput>;
|
||||
|
||||
// ── Adapt navigator UI payload → ArtboardQueryInput ──────────────────────
|
||||
let input: ArtboardQueryInput;
|
||||
|
||||
if (raw.query !== undefined && raw.artboardsJson !== undefined) {
|
||||
// Already canonical
|
||||
input = raw as ArtboardQueryInput;
|
||||
} else {
|
||||
// Navigator UI shape: { workspace_id, question }
|
||||
const queryText = (raw.question ?? raw.query ?? '').trim();
|
||||
if (!queryText) return NextResponse.json({ error: 'question is required' }, { status: 400 });
|
||||
|
||||
const workspaceId = raw.workspace_id;
|
||||
if (!workspaceId) return NextResponse.json({ error: 'workspace_id is required' }, { status: 400 });
|
||||
|
||||
// Fetch artboard metadata to give the AI context
|
||||
let artboardsJson = '[]';
|
||||
try {
|
||||
const db = serverClient();
|
||||
const artboards = await getArtboards(db, workspaceId);
|
||||
artboardsJson = JSON.stringify(
|
||||
artboards.map(ab => ({
|
||||
id: ab.id,
|
||||
name: ab.name,
|
||||
metadata: ab.metadata_jsonb,
|
||||
created_at: ab.created_at,
|
||||
}))
|
||||
);
|
||||
} catch { /* non-fatal: AI will work with empty list */ }
|
||||
|
||||
input = { query: queryText, artboardsJson };
|
||||
}
|
||||
|
||||
try {
|
||||
const gateway = new AIGateway();
|
||||
const result = await queryCrossArtboard(gateway, input);
|
||||
|
||||
// Return both the canonical shape AND a human-readable answer string
|
||||
// so the navigator UI's `data.answer ?? data.result` check gets something useful.
|
||||
const answerText = result.reasoning ||
|
||||
(result.results.length > 0
|
||||
? result.results.map(r => `${r.artboardId}: ${r.reason}`).join('\n')
|
||||
: 'No matching artboards found.');
|
||||
|
||||
return NextResponse.json({ ...result, answer: answerText, result: answerText });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'AI error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// GET /api/artboards/:id → fetch single artboard
|
||||
// PATCH /api/artboards/:id → update name / metadata_jsonb
|
||||
// DELETE /api/artboards/:id → permanently remove artboard
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getArtboard, updateArtboard, deleteArtboard } from '@originmain/origin-graph';
|
||||
import type { InsertArtboard } from '@originmain/origin-graph';
|
||||
|
||||
type RouteContext = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: RouteContext) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
try {
|
||||
const artboard = await getArtboard(serverClient(), id);
|
||||
return NextResponse.json(artboard);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
const status = message.includes('0 rows') ? 404 : 500;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: RouteContext) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = (await req.json().catch(() => ({}))) as Partial<InsertArtboard>;
|
||||
|
||||
// Only allow safe fields to be patched
|
||||
const patch: Partial<InsertArtboard> = {};
|
||||
if (typeof body.name === 'string' && body.name.trim()) patch.name = body.name.trim();
|
||||
if (body.metadata_jsonb !== undefined) patch.metadata_jsonb = body.metadata_jsonb;
|
||||
// isolation_props are stored as a direct DB column and patched independently
|
||||
// of metadata_jsonb so IsolationFrame can receive live prop updates.
|
||||
if (body.isolation_props !== undefined) patch.isolation_props = body.isolation_props;
|
||||
|
||||
if (Object.keys(patch).length === 0)
|
||||
return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const updated = await updateArtboard(serverClient(), id, patch);
|
||||
return NextResponse.json(updated);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: RouteContext) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
try {
|
||||
await deleteArtboard(serverClient(), id);
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// GET /api/artboards?workspaceId=<uuid>[&projectId=<uuid>] → list artboards
|
||||
// POST /api/artboards → create artboard
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getArtboards, createArtboard } from '@originmain/origin-graph';
|
||||
import type { InsertArtboard } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const workspaceId = req.nextUrl.searchParams.get('workspaceId');
|
||||
if (!workspaceId) return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 });
|
||||
|
||||
const projectId = req.nextUrl.searchParams.get('projectId') ?? undefined;
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const artboards = await getArtboards(db, workspaceId, projectId);
|
||||
return NextResponse.json(artboards);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as InsertArtboard;
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const artboard = await createArtboard(db, body);
|
||||
return NextResponse.json(artboard, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// POST /api/artboards/thumbnail
|
||||
//
|
||||
// Accepts a base64 JPEG data URL captured by html2canvas inside an artboard
|
||||
// iframe, uploads it to Supabase Storage, and persists the public URL in the
|
||||
// artboards.thumbnail_url column.
|
||||
//
|
||||
// Request body: { artboardId: string; workspaceId: string; dataUrl: string }
|
||||
// Response: { publicUrl: string }
|
||||
//
|
||||
// Storage path: artboard-thumbnails/{workspaceId}/{artboardId}.jpg
|
||||
// Bucket policy: public read, authenticated write.
|
||||
//
|
||||
// spec: SOURCE-AWARE-CANVAS.md Phase 0 §3.6 "Thumbnail capture"
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { updateArtboard } from '@originmain/origin-graph';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
const BUCKET = 'artboard-thumbnails';
|
||||
const MAX_DATA_URL = 5 * 1024 * 1024; // 5 MB safety cap — rejects obviously corrupted payloads
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
let body: { artboardId?: string; workspaceId?: string; dataUrl?: string };
|
||||
try {
|
||||
body = (await req.json()) as typeof body;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { artboardId, workspaceId, dataUrl } = body;
|
||||
if (!artboardId || !workspaceId || !dataUrl) {
|
||||
return NextResponse.json({ error: 'artboardId, workspaceId, and dataUrl are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (dataUrl.length > MAX_DATA_URL) {
|
||||
return NextResponse.json({ error: 'dataUrl exceeds 5 MB limit' }, { status: 413 });
|
||||
}
|
||||
|
||||
// Strip the `data:image/jpeg;base64,` prefix and decode to bytes
|
||||
const base64 = dataUrl.replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
let imageBytes: Buffer;
|
||||
try {
|
||||
imageBytes = Buffer.from(base64, 'base64');
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid base64 data URL' }, { status: 400 });
|
||||
}
|
||||
|
||||
const db = serverClient() as unknown as SupabaseClient;
|
||||
const storagePath = `${workspaceId}/${artboardId}.jpg`;
|
||||
|
||||
// ── Upload to Supabase Storage ────────────────────────────────────────────
|
||||
const { error: uploadError } = await db.storage
|
||||
.from(BUCKET)
|
||||
.upload(storagePath, imageBytes, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true, // overwrite on repeat capture
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
return NextResponse.json({ error: `Storage upload failed: ${uploadError.message}` }, { status: 500 });
|
||||
}
|
||||
|
||||
// ── Get public URL ────────────────────────────────────────────────────────
|
||||
const { data: urlData } = db.storage.from(BUCKET).getPublicUrl(storagePath);
|
||||
const publicUrl = urlData.publicUrl;
|
||||
|
||||
// ── Persist to artboards.thumbnail_url ───────────────────────────────────
|
||||
try {
|
||||
await updateArtboard(serverClient(), artboardId, { thumbnail_url: publicUrl });
|
||||
} catch (err) {
|
||||
// Non-fatal: the in-memory data URL still works for this session
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.warn(`[thumbnail] DB update failed for ${artboardId}: ${msg}`);
|
||||
}
|
||||
|
||||
return NextResponse.json({ publicUrl });
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
// GET /api/cli-auth?callback=<url>
|
||||
//
|
||||
// Browser-initiated CLI auth endpoint. The user arrives here after `originmain
|
||||
// login` opens their browser. Authenticates via Clerk, issues a HMAC workspace
|
||||
// token, and redirects to the CLI's local callback server with the credentials.
|
||||
//
|
||||
// Query params:
|
||||
// callback — The local CLI callback URL (must be localhost).
|
||||
// workspace_id — Optional. If omitted, uses the user's first workspace.
|
||||
//
|
||||
// Redirect target: <callback>?token=X&workspaceId=Y&bridgeUrl=Z
|
||||
// or <callback>?error=<message> on failure.
|
||||
//
|
||||
// Security:
|
||||
// • callback must be a localhost URL — external URLs are rejected.
|
||||
// • Requires Clerk authentication; unauthenticated users are redirected to sign-in.
|
||||
//
|
||||
// spec: SOURCE-AWARE-CANVAS.md Phase 5 §8.6
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { issueWorkspaceToken } from '@originmain/agent-bridge';
|
||||
|
||||
const DEFAULT_BRIDGE_URL = process.env['ORIGINMAIN_BRIDGE_URL'] ?? 'http://localhost:4172';
|
||||
const APP_URL = process.env['NEXT_PUBLIC_APP_URL'] ?? 'http://localhost:3000';
|
||||
|
||||
/** Only localhost callback URLs are accepted — prevents open-redirect attacks. */
|
||||
function isLocalhostCallback(raw: string): boolean {
|
||||
try {
|
||||
const u = new URL(raw);
|
||||
return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '::1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function errorRedirect(callbackUrl: string, message: string): NextResponse {
|
||||
const target = new URL(callbackUrl);
|
||||
target.searchParams.set('error', message);
|
||||
return NextResponse.redirect(target.toString());
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest): Promise<NextResponse> {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const callbackUrl = searchParams.get('callback');
|
||||
const workspaceIdParam = searchParams.get('workspace_id');
|
||||
|
||||
// ── Validate callback URL ─────────────────────────────────────────────────
|
||||
if (!callbackUrl) {
|
||||
return NextResponse.json({ error: '`callback` query param is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!isLocalhostCallback(callbackUrl)) {
|
||||
return NextResponse.json(
|
||||
{ error: '`callback` must be a localhost URL' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Require authentication ────────────────────────────────────────────────
|
||||
const user = await currentUser();
|
||||
if (!user) {
|
||||
const signInUrl = new URL('/sign-in', APP_URL);
|
||||
signInUrl.searchParams.set('redirect_url', req.nextUrl.toString());
|
||||
return NextResponse.redirect(signInUrl.toString());
|
||||
}
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) {
|
||||
return errorRedirect(callbackUrl, 'No verified email address on this account.');
|
||||
}
|
||||
|
||||
// ── Resolve workspace ─────────────────────────────────────────────────────
|
||||
const db = serverClient();
|
||||
let workspaceId = workspaceIdParam;
|
||||
|
||||
if (!workspaceId) {
|
||||
// Use the user's first workspace membership.
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('workspace_id')
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single() as unknown as { data: { workspace_id: string } | null; error: unknown };
|
||||
|
||||
if (!member) {
|
||||
return errorRedirect(
|
||||
callbackUrl,
|
||||
'No workspace found for this account. Create a workspace at ' + APP_URL,
|
||||
);
|
||||
}
|
||||
workspaceId = member.workspace_id;
|
||||
} else {
|
||||
// Verify the user is a member of the requested workspace.
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!member) {
|
||||
return errorRedirect(callbackUrl, `You are not a member of workspace ${workspaceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Issue workspace token ─────────────────────────────────────────────────
|
||||
let token: string;
|
||||
try {
|
||||
token = issueWorkspaceToken(workspaceId, 'GENERIC');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Token generation failed';
|
||||
return errorRedirect(callbackUrl, msg);
|
||||
}
|
||||
|
||||
// ── Redirect to CLI callback ──────────────────────────────────────────────
|
||||
const target = new URL(callbackUrl);
|
||||
target.searchParams.set('token', token);
|
||||
target.searchParams.set('workspaceId', workspaceId);
|
||||
target.searchParams.set('bridgeUrl', DEFAULT_BRIDGE_URL);
|
||||
|
||||
return NextResponse.redirect(target.toString());
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
// POST /api/design-language/fetch
|
||||
// Server-side CORS proxy for fetching user-supplied design token JSON files.
|
||||
//
|
||||
// The browser cannot directly fetch a token file hosted at an arbitrary URL due
|
||||
// to CORS restrictions. This route performs the fetch server-side and returns
|
||||
// the raw JSON text so the client can run the standard validation pipeline.
|
||||
//
|
||||
// Security mitigations:
|
||||
// 1. Auth: requires a valid Clerk session — unauthenticated callers are rejected.
|
||||
// 2. HTTPS only: rejects http:// URLs to prevent plaintext credential exposure.
|
||||
// 3. Private-IP block: rejects requests to localhost, RFC-1918 ranges, and
|
||||
// link-local addresses to prevent SSRF (Server-Side Request Forgery).
|
||||
// 4. Size cap: response bodies larger than 1 MB are rejected.
|
||||
// 5. Content-Type guard: the upstream response must be JSON-like.
|
||||
//
|
||||
// spec: SOURCE-AWARE-CANVAS §3 Phase 6 — "Fetch from URL" token import flow
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
// Maximum allowed response body size (1 MB). Token files are never this large;
|
||||
// the cap protects against slow-loris and accidental large-file fetches.
|
||||
const MAX_BODY_BYTES = 1_048_576;
|
||||
|
||||
/**
|
||||
* Returns true when the URL hostname resolves to a private / loopback address
|
||||
* that should never be reachable from a proxied server-side request.
|
||||
* We guard against SSRF by rejecting hostnames that literally look private —
|
||||
* a full DNS-resolution check is not performed here because it would require
|
||||
* an extra async lookup and still be racy.
|
||||
*/
|
||||
function isPrivateHost(hostname: string): boolean {
|
||||
// Loopback
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') return true;
|
||||
// RFC-1918 private ranges (textual prefix match is sufficient for common cases)
|
||||
if (/^10\./.test(hostname)) return true; // 10.0.0.0/8
|
||||
if (/^192\.168\./.test(hostname)) return true; // 192.168.0.0/16
|
||||
if (/^172\.(1[6-9]|2\d|3[01])\./.test(hostname)) return true; // 172.16.0.0/12
|
||||
// Link-local
|
||||
if (/^169\.254\./.test(hostname)) return true;
|
||||
if (/^fe80:/i.test(hostname)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// ── Auth ───────────────────────────────────────────────────────────────────
|
||||
const { userId } = await auth();
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ── Parse request body ─────────────────────────────────────────────────────
|
||||
let body: { url?: string };
|
||||
try {
|
||||
body = await req.json() as { url?: string };
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Request body must be JSON' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { url } = body;
|
||||
if (!url || typeof url !== 'string') {
|
||||
return NextResponse.json({ error: '`url` field is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// ── URL validation ─────────────────────────────────────────────────────────
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(url);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (parsed.protocol !== 'https:') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only HTTPS URLs are supported' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (isPrivateHost(parsed.hostname)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Requests to private or loopback addresses are not allowed' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Proxy fetch ────────────────────────────────────────────────────────────
|
||||
let upstream: Response;
|
||||
try {
|
||||
upstream = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'User-Agent': 'Originmain-DLF-Proxy/1.0',
|
||||
},
|
||||
// 10-second timeout via AbortSignal
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return NextResponse.json({ error: `Fetch failed: ${msg}` }, { status: 502 });
|
||||
}
|
||||
|
||||
if (!upstream.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: `Upstream returned ${upstream.status} ${upstream.statusText}` },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Content-Type guard ────────────────────────────────────────────────────
|
||||
const contentType = upstream.headers.get('content-type') ?? '';
|
||||
if (!contentType.includes('json') && !contentType.includes('text')) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Upstream response is not JSON or plain text' },
|
||||
{ status: 415 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── Size cap ──────────────────────────────────────────────────────────────
|
||||
const bytes = await upstream.arrayBuffer();
|
||||
if (bytes.byteLength > MAX_BODY_BYTES) {
|
||||
return NextResponse.json(
|
||||
{ error: `Response too large (max ${MAX_BODY_BYTES / 1024} KB)` },
|
||||
{ status: 413 },
|
||||
);
|
||||
}
|
||||
|
||||
const text = new TextDecoder().decode(bytes);
|
||||
|
||||
// Validate that the body parses as JSON before forwarding — the client
|
||||
// expects valid JSON, not a redirect page or HTML error body.
|
||||
try {
|
||||
JSON.parse(text);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: 'Upstream response is not valid JSON' },
|
||||
{ status: 422 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ json: text }, { status: 200 });
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
// GET /api/design-language?workspaceId=<uuid> → active DesignLanguage row
|
||||
// GET /api/design-language?workspaceId=<uuid>&all=1 → version history (newest first)
|
||||
// POST /api/design-language → upload / replace token file
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import {
|
||||
getDesignLanguage,
|
||||
upsertDesignLanguage,
|
||||
getDesignLanguageVersions,
|
||||
} from '@originmain/origin-graph';
|
||||
import type { InsertDesignLanguage } from '@originmain/origin-graph';
|
||||
|
||||
// ── Auth + workspace-member guard (shared) ────────────────────────────────────
|
||||
|
||||
async function assertMember(email: string, workspaceId: string) {
|
||||
const db = serverClient();
|
||||
const { data } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
return Boolean(data);
|
||||
}
|
||||
|
||||
// ── GET ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const workspaceId = req.nextUrl.searchParams.get('workspaceId');
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!(await assertMember(email, workspaceId))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
// ?all=1 — full version history for the workspace's design language.
|
||||
if (req.nextUrl.searchParams.get('all') === '1') {
|
||||
const dl = await getDesignLanguage(db, workspaceId).catch(() => null);
|
||||
if (!dl) return NextResponse.json([]);
|
||||
const versions = await getDesignLanguageVersions(db, dl.id).catch(() => []);
|
||||
return NextResponse.json(versions);
|
||||
}
|
||||
|
||||
// Default — active token file (one row per workspace).
|
||||
const dl = await getDesignLanguage(db, workspaceId).catch((err: Error) => {
|
||||
throw new Response(JSON.stringify({ error: err.message }), { status: 500 });
|
||||
});
|
||||
return NextResponse.json(dl ?? null);
|
||||
}
|
||||
|
||||
// ── POST ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
let body: Partial<InsertDesignLanguage>;
|
||||
try {
|
||||
body = (await req.json()) as Partial<InsertDesignLanguage>;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { workspace_id, name, raw_json, normalized, source_format, token_count } = body;
|
||||
|
||||
if (!workspace_id || !raw_json || !normalized || !source_format) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: workspace_id, raw_json, normalized, source_format' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!(await assertMember(email, workspace_id))) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
// Determine next version number: current + 1, or 1 for first upload.
|
||||
const existing = await getDesignLanguage(db, workspace_id).catch(() => null);
|
||||
const version = existing ? existing.version + 1 : 1;
|
||||
|
||||
const row: InsertDesignLanguage = {
|
||||
workspace_id,
|
||||
name: name ?? 'Design Language',
|
||||
raw_json,
|
||||
normalized,
|
||||
source_format,
|
||||
token_count: token_count ?? (Array.isArray(normalized) ? normalized.length : 0),
|
||||
version,
|
||||
};
|
||||
|
||||
try {
|
||||
const dl = await upsertDesignLanguage(db, row);
|
||||
|
||||
// Insert a version snapshot — the prune trigger fires after this insert
|
||||
// and deletes any rows beyond the 10-most-recent.
|
||||
await db
|
||||
.from('design_language_versions')
|
||||
.insert({
|
||||
design_language_id: dl.id,
|
||||
version: dl.version,
|
||||
raw_json: dl.raw_json,
|
||||
normalized: dl.normalized,
|
||||
source_format: dl.source_format,
|
||||
});
|
||||
|
||||
return NextResponse.json(dl, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// GET /api/diffs/:id → fetch single diff
|
||||
// PATCH /api/diffs/:id → update status + notes
|
||||
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getDiff, updateDiffStatus } from '@originmain/origin-graph';
|
||||
import type { DiffStatus } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const diff = await getDiff(db, id);
|
||||
return NextResponse.json(diff);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
const status = message.includes('0 rows') ? 404 : 500;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = (await req.json()) as { status: DiffStatus; notes?: string };
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const updated = await updateDiffStatus(db, id, body.status, body.notes);
|
||||
return NextResponse.json(updated);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// GET /api/diffs?artboardId=<uuid> → list diffs for an artboard
|
||||
// POST /api/diffs → create draft diff
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { getDiffs, createDiff } from '@originmain/origin-graph';
|
||||
import type { InsertIntentDiff } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const artboardId = req.nextUrl.searchParams.get('artboardId');
|
||||
if (!artboardId) return NextResponse.json({ error: 'artboardId is required' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const diffs = await getDiffs(db, artboardId);
|
||||
return NextResponse.json(diffs);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const authorEmail = user.primaryEmailAddress?.emailAddress;
|
||||
if (!authorEmail) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const body = (await req.json()) as Omit<InsertIntentDiff, 'author_email'>;
|
||||
const insert: InsertIntentDiff = { ...body, author_email: authorEmail };
|
||||
|
||||
try {
|
||||
const db = serverClient();
|
||||
const diff = await createDiff(db, insert);
|
||||
return NextResponse.json(diff, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* POST /api/intent
|
||||
*
|
||||
* Canvas → Agent Bridge intent push endpoint (spec Phase 4 §8.4).
|
||||
*
|
||||
* The canvas calls this when the designer exports a style diff. This route:
|
||||
* 1. Validates the authenticated user and payload
|
||||
* 2. Persists the intent to the `intent_diffs` Supabase table (durable)
|
||||
* 3. Enqueues the intent in the agent-bridge in-memory pending queue using
|
||||
* the Supabase row UUID so the agent's update_diff_status tool can
|
||||
* reference the same row by id
|
||||
* 4. Returns the intentId (Supabase UUID) so the canvas can track status
|
||||
*
|
||||
* The connected IDE agent drains the queue the next time it calls push_intent
|
||||
* (or receives an INTENT_RECEIVED WebSocket push in Phase 5+).
|
||||
*/
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { storePendingIntent } from '@originmain/agent-bridge';
|
||||
import { createDiff } from '@originmain/origin-graph';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
interface IntentPayload {
|
||||
/** Supabase workspace ID. */
|
||||
workspaceId: string;
|
||||
/** The artboard the diff came from. */
|
||||
artboardId: string;
|
||||
/** Display name of the component that was edited. */
|
||||
componentName: string;
|
||||
/** JSON-serialised StylePatch[] from diff-generator.ts */
|
||||
patchJson: string;
|
||||
/** One of: 'css' | 'prop' | 'tailwind' */
|
||||
strategy: string;
|
||||
/** Optional AI-generated summary sentence. */
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const authorEmail = user.primaryEmailAddress?.emailAddress;
|
||||
if (!authorEmail) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
let body: IntentPayload;
|
||||
try {
|
||||
body = await req.json() as IntentPayload;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { workspaceId, artboardId, componentName, patchJson, strategy, summary } = body;
|
||||
|
||||
if (!workspaceId || !artboardId || !componentName || !patchJson || !strategy) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: workspaceId, artboardId, componentName, patchJson, strategy' },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const validStrategies = ['css', 'prop', 'tailwind'];
|
||||
if (!validStrategies.includes(strategy)) {
|
||||
return NextResponse.json(
|
||||
{ error: `Invalid strategy. Must be one of: ${validStrategies.join(', ')}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// ── 1. Persist to Supabase (durable) ──────────────────────────────────────
|
||||
let intentId: string;
|
||||
try {
|
||||
const db = serverClient();
|
||||
|
||||
// Parse patchJson safely; fall back to wrapping it verbatim so the row
|
||||
// is always writable even if the client sends a malformed payload.
|
||||
let changesRecord: Record<string, unknown>;
|
||||
try {
|
||||
changesRecord = JSON.parse(patchJson) as Record<string, unknown>;
|
||||
// Wrap plain arrays (StylePatch[]) under a "patches" key so the column
|
||||
// type (jsonb object) is satisfied.
|
||||
if (Array.isArray(changesRecord)) {
|
||||
changesRecord = { patches: changesRecord, strategy };
|
||||
}
|
||||
} catch {
|
||||
changesRecord = { raw: patchJson, strategy };
|
||||
}
|
||||
|
||||
const row = await createDiff(db, {
|
||||
artboard_id: artboardId,
|
||||
author_email: authorEmail,
|
||||
changes: changesRecord,
|
||||
aggregate_summary: summary ?? `${componentName} style changes`,
|
||||
status: 'EXPORTED',
|
||||
});
|
||||
|
||||
intentId = row.id;
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: `DB insert failed: ${message}` }, { status: 500 });
|
||||
}
|
||||
|
||||
// ── 2. Enqueue in agent-bridge (real-time poll / WebSocket path) ───────────
|
||||
// Pass the Supabase UUID so update_diff_status can reference the same row.
|
||||
storePendingIntent(workspaceId, {
|
||||
artboardId,
|
||||
componentName,
|
||||
patchJson,
|
||||
strategy,
|
||||
summary: summary ?? '',
|
||||
}, intentId);
|
||||
|
||||
return NextResponse.json({ intentId, status: 'EXPORTED' }, { status: 201 });
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// ── SDK Bridge — canvas commands channel ──────────────────────────────────────
|
||||
//
|
||||
// GET /api/sdk/[projectId]/commands — SDK subscribes (SSE). Receives canvas
|
||||
// commands: PATCH_ELEMENT_STYLE,
|
||||
// REQUEST_ELEMENT_STYLES, SELECT_COMPONENT,
|
||||
// SET_DESIGN_TOKENS, etc.
|
||||
// Auth: Bearer <sdk-token>.
|
||||
//
|
||||
// POST /api/sdk/[projectId]/commands — Canvas pushes a command to the bridge.
|
||||
// Auth: Supabase session cookie.
|
||||
// Body: HostMessage JSON.
|
||||
// The bridge fans the command out to all
|
||||
// connected SDK instances for this project.
|
||||
//
|
||||
// This route handles the Canvas → SDK direction of the bridge.
|
||||
// For SDK → Canvas events see: /api/sdk/[projectId]/route.ts
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { extractSdkToken } from '@/lib/sdk-auth';
|
||||
import {
|
||||
registerSdkSink,
|
||||
pushToSdk,
|
||||
canvasSubscriberCount,
|
||||
} from '@/lib/sdk-bridge-registry';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
// ── GET — SDK subscribes to canvas commands ───────────────────────────────────
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
// Authenticate the SDK via Bearer token.
|
||||
const sdkToken = extractSdkToken(req.headers.get('authorization'));
|
||||
if (!sdkToken) {
|
||||
return new NextResponse('Invalid or missing SDK token', { status: 401 });
|
||||
}
|
||||
if (sdkToken.projectId !== projectId) {
|
||||
return new NextResponse('Token project mismatch', { status: 403 });
|
||||
}
|
||||
|
||||
const enc = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(enc.encode(': sdk-commands connected\n\n'));
|
||||
// Tell the SDK how many canvas tabs are actively subscribed.
|
||||
controller.enqueue(enc.encode(
|
||||
`data: ${JSON.stringify({ type: '__bridge_status__', canvasCount: canvasSubscriberCount(projectId) })}\n\n`,
|
||||
));
|
||||
|
||||
// Register this SDK instance as a sink for canvas commands.
|
||||
const unsubscribe = registerSdkSink(projectId, (chunk) => {
|
||||
try { controller.enqueue(enc.encode(chunk)); } catch { /* disconnected */ }
|
||||
});
|
||||
|
||||
const keepAlive = setInterval(() => {
|
||||
try { controller.enqueue(enc.encode(': keep-alive\n\n')); } catch {
|
||||
clearInterval(keepAlive);
|
||||
}
|
||||
}, 25_000);
|
||||
|
||||
req.signal.addEventListener('abort', () => {
|
||||
clearInterval(keepAlive);
|
||||
unsubscribe();
|
||||
try { controller.close(); } catch { /* already closed */ }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── POST — Canvas pushes a command to the SDK ─────────────────────────────────
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
// Verify the canvas user has a valid session.
|
||||
const authCookie = req.cookies.get('sb-access-token')?.value
|
||||
?? req.headers.get('x-supabase-auth')
|
||||
?? null;
|
||||
|
||||
if (!authCookie) {
|
||||
return NextResponse.json({ error: 'Unauthorized — missing session' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Lightweight project access check.
|
||||
const db = serverClient();
|
||||
const { error: projectError } = await db
|
||||
.from('artboards')
|
||||
.select('id')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
if (projectError) {
|
||||
return NextResponse.json({ error: 'Project not found or access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Parse the command body.
|
||||
let command: Record<string, unknown>;
|
||||
try {
|
||||
command = (await req.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof command.type !== 'string') {
|
||||
return NextResponse.json({ error: 'Missing command.type' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fan out to all SDK SSE subscribers for this project.
|
||||
pushToSdk(projectId, command);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// ── SDK Bridge — fiber events channel ─────────────────────────────────────────
|
||||
//
|
||||
// GET /api/sdk/[projectId] — Canvas subscribes (SSE). Receives SDK events:
|
||||
// READY, FIBER_TREE_UPDATE, ELEMENT_STYLES, etc.
|
||||
// Auth: Supabase session cookie.
|
||||
//
|
||||
// POST /api/sdk/[projectId] — SDK pushes an event to the bridge.
|
||||
// Auth: Bearer <sdk-token>.
|
||||
// Body: RendererMessage JSON.
|
||||
// The bridge fans the event out to all canvas SSE
|
||||
// subscribers for this project.
|
||||
//
|
||||
// This route handles the SDK → Canvas direction of the bridge.
|
||||
// For Canvas → SDK commands see: /api/sdk/[projectId]/commands/route.ts
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { extractSdkToken } from '@/lib/sdk-auth';
|
||||
import {
|
||||
registerCanvasSink,
|
||||
pushToCanvas,
|
||||
sdkSubscriberCount,
|
||||
} from '@/lib/sdk-bridge-registry';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
// ── GET — Canvas subscribes to SDK events ─────────────────────────────────────
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
// Verify the canvas user has access to this project via Supabase session.
|
||||
// We read the session from the cookie — canvas is a browser client.
|
||||
const db = serverClient();
|
||||
const authToken = req.cookies.get('sb-access-token')?.value
|
||||
?? req.headers.get('x-supabase-auth')
|
||||
?? null;
|
||||
|
||||
if (!authToken) {
|
||||
return new NextResponse('Unauthorized — missing session', { status: 401 });
|
||||
}
|
||||
|
||||
// Verify the project exists and belongs to a workspace the user can access.
|
||||
// For now we do a lightweight existence check. Full RLS is enforced by the
|
||||
// service-role client below — swap for auth-user client for full RLS.
|
||||
const { error: projectError } = await db
|
||||
.from('artboards')
|
||||
.select('id')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
if (projectError) {
|
||||
return new NextResponse('Project not found or access denied', { status: 403 });
|
||||
}
|
||||
|
||||
const enc = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// SSE preamble: tells the browser the connection is alive.
|
||||
controller.enqueue(enc.encode(': sdk-bridge connected\n\n'));
|
||||
// Inform the canvas how many SDK instances are currently connected.
|
||||
controller.enqueue(enc.encode(
|
||||
`data: ${JSON.stringify({ type: '__bridge_status__', sdkCount: sdkSubscriberCount(projectId) })}\n\n`,
|
||||
));
|
||||
|
||||
// Register this canvas tab as a sink for SDK events.
|
||||
const unsubscribe = registerCanvasSink(projectId, (chunk) => {
|
||||
try { controller.enqueue(enc.encode(chunk)); } catch { /* disconnected */ }
|
||||
});
|
||||
|
||||
// Keep-alive every 25 s to prevent proxy idle timeouts.
|
||||
const keepAlive = setInterval(() => {
|
||||
try { controller.enqueue(enc.encode(': keep-alive\n\n')); } catch {
|
||||
clearInterval(keepAlive);
|
||||
}
|
||||
}, 25_000);
|
||||
|
||||
req.signal.addEventListener('abort', () => {
|
||||
clearInterval(keepAlive);
|
||||
unsubscribe();
|
||||
try { controller.close(); } catch { /* already closed */ }
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── POST — SDK pushes a fiber event ──────────────────────────────────────────
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const { projectId } = await params;
|
||||
|
||||
// Authenticate the SDK via Bearer token.
|
||||
const sdkToken = extractSdkToken(req.headers.get('authorization'));
|
||||
if (!sdkToken) {
|
||||
return NextResponse.json({ error: 'Invalid or missing SDK token' }, { status: 401 });
|
||||
}
|
||||
|
||||
// The token is scoped to a specific project — verify it matches the URL.
|
||||
if (sdkToken.projectId !== projectId) {
|
||||
return NextResponse.json({ error: 'Token project mismatch' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Parse the SDK event body.
|
||||
let message: Record<string, unknown>;
|
||||
try {
|
||||
message = (await req.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (typeof message.type !== 'string') {
|
||||
return NextResponse.json({ error: 'Missing message.type' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Fan out to all canvas SSE subscribers for this project.
|
||||
pushToCanvas(projectId, message);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// ── SDK token issuance ────────────────────────────────────────────────────────
|
||||
//
|
||||
// POST /api/sdk/token
|
||||
// Body: { projectId: string, workspaceId: string }
|
||||
// Returns: { token: string }
|
||||
//
|
||||
// Issues a signed SDK token scoped to a project. The token is used by
|
||||
// @originmain/dev to authenticate with the bridge:
|
||||
// - POST /api/sdk/[projectId] (SDK → canvas, fiber events)
|
||||
// - GET /api/sdk/[projectId]/commands (SDK ← canvas, edit commands)
|
||||
//
|
||||
// Auth: Supabase session cookie (canvas user must own the project/workspace).
|
||||
//
|
||||
// The token is HMAC-signed (SHA-256) and expires in 90 days. It is NOT stored
|
||||
// in the database — it is self-contained and verifiable without a DB lookup.
|
||||
// To revoke: rotate AGENT_BRIDGE_SECRET (invalidates all outstanding tokens).
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { issueSdkToken } from '@/lib/sdk-auth';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
// Require a canvas session.
|
||||
const authCookie = req.cookies.get('sb-access-token')?.value
|
||||
?? req.headers.get('x-supabase-auth')
|
||||
?? null;
|
||||
|
||||
if (!authCookie) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Parse request body.
|
||||
let body: { projectId?: string; workspaceId?: string };
|
||||
try {
|
||||
body = (await req.json()) as typeof body;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { projectId, workspaceId } = body;
|
||||
if (!projectId || !workspaceId) {
|
||||
return NextResponse.json({ error: 'projectId and workspaceId are required' }, { status: 422 });
|
||||
}
|
||||
|
||||
// Verify the project exists and belongs to the specified workspace.
|
||||
// The service-role client bypasses RLS — so we manually check workspace membership.
|
||||
const db = serverClient();
|
||||
|
||||
const { error: projectError } = await db
|
||||
.from('artboards')
|
||||
.select('id, workspace_id')
|
||||
.eq('id', projectId)
|
||||
.eq('workspace_id', workspaceId)
|
||||
.single();
|
||||
|
||||
if (projectError) {
|
||||
return NextResponse.json({ error: 'Project not found in workspace' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Issue the token.
|
||||
let token: string;
|
||||
try {
|
||||
token = issueSdkToken(projectId, workspaceId);
|
||||
} catch (err) {
|
||||
// AGENT_BRIDGE_SECRET not configured on this server.
|
||||
const message = err instanceof Error ? err.message : 'Token signing failed';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ token });
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// ── tRPC HTTP handler (Next.js App Router) ────────────────────────────────────
|
||||
// Mounts the tRPC appRouter at /api/trpc/* using the fetch adapter.
|
||||
// Both GET (queries) and POST (mutations) are needed.
|
||||
|
||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
import { appRouter } from '@/server/routers/index';
|
||||
import { createTRPCContext } from '@/server/trpc';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
const handler = (req: NextRequest) =>
|
||||
fetchRequestHandler({
|
||||
endpoint: '/api/trpc',
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
onError: ({ path, error }) => {
|
||||
console.error(`tRPC error on /${path}:`, error.message);
|
||||
},
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -1,164 +0,0 @@
|
||||
// POST /api/webhooks/:provider
|
||||
// Ingests webhook events from Linear, Slack, GitHub, and Intercom.
|
||||
// workspaceId must be passed as a query param: ?workspace=<uuid>
|
||||
// Signature verification is skipped gracefully when the secret env var is absent
|
||||
// (useful for local development), but enforced in production.
|
||||
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
linearIngester,
|
||||
slackIngester,
|
||||
githubIngester,
|
||||
intercomIngester,
|
||||
} from '@originmain/integrations';
|
||||
import type { OriginIngester } from '@originmain/integrations';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { createOrigin, createArtboard } from '@originmain/origin-graph';
|
||||
|
||||
// ── Signature helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function hmac(algo: 'sha256' | 'sha1', secret: string, body: Buffer): string {
|
||||
return createHmac(algo, secret).update(body).digest('hex');
|
||||
}
|
||||
|
||||
function safeEqual(a: string, b: string): boolean {
|
||||
try {
|
||||
const ab = Buffer.from(a.padEnd(b.length, '\0'));
|
||||
const bb = Buffer.from(b.padEnd(a.length, '\0'));
|
||||
return timingSafeEqual(ab, bb) && a.length === b.length;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyLinear(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.LINEAR_WEBHOOK_SECRET;
|
||||
if (!secret) return true; // skip verification in dev
|
||||
const sig = headers.get('x-linear-signature') ?? '';
|
||||
return safeEqual(hmac('sha256', secret, rawBody), sig);
|
||||
}
|
||||
|
||||
function verifySlack(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.SLACK_SIGNING_SECRET;
|
||||
if (!secret) return true;
|
||||
const ts = headers.get('x-slack-request-timestamp') ?? '';
|
||||
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
|
||||
const baseString = `v0:${ts}:${rawBody.toString()}`;
|
||||
const expected = `v0=${hmac('sha256', secret, Buffer.from(baseString))}`;
|
||||
const provided = headers.get('x-slack-signature') ?? '';
|
||||
return safeEqual(expected, provided);
|
||||
}
|
||||
|
||||
function verifyGitHub(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.GITHUB_WEBHOOK_SECRET;
|
||||
if (!secret) return true;
|
||||
const expected = `sha256=${hmac('sha256', secret, rawBody)}`;
|
||||
const provided = headers.get('x-hub-signature-256') ?? '';
|
||||
return safeEqual(expected, provided);
|
||||
}
|
||||
|
||||
function verifyIntercom(rawBody: Buffer, headers: Headers): boolean {
|
||||
const secret = process.env.INTERCOM_WEBHOOK_SECRET;
|
||||
if (!secret) return true;
|
||||
const expected = `sha1=${hmac('sha1', secret, rawBody)}`;
|
||||
const provided = headers.get('x-hub-signature') ?? '';
|
||||
return safeEqual(expected, provided);
|
||||
}
|
||||
|
||||
// ── Route config ──────────────────────────────────────────────────────────────
|
||||
|
||||
const INGESTER_MAP: Record<
|
||||
string,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ingester: OriginIngester<any>;
|
||||
verify: (body: Buffer, headers: Headers) => boolean;
|
||||
}
|
||||
> = {
|
||||
linear: { ingester: linearIngester, verify: verifyLinear },
|
||||
slack: { ingester: slackIngester, verify: verifySlack },
|
||||
github: { ingester: githubIngester, verify: verifyGitHub },
|
||||
intercom: { ingester: intercomIngester, verify: verifyIntercom },
|
||||
};
|
||||
|
||||
// ── Handler ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ provider: string }> },
|
||||
) {
|
||||
const { provider } = await params;
|
||||
const entry = INGESTER_MAP[provider];
|
||||
if (!entry) {
|
||||
return NextResponse.json({ error: `Unknown provider: ${provider}` }, { status: 404 });
|
||||
}
|
||||
|
||||
const workspaceId = req.nextUrl.searchParams.get('workspace');
|
||||
if (!workspaceId) {
|
||||
return NextResponse.json({ error: 'workspace query param is required' }, { status: 400 });
|
||||
}
|
||||
// Optional: ?project=<uuid> scopes the created artboard to a specific project
|
||||
const projectId = req.nextUrl.searchParams.get('project');
|
||||
|
||||
const rawBody = Buffer.from(await req.arrayBuffer());
|
||||
|
||||
if (!entry.verify(rawBody, req.headers)) {
|
||||
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(rawBody.toString('utf-8'));
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Slack sends a challenge request during webhook setup — respond immediately.
|
||||
if (
|
||||
provider === 'slack' &&
|
||||
typeof parsed === 'object' &&
|
||||
parsed !== null &&
|
||||
'type' in parsed &&
|
||||
(parsed as { type: string }).type === 'url_verification'
|
||||
) {
|
||||
return NextResponse.json({ challenge: (parsed as { challenge?: string }).challenge ?? '' });
|
||||
}
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = entry.ingester.parsePayload(parsed);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Payload validation failed';
|
||||
return NextResponse.json({ error: message }, { status: 422 });
|
||||
}
|
||||
|
||||
const result = entry.ingester.ingest(payload);
|
||||
const db = serverClient();
|
||||
|
||||
try {
|
||||
const origin = await createOrigin(db, result.origin);
|
||||
|
||||
const meta: Record<string, unknown> = {
|
||||
x: 120,
|
||||
y: 100,
|
||||
width: 360,
|
||||
height: 240,
|
||||
};
|
||||
if (result.renderUrl !== undefined) meta['renderUrl'] = result.renderUrl;
|
||||
|
||||
const artboard = await createArtboard(db, {
|
||||
workspace_id: workspaceId,
|
||||
project_id: projectId,
|
||||
name: result.artboardTitle,
|
||||
origin_id: origin.id,
|
||||
parent_artboard_id: null,
|
||||
metadata_jsonb: meta,
|
||||
});
|
||||
|
||||
return NextResponse.json({ originId: origin.id, artboardId: artboard.id }, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Database error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
// POST /api/workspace/:id/invite → add a member by email address
|
||||
// DELETE /api/workspace/:id/invite → remove a member by email address (owners only)
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { addTeamMember, removeTeamMember } from '@originmain/origin-graph';
|
||||
import type { TeamRole } from '@originmain/origin-graph';
|
||||
|
||||
type Ctx = { params: Promise<{ id: string }> };
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export async function POST(req: NextRequest, { params }: Ctx) {
|
||||
const caller = await currentUser();
|
||||
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const callerEmail = caller.primaryEmailAddress?.emailAddress;
|
||||
if (!callerEmail) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
// Only workspace owners can invite.
|
||||
const { data: ownerRow } = await db
|
||||
.from('workspaces')
|
||||
.select('id')
|
||||
.eq('id', workspaceId)
|
||||
.eq('owner_email', callerEmail)
|
||||
.single();
|
||||
|
||||
if (!ownerRow)
|
||||
return NextResponse.json({ error: 'Forbidden — only owners can invite members' }, { status: 403 });
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as { email?: string; role?: TeamRole };
|
||||
|
||||
if (!body.email || !EMAIL_RE.test(body.email))
|
||||
return NextResponse.json({ error: 'A valid email address is required' }, { status: 400 });
|
||||
|
||||
const inviteEmail = body.email.toLowerCase().trim();
|
||||
const role: TeamRole = body.role ?? 'DESIGNER';
|
||||
|
||||
// Check if already a member.
|
||||
const { data: existing } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', inviteEmail)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (existing) return NextResponse.json({ error: 'User is already a member' }, { status: 409 });
|
||||
|
||||
try {
|
||||
const member = await addTeamMember(db, {
|
||||
workspace_id: workspaceId,
|
||||
email: inviteEmail,
|
||||
role,
|
||||
});
|
||||
return NextResponse.json(member, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest, { params }: Ctx) {
|
||||
const caller = await currentUser();
|
||||
if (!caller) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const callerEmail = caller.primaryEmailAddress?.emailAddress;
|
||||
if (!callerEmail) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as { email?: string };
|
||||
if (!body.email) return NextResponse.json({ error: 'email is required' }, { status: 400 });
|
||||
|
||||
const targetEmail = body.email.toLowerCase().trim();
|
||||
|
||||
// Owners can remove anyone; members can remove themselves.
|
||||
const { data: ownerRow } = await db
|
||||
.from('workspaces')
|
||||
.select('id')
|
||||
.eq('id', workspaceId)
|
||||
.eq('owner_email', callerEmail)
|
||||
.single();
|
||||
|
||||
if (!ownerRow && targetEmail !== callerEmail)
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
try {
|
||||
await removeTeamMember(db, workspaceId, targetEmail);
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// GET /api/workspace/:id/projects/:pid → fetch single project
|
||||
// PATCH /api/workspace/:id/projects/:pid → update name / app_url / framework / description
|
||||
// DELETE /api/workspace/:id/projects/:pid → permanently delete project
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { updateProject, deleteProject } from '@originmain/origin-graph';
|
||||
import type { InsertProject, Project } from '@originmain/origin-graph';
|
||||
|
||||
type Ctx = { params: Promise<{ id: string; pid: string }> };
|
||||
|
||||
async function assertMember(workspaceId: string, email: string): Promise<boolean> {
|
||||
const db = serverClient();
|
||||
const { data } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
return !!data;
|
||||
}
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId, pid } = await params;
|
||||
if (!(await assertMember(workspaceId, email)))
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { data, error } = await serverClient()
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', pid)
|
||||
.eq('workspace_id', workspaceId)
|
||||
.single() as unknown as { data: Project | null; error: { message: string } | null };
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
if (!data) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: Ctx) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId, pid } = await params;
|
||||
if (!(await assertMember(workspaceId, email)))
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as Partial<InsertProject>;
|
||||
const patch: Partial<InsertProject> = {};
|
||||
if (typeof body.name === 'string' && body.name.trim()) patch.name = body.name.trim();
|
||||
if ('description' in body) patch.description = body.description ?? null;
|
||||
if ('app_url' in body) patch.app_url = body.app_url ?? null;
|
||||
if ('framework' in body) patch.framework = body.framework ?? null;
|
||||
|
||||
if (Object.keys(patch).length === 0)
|
||||
return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 });
|
||||
|
||||
try {
|
||||
const updated = await updateProject(serverClient(), pid, patch);
|
||||
return NextResponse.json(updated);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: Ctx) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId, pid } = await params;
|
||||
if (!(await assertMember(workspaceId, email)))
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
try {
|
||||
await deleteProject(serverClient(), pid);
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// GET /api/workspace/[id]/projects — list projects in a workspace
|
||||
// POST /api/workspace/[id]/projects — create a new project
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import type { Project, InsertProject } from '@originmain/origin-graph';
|
||||
|
||||
async function assertMember(
|
||||
db: ReturnType<typeof serverClient>,
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
) {
|
||||
const { data } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
return !!data;
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
if (!(await assertMember(db, workspaceId, email)))
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { data, error } = await db
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json(data as Project[]);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
if (!(await assertMember(db, workspaceId, email)))
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : null;
|
||||
if (!name) return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
||||
|
||||
const insert: InsertProject = {
|
||||
workspace_id: workspaceId,
|
||||
name,
|
||||
description: typeof body.description === 'string' ? body.description.trim() || null : null,
|
||||
app_url: typeof body.app_url === 'string' ? body.app_url.trim() || null : null,
|
||||
framework: typeof body.framework === 'string' ? body.framework.trim() || null : null,
|
||||
};
|
||||
|
||||
const { data, error } = await db
|
||||
.from('projects')
|
||||
.insert(insert)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json(data as Project, { status: 201 });
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// GET /api/workspace/:id → fetch workspace (any member)
|
||||
// PATCH /api/workspace/:id → rename workspace (owner only)
|
||||
// DELETE /api/workspace/:id → delete workspace and all its data (owner only)
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { deleteWorkspace } from '@originmain/origin-graph';
|
||||
import type { Workspace } from '@originmain/origin-graph';
|
||||
|
||||
type RouteContext = { params: Promise<{ id: string }> };
|
||||
|
||||
async function getCallerEmail(): Promise<string | null> {
|
||||
const user = await currentUser();
|
||||
return user?.primaryEmailAddress?.emailAddress ?? null;
|
||||
}
|
||||
|
||||
async function assertOwner(
|
||||
db: ReturnType<typeof serverClient>,
|
||||
workspaceId: string,
|
||||
email: string,
|
||||
) {
|
||||
const { data } = await db
|
||||
.from('team_members')
|
||||
.select('role')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
return (data as { role: string } | null)?.role === 'OWNER';
|
||||
}
|
||||
|
||||
export async function GET(_req: NextRequest, { params }: RouteContext) {
|
||||
const email = await getCallerEmail();
|
||||
if (!email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
// Verify membership
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', id)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!member) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { data, error } = await db.from('workspaces').select('*').eq('id', id).single();
|
||||
if (error || !data) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
return NextResponse.json(data as Workspace);
|
||||
}
|
||||
|
||||
export async function PATCH(req: NextRequest, { params }: RouteContext) {
|
||||
const email = await getCallerEmail();
|
||||
if (!email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
const isOwner = await assertOwner(db, id, email);
|
||||
if (!isOwner) return NextResponse.json({ error: 'Only workspace owners can rename' }, { status: 403 });
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as { name?: string };
|
||||
if (typeof body.name !== 'string' || !body.name.trim()) {
|
||||
return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data, error } = await db
|
||||
.from('workspaces')
|
||||
.update({ name: body.name.trim() })
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error || !data) return NextResponse.json({ error: error?.message ?? 'Update failed' }, { status: 500 });
|
||||
|
||||
return NextResponse.json(data as Workspace);
|
||||
}
|
||||
|
||||
export async function DELETE(_req: NextRequest, { params }: RouteContext) {
|
||||
const email = await getCallerEmail();
|
||||
if (!email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
const isOwner = await assertOwner(db, id, email);
|
||||
if (!isOwner) return NextResponse.json({ error: 'Only workspace owners can delete' }, { status: 403 });
|
||||
|
||||
try {
|
||||
await deleteWorkspace(db, id);
|
||||
return new NextResponse(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Delete failed';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// POST /api/workspace/:id/tokens
|
||||
// Issues a signed HMAC workspace token for IDE integrations (Cursor, Claude Code).
|
||||
// Returns the token plus ready-to-paste config snippets for each IDE.
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import {
|
||||
issueWorkspaceToken,
|
||||
generateCursorConfig,
|
||||
generateClaudeCodeConfig,
|
||||
} from '@originmain/agent-bridge';
|
||||
import type { AgentType } from '@originmain/agent-bridge';
|
||||
|
||||
export async function POST(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const { id: workspaceId } = await params;
|
||||
const db = serverClient();
|
||||
|
||||
// Verify membership.
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', workspaceId)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!member) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
agentType?: AgentType;
|
||||
workspaceName?: string;
|
||||
projectName?: string;
|
||||
};
|
||||
|
||||
const agentType: AgentType = body.agentType ?? 'GENERIC';
|
||||
const workspaceName = body.workspaceName ?? 'My Workspace';
|
||||
|
||||
try {
|
||||
const token = issueWorkspaceToken(workspaceId, agentType);
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
|
||||
const mcpServerUrl = `${appUrl}/api/agent-bridge`;
|
||||
|
||||
const cursorConfig = generateCursorConfig({ mcpServerUrl, workspaceToken: token, workspaceName });
|
||||
const claudeCodeConfig = generateClaudeCodeConfig({
|
||||
mcpServerUrl,
|
||||
workspaceToken: token,
|
||||
workspaceName,
|
||||
...(body.projectName !== undefined ? { projectName: body.projectName } : {}),
|
||||
});
|
||||
|
||||
return NextResponse.json({ token, cursorConfig, claudeCodeConfig });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Token generation failed';
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// GET /api/workspace
|
||||
// Returns the authenticated user's workspace. Auto-creates one on first visit
|
||||
// (FREE plan, name derived from Clerk user display name or email).
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import type { Workspace, InsertWorkspace } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET() {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
// Look up an existing workspace where this user is a member.
|
||||
const { data: existing } = await db
|
||||
.from('team_members')
|
||||
.select('workspace_id')
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single() as unknown as { data: { workspace_id: string } | null };
|
||||
|
||||
if (existing) {
|
||||
const { data: ws, error } = await db
|
||||
.from('workspaces')
|
||||
.select('*')
|
||||
.eq('id', existing.workspace_id)
|
||||
.single();
|
||||
if (!error && ws) return NextResponse.json(ws as Workspace);
|
||||
}
|
||||
|
||||
// No workspace yet — auto-create one with a friendly name.
|
||||
const name = user.fullName ?? email;
|
||||
|
||||
const insert: InsertWorkspace = {
|
||||
owner_email: email,
|
||||
name,
|
||||
plan: 'FREE',
|
||||
settings_jsonb: {},
|
||||
};
|
||||
|
||||
const { data: created, error: insertError } = await db
|
||||
.from('workspaces')
|
||||
.insert(insert)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertError) {
|
||||
return NextResponse.json({ error: insertError.message }, { status: 500 });
|
||||
}
|
||||
|
||||
// Add the owner as a team member so GET /api/workspaces can find it.
|
||||
await db.from('team_members').insert({
|
||||
workspace_id: (created as Workspace).id,
|
||||
email,
|
||||
role: 'OWNER',
|
||||
});
|
||||
|
||||
return NextResponse.json(created as Workspace, { status: 201 });
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// GET /api/workspaces — list every workspace the signed-in user belongs to
|
||||
// POST /api/workspaces — create a new workspace + add creator as OWNER
|
||||
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import type { Workspace, InsertWorkspace } from '@originmain/origin-graph';
|
||||
|
||||
export async function GET() {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
// Fetch all workspace IDs the user is a member of, then join workspaces.
|
||||
const { data: memberships, error: mErr } = await db
|
||||
.from('team_members')
|
||||
.select('workspace_id')
|
||||
.eq('email', email);
|
||||
|
||||
if (mErr) return NextResponse.json({ error: mErr.message }, { status: 500 });
|
||||
|
||||
const ids = (memberships ?? []).map((m) => (m as { workspace_id: string }).workspace_id);
|
||||
if (ids.length === 0) return NextResponse.json([]);
|
||||
|
||||
const { data: workspaces, error: wErr } = await db
|
||||
.from('workspaces')
|
||||
.select('*')
|
||||
.in('id', ids)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (wErr) return NextResponse.json({ error: wErr.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json(workspaces as Workspace[]);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress;
|
||||
if (!email) return NextResponse.json({ error: 'No verified email on account' }, { status: 401 });
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const name = typeof body.name === 'string' && body.name.trim()
|
||||
? body.name.trim()
|
||||
: null;
|
||||
|
||||
if (!name) return NextResponse.json({ error: 'name is required' }, { status: 400 });
|
||||
|
||||
const db = serverClient();
|
||||
|
||||
const insert: InsertWorkspace = {
|
||||
owner_email: email,
|
||||
name,
|
||||
plan: 'FREE',
|
||||
settings_jsonb: {},
|
||||
};
|
||||
|
||||
const { data: created, error: insertErr } = await db
|
||||
.from('workspaces')
|
||||
.insert(insert)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (insertErr) return NextResponse.json({ error: insertErr.message }, { status: 500 });
|
||||
|
||||
const workspace = created as Workspace;
|
||||
|
||||
// Add creator as OWNER team member so GET /api/workspaces can find it.
|
||||
await db.from('team_members').insert({
|
||||
workspace_id: workspace.id,
|
||||
email,
|
||||
role: 'OWNER',
|
||||
});
|
||||
|
||||
return NextResponse.json(workspace, { status: 201 });
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// The canvas now lives at /workspace/[wid]/project/[pid].
|
||||
// Redirect anyone hitting the old /canvas route to their workspace list.
|
||||
export default function CanvasPage() {
|
||||
redirect('/workspaces');
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('[GlobalError]', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body style={{
|
||||
margin: 0, minHeight: '100vh',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
background: 'var(--page-bg)',
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
padding: 24,
|
||||
}}>
|
||||
<div style={{
|
||||
background: 'var(--card-bg)', border: '1px solid rgba(220,38,38,0.15)',
|
||||
borderRadius: 16, padding: '44px 48px', maxWidth: 480, width: '100%',
|
||||
boxShadow: '0 4px 24px rgba(0,0,0,0.06)', textAlign: 'center',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 12,
|
||||
background: 'rgba(220,38,38,0.07)', border: '1px solid rgba(220,38,38,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
margin: '0 auto 20px',
|
||||
}}>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 6v4M10 14h.01" stroke="#DC2626" strokeWidth="1.8" strokeLinecap="round"/>
|
||||
<circle cx="10" cy="10" r="8.5" stroke="#DC2626" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 style={{ fontSize: '1.125rem', fontWeight: 700, color: 'var(--card-text)', margin: '0 0 8px', letterSpacing: '-0.02em' }}>
|
||||
Something went wrong
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--card-muted)', margin: '0 0 28px', lineHeight: 1.6 }}>
|
||||
{error.message || 'An unexpected error occurred.'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
padding: '9px 20px', borderRadius: 8,
|
||||
fontSize: '0.875rem', fontWeight: 600,
|
||||
background: 'var(--btn-bg)', color: 'var(--btn-fg)', border: 'none',
|
||||
cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
<a
|
||||
href="/workspaces"
|
||||
style={{
|
||||
padding: '9px 20px', borderRadius: 8,
|
||||
fontSize: '0.875rem', fontWeight: 600,
|
||||
background: 'var(--card-subtle)', color: 'var(--btn-idle-fg)',
|
||||
border: 'none', textDecoration: 'none',
|
||||
display: 'inline-flex', alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Back to workspaces
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* overflow: hidden is applied by AppChrome directly on its root div
|
||||
for the canvas route only. Setting it globally here would break
|
||||
scrolling on settings, workspace, and other non-canvas pages. */
|
||||
|
||||
/* ── Theme CSS variables ─────────────────────────────────────────────────────
|
||||
--page-bg : background of workspace / settings / plugin shell pages
|
||||
--page-text: primary text colour on those pages
|
||||
The canvas editor (AppChrome) uses its own inline dark styles and does not
|
||||
consume these variables. Toggling data-theme on <html> via Providers is
|
||||
enough for all shell pages to respond. */
|
||||
|
||||
:root {
|
||||
--page-bg: #FAFAFA;
|
||||
--page-text: #0A0A0A;
|
||||
/* Cards / sections */
|
||||
--card-bg: #FFFFFF;
|
||||
--card-border: rgba(0,0,0,0.08);
|
||||
--card-text: #0A0A0A;
|
||||
--card-muted: #71717A;
|
||||
--card-subtle: #F4F4F5;
|
||||
--card-icon-bg: #0A0A0A;
|
||||
--card-icon-fg: #FFFFFF;
|
||||
/* Inputs / selects */
|
||||
--input-bg: #F8F8F8;
|
||||
--input-border: rgba(0,0,0,0.12);
|
||||
--input-text: #0A0A0A;
|
||||
/* Buttons */
|
||||
--btn-bg: #0A0A0A;
|
||||
--btn-fg: #FFFFFF;
|
||||
--btn-idle-bg: #FFFFFF;
|
||||
--btn-idle-fg: #52525B;
|
||||
--btn-idle-border: rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--page-bg: #0C0C10;
|
||||
--page-text: #FAFAFA;
|
||||
/* Cards / sections */
|
||||
--card-bg: #16161C;
|
||||
--card-border: rgba(255,255,255,0.07);
|
||||
--card-text: rgba(255,255,255,0.88);
|
||||
--card-muted: rgba(255,255,255,0.42);
|
||||
--card-subtle: rgba(255,255,255,0.06);
|
||||
--card-icon-bg: rgba(255,255,255,0.10);
|
||||
--card-icon-fg: rgba(255,255,255,0.88);
|
||||
/* Inputs / selects */
|
||||
--input-bg: rgba(255,255,255,0.05);
|
||||
--input-border: rgba(255,255,255,0.11);
|
||||
--input-text: rgba(255,255,255,0.88);
|
||||
/* Buttons */
|
||||
--btn-bg: rgba(255,255,255,0.92);
|
||||
--btn-fg: #0A0A0A;
|
||||
--btn-idle-bg: rgba(255,255,255,0.05);
|
||||
--btn-idle-fg: rgba(255,255,255,0.55);
|
||||
--btn-idle-border: rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI Variable', 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
background: var(--page-bg);
|
||||
color: var(--page-text);
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
/* ── Accessible focus indicators ────────────────────────────────────────────
|
||||
Inline styles set outline:none to suppress browser defaults, but without a
|
||||
replacement this breaks keyboard navigation. :focus-visible adds a visible
|
||||
ring only for keyboard focus (not mouse clicks) across the entire app.
|
||||
The ring colour adapts to context: blue for light-bg pages (#0066FF),
|
||||
and a lighter blue for dark-bg panels (#3385FF / canvas inspector etc.).
|
||||
We use !important to override inline outline:none intentionally. */
|
||||
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid #0066FF !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
/* Canvas panels sit on a dark background — the ring matches the panel accent */
|
||||
.dark-panel input:focus-visible,
|
||||
.dark-panel textarea:focus-visible {
|
||||
outline-color: #3385FF !important;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { ClerkProvider } from '@clerk/nextjs';
|
||||
import { Providers } from './providers';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Originmain',
|
||||
description: 'AI-Native Design Engineering Platform',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{/* signInForceRedirectUrl / signUpForceRedirectUrl override any redirect_url
|
||||
param that OAuth providers (GitHub, Google) may inject — ensuring we
|
||||
always land on the right page regardless of the OAuth callback URL. */}
|
||||
<ClerkProvider
|
||||
signInForceRedirectUrl="/workspaces"
|
||||
signUpForceRedirectUrl="/onboarding"
|
||||
>
|
||||
<Providers>{children}</Providers>
|
||||
</ClerkProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,855 +0,0 @@
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
MARKETING PAGE — design tokens, components, sections
|
||||
Imported by app/page.tsx (marketing route).
|
||||
All CSS variables here use the --bg/--fg namespace which is
|
||||
separate from the --page-bg/--page-text namespace used by
|
||||
the shell pages. Both namespaces respond to [data-theme="dark"]
|
||||
set by providers.tsx on <html>.
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Design tokens ─────────────────────────────────────────── */
|
||||
:root {
|
||||
--bg: #FFFFFF;
|
||||
--bg-subtle: #FAFAFA;
|
||||
--bg-muted: #F4F4F5;
|
||||
--bg-strong: #EBEBEB;
|
||||
--bg-inv: #0A0A0A;
|
||||
--bg-inv-2: #141414;
|
||||
--bg-inv-3: #1C1C1C;
|
||||
|
||||
--fg: #0A0A0A;
|
||||
--fg-2: #52525B;
|
||||
--fg-3: #A1A1AA;
|
||||
--fg-4: #D4D4D8;
|
||||
--fg-inv: rgba(255,255,255,0.92);
|
||||
--fg-inv-2: rgba(255,255,255,0.52);
|
||||
--fg-inv-3: rgba(255,255,255,0.28);
|
||||
|
||||
--blue: #0066FF;
|
||||
--blue-dark: #0050CC;
|
||||
--blue-light: #3385FF;
|
||||
--blue-soft: rgba(0,102,255,0.07);
|
||||
--blue-mid: rgba(0,102,255,0.15);
|
||||
--purple: #7C3AED;
|
||||
--teal: #0891B2;
|
||||
|
||||
--border: rgba(0,0,0,0.07);
|
||||
--border-2: rgba(0,0,0,0.12);
|
||||
--border-inv: rgba(255,255,255,0.08);
|
||||
--border-inv-2:rgba(255,255,255,0.14);
|
||||
|
||||
--r-1: 4px;
|
||||
--r-2: 8px;
|
||||
--r-3: 12px;
|
||||
--r-4: 16px;
|
||||
--r-5: 20px;
|
||||
--r-6: 24px;
|
||||
--r-full: 9999px;
|
||||
|
||||
--sh-xs: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--sh-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.05);
|
||||
--sh-md: 0 4px 8px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04);
|
||||
--sh-lg: 0 10px 24px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.05);
|
||||
--sh-xl: 0 20px 40px rgba(0,0,0,0.09), 0 8px 16px rgba(0,0,0,0.05);
|
||||
--sh-2xl:0 32px 64px rgba(0,0,0,0.11), 0 16px 32px rgba(0,0,0,0.06);
|
||||
--sh-product: 0 48px 96px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.05);
|
||||
|
||||
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--mono: 'JetBrains Mono', 'SF Mono', ui-monospace, 'Cascadia Code', monospace;
|
||||
}
|
||||
|
||||
/* ── Dark mode token overrides ─────────────────────────────── */
|
||||
[data-theme="dark"] {
|
||||
--bg: #0A0A0A;
|
||||
--bg-subtle: #0F0F11;
|
||||
--bg-muted: #161618;
|
||||
--bg-strong: #1C1C1E;
|
||||
--bg-inv: #F4F4F5;
|
||||
--bg-inv-2: #E4E4E7;
|
||||
--bg-inv-3: #D4D4D8;
|
||||
|
||||
--fg: rgba(255,255,255,0.92);
|
||||
--fg-2: rgba(255,255,255,0.58);
|
||||
--fg-3: rgba(255,255,255,0.34);
|
||||
--fg-4: rgba(255,255,255,0.16);
|
||||
--fg-inv: #09090B;
|
||||
--fg-inv-2: rgba(0,0,0,0.62);
|
||||
--fg-inv-3: rgba(0,0,0,0.35);
|
||||
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--border-2: rgba(255,255,255,0.12);
|
||||
--border-inv: rgba(0,0,0,0.08);
|
||||
--border-inv-2:rgba(0,0,0,0.14);
|
||||
|
||||
--sh-xs: 0 1px 2px rgba(0,0,0,0.3);
|
||||
--sh-sm: 0 1px 3px rgba(0,0,0,0.35), 0 1px 2px rgba(0,0,0,0.25);
|
||||
--sh-md: 0 4px 8px rgba(0,0,0,0.32), 0 2px 4px rgba(0,0,0,0.22);
|
||||
--sh-lg: 0 10px 24px rgba(0,0,0,0.38), 0 4px 8px rgba(0,0,0,0.26);
|
||||
--sh-xl: 0 20px 40px rgba(0,0,0,0.42), 0 8px 16px rgba(0,0,0,0.28);
|
||||
--sh-2xl:0 32px 64px rgba(0,0,0,0.48), 0 16px 32px rgba(0,0,0,0.32);
|
||||
--sh-product: 0 48px 96px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
/* ── Dark-mode comparison card hero fix ────────────────────────
|
||||
The hero card has a hardcoded #06060E bg. In dark mode (--bg = #0A0A0A)
|
||||
it vanishes against the page. Lift it to the elevated surface token. */
|
||||
[data-theme="dark"] .mkt-cmp-card-hero {
|
||||
background: var(--bg-strong);
|
||||
box-shadow: 0 0 0 1px rgba(51,133,255,0.28), var(--sh-xl);
|
||||
}
|
||||
|
||||
/* ── Dark-mode button corrections ──────────────────────────────
|
||||
The default --border-2 token (rgba(255,255,255,0.12)) is too faint
|
||||
on near-black backgrounds. Lift secondary button borders and hover
|
||||
states so they're clearly legible without the light-mode definition. */
|
||||
[data-theme="dark"] .mkt-btn-secondary {
|
||||
border-color: rgba(255,255,255,0.22);
|
||||
}
|
||||
[data-theme="dark"] .mkt-btn-secondary:hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-color: rgba(255,255,255,0.32);
|
||||
}
|
||||
|
||||
/* Primary-button ::after hover overlay is white-on-white in dark mode
|
||||
(white button, white overlay = invisible). Flip it to a dark tint. */
|
||||
[data-theme="dark"] .mkt-btn-primary::after,
|
||||
[data-theme="dark"] .mkt-btn-white::after {
|
||||
background: rgba(0,0,0,0) !important;
|
||||
}
|
||||
[data-theme="dark"] .mkt-btn-primary:hover::after,
|
||||
[data-theme="dark"] .mkt-btn-white:hover::after {
|
||||
background: rgba(0,0,0,0.08) !important;
|
||||
}
|
||||
|
||||
/* ── Marketing page body reset ─────────────────────────────── */
|
||||
.mkt-root {
|
||||
font-family: var(--font);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.mkt-root a { text-decoration: none; }
|
||||
.mkt-root a:not(.mkt-btn) { color: inherit; }
|
||||
.mkt-root button { font-family: inherit; cursor: pointer; border: none; background: none; }
|
||||
.mkt-root img, .mkt-root svg { display: block; max-width: 100%; }
|
||||
|
||||
/* ── Layout ────────────────────────────────────────────────── */
|
||||
.mkt-wrap { max-width: 1120px; margin: 0 auto; padding: 0 40px; }
|
||||
.mkt-wrap-wide { max-width: 1280px; margin: 0 auto; padding: 0 40px; }
|
||||
.mkt-section { padding: 128px 0; }
|
||||
.mkt-section-muted { background: var(--bg-subtle); }
|
||||
.mkt-section-dark { background: var(--bg-inv); }
|
||||
|
||||
/* ── Type scale ────────────────────────────────────────────── */
|
||||
.mkt-d1 {
|
||||
font-size: clamp(3rem, 7.5vw, 5.5rem);
|
||||
font-weight: 900;
|
||||
line-height: 1.0;
|
||||
letter-spacing: -0.045em;
|
||||
color: var(--fg);
|
||||
}
|
||||
.mkt-d2 {
|
||||
font-size: clamp(2rem, 4vw, 3rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.038em;
|
||||
color: var(--fg);
|
||||
}
|
||||
.mkt-d3 {
|
||||
font-size: clamp(1.25rem, 2.5vw, 1.625rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.028em;
|
||||
color: var(--fg);
|
||||
}
|
||||
.mkt-subtitle {
|
||||
font-size: clamp(1rem, 2vw, 1.1875rem);
|
||||
font-weight: 400;
|
||||
line-height: 1.7;
|
||||
color: var(--fg-2);
|
||||
}
|
||||
.mkt-label {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fg-3);
|
||||
}
|
||||
.mkt-label-blue { color: var(--blue); }
|
||||
|
||||
/* ── Buttons ───────────────────────────────────────────────── */
|
||||
.mkt-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: var(--font);
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
padding: 11px 22px;
|
||||
border-radius: var(--r-2);
|
||||
transition: transform 0.12s cubic-bezier(.16,1,.3,1), opacity 0.12s, box-shadow 0.12s;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.01em;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mkt-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255,255,255,0);
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.mkt-btn:hover::after { background: rgba(255,255,255,0.06); }
|
||||
.mkt-btn:active { transform: scale(0.98) !important; }
|
||||
|
||||
.mkt-btn-primary {
|
||||
background: var(--fg);
|
||||
color: var(--fg-inv);
|
||||
box-shadow: var(--sh-sm);
|
||||
}
|
||||
.mkt-btn-primary:hover { transform: translateY(-1px); box-shadow: var(--sh-md); }
|
||||
|
||||
.mkt-btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border-2);
|
||||
}
|
||||
.mkt-btn-secondary:hover { background: var(--bg-muted); transform: translateY(-1px); }
|
||||
|
||||
.mkt-btn-blue {
|
||||
background: var(--blue);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,102,255,0.3);
|
||||
}
|
||||
.mkt-btn-blue:hover { background: var(--blue-dark); transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0,102,255,0.35); }
|
||||
|
||||
.mkt-btn-white {
|
||||
background: #fff;
|
||||
color: #0A0A0A;
|
||||
box-shadow: 0 4px 12px rgba(255,255,255,0.15);
|
||||
}
|
||||
.mkt-btn-white:hover { background: #F5F5F5; transform: translateY(-1px); box-shadow: 0 6px 20px rgba(255,255,255,0.2); }
|
||||
|
||||
.mkt-btn-outline-white {
|
||||
background: transparent;
|
||||
color: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(255,255,255,0.28);
|
||||
}
|
||||
.mkt-btn-outline-white:hover { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.96); border-color: rgba(255,255,255,0.42); transform: translateY(-1px); }
|
||||
|
||||
.mkt-btn-lg { font-size: 1rem; padding: 13px 28px; }
|
||||
.mkt-btn-xl { font-size: 1.0625rem; padding: 15px 34px; border-radius: var(--r-3); }
|
||||
.mkt-btn-full { width: 100%; justify-content: center; }
|
||||
|
||||
/* ── Badge / Pills ─────────────────────────────────────────── */
|
||||
.mkt-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
padding: 5px 12px;
|
||||
border-radius: var(--r-full);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-subtle);
|
||||
color: var(--fg-2);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.mkt-badge-new {
|
||||
background: linear-gradient(135deg, rgba(124,58,237,0.08), rgba(0,102,255,0.08));
|
||||
border-color: rgba(124,58,237,0.2);
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
/* ── Section header ────────────────────────────────────────── */
|
||||
.mkt-section-head { text-align: center; max-width: 640px; margin: 0 auto 72px; }
|
||||
.mkt-section-head .mkt-label { display: block; margin-bottom: 16px; }
|
||||
.mkt-section-head .mkt-d2 { margin-bottom: 18px; }
|
||||
.mkt-section-head .mkt-subtitle { margin: 0 auto; }
|
||||
|
||||
/* ── Navigation ────────────────────────────────────────────── */
|
||||
#mkt-nav {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 200;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 40px;
|
||||
transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
#mkt-nav.scrolled {
|
||||
/* color-mix collapses the light/dark hardcoded rgba pairs into one token-aware rule */
|
||||
background: color-mix(in srgb, var(--bg) 88%, transparent);
|
||||
backdrop-filter: blur(20px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--sh-xs);
|
||||
}
|
||||
.mkt-nav-inner {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
.mkt-nav-logo { justify-self: start; }
|
||||
.mkt-nav-links { justify-self: center; }
|
||||
.mkt-nav-actions{ justify-self: end; }
|
||||
.mkt-nav-logo {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
.mkt-nav-logo span { color: var(--blue); }
|
||||
.mkt-nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.mkt-nav-links a {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: var(--fg-2);
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--r-1);
|
||||
transition: color 0.12s, background 0.12s;
|
||||
letter-spacing: -0.01em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.mkt-nav-links a:hover { color: var(--fg); }
|
||||
.mkt-nav-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.mkt-nav-theme-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: var(--r-2);
|
||||
background: none;
|
||||
border: 1px solid var(--border-2);
|
||||
color: var(--fg-3);
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mkt-nav-theme-btn:hover { background: var(--bg-muted); color: var(--fg); border-color: var(--border-2); }
|
||||
|
||||
/* Hamburger */
|
||||
.mkt-nav-burger {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--r-2);
|
||||
transition: background 0.12s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mkt-nav-burger:hover { background: var(--bg-muted); }
|
||||
.mkt-nav-burger span {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: var(--fg);
|
||||
border-radius: 2px;
|
||||
transition: transform 0.22s cubic-bezier(0.16,1,0.3,1), opacity 0.18s;
|
||||
transform-origin: center;
|
||||
}
|
||||
.mkt-nav-burger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
|
||||
.mkt-nav-burger.open span:nth-child(2) { opacity: 0; transform: scaleX(0); }
|
||||
.mkt-nav-burger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
|
||||
|
||||
/* Mobile drawer */
|
||||
.mkt-nav-mobile {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 60px; left: 0; right: 0;
|
||||
z-index: 199;
|
||||
background: color-mix(in srgb, var(--bg) 97%, transparent);
|
||||
backdrop-filter: blur(20px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--sh-lg);
|
||||
padding: 12px 24px 24px;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: calc(100dvh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.mkt-nav-mobile.open { display: flex; }
|
||||
.mkt-nav-mobile ul { list-style: none; display: flex; flex-direction: column; }
|
||||
.mkt-nav-mobile ul li a {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--fg-2);
|
||||
padding: 11px 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
letter-spacing: -0.01em;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
.mkt-nav-mobile ul li:last-child a { border-bottom: none; }
|
||||
.mkt-nav-mobile ul li a:hover { color: var(--fg); }
|
||||
.mkt-nav-mobile-ctas {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.mkt-nav-mobile-ctas .mkt-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
padding: 10px 12px;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
/* Nav dropdown */
|
||||
.mkt-nav-dropdown { position: relative; }
|
||||
.mkt-nav-dd-caret { transition: transform 0.15s; }
|
||||
.mkt-nav-dropdown.open .mkt-nav-dd-caret { transform: rotate(180deg); }
|
||||
.mkt-nav-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-4px);
|
||||
/* --bg-subtle gives the dropdown a slight surface lift vs the page bg */
|
||||
background: color-mix(in srgb, var(--bg-subtle) 98%, transparent);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-3);
|
||||
box-shadow: var(--sh-xl);
|
||||
padding: 10px 8px 8px;
|
||||
min-width: 248px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mkt-nav-dropdown-menu.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
.mkt-nav-mobile-sub { display: none; padding: 2px 0 6px 14px; flex-direction: column; }
|
||||
.mkt-nav-mobile-sub.open { display: flex; }
|
||||
.mkt-nav-mobile-sub a { font-size: 0.875rem; color: var(--fg-3); padding: 8px 4px; border-bottom: 1px solid var(--border); letter-spacing: -0.01em; transition: color 0.1s; }
|
||||
.mkt-nav-mobile-sub a:last-child { border-bottom: none; }
|
||||
.mkt-nav-mobile-sub a:hover { color: var(--fg); }
|
||||
.mkt-nav-dd-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
border-radius: var(--r-2);
|
||||
transition: background 0.1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.mkt-nav-dd-item:hover { background: var(--bg-muted); }
|
||||
.mkt-nav-dd-icon {
|
||||
width: 30px; height: 30px;
|
||||
background: var(--blue-soft);
|
||||
border: 1px solid var(--blue-mid);
|
||||
border-radius: var(--r-2);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--blue);
|
||||
}
|
||||
.mkt-nav-dd-text { display: flex; flex-direction: column; }
|
||||
.mkt-nav-dd-name { font-size: 0.8125rem; font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
|
||||
.mkt-nav-dd-desc { font-size: 0.6875rem; color: var(--fg-3); margin-top: 1px; line-height: 1.4; }
|
||||
.mkt-nav-dd-sep { height: 1px; background: var(--border); margin: 4px 0; }
|
||||
|
||||
/* ── Hero ───────────────────────────────────────────────────── */
|
||||
#mkt-hero {
|
||||
padding: 168px 40px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mkt-hero-noise {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 50% at 50% -10%, rgba(0,102,255,0.045) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 40% 30% at 80% 20%, rgba(124,58,237,0.03) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.mkt-hero-badge { margin-bottom: 32px; }
|
||||
.mkt-hero-hl { max-width: 920px; margin-bottom: 24px; }
|
||||
.mkt-hero-sub { max-width: 520px; margin: 0 auto 48px; }
|
||||
.mkt-hero-ctas { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 88px; }
|
||||
.mkt-hero-note {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--fg-3);
|
||||
margin-top: -72px;
|
||||
margin-bottom: 56px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ── Product shell ──────────────────────────────────────────── */
|
||||
.mkt-product-shell {
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
border-radius: var(--r-4) var(--r-4) 0 0;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--sh-product);
|
||||
border: 1px solid var(--border-inv);
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
}
|
||||
.mkt-p-chrome {
|
||||
background: #1C1C22;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mkt-p-dot { width: 12px; height: 12px; border-radius: 50%; }
|
||||
.mkt-p-dot-r { background: #FF5F57; }
|
||||
.mkt-p-dot-y { background: #FFBD2E; }
|
||||
.mkt-p-dot-g { background: #28C840; }
|
||||
.mkt-p-url {
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 6px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.6875rem;
|
||||
color: rgba(255,255,255,0.35);
|
||||
max-width: 360px;
|
||||
}
|
||||
.mkt-p-lock { font-size: 0.625rem; margin-right: 6px; color: rgba(255,255,255,0.25); }
|
||||
.mkt-p-app {
|
||||
display: grid;
|
||||
grid-template-rows: 44px 1fr;
|
||||
grid-template-columns: 220px 1fr 284px;
|
||||
height: 560px;
|
||||
background: #0D0D11;
|
||||
}
|
||||
.mkt-p-toolbar {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
background: #141418;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.055);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px;
|
||||
gap: 6px;
|
||||
}
|
||||
.mkt-p-wordmark { font-size: 0.875rem; font-weight: 800; letter-spacing: -0.04em; color: rgba(255,255,255,0.9); padding: 0 10px; margin-right: 4px; }
|
||||
.mkt-p-wordmark span { color: #3385FF; }
|
||||
.mkt-p-tb-sep { width: 1px; height: 20px; background: rgba(255,255,255,0.08); margin: 0 4px; }
|
||||
.mkt-p-tb-group { display: flex; align-items: center; gap: 2px; }
|
||||
.mkt-p-tb-btn { width: 30px; height: 30px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 0.8125rem; color: rgba(255,255,255,0.38); cursor: default; }
|
||||
.mkt-p-tb-btn-active { background: rgba(51,133,255,0.15); color: #3385FF; }
|
||||
.mkt-p-tb-btn-on { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7); }
|
||||
.mkt-p-zoom { font-family: var(--mono); font-size: 0.6875rem; color: rgba(255,255,255,0.4); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 3px 8px; cursor: default; min-width: 44px; text-align: center; }
|
||||
.mkt-p-live { margin-left: auto; display: flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 6px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.2); }
|
||||
.mkt-p-live-dot { width: 6px; height: 6px; border-radius: 50%; background: #10B981; box-shadow: 0 0 0 2px rgba(16,185,129,0.25); animation: mkt-pulse-dot 2s ease-in-out infinite; }
|
||||
@keyframes mkt-pulse-dot {
|
||||
0%, 100% { box-shadow: 0 0 0 2px rgba(16,185,129,0.25); }
|
||||
50% { box-shadow: 0 0 0 4px rgba(16,185,129,0.1); }
|
||||
}
|
||||
.mkt-p-live-txt { font-family: var(--mono); font-size: 0.625rem; color: #10B981; font-weight: 500; }
|
||||
.mkt-p-nav { background: #111115; border-right: 1px solid rgba(255,255,255,0.055); display: flex; flex-direction: column; overflow: hidden; }
|
||||
.mkt-p-nav-section { padding: 14px 12px 4px; font-family: var(--mono); font-size: 0.5625rem; color: rgba(255,255,255,0.25); letter-spacing: 0.1em; text-transform: uppercase; }
|
||||
.mkt-p-nav-item { display: flex; align-items: center; gap: 8px; padding: 7px 12px; font-size: 0.75rem; color: rgba(255,255,255,0.42); cursor: default; }
|
||||
.mkt-p-nav-item-sel { background: rgba(51,133,255,0.12); color: rgba(255,255,255,0.88); }
|
||||
.mkt-p-nav-dot { width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,0.2); flex-shrink: 0; }
|
||||
.mkt-p-nav-dot-sel { background: #3385FF; }
|
||||
.mkt-p-sep-line { height: 1px; background: rgba(255,255,255,0.04); margin: 6px 0; }
|
||||
.mkt-p-canvas { position: relative; overflow: hidden; background: #0D0D11; display: flex; align-items: center; justify-content: center; }
|
||||
.mkt-p-grid { position: absolute; inset: 0; background-image: radial-gradient(circle, rgba(255,255,255,0.06) 1px, transparent 1px); background-size: 24px 24px; pointer-events: none; }
|
||||
.mkt-p-artboard { position: absolute; background: #FFFFFF; border-radius: 8px; box-shadow: 0 8px 40px rgba(0,0,0,0.6); }
|
||||
.mkt-p-ab-label { position: absolute; top: -22px; left: 0; font-family: var(--mono); font-size: 0.5625rem; color: rgba(255,255,255,0.4); white-space: nowrap; letter-spacing: 0.01em; }
|
||||
.mkt-p-artboard-sel { box-shadow: 0 0 0 2px #3385FF, 0 8px 40px rgba(0,0,0,0.6); }
|
||||
.mkt-p-artboard-sel .mkt-p-ab-label { color: #3385FF; }
|
||||
.mkt-p-handle { position: absolute; width: 8px; height: 8px; background: #fff; border: 2px solid #3385FF; border-radius: 2px; z-index: 10; }
|
||||
.mkt-p-handle-tl { top: -4px; left: -4px; }
|
||||
.mkt-p-handle-tr { top: -4px; right: -4px; }
|
||||
.mkt-p-handle-bl { bottom: -4px; left: -4px; }
|
||||
.mkt-p-handle-br { bottom: -4px; right: -4px; }
|
||||
.mkt-p-dash-card { padding: 18px; }
|
||||
.mkt-p-dc-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.mkt-p-dc-title { font-size: 0.6875rem; font-weight: 600; color: #111; letter-spacing: -0.01em; }
|
||||
.mkt-p-dc-badge { font-size: 0.5625rem; background: #ECFDF5; color: #059669; padding: 2px 7px; border-radius: 99px; font-weight: 600; font-family: var(--mono); }
|
||||
.mkt-p-dc-val { font-size: 1.375rem; font-weight: 800; color: #0A0A0A; letter-spacing: -0.045em; line-height: 1; margin-bottom: 5px; }
|
||||
.mkt-p-dc-delta { font-size: 0.625rem; color: #059669; font-weight: 600; margin-bottom: 14px; }
|
||||
.mkt-p-dc-bar { height: 3px; background: #F0F0F0; border-radius: 99px; overflow: hidden; margin-bottom: 14px; }
|
||||
.mkt-p-dc-fill { height: 100%; width: 68%; background: linear-gradient(90deg, #0066FF, #3385FF); border-radius: 99px; }
|
||||
.mkt-p-dc-chips { display: flex; gap: 4px; }
|
||||
.mkt-p-chip { font-size: 0.5rem; background: #F5F5F5; color: #666; padding: 3px 8px; border-radius: 99px; font-weight: 500; }
|
||||
.mkt-p-profile { padding: 20px; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||
.mkt-p-avatar { width: 44px; height: 44px; border-radius: 50%; background: linear-gradient(135deg, #7C3AED, #0066FF); margin: 0 auto 12px; }
|
||||
.mkt-p-pname { font-size: 0.75rem; font-weight: 700; color: #0A0A0A; letter-spacing: -0.02em; margin-bottom: 3px; }
|
||||
.mkt-p-prole { font-size: 0.5875rem; color: #A1A1AA; margin-bottom: 14px; }
|
||||
.mkt-p-prow { display: flex; justify-content: space-between; width: 100%; margin-bottom: 5px; }
|
||||
.mkt-p-pk { font-size: 0.5875rem; color: #A1A1AA; }
|
||||
.mkt-p-pv { font-size: 0.5875rem; font-weight: 600; color: #0A0A0A; }
|
||||
.mkt-p-insp { background: #111115; border-left: 1px solid rgba(255,255,255,0.055); display: flex; flex-direction: column; overflow: hidden; }
|
||||
.mkt-p-tabs { display: flex; border-bottom: 1px solid rgba(255,255,255,0.055); }
|
||||
.mkt-p-tab { flex: 1; padding: 11px 0; font-family: var(--mono); font-size: 0.625rem; color: rgba(255,255,255,0.3); text-align: center; letter-spacing: 0.06em; cursor: default; text-transform: uppercase; }
|
||||
.mkt-p-tab-on { color: rgba(255,255,255,0.88); border-bottom: 2px solid #3385FF; }
|
||||
.mkt-p-isec { padding: 12px 14px; border-bottom: 1px solid rgba(255,255,255,0.04); }
|
||||
.mkt-p-isec-hd { font-family: var(--mono); font-size: 0.5rem; color: rgba(255,255,255,0.25); letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 10px; }
|
||||
.mkt-p-irow { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
|
||||
.mkt-p-ik { font-family: var(--mono); font-size: 0.625rem; color: rgba(255,255,255,0.35); }
|
||||
.mkt-p-iv { font-family: var(--mono); font-size: 0.625rem; }
|
||||
.mkt-p-iv-s { color: #7DD3A8; }
|
||||
.mkt-p-iv-n { color: #7EB8FF; }
|
||||
.mkt-p-iv-b { color: #FFBA7B; }
|
||||
.mkt-p-diff { font-family: var(--mono); font-size: 0.6rem; padding: 3px 8px; border-radius: 4px; margin-bottom: 3px; line-height: 1.5; }
|
||||
.mkt-p-diff-del { background: rgba(255,70,70,0.09); color: #FF8080; }
|
||||
.mkt-p-diff-add { background: rgba(70,220,120,0.09); color: #7DDBA0; }
|
||||
.mkt-p-statusbar { margin-top: auto; height: 26px; background: #0D0D11; border-top: 1px solid rgba(255,255,255,0.04); display: flex; align-items: center; padding: 0 14px; gap: 14px; flex-shrink: 0; }
|
||||
.mkt-p-st { font-family: var(--mono); font-size: 0.5625rem; color: rgba(255,255,255,0.25); }
|
||||
.mkt-p-st-on { color: rgba(255,255,255,0.55); }
|
||||
|
||||
/* ── Logos strip ────────────────────────────────────────────── */
|
||||
#mkt-logos { padding: 56px 0 80px; border-bottom: 1px solid var(--border); }
|
||||
.mkt-logos-label { text-align: center; margin-bottom: 32px; }
|
||||
.mkt-logos-list { display: flex; align-items: center; justify-content: center; gap: 56px; flex-wrap: wrap; }
|
||||
.mkt-logo-item { font-size: 1rem; font-weight: 700; color: var(--fg-4); letter-spacing: -0.03em; transition: color 0.15s; cursor: default; }
|
||||
.mkt-logo-item:hover { color: var(--fg-3); }
|
||||
|
||||
/* ── Feature blocks ─────────────────────────────────────────── */
|
||||
.mkt-feat-block { padding: 120px 40px; overflow: hidden; }
|
||||
.mkt-feat-block-inner { max-width: 1120px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 96px; align-items: center; }
|
||||
.mkt-feat-block-inner-reverse { direction: rtl; }
|
||||
.mkt-feat-block-inner-reverse > * { direction: ltr; }
|
||||
.mkt-feat-copy .mkt-label { display: block; margin-bottom: 20px; }
|
||||
.mkt-feat-copy .mkt-d3 { margin-bottom: 18px; }
|
||||
.mkt-feat-copy .mkt-subtitle { margin-bottom: 28px; }
|
||||
.mkt-feat-points { list-style: none; display: flex; flex-direction: column; gap: 12px; margin-bottom: 32px; }
|
||||
.mkt-feat-points li { display: flex; align-items: flex-start; gap: 12px; font-size: 0.9375rem; color: var(--fg-2); line-height: 1.55; }
|
||||
.mkt-feat-point-icon { width: 20px; height: 20px; border-radius: var(--r-1); background: var(--blue-soft); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; font-size: 0.625rem; color: var(--blue); }
|
||||
.mkt-feat-visual { border-radius: var(--r-5); overflow: hidden; box-shadow: var(--sh-2xl); border: 1px solid var(--border); aspect-ratio: 4/3; position: relative; }
|
||||
.mkt-feat-visual-dark { background: #0D0D11; border-color: rgba(255,255,255,0.07); }
|
||||
.mkt-fv-artboard-scene { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.mkt-fv-grid { position: absolute; inset: 0; background-image: radial-gradient(circle, rgba(255,255,255,0.06) 1px, transparent 1px); background-size: 22px 22px; }
|
||||
.mkt-fv-ab { width: 200px; background: #fff; border-radius: 8px; padding: 16px; box-shadow: 0 8px 40px rgba(0,0,0,0.55); position: relative; z-index: 1; outline: 2px solid var(--blue-light); outline-offset: 2px; }
|
||||
.mkt-fv-ab-lbl { position: absolute; top: -20px; left: 0; font-family: var(--mono); font-size: 0.5rem; color: #3385FF; white-space: nowrap; }
|
||||
.mkt-fv-mini-title { font-size: 0.625rem; font-weight: 700; color: #111; margin-bottom: 8px; }
|
||||
.mkt-fv-mini-val { font-size: 1rem; font-weight: 800; color: #0A0A0A; letter-spacing: -0.04em; margin-bottom: 6px; }
|
||||
.mkt-fv-mini-bar { height: 2px; background: #F0F0F0; border-radius: 99px; overflow: hidden; }
|
||||
.mkt-fv-mini-fill { height: 100%; width: 68%; background: #0066FF; }
|
||||
.mkt-fv-diff-scene { padding: 24px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.mkt-fv-diff-header { font-family: var(--mono); font-size: 0.5625rem; color: rgba(255,255,255,0.3); letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 8px; }
|
||||
.mkt-fv-diff-file { font-family: var(--mono); font-size: 0.625rem; color: rgba(255,255,255,0.5); margin-bottom: 12px; }
|
||||
.mkt-fv-line { font-family: var(--mono); font-size: 0.65rem; padding: 4px 8px; border-radius: 4px; line-height: 1.5; }
|
||||
.mkt-fv-line-ctx { color: rgba(255,255,255,0.3); }
|
||||
.mkt-fv-line-del { background: rgba(255,70,70,0.1); color: #FF8080; }
|
||||
.mkt-fv-line-add { background: rgba(70,220,120,0.1); color: #7DDBA0; }
|
||||
.mkt-fv-graph-scene { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
||||
.mkt-fv-node { position: absolute; background: #1C1C26; border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 7px 12px; font-family: var(--mono); font-size: 0.5875rem; color: rgba(255,255,255,0.7); white-space: nowrap; box-shadow: 0 4px 12px rgba(0,0,0,0.4); }
|
||||
.mkt-fv-node-root { background: rgba(51,133,255,0.15); border-color: rgba(51,133,255,0.3); color: #7EB8FF; }
|
||||
|
||||
/* ── Features grid ──────────────────────────────────────────── */
|
||||
#mkt-features { padding: 128px 0; background: var(--bg-subtle); }
|
||||
.mkt-feat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); border-radius: var(--r-5); overflow: hidden; }
|
||||
.mkt-feat-cell { background: var(--bg-subtle); padding: 40px 36px; transition: background 0.15s; position: relative; }
|
||||
.mkt-feat-cell:hover { background: var(--bg); }
|
||||
.mkt-feat-cell::after { content: ''; position: absolute; inset: 0; opacity: 0; background: radial-gradient(ellipse 80% 60% at var(--mx,50%) var(--my,50%), rgba(0,102,255,0.04), transparent); transition: opacity 0.3s; pointer-events: none; }
|
||||
.mkt-feat-cell:hover::after { opacity: 1; }
|
||||
.mkt-feat-icon { width: 40px; height: 40px; border-radius: var(--r-2); background: var(--blue-soft); display: flex; align-items: center; justify-content: center; font-size: 1.125rem; margin-bottom: 20px; border: 1px solid var(--blue-mid); }
|
||||
.mkt-feat-cell h3 { font-size: 1.0625rem; font-weight: 700; color: var(--fg); letter-spacing: -0.025em; margin-bottom: 10px; line-height: 1.25; }
|
||||
.mkt-feat-cell p { font-size: 0.9375rem; color: var(--fg-2); line-height: 1.65; }
|
||||
|
||||
/* ── Workflow ───────────────────────────────────────────────── */
|
||||
#mkt-workflow { padding: 128px 0; }
|
||||
.mkt-steps-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0; margin-top: 72px; position: relative; }
|
||||
.mkt-steps-row::before { content: ''; position: absolute; top: 26px; left: calc(10% + 8px); right: calc(10% + 8px); height: 1px; background: linear-gradient(90deg, transparent, var(--border-2) 15%, var(--border-2) 85%, transparent); z-index: 0; }
|
||||
.mkt-step { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 0 16px; position: relative; z-index: 1; }
|
||||
.mkt-step-num { width: 52px; height: 52px; border-radius: 50%; background: var(--bg); border: 1px solid var(--border-2); display: flex; align-items: center; justify-content: center; font-size: 1rem; font-weight: 800; color: var(--blue); letter-spacing: -0.04em; margin-bottom: 20px; box-shadow: var(--sh-sm); }
|
||||
.mkt-step h3 { font-size: 0.9375rem; font-weight: 700; color: var(--fg); letter-spacing: -0.02em; margin-bottom: 8px; }
|
||||
.mkt-step p { font-size: 0.8125rem; color: var(--fg-2); line-height: 1.6; }
|
||||
|
||||
/* ── Integrations ───────────────────────────────────────────── */
|
||||
#mkt-integrations { background: #08080C; padding: 140px 0; position: relative; overflow: hidden; }
|
||||
#mkt-integrations::before { content: ''; position: absolute; inset: 0; background-image: radial-gradient(circle, rgba(255,255,255,0.032) 1px, transparent 1px); background-size: 28px 28px; pointer-events: none; }
|
||||
#mkt-integrations .mkt-section-head .mkt-label { color: rgba(255,255,255,0.28); }
|
||||
#mkt-integrations .mkt-section-head .mkt-d2 { color: #fff; }
|
||||
#mkt-integrations .mkt-section-head .mkt-subtitle { color: rgba(255,255,255,0.42); }
|
||||
.mkt-int-orb { position: absolute; border-radius: 50%; pointer-events: none; filter: blur(80px); opacity: 0.55; }
|
||||
.mkt-int-orb-1 { width:500px;height:300px;top:0;left:50%;transform:translateX(-50%); background:radial-gradient(ellipse,rgba(51,133,255,.15),transparent 70%); }
|
||||
.mkt-int-orb-2 { width:320px;height:320px;bottom:10%;left:5%; background:radial-gradient(ellipse,rgba(162,89,255,.1),transparent 70%); }
|
||||
.mkt-int-orb-3 { width:260px;height:260px;bottom:15%;right:8%; background:radial-gradient(ellipse,rgba(97,218,251,.08),transparent 70%); }
|
||||
.mkt-int-bento { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 64px; position: relative; z-index: 1; }
|
||||
.mkt-int-card { background: #111115; border: 1px solid rgba(255,255,255,0.07); border-radius: 16px; padding: 26px 28px; position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 14px; min-height: 168px; transition: border-color .2s, box-shadow .25s cubic-bezier(.16,1,.3,1), transform .2s cubic-bezier(.16,1,.3,1); cursor: default; }
|
||||
.mkt-int-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--glow,rgba(255,255,255,.3)) 40%, transparent); opacity: .65; }
|
||||
.mkt-int-card::after { content: ''; position: absolute; inset: 0; opacity: 0; background: radial-gradient(ellipse 70% 60% at var(--mx,50%) var(--my,50%), var(--glow-soft,rgba(255,255,255,.04)), transparent); transition: opacity .3s; pointer-events: none; }
|
||||
.mkt-int-card:hover::after { opacity: 1; }
|
||||
.mkt-int-card:hover { border-color: rgba(255,255,255,.13); box-shadow: 0 0 0 1px var(--glow-dim,rgba(255,255,255,.06)), 0 20px 48px rgba(0,0,0,.5), 0 0 56px var(--glow-ambient,rgba(255,255,255,.02)); transform: translateY(-3px); }
|
||||
.mkt-int-card-w2 { grid-column: span 2; }
|
||||
.mkt-int-ico { width: 42px; height: 42px; border-radius: 11px; background: var(--ico-bg,rgba(255,255,255,.07)); border: 1px solid var(--ico-bd,rgba(255,255,255,.1)); display: flex; align-items: center; justify-content: center; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
.mkt-int-name { font-size: .9375rem; font-weight: 700; color: rgba(255,255,255,.9); letter-spacing: -.025em; line-height: 1.25; position: relative; z-index: 1; }
|
||||
.mkt-int-desc { font-size: .8rem; color: rgba(255,255,255,.36); line-height: 1.6; flex: 1; position: relative; z-index: 1; }
|
||||
.mkt-int-footer { display: flex; align-items: center; justify-content: space-between; padding-top: 12px; border-top: 1px solid rgba(255,255,255,.05); position: relative; z-index: 1; margin-top: auto; }
|
||||
.mkt-int-tag { font-family: var(--mono); font-size: .5rem; font-weight: 500; letter-spacing: .09em; text-transform: uppercase; color: rgba(255,255,255,.2); }
|
||||
.mkt-int-status { display: flex; align-items: center; gap: 5px; font-family: var(--mono); font-size: .5rem; font-weight: 500; letter-spacing: .05em; color: rgba(255,255,255,.28); }
|
||||
.mkt-int-dot { width: 5px; height: 5px; border-radius: 50%; background: #10B981; box-shadow: 0 0 6px rgba(16,185,129,.7); animation: mkt-int-pulse 2.4s ease-in-out infinite; }
|
||||
@keyframes mkt-int-pulse {
|
||||
0%,100% { opacity:1; box-shadow:0 0 6px rgba(16,185,129,.7); }
|
||||
50% { opacity:.4; box-shadow:0 0 2px rgba(16,185,129,.3); }
|
||||
}
|
||||
.mkt-int-more { background: transparent !important; border-style: dashed !important; border-color: rgba(255,255,255,.08) !important; align-items: center; justify-content: center; text-align: center; gap: 6px !important; min-height: 168px; }
|
||||
.mkt-int-more::before, .mkt-int-more::after { display: none; }
|
||||
.mkt-int-more-n { font-size: 2rem; font-weight: 900; color: rgba(255,255,255,.1); letter-spacing: -.05em; line-height: 1; }
|
||||
.mkt-int-more-l { font-size: .75rem; color: rgba(255,255,255,.22); font-weight: 500; letter-spacing: -.01em; }
|
||||
|
||||
/* ── Comparison ─────────────────────────────────────────────── */
|
||||
#mkt-comparison { padding: 128px 0; }
|
||||
.mkt-cmp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 72px; }
|
||||
.mkt-cmp-card { border-radius: var(--r-5); padding: 40px 32px; border: 1px solid var(--border); background: var(--bg); transition: box-shadow 0.2s; }
|
||||
.mkt-cmp-card:hover { box-shadow: var(--sh-lg); }
|
||||
.mkt-cmp-card-hero { background: #06060E; border-color: rgba(51,133,255,0.4); box-shadow: 0 0 0 1px rgba(51,133,255,0.18), var(--sh-xl); transform: translateY(-10px); }
|
||||
.mkt-cmp-eyebrow { font-family: var(--mono); font-size: 0.625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 14px; }
|
||||
.mkt-cmp-card-hero .mkt-cmp-eyebrow { color: rgba(51,133,255,0.85); }
|
||||
.mkt-cmp-name { font-size: 1.625rem; font-weight: 800; letter-spacing: -0.04em; color: var(--fg); margin-bottom: 16px; line-height: 1.15; }
|
||||
.mkt-cmp-card-hero .mkt-cmp-name { color: #fff; }
|
||||
.mkt-cmp-hr { height: 1px; background: var(--border); margin: 18px 0; }
|
||||
.mkt-cmp-card-hero .mkt-cmp-hr { background: rgba(255,255,255,0.08); }
|
||||
.mkt-cmp-body { font-size: 0.9375rem; line-height: 1.7; color: var(--fg-2); margin-bottom: 20px; }
|
||||
.mkt-cmp-card-hero .mkt-cmp-body { color: rgba(255,255,255,0.62); }
|
||||
.mkt-cmp-tags { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.mkt-cmp-tag { font-size: 0.8125rem; padding: 4px 12px; border-radius: var(--r-full); font-weight: 500; background: var(--bg-muted); color: var(--fg-2); }
|
||||
.mkt-cmp-card-hero .mkt-cmp-tag { background: rgba(51,133,255,0.12); color: rgba(51,133,255,0.9); border: 1px solid rgba(51,133,255,0.2); }
|
||||
|
||||
/* ── Testimonial ────────────────────────────────────────────── */
|
||||
#mkt-testimonial { padding: 160px 40px; background: var(--bg-subtle); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||||
.mkt-testi-inner { max-width: 760px; margin: 0 auto; text-align: center; }
|
||||
.mkt-testi-logo { margin: 0 auto 40px; display: flex; align-items: center; justify-content: center; gap: 10px; }
|
||||
.mkt-testi-logo-dot { width: 32px; height: 32px; background: linear-gradient(135deg, var(--purple), var(--blue)); border-radius: var(--r-2); }
|
||||
.mkt-testi-logo-name { font-size: 0.9375rem; font-weight: 700; color: var(--fg); letter-spacing: -0.02em; }
|
||||
.mkt-testi-quote { font-size: clamp(1.25rem, 3vw, 1.875rem); font-weight: 600; line-height: 1.45; letter-spacing: -0.028em; color: var(--fg); margin-bottom: 32px; }
|
||||
.mkt-testi-attr { font-size: 0.9375rem; color: var(--fg-3); letter-spacing: -0.01em; }
|
||||
.mkt-testi-attr strong { color: var(--fg-2); font-weight: 500; }
|
||||
|
||||
/* ── Pricing ────────────────────────────────────────────────── */
|
||||
#mkt-pricing { padding: 128px 0; }
|
||||
.mkt-pricing-toggle { display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 56px; }
|
||||
.mkt-toggle-pill { display: flex; background: var(--bg-muted); border-radius: var(--r-full); padding: 4px; border: 1px solid var(--border); }
|
||||
.mkt-toggle-opt { padding: 7px 20px; border-radius: var(--r-full); font-size: 0.875rem; font-weight: 500; color: var(--fg-2); cursor: pointer; transition: all 0.15s; letter-spacing: -0.01em; }
|
||||
.mkt-toggle-opt-active { background: var(--bg); color: var(--fg); box-shadow: var(--sh-xs); }
|
||||
.mkt-toggle-save { font-size: 0.8125rem; font-weight: 600; color: #059669; background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.2); padding: 4px 12px; border-radius: var(--r-full); }
|
||||
.mkt-px-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; align-items: start; }
|
||||
.mkt-px-card { border-radius: var(--r-5); padding: 40px 32px; background: var(--bg); border: 1px solid var(--border); transition: box-shadow 0.2s; }
|
||||
.mkt-px-card:hover { box-shadow: var(--sh-lg); }
|
||||
.mkt-px-card-featured { border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue), var(--sh-xl); position: relative; }
|
||||
.mkt-px-tag { position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: var(--blue); color: #fff; font-size: 0.75rem; font-weight: 600; padding: 4px 14px; border-radius: var(--r-full); white-space: nowrap; }
|
||||
.mkt-px-tier { font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 12px; }
|
||||
.mkt-px-card-featured .mkt-px-tier { color: var(--blue); }
|
||||
.mkt-px-price { font-size: 3rem; font-weight: 900; letter-spacing: -0.05em; color: var(--fg); line-height: 1; margin-bottom: 4px; }
|
||||
.mkt-px-period { font-size: 0.875rem; color: var(--fg-3); margin-bottom: 28px; letter-spacing: -0.01em; }
|
||||
.mkt-px-hr { height: 1px; background: var(--border); margin: 24px 0; }
|
||||
.mkt-px-feats { list-style: none; display: flex; flex-direction: column; gap: 12px; margin-bottom: 32px; }
|
||||
.mkt-px-feats li { display: flex; align-items: flex-start; gap: 12px; font-size: 0.9375rem; color: var(--fg-2); line-height: 1.5; letter-spacing: -0.01em; }
|
||||
.mkt-px-check { color: var(--blue); font-weight: 700; flex-shrink: 0; margin-top: 1px; font-size: 0.875rem; }
|
||||
|
||||
/* ── Bottom CTA ─────────────────────────────────────────────── */
|
||||
#mkt-cta-bottom { background: #06060E; padding: 140px 40px; position: relative; overflow: hidden; text-align: center; }
|
||||
#mkt-cta-bottom::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse 60% 80% at 30% 50%, rgba(124,58,237,0.18) 0%, transparent 60%), radial-gradient(ellipse 60% 80% at 70% 50%, rgba(0,102,255,0.15) 0%, transparent 60%); pointer-events: none; }
|
||||
.mkt-cta-inner { position: relative; max-width: 720px; margin: 0 auto; }
|
||||
.mkt-cta-headline { font-size: clamp(2.5rem, 5vw, 4rem); font-weight: 900; letter-spacing: -0.045em; color: rgba(255,255,255,0.94); line-height: 1.05; margin-bottom: 20px; }
|
||||
.mkt-cta-sub { font-size: 1.125rem; color: rgba(255,255,255,0.52); margin-bottom: 48px; line-height: 1.65; }
|
||||
.mkt-cta-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
||||
|
||||
/* ── Footer ─────────────────────────────────────────────────── */
|
||||
.mkt-footer { background: #08080C; border-top: 1px solid rgba(255,255,255,0.06); padding: 80px 40px 40px; }
|
||||
.mkt-ft-inner { max-width: 1120px; margin: 0 auto; }
|
||||
.mkt-ft-top { display: grid; grid-template-columns: 280px repeat(4, 1fr); gap: 48px; padding-bottom: 64px; border-bottom: 1px solid rgba(255,255,255,0.07); margin-bottom: 40px; }
|
||||
.mkt-ft-brand-logo { font-size: 1.0625rem; font-weight: 800; letter-spacing: -0.04em; color: rgba(255,255,255,0.92); margin-bottom: 10px; }
|
||||
.mkt-ft-brand-logo span { color: #3385FF; }
|
||||
.mkt-ft-brand-tag { font-size: 0.875rem; color: rgba(255,255,255,0.32); line-height: 1.65; max-width: 200px; }
|
||||
.mkt-ft-col h4 { font-size: 0.9375rem; font-weight: 600; color: rgba(255,255,255,0.75); margin-bottom: 18px; letter-spacing: -0.02em; }
|
||||
.mkt-ft-col ul { list-style: none; display: flex; flex-direction: column; gap: 11px; }
|
||||
.mkt-ft-col a { font-size: 0.9375rem; color: rgba(255,255,255,0.35); transition: color 0.14s; letter-spacing: -0.01em; }
|
||||
.mkt-ft-col a:hover { color: rgba(255,255,255,0.7); }
|
||||
.mkt-ft-bottom { display: flex; justify-content: space-between; align-items: center; }
|
||||
.mkt-ft-copy { font-size: 0.875rem; color: rgba(255,255,255,0.25); letter-spacing: -0.01em; }
|
||||
.mkt-theme-toggle { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: var(--r-full); background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); font-size: 0.875rem; color: rgba(255,255,255,0.45); transition: background 0.14s, color 0.14s; letter-spacing: -0.01em; cursor: pointer; }
|
||||
.mkt-theme-toggle:hover { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.75); }
|
||||
|
||||
/* ── Scroll reveal ──────────────────────────────────────────── */
|
||||
.mkt-reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.7s cubic-bezier(0.16,1,0.3,1), transform 0.7s cubic-bezier(0.16,1,0.3,1); }
|
||||
.mkt-reveal.in { opacity: 1; transform: none; }
|
||||
.mkt-stagger > * { opacity: 0; transform: translateY(18px); transition: opacity 0.55s cubic-bezier(0.16,1,0.3,1), transform 0.55s cubic-bezier(0.16,1,0.3,1); }
|
||||
.mkt-stagger.in > *:nth-child(1) { opacity: 1; transform: none; transition-delay: 0.00s; }
|
||||
.mkt-stagger.in > *:nth-child(2) { opacity: 1; transform: none; transition-delay: 0.07s; }
|
||||
.mkt-stagger.in > *:nth-child(3) { opacity: 1; transform: none; transition-delay: 0.14s; }
|
||||
.mkt-stagger.in > *:nth-child(4) { opacity: 1; transform: none; transition-delay: 0.21s; }
|
||||
.mkt-stagger.in > *:nth-child(5) { opacity: 1; transform: none; transition-delay: 0.28s; }
|
||||
.mkt-stagger.in > *:nth-child(6) { opacity: 1; transform: none; transition-delay: 0.35s; }
|
||||
|
||||
/* ── Responsive ─────────────────────────────────────────────── */
|
||||
@media (max-width: 1024px) {
|
||||
.mkt-feat-block-inner { grid-template-columns: 1fr; gap: 56px; }
|
||||
.mkt-feat-block-inner-reverse { direction: ltr; }
|
||||
.mkt-feat-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.mkt-cmp-grid, .mkt-px-grid { grid-template-columns: 1fr; max-width: 480px; margin-left: auto; margin-right: auto; }
|
||||
.mkt-px-card-featured { margin: 8px 0; }
|
||||
.mkt-int-bento { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.mkt-int-bento { grid-template-columns: repeat(2, 1fr); }
|
||||
.mkt-int-card-w2 { grid-column: span 2; }
|
||||
.mkt-wrap, .mkt-wrap-wide { padding: 0 24px; }
|
||||
#mkt-nav { padding: 0 24px; }
|
||||
.mkt-nav-inner { display: flex; justify-content: space-between; align-items: center; width: 100%; }
|
||||
.mkt-nav-links { display: none; }
|
||||
.mkt-nav-actions .mkt-btn { display: none; }
|
||||
.mkt-nav-actions .mkt-nav-theme-btn { display: none; }
|
||||
.mkt-nav-burger { display: flex; }
|
||||
.mkt-feat-block { padding: 80px 24px; }
|
||||
#mkt-hero { padding: 140px 24px 0; }
|
||||
.mkt-hero-ctas { flex-direction: column; align-items: center; }
|
||||
.mkt-p-app { height: 380px; grid-template-columns: 160px 1fr; }
|
||||
.mkt-p-insp { display: none; }
|
||||
.mkt-feat-grid { grid-template-columns: 1fr; }
|
||||
.mkt-steps-row { grid-template-columns: 1fr 1fr; gap: 40px; }
|
||||
.mkt-steps-row::before { display: none; }
|
||||
.mkt-ft-top { grid-template-columns: 1fr 1fr; }
|
||||
.mkt-logos-list { gap: 32px; }
|
||||
.mkt-cta-actions { flex-direction: column; align-items: center; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.mkt-d1 { font-size: 2.5rem; }
|
||||
.mkt-ft-top { grid-template-columns: 1fr; }
|
||||
.mkt-p-app { grid-template-columns: 1fr; height: 320px; }
|
||||
.mkt-p-nav { display: none; }
|
||||
.mkt-int-bento { grid-template-columns: 1fr; }
|
||||
.mkt-int-card-w2 { grid-column: span 1; }
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useUser } from '@clerk/nextjs';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useTheme } from '@/store/theme';
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const { user, isLoaded } = useUser();
|
||||
const router = useRouter();
|
||||
const { mode, toggle } = useTheme();
|
||||
|
||||
const defaultName = isLoaded
|
||||
? `${user?.firstName ?? user?.emailAddresses[0]?.emailAddress?.split('@')[0] ?? 'My'}'s Workspace`
|
||||
: '';
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const workspaceName = name.trim() || defaultName;
|
||||
|
||||
async function handleCreate(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!workspaceName) return;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const res = await fetch('/api/workspaces', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: workspaceName }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setError(data.error ?? 'Something went wrong');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = await res.json();
|
||||
router.push(`/workspace/${ws.id}`);
|
||||
}
|
||||
|
||||
if (!isLoaded) return null;
|
||||
|
||||
const firstName = user?.firstName ?? user?.emailAddresses[0]?.emailAddress?.split('@')[0] ?? 'there';
|
||||
|
||||
return (
|
||||
<main style={{
|
||||
minHeight: '100dvh',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'var(--page-bg)',
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif",
|
||||
padding: '24px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={toggle}
|
||||
title={`Switch to ${mode === 'dark' ? 'light' : 'dark'} mode`}
|
||||
style={{
|
||||
position: 'absolute', top: 16, right: 16,
|
||||
background: 'none', border: 'none', cursor: 'pointer',
|
||||
color: mode === 'dark' ? 'rgba(255,255,255,0.4)' : '#A1A1AA',
|
||||
padding: '7px', display: 'flex', alignItems: 'center', borderRadius: 8,
|
||||
transition: 'color 0.12s, background 0.12s',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.color = mode === 'dark' ? 'rgba(255,255,255,0.85)' : '#0A0A0A';
|
||||
e.currentTarget.style.background = mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.05)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.color = mode === 'dark' ? 'rgba(255,255,255,0.4)' : '#A1A1AA';
|
||||
e.currentTarget.style.background = 'none';
|
||||
}}
|
||||
>
|
||||
{mode === 'dark' ? (
|
||||
<svg width="16" height="16" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="2.5" stroke="currentColor" strokeWidth="1.2"/>
|
||||
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.93 2.93l1.06 1.06M10.01 10.01l1.06 1.06M2.93 11.07l1.06-1.06M10.01 3.99l1.06-1.06" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M12 8.5A5.5 5.5 0 0 1 5.5 2a5.5 5.5 0 1 0 6.5 6.5z" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
{/* Logo */}
|
||||
<div style={{ marginBottom: 40, fontSize: '1.125rem', fontWeight: 700, letterSpacing: '-0.02em', color: 'var(--page-text)' }}>
|
||||
Origin<span style={{ color: '#0066FF' }}>main</span>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div style={{
|
||||
background: 'var(--card-bg)', border: '1px solid var(--card-border)',
|
||||
borderRadius: 20, padding: '44px 48px', maxWidth: 460, width: '100%',
|
||||
boxShadow: '0 4px 24px rgba(0,0,0,0.06)', transition: 'background 0.2s',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: 13,
|
||||
background: 'rgba(0,102,255,0.07)', border: '1px solid rgba(0,102,255,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: 22,
|
||||
}}>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
|
||||
<rect x="1.5" y="1.5" width="8.5" height="8.5" rx="2" fill="#0066FF" opacity="0.7"/>
|
||||
<rect x="12" y="1.5" width="8.5" height="8.5" rx="2" fill="#0066FF" opacity="0.3"/>
|
||||
<rect x="1.5" y="12" width="8.5" height="8.5" rx="2" fill="#0066FF" opacity="0.3"/>
|
||||
<rect x="12" y="12" width="8.5" height="8.5" rx="2" fill="#0066FF" opacity="0.15"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontSize: '1.375rem', fontWeight: 700, letterSpacing: '-0.03em', color: 'var(--card-text)', margin: '0 0 8px' }}>
|
||||
Welcome, {firstName}!
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.9rem', color: 'var(--card-muted)', lineHeight: 1.6, margin: '0 0 28px' }}>
|
||||
Let's create your workspace. A workspace holds your projects and team — usually named after your company or product.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleCreate}>
|
||||
<label style={{ display: 'block', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--card-text)', marginBottom: 6 }}>
|
||||
Workspace name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder={defaultName}
|
||||
autoFocus
|
||||
style={{
|
||||
width: '100%', boxSizing: 'border-box',
|
||||
padding: '11px 13px', fontSize: '0.9375rem',
|
||||
border: '1px solid var(--input-border)', borderRadius: 9,
|
||||
color: 'var(--input-text)', background: 'var(--input-bg)',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = '#0066FF')}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = 'var(--input-border)')}
|
||||
/>
|
||||
<p style={{ margin: '6px 0 0', fontSize: '0.75rem', color: 'var(--card-muted)' }}>
|
||||
Leave blank to use "{defaultName}"
|
||||
</p>
|
||||
|
||||
{error && <p style={{ margin: '10px 0 0', fontSize: '0.8125rem', color: '#DC2626' }}>{error}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
marginTop: 24, width: '100%',
|
||||
padding: '12px', borderRadius: 9,
|
||||
fontSize: '0.9375rem', fontWeight: 600,
|
||||
background: loading ? 'var(--card-subtle)' : 'var(--btn-bg)',
|
||||
color: loading ? 'var(--card-muted)' : 'var(--btn-fg)', border: 'none',
|
||||
cursor: loading ? 'default' : 'pointer',
|
||||
fontFamily: 'inherit', letterSpacing: '-0.01em',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||
}}
|
||||
>
|
||||
{loading ? 'Creating workspace…' : (
|
||||
<>
|
||||
Create workspace
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p style={{ marginTop: 24, fontSize: '0.8125rem', color: '#A1A1AA' }}>
|
||||
Already have a workspace?{' '}
|
||||
<a href="/workspaces" style={{ color: '#0066FF', textDecoration: 'none' }}>View all workspaces</a>
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, type ReactNode } from 'react';
|
||||
import { FluentProvider } from '@fluentui/react-components';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { httpBatchLink } from '@trpc/client';
|
||||
import { originmainLightTheme, originmainDarkTheme } from '@originmain/ui';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
import { useTheme } from '@/store/theme';
|
||||
import { TourOverlay } from '@/components/walkthrough/TourOverlay';
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
// TanStack Query v5: create QueryClient inside useState so it's stable across
|
||||
// re-renders and each client-side navigation gets the same instance.
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000,
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// tRPC client — shares the QueryClient so tRPC queries/mutations go into the
|
||||
// same cache as all other TanStack Query calls in the app.
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({ url: '/api/trpc' }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const mode = useTheme((s) => s.mode);
|
||||
const theme = mode === 'dark' ? originmainDarkTheme : originmainLightTheme;
|
||||
|
||||
// Sync data-theme on <html> so CSS variables and server-rendered page
|
||||
// backgrounds can respond to the user's preference without prop drilling.
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', mode);
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<FluentProvider theme={theme} style={{ height: '100%' }}>
|
||||
{children}
|
||||
<TourOverlay />
|
||||
</FluentProvider>
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,776 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* /settings/design-language — Phase 6
|
||||
*
|
||||
* Design Language Settings page. Allows workspace admins to:
|
||||
* 1. Upload a token file (Style Dictionary / W3C DTCG / flat CSS vars)
|
||||
* 2. Fetch a token file from an HTTPS URL (via the SSRF-guarded proxy)
|
||||
* 3. Validate the parsed token set before activating
|
||||
* 4. Activate the tokens workspace-wide (stored in Supabase + canvas store)
|
||||
* 5. View version history of previously uploaded token files + restore any version
|
||||
*
|
||||
* spec: SOURCE-AWARE-CANVAS.md Phase 6 §9.5
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useCanvas } from '@/store/canvas';
|
||||
import type { DesignToken } from '@/store/canvas.types';
|
||||
import type { DesignLanguageFile } from '@originmain/origin-graph';
|
||||
|
||||
// ── Upload & validation states ─────────────────────────────────────────────────
|
||||
|
||||
type ParseState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'parsing' }
|
||||
| { status: 'parsed'; tokens: DesignToken[]; filename: string; rawJson: string }
|
||||
| { status: 'error'; message: string };
|
||||
|
||||
type ActivateState = 'idle' | 'activating' | 'done' | 'error';
|
||||
type FetchUrlState = 'idle' | 'fetching' | 'error';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function parseTokenFileClient(jsonText: string): Promise<DesignToken[]> {
|
||||
const { parseTokenFileJson } = await import('@originmain/design-language');
|
||||
return parseTokenFileJson(jsonText) as DesignToken[];
|
||||
}
|
||||
|
||||
function groupByCategory(tokens: DesignToken[]): Record<string, DesignToken[]> {
|
||||
const groups: Record<string, DesignToken[]> = {};
|
||||
for (const t of tokens) {
|
||||
const g = t.group;
|
||||
(groups[g] ??= []).push(t);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function relativeTime(iso: string): string {
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
const mins = Math.floor(diff / 60_000);
|
||||
const hours = Math.floor(diff / 3_600_000);
|
||||
const days = Math.floor(diff / 86_400_000);
|
||||
if (mins < 2) return 'just now';
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function DesignLanguagePage() {
|
||||
const { designLanguageTokens, setDesignLanguageTokens, workspaceId } = useCanvas();
|
||||
const [parseState, setParseState] = useState<ParseState>({ status: 'idle' });
|
||||
const [activateState, setActivateState] = useState<ActivateState>('idle');
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [fetchUrl, setFetchUrl] = useState('');
|
||||
const [fetchUrlState, setFetchUrlState] = useState<FetchUrlState>('idle');
|
||||
const [fetchUrlError, setFetchUrlError] = useState('');
|
||||
const [history, setHistory] = useState<DesignLanguageFile[]>([]);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// ── Load version history ────────────────────────────────────────────────────
|
||||
|
||||
const loadHistory = useCallback(async () => {
|
||||
if (!workspaceId) return;
|
||||
setHistoryLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/design-language?workspaceId=${workspaceId}&all=1`);
|
||||
if (res.ok) {
|
||||
const data = await res.json() as DesignLanguageFile[];
|
||||
setHistory(Array.isArray(data) ? data : []);
|
||||
}
|
||||
} catch { /* non-fatal */ }
|
||||
finally { setHistoryLoading(false); }
|
||||
}, [workspaceId]);
|
||||
|
||||
useEffect(() => { void loadHistory(); }, [loadHistory]);
|
||||
|
||||
// ── File handling ───────────────────────────────────────────────────────────
|
||||
|
||||
const processJsonText = useCallback(async (text: string, filename: string) => {
|
||||
setParseState({ status: 'parsing' });
|
||||
try {
|
||||
const tokens = await parseTokenFileClient(text);
|
||||
if (tokens.length === 0) {
|
||||
setParseState({ status: 'error', message: 'No tokens found. Check the file format.' });
|
||||
return;
|
||||
}
|
||||
setParseState({ status: 'parsed', tokens, filename, rawJson: text });
|
||||
setActivateState('idle');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setParseState({ status: 'error', message: `Parse failed: ${msg}` });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const processFile = useCallback(async (file: File) => {
|
||||
if (!file.name.endsWith('.json')) {
|
||||
setParseState({ status: 'error', message: 'Only .json token files are supported.' });
|
||||
return;
|
||||
}
|
||||
const text = await file.text();
|
||||
await processJsonText(text, file.name);
|
||||
}, [processJsonText]);
|
||||
|
||||
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) void processFile(file);
|
||||
e.target.value = '';
|
||||
}, [processFile]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) void processFile(file);
|
||||
}, [processFile]);
|
||||
|
||||
// ── Fetch from URL ──────────────────────────────────────────────────────────
|
||||
|
||||
const handleFetchUrl = useCallback(async () => {
|
||||
const url = fetchUrl.trim();
|
||||
if (!url) return;
|
||||
if (!url.startsWith('https://')) {
|
||||
setFetchUrlState('error');
|
||||
setFetchUrlError('Only https:// URLs are supported.');
|
||||
return;
|
||||
}
|
||||
setFetchUrlState('fetching');
|
||||
setFetchUrlError('');
|
||||
try {
|
||||
const res = await fetch('/api/design-language/fetch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json() as { error?: string };
|
||||
throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||
}
|
||||
const { json } = await res.json() as { json: string };
|
||||
const filename = url.split('/').pop() ?? 'tokens.json';
|
||||
await processJsonText(json, filename);
|
||||
setFetchUrlState('idle');
|
||||
} catch (err) {
|
||||
setFetchUrlState('error');
|
||||
setFetchUrlError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}, [fetchUrl, processJsonText]);
|
||||
|
||||
// ── Activation ──────────────────────────────────────────────────────────────
|
||||
|
||||
const activate = useCallback(async () => {
|
||||
if (parseState.status !== 'parsed') return;
|
||||
if (!workspaceId) {
|
||||
setActivateState('error');
|
||||
return;
|
||||
}
|
||||
setActivateState('activating');
|
||||
try {
|
||||
// 1. Parse the raw JSON to a schema_jsonb object for Supabase.
|
||||
const schemaObj = JSON.parse(parseState.rawJson) as Record<string, unknown>;
|
||||
|
||||
// 2. Persist to Supabase — this triggers the Realtime subscription in
|
||||
// AppChrome so all other sessions update their token store too.
|
||||
const res = await fetch('/api/design-language', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspace_id: workspaceId,
|
||||
name: parseState.filename,
|
||||
schema_jsonb: schemaObj,
|
||||
is_active: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json() as { error?: string };
|
||||
throw new Error(body.error ?? `HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
// 3. Update local canvas store immediately (don't wait for Realtime).
|
||||
setDesignLanguageTokens(parseState.tokens);
|
||||
setActivateState('done');
|
||||
|
||||
// 4. Refresh version history.
|
||||
void loadHistory();
|
||||
} catch (err) {
|
||||
console.error('[DLF] Activation failed:', err);
|
||||
setActivateState('error');
|
||||
}
|
||||
}, [parseState, workspaceId, setDesignLanguageTokens, loadHistory]);
|
||||
|
||||
const deactivate = useCallback(() => {
|
||||
setDesignLanguageTokens(null);
|
||||
setParseState({ status: 'idle' });
|
||||
setActivateState('idle');
|
||||
}, [setDesignLanguageTokens]);
|
||||
|
||||
// ── Restore a historical version ────────────────────────────────────────────
|
||||
|
||||
const restoreVersion = useCallback(async (row: DesignLanguageFile) => {
|
||||
if (!workspaceId) return;
|
||||
try {
|
||||
const { parseTokenFile } = await import('@originmain/design-language');
|
||||
const tokens = parseTokenFile(row.schema_jsonb as Record<string, unknown>) as DesignToken[];
|
||||
|
||||
const res = await fetch('/api/design-language', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspace_id: workspaceId,
|
||||
name: row.name,
|
||||
schema_jsonb: row.schema_jsonb,
|
||||
is_active: true,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error('Restore failed');
|
||||
setDesignLanguageTokens(tokens);
|
||||
void loadHistory();
|
||||
} catch (err) {
|
||||
console.error('[DLF] Restore failed:', err);
|
||||
}
|
||||
}, [workspaceId, setDesignLanguageTokens, loadHistory]);
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
background: '#0F1117',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontFamily: "'Inter', system-ui, sans-serif",
|
||||
padding: '48px 40px',
|
||||
maxWidth: 800,
|
||||
margin: '0 auto',
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 40 }}>
|
||||
<h1 style={{
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
margin: 0,
|
||||
marginBottom: 8,
|
||||
letterSpacing: '-0.03em',
|
||||
}}>
|
||||
Design Language
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.8125rem', color: 'rgba(255,255,255,0.4)', margin: 0, lineHeight: 1.6 }}>
|
||||
Upload a design token file to enable token-aware inputs and constraint checking across all artboards.
|
||||
Supports Style Dictionary, W3C DTCG, and flat CSS variable formats.
|
||||
</p>
|
||||
{!workspaceId && (
|
||||
<div style={{ marginTop: 12, padding: '8px 12px', background: 'rgba(255,186,0,0.08)', border: '1px solid rgba(255,186,0,0.25)', borderRadius: 8, fontSize: '0.75rem', color: '#FFBA7B' }}>
|
||||
⚠ Open a workspace in the canvas first to enable saving token files.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Active token set status */}
|
||||
{designLanguageTokens && (
|
||||
<ActiveTokenBanner tokens={designLanguageTokens} onDeactivate={deactivate} />
|
||||
)}
|
||||
|
||||
{/* Upload area */}
|
||||
<UploadZone
|
||||
dragOver={dragOver}
|
||||
onDragOver={() => setDragOver(true)}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{/* Fetch from URL */}
|
||||
<FetchFromUrl
|
||||
value={fetchUrl}
|
||||
onChange={setFetchUrl}
|
||||
state={fetchUrlState}
|
||||
error={fetchUrlError}
|
||||
onFetch={() => void handleFetchUrl()}
|
||||
/>
|
||||
|
||||
{/* Parse state feedback */}
|
||||
{parseState.status === 'parsing' && (
|
||||
<StatusCard>
|
||||
<Spinner />
|
||||
<span style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.5)' }}>Parsing token file…</span>
|
||||
</StatusCard>
|
||||
)}
|
||||
|
||||
{parseState.status === 'error' && (
|
||||
<StatusCard color="rgba(255,80,80,0.08)" border="rgba(255,80,80,0.3)">
|
||||
<span style={{ fontSize: '0.8125rem', color: '#FF8080' }}>⚠ {parseState.message}</span>
|
||||
</StatusCard>
|
||||
)}
|
||||
|
||||
{parseState.status === 'parsed' && (
|
||||
<TokenPreview
|
||||
tokens={parseState.tokens}
|
||||
filename={parseState.filename}
|
||||
activateState={activateState}
|
||||
workspaceId={workspaceId}
|
||||
onActivate={() => void activate()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Version history */}
|
||||
{(history.length > 0 || historyLoading) && (
|
||||
<VersionHistory
|
||||
rows={history}
|
||||
loading={historyLoading}
|
||||
onRestore={(row) => void restoreVersion(row)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Format reference */}
|
||||
<FormatReference />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Sub-components ────────────────────────────────────────────────────────────
|
||||
|
||||
function ActiveTokenBanner({ tokens, onDeactivate }: { tokens: DesignToken[]; onDeactivate: () => void }) {
|
||||
const groups = groupByCategory(tokens);
|
||||
return (
|
||||
<div style={{
|
||||
padding: '14px 18px',
|
||||
background: 'rgba(125,211,168,0.06)',
|
||||
border: '1px solid rgba(125,211,168,0.25)',
|
||||
borderRadius: 10,
|
||||
marginBottom: 28,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
}}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#7DD3A8', flexShrink: 0, boxShadow: '0 0 8px rgba(125,211,168,0.6)' }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.6875rem', color: '#7DD3A8', fontWeight: 600, marginBottom: 3 }}>
|
||||
{tokens.length} tokens active
|
||||
</div>
|
||||
<div style={{ fontSize: '0.625rem', color: 'rgba(255,255,255,0.35)', fontFamily: "'JetBrains Mono', monospace" }}>
|
||||
{Object.entries(groups).map(([g, ts]) => `${g}:${ts.length}`).join(' · ')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDeactivate}
|
||||
style={{
|
||||
background: 'none', border: '1px solid rgba(255,80,80,0.3)', borderRadius: 6,
|
||||
color: '#FF8080', padding: '5px 12px', cursor: 'pointer',
|
||||
fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem',
|
||||
letterSpacing: '0.04em', transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(255,80,80,0.08)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = 'none')}
|
||||
>
|
||||
Deactivate
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UploadZone({
|
||||
dragOver, onDragOver, onDragLeave, onDrop, onClick,
|
||||
}: {
|
||||
dragOver: boolean;
|
||||
onDragOver: () => void;
|
||||
onDragLeave: () => void;
|
||||
onDrop: (e: React.DragEvent) => void;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onDragOver={e => { e.preventDefault(); onDragOver(); }}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
style={{
|
||||
border: `2px dashed ${dragOver ? '#3385FF' : 'rgba(255,255,255,0.15)'}`,
|
||||
borderRadius: 12,
|
||||
padding: '36px 24px',
|
||||
textAlign: 'center',
|
||||
cursor: 'pointer',
|
||||
background: dragOver ? 'rgba(51,133,255,0.06)' : 'rgba(255,255,255,0.02)',
|
||||
transition: 'border-color 0.15s, background 0.15s',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '1.5rem', marginBottom: 10, opacity: 0.4 }}>📂</div>
|
||||
<div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.75rem', color: 'rgba(255,255,255,0.6)', marginBottom: 6 }}>
|
||||
Drop token file here or click to browse
|
||||
</div>
|
||||
<div style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.625rem', color: 'rgba(255,255,255,0.25)' }}>
|
||||
.json — Style Dictionary · W3C DTCG · Flat CSS vars
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FetchFromUrl({
|
||||
value, onChange, state, error, onFetch,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
state: FetchUrlState;
|
||||
error: string;
|
||||
onFetch: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<input
|
||||
type="url"
|
||||
placeholder="Import from URL https://cdn.example.com/tokens.json"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') onFetch(); e.stopPropagation(); }}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.6875rem',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: `1px solid ${state === 'error' ? 'rgba(255,80,80,0.4)' : 'rgba(255,255,255,0.12)'}`,
|
||||
borderRadius: 8,
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
padding: '9px 14px',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.15s',
|
||||
}}
|
||||
onFocus={e => (e.target.style.borderColor = '#3385FF')}
|
||||
onBlur={e => (e.target.style.borderColor = state === 'error' ? 'rgba(255,80,80,0.4)' : 'rgba(255,255,255,0.12)')}
|
||||
/>
|
||||
<button
|
||||
onClick={onFetch}
|
||||
disabled={state === 'fetching' || !value.trim()}
|
||||
style={{
|
||||
padding: '9px 18px',
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: 8,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: '0.75rem',
|
||||
cursor: state === 'fetching' || !value.trim() ? 'not-allowed' : 'pointer',
|
||||
opacity: state === 'fetching' || !value.trim() ? 0.5 : 1,
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={e => { if (state !== 'fetching') e.currentTarget.style.background = 'rgba(255,255,255,0.1)'; }}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = 'rgba(255,255,255,0.06)')}
|
||||
>
|
||||
{state === 'fetching' ? 'Fetching…' : 'Fetch'}
|
||||
</button>
|
||||
</div>
|
||||
{state === 'error' && error && (
|
||||
<div style={{ marginTop: 6, fontSize: '0.6875rem', color: '#FF8080', fontFamily: "'Inter', sans-serif" }}>
|
||||
⚠ {error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusCard({ children, color, border }: { children: React.ReactNode; color?: string; border?: string }) {
|
||||
return (
|
||||
<div style={{
|
||||
padding: '14px 18px',
|
||||
background: color ?? 'rgba(255,255,255,0.04)',
|
||||
border: `1px solid ${border ?? 'rgba(255,255,255,0.12)'}`,
|
||||
borderRadius: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Spinner() {
|
||||
return (
|
||||
<div style={{
|
||||
width: 14, height: 14, borderRadius: '50%',
|
||||
border: '2px solid rgba(255,255,255,0.15)',
|
||||
borderTopColor: '#3385FF',
|
||||
animation: 'spin 0.8s linear infinite',
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
function TokenPreview({
|
||||
tokens, filename, activateState, workspaceId, onActivate,
|
||||
}: {
|
||||
tokens: DesignToken[];
|
||||
filename: string;
|
||||
activateState: ActivateState;
|
||||
workspaceId: string | null;
|
||||
onActivate: () => void;
|
||||
}) {
|
||||
const groups = groupByCategory(tokens);
|
||||
const [expanded, setExpanded] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
{/* Preview header */}
|
||||
<div style={{
|
||||
padding: '14px 18px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.07)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
}}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.625rem', color: '#7DD3A8' }}>✓ Parsed</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.6875rem', color: 'rgba(255,255,255,0.7)', flex: 1 }}>{filename}</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color: 'rgba(255,255,255,0.35)' }}>
|
||||
{tokens.length} tokens · {Object.keys(groups).length} groups
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Group list */}
|
||||
{Object.entries(groups).map(([group, groupTokens]) => (
|
||||
<div key={group} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<button
|
||||
onClick={() => setExpanded(expanded === group ? null : group)}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', padding: '10px 18px', background: 'none', border: 'none', cursor: 'pointer', textAlign: 'left' }}
|
||||
>
|
||||
<span style={{ fontSize: '0.5rem', color: 'rgba(255,255,255,0.25)' }}>{expanded === group ? '▼' : '▶'}</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.6875rem', color: 'rgba(255,255,255,0.75)', flex: 1, textTransform: 'capitalize' }}>{group}</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5rem', color: 'rgba(255,255,255,0.25)' }}>{groupTokens.length}</span>
|
||||
<div style={{ display: 'flex', gap: 3 }}>
|
||||
{groupTokens.filter(t => t.type === 'color').slice(0, 6).map(t => (
|
||||
<div key={t.key} style={{ width: 12, height: 12, borderRadius: 2, background: t.rawValue, border: '1px solid rgba(255,255,255,0.1)' }} />
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
{expanded === group && (
|
||||
<div style={{ paddingBottom: 8 }}>
|
||||
{groupTokens.map(token => (
|
||||
<div key={token.key} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '5px 18px 5px 36px' }}>
|
||||
{token.type === 'color' && (
|
||||
<div style={{ width: 14, height: 14, borderRadius: 3, background: token.rawValue, flexShrink: 0, border: '1px solid rgba(255,255,255,0.15)' }} />
|
||||
)}
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color: '#3385FF', flex: 1, letterSpacing: '-0.01em' }}>{token.key}</span>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color: 'rgba(255,255,255,0.4)', letterSpacing: '-0.01em' }}>{token.rawValue}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Activate button */}
|
||||
<div style={{ padding: '14px 18px', display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<button
|
||||
onClick={onActivate}
|
||||
disabled={activateState === 'activating' || activateState === 'done' || !workspaceId}
|
||||
title={!workspaceId ? 'Open a workspace in the canvas first' : undefined}
|
||||
style={{
|
||||
padding: '9px 20px',
|
||||
background: activateState === 'done' ? 'rgba(125,211,168,0.15)' : '#3385FF',
|
||||
border: `1px solid ${activateState === 'done' ? 'rgba(125,211,168,0.4)' : 'transparent'}`,
|
||||
borderRadius: 7,
|
||||
color: activateState === 'done' ? '#7DD3A8' : 'white',
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
cursor: (activateState === 'activating' || activateState === 'done' || !workspaceId) ? 'not-allowed' : 'pointer',
|
||||
opacity: (activateState === 'activating' || !workspaceId) ? 0.6 : 1,
|
||||
transition: 'background 0.15s, opacity 0.15s',
|
||||
}}
|
||||
>
|
||||
{activateState === 'activating' ? 'Saving…' :
|
||||
activateState === 'done' ? '✓ Tokens saved & activated' :
|
||||
`Activate ${tokens.length} tokens`}
|
||||
</button>
|
||||
{activateState === 'error' && (
|
||||
<span style={{ fontSize: '0.625rem', color: '#FF8080', fontFamily: "'Inter', sans-serif" }}>
|
||||
Save failed — check console for details
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionHistory({
|
||||
rows, loading, onRestore,
|
||||
}: {
|
||||
rows: DesignLanguageFile[];
|
||||
loading: boolean;
|
||||
onRestore: (row: DesignLanguageFile) => void;
|
||||
}) {
|
||||
if (loading && rows.length === 0) {
|
||||
return (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<SectionTitle>Version History</SectionTitle>
|
||||
<StatusCard><Spinner /><span style={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.4)' }}>Loading…</span></StatusCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 32 }}>
|
||||
<SectionTitle>Version History</SectionTitle>
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{rows.map((row, i) => (
|
||||
<div
|
||||
key={row.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '10px 16px',
|
||||
borderBottom: i < rows.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
|
||||
background: row.is_active ? 'rgba(125,211,168,0.04)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
{/* Version badge */}
|
||||
<div style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.5625rem',
|
||||
color: row.is_active ? '#7DD3A8' : 'rgba(255,255,255,0.3)',
|
||||
background: row.is_active ? 'rgba(125,211,168,0.12)' : 'rgba(255,255,255,0.06)',
|
||||
border: `1px solid ${row.is_active ? 'rgba(125,211,168,0.3)' : 'rgba(255,255,255,0.1)'}`,
|
||||
borderRadius: 4,
|
||||
padding: '2px 7px',
|
||||
flexShrink: 0,
|
||||
minWidth: 36,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
v{row.version}
|
||||
</div>
|
||||
|
||||
{/* File name */}
|
||||
<span style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.6875rem',
|
||||
color: row.is_active ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.5)',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{row.name}
|
||||
</span>
|
||||
|
||||
{/* Timestamp */}
|
||||
<span style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.5875rem', color: 'rgba(255,255,255,0.25)', flexShrink: 0 }}>
|
||||
{relativeTime(row.created_at)}
|
||||
</span>
|
||||
|
||||
{/* Active indicator OR restore button */}
|
||||
{row.is_active ? (
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5rem', color: '#7DD3A8', letterSpacing: '0.06em' }}>ACTIVE</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onRestore(row)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
borderRadius: 5,
|
||||
color: 'rgba(255,255,255,0.45)',
|
||||
padding: '3px 10px',
|
||||
cursor: 'pointer',
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
fontSize: '0.5875rem',
|
||||
transition: 'border-color 0.1s, color 0.1s',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = '#3385FF';
|
||||
e.currentTarget.style.color = '#3385FF';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.15)';
|
||||
e.currentTarget.style.color = 'rgba(255,255,255,0.45)';
|
||||
}}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.5875rem',
|
||||
color: 'rgba(255,255,255,0.3)',
|
||||
letterSpacing: '0.08em',
|
||||
textTransform: 'uppercase',
|
||||
marginBottom: 12,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormatReference() {
|
||||
return (
|
||||
<details style={{ marginTop: 8 }}>
|
||||
<summary style={{
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
fontSize: '0.5875rem',
|
||||
color: 'rgba(255,255,255,0.3)',
|
||||
cursor: 'pointer',
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
userSelect: 'none',
|
||||
listStyle: 'none',
|
||||
}}>
|
||||
Supported formats ↓
|
||||
</summary>
|
||||
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{[
|
||||
{ label: 'W3C DTCG', desc: '$value / $type fields', example: '{\n "color": {\n "primary": { "$value": "#0066FF", "$type": "color" }\n }\n}' },
|
||||
{ label: 'Style Dictionary', desc: 'Nested with value field', example: '{\n "color": {\n "primary": { "value": "#0066FF" }\n }\n}' },
|
||||
{ label: 'Flat CSS Variables', desc: 'All keys start with --', example: '{\n "--color-primary": "#0066FF",\n "--spacing-md": "16px"\n}' },
|
||||
].map(({ label, desc, example }) => (
|
||||
<div key={label} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<div style={{ padding: '10px 14px 6px', display: 'flex', gap: 10, alignItems: 'baseline' }}>
|
||||
<span style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: '0.6875rem', color: 'rgba(255,255,255,0.7)', fontWeight: 600 }}>{label}</span>
|
||||
<span style={{ fontFamily: "'Inter', sans-serif", fontSize: '0.5875rem', color: 'rgba(255,255,255,0.3)' }}>{desc}</span>
|
||||
</div>
|
||||
<pre style={{ margin: 0, padding: '8px 14px 12px', fontFamily: "'JetBrains Mono', monospace", fontSize: '0.5625rem', color: '#7EB8FF', background: 'rgba(0,0,0,0.2)', overflow: 'auto', lineHeight: 1.65 }}>
|
||||
{example}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { SignIn } from '@clerk/nextjs';
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
|
||||
export default function SignInPage() {
|
||||
const T = useCanvasTheme();
|
||||
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
// Use the exact canvas background tokens so this page matches the
|
||||
// product's dark / light canvas aesthetic.
|
||||
background: T.canvasBg,
|
||||
backgroundImage: `radial-gradient(circle, ${T.dotColor} 1px, transparent 1px)`,
|
||||
backgroundSize: '24px 24px',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
<SignIn />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { SignUp } from '@clerk/nextjs';
|
||||
import { useCanvasTheme } from '@/store/canvasTheme';
|
||||
|
||||
export default function SignUpPage() {
|
||||
const T = useCanvasTheme();
|
||||
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
// Use the exact canvas background tokens so this page matches the
|
||||
// product's dark / light canvas aesthetic.
|
||||
background: T.canvasBg,
|
||||
backgroundImage: `radial-gradient(circle, ${T.dotColor} 1px, transparent 1px)`,
|
||||
backgroundSize: '24px 24px',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
<SignUp />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { AppHeader } from '@/components/shell/AppHeader';
|
||||
|
||||
export default function WorkspaceError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error('[WorkspaceError]', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'var(--page-bg)', fontFamily: "'Inter', -apple-system, sans-serif" }}>
|
||||
<AppHeader breadcrumbs={[{ label: 'Workspaces', href: '/workspaces' }]} />
|
||||
<main style={{ maxWidth: 480, margin: '80px auto', padding: '0 24px', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '2rem', marginBottom: 16 }}>⚠️</div>
|
||||
<h1 style={{ fontSize: '1.125rem', fontWeight: 700, color: '#0A0A0A', margin: '0 0 8px' }}>
|
||||
Failed to load workspace
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.875rem', color: '#71717A', margin: '0 0 24px', lineHeight: 1.6 }}>
|
||||
{error.message}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
padding: '9px 20px', borderRadius: 8, fontSize: '0.875rem',
|
||||
fontWeight: 600, background: '#0A0A0A', color: '#FFFFFF',
|
||||
border: 'none', cursor: 'pointer', fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<Link
|
||||
href="/workspaces"
|
||||
style={{
|
||||
padding: '9px 20px', borderRadius: 8, fontSize: '0.875rem',
|
||||
fontWeight: 600, background: '#F4F4F5', color: '#3F3F46',
|
||||
textDecoration: 'none', display: 'inline-flex', alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
All workspaces
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { AppHeader } from '@/components/shell/AppHeader';
|
||||
import { ProjectCard } from '@/components/shell/ProjectCard';
|
||||
import { DesignLanguageUpload } from '@/components/shell/DesignLanguageUpload';
|
||||
import { TourAutoStart } from '@/components/walkthrough/TourAutoStart';
|
||||
import { getActiveDesignLanguageFile } from '@originmain/origin-graph';
|
||||
import type { Workspace, Project } from '@originmain/origin-graph';
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ wid: string }> }) {
|
||||
const { wid } = await params;
|
||||
const db = serverClient();
|
||||
const result = (await db.from('workspaces').select('name').eq('id', wid).single()) as unknown as { data: { name: string } | null };
|
||||
return { title: `${result.data?.name ?? 'Workspace'} — Originmain` };
|
||||
}
|
||||
|
||||
export default async function WorkspacePage({ params }: { params: Promise<{ wid: string }> }) {
|
||||
const { wid } = await params;
|
||||
const user = await currentUser();
|
||||
if (!user) redirect('/sign-in');
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress ?? '';
|
||||
const db = serverClient();
|
||||
|
||||
// Verify membership
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', wid)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!member) redirect('/workspaces');
|
||||
|
||||
// Fetch workspace + projects + design language in parallel
|
||||
const [{ data: wsData }, { data: projectsData }, designLanguage] = await Promise.all([
|
||||
db.from('workspaces').select('*').eq('id', wid).single(),
|
||||
db.from('projects').select('*').eq('workspace_id', wid).order('created_at', { ascending: true }),
|
||||
getActiveDesignLanguageFile(db, wid).catch(() => null),
|
||||
]);
|
||||
|
||||
if (!wsData) redirect('/workspaces');
|
||||
|
||||
const workspace = wsData as Workspace;
|
||||
const projects = (projectsData ?? []) as Project[];
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100dvh', background: 'var(--page-bg)', fontFamily: "'Inter', -apple-system, sans-serif" }}>
|
||||
{/* Auto-start tour for first-time visitors (client component, renders null) */}
|
||||
<TourAutoStart />
|
||||
<AppHeader breadcrumbs={[
|
||||
{ label: 'Workspaces', href: '/workspaces' },
|
||||
{ label: workspace.name },
|
||||
]} />
|
||||
|
||||
<main style={{ maxWidth: 960, margin: '0 auto', padding: '48px 24px' }}>
|
||||
|
||||
{/* Title row — data-tour="projects-section" anchors the tour spotlight */}
|
||||
<div data-tour="projects-section" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 32 }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 700, letterSpacing: '-0.03em', color: 'var(--page-text)', margin: 0 }}>
|
||||
Projects
|
||||
</h1>
|
||||
<p style={{ margin: '4px 0 0', fontSize: '0.875rem', color: 'var(--card-muted)' }}>
|
||||
{projects.length === 0
|
||||
? 'No projects yet — connect your first app to get started'
|
||||
: `${projects.length} project${projects.length !== 1 ? 's' : ''}`}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
<Link href={`/workspace/${wid}/plugins`} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: 'transparent', color: 'var(--card-muted)',
|
||||
fontSize: '0.875rem', fontWeight: 500,
|
||||
padding: '9px 16px', borderRadius: 9,
|
||||
textDecoration: 'none', border: '1px solid var(--card-border)',
|
||||
}}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2a2 2 0 0 1 2 2v1h3a1 1 0 0 1 1 1v3h1a2 2 0 0 1 0 4h-1v3a1 1 0 0 1-1 1h-3v1a2 2 0 0 1-4 0v-1H7a1 1 0 0 1-1-1v-3H5a2 2 0 0 1 0-4h1V6a1 1 0 0 1 1-1h3V4a2 2 0 0 1 2-2z" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
Plugins
|
||||
</Link>
|
||||
<Link href={`/workspace/${wid}/settings`} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: 'transparent', color: 'var(--card-muted)',
|
||||
fontSize: '0.875rem', fontWeight: 500,
|
||||
padding: '9px 16px', borderRadius: 9,
|
||||
textDecoration: 'none', border: '1px solid var(--card-border)',
|
||||
}}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="2" stroke="currentColor" strokeWidth="1.4"/>
|
||||
<path d="M7 1v1.5M7 11.5V13M1 7h1.5M11.5 7H13M2.93 2.93l1.06 1.06M10.01 10.01l1.06 1.06M2.93 11.07l1.06-1.06M10.01 3.99l1.06-1.06" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/>
|
||||
</svg>
|
||||
Settings
|
||||
</Link>
|
||||
<Link
|
||||
href={`/workspace/${wid}/project/new`}
|
||||
data-tour="new-project-btn"
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: 'var(--btn-bg)', color: 'var(--btn-fg)',
|
||||
fontSize: '0.875rem', fontWeight: 600,
|
||||
padding: '9px 18px', borderRadius: 9,
|
||||
textDecoration: 'none', letterSpacing: '-0.01em',
|
||||
}}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M7 1v12M1 7h12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
|
||||
</svg>
|
||||
New project
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{projects.length === 0 && (
|
||||
<div style={{
|
||||
background: 'var(--card-bg)', border: '1px dashed var(--card-border)',
|
||||
borderRadius: 16, padding: '56px 40px', textAlign: 'center',
|
||||
transition: 'background 0.2s',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: 14,
|
||||
background: 'rgba(0,102,255,0.07)', border: '1px solid rgba(0,102,255,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 20px',
|
||||
}}>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
|
||||
<rect x="2" y="4" width="18" height="14" rx="2.5" stroke="#0066FF" strokeWidth="1.5"/>
|
||||
<path d="M7 9.5l2.5 2.5L15 7" stroke="#0066FF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--card-text)', margin: '0 0 8px' }}>
|
||||
Connect your first app
|
||||
</h3>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--card-muted)', margin: '0 0 24px', maxWidth: 360, marginLeft: 'auto', marginRight: 'auto', lineHeight: 1.6 }}>
|
||||
A project connects to your running application — local or staging — so you can inspect components and ship intent-level diffs.
|
||||
</p>
|
||||
<Link href={`/workspace/${wid}/project/new`} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: '#0066FF', color: '#FFFFFF',
|
||||
fontSize: '0.875rem', fontWeight: 600,
|
||||
padding: '10px 20px', borderRadius: 9, textDecoration: 'none',
|
||||
}}>
|
||||
Create a project →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project cards */}
|
||||
{projects.length > 0 && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 16 }}>
|
||||
{projects.map(project => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
workspaceId={wid}
|
||||
projectId={project.id}
|
||||
name={project.name}
|
||||
framework={project.framework}
|
||||
appUrl={project.app_url}
|
||||
description={project.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Design Language */}
|
||||
<DesignLanguageUpload workspaceId={wid} current={designLanguage} />
|
||||
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { currentUser } from '@clerk/nextjs/server';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { serverClient } from '@/lib/supabase';
|
||||
import { AppHeader } from '@/components/shell/AppHeader';
|
||||
import type { Workspace } from '@originmain/origin-graph';
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ wid: string }> }) {
|
||||
const { wid } = await params;
|
||||
const db = serverClient();
|
||||
const result = (await db.from('workspaces').select('name').eq('id', wid).single()) as unknown as { data: { name: string } | null };
|
||||
return { title: `Plugins — ${result.data?.name ?? 'Workspace'} — Originmain` };
|
||||
}
|
||||
|
||||
export default async function WorkspacePluginsPage({ params }: { params: Promise<{ wid: string }> }) {
|
||||
const { wid } = await params;
|
||||
const user = await currentUser();
|
||||
if (!user) redirect('/sign-in');
|
||||
|
||||
const email = user.primaryEmailAddress?.emailAddress ?? '';
|
||||
const db = serverClient();
|
||||
|
||||
// Verify membership
|
||||
const { data: member } = await db
|
||||
.from('team_members')
|
||||
.select('id')
|
||||
.eq('workspace_id', wid)
|
||||
.eq('email', email)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (!member) redirect('/workspaces');
|
||||
|
||||
const { data: ws } = await db.from('workspaces').select('*').eq('id', wid).single();
|
||||
const workspace = ws as Workspace | null;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100dvh', background: 'var(--page-bg)', fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" }}>
|
||||
<AppHeader workspaceName={workspace?.name ?? 'Workspace'} workspaceId={wid} />
|
||||
|
||||
<div style={{ maxWidth: 760, margin: '0 auto', padding: '48px 24px' }}>
|
||||
{/* Breadcrumb */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 32, fontSize: '0.8125rem', color: 'var(--card-muted)' }}>
|
||||
<Link href="/workspaces" style={{ color: 'var(--card-muted)', textDecoration: 'none' }}>Workspaces</Link>
|
||||
<span>/</span>
|
||||
<Link href={`/workspace/${wid}`} style={{ color: 'var(--card-muted)', textDecoration: 'none' }}>{workspace?.name ?? 'Workspace'}</Link>
|
||||
<span>/</span>
|
||||
<span style={{ color: 'var(--page-text)' }}>Plugins</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{ fontSize: '1.5rem', fontWeight: 700, letterSpacing: '-0.025em', color: 'var(--page-text)', margin: '0 0 8px' }}>
|
||||
Plugins
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.9375rem', color: 'var(--card-muted)', margin: '0 0 40px', lineHeight: 1.6 }}>
|
||||
Extend Originmain with custom completion zones, ingestion connectors, and design tools.
|
||||
</p>
|
||||
|
||||
{/* Coming soon card */}
|
||||
<div style={{
|
||||
background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 14,
|
||||
padding: '40px 36px', textAlign: 'center', transition: 'background 0.2s',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: 14,
|
||||
background: 'rgba(51,133,255,0.08)', border: '1px solid rgba(51,133,255,0.15)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
margin: '0 auto 20px',
|
||||
}}>
|
||||
{/* Puzzle icon */}
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2a2 2 0 0 1 2 2v1h3a1 1 0 0 1 1 1v3h1a2 2 0 0 1 0 4h-1v3a1 1 0 0 1-1 1h-3v1a2 2 0 0 1-4 0v-1H7a1 1 0 0 1-1-1v-3H5a2 2 0 0 1 0-4h1V6a1 1 0 0 1 1-1h3V4a2 2 0 0 1 2-2z" stroke="#3385FF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, color: 'var(--card-text)', margin: '0 0 10px', letterSpacing: '-0.015em' }}>
|
||||
Plugin marketplace — coming in Phase 4
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.875rem', color: 'var(--card-muted)', maxWidth: 480, margin: '0 auto 28px', lineHeight: 1.65 }}>
|
||||
The plugin API lets teams register custom AI completion zones, custom webhook ingesters, and design-language extensions — all sandboxed and permission-scoped.
|
||||
</p>
|
||||
|
||||
{/* Permissions preview */}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, justifyContent: 'center', marginBottom: 28 }}>
|
||||
{[
|
||||
'artboards:read', 'artboards:write', 'diffs:read', 'diffs:export',
|
||||
'completion-zones:register', 'ingesters:register', 'design-language:read',
|
||||
].map(scope => (
|
||||
<span key={scope} style={{
|
||||
fontSize: '0.6875rem', fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
background: 'rgba(51,133,255,0.07)', color: '#3385FF',
|
||||
border: '1px solid rgba(51,133,255,0.18)',
|
||||
borderRadius: 6, padding: '3px 8px',
|
||||
letterSpacing: '-0.01em',
|
||||
}}>
|
||||
{scope}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/workspace/${wid}/settings`}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
fontSize: '0.875rem', fontWeight: 500, color: '#3385FF',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Back to settings →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export default function CanvasError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
const params = useParams<{ wid: string }>();
|
||||
|
||||
useEffect(() => {
|
||||
console.error('[CanvasError]', error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '100vh',
|
||||
background: '#0C0C10',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: "'Inter', -apple-system, sans-serif",
|
||||
padding: 24,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 420,
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(220,38,38,0.08)',
|
||||
border: '1px solid rgba(220,38,38,0.15)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 20px',
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M10 6v4M10 14h.01" stroke="#DC2626" strokeWidth="1.8" strokeLinecap="round"/>
|
||||
<circle cx="10" cy="10" r="8.5" stroke="#DC2626" strokeWidth="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 700,
|
||||
color: 'rgba(255,255,255,0.88)',
|
||||
margin: '0 0 8px',
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
Canvas failed to load
|
||||
</h1>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '0.8125rem',
|
||||
color: 'rgba(255,255,255,0.38)',
|
||||
margin: '0 0 28px',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{error.message || 'An unexpected error occurred.'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
padding: '9px 20px',
|
||||
borderRadius: 8,
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: 'rgba(255,255,255,0.88)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
<Link
|
||||
href={params?.wid ? `/workspace/${params.wid}` : '/workspaces'}
|
||||
style={{
|
||||
padding: '9px 20px',
|
||||
borderRadius: 8,
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
background: 'transparent',
|
||||
color: 'rgba(255,255,255,0.42)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
textDecoration: 'none',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Back to workspace
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user