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
| File | Responsibility |
|---|---|
src/types/builder.ts | All TypeScript types |
src/configs/blocks/*.json | One file per API block |
src/configs/currencies/*.json | One file per currency |
src/configs/chains/*.json | One file per chain |
src/configs/payment-methods/*.json | One file per payment method |
src/registry/BlockRegistry.ts | Loads and queries all configs |
src/engine/FlowEngine.ts | Validation + topological sort |
src/engine/CodeGenerator.ts | cURL/Node/Python/Go output |
src/engine/MarkdownGenerator.ts | Markdown report output |
src/engine/BindingResolver.ts | Auto-bind output→input |
src/store/useBuilderStore.ts | Zustand state + undo/redo |
src/hooks/useFlowValidation.ts | Real-time validation hook |
src/hooks/useTheme.ts | Dark/light mode |
src/hooks/useCanvasExport.ts | SVG/PNG export |
src/hooks/useAutoBinding.ts | Binding resolution hook |
src/components/builder/Canvas.tsx | React Flow wrapper |
src/components/builder/Sidebar.tsx | Collapsible block palette |
src/components/builder/TopBar.tsx | Controls, env selector, generate |
src/components/builder/nodes/BaseNode.tsx | Node rendering (collapsed/expanded) |
src/components/builder/edges/AnimatedEdge.tsx | Animated edge with particles |
src/components/builder/config/NodeConfigPanel.tsx | Inline config form |
src/components/builder/report/ReportModal.tsx | Report overlay modal |
src/components/builder/report/CodeTab.tsx | Code language tabs |
src/components/builder/report/ExportActions.tsx | Download/export buttons |
src/components/builder/validation/FlowValidator.tsx | Validation badges |
src/components/builder/shared/MethodBadge.tsx | HTTP method badge |
src/components/builder/shared/GuideLink.tsx | Integration Guide link |
src/components/builder/shared/BindingBadge.tsx | Binding display |
src/pages/integration-builder.tsx | Page shell |