Skip to main content

Integration Builder v2 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace the monolithic 1,530-line form wizard with a no-code drag & drop canvas builder powered by React Flow, where developers visually compose API integration flows and generate executable code snippets.

Architecture: Data-driven block system with JSON configs per block/currency/chain/payment-method. React Flow canvas with custom nodes (collapsed/expanded), Zustand state management, and an engine layer (FlowEngine, CodeGenerator, BindingResolver, MarkdownGenerator) that transforms the visual graph into code and reports.

Tech Stack: React 19, TypeScript 5.6, Docusaurus 3.7.0, React Flow (@xyflow/react), Zustand, Framer Motion, CSS Modules, Lucide React, dagre (auto-layout), html-to-image (canvas export), lz-string (URL state)


Phase 0: Project Setup

Task 0.1: Create Branch and Install Dependencies

Files:

  • Modify: package.json

  • Step 1: Create feature branch

cd ~/Projects/front/integration-guide
git checkout main
git pull origin main
git checkout -b feat/integration-builder-v2
  • Step 2: Install dependencies
npm install @xyflow/react zustand framer-motion html-to-image dagre lz-string
npm install -D @types/dagre vitest @testing-library/react @testing-library/jest-dom jsdom
  • Step 3: Verify build still works
npm run build

Expected: Build succeeds with no errors.

  • Step 4: Commit
git add package.json package-lock.json
git commit -m "chore: add react-flow, zustand, and builder dependencies"

Task 0.2: Configure Vitest

Files:

  • Create: vitest.config.ts

  • Step 1: Create vitest config

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: [],
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
exclude: ['node_modules', 'build', '.docusaurus'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
});
  • Step 2: Add test script to package.json

Add to "scripts":

"test": "vitest run",
"test:watch": "vitest"
  • Step 3: Run vitest to confirm it works
npm test

Expected: "No test files found" (not an error — just no tests yet).

  • Step 4: Commit
git add vitest.config.ts package.json
git commit -m "chore: configure vitest for unit testing"

Phase 1: Type Definitions and Config System

Task 1.1: Core Type Definitions

Files:

  • Create: src/types/builder.ts

  • Step 1: Write the types file

// src/types/builder.ts

// --- Block Config Types ---

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

export type BlockCategory = 'auth' | 'account' | 'kyc' | 'operations' | 'beneficiaries' | 'admin';

export type FieldType =
| 'string'
| 'number'
| 'boolean'
| 'currency-select'
| 'chain-select'
| 'payment-method-select'
| 'select'
| 'json'
| 'uuid';

export interface ConfigField {
id: string;
label: string;
type: FieldType;
required?: boolean;
placeholder?: string;
default?: string | number | boolean;
options?: string[];
filter?: Record<string, string>;
description?: string;
}

export interface BlockInput {
type: string;
required: boolean;
from?: string; // e.g. "login.accessToken"
description?: string;
}

export interface BlockOutput {
type: string;
description: string;
}

export interface BlockDefinition {
id: string;
name: string;
category: BlockCategory;
icon: string; // Lucide icon name
description: string;
method: HttpMethod;
endpoint: string;
guideLink: string;
inputs: Record<string, BlockInput>;
outputs: Record<string, BlockOutput>;
configFields: ConfigField[];
dependencies?: string[]; // block IDs that must come before
}

// --- Currency/Chain/PaymentMethod Config Types ---

export type CurrencyType = 'fiat' | 'crypto';

export interface CurrencyDefinition {
id: string;
symbol: string;
name: string;
type: CurrencyType;
flag?: string; // ISO country code for fiat, empty for crypto
supportedAs: ('input' | 'output')[];
paymentMethods: string[];
chains: string[];
minAmount?: number;
decimalPlaces: number;
}

export interface ChainDefinition {
id: string;
name: string;
symbol: string;
currencies: string[];
sendMethods: ('PERMIT' | 'TRANSFER')[];
explorerUrl?: string;
}

export interface PaymentMethodDefinition {
id: string;
name: string;
type: 'fiat' | 'blockchain';
currencies: string[];
direction: ('input' | 'output')[];
icon?: string;
}

// --- Canvas State Types ---

export interface NodeConfig {
[fieldId: string]: string | number | boolean | undefined;
}

export interface BuilderNodeData {
blockId: string;
label: string;
icon: string;
method: HttpMethod;
endpoint: string;
category: BlockCategory;
guideLink: string;
config: NodeConfig;
expanded: boolean;
validationErrors: ValidationError[];
}

export interface Binding {
sourceNodeId: string;
sourceField: string;
targetField: string;
auto: boolean; // true = auto-detected, false = manual override
}

export interface BuilderEdgeData {
bindings: Binding[];
animated: boolean;
}

// --- Validation Types ---

export type ValidationSeverity = 'error' | 'warning' | 'info';

export interface ValidationError {
nodeId: string;
field?: string;
severity: ValidationSeverity;
message: string;
rule: string;
}

// --- Flow Output Types ---

export type CodeLanguage = 'curl' | 'node' | 'python' | 'go';

export type Environment = 'sandbox' | 'production' | 'developer';

export interface FlowStep {
order: number;
nodeId: string;
blockId: string;
name: string;
method: HttpMethod;
endpoint: string;
headers: Record<string, string>;
queryParams: Record<string, string>;
body: Record<string, unknown> | null;
bindings: Binding[];
guideLink: string;
description: string;
}

export interface GeneratedFlow {
steps: FlowStep[];
environment: Environment;
markdown: string;
code: Record<CodeLanguage, string>;
}

// --- Store Types ---

export type ThemeMode = 'dark' | 'light';

export interface BuilderState {
// Canvas
nodes: import('@xyflow/react').Node<BuilderNodeData>[];
edges: import('@xyflow/react').Edge<BuilderEdgeData>[];

// UI
sidebarOpen: boolean;
theme: ThemeMode;
environment: Environment;
selectedNodeId: string | null;
reportOpen: boolean;

// Generated
generatedFlow: GeneratedFlow | null;
validationErrors: ValidationError[];
}
  • Step 2: Verify TypeScript compiles
cd ~/Projects/front/integration-guide
npx tsc --noEmit src/types/builder.ts 2>&1 || true

Expected: May show module resolution warnings for @xyflow/react — that's fine as long as no type errors in the file itself.

  • Step 3: Commit
git add src/types/builder.ts
git commit -m "feat: add core type definitions for builder v2"

Task 1.2: Block Config JSON Files (Auth + Account)

Files:

  • Create: src/configs/blocks/login.json

  • Create: src/configs/blocks/validate-login.json

  • Create: src/configs/blocks/create-account.json

  • Create: src/configs/blocks/account-info.json

  • Create: src/configs/blocks/balances.json

  • Create: src/configs/blocks/metadata.json

  • Create: src/configs/blocks/limits.json

  • Create: src/configs/blocks/statement.json

  • Step 1: Create login block

{
"id": "login",
"name": "Login",
"category": "auth",
"icon": "log-in",
"description": "Send login email with verification code",
"method": "POST",
"endpoint": "/v2/auth/login",
"guideLink": "/docs/Avenia-Account-Management/login-guide",
"inputs": {},
"outputs": {},
"configFields": [
{
"id": "email",
"label": "Email",
"type": "string",
"required": true,
"placeholder": "user@example.com"
},
{
"id": "password",
"label": "Password",
"type": "string",
"required": true,
"placeholder": "********"
}
],
"dependencies": []
}
  • Step 2: Create validate-login block
{
"id": "validate-login",
"name": "Validate Login",
"category": "auth",
"icon": "shield-check",
"description": "Confirm email token and receive JWT access token",
"method": "POST",
"endpoint": "/v2/auth/validate-login",
"guideLink": "/docs/Avenia-Account-Management/login-guide",
"inputs": {},
"outputs": {
"accessToken": { "type": "token", "description": "JWT access token for authenticated requests" },
"refreshToken": { "type": "token", "description": "Refresh token for renewing access" }
},
"configFields": [
{
"id": "email",
"label": "Email",
"type": "string",
"required": true,
"placeholder": "user@example.com"
},
{
"id": "emailToken",
"label": "Email Token",
"type": "string",
"required": true,
"placeholder": "000000 (dev bypass)",
"description": "6-digit code from email. Use 000000 in developer env."
}
],
"dependencies": ["login"]
}
  • Step 3: Create create-account block
{
"id": "create-account",
"name": "Create Account",
"category": "auth",
"icon": "user-plus",
"description": "Register a new user account",
"method": "POST",
"endpoint": "/v2/auth/create",
"guideLink": "/docs/Avenia-Account-Management/about-login",
"inputs": {},
"outputs": {},
"configFields": [
{
"id": "email",
"label": "Email",
"type": "string",
"required": true,
"placeholder": "user@example.com"
},
{
"id": "password",
"label": "Password",
"type": "string",
"required": true,
"placeholder": "********"
},
{
"id": "confirmPassword",
"label": "Confirm Password",
"type": "string",
"required": true,
"placeholder": "********"
},
{
"id": "name",
"label": "Full Name",
"type": "string",
"required": true,
"placeholder": "John Doe"
},
{
"id": "countryTaxResidence",
"label": "Country (ISO alpha-3)",
"type": "string",
"required": true,
"placeholder": "BRA"
},
{
"id": "countrySubdivisionTaxResidence",
"label": "State/Subdivision",
"type": "string",
"required": true,
"placeholder": "SP"
}
],
"dependencies": []
}
  • Step 4: Create account-info block
{
"id": "account-info",
"name": "Account Info",
"category": "account",
"icon": "wallet",
"description": "Get account details, wallets, and PIX key",
"method": "GET",
"endpoint": "/v2/account/account-info",
"guideLink": "/docs/Avenia-Account-Management/about-login",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Account ID" },
"wallets": { "type": "array", "description": "User wallets with chain and address" },
"brCode": { "type": "string", "description": "Static BR code for receiving PIX" },
"pixKey": { "type": "string", "description": "PIX key for receiving" }
},
"configFields": [],
"dependencies": ["validate-login"]
}
  • Step 5: Create balances block
{
"id": "balances",
"name": "Balances",
"category": "account",
"icon": "coins",
"description": "Get all currency balances",
"method": "GET",
"endpoint": "/v2/account/balances",
"guideLink": "/docs/Avenia-Account-Management/about-login",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"balances": { "type": "object", "description": "Map of currency symbol to balance string" }
},
"configFields": [],
"dependencies": ["validate-login"]
}
  • Step 6: Create metadata block
{
"id": "metadata",
"name": "Account Metadata",
"category": "account",
"icon": "info",
"description": "Get account flags (KYC status, unlocked currencies)",
"method": "GET",
"endpoint": "/v2/account/metadata",
"guideLink": "/docs/Avenia-Account-Management/about-login",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"brlUnlocked": { "type": "boolean", "description": "BRL operations enabled" },
"usdUnlocked": { "type": "boolean", "description": "USD operations enabled" },
"eurUnlocked": { "type": "boolean", "description": "EUR operations enabled" },
"copUnlocked": { "type": "boolean", "description": "COP operations enabled" },
"mxnUnlocked": { "type": "boolean", "description": "MXN operations enabled" }
},
"configFields": [],
"dependencies": ["validate-login"]
}
  • Step 7: Create limits block
{
"id": "limits",
"name": "Limits",
"category": "account",
"icon": "gauge",
"description": "Get transaction limits per currency",
"method": "GET",
"endpoint": "/v2/account/limits",
"guideLink": "/docs/Avenia-Account-Management/about-login",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"limits": { "type": "array", "description": "Limits per currency with usage" }
},
"configFields": [],
"dependencies": ["validate-login"]
}
  • Step 8: Create statement block
{
"id": "statement",
"name": "Statement",
"category": "account",
"icon": "scroll-text",
"description": "Get account statement with pagination",
"method": "GET",
"endpoint": "/v2/account/statement",
"guideLink": "/docs/Avenia-Account-Management/account-statement",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"logs": { "type": "array", "description": "Statement entries" },
"cursor": { "type": "string", "description": "Pagination cursor" }
},
"configFields": [
{
"id": "createdAfter",
"label": "Created After",
"type": "string",
"required": false,
"placeholder": "2024-01-01T00:00:00Z"
},
{
"id": "createdBefore",
"label": "Created Before",
"type": "string",
"required": false,
"placeholder": "2024-12-31T23:59:59Z"
}
],
"dependencies": ["validate-login"]
}
  • Step 9: Commit
git add src/configs/blocks/
git commit -m "feat: add auth and account block config files"

Task 1.3: Block Config JSON Files (KYC)

Files:

  • Create: src/configs/blocks/kyc-level1-api.json

  • Create: src/configs/blocks/kyc-level1-websdk.json

  • Create: src/configs/blocks/kyc-usd.json

  • Create: src/configs/blocks/kyc-eur.json

  • Create: src/configs/blocks/kyc-cop.json

  • Create: src/configs/blocks/kyc-mxn.json

  • Create: src/configs/blocks/document-upload.json

  • Step 1: Create kyc-level1-api block

{
"id": "kyc-level1-api",
"name": "KYC Level 1 (API)",
"category": "kyc",
"icon": "user-check",
"description": "Submit KYC Level 1 via API with document IDs",
"method": "POST",
"endpoint": "/v2/kyc/new-level-1/api",
"guideLink": "/docs/KYC/KYC-level-1",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "KYC attempt ID" }
},
"configFields": [
{ "id": "fullName", "label": "Full Name", "type": "string", "required": true, "placeholder": "John Doe" },
{ "id": "dateOfBirth", "label": "Date of Birth", "type": "string", "required": true, "placeholder": "1990-01-15" },
{ "id": "countryOfTaxId", "label": "Country (ISO alpha-3)", "type": "string", "required": true, "placeholder": "BRA" },
{ "id": "taxIdNumber", "label": "Tax ID", "type": "string", "required": true, "placeholder": "12345678901" },
{ "id": "email", "label": "Email", "type": "string", "required": true, "placeholder": "user@example.com" },
{ "id": "country", "label": "Address Country", "type": "string", "required": true, "placeholder": "BRA" },
{ "id": "state", "label": "State", "type": "string", "required": true, "placeholder": "SP" },
{ "id": "city", "label": "City", "type": "string", "required": true, "placeholder": "Sao Paulo" },
{ "id": "zipCode", "label": "ZIP Code", "type": "string", "required": true, "placeholder": "01310-100" },
{ "id": "streetAddress", "label": "Street Address", "type": "string", "required": true, "placeholder": "Av. Paulista 1000" },
{ "id": "uploadedSelfieId", "label": "Selfie Document ID", "type": "string", "required": true, "placeholder": "from /v2/documents/" },
{ "id": "uploadedDocumentId", "label": "ID Document ID", "type": "string", "required": true, "placeholder": "from /v2/documents/" }
],
"dependencies": ["validate-login"]
}
  • Step 2: Create kyc-level1-websdk block
{
"id": "kyc-level1-websdk",
"name": "KYC Level 1 (Web SDK)",
"category": "kyc",
"icon": "user-check",
"description": "Start KYC Level 1 via hosted web SDK redirect",
"method": "POST",
"endpoint": "/v2/kyc/new-level-1/web-sdk",
"guideLink": "/docs/KYC/KYC-level-1",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"attemptId": { "type": "string", "description": "KYC attempt ID" },
"authorizedRepresentativeUrl": { "type": "string", "description": "Redirect URL for KYC" }
},
"configFields": [
{ "id": "redirectUrl", "label": "Redirect URL (optional)", "type": "string", "required": false, "placeholder": "https://yourapp.com/kyc-callback" }
],
"dependencies": ["validate-login"]
}
  • Step 3: Create currency-specific KYC blocks

kyc-usd.json:

{
"id": "kyc-usd",
"name": "KYC USD",
"category": "kyc",
"icon": "badge-dollar-sign",
"description": "Unlock USD operations (requires Level 1 approved)",
"method": "POST",
"endpoint": "/v2/kyc/usd/api",
"guideLink": "/docs/KYC/KYC-USD",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"attemptId": { "type": "uuid", "description": "USD KYC attempt ID" }
},
"configFields": [
{ "id": "sandboxReject", "label": "Sandbox Reject", "type": "select", "options": ["false", "true"], "default": "false" }
],
"dependencies": ["validate-login", "kyc-level1-api"]
}

kyc-eur.json:

{
"id": "kyc-eur",
"name": "KYC EUR",
"category": "kyc",
"icon": "badge-euro",
"description": "Unlock EUR operations (requires Level 1 approved)",
"method": "POST",
"endpoint": "/v2/kyc/eur/api",
"guideLink": "/docs/KYC/KYC-EUR",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"attemptId": { "type": "uuid", "description": "EUR KYC attempt ID" }
},
"configFields": [
{ "id": "sandboxReject", "label": "Sandbox Reject", "type": "select", "options": ["false", "true"], "default": "false" }
],
"dependencies": ["validate-login", "kyc-level1-api"]
}

kyc-cop.json:

{
"id": "kyc-cop",
"name": "KYC COP",
"category": "kyc",
"icon": "badge-cent",
"description": "Unlock COP operations (requires Level 1 approved)",
"method": "POST",
"endpoint": "/v2/kyc/cop/api",
"guideLink": "/docs/KYC/KYC-level-1",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"attemptId": { "type": "uuid", "description": "COP KYC attempt ID" }
},
"configFields": [],
"dependencies": ["validate-login", "kyc-level1-api"]
}

kyc-mxn.json:

{
"id": "kyc-mxn",
"name": "KYC MXN",
"category": "kyc",
"icon": "badge-cent",
"description": "Unlock MXN operations (requires Level 1 approved)",
"method": "POST",
"endpoint": "/v2/kyc/mxn/api",
"guideLink": "/docs/KYC/KYC-level-1",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"attemptId": { "type": "uuid", "description": "MXN KYC attempt ID" }
},
"configFields": [],
"dependencies": ["validate-login", "kyc-level1-api"]
}
  • Step 4: Create document-upload block
{
"id": "document-upload",
"name": "Document Upload",
"category": "kyc",
"icon": "file-up",
"description": "Get pre-signed URL for document upload (selfie, ID, etc.)",
"method": "POST",
"endpoint": "/v2/documents/",
"guideLink": "/docs/KYC/KYC-level-1",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"documentId": { "type": "string", "description": "Document ID for KYC submission" },
"uploadUrl": { "type": "string", "description": "S3 pre-signed URL to PUT the file" }
},
"configFields": [
{
"id": "documentType",
"label": "Document Type",
"type": "select",
"required": true,
"options": ["ID", "DRIVERS-LICENSE", "PASSPORT", "SELFIE", "SELFIE-FROM-LIVENESS"]
}
],
"dependencies": ["validate-login"]
}
  • Step 5: Commit
git add src/configs/blocks/
git commit -m "feat: add KYC and document upload block configs"

Task 1.4: Block Config JSON Files (Operations)

Files:

  • Create: src/configs/blocks/get-quote.json

  • Create: src/configs/blocks/create-ticket.json

  • Create: src/configs/blocks/get-ticket.json

  • Create: src/configs/blocks/cancel-ticket.json

  • Create: src/configs/blocks/ticket-receipt.json

  • Create: src/configs/blocks/convert.json

  • Create: src/configs/blocks/static-br-code.json

  • Create: src/configs/blocks/payment-session.json

  • Step 1: Create get-quote block

{
"id": "get-quote",
"name": "Get Quote",
"category": "operations",
"icon": "arrow-left-right",
"description": "Get exchange rate and quote token (expires ~15s)",
"method": "GET",
"endpoint": "/v2/account/quote/fixed-rate",
"guideLink": "/docs/Operations/quotes-and-tickets",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"quoteToken": { "type": "token", "description": "JWT quote token — pass to create-ticket" },
"inputAmount": { "type": "number", "description": "Resolved input amount" },
"outputAmount": { "type": "number", "description": "Resolved output amount" },
"basePrice": { "type": "number", "description": "Exchange rate" },
"appliedFees": { "type": "array", "description": "Fee breakdown" }
},
"configFields": [
{ "id": "inputCurrency", "label": "Input Currency", "type": "currency-select", "required": true, "filter": { "supportedAs": "input" } },
{ "id": "outputCurrency", "label": "Output Currency", "type": "currency-select", "required": true, "filter": { "supportedAs": "output" } },
{ "id": "inputAmount", "label": "Input Amount", "type": "number", "required": false, "placeholder": "1000.00", "description": "Specify inputAmount OR outputAmount, not both" },
{ "id": "outputAmount", "label": "Output Amount", "type": "number", "required": false, "placeholder": "100.00" },
{ "id": "inputPaymentMethod", "label": "Input Payment Method", "type": "payment-method-select", "required": true, "filter": { "currency": "inputCurrency", "direction": "input" } },
{ "id": "outputPaymentMethod", "label": "Output Payment Method", "type": "payment-method-select", "required": true, "filter": { "currency": "outputCurrency", "direction": "output" } },
{ "id": "blockchainSendMethod", "label": "Blockchain Send Method", "type": "select", "options": ["PERMIT", "TRANSFER"], "default": "PERMIT", "description": "Required when input is blockchain" },
{ "id": "inputThirdParty", "label": "Input Third Party", "type": "select", "options": ["false", "true"], "default": "false" },
{ "id": "outputThirdParty", "label": "Output Third Party", "type": "select", "options": ["false", "true"], "default": "false" }
],
"dependencies": ["validate-login"]
}
  • Step 2: Create create-ticket block
{
"id": "create-ticket",
"name": "Create Ticket",
"category": "operations",
"icon": "receipt",
"description": "Execute a transaction using a quote token",
"method": "POST",
"endpoint": "/v2/account/tickets/",
"guideLink": "/docs/Operations/quotes-and-tickets",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" },
"quoteToken": { "type": "token", "required": true, "from": "get-quote.quoteToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Ticket ID" },
"brCode": { "type": "string", "description": "PIX BR code (BRL input only)" },
"depositUrl": { "type": "string", "description": "PSE payment link (COP input only)" },
"speiClabe": { "type": "string", "description": "CLABE for SPEI (MXN input only)" },
"usdDepositInstructions": { "type": "object", "description": "Wire/ACH instructions (USD input)" },
"eurDepositInstructions": { "type": "object", "description": "SEPA instructions (EUR input)" }
},
"configFields": [
{ "id": "quoteToken", "label": "Quote Token", "type": "string", "required": true, "placeholder": "from get-quote step" },
{ "id": "externalId", "label": "External ID (optional)", "type": "string", "required": false, "placeholder": "your-tracking-id", "description": "Max 68 chars" }
],
"dependencies": ["validate-login", "get-quote"]
}
  • Step 3: Create get-ticket block
{
"id": "get-ticket",
"name": "Get Ticket",
"category": "operations",
"icon": "search",
"description": "Check ticket status and details",
"method": "GET",
"endpoint": "/v2/account/tickets/{ticketId}",
"guideLink": "/docs/Operations/quotes-and-tickets",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" },
"ticketId": { "type": "uuid", "required": true, "from": "create-ticket.id" }
},
"outputs": {
"id": { "type": "uuid", "description": "Ticket ID" },
"status": { "type": "string", "description": "UNPAID|PROCESSING|PAID|FAILED|PARTIAL-FAILED|CANCELED" }
},
"configFields": [
{ "id": "ticketId", "label": "Ticket ID", "type": "string", "required": true, "placeholder": "uuid from create-ticket" }
],
"dependencies": ["validate-login"]
}
  • Step 4: Create cancel-ticket block
{
"id": "cancel-ticket",
"name": "Cancel Ticket",
"category": "operations",
"icon": "x-circle",
"description": "Cancel an unpaid ticket",
"method": "PATCH",
"endpoint": "/v2/account/tickets/{ticketId}/cancel",
"guideLink": "/docs/Operations/quotes-and-tickets",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" },
"ticketId": { "type": "uuid", "required": true, "from": "create-ticket.id" }
},
"outputs": {},
"configFields": [
{ "id": "ticketId", "label": "Ticket ID", "type": "string", "required": true, "placeholder": "uuid from create-ticket" }
],
"dependencies": ["validate-login"]
}
  • Step 5: Create ticket-receipt block
{
"id": "ticket-receipt",
"name": "Ticket Receipt",
"category": "operations",
"icon": "file-text",
"description": "Download receipt PDF for a paid ticket",
"method": "GET",
"endpoint": "/v2/account/tickets/{ticketId}/receipt",
"guideLink": "/docs/Operations/ticket-receipt",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" },
"ticketId": { "type": "uuid", "required": true, "from": "create-ticket.id" }
},
"outputs": {},
"configFields": [
{ "id": "ticketId", "label": "Ticket ID", "type": "string", "required": true, "placeholder": "uuid of a PAID ticket" }
],
"dependencies": ["validate-login"]
}
  • Step 6: Create convert, static-br-code, payment-session blocks

convert.json:

{
"id": "convert",
"name": "Convert",
"category": "operations",
"icon": "refresh-cw",
"description": "Convert between internal balances (quote + ticket shortcut)",
"method": "POST",
"endpoint": "/v2/account/tickets/",
"guideLink": "/docs/Operations/quotes-and-tickets",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" },
"quoteToken": { "type": "token", "required": true, "from": "get-quote.quoteToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Ticket ID" }
},
"configFields": [
{ "id": "quoteToken", "label": "Quote Token", "type": "string", "required": true, "placeholder": "from get-quote (INTERNAL→INTERNAL)" }
],
"dependencies": ["validate-login", "get-quote"]
}

static-br-code.json:

{
"id": "static-br-code",
"name": "Static BR Code",
"category": "operations",
"icon": "qr-code",
"description": "Generate a static PIX BR code for receiving",
"method": "GET",
"endpoint": "/v2/account/bank-accounts/brl/static-br-code",
"guideLink": "/docs/Bank Accounts/static-br-code-guide",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"brCode": { "type": "string", "description": "Static BR code string" }
},
"configFields": [
{ "id": "amount", "label": "Amount (BRL)", "type": "number", "required": true, "placeholder": "100.00" },
{ "id": "referenceLabel", "label": "Reference Label", "type": "string", "required": true, "placeholder": "INV-001", "description": "Max 19 chars" }
],
"dependencies": ["validate-login"]
}

payment-session.json:

{
"id": "payment-session",
"name": "Payment Session",
"category": "operations",
"icon": "credit-card",
"description": "Create a hosted payment page session",
"method": "POST",
"endpoint": "/v2/account/payment-session/",
"guideLink": "/docs/Operations/quotes-and-tickets",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Payment session ID" }
},
"configFields": [],
"dependencies": ["validate-login"]
}
  • Step 7: Commit
git add src/configs/blocks/
git commit -m "feat: add operations block configs (quote, ticket, convert, etc.)"

Task 1.5: Block Config JSON Files (Beneficiaries)

Files:

  • Create: src/configs/blocks/beneficiary-wallet.json

  • Create: src/configs/blocks/beneficiary-brl-bank.json

  • Create: src/configs/blocks/beneficiary-usd-bank.json

  • Create: src/configs/blocks/beneficiary-eur-bank.json

  • Create: src/configs/blocks/beneficiary-cop-bank.json

  • Create: src/configs/blocks/beneficiary-mxn-bank.json

  • Create: src/configs/blocks/beneficiary-ars-bank.json

  • Step 1: Create beneficiary-wallet block

{
"id": "beneficiary-wallet",
"name": "Beneficiary (Wallet)",
"category": "beneficiaries",
"icon": "send",
"description": "Register a blockchain wallet beneficiary",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/wallets/",
"guideLink": "/docs/Beneficiaries-Wallets/wallets-guide",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary wallet ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "My Wallet" },
{ "id": "description", "label": "Description", "type": "string", "required": false, "placeholder": "Main trading wallet" },
{ "id": "walletAddress", "label": "Wallet Address", "type": "string", "required": true, "placeholder": "0x..." },
{ "id": "walletChain", "label": "Chain", "type": "chain-select", "required": true },
{ "id": "walletMemo", "label": "Memo (optional)", "type": "string", "required": false }
],
"dependencies": ["validate-login"]
}
  • Step 2: Create beneficiary-brl-bank block
{
"id": "beneficiary-brl-bank",
"name": "Beneficiary (BRL Bank)",
"category": "beneficiaries",
"icon": "landmark",
"description": "Register a BRL bank account (PIX key or full details)",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/bank-accounts/brl/",
"guideLink": "/docs/Beneficiaries-Bank-Accounts/beneficiariesBankAccountsBrl",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary bank account ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "My PIX" },
{ "id": "pixKey", "label": "PIX Key", "type": "string", "required": false, "placeholder": "email@example.com or CPF", "description": "If set, bank details auto-resolve" },
{ "id": "userName", "label": "Account Holder Name", "type": "string", "required": false },
{ "id": "bankCode", "label": "Bank Code", "type": "string", "required": false, "placeholder": "260" },
{ "id": "branchCode", "label": "Branch", "type": "string", "required": false, "placeholder": "0001" },
{ "id": "accountNumber", "label": "Account Number", "type": "string", "required": false },
{ "id": "accountType", "label": "Account Type", "type": "select", "options": ["checking", "payment", "savings", "salary"], "required": false },
{ "id": "taxId", "label": "Tax ID (CPF/CNPJ)", "type": "string", "required": false, "placeholder": "12345678901" }
],
"dependencies": ["validate-login"]
}
  • Step 3: Create beneficiary-usd-bank block
{
"id": "beneficiary-usd-bank",
"name": "Beneficiary (USD Bank)",
"category": "beneficiaries",
"icon": "landmark",
"description": "Register a USD bank account (WIRE/ACH)",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/bank-accounts/usd/",
"guideLink": "/docs/Beneficiaries-Bank-Accounts/beneficiariesBankAccountsUsd",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary USD account ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "US Bank" },
{ "id": "bankAccountNumber", "label": "Account Number", "type": "string", "required": true },
{ "id": "bankRoutingNumber", "label": "Routing Number (ABA)", "type": "string", "required": true },
{ "id": "bankBeneficiaryName", "label": "Beneficiary Name", "type": "string", "required": true },
{ "id": "bankName", "label": "Bank Name", "type": "string", "required": true }
],
"dependencies": ["validate-login"]
}
  • Step 4: Create beneficiary-eur-bank block
{
"id": "beneficiary-eur-bank",
"name": "Beneficiary (EUR Bank)",
"category": "beneficiaries",
"icon": "landmark",
"description": "Register a EUR SEPA bank account",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/bank-accounts/eur/",
"guideLink": "/docs/Beneficiaries-Bank-Accounts/beneficiariesBankAccountsEur",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary EUR account ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "EU Bank" },
{ "id": "iban", "label": "IBAN", "type": "string", "required": true },
{ "id": "bic", "label": "BIC/SWIFT (optional)", "type": "string", "required": false },
{ "id": "country", "label": "Country (ISO alpha-3)", "type": "string", "required": true, "placeholder": "DEU" },
{ "id": "bankBeneficiaryName", "label": "Beneficiary Name", "type": "string", "required": true },
{ "id": "isBusiness", "label": "Is Business", "type": "select", "options": ["false", "true"], "default": "false" }
],
"dependencies": ["validate-login"]
}
  • Step 5: Create beneficiary-cop-bank block
{
"id": "beneficiary-cop-bank",
"name": "Beneficiary (COP Bank)",
"category": "beneficiaries",
"icon": "landmark",
"description": "Register a COP Colombian bank account",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/bank-accounts/cop/",
"guideLink": "/docs/Beneficiaries-Bank-Accounts/beneficiariesBankAccountsBrl",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary COP account ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "COP Bank" },
{ "id": "beneficiaryName", "label": "Beneficiary Name", "type": "string", "required": true },
{ "id": "documentNumber", "label": "Document Number (Cedula)", "type": "string", "required": true, "placeholder": "6-10 digits" },
{ "id": "documentType", "label": "Document Type", "type": "string", "required": true, "placeholder": "CC" },
{ "id": "bankId", "label": "Bank ID", "type": "string", "required": true, "placeholder": "bank_cop_022", "description": "Use /supported-banks to get valid IDs" },
{ "id": "bankAccountNumber", "label": "Account Number", "type": "string", "required": true },
{ "id": "accountType", "label": "Account Type", "type": "string", "required": true }
],
"dependencies": ["validate-login"]
}
  • Step 6: Create beneficiary-mxn-bank and beneficiary-ars-bank blocks

beneficiary-mxn-bank.json:

{
"id": "beneficiary-mxn-bank",
"name": "Beneficiary (MXN Bank)",
"category": "beneficiaries",
"icon": "landmark",
"description": "Register a MXN bank account (SPEI/CLABE)",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/bank-accounts/mxn/",
"guideLink": "/docs/Beneficiaries-Bank-Accounts/beneficiariesBankAccountsBrl",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary MXN account ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "MXN Bank" },
{ "id": "beneficiaryName", "label": "Beneficiary Name", "type": "string", "required": true },
{ "id": "clabe", "label": "CLABE", "type": "string", "required": true, "placeholder": "18 digits" },
{ "id": "bankId", "label": "Bank ID", "type": "string", "required": true },
{ "id": "bankName", "label": "Bank Name", "type": "string", "required": true }
],
"dependencies": ["validate-login"]
}

beneficiary-ars-bank.json:

{
"id": "beneficiary-ars-bank",
"name": "Beneficiary (ARS Bank)",
"category": "beneficiaries",
"icon": "landmark",
"description": "Register an ARS Argentine bank account (CVU)",
"method": "POST",
"endpoint": "/v2/account/beneficiaries/bank-accounts/ars/",
"guideLink": "/docs/Beneficiaries-Bank-Accounts/beneficiariesBankAccountsBrl",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Beneficiary ARS account ID" }
},
"configFields": [
{ "id": "alias", "label": "Alias", "type": "string", "required": true, "placeholder": "ARS Bank" },
{ "id": "cvu", "label": "CVU", "type": "string", "required": true, "placeholder": "22-digit CVU" }
],
"dependencies": ["validate-login"]
}
  • Step 7: Commit
git add src/configs/blocks/
git commit -m "feat: add beneficiary block configs for all currencies"

Task 1.6: Block Config JSON Files (Admin)

Files:

  • Create: src/configs/blocks/subaccount-create.json

  • Create: src/configs/blocks/webhook-register.json

  • Step 1: Create subaccount-create block

{
"id": "subaccount-create",
"name": "Create Subaccount",
"category": "admin",
"icon": "users",
"description": "Create a sub-account for end-user segregation",
"method": "POST",
"endpoint": "/v2/account/sub-accounts/",
"guideLink": "/docs/Avenia Subaccounts/subaccounts-management",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Sub-account ID" }
},
"configFields": [
{ "id": "name", "label": "Sub-account Name", "type": "string", "required": true, "placeholder": "End User 1" },
{ "id": "accountType", "label": "Account Type", "type": "select", "options": ["INDIVIDUAL"], "default": "INDIVIDUAL", "required": true }
],
"dependencies": ["validate-login"]
}
  • Step 2: Create webhook-register block
{
"id": "webhook-register",
"name": "Register Webhook",
"category": "admin",
"icon": "webhook",
"description": "Register a webhook URL for event notifications",
"method": "POST",
"endpoint": "/v2/notifications/webhooks/",
"guideLink": "/docs/Webhooks/webhook-management",
"inputs": {
"authToken": { "type": "token", "required": true, "from": "validate-login.accessToken" }
},
"outputs": {
"id": { "type": "uuid", "description": "Webhook ID" }
},
"configFields": [
{ "id": "url", "label": "Webhook URL", "type": "string", "required": true, "placeholder": "https://yourapp.com/webhooks/avenia" },
{
"id": "subscriptions",
"label": "Subscriptions",
"type": "select",
"required": true,
"options": ["*", "TICKET", "KYC", "LIMIT-UPDATE"],
"default": "*",
"description": "Use * for all events. Max 3 webhooks per account."
}
],
"dependencies": ["validate-login"]
}
  • Step 3: Commit
git add src/configs/blocks/
git commit -m "feat: add admin block configs (subaccount, webhook)"

Task 1.7: Currency, Chain, and Payment Method Configs

Files:

  • Create: src/configs/currencies/*.json (12 files)

  • Create: src/configs/chains/*.json (9 files)

  • Create: src/configs/payment-methods/*.json (8+ files)

  • Step 1: Create fiat currency configs

src/configs/currencies/brl.json:

{ "id": "brl", "symbol": "BRL", "name": "Brazilian Real", "type": "fiat", "flag": "br", "supportedAs": ["input", "output"], "paymentMethods": ["pix"], "chains": [], "minAmount": 1, "decimalPlaces": 2 }

src/configs/currencies/usd.json:

{ "id": "usd", "symbol": "USD", "name": "US Dollar", "type": "fiat", "flag": "us", "supportedAs": ["input", "output"], "paymentMethods": ["wire", "ach"], "chains": [], "minAmount": 1, "decimalPlaces": 2 }

src/configs/currencies/eur.json:

{ "id": "eur", "symbol": "EUR", "name": "Euro", "type": "fiat", "flag": "eu", "supportedAs": ["input", "output"], "paymentMethods": ["sepa"], "chains": [], "minAmount": 1, "decimalPlaces": 2 }

src/configs/currencies/ars.json:

{ "id": "ars", "symbol": "ARS", "name": "Argentine Peso", "type": "fiat", "flag": "ar", "supportedAs": ["input", "output"], "paymentMethods": ["bank-transfer"], "chains": [], "minAmount": 1, "decimalPlaces": 2 }

src/configs/currencies/cop.json:

{ "id": "cop", "symbol": "COP", "name": "Colombian Peso", "type": "fiat", "flag": "co", "supportedAs": ["input", "output"], "paymentMethods": ["bank-transfer"], "chains": [], "minAmount": 1000, "decimalPlaces": 2 }

src/configs/currencies/mxn.json:

{ "id": "mxn", "symbol": "MXN", "name": "Mexican Peso", "type": "fiat", "flag": "mx", "supportedAs": ["input", "output"], "paymentMethods": ["spei"], "chains": [], "minAmount": 1, "decimalPlaces": 2 }
  • Step 2: Create crypto currency configs

src/configs/currencies/brla.json:

{ "id": "brla", "symbol": "BRLA", "name": "BRLA Stablecoin", "type": "crypto", "supportedAs": ["input", "output"], "paymentMethods": ["internal"], "chains": ["polygon", "moonbeam", "celo", "gnosis", "base", "ethereum", "avalanche"], "minAmount": 0.01, "decimalPlaces": 6 }

src/configs/currencies/usdc.json:

{ "id": "usdc", "symbol": "USDC", "name": "USD Coin", "type": "crypto", "supportedAs": ["input", "output"], "paymentMethods": ["internal"], "chains": ["polygon", "moonbeam", "celo", "gnosis", "base", "ethereum"], "minAmount": 0.01, "decimalPlaces": 6 }

src/configs/currencies/usdce.json:

{ "id": "usdce", "symbol": "USDCe", "name": "Bridged USDC", "type": "crypto", "supportedAs": ["input", "output"], "paymentMethods": ["internal"], "chains": ["polygon"], "minAmount": 0.01, "decimalPlaces": 6 }

src/configs/currencies/usdt.json:

{ "id": "usdt", "symbol": "USDT", "name": "Tether USD", "type": "crypto", "supportedAs": ["input", "output"], "paymentMethods": ["internal"], "chains": ["polygon", "moonbeam", "celo", "gnosis", "ethereum", "tron"], "minAmount": 0.01, "decimalPlaces": 6 }

src/configs/currencies/usdm.json:

{ "id": "usdm", "symbol": "USDM", "name": "USDM", "type": "crypto", "supportedAs": ["input", "output"], "paymentMethods": ["internal"], "chains": [], "minAmount": 0.01, "decimalPlaces": 6 }

src/configs/currencies/eurc.json:

{ "id": "eurc", "symbol": "EURC", "name": "Euro Coin", "type": "crypto", "supportedAs": ["input", "output"], "paymentMethods": ["internal"], "chains": [], "minAmount": 0.01, "decimalPlaces": 6 }
  • Step 3: Create chain configs

Create one file per chain in src/configs/chains/:

polygon.json:

{ "id": "polygon", "name": "Polygon", "symbol": "POL", "currencies": ["brla", "usdc", "usdce", "usdt"], "sendMethods": ["PERMIT", "TRANSFER"] }

ethereum.json:

{ "id": "ethereum", "name": "Ethereum", "symbol": "ETH", "currencies": ["brla", "usdc", "usdt"], "sendMethods": ["PERMIT", "TRANSFER"] }

base.json:

{ "id": "base", "name": "Base", "symbol": "ETH", "currencies": ["brla", "usdc"], "sendMethods": ["PERMIT", "TRANSFER"] }

celo.json:

{ "id": "celo", "name": "Celo", "symbol": "CELO", "currencies": ["brla", "usdc", "usdt"], "sendMethods": ["PERMIT", "TRANSFER"] }

gnosis.json:

{ "id": "gnosis", "name": "Gnosis", "symbol": "xDAI", "currencies": ["brla", "usdc", "usdt"], "sendMethods": ["PERMIT", "TRANSFER"] }

moonbeam.json:

{ "id": "moonbeam", "name": "Moonbeam", "symbol": "GLMR", "currencies": ["brla", "usdc", "usdt"], "sendMethods": ["TRANSFER"] }

avalanche.json:

{ "id": "avalanche", "name": "Avalanche", "symbol": "AVAX", "currencies": ["brla"], "sendMethods": ["PERMIT", "TRANSFER"] }

tron.json:

{ "id": "tron", "name": "Tron", "symbol": "TRX", "currencies": ["usdt"], "sendMethods": ["TRANSFER"] }

internal.json:

{ "id": "internal", "name": "Internal", "symbol": "INTERNAL", "currencies": ["brla", "usdc", "usdt", "usdm", "eurc"], "sendMethods": [] }
  • Step 4: Create payment method configs

src/configs/payment-methods/pix.json:

{ "id": "pix", "name": "PIX", "type": "fiat", "currencies": ["brl"], "direction": ["input", "output"], "icon": "zap" }

src/configs/payment-methods/wire.json:

{ "id": "wire", "name": "Wire Transfer", "type": "fiat", "currencies": ["usd"], "direction": ["input", "output"], "icon": "building-2" }

src/configs/payment-methods/ach.json:

{ "id": "ach", "name": "ACH", "type": "fiat", "currencies": ["usd"], "direction": ["input", "output"], "icon": "building-2" }

src/configs/payment-methods/sepa.json:

{ "id": "sepa", "name": "SEPA", "type": "fiat", "currencies": ["eur"], "direction": ["input", "output"], "icon": "landmark" }

src/configs/payment-methods/bank-transfer.json:

{ "id": "bank-transfer", "name": "Bank Transfer", "type": "fiat", "currencies": ["ars", "cop"], "direction": ["input", "output"], "icon": "landmark" }

src/configs/payment-methods/spei.json:

{ "id": "spei", "name": "SPEI", "type": "fiat", "currencies": ["mxn"], "direction": ["input", "output"], "icon": "landmark" }

src/configs/payment-methods/internal.json:

{ "id": "internal", "name": "Internal Balance", "type": "blockchain", "currencies": ["brla", "usdc", "usdce", "usdt", "usdm", "eurc"], "direction": ["input", "output"], "icon": "wallet" }
  • Step 5: Commit
git add src/configs/
git commit -m "feat: add currency, chain, and payment method config files"

Task 1.8: Block Registry

Files:

  • Create: src/registry/BlockRegistry.ts

  • Create: src/registry/index.ts

  • Create: src/registry/__tests__/BlockRegistry.test.ts

  • Step 1: Write the failing test

// src/registry/__tests__/BlockRegistry.test.ts
import { describe, it, expect } from 'vitest';
import { BlockRegistry } from '../BlockRegistry';

describe('BlockRegistry', () => {
const registry = new BlockRegistry();

it('loads all block definitions', () => {
const blocks = registry.getAllBlocks();
expect(blocks.length).toBeGreaterThanOrEqual(30);
});

it('finds a block by id', () => {
const login = registry.getBlock('login');
expect(login).toBeDefined();
expect(login!.name).toBe('Login');
expect(login!.method).toBe('POST');
expect(login!.endpoint).toBe('/v2/auth/login');
});

it('gets blocks by category', () => {
const authBlocks = registry.getBlocksByCategory('auth');
expect(authBlocks.length).toBeGreaterThanOrEqual(2);
expect(authBlocks.every(b => b.category === 'auth')).toBe(true);
});

it('loads all currency definitions', () => {
const currencies = registry.getAllCurrencies();
expect(currencies.length).toBeGreaterThanOrEqual(12);
});

it('finds currencies by type', () => {
const fiat = registry.getCurrenciesByType('fiat');
expect(fiat.every(c => c.type === 'fiat')).toBe(true);
expect(fiat.some(c => c.symbol === 'BRL')).toBe(true);
});

it('loads all chain definitions', () => {
const chains = registry.getAllChains();
expect(chains.length).toBeGreaterThanOrEqual(9);
});

it('loads all payment method definitions', () => {
const methods = registry.getAllPaymentMethods();
expect(methods.length).toBeGreaterThanOrEqual(7);
});

it('finds compatible payment methods for a currency', () => {
const brlMethods = registry.getPaymentMethodsForCurrency('brl');
expect(brlMethods.some(m => m.id === 'pix')).toBe(true);
});

it('finds chains for a currency', () => {
const brlaChains = registry.getChainsForCurrency('brla');
expect(brlaChains.some(c => c.id === 'polygon')).toBe(true);
expect(brlaChains.some(c => c.id === 'tron')).toBe(false);
});
});
  • Step 2: Run test to verify it fails
cd ~/Projects/front/integration-guide && npx vitest run src/registry/__tests__/BlockRegistry.test.ts

Expected: FAIL — module not found.

  • Step 3: Implement BlockRegistry
// src/registry/BlockRegistry.ts
import type {
BlockDefinition,
BlockCategory,
CurrencyDefinition,
CurrencyType,
ChainDefinition,
PaymentMethodDefinition,
} from '../types/builder';

// Import all block configs
import login from '../configs/blocks/login.json';
import validateLogin from '../configs/blocks/validate-login.json';
import createAccount from '../configs/blocks/create-account.json';
import accountInfo from '../configs/blocks/account-info.json';
import balances from '../configs/blocks/balances.json';
import metadata from '../configs/blocks/metadata.json';
import limits from '../configs/blocks/limits.json';
import statement from '../configs/blocks/statement.json';
import kycLevel1Api from '../configs/blocks/kyc-level1-api.json';
import kycLevel1Websdk from '../configs/blocks/kyc-level1-websdk.json';
import kycUsd from '../configs/blocks/kyc-usd.json';
import kycEur from '../configs/blocks/kyc-eur.json';
import kycCop from '../configs/blocks/kyc-cop.json';
import kycMxn from '../configs/blocks/kyc-mxn.json';
import documentUpload from '../configs/blocks/document-upload.json';
import getQuote from '../configs/blocks/get-quote.json';
import createTicket from '../configs/blocks/create-ticket.json';
import getTicket from '../configs/blocks/get-ticket.json';
import cancelTicket from '../configs/blocks/cancel-ticket.json';
import ticketReceipt from '../configs/blocks/ticket-receipt.json';
import convert from '../configs/blocks/convert.json';
import staticBrCode from '../configs/blocks/static-br-code.json';
import paymentSession from '../configs/blocks/payment-session.json';
import beneficiaryWallet from '../configs/blocks/beneficiary-wallet.json';
import beneficiaryBrlBank from '../configs/blocks/beneficiary-brl-bank.json';
import beneficiaryUsdBank from '../configs/blocks/beneficiary-usd-bank.json';
import beneficiaryEurBank from '../configs/blocks/beneficiary-eur-bank.json';
import beneficiaryCopBank from '../configs/blocks/beneficiary-cop-bank.json';
import beneficiaryMxnBank from '../configs/blocks/beneficiary-mxn-bank.json';
import beneficiaryArsBank from '../configs/blocks/beneficiary-ars-bank.json';
import subaccountCreate from '../configs/blocks/subaccount-create.json';
import webhookRegister from '../configs/blocks/webhook-register.json';

// Import currency configs
import brl from '../configs/currencies/brl.json';
import usd from '../configs/currencies/usd.json';
import eur from '../configs/currencies/eur.json';
import ars from '../configs/currencies/ars.json';
import cop from '../configs/currencies/cop.json';
import mxn from '../configs/currencies/mxn.json';
import brla from '../configs/currencies/brla.json';
import usdc from '../configs/currencies/usdc.json';
import usdce from '../configs/currencies/usdce.json';
import usdt from '../configs/currencies/usdt.json';
import usdm from '../configs/currencies/usdm.json';
import eurc from '../configs/currencies/eurc.json';

// Import chain configs
import polygon from '../configs/chains/polygon.json';
import ethereum from '../configs/chains/ethereum.json';
import base from '../configs/chains/base.json';
import celo from '../configs/chains/celo.json';
import gnosis from '../configs/chains/gnosis.json';
import moonbeam from '../configs/chains/moonbeam.json';
import avalanche from '../configs/chains/avalanche.json';
import tron from '../configs/chains/tron.json';
import internal from '../configs/chains/internal.json';

// Import payment method configs
import pix from '../configs/payment-methods/pix.json';
import wire from '../configs/payment-methods/wire.json';
import ach from '../configs/payment-methods/ach.json';
import sepa from '../configs/payment-methods/sepa.json';
import bankTransfer from '../configs/payment-methods/bank-transfer.json';
import spei from '../configs/payment-methods/spei.json';
import internalMethod from '../configs/payment-methods/internal.json';

const ALL_BLOCKS: BlockDefinition[] = [
login, validateLogin, createAccount,
accountInfo, balances, metadata, limits, statement,
kycLevel1Api, kycLevel1Websdk, kycUsd, kycEur, kycCop, kycMxn, documentUpload,
getQuote, createTicket, getTicket, cancelTicket, ticketReceipt, convert, staticBrCode, paymentSession,
beneficiaryWallet, beneficiaryBrlBank, beneficiaryUsdBank, beneficiaryEurBank,
beneficiaryCopBank, beneficiaryMxnBank, beneficiaryArsBank,
subaccountCreate, webhookRegister,
] as BlockDefinition[];

const ALL_CURRENCIES: CurrencyDefinition[] = [
brl, usd, eur, ars, cop, mxn, brla, usdc, usdce, usdt, usdm, eurc,
] as CurrencyDefinition[];

const ALL_CHAINS: ChainDefinition[] = [
polygon, ethereum, base, celo, gnosis, moonbeam, avalanche, tron, internal,
] as ChainDefinition[];

const ALL_PAYMENT_METHODS: PaymentMethodDefinition[] = [
pix, wire, ach, sepa, bankTransfer, spei, internalMethod,
] as PaymentMethodDefinition[];

export class BlockRegistry {
private blocks: Map<string, BlockDefinition>;
private currencies: Map<string, CurrencyDefinition>;
private chains: Map<string, ChainDefinition>;
private paymentMethods: Map<string, PaymentMethodDefinition>;

constructor() {
this.blocks = new Map(ALL_BLOCKS.map(b => [b.id, b]));
this.currencies = new Map(ALL_CURRENCIES.map(c => [c.id, c]));
this.chains = new Map(ALL_CHAINS.map(c => [c.id, c]));
this.paymentMethods = new Map(ALL_PAYMENT_METHODS.map(m => [m.id, m]));
}

getAllBlocks(): BlockDefinition[] {
return ALL_BLOCKS;
}

getBlock(id: string): BlockDefinition | undefined {
return this.blocks.get(id);
}

getBlocksByCategory(category: BlockCategory): BlockDefinition[] {
return ALL_BLOCKS.filter(b => b.category === category);
}

getAllCurrencies(): CurrencyDefinition[] {
return ALL_CURRENCIES;
}

getCurrency(id: string): CurrencyDefinition | undefined {
return this.currencies.get(id);
}

getCurrenciesByType(type: CurrencyType): CurrencyDefinition[] {
return ALL_CURRENCIES.filter(c => c.type === type);
}

getAllChains(): ChainDefinition[] {
return ALL_CHAINS;
}

getChain(id: string): ChainDefinition | undefined {
return this.chains.get(id);
}

getChainsForCurrency(currencyId: string): ChainDefinition[] {
const currency = this.currencies.get(currencyId);
if (!currency) return [];
return currency.chains
.map(cId => this.chains.get(cId))
.filter((c): c is ChainDefinition => c !== undefined);
}

getAllPaymentMethods(): PaymentMethodDefinition[] {
return ALL_PAYMENT_METHODS;
}

getPaymentMethod(id: string): PaymentMethodDefinition | undefined {
return this.paymentMethods.get(id);
}

getPaymentMethodsForCurrency(currencyId: string): PaymentMethodDefinition[] {
return ALL_PAYMENT_METHODS.filter(m => m.currencies.includes(currencyId));
}
}
  • Step 4: Create registry index
// src/registry/index.ts
import { BlockRegistry } from './BlockRegistry';

export const registry = new BlockRegistry();
export { BlockRegistry };
  • Step 5: Run tests
cd ~/Projects/front/integration-guide && npx vitest run src/registry/__tests__/BlockRegistry.test.ts

Expected: All tests PASS.

  • Step 6: Commit
git add src/registry/ src/types/
git commit -m "feat: implement BlockRegistry with config loading and querying"

Phase 2: Engine Layer (Pure Logic, No UI)

Task 2.1: BindingResolver

Files:

  • Create: src/engine/BindingResolver.ts

  • Create: src/engine/__tests__/BindingResolver.test.ts

  • Step 1: Write the failing test

// src/engine/__tests__/BindingResolver.test.ts
import { describe, it, expect } from 'vitest';
import { BindingResolver } from '../BindingResolver';
import { registry } from '../../registry';
import type { Binding } from '../../types/builder';

describe('BindingResolver', () => {
const resolver = new BindingResolver(registry);

it('auto-binds validate-login.accessToken to account-info.authToken', () => {
const bindings = resolver.resolve('validate-login', 'account-info');
expect(bindings).toEqual<Binding[]>([
{
sourceNodeId: 'validate-login',
sourceField: 'accessToken',
targetField: 'authToken',
auto: true,
},
]);
});

it('auto-binds get-quote.quoteToken to create-ticket.quoteToken', () => {
const bindings = resolver.resolve('get-quote', 'create-ticket');
expect(bindings.some(b => b.sourceField === 'quoteToken' && b.targetField === 'quoteToken')).toBe(true);
});

it('returns empty bindings for incompatible blocks', () => {
const bindings = resolver.resolve('login', 'get-quote');
expect(bindings).toEqual([]);
});

it('does not bind when source has no outputs', () => {
const bindings = resolver.resolve('login', 'validate-login');
expect(bindings).toEqual([]);
});
});
  • Step 2: Run test to verify it fails
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/BindingResolver.test.ts
  • Step 3: Implement BindingResolver
// src/engine/BindingResolver.ts
import type { BlockRegistry } from '../registry/BlockRegistry';
import type { Binding } from '../types/builder';

export class BindingResolver {
constructor(private registry: BlockRegistry) {}

resolve(sourceBlockId: string, targetBlockId: string): Binding[] {
const source = this.registry.getBlock(sourceBlockId);
const target = this.registry.getBlock(targetBlockId);
if (!source || !target) return [];

const bindings: Binding[] = [];
const sourceOutputs = source.outputs;
const targetInputs = target.inputs;

for (const [targetField, targetInput] of Object.entries(targetInputs)) {
if (!targetInput.from) continue;

const [fromBlockId, fromField] = targetInput.from.split('.');
if (fromBlockId !== sourceBlockId) continue;

if (fromField in sourceOutputs) {
bindings.push({
sourceNodeId: sourceBlockId,
sourceField: fromField,
targetField,
auto: true,
});
}
}

// Also check configFields for implicit bindings (quoteToken etc.)
for (const field of target.configFields) {
if (field.id in sourceOutputs) {
const alreadyBound = bindings.some(b => b.targetField === field.id);
if (!alreadyBound) {
bindings.push({
sourceNodeId: sourceBlockId,
sourceField: field.id,
targetField: field.id,
auto: true,
});
}
}
}

return bindings;
}
}
  • Step 4: Run tests
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/BindingResolver.test.ts

Expected: All PASS.

  • Step 5: Commit
git add src/engine/
git commit -m "feat: implement BindingResolver for auto-binding between blocks"

Task 2.2: FlowEngine

Files:

  • Create: src/engine/FlowEngine.ts

  • Create: src/engine/__tests__/FlowEngine.test.ts

  • Step 1: Write the failing test

// src/engine/__tests__/FlowEngine.test.ts
import { describe, it, expect } from 'vitest';
import { FlowEngine } from '../FlowEngine';
import { registry } from '../../registry';
import type { Node, Edge } from '@xyflow/react';
import type { BuilderNodeData, BuilderEdgeData } from '../../types/builder';

function makeNode(id: string, blockId: string, config: Record<string, unknown> = {}): Node<BuilderNodeData> {
const block = registry.getBlock(blockId)!;
return {
id,
type: 'collapsed',
position: { x: 0, y: 0 },
data: {
blockId,
label: block.name,
icon: block.icon,
method: block.method,
endpoint: block.endpoint,
category: block.category,
guideLink: block.guideLink,
config,
expanded: false,
validationErrors: [],
},
};
}

function makeEdge(source: string, target: string): Edge<BuilderEdgeData> {
return {
id: `${source}-${target}`,
source,
target,
data: { bindings: [], animated: true },
};
}

describe('FlowEngine', () => {
const engine = new FlowEngine(registry);

it('validates a valid login → validate-login → quote → ticket flow', () => {
const nodes = [
makeNode('n1', 'login'),
makeNode('n2', 'validate-login'),
makeNode('n3', 'get-quote', { inputCurrency: 'BRL', outputCurrency: 'USDC', inputAmount: 100, inputPaymentMethod: 'PIX', outputPaymentMethod: 'INTERNAL', blockchainSendMethod: 'PERMIT', inputThirdParty: 'false', outputThirdParty: 'false' }),
makeNode('n4', 'create-ticket', { quoteToken: 'from-prev' }),
];
const edges = [makeEdge('n1', 'n2'), makeEdge('n2', 'n3'), makeEdge('n3', 'n4')];
const errors = engine.validate(nodes, edges);
const blocking = errors.filter(e => e.severity === 'error');
expect(blocking).toEqual([]);
});

it('detects circular dependency', () => {
const nodes = [makeNode('a', 'login'), makeNode('b', 'validate-login')];
const edges = [makeEdge('a', 'b'), makeEdge('b', 'a')];
const errors = engine.validate(nodes, edges);
expect(errors.some(e => e.rule === 'circular-dependency')).toBe(true);
});

it('detects disconnected node', () => {
const nodes = [makeNode('a', 'login'), makeNode('b', 'get-quote')];
const edges: Edge<BuilderEdgeData>[] = [];
const errors = engine.validate(nodes, edges);
expect(errors.some(e => e.rule === 'disconnected-node' && e.nodeId === 'b')).toBe(true);
});

it('generates ordered flow steps', () => {
const nodes = [
makeNode('n1', 'login'),
makeNode('n2', 'validate-login'),
makeNode('n3', 'get-quote', { inputCurrency: 'BRL', outputCurrency: 'USDC', inputAmount: 100, inputPaymentMethod: 'PIX', outputPaymentMethod: 'INTERNAL', blockchainSendMethod: 'PERMIT', inputThirdParty: 'false', outputThirdParty: 'false' }),
];
const edges = [makeEdge('n1', 'n2'), makeEdge('n2', 'n3')];
const steps = engine.generateSteps(nodes, edges, 'sandbox');
expect(steps.length).toBe(3);
expect(steps[0].blockId).toBe('login');
expect(steps[1].blockId).toBe('validate-login');
expect(steps[2].blockId).toBe('get-quote');
});
});
  • Step 2: Run test to verify it fails
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/FlowEngine.test.ts
  • Step 3: Implement FlowEngine
// src/engine/FlowEngine.ts
import type { Node, Edge } from '@xyflow/react';
import type { BlockRegistry } from '../registry/BlockRegistry';
import type {
BuilderNodeData,
BuilderEdgeData,
ValidationError,
FlowStep,
Environment,
} from '../types/builder';

const ENV_URLS: Record<Environment, string> = {
sandbox: 'https://api.sandbox.avenia.io:10952',
production: 'https://api.avenia.io',
developer: 'http://localhost:10952',
};

export class FlowEngine {
constructor(private registry: BlockRegistry) {}

validate(
nodes: Node<BuilderNodeData>[],
edges: Edge<BuilderEdgeData>[],
): ValidationError[] {
const errors: ValidationError[] = [];

// Check circular dependencies
if (this.hasCycle(nodes, edges)) {
errors.push({
nodeId: '',
severity: 'error',
message: 'Circular dependency detected in flow',
rule: 'circular-dependency',
});
}

// Check disconnected nodes (nodes with no edges, except if it's the only node)
if (nodes.length > 1) {
const connectedIds = new Set<string>();
for (const edge of edges) {
connectedIds.add(edge.source);
connectedIds.add(edge.target);
}
for (const node of nodes) {
if (!connectedIds.has(node.id)) {
errors.push({
nodeId: node.id,
severity: 'warning',
message: `${node.data.label} is not connected to any other block`,
rule: 'disconnected-node',
});
}
}
}

// Check required config fields
for (const node of nodes) {
const block = this.registry.getBlock(node.data.blockId);
if (!block) continue;
for (const field of block.configFields) {
if (field.required) {
const val = node.data.config[field.id];
if (val === undefined || val === '' || val === null) {
errors.push({
nodeId: node.id,
field: field.id,
severity: 'warning',
message: `${field.label} is required`,
rule: 'missing-required-field',
});
}
}
}
}

return errors;
}

generateSteps(
nodes: Node<BuilderNodeData>[],
edges: Edge<BuilderEdgeData>[],
environment: Environment,
): FlowStep[] {
const sorted = this.topologicalSort(nodes, edges);
const baseUrl = ENV_URLS[environment];

return sorted.map((node, index) => {
const block = this.registry.getBlock(node.data.blockId);
const config = node.data.config;
const method = block?.method ?? node.data.method;
let endpoint = block?.endpoint ?? node.data.endpoint;

// Build query params for GET requests
const queryParams: Record<string, string> = {};
const body: Record<string, unknown> = {};

if (block) {
for (const field of block.configFields) {
const val = config[field.id];
if (val === undefined || val === '') continue;

if (method === 'GET') {
queryParams[field.id] = String(val);
} else {
body[field.id] = val;
}
}
}

// Replace path params
for (const [key, val] of Object.entries(config)) {
if (typeof val === 'string' && endpoint.includes(`{${key}}`)) {
endpoint = endpoint.replace(`{${key}}`, val);
}
}

const incomingEdges = edges.filter(e => e.target === node.id);
const bindings = incomingEdges.flatMap(e => e.data?.bindings ?? []);

return {
order: index + 1,
nodeId: node.id,
blockId: node.data.blockId,
name: node.data.label,
method,
endpoint: `${baseUrl}${endpoint}`,
headers: { 'Content-Type': 'application/json' },
queryParams,
body: method !== 'GET' ? body : null,
bindings,
guideLink: node.data.guideLink,
description: block?.description ?? '',
};
});
}

private hasCycle(nodes: Node<BuilderNodeData>[], edges: Edge<BuilderEdgeData>[]): boolean {
const adjacency = new Map<string, string[]>();
for (const node of nodes) adjacency.set(node.id, []);
for (const edge of edges) {
adjacency.get(edge.source)?.push(edge.target);
}

const visited = new Set<string>();
const inStack = new Set<string>();

const dfs = (nodeId: string): boolean => {
if (inStack.has(nodeId)) return true;
if (visited.has(nodeId)) return false;
visited.add(nodeId);
inStack.add(nodeId);
for (const neighbor of adjacency.get(nodeId) ?? []) {
if (dfs(neighbor)) return true;
}
inStack.delete(nodeId);
return false;
};

for (const node of nodes) {
if (dfs(node.id)) return true;
}
return false;
}

private topologicalSort(
nodes: Node<BuilderNodeData>[],
edges: Edge<BuilderEdgeData>[],
): Node<BuilderNodeData>[] {
const inDegree = new Map<string, number>();
const adjacency = new Map<string, string[]>();
const nodeMap = new Map<string, Node<BuilderNodeData>>();

for (const node of nodes) {
inDegree.set(node.id, 0);
adjacency.set(node.id, []);
nodeMap.set(node.id, node);
}

for (const edge of edges) {
adjacency.get(edge.source)?.push(edge.target);
inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1);
}

const queue: string[] = [];
for (const [id, deg] of inDegree) {
if (deg === 0) queue.push(id);
}

const result: Node<BuilderNodeData>[] = [];
while (queue.length > 0) {
const id = queue.shift()!;
result.push(nodeMap.get(id)!);
for (const neighbor of adjacency.get(id) ?? []) {
const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
inDegree.set(neighbor, newDeg);
if (newDeg === 0) queue.push(neighbor);
}
}

return result;
}
}
  • Step 4: Run tests
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/FlowEngine.test.ts

Expected: All PASS.

  • Step 5: Commit
git add src/engine/
git commit -m "feat: implement FlowEngine with validation and topological ordering"

Task 2.3: CodeGenerator

Files:

  • Create: src/engine/CodeGenerator.ts

  • Create: src/engine/__tests__/CodeGenerator.test.ts

  • Step 1: Write the failing test

// src/engine/__tests__/CodeGenerator.test.ts
import { describe, it, expect } from 'vitest';
import { CodeGenerator } from '../CodeGenerator';
import type { FlowStep } from '../../types/builder';

const sampleSteps: FlowStep[] = [
{
order: 1,
nodeId: 'n1',
blockId: 'login',
name: 'Login',
method: 'POST',
endpoint: 'https://api.sandbox.avenia.io:10952/v2/auth/login',
headers: { 'Content-Type': 'application/json' },
queryParams: {},
body: { email: 'user@example.com', password: 'secret' },
bindings: [],
guideLink: '/docs/Avenia-Account-Management/login-guide',
description: 'Send login email',
},
{
order: 2,
nodeId: 'n2',
blockId: 'validate-login',
name: 'Validate Login',
method: 'POST',
endpoint: 'https://api.sandbox.avenia.io:10952/v2/auth/validate-login',
headers: { 'Content-Type': 'application/json' },
queryParams: {},
body: { email: 'user@example.com', emailToken: '000000' },
bindings: [],
guideLink: '/docs/Avenia-Account-Management/login-guide',
description: 'Confirm email token',
},
];

describe('CodeGenerator', () => {
const generator = new CodeGenerator();

it('generates valid cURL commands', () => {
const curl = generator.generate(sampleSteps, 'curl');
expect(curl).toContain('curl');
expect(curl).toContain('/v2/auth/login');
expect(curl).toContain('-X POST');
expect(curl).toContain('"email"');
});

it('generates valid Node.js code', () => {
const node = generator.generate(sampleSteps, 'node');
expect(node).toContain('fetch');
expect(node).toContain('/v2/auth/login');
expect(node).toContain('Authorization');
});

it('generates valid Python code', () => {
const python = generator.generate(sampleSteps, 'python');
expect(python).toContain('requests');
expect(python).toContain('/v2/auth/login');
});

it('generates valid Go code', () => {
const go = generator.generate(sampleSteps, 'go');
expect(go).toContain('http.NewRequest');
expect(go).toContain('/v2/auth/login');
});

it('includes Authorization header for authenticated steps', () => {
const curl = generator.generate(sampleSteps, 'curl');
// Second step should reference the token from first step
expect(curl).toContain('Authorization');
});

it('handles GET with query params', () => {
const getStep: FlowStep = {
order: 1,
nodeId: 'q1',
blockId: 'get-quote',
name: 'Get Quote',
method: 'GET',
endpoint: 'https://api.sandbox.avenia.io:10952/v2/account/quote/fixed-rate',
headers: { 'Content-Type': 'application/json' },
queryParams: { inputCurrency: 'BRL', outputCurrency: 'USDC', inputAmount: '100' },
body: null,
bindings: [],
guideLink: '/docs/Operations/quotes-and-tickets',
description: 'Get quote',
};
const curl = generator.generate([getStep], 'curl');
expect(curl).toContain('inputCurrency=BRL');
expect(curl).toContain('outputCurrency=USDC');
});
});
  • Step 2: Run test to verify it fails
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/CodeGenerator.test.ts
  • Step 3: Implement CodeGenerator
// src/engine/CodeGenerator.ts
import type { FlowStep, CodeLanguage } from '../types/builder';

export class CodeGenerator {
generate(steps: FlowStep[], language: CodeLanguage): string {
switch (language) {
case 'curl': return this.generateCurl(steps);
case 'node': return this.generateNode(steps);
case 'python': return this.generatePython(steps);
case 'go': return this.generateGo(steps);
}
}

private buildUrl(step: FlowStep): string {
const params = Object.entries(step.queryParams);
if (params.length === 0) return step.endpoint;
const qs = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
return `${step.endpoint}?${qs}`;
}

private needsAuth(step: FlowStep): boolean {
return step.blockId !== 'login' && step.blockId !== 'create-account';
}

private generateCurl(steps: FlowStep[]): string {
const lines: string[] = ['#!/bin/bash', '# Generated by Avenia Integration Builder', ''];

for (const step of steps) {
lines.push(`# Step ${step.order}: ${step.name}`);
lines.push(`# ${step.description}`);
lines.push(`# Docs: ${step.guideLink}`);

const url = this.buildUrl(step);
const parts = [`curl -X ${step.method}`];
parts.push(` -H "Content-Type: application/json"`);

if (this.needsAuth(step)) {
parts.push(` -H "Authorization: Bearer $ACCESS_TOKEN"`);
}

if (step.body && Object.keys(step.body).length > 0) {
parts.push(` -d '${JSON.stringify(step.body, null, 2)}'`);
}

parts.push(` "${url}"`);
lines.push(parts.join(' \\\n'));

// Capture token from validate-login
if (step.blockId === 'validate-login') {
lines.push('');
lines.push('# Save the access token for subsequent requests');
lines.push('# ACCESS_TOKEN=$(echo $RESPONSE | jq -r .accessToken)');
}

// Capture quoteToken from get-quote
if (step.blockId === 'get-quote') {
lines.push('');
lines.push('# Save the quote token (expires in ~15 seconds)');
lines.push('# QUOTE_TOKEN=$(echo $RESPONSE | jq -r .quoteToken)');
}

lines.push('');
}

return lines.join('\n');
}

private generateNode(steps: FlowStep[]): string {
const lines: string[] = [
'// Generated by Avenia Integration Builder',
'',
'const BASE_URL = "' + (steps[0]?.endpoint.split('/v2')[0] ?? '') + '";',
'',
'async function runFlow() {',
' let accessToken = "";',
' let quoteToken = "";',
'',
];

for (const step of steps) {
lines.push(` // Step ${step.order}: ${step.name}`);
lines.push(` // Docs: ${step.guideLink}`);

const url = this.buildUrl(step).replace(steps[0]?.endpoint.split('/v2')[0] ?? '', '${BASE_URL}');

if (step.method === 'GET') {
lines.push(` const step${step.order}Response = await fetch(\`${url}\`, {`);
lines.push(` method: "${step.method}",`);
lines.push(` headers: {`);
lines.push(` "Content-Type": "application/json",`);
if (this.needsAuth(step)) {
lines.push(` "Authorization": \`Bearer \${accessToken}\`,`);
}
lines.push(` },`);
lines.push(` });`);
} else {
const bodyStr = step.body ? JSON.stringify(step.body, null, 4).replace(/^/gm, ' ').trim() : '{}';
lines.push(` const step${step.order}Response = await fetch(\`${url}\`, {`);
lines.push(` method: "${step.method}",`);
lines.push(` headers: {`);
lines.push(` "Content-Type": "application/json",`);
if (this.needsAuth(step)) {
lines.push(` "Authorization": \`Bearer \${accessToken}\`,`);
}
lines.push(` },`);
lines.push(` body: JSON.stringify(${bodyStr}),`);
lines.push(` });`);
}

lines.push(` const step${step.order}Data = await step${step.order}Response.json();`);
lines.push(` console.log("Step ${step.order} (${step.name}):", step${step.order}Data);`);

if (step.blockId === 'validate-login') {
lines.push(` accessToken = step${step.order}Data.accessToken;`);
}
if (step.blockId === 'get-quote') {
lines.push(` quoteToken = step${step.order}Data.quoteToken;`);
}

lines.push('');
}

lines.push('}');
lines.push('');
lines.push('runFlow().catch(console.error);');

return lines.join('\n');
}

private generatePython(steps: FlowStep[]): string {
const lines: string[] = [
'# Generated by Avenia Integration Builder',
'import requests',
'',
`BASE_URL = "${steps[0]?.endpoint.split('/v2')[0] ?? ''}"`,
'',
'access_token = ""',
'quote_token = ""',
'',
];

for (const step of steps) {
lines.push(`# Step ${step.order}: ${step.name}`);
lines.push(`# Docs: ${step.guideLink}`);

const relPath = step.endpoint.replace(steps[0]?.endpoint.split('/v2')[0] ?? '', '');
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.needsAuth(step)) {
headers['Authorization'] = 'Bearer {access_token}';
}

const headerStr = JSON.stringify(headers).replace('{access_token}', '{access_token}');

if (step.method === 'GET') {
const params = Object.entries(step.queryParams);
if (params.length > 0) {
const paramStr = params.map(([k, v]) => `"${k}": "${v}"`).join(', ');
lines.push(`response = requests.get(`);
lines.push(` f"{BASE_URL}${relPath}",`);
lines.push(` headers={"Content-Type": "application/json"${this.needsAuth(step) ? ', "Authorization": f"Bearer {access_token}"' : ''}},`);
lines.push(` params={${paramStr}},`);
lines.push(`)`);
} else {
lines.push(`response = requests.get(`);
lines.push(` f"{BASE_URL}${relPath}",`);
lines.push(` headers={"Content-Type": "application/json"${this.needsAuth(step) ? ', "Authorization": f"Bearer {access_token}"' : ''}},`);
lines.push(`)`);
}
} else {
const bodyStr = step.body ? JSON.stringify(step.body, null, 4) : '{}';
lines.push(`response = requests.${step.method.toLowerCase()}(`);
lines.push(` f"{BASE_URL}${relPath}",`);
lines.push(` headers={"Content-Type": "application/json"${this.needsAuth(step) ? ', "Authorization": f"Bearer {access_token}"' : ''}},`);
lines.push(` json=${bodyStr},`);
lines.push(`)`);
}

lines.push(`step${step.order}_data = response.json()`);
lines.push(`print(f"Step ${step.order} (${step.name}):", step${step.order}_data)`);

if (step.blockId === 'validate-login') {
lines.push(`access_token = step${step.order}_data["accessToken"]`);
}
if (step.blockId === 'get-quote') {
lines.push(`quote_token = step${step.order}_data["quoteToken"]`);
}

lines.push('');
}

return lines.join('\n');
}

private generateGo(steps: FlowStep[]): string {
const lines: string[] = [
'// Generated by Avenia Integration Builder',
'package main',
'',
'import (',
'\t"bytes"',
'\t"encoding/json"',
'\t"fmt"',
'\t"io"',
'\t"net/http"',
')',
'',
`const baseURL = "${steps[0]?.endpoint.split('/v2')[0] ?? ''}"`,
'',
'func main() {',
'\tvar accessToken string',
'\t_ = accessToken',
'',
];

for (const step of steps) {
lines.push(`\t// Step ${step.order}: ${step.name}`);
lines.push(`\t// Docs: ${step.guideLink}`);

const relPath = step.endpoint.replace(steps[0]?.endpoint.split('/v2')[0] ?? '', '');
const url = step.method === 'GET' && Object.keys(step.queryParams).length > 0
? `${relPath}?${Object.entries(step.queryParams).map(([k, v]) => `${k}=${v}`).join('&')}`
: relPath;

if (step.body && Object.keys(step.body).length > 0) {
const bodyStr = JSON.stringify(step.body);
lines.push(`\tbody${step.order} := []byte(\`${bodyStr}\`)`);
lines.push(`\treq${step.order}, _ := http.NewRequest("${step.method}", baseURL+"${url}", bytes.NewBuffer(body${step.order}))`);
} else {
lines.push(`\treq${step.order}, _ := http.NewRequest("${step.method}", baseURL+"${url}", nil)`);
}

lines.push(`\treq${step.order}.Header.Set("Content-Type", "application/json")`);
if (this.needsAuth(step)) {
lines.push(`\treq${step.order}.Header.Set("Authorization", "Bearer "+accessToken)`);
}

lines.push(`\tresp${step.order}, _ := http.DefaultClient.Do(req${step.order})`);
lines.push(`\tdefer resp${step.order}.Body.Close()`);
lines.push(`\trespBody${step.order}, _ := io.ReadAll(resp${step.order}.Body)`);
lines.push(`\tfmt.Printf("Step ${step.order} (${step.name}): %s\\n", respBody${step.order})`);

if (step.blockId === 'validate-login') {
lines.push(`\tvar loginResult${step.order} map[string]interface{}`);
lines.push(`\tjson.Unmarshal(respBody${step.order}, &loginResult${step.order})`);
lines.push(`\taccessToken = loginResult${step.order}["accessToken"].(string)`);
}

lines.push('');
}

lines.push('}');

return lines.join('\n');
}
}
  • Step 4: Run tests
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/CodeGenerator.test.ts

Expected: All PASS.

  • Step 5: Commit
git add src/engine/
git commit -m "feat: implement CodeGenerator for cURL, Node, Python, Go snippets"

Task 2.4: MarkdownGenerator

Files:

  • Create: src/engine/MarkdownGenerator.ts

  • Create: src/engine/__tests__/MarkdownGenerator.test.ts

  • Step 1: Write the failing test

// src/engine/__tests__/MarkdownGenerator.test.ts
import { describe, it, expect } from 'vitest';
import { MarkdownGenerator } from '../MarkdownGenerator';
import type { FlowStep } from '../../types/builder';

const sampleSteps: FlowStep[] = [
{
order: 1,
nodeId: 'n1',
blockId: 'login',
name: 'Login',
method: 'POST',
endpoint: 'https://api.sandbox.avenia.io:10952/v2/auth/login',
headers: { 'Content-Type': 'application/json' },
queryParams: {},
body: { email: 'user@example.com', password: 'secret' },
bindings: [],
guideLink: '/docs/Avenia-Account-Management/login-guide',
description: 'Send login email',
},
];

describe('MarkdownGenerator', () => {
const generator = new MarkdownGenerator();

it('generates valid markdown with title and steps', () => {
const md = generator.generate(sampleSteps);
expect(md).toContain('# Integration Flow');
expect(md).toContain('## Step 1: Login');
expect(md).toContain('POST');
expect(md).toContain('/v2/auth/login');
});

it('includes guide links', () => {
const md = generator.generate(sampleSteps);
expect(md).toContain('/docs/Avenia-Account-Management/login-guide');
});

it('includes JSON body in code block', () => {
const md = generator.generate(sampleSteps);
expect(md).toContain('```json');
expect(md).toContain('"email"');
});
});
  • Step 2: Run test to verify it fails
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/MarkdownGenerator.test.ts
  • Step 3: Implement MarkdownGenerator
// src/engine/MarkdownGenerator.ts
import type { FlowStep } from '../types/builder';

export class MarkdownGenerator {
generate(steps: FlowStep[]): string {
const lines: string[] = [
'# Integration Flow',
'',
`> Generated by [Avenia Integration Builder](/integration-builder)`,
`> ${steps.length} step${steps.length !== 1 ? 's' : ''}`,
'',
'---',
'',
];

for (const step of steps) {
lines.push(`## Step ${step.order}: ${step.name}`);
lines.push('');
lines.push(`**${step.method}** \`${step.endpoint}\``);
lines.push('');
lines.push(step.description);
lines.push('');
lines.push(`[View Documentation](${step.guideLink})`);
lines.push('');

// Headers
if (Object.keys(step.headers).length > 0) {
lines.push('**Headers:**');
lines.push('```');
for (const [key, val] of Object.entries(step.headers)) {
lines.push(`${key}: ${val}`);
}
if (step.blockId !== 'login' && step.blockId !== 'create-account') {
lines.push('Authorization: Bearer {accessToken}');
}
lines.push('```');
lines.push('');
}

// Query params
if (Object.keys(step.queryParams).length > 0) {
lines.push('**Query Parameters:**');
lines.push('| Parameter | Value |');
lines.push('|-----------|-------|');
for (const [key, val] of Object.entries(step.queryParams)) {
lines.push(`| ${key} | ${val} |`);
}
lines.push('');
}

// Body
if (step.body && Object.keys(step.body).length > 0) {
lines.push('**Request Body:**');
lines.push('```json');
lines.push(JSON.stringify(step.body, null, 2));
lines.push('```');
lines.push('');
}

// Bindings
if (step.bindings.length > 0) {
lines.push('**Data Bindings:**');
for (const binding of step.bindings) {
lines.push(`- \`step${binding.sourceNodeId}.${binding.sourceField}\` -> \`${binding.targetField}\``);
}
lines.push('');
}

lines.push('---');
lines.push('');
}

return lines.join('\n');
}
}
  • Step 4: Run tests
cd ~/Projects/front/integration-guide && npx vitest run src/engine/__tests__/MarkdownGenerator.test.ts

Expected: All PASS.

  • Step 5: Commit
git add src/engine/
git commit -m "feat: implement MarkdownGenerator for flow report output"

Task 2.5: Engine Index

Files:

  • Create: src/engine/index.ts

  • Step 1: Create engine barrel export

// src/engine/index.ts
export { FlowEngine } from './FlowEngine';
export { CodeGenerator } from './CodeGenerator';
export { MarkdownGenerator } from './MarkdownGenerator';
export { BindingResolver } from './BindingResolver';
  • Step 2: Run all engine tests
cd ~/Projects/front/integration-guide && npx vitest run src/engine/

Expected: All tests PASS.

  • Step 3: Commit
git add src/engine/index.ts
git commit -m "feat: add engine barrel export"

Phase 3: Zustand Store and Hooks

Task 3.1: Builder Store

Files:

  • Create: src/store/useBuilderStore.ts

  • Step 1: Implement the store

// src/store/useBuilderStore.ts
import { create } from 'zustand';
import type { Node, Edge, OnNodesChange, OnEdgesChange, Connection } from '@xyflow/react';
import { applyNodeChanges, applyEdgeChanges, addEdge } from '@xyflow/react';
import type {
BuilderNodeData,
BuilderEdgeData,
ThemeMode,
Environment,
ValidationError,
GeneratedFlow,
NodeConfig,
} from '../types/builder';
import { registry } from '../registry';
import { BindingResolver } from '../engine/BindingResolver';

const bindingResolver = new BindingResolver(registry);

interface BuilderActions {
// Node operations
onNodesChange: OnNodesChange<Node<BuilderNodeData>>;
onEdgesChange: OnEdgesChange<Edge<BuilderEdgeData>>;
onConnect: (connection: Connection) => void;
addNode: (blockId: string, position: { x: number; y: number }) => void;
removeNode: (nodeId: string) => void;
toggleNodeExpanded: (nodeId: string) => void;
updateNodeConfig: (nodeId: string, config: NodeConfig) => void;
selectNode: (nodeId: string | null) => void;

// UI
toggleSidebar: () => void;
setTheme: (theme: ThemeMode) => void;
setEnvironment: (env: Environment) => void;
toggleReport: (open?: boolean) => void;

// Flow
setGeneratedFlow: (flow: GeneratedFlow | null) => void;
setValidationErrors: (errors: ValidationError[]) => void;
clearCanvas: () => void;

// Undo
undo: () => void;
redo: () => void;
}

interface UndoState {
nodes: Node<BuilderNodeData>[];
edges: Edge<BuilderEdgeData>[];
}

interface BuilderStore {
nodes: Node<BuilderNodeData>[];
edges: Edge<BuilderEdgeData>[];
sidebarOpen: boolean;
theme: ThemeMode;
environment: Environment;
selectedNodeId: string | null;
reportOpen: boolean;
generatedFlow: GeneratedFlow | null;
validationErrors: ValidationError[];
undoStack: UndoState[];
redoStack: UndoState[];
actions: BuilderActions;
}

let nodeCounter = 0;

const pushUndo = (state: BuilderStore): Partial<BuilderStore> => ({
undoStack: [...state.undoStack.slice(-49), { nodes: state.nodes, edges: state.edges }],
redoStack: [],
});

export const useBuilderStore = create<BuilderStore>((set, get) => ({
nodes: [],
edges: [],
sidebarOpen: true,
theme: 'dark',
environment: 'sandbox',
selectedNodeId: null,
reportOpen: false,
generatedFlow: null,
validationErrors: [],
undoStack: [],
redoStack: [],

actions: {
onNodesChange: (changes) => {
set((state) => ({
nodes: applyNodeChanges(changes, state.nodes) as Node<BuilderNodeData>[],
}));
},

onEdgesChange: (changes) => {
set((state) => ({
edges: applyEdgeChanges(changes, state.edges) as Edge<BuilderEdgeData>[],
}));
},

onConnect: (connection) => {
const state = get();
const sourceNode = state.nodes.find(n => n.id === connection.source);
const targetNode = state.nodes.find(n => n.id === connection.target);

let bindings: BuilderEdgeData['bindings'] = [];
if (sourceNode && targetNode) {
bindings = bindingResolver.resolve(sourceNode.data.blockId, targetNode.data.blockId);
}

set((state) => ({
...pushUndo(state),
edges: addEdge(
{
...connection,
data: { bindings, animated: true },
},
state.edges,
) as Edge<BuilderEdgeData>[],
}));
},

addNode: (blockId, position) => {
const block = registry.getBlock(blockId);
if (!block) return;

const id = `node-${++nodeCounter}`;
const defaultConfig: NodeConfig = {};
for (const field of block.configFields) {
if (field.default !== undefined) {
defaultConfig[field.id] = field.default;
}
}

const newNode: Node<BuilderNodeData> = {
id,
type: 'builderNode',
position,
data: {
blockId: block.id,
label: block.name,
icon: block.icon,
method: block.method,
endpoint: block.endpoint,
category: block.category,
guideLink: block.guideLink,
config: defaultConfig,
expanded: false,
validationErrors: [],
},
};

set((state) => ({
...pushUndo(state),
nodes: [...state.nodes, newNode],
}));
},

removeNode: (nodeId) => {
set((state) => ({
...pushUndo(state),
nodes: state.nodes.filter(n => n.id !== nodeId),
edges: state.edges.filter(e => e.source !== nodeId && e.target !== nodeId),
selectedNodeId: state.selectedNodeId === nodeId ? null : state.selectedNodeId,
}));
},

toggleNodeExpanded: (nodeId) => {
set((state) => ({
nodes: state.nodes.map(n =>
n.id === nodeId ? { ...n, data: { ...n.data, expanded: !n.data.expanded } } : n,
),
}));
},

updateNodeConfig: (nodeId, config) => {
set((state) => ({
nodes: state.nodes.map(n =>
n.id === nodeId ? { ...n, data: { ...n.data, config: { ...n.data.config, ...config } } } : n,
),
}));
},

selectNode: (nodeId) => {
set({ selectedNodeId: nodeId });
},

toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setTheme: (theme) => set({ theme }),
setEnvironment: (env) => set({ environment: env }),
toggleReport: (open) => set((s) => ({ reportOpen: open ?? !s.reportOpen })),
setGeneratedFlow: (flow) => set({ generatedFlow: flow }),
setValidationErrors: (errors) => set({ validationErrors: errors }),

clearCanvas: () => {
set((state) => ({
...pushUndo(state),
nodes: [],
edges: [],
selectedNodeId: null,
generatedFlow: null,
validationErrors: [],
}));
},

undo: () => {
const state = get();
const prev = state.undoStack[state.undoStack.length - 1];
if (!prev) return;
set({
nodes: prev.nodes,
edges: prev.edges,
undoStack: state.undoStack.slice(0, -1),
redoStack: [...state.redoStack, { nodes: state.nodes, edges: state.edges }],
});
},

redo: () => {
const state = get();
const next = state.redoStack[state.redoStack.length - 1];
if (!next) return;
set({
nodes: next.nodes,
edges: next.edges,
redoStack: state.redoStack.slice(0, -1),
undoStack: [...state.undoStack, { nodes: state.nodes, edges: state.edges }],
});
},
},
}));
  • Step 2: Commit
git add src/store/
git commit -m "feat: implement Zustand builder store with undo/redo"

Task 3.2: Custom Hooks

Files:

  • Create: src/hooks/useFlowValidation.ts

  • Create: src/hooks/useAutoBinding.ts

  • Create: src/hooks/useTheme.ts

  • Create: src/hooks/useCanvasExport.ts

  • Step 1: Implement useFlowValidation

// src/hooks/useFlowValidation.ts
import { useEffect } from 'react';
import { useBuilderStore } from '../store/useBuilderStore';
import { FlowEngine } from '../engine/FlowEngine';
import { registry } from '../registry';

const engine = new FlowEngine(registry);

export function useFlowValidation() {
const nodes = useBuilderStore((s) => s.nodes);
const edges = useBuilderStore((s) => s.edges);
const setValidationErrors = useBuilderStore((s) => s.actions.setValidationErrors);

useEffect(() => {
const errors = engine.validate(nodes, edges);
setValidationErrors(errors);
}, [nodes, edges, setValidationErrors]);

const validationErrors = useBuilderStore((s) => s.validationErrors);
const hasBlockingErrors = validationErrors.some((e) => e.severity === 'error');
const canGenerate = nodes.length > 0 && !hasBlockingErrors;

return { validationErrors, hasBlockingErrors, canGenerate };
}
  • Step 2: Implement useTheme
// src/hooks/useTheme.ts
import { useEffect } from 'react';
import { useBuilderStore } from '../store/useBuilderStore';

export function useTheme() {
const theme = useBuilderStore((s) => s.theme);
const setTheme = useBuilderStore((s) => s.actions.setTheme);

useEffect(() => {
document.documentElement.setAttribute('data-builder-theme', theme);
}, [theme]);

const toggle = () => setTheme(theme === 'dark' ? 'light' : 'dark');

return { theme, setTheme, toggle };
}
  • Step 3: Implement useCanvasExport
// src/hooks/useCanvasExport.ts
import { useCallback } from 'react';
import { toPng, toSvg } from 'html-to-image';

export function useCanvasExport() {
const exportPng = useCallback(async () => {
const canvas = document.querySelector('.react-flow') as HTMLElement;
if (!canvas) return;
const dataUrl = await toPng(canvas, { backgroundColor: '#060607' });
const link = document.createElement('a');
link.download = 'integration-flow.png';
link.href = dataUrl;
link.click();
}, []);

const exportSvg = useCallback(async () => {
const canvas = document.querySelector('.react-flow') as HTMLElement;
if (!canvas) return;
const dataUrl = await toSvg(canvas, { backgroundColor: '#060607' });
const link = document.createElement('a');
link.download = 'integration-flow.svg';
link.href = dataUrl;
link.click();
}, []);

return { exportPng, exportSvg };
}
  • Step 4: Implement useAutoBinding (already handled in store's onConnect, this hook is for edge updates)
// src/hooks/useAutoBinding.ts
import { useCallback } from 'react';
import { useBuilderStore } from '../store/useBuilderStore';
import { BindingResolver } from '../engine/BindingResolver';
import { registry } from '../registry';

const resolver = new BindingResolver(registry);

export function useAutoBinding() {
const nodes = useBuilderStore((s) => s.nodes);

const resolveBindings = useCallback((sourceNodeId: string, targetNodeId: string) => {
const sourceNode = nodes.find(n => n.id === sourceNodeId);
const targetNode = nodes.find(n => n.id === targetNodeId);
if (!sourceNode || !targetNode) return [];
return resolver.resolve(sourceNode.data.blockId, targetNode.data.blockId);
}, [nodes]);

return { resolveBindings };
}
  • Step 5: Commit
git add src/hooks/
git commit -m "feat: add builder hooks (validation, theme, export, auto-binding)"

Phase 4: UI Components

Task 4.1: CSS Variables and Theme

Files:

  • Create: src/components/builder/builder.module.css

  • Step 1: Create the CSS variables file

/* src/components/builder/builder.module.css */

/* Dark theme (default) */
:root,
[data-builder-theme="dark"] {
--builder-bg-deep: #060607;
--builder-bg-surface: #0F0A1A;
--builder-bg-card: #130E20;
--builder-border: #2A1D45;
--builder-border-subtle: #1A1230;
--builder-accent: #A78BFA;
--builder-accent-dark: #7C3AED;
--builder-accent-bg: #1E1035;
--builder-secondary: #F472B6;
--builder-success: #4ADE80;
--builder-info: #60A5FA;
--builder-warning: #FBBF24;
--builder-text-primary: #E0DCD8;
--builder-text-secondary: #BCB6B8;
--builder-text-muted: #827F9A;
--builder-text-dim: #3C3C54;
}

/* Light theme */
[data-builder-theme="light"] {
--builder-bg-deep: #F8F6F4;
--builder-bg-surface: #FFFFFF;
--builder-bg-card: #F0EDE8;
--builder-border: #D2CCCA;
--builder-border-subtle: #E0DCD8;
--builder-accent: #7C3AED;
--builder-accent-dark: #6D28D9;
--builder-accent-bg: #EDE9FE;
--builder-secondary: #F472B6;
--builder-success: #22C55E;
--builder-info: #3B82F6;
--builder-warning: #F59E0B;
--builder-text-primary: #1A1230;
--builder-text-secondary: #3C3C54;
--builder-text-muted: #827F9A;
--builder-text-dim: #D2CCCA;
}

.builderLayout {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
background: var(--builder-bg-deep);
color: var(--builder-text-primary);
font-family: 'DM Mono', monospace;
position: relative;
}

.canvasArea {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
}

.canvasWrapper {
flex: 1;
position: relative;
}
  • Step 2: Commit
git add src/components/builder/
git commit -m "feat: add Aurora Violet CSS variables and builder layout"

Task 4.2: Shared Components

Files:

  • Create: src/components/builder/shared/MethodBadge.tsx

  • Create: src/components/builder/shared/MethodBadge.module.css

  • Create: src/components/builder/shared/GuideLink.tsx

  • Create: src/components/builder/shared/GuideLink.module.css

  • Create: src/components/builder/shared/BindingBadge.tsx

  • Create: src/components/builder/shared/BindingBadge.module.css

  • Step 1: Create MethodBadge

// src/components/builder/shared/MethodBadge.tsx
import React, { memo } from 'react';
import type { HttpMethod } from '../../../types/builder';
import styles from './MethodBadge.module.css';

const METHOD_COLORS: Record<HttpMethod, string> = {
GET: 'var(--builder-info)',
POST: 'var(--builder-success)',
PUT: 'var(--builder-warning)',
PATCH: 'var(--builder-warning)',
DELETE: '#EF4444',
};

interface MethodBadgeProps {
method: HttpMethod;
}

export const MethodBadge = memo(function MethodBadge({ method }: MethodBadgeProps) {
return (
<span
className={styles.badge}
style={{ backgroundColor: METHOD_COLORS[method], color: '#fff' }}
>
{method}
</span>
);
});
/* src/components/builder/shared/MethodBadge.module.css */
.badge {
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
line-height: 1.4;
text-transform: uppercase;
}
  • Step 2: Create GuideLink
// src/components/builder/shared/GuideLink.tsx
import React, { memo } from 'react';
import { ExternalLink } from 'lucide-react';
import styles from './GuideLink.module.css';

interface GuideLinkProps {
href: string;
label?: string;
}

export const GuideLink = memo(function GuideLink({ href, label = 'Integration Guide' }: GuideLinkProps) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" className={styles.link}>
<ExternalLink size={12} strokeWidth={1.5} />
{label}
</a>
);
});
/* src/components/builder/shared/GuideLink.module.css */
.link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--builder-accent);
text-decoration: none;
transition: opacity 0.2s;
}

.link:hover {
opacity: 0.8;
text-decoration: underline;
}
  • Step 3: Create BindingBadge
// src/components/builder/shared/BindingBadge.tsx
import React, { memo } from 'react';
import type { Binding } from '../../../types/builder';
import styles from './BindingBadge.module.css';

interface BindingBadgeProps {
binding: Binding;
}

export const BindingBadge = memo(function BindingBadge({ binding }: BindingBadgeProps) {
return (
<span className={styles.badge}>
{binding.sourceField}{binding.targetField}
</span>
);
});
/* src/components/builder/shared/BindingBadge.module.css */
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
background: var(--builder-accent-bg);
color: var(--builder-accent);
border: 1px solid var(--builder-border-subtle);
}
  • Step 4: Commit
git add src/components/builder/shared/
git commit -m "feat: add shared builder components (MethodBadge, GuideLink, BindingBadge)"

Task 4.3: Builder Nodes

Files:

  • Create: src/components/builder/nodes/BaseNode.tsx

  • Create: src/components/builder/nodes/BaseNode.module.css

  • Create: src/components/builder/config/NodeConfigPanel.tsx

  • Create: src/components/builder/config/NodeConfigPanel.module.css

  • Step 1: Create BaseNode (handles both collapsed and expanded)

// src/components/builder/nodes/BaseNode.tsx
import React, { memo, useCallback } from 'react';
import { Handle, Position } from '@xyflow/react';
import type { NodeProps } from '@xyflow/react';
import * as LucideIcons from 'lucide-react';
import type { BuilderNodeData } from '../../../types/builder';
import { MethodBadge } from '../shared/MethodBadge';
import { GuideLink } from '../shared/GuideLink';
import { NodeConfigPanel } from '../config/NodeConfigPanel';
import { useBuilderStore } from '../../../store/useBuilderStore';
import styles from './BaseNode.module.css';

type LucideIconComponent = React.FC<{ size?: number; strokeWidth?: number }>;

function getIcon(name: string): LucideIconComponent {
const pascalName = name
.split('-')
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
const Icon = (LucideIcons as Record<string, LucideIconComponent>)[pascalName];
return Icon ?? LucideIcons.Box;
}

export const BuilderNode = memo(function BuilderNode({ id, data, selected }: NodeProps<BuilderNodeData>) {
const toggleExpanded = useBuilderStore(s => s.actions.toggleNodeExpanded);
const validationErrors = data.validationErrors;
const Icon = getIcon(data.icon);

const handleClick = useCallback(() => {
toggleExpanded(id);
}, [id, toggleExpanded]);

return (
<div
className={`${styles.node} ${selected ? styles.selected : ''} ${data.expanded ? styles.expanded : ''}`}
onDoubleClick={handleClick}
>
<Handle type="target" position={Position.Top} className={styles.handle} />

<div className={styles.header}>
<div className={styles.iconWrap}>
<Icon size={18} strokeWidth={1.5} />
</div>
<div className={styles.headerText}>
<div className={styles.title}>{data.label}</div>
<div className={styles.endpoint}>
<MethodBadge method={data.method} />
<span className={styles.endpointText}>{data.endpoint}</span>
</div>
</div>
{validationErrors.length > 0 && (
<div className={styles.warningDot} title={validationErrors[0].message} />
)}
</div>

<GuideLink href={data.guideLink} />

{data.expanded && (
<div className={styles.configArea}>
<NodeConfigPanel nodeId={id} blockId={data.blockId} config={data.config} />
</div>
)}

<Handle type="source" position={Position.Bottom} className={styles.handle} />
</div>
);
});
/* src/components/builder/nodes/BaseNode.module.css */
.node {
background: var(--builder-bg-card);
border: 1px solid var(--builder-border-subtle);
border-radius: 8px;
padding: 10px 12px;
min-width: 185px;
max-width: 280px;
transition: border-color 0.2s, box-shadow 0.2s, max-height 0.25s ease-out;
cursor: pointer;
}

.node:hover {
border-color: var(--builder-border);
box-shadow: 0 2px 8px rgba(167, 139, 250, 0.1);
}

.selected {
border-color: var(--builder-accent-dark);
box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.2);
}

.expanded {
min-width: 260px;
max-width: 320px;
}

.header {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 6px;
}

.iconWrap {
width: 26px;
height: 26px;
border-radius: 6px;
background: linear-gradient(135deg, var(--builder-accent-bg), var(--builder-accent-dark));
display: flex;
align-items: center;
justify-content: center;
color: var(--builder-accent);
flex-shrink: 0;
}

.headerText {
flex: 1;
min-width: 0;
}

.title {
font-size: 12px;
font-weight: 600;
color: var(--builder-text-primary);
margin-bottom: 2px;
}

.endpoint {
display: flex;
align-items: center;
gap: 4px;
}

.endpointText {
font-size: 10px;
color: var(--builder-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.warningDot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--builder-warning);
flex-shrink: 0;
margin-top: 4px;
}

.configArea {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--builder-border-subtle);
}

.handle {
width: 8px;
height: 8px;
background: var(--builder-accent);
border: 2px solid var(--builder-bg-card);
}
  • Step 2: Create NodeConfigPanel
// src/components/builder/config/NodeConfigPanel.tsx
import React, { memo, useCallback } from 'react';
import { useBuilderStore } from '../../../store/useBuilderStore';
import { registry } from '../../../registry';
import type { NodeConfig, ConfigField } from '../../../types/builder';
import styles from './NodeConfigPanel.module.css';

interface NodeConfigPanelProps {
nodeId: string;
blockId: string;
config: NodeConfig;
}

export const NodeConfigPanel = memo(function NodeConfigPanel({ nodeId, blockId, config }: NodeConfigPanelProps) {
const updateConfig = useBuilderStore(s => s.actions.updateNodeConfig);
const block = registry.getBlock(blockId);
if (!block || block.configFields.length === 0) return null;

const handleChange = useCallback((fieldId: string, value: string | number | boolean) => {
updateConfig(nodeId, { [fieldId]: value });
}, [nodeId, updateConfig]);

return (
<div className={styles.panel}>
{block.configFields.map((field) => (
<ConfigFieldInput
key={field.id}
field={field}
value={config[field.id]}
onChange={(val) => handleChange(field.id, val)}
/>
))}
</div>
);
});

interface ConfigFieldInputProps {
field: ConfigField;
value: string | number | boolean | undefined;
onChange: (value: string | number | boolean) => void;
}

function ConfigFieldInput({ field, value, onChange }: ConfigFieldInputProps) {
if (field.type === 'select' && field.options) {
return (
<label className={styles.field}>
<span className={styles.label}>{field.label}</span>
<select
className={styles.select}
value={String(value ?? field.default ?? '')}
onChange={(e) => onChange(e.target.value)}
>
<option value="">Select...</option>
{field.options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</label>
);
}

if (field.type === 'currency-select') {
const currencies = registry.getAllCurrencies();
return (
<label className={styles.field}>
<span className={styles.label}>{field.label}</span>
<select
className={styles.select}
value={String(value ?? '')}
onChange={(e) => onChange(e.target.value)}
>
<option value="">Select currency...</option>
{currencies.map((c) => (
<option key={c.id} value={c.symbol}>{c.symbol} - {c.name}</option>
))}
</select>
</label>
);
}

if (field.type === 'chain-select') {
const chains = registry.getAllChains();
return (
<label className={styles.field}>
<span className={styles.label}>{field.label}</span>
<select
className={styles.select}
value={String(value ?? '')}
onChange={(e) => onChange(e.target.value)}
>
<option value="">Select chain...</option>
{chains.map((c) => (
<option key={c.id} value={c.name.toUpperCase()}>{c.name}</option>
))}
</select>
</label>
);
}

if (field.type === 'payment-method-select') {
const methods = registry.getAllPaymentMethods();
return (
<label className={styles.field}>
<span className={styles.label}>{field.label}</span>
<select
className={styles.select}
value={String(value ?? '')}
onChange={(e) => onChange(e.target.value)}
>
<option value="">Select method...</option>
{methods.map((m) => (
<option key={m.id} value={m.id.toUpperCase()}>{m.name}</option>
))}
</select>
</label>
);
}

if (field.type === 'number') {
return (
<label className={styles.field}>
<span className={styles.label}>{field.label}</span>
<input
className={styles.input}
type="number"
value={value !== undefined ? String(value) : ''}
placeholder={field.placeholder}
onChange={(e) => onChange(e.target.value ? Number(e.target.value) : '')}
/>
</label>
);
}

return (
<label className={styles.field}>
<span className={styles.label}>{field.label}</span>
<input
className={styles.input}
type="text"
value={String(value ?? '')}
placeholder={field.placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
}
/* src/components/builder/config/NodeConfigPanel.module.css */
.panel {
display: flex;
flex-direction: column;
gap: 6px;
}

.field {
display: flex;
flex-direction: column;
gap: 2px;
}

.label {
font-size: 10px;
color: var(--builder-text-muted);
font-weight: 500;
}

.input,
.select {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--builder-border-subtle);
background: var(--builder-bg-deep);
color: var(--builder-text-primary);
font-size: 11px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
}

.input:focus,
.select:focus {
border-color: var(--builder-accent);
}

.input::placeholder {
color: var(--builder-text-dim);
}
  • Step 3: Commit
git add src/components/builder/nodes/ src/components/builder/config/
git commit -m "feat: add BuilderNode and NodeConfigPanel components"

Task 4.4: AnimatedEdge

Files:

  • Create: src/components/builder/edges/AnimatedEdge.tsx

  • Create: src/components/builder/edges/AnimatedEdge.module.css

  • Step 1: Create AnimatedEdge

// src/components/builder/edges/AnimatedEdge.tsx
import React, { memo } from 'react';
import { getBezierPath, BaseEdge } from '@xyflow/react';
import type { EdgeProps } from '@xyflow/react';
import type { BuilderEdgeData } from '../../../types/builder';

export const AnimatedEdge = memo(function AnimatedEdge(props: EdgeProps<BuilderEdgeData>) {
const { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, id } = props;

const [edgePath] = getBezierPath({
sourceX, sourceY, sourcePosition,
targetX, targetY, targetPosition,
});

return (
<>
<BaseEdge
id={id}
path={edgePath}
style={{
stroke: 'url(#edge-gradient)',
strokeWidth: 1.5,
strokeDasharray: '5 5',
}}
/>
<defs>
<linearGradient id="edge-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="var(--builder-accent)" />
<stop offset="100%" stopColor="var(--builder-secondary)" />
</linearGradient>
</defs>
<circle r="2.5" fill="var(--builder-accent)">
<animateMotion dur="2.5s" repeatCount="indefinite" path={edgePath} />
</circle>
</>
);
});
  • Step 2: Commit
git add src/components/builder/edges/
git commit -m "feat: add AnimatedEdge with particle animation"

Task 4.5: Sidebar

Files:

  • Create: src/components/builder/Sidebar.tsx

  • Create: src/components/builder/Sidebar.module.css

  • Step 1: Create Sidebar

// src/components/builder/Sidebar.tsx
import React, { memo, useState, useCallback, DragEvent } from 'react';
import * as LucideIcons from 'lucide-react';
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
import { useBuilderStore } from '../../store/useBuilderStore';
import { registry } from '../../registry';
import { MethodBadge } from './shared/MethodBadge';
import type { BlockDefinition, BlockCategory } from '../../types/builder';
import styles from './Sidebar.module.css';

type LucideIconComponent = React.FC<{ size?: number; strokeWidth?: number }>;

function getIcon(name: string): LucideIconComponent {
const pascalName = name.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
return (LucideIcons as Record<string, LucideIconComponent>)[pascalName] ?? LucideIcons.Box;
}

const CATEGORY_ORDER: { id: BlockCategory; label: string }[] = [
{ id: 'auth', label: 'Authentication' },
{ id: 'account', label: 'Account' },
{ id: 'kyc', label: 'KYC / KYB' },
{ id: 'operations', label: 'Operations' },
{ id: 'beneficiaries', label: 'Beneficiaries' },
{ id: 'admin', label: 'Admin' },
];

export const Sidebar = memo(function Sidebar() {
const isOpen = useBuilderStore(s => s.sidebarOpen);
const toggleSidebar = useBuilderStore(s => s.actions.toggleSidebar);
const [search, setSearch] = useState('');

const allBlocks = registry.getAllBlocks();

const filtered = search
? allBlocks.filter(b =>
b.name.toLowerCase().includes(search.toLowerCase()) ||
b.description.toLowerCase().includes(search.toLowerCase())
)
: allBlocks;

const onDragStart = useCallback((e: DragEvent, block: BlockDefinition) => {
e.dataTransfer.setData('application/builder-block', block.id);
e.dataTransfer.effectAllowed = 'move';
}, []);

return (
<div className={`${styles.sidebar} ${isOpen ? styles.open : styles.collapsed}`}>
<button className={styles.toggleBtn} onClick={toggleSidebar}>
{isOpen ? <ChevronLeft size={16} strokeWidth={1.5} /> : <ChevronRight size={16} strokeWidth={1.5} />}
</button>

{isOpen && (
<>
<div className={styles.searchWrap}>
<Search size={14} strokeWidth={1.5} className={styles.searchIcon} />
<input
className={styles.searchInput}
type="text"
placeholder="Search blocks..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>

<div className={styles.blockList}>
{CATEGORY_ORDER.map(cat => {
const catBlocks = filtered.filter(b => b.category === cat.id);
if (catBlocks.length === 0) return null;
return (
<div key={cat.id}>
<div className={styles.categoryHeader}>{cat.label}</div>
{catBlocks.map(block => {
const Icon = getIcon(block.icon);
return (
<div
key={block.id}
className={styles.blockItem}
draggable
onDragStart={(e) => onDragStart(e, block)}
>
<div className={styles.blockIcon}>
<Icon size={16} strokeWidth={1.5} />
</div>
<div className={styles.blockInfo}>
<div className={styles.blockName}>{block.name}</div>
<div className={styles.blockEndpoint}>
<MethodBadge method={block.method} />
<span>{block.endpoint}</span>
</div>
</div>
</div>
);
})}
</div>
);
})}
</div>
</>
)}

{!isOpen && (
<div className={styles.collapsedIcons}>
{CATEGORY_ORDER.map(cat => {
const catBlocks = allBlocks.filter(b => b.category === cat.id);
if (catBlocks.length === 0) return null;
const FirstIcon = getIcon(catBlocks[0].icon);
return (
<div key={cat.id} className={styles.collapsedIcon} title={cat.label}>
<FirstIcon size={16} strokeWidth={1.5} />
</div>
);
})}
</div>
)}
</div>
);
});
/* src/components/builder/Sidebar.module.css */
.sidebar {
position: relative;
background: var(--builder-bg-surface);
border-right: 1px solid var(--builder-border-subtle);
display: flex;
flex-direction: column;
transition: width 0.25s ease;
overflow: hidden;
z-index: 10;
}

.open { width: 230px; }
.collapsed { width: 48px; }

.toggleBtn {
position: absolute;
top: 8px;
right: 8px;
background: var(--builder-bg-card);
border: 1px solid var(--builder-border-subtle);
border-radius: 4px;
color: var(--builder-text-muted);
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}

.toggleBtn:hover { color: var(--builder-text-primary); }

.searchWrap {
padding: 8px 10px;
padding-top: 36px;
position: relative;
}

.searchIcon {
position: absolute;
left: 18px;
top: 45px;
color: var(--builder-text-dim);
}

.searchInput {
width: 100%;
padding: 6px 8px 6px 28px;
border-radius: 6px;
border: 1px solid var(--builder-border-subtle);
background: var(--builder-bg-deep);
color: var(--builder-text-primary);
font-size: 11px;
font-family: inherit;
outline: none;
}

.searchInput:focus { border-color: var(--builder-accent); }
.searchInput::placeholder { color: var(--builder-text-dim); }

.blockList {
flex: 1;
overflow-y: auto;
padding: 4px 10px 10px;
}

.categoryHeader {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
color: var(--builder-text-dim);
letter-spacing: 0.5px;
padding: 8px 0 4px;
}

.blockItem {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
cursor: grab;
transition: background 0.15s;
}

.blockItem:hover { background: var(--builder-accent-bg); }
.blockItem:active { cursor: grabbing; }

.blockIcon {
width: 26px;
height: 26px;
border-radius: 6px;
background: linear-gradient(135deg, var(--builder-accent-bg), var(--builder-accent-dark));
display: flex;
align-items: center;
justify-content: center;
color: var(--builder-accent);
flex-shrink: 0;
}

.blockInfo { flex: 1; min-width: 0; }

.blockName {
font-size: 11px;
font-weight: 500;
color: var(--builder-text-primary);
}

.blockEndpoint {
display: flex;
align-items: center;
gap: 4px;
font-size: 9px;
color: var(--builder-text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.collapsedIcons {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 40px;
gap: 12px;
}

.collapsedIcon {
color: var(--builder-text-muted);
cursor: pointer;
}

.collapsedIcon:hover { color: var(--builder-accent); }
  • Step 2: Commit
git add src/components/builder/Sidebar.tsx src/components/builder/Sidebar.module.css
git commit -m "feat: add collapsible Sidebar with drag-and-drop blocks"

Task 4.6: TopBar

Files:

  • Create: src/components/builder/TopBar.tsx

  • Create: src/components/builder/TopBar.module.css

  • Step 1: Create TopBar

// src/components/builder/TopBar.tsx
import React, { memo, useCallback } from 'react';
import { Sun, Moon, Sparkles, Trash2, Undo2, Redo2 } from 'lucide-react';
import { useBuilderStore } from '../../store/useBuilderStore';
import { useTheme } from '../../hooks/useTheme';
import { useFlowValidation } from '../../hooks/useFlowValidation';
import { FlowEngine } from '../../engine/FlowEngine';
import { CodeGenerator } from '../../engine/CodeGenerator';
import { MarkdownGenerator } from '../../engine/MarkdownGenerator';
import { registry } from '../../registry';
import type { Environment, GeneratedFlow } from '../../types/builder';
import styles from './TopBar.module.css';

const engine = new FlowEngine(registry);
const codeGen = new CodeGenerator();
const mdGen = new MarkdownGenerator();

export const TopBar = memo(function TopBar() {
const nodes = useBuilderStore(s => s.nodes);
const edges = useBuilderStore(s => s.edges);
const environment = useBuilderStore(s => s.environment);
const setEnvironment = useBuilderStore(s => s.actions.setEnvironment);
const toggleReport = useBuilderStore(s => s.actions.toggleReport);
const setGeneratedFlow = useBuilderStore(s => s.actions.setGeneratedFlow);
const clearCanvas = useBuilderStore(s => s.actions.clearCanvas);
const undo = useBuilderStore(s => s.actions.undo);
const redo = useBuilderStore(s => s.actions.redo);

const { theme, toggle: toggleTheme } = useTheme();
const { canGenerate, validationErrors } = useFlowValidation();

const handleGenerate = useCallback(() => {
if (!canGenerate) return;
const steps = engine.generateSteps(nodes, edges, environment);
const flow: GeneratedFlow = {
steps,
environment,
markdown: mdGen.generate(steps),
code: {
curl: codeGen.generate(steps, 'curl'),
node: codeGen.generate(steps, 'node'),
python: codeGen.generate(steps, 'python'),
go: codeGen.generate(steps, 'go'),
},
};
setGeneratedFlow(flow);
toggleReport(true);
}, [canGenerate, nodes, edges, environment, setGeneratedFlow, toggleReport]);

const warningCount = validationErrors.filter(e => e.severity === 'warning').length;

return (
<div className={styles.topBar}>
<div className={styles.left}>
<span className={styles.logo}>Integration Builder</span>
<span className={styles.version}>v2</span>
</div>

<div className={styles.center}>
<button className={styles.iconBtn} onClick={undo} title="Undo (Ctrl+Z)">
<Undo2 size={16} strokeWidth={1.5} />
</button>
<button className={styles.iconBtn} onClick={redo} title="Redo (Ctrl+Shift+Z)">
<Redo2 size={16} strokeWidth={1.5} />
</button>
<button className={styles.iconBtn} onClick={clearCanvas} title="Clear All">
<Trash2 size={16} strokeWidth={1.5} />
</button>

<div className={styles.divider} />

<select
className={styles.envSelect}
value={environment}
onChange={(e) => setEnvironment(e.target.value as Environment)}
>
<option value="sandbox">Sandbox</option>
<option value="production">Production</option>
<option value="developer">Developer</option>
</select>
</div>

<div className={styles.right}>
{nodes.length > 0 && (
<span className={styles.stats}>
{nodes.length} block{nodes.length !== 1 ? 's' : ''}
{warningCount > 0 && ` · ${warningCount} warning${warningCount !== 1 ? 's' : ''}`}
</span>
)}

<button className={styles.iconBtn} onClick={toggleTheme} title="Toggle theme">
{theme === 'dark' ? <Sun size={16} strokeWidth={1.5} /> : <Moon size={16} strokeWidth={1.5} />}
</button>

<button
className={`${styles.generateBtn} ${canGenerate ? styles.generateEnabled : styles.generateDisabled}`}
onClick={handleGenerate}
disabled={!canGenerate}
title={canGenerate ? 'Generate Flow (Ctrl+G)' : 'Add blocks and fix errors first'}
>
<Sparkles size={14} strokeWidth={1.5} />
Generate Flow
</button>
</div>
</div>
);
});
/* src/components/builder/TopBar.module.css */
.topBar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background: var(--builder-bg-surface);
border-bottom: 1px solid var(--builder-border-subtle);
height: 48px;
flex-shrink: 0;
z-index: 10;
}

.left {
display: flex;
align-items: center;
gap: 6px;
}

.logo {
font-size: 13px;
font-weight: 600;
color: var(--builder-text-primary);
}

.version {
font-size: 10px;
color: var(--builder-accent);
background: var(--builder-accent-bg);
padding: 1px 5px;
border-radius: 4px;
}

.center {
display: flex;
align-items: center;
gap: 4px;
}

.iconBtn {
background: none;
border: 1px solid transparent;
border-radius: 4px;
color: var(--builder-text-muted);
cursor: pointer;
padding: 4px 6px;
display: flex;
align-items: center;
transition: all 0.15s;
}

.iconBtn:hover {
color: var(--builder-text-primary);
background: var(--builder-accent-bg);
border-color: var(--builder-border-subtle);
}

.divider {
width: 1px;
height: 20px;
background: var(--builder-border-subtle);
margin: 0 4px;
}

.envSelect {
padding: 4px 8px;
border-radius: 4px;
border: 1px solid var(--builder-border-subtle);
background: var(--builder-bg-deep);
color: var(--builder-text-primary);
font-size: 11px;
font-family: inherit;
cursor: pointer;
}

.right {
display: flex;
align-items: center;
gap: 8px;
}

.stats {
font-size: 11px;
color: var(--builder-text-muted);
}

.generateBtn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
border: none;
transition: all 0.2s;
}

.generateEnabled {
background: linear-gradient(135deg, var(--builder-accent-dark), var(--builder-accent));
color: white;
box-shadow: 0 2px 12px rgba(167, 139, 250, 0.3);
}

.generateEnabled:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(167, 139, 250, 0.4);
}

.generateDisabled {
background: var(--builder-bg-card);
color: var(--builder-text-dim);
cursor: not-allowed;
}
  • Step 2: Commit
git add src/components/builder/TopBar.tsx src/components/builder/TopBar.module.css
git commit -m "feat: add TopBar with env selector, theme toggle, generate flow button"

Task 4.7: Canvas

Files:

  • Create: src/components/builder/Canvas.tsx

  • Create: src/components/builder/Canvas.module.css

  • Step 1: Create Canvas

// src/components/builder/Canvas.tsx
import React, { memo, useCallback, DragEvent } from 'react';
import {
ReactFlow,
Background,
Controls,
MiniMap,
BackgroundVariant,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useBuilderStore } from '../../store/useBuilderStore';
import { BuilderNode } from './nodes/BaseNode';
import { AnimatedEdge } from './edges/AnimatedEdge';
import styles from './Canvas.module.css';

const nodeTypes = { builderNode: BuilderNode };
const edgeTypes = { animated: AnimatedEdge };

export const Canvas = memo(function Canvas() {
const nodes = useBuilderStore(s => s.nodes);
const edges = useBuilderStore(s => s.edges);
const onNodesChange = useBuilderStore(s => s.actions.onNodesChange);
const onEdgesChange = useBuilderStore(s => s.actions.onEdgesChange);
const onConnect = useBuilderStore(s => s.actions.onConnect);
const addNode = useBuilderStore(s => s.actions.addNode);
const selectNode = useBuilderStore(s => s.actions.selectNode);

const onDragOver = useCallback((e: DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}, []);

const onDrop = useCallback((e: DragEvent) => {
e.preventDefault();
const blockId = e.dataTransfer.getData('application/builder-block');
if (!blockId) return;

const reactFlowBounds = (e.target as HTMLElement).closest('.react-flow')?.getBoundingClientRect();
if (!reactFlowBounds) return;

const position = {
x: e.clientX - reactFlowBounds.left,
y: e.clientY - reactFlowBounds.top,
};

addNode(blockId, position);
}, [addNode]);

const onNodeClick = useCallback((_: React.MouseEvent, node: { id: string }) => {
selectNode(node.id);
}, [selectNode]);

const onPaneClick = useCallback(() => {
selectNode(null);
}, [selectNode]);

const defaultEdgeOptions = {
type: 'animated',
animated: true,
};

return (
<div className={styles.canvas} onDragOver={onDragOver} onDrop={onDrop}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
defaultEdgeOptions={defaultEdgeOptions}
snapToGrid
snapGrid={[24, 24]}
fitView
deleteKeyCode={['Delete', 'Backspace']}
>
<Background
variant={BackgroundVariant.Dots}
gap={24}
size={1}
color="var(--builder-border-subtle)"
/>
<Controls
position="bottom-right"
style={{ background: 'var(--builder-bg-surface)', borderColor: 'var(--builder-border-subtle)' }}
/>
<MiniMap
position="bottom-left"
style={{ background: 'var(--builder-bg-surface)' }}
nodeColor="var(--builder-accent)"
maskColor="rgba(6, 6, 7, 0.7)"
/>
</ReactFlow>

{nodes.length === 0 && (
<div className={styles.emptyState}>
<p className={styles.emptyTitle}>Drag blocks from the sidebar to get started</p>
<p className={styles.emptySubtitle}>Connect them to build your integration flow</p>
</div>
)}
</div>
);
});
/* src/components/builder/Canvas.module.css */
.canvas {
flex: 1;
position: relative;
}

.canvas :global(.react-flow__node) {
cursor: pointer;
}

.emptyState {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
}

.emptyTitle {
font-size: 16px;
color: var(--builder-text-secondary);
margin-bottom: 4px;
}

.emptySubtitle {
font-size: 13px;
color: var(--builder-text-muted);
}
  • Step 2: Commit
git add src/components/builder/Canvas.tsx src/components/builder/Canvas.module.css
git commit -m "feat: add React Flow Canvas with drag-drop, minimap, snap-to-grid"

Task 4.8: Report Modal

Files:

  • Create: src/components/builder/report/ReportModal.tsx

  • Create: src/components/builder/report/ReportModal.module.css

  • Create: src/components/builder/report/CodeTab.tsx

  • Create: src/components/builder/report/CodeTab.module.css

  • Create: src/components/builder/report/ExportActions.tsx

  • Create: src/components/builder/report/ExportActions.module.css

  • Step 1: Create CodeTab

// src/components/builder/report/CodeTab.tsx
import React, { memo, useState, useCallback } from 'react';
import { Copy, Check } from 'lucide-react';
import type { CodeLanguage } from '../../../types/builder';
import styles from './CodeTab.module.css';

interface CodeTabProps {
code: Record<CodeLanguage, string>;
}

const TABS: { id: CodeLanguage; label: string }[] = [
{ id: 'curl', label: 'cURL' },
{ id: 'node', label: 'Node.js' },
{ id: 'python', label: 'Python' },
{ id: 'go', label: 'Go' },
];

export const CodeTab = memo(function CodeTab({ code }: CodeTabProps) {
const [active, setActive] = useState<CodeLanguage>('curl');
const [copied, setCopied] = useState(false);

const handleCopy = useCallback(() => {
navigator.clipboard.writeText(code[active]);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [code, active]);

return (
<div className={styles.container}>
<div className={styles.tabs}>
{TABS.map(tab => (
<button
key={tab.id}
className={`${styles.tab} ${active === tab.id ? styles.activeTab : ''}`}
onClick={() => setActive(tab.id)}
>
{tab.label}
</button>
))}
<button className={styles.copyBtn} onClick={handleCopy}>
{copied ? <Check size={14} /> : <Copy size={14} />}
{copied ? 'Copied' : 'Copy'}
</button>
</div>
<pre className={styles.codeBlock}>
<code>{code[active]}</code>
</pre>
</div>
);
});
/* src/components/builder/report/CodeTab.module.css */
.container { display: flex; flex-direction: column; }

.tabs {
display: flex;
align-items: center;
gap: 2px;
padding: 4px;
background: var(--builder-bg-deep);
border-radius: 6px 6px 0 0;
}

.tab {
padding: 4px 12px;
border: none;
background: transparent;
color: var(--builder-text-muted);
font-size: 11px;
font-family: inherit;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}

.tab:hover { color: var(--builder-text-primary); }
.activeTab { background: var(--builder-accent-bg); color: var(--builder-accent); }

.copyBtn {
margin-left: auto;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border: 1px solid var(--builder-border-subtle);
background: transparent;
color: var(--builder-text-muted);
font-size: 11px;
font-family: inherit;
cursor: pointer;
border-radius: 4px;
}

.copyBtn:hover { color: var(--builder-accent); border-color: var(--builder-accent); }

.codeBlock {
background: var(--builder-bg-deep);
border-radius: 0 0 6px 6px;
padding: 12px 16px;
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
color: var(--builder-text-secondary);
margin: 0;
max-height: 400px;
overflow-y: auto;
}
  • Step 2: Create ExportActions
// src/components/builder/report/ExportActions.tsx
import React, { memo, useCallback } from 'react';
import { Download, Image, FileText } from 'lucide-react';
import { useCanvasExport } from '../../../hooks/useCanvasExport';
import styles from './ExportActions.module.css';

interface ExportActionsProps {
markdown: string;
}

export const ExportActions = memo(function ExportActions({ markdown }: ExportActionsProps) {
const { exportPng, exportSvg } = useCanvasExport();

const downloadMarkdown = useCallback(() => {
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = 'integration-flow.md';
link.href = url;
link.click();
URL.revokeObjectURL(url);
}, [markdown]);

return (
<div className={styles.actions}>
<button className={styles.btn} onClick={downloadMarkdown}>
<FileText size={14} strokeWidth={1.5} /> Download .md
</button>
<button className={styles.btn} onClick={exportSvg}>
<Image size={14} strokeWidth={1.5} /> Export SVG
</button>
<button className={styles.btn} onClick={exportPng}>
<Download size={14} strokeWidth={1.5} /> Export PNG
</button>
</div>
);
});
/* src/components/builder/report/ExportActions.module.css */
.actions { display: flex; gap: 8px; flex-wrap: wrap; }

.btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--builder-border);
background: var(--builder-bg-card);
color: var(--builder-text-primary);
font-size: 12px;
font-family: inherit;
cursor: pointer;
border-radius: 6px;
transition: all 0.15s;
}

.btn:hover { border-color: var(--builder-accent); color: var(--builder-accent); }
  • Step 3: Create ReportModal
// src/components/builder/report/ReportModal.tsx
import React, { memo, useEffect, useState, useCallback } from 'react';
import { X } from 'lucide-react';
import { useBuilderStore } from '../../../store/useBuilderStore';
import { CodeTab } from './CodeTab';
import { ExportActions } from './ExportActions';
import { MethodBadge } from '../shared/MethodBadge';
import { GuideLink } from '../shared/GuideLink';
import styles from './ReportModal.module.css';

type ReportTab = 'preview' | 'markdown' | 'code';

export const ReportModal = memo(function ReportModal() {
const open = useBuilderStore(s => s.reportOpen);
const flow = useBuilderStore(s => s.generatedFlow);
const toggleReport = useBuilderStore(s => s.actions.toggleReport);
const [tab, setTab] = useState<ReportTab>('preview');

useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape' && open) toggleReport(false);
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [open, toggleReport]);

const handleClose = useCallback(() => toggleReport(false), [toggleReport]);

if (!open || !flow) return null;

return (
<div className={styles.backdrop} onClick={handleClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.header}>
<h2 className={styles.title}>Integration Flow Report</h2>
<div className={styles.headerTabs}>
{(['preview', 'markdown', 'code'] as ReportTab[]).map(t => (
<button
key={t}
className={`${styles.headerTab} ${tab === t ? styles.activeTab : ''}`}
onClick={() => setTab(t)}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
<button className={styles.closeBtn} onClick={handleClose}>
<X size={18} strokeWidth={1.5} />
</button>
</div>

<div className={styles.body}>
{tab === 'preview' && (
<div className={styles.preview}>
{flow.steps.map(step => (
<div key={step.nodeId} className={styles.stepCard}>
<div className={styles.stepHeader}>
<span className={styles.stepNumber}>{step.order}</span>
<span className={styles.stepName}>{step.name}</span>
<MethodBadge method={step.method} />
</div>
<div className={styles.stepEndpoint}>{step.endpoint}</div>
<p className={styles.stepDesc}>{step.description}</p>
{step.body && Object.keys(step.body).length > 0 && (
<pre className={styles.stepCode}>{JSON.stringify(step.body, null, 2)}</pre>
)}
<GuideLink href={step.guideLink} />
</div>
))}
</div>
)}

{tab === 'markdown' && (
<pre className={styles.markdownRaw}>{flow.markdown}</pre>
)}

{tab === 'code' && (
<CodeTab code={flow.code} />
)}
</div>

<div className={styles.footer}>
<ExportActions markdown={flow.markdown} />
</div>
</div>
</div>
);
});
/* src/components/builder/report/ReportModal.module.css */
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
animation: fadeIn 0.2s ease;
}

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

.modal {
background: var(--builder-bg-surface);
border: 1px solid var(--builder-border);
border-radius: 12px;
width: 90%;
max-width: 800px;
max-height: 85vh;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease-out;
}

@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

.header {
display: flex;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--builder-border-subtle);
gap: 12px;
}

.title { font-size: 14px; font-weight: 600; color: var(--builder-text-primary); margin: 0; }

.headerTabs { display: flex; gap: 2px; margin-left: auto; }

.headerTab {
padding: 4px 10px;
border: none;
background: transparent;
color: var(--builder-text-muted);
font-size: 11px;
font-family: inherit;
cursor: pointer;
border-radius: 4px;
}

.headerTab:hover { color: var(--builder-text-primary); }
.activeTab { background: var(--builder-accent-bg); color: var(--builder-accent); }

.closeBtn {
background: none;
border: none;
color: var(--builder-text-muted);
cursor: pointer;
padding: 4px;
margin-left: 8px;
}

.closeBtn:hover { color: var(--builder-text-primary); }

.body {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}

.preview { display: flex; flex-direction: column; gap: 12px; }

.stepCard {
background: var(--builder-bg-card);
border: 1px solid var(--builder-border-subtle);
border-radius: 8px;
padding: 12px 16px;
}

.stepHeader { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }

.stepNumber {
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, var(--builder-accent-dark), var(--builder-accent));
color: white;
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}

.stepName { font-size: 13px; font-weight: 600; color: var(--builder-text-primary); }
.stepEndpoint { font-size: 11px; color: var(--builder-text-muted); margin-bottom: 6px; font-family: monospace; }
.stepDesc { font-size: 12px; color: var(--builder-text-secondary); margin: 4px 0 8px; }

.stepCode {
background: var(--builder-bg-deep);
border-radius: 4px;
padding: 8px 12px;
font-size: 11px;
color: var(--builder-text-secondary);
overflow-x: auto;
margin: 6px 0;
}

.markdownRaw {
background: var(--builder-bg-deep);
border-radius: 6px;
padding: 12px 16px;
font-size: 12px;
color: var(--builder-text-secondary);
overflow: auto;
white-space: pre-wrap;
margin: 0;
}

.footer {
padding: 12px 20px;
border-top: 1px solid var(--builder-border-subtle);
}
  • Step 4: Commit
git add src/components/builder/report/
git commit -m "feat: add ReportModal with preview, markdown, and code tabs"

Task 4.9: FlowValidator Display

Files:

  • Create: src/components/builder/validation/FlowValidator.tsx

  • Create: src/components/builder/validation/FlowValidator.module.css

  • Step 1: Create FlowValidator

// src/components/builder/validation/FlowValidator.tsx
import React, { memo } from 'react';
import { AlertTriangle, AlertCircle, Info } from 'lucide-react';
import { useFlowValidation } from '../../../hooks/useFlowValidation';
import styles from './FlowValidator.module.css';

export const FlowValidator = memo(function FlowValidator() {
const { validationErrors } = useFlowValidation();

if (validationErrors.length === 0) return null;

const errors = validationErrors.filter(e => e.severity === 'error');
const warnings = validationErrors.filter(e => e.severity === 'warning');

return (
<div className={styles.container}>
{errors.length > 0 && (
<div className={styles.errorBadge}>
<AlertCircle size={12} strokeWidth={1.5} />
{errors.length} error{errors.length !== 1 ? 's' : ''}
</div>
)}
{warnings.length > 0 && (
<div className={styles.warningBadge}>
<AlertTriangle size={12} strokeWidth={1.5} />
{warnings.length} warning{warnings.length !== 1 ? 's' : ''}
</div>
)}
</div>
);
});
/* src/components/builder/validation/FlowValidator.module.css */
.container {
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 6px;
z-index: 5;
}

.errorBadge,
.warningBadge {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
}

.errorBadge {
background: rgba(239, 68, 68, 0.15);
color: #EF4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}

.warningBadge {
background: rgba(251, 191, 36, 0.15);
color: var(--builder-warning);
border: 1px solid rgba(251, 191, 36, 0.3);
}
  • Step 2: Commit
git add src/components/builder/validation/
git commit -m "feat: add FlowValidator overlay with error/warning badges"

Phase 5: Page Assembly and Integration

Task 5.1: Integration Builder Page

Files:

  • Modify: src/pages/integration-builder.tsx (full rewrite)

  • Modify: src/pages/integration-builder.module.css (full rewrite)

  • Step 1: Rewrite the page shell

The new page uses BrowserOnly to wrap the React Flow canvas (client-only), provides the builder layout.

// src/pages/integration-builder.tsx
import React, { useEffect } from 'react';
import BrowserOnly from '@docusaurus/BrowserOnly';
import Layout from '@theme/Layout';
import { useBuilderStore } from '../store/useBuilderStore';

function BuilderApp() {
// Lazy imports to avoid SSR issues with React Flow
const [Components, setComponents] = React.useState<{
Sidebar: React.ComponentType;
TopBar: React.ComponentType;
Canvas: React.ComponentType;
ReportModal: React.ComponentType;
FlowValidator: React.ComponentType;
} | null>(null);

useEffect(() => {
Promise.all([
import('../components/builder/Sidebar'),
import('../components/builder/TopBar'),
import('../components/builder/Canvas'),
import('../components/builder/report/ReportModal'),
import('../components/builder/validation/FlowValidator'),
]).then(([sidebar, topbar, canvas, report, validator]) => {
setComponents({
Sidebar: sidebar.Sidebar,
TopBar: topbar.TopBar,
Canvas: canvas.Canvas,
ReportModal: report.ReportModal,
FlowValidator: validator.FlowValidator,
});
});
}, []);

const theme = useBuilderStore(s => s.theme);

useEffect(() => {
document.documentElement.setAttribute('data-builder-theme', theme);
}, [theme]);

// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const store = useBuilderStore.getState();

if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault();
store.actions.undo();
}
if (e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
e.preventDefault();
store.actions.redo();
}
if (e.key === 'g' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
// Trigger generate (will be handled by TopBar logic)
}
if (e.key === 'Escape') {
store.actions.selectNode(null);
store.actions.toggleReport(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);

if (!Components) {
return <div style={{ display: 'flex', height: '100vh', alignItems: 'center', justifyContent: 'center', background: '#060607', color: '#E0DCD8' }}>Loading builder...</div>;
}

const { Sidebar, TopBar, Canvas, ReportModal, FlowValidator } = Components;

return (
<div style={{ display: 'flex', height: '100vh', width: '100vw', overflow: 'hidden', background: 'var(--builder-bg-deep)' }}>
<Sidebar />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<TopBar />
<div style={{ flex: 1, position: 'relative' }}>
<Canvas />
<FlowValidator />
</div>
</div>
<ReportModal />
</div>
);
}

export default function IntegrationBuilderPage(): React.JSX.Element {
return (
<Layout
title="Integration Builder"
description="Visual no-code integration flow builder"
noFooter
>
<BrowserOnly fallback={<div>Loading...</div>}>
{() => <BuilderApp />}
</BrowserOnly>
</Layout>
);
}
  • Step 2: Rewrite the CSS module
/* src/pages/integration-builder.module.css */
/* Minimal — most styles are in component CSS modules */
/* This file kept for potential page-level overrides */
  • Step 3: Verify build compiles
cd ~/Projects/front/integration-guide && npm run build 2>&1 | tail -20

Expected: Build succeeds (or shows only CSS/asset warnings, not TS errors).

  • Step 4: Commit
git add src/pages/integration-builder.tsx src/pages/integration-builder.module.css
git commit -m "feat: rewrite integration-builder page with React Flow canvas"

Task 5.2: Full Build Verification

  • Step 1: Run all tests
cd ~/Projects/front/integration-guide && npm test

Expected: All engine tests pass.

  • Step 2: Run typecheck
cd ~/Projects/front/integration-guide && npx tsc --noEmit

Expected: No type errors.

  • Step 3: Run build
cd ~/Projects/front/integration-guide && npm run build

Expected: Build succeeds.

  • Step 4: Fix any issues and commit fixes

If any errors, fix them and commit:

git add -u
git commit -m "fix: resolve build/type errors in integration builder v2"

Phase 6: Polish and Final Verification

Task 6.1: Keyboard Shortcuts Integration

Verify all keyboard shortcuts work:

  • Delete/Backspace removes selected node
  • Ctrl+Z undoes
  • Ctrl+Shift+Z redoes
  • Escape deselects and closes modal
  • Space for pan mode (built into React Flow)

Task 6.2: Final Test Suite Run

  • Step 1: Run complete test suite
cd ~/Projects/front/integration-guide && npm test
  • Step 2: Run full build
cd ~/Projects/front/integration-guide && npm run build
  • Step 3: Final commit
git add -A
git commit -m "feat: Integration Builder v2 complete — React Flow canvas builder"

File Index

FileResponsibility
src/types/builder.tsAll TypeScript types
src/configs/blocks/*.jsonOne file per API block
src/configs/currencies/*.jsonOne file per currency
src/configs/chains/*.jsonOne file per chain
src/configs/payment-methods/*.jsonOne file per payment method
src/registry/BlockRegistry.tsLoads and queries all configs
src/engine/FlowEngine.tsValidation + topological sort
src/engine/CodeGenerator.tscURL/Node/Python/Go output
src/engine/MarkdownGenerator.tsMarkdown report output
src/engine/BindingResolver.tsAuto-bind output→input
src/store/useBuilderStore.tsZustand state + undo/redo
src/hooks/useFlowValidation.tsReal-time validation hook
src/hooks/useTheme.tsDark/light mode
src/hooks/useCanvasExport.tsSVG/PNG export
src/hooks/useAutoBinding.tsBinding resolution hook
src/components/builder/Canvas.tsxReact Flow wrapper
src/components/builder/Sidebar.tsxCollapsible block palette
src/components/builder/TopBar.tsxControls, env selector, generate
src/components/builder/nodes/BaseNode.tsxNode rendering (collapsed/expanded)
src/components/builder/edges/AnimatedEdge.tsxAnimated edge with particles
src/components/builder/config/NodeConfigPanel.tsxInline config form
src/components/builder/report/ReportModal.tsxReport overlay modal
src/components/builder/report/CodeTab.tsxCode language tabs
src/components/builder/report/ExportActions.tsxDownload/export buttons
src/components/builder/validation/FlowValidator.tsxValidation badges
src/components/builder/shared/MethodBadge.tsxHTTP method badge
src/components/builder/shared/GuideLink.tsxIntegration Guide link
src/components/builder/shared/BindingBadge.tsxBinding display
src/pages/integration-builder.tsxPage shell