Files
ColaFlow/docs/design/ui-component-specs.md
Yaojia Wang fe8ad1c1f9
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
In progress
2025-11-03 11:51:02 +01:00

39 KiB
Raw Blame History

UI Component Specifications

Document Overview

This document provides detailed specifications for all UI components required for multi-tenant, SSO, and MCP Token management features in ColaFlow.

Version: 1.0 Last Updated: 2025-11-03 Tech Stack: Next.js 16 + React 19 + shadcn/ui + Tailwind CSS 4 Owner: UX/UI Team


Table of Contents

  1. Design System Foundation
  2. Registration Components
  3. Login Components
  4. SSO Configuration Components
  5. MCP Token Components
  6. Shared Components
  7. Component Props Reference

Design System Foundation

Color System

Based on ColaFlow's existing design system:

// tailwind.config.ts
const colors = {
  primary: {
    50: '#e3f2fd',
    100: '#bbdefb',
    200: '#90caf9',
    300: '#64b5f6',
    400: '#42a5f5',
    500: '#2196F3', // Main brand color
    600: '#1e88e5',
    700: '#1976D2', // Darker shade
    800: '#1565c0',
    900: '#0d47a1',
  },
  secondary: {
    success: '#4CAF50',
    warning: '#FF9800',
    error: '#F44336',
    info: '#2196F3',
  },
  priority: {
    urgent: '#F44336',
    high: '#FF9800',
    medium: '#2196F3',
    low: '#9E9E9E',
  },
  status: {
    active: '#4CAF50',
    suspended: '#FF9800',
    cancelled: '#F44336',
    pending: '#FFC107',
  },
  gray: {
    50: '#fafafa',
    100: '#f5f5f5',
    200: '#eeeeee',
    300: '#e0e0e0',
    400: '#bdbdbd',
    500: '#9e9e9e',
    600: '#757575',
    700: '#616161',
    800: '#424242',
    900: '#212121',
  },
}

Typography Scale

// Font families
const fontFamily = {
  sans: ['Inter', 'Roboto', 'PingFang SC', 'Microsoft YaHei', 'sans-serif'],
  mono: ['JetBrains Mono', 'Consolas', 'monospace'],
}

// Font sizes
const fontSize = {
  'xs': ['12px', { lineHeight: '16px' }],
  'sm': ['14px', { lineHeight: '20px' }],
  'base': ['16px', { lineHeight: '24px' }],
  'lg': ['18px', { lineHeight: '28px' }],
  'xl': ['20px', { lineHeight: '28px' }],
  '2xl': ['24px', { lineHeight: '32px' }],
  '3xl': ['30px', { lineHeight: '36px' }],
  '4xl': ['36px', { lineHeight: '40px' }],
}

// Font weights
const fontWeight = {
  normal: '400',
  medium: '500',
  semibold: '600',
  bold: '700',
}

Spacing System (8px base)

const spacing = {
  0: '0px',
  1: '4px',    // 0.5 * 8
  2: '8px',    // 1 * 8
  3: '12px',   // 1.5 * 8
  4: '16px',   // 2 * 8
  5: '20px',   // 2.5 * 8
  6: '24px',   // 3 * 8
  8: '32px',   // 4 * 8
  10: '40px',  // 5 * 8
  12: '48px',  // 6 * 8
  16: '64px',  // 8 * 8
  20: '80px',  // 10 * 8
  24: '96px',  // 12 * 8
}

Shadows

const boxShadow = {
  sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
  DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
  md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
  lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
  xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
  '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
}

Border Radius

const borderRadius = {
  none: '0',
  sm: '4px',
  DEFAULT: '6px',
  md: '8px',
  lg: '12px',
  xl: '16px',
  '2xl': '24px',
  full: '9999px',
}

Registration Components

1. TenantSlugInput

Purpose: Input field with real-time availability check and subdomain preview.

Visual:

Company Slug *                              []
[acme                          ] [⟳]
✓ acme.colaflow.com is available

Component Structure:

interface TenantSlugInputProps {
  value: string;
  onChange: (value: string) => void;
  onValidationChange: (isValid: boolean) => void;
  disabled?: boolean;
}

interface ValidationState {
  status: 'idle' | 'checking' | 'valid' | 'invalid' | 'error';
  message?: string;
  suggestions?: string[];
}

States:

  1. Idle: No input yet

    • Border: gray-300
    • No icon
  2. Checking: Validating in real-time

    • Border: primary-500
    • Icon: Spinner (animated)
    • Message: "Checking availability..."
  3. Valid: Slug is available

    • Border: green-500
    • Icon: Checkmark (green)
    • Message: "✓ acme.colaflow.com is available"
  4. Invalid: Slug is taken or has errors

    • Border: red-500
    • Icon: X (red)
    • Message: "This URL is already taken. Try: acme-corp, acme-team"
    • Show suggestion chips (clickable)
  5. Error: Network or validation error

    • Border: red-500
    • Icon: Alert (red)
    • Message: "Unable to check availability. Please try again."

Behavior:

  • Debounce input: 500ms
  • Auto-lowercase
  • Strip invalid characters
  • Show preview domain below input
  • Validate format: ^[a-z0-9]+(?:-[a-z0-9]+)*$
  • API call: GET /api/tenants/check-slug?slug=acme

Accessibility:

  • aria-label: "Company URL slug"
  • aria-describedby: Points to preview text
  • aria-invalid: true when status is invalid
  • aria-live: "polite" for validation messages

2. PasswordStrengthInput

Purpose: Password input with strength indicator and requirements checklist.

Visual:

Password *
[******************] [👁️]

Strength: [███████░░░] Medium

Requirements:
☑ At least 8 characters
☑ One uppercase letter (A-Z)
☑ One lowercase letter (a-z)
☐ One number (0-9)
☐ One special character (!@#$%^&*)

Component Structure:

interface PasswordStrengthInputProps {
  value: string;
  onChange: (value: string) => void;
  showRequirements?: boolean;
  minStrength?: 'weak' | 'medium' | 'strong';
}

interface PasswordStrength {
  level: 'weak' | 'medium' | 'strong';
  score: number; // 0-100
  requirements: PasswordRequirement[];
}

interface PasswordRequirement {
  label: string;
  met: boolean;
  regex: RegExp;
}

Strength Calculation:

const calculateStrength = (password: string): PasswordStrength => {
  let score = 0;
  const requirements: PasswordRequirement[] = [
    { label: 'At least 8 characters', met: password.length >= 8, regex: /.{8,}/ },
    { label: 'One uppercase letter (A-Z)', met: /[A-Z]/.test(password), regex: /[A-Z]/ },
    { label: 'One lowercase letter (a-z)', met: /[a-z]/.test(password), regex: /[a-z]/ },
    { label: 'One number (0-9)', met: /\d/.test(password), regex: /\d/ },
    { label: 'One special character (!@#$%^&*)', met: /[!@#$%^&*]/.test(password), regex: /[!@#$%^&*]/ },
  ];

  requirements.forEach(req => {
    if (req.met) score += 20;
  });

  let level: 'weak' | 'medium' | 'strong' = 'weak';
  if (score >= 80) level = 'strong';
  else if (score >= 60) level = 'medium';

  return { level, score, requirements };
};

Strength Bar Colors:

  • Weak (0-40%): bg-red-500
  • Medium (40-80%): bg-yellow-500
  • Strong (80-100%): bg-green-500

Behavior:

  • Show/hide toggle button
  • Update strength on every keystroke
  • Prevent paste (optional, for security)
  • Animated strength bar transition

Accessibility:

  • aria-label: "Password, show requirements below"
  • aria-describedby: Points to requirements list
  • Toggle button: aria-label "Show password" / "Hide password"
  • Requirements list: role="list"

3. SubscriptionPlanCard

Purpose: Selectable subscription plan card with feature comparison.

Visual:

┌─────────────────────────────────┐
│  MOST POPULAR                   │  <- Badge (optional)
├─────────────────────────────────┤
│  Professional                   │
│  $49 /month                     │
│                                 │
│  Everything in Starter, plus:   │
│  ✓ 50 users                     │
│  ✓ 100 projects                 │
│  ✓ 100 GB storage               │
│  ✓ SSO integration              │
│  ✓ Priority support             │
│  ✓ Advanced analytics           │
│                                 │
│  [Start with Professional]      │
│                                 │
│  14-day free trial              │
└─────────────────────────────────┘

Component Structure:

interface SubscriptionPlanCardProps {
  plan: SubscriptionPlan;
  selected: boolean;
  onSelect: (plan: SubscriptionPlan) => void;
  showBadge?: boolean;
  badgeText?: string;
}

interface SubscriptionPlan {
  id: string;
  name: string;
  price: number;
  billingPeriod: 'month' | 'year';
  features: PlanFeature[];
  limits: PlanLimits;
}

interface PlanFeature {
  label: string;
  included: boolean;
  tooltip?: string;
}

interface PlanLimits {
  users: number | 'unlimited';
  projects: number | 'unlimited';
  storageGB: number;
  mcpTokens: number | 'unlimited';
}

States:

  1. Unselected:

    • Border: gray-300
    • Background: white
    • Shadow: sm
  2. Hover:

    • Border: primary-500
    • Shadow: md
    • Transform: scale(1.02)
    • Transition: 200ms ease-out
  3. Selected:

    • Border: primary-500 (2px)
    • Background: primary-50
    • Shadow: lg
    • Checkmark icon in top-right corner

Badge (optional):

  • Position: absolute top-0 right-0
  • Background: primary-500
  • Text: white, text-xs, uppercase
  • Padding: 4px 12px
  • Border-radius: bottom-left only

Button:

  • Full width
  • Size: large
  • Variant: selected ? primary : secondary

Accessibility:

  • role: "radio"
  • aria-checked: selected
  • Keyboard: Space/Enter to select
  • Focus ring: 2px offset

Login Components

4. SsoButton

Purpose: Branded SSO login button for different identity providers.

Visual Examples:

Azure AD:
[🟦 Continue with Microsoft]

Google:
[🔴 Continue with Google]

Okta:
[🔵 Continue with Okta]

Generic SAML:
[🔐 Continue with SSO]

Component Structure:

interface SsoButtonProps {
  provider: 'AzureAD' | 'Google' | 'Okta' | 'GenericSaml';
  onClick: () => void;
  loading?: boolean;
  disabled?: boolean;
  fullWidth?: boolean;
}

interface ProviderConfig {
  name: string;
  icon: React.ReactNode;
  bgColor: string;
  textColor: string;
  hoverBgColor: string;
}

Provider Configurations:

const providerConfigs: Record<string, ProviderConfig> = {
  AzureAD: {
    name: 'Microsoft',
    icon: <MicrosoftIcon />,
    bgColor: '#00A4EF', // Microsoft blue
    textColor: '#FFFFFF',
    hoverBgColor: '#0078D4',
  },
  Google: {
    name: 'Google',
    icon: <GoogleIcon />,
    bgColor: '#FFFFFF',
    textColor: '#3C4043',
    hoverBgColor: '#F8F9FA',
  },
  Okta: {
    name: 'Okta',
    icon: <OktaIcon />,
    bgColor: '#007DC1',
    textColor: '#FFFFFF',
    hoverBgColor: '#005F96',
  },
  GenericSaml: {
    name: 'SSO',
    icon: <LockIcon />,
    bgColor: '#6B7280',
    textColor: '#FFFFFF',
    hoverBgColor: '#4B5563',
  },
};

States:

  1. Default:

    • Provider-specific colors
    • Border: none
    • Height: 48px (large)
    • Padding: 12px 24px
    • Font: medium weight
  2. Hover:

    • Background: hoverBgColor
    • Shadow: md
    • Cursor: pointer
  3. Loading:

    • Spinner animation (white for dark bg, gray for light bg)
    • Text: "Signing in..."
    • Disabled cursor
  4. Disabled:

    • Opacity: 50%
    • Cursor: not-allowed

Layout:

[Icon] Continue with {Provider}
  • Icon: 20x20px, margin-right 12px
  • Text: align center, font-size 16px

Accessibility:

  • aria-label: "Sign in with {provider}"
  • role: "button"
  • Keyboard: Enter/Space to activate

5. LoginDivider

Purpose: Visual separator between SSO and local login options.

Visual:

──────────── OR ────────────

Component Structure:

interface LoginDividerProps {
  text?: string; // Default: "OR"
  className?: string;
}

Implementation:

<div className="relative my-6">
  <div className="absolute inset-0 flex items-center">
    <div className="w-full border-t border-gray-300"></div>
  </div>
  <div className="relative flex justify-center text-sm">
    <span className="px-4 bg-white text-gray-500 uppercase tracking-wider">
      {text || 'OR'}
    </span>
  </div>
</div>

SSO Configuration Components

6. SsoProviderSelect

Purpose: Dropdown to select SSO provider type.

Visual:

SSO Provider *
[Azure AD / Microsoft Entra    ▼]

Options:
  - Azure AD / Microsoft Entra
  - Google Workspace
  - Okta
  - Generic SAML 2.0

Component Structure:

interface SsoProviderSelectProps {
  value: SsoProvider;
  onChange: (provider: SsoProvider) => void;
  disabled?: boolean;
}

type SsoProvider = 'AzureAD' | 'Google' | 'Okta' | 'GenericSaml';

interface ProviderOption {
  value: SsoProvider;
  label: string;
  description: string;
  icon: React.ReactNode;
  docsLink: string;
}

Provider Options:

const providerOptions: ProviderOption[] = [
  {
    value: 'AzureAD',
    label: 'Azure AD / Microsoft Entra',
    description: 'For Microsoft 365 and Azure Active Directory',
    icon: <MicrosoftIcon />,
    docsLink: '/docs/sso/azure-ad',
  },
  {
    value: 'Google',
    label: 'Google Workspace',
    description: 'For Google Workspace (formerly G Suite)',
    icon: <GoogleIcon />,
    docsLink: '/docs/sso/google',
  },
  {
    value: 'Okta',
    label: 'Okta',
    description: 'For Okta Identity Cloud',
    icon: <OktaIcon />,
    docsLink: '/docs/sso/okta',
  },
  {
    value: 'GenericSaml',
    label: 'Generic SAML 2.0',
    description: 'For any SAML 2.0 compliant IdP',
    icon: <ShieldIcon />,
    docsLink: '/docs/sso/saml',
  },
];

Dropdown Item Layout:

[Icon]  Provider Name
        Description
  • Icon: 24x24px
  • Name: font-medium, text-gray-900
  • Description: text-sm, text-gray-500

Behavior:

  • Show help icon with link to docs
  • Highlight selected option
  • Keyboard navigation (arrow keys)

7. DomainListInput

Purpose: Add/remove allowed email domains for SSO.

Visual:

Allowed Email Domains (Optional)

[@acme.com              ] [+ Add Domain]

Added Domains:
┌──────────────────────────────────┐
│ @acme.com                   [🗑️] │
│ @acme.co.uk                 [🗑️] │
│ @acme-corp.com              [🗑️] │
└──────────────────────────────────┘

 Only users with these email domains can log in via SSO.
   Leave empty to allow all domains.

Component Structure:

interface DomainListInputProps {
  domains: string[];
  onChange: (domains: string[]) => void;
  placeholder?: string;
  helperText?: string;
}

interface ValidationResult {
  isValid: boolean;
  error?: string;
}

Validation:

const validateDomain = (domain: string): ValidationResult => {
  // Remove @ if present
  const cleanDomain = domain.replace('@', '').trim().toLowerCase();

  // Check format: valid domain
  const domainRegex = /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$/;

  if (!cleanDomain) {
    return { isValid: false, error: 'Domain cannot be empty' };
  }

  if (!domainRegex.test(cleanDomain)) {
    return { isValid: false, error: 'Invalid domain format' };
  }

  return { isValid: true };
};

Behavior:

  • Input always shows "@" prefix
  • Add domain on Enter or button click
  • Prevent duplicate domains
  • Show validation error below input
  • Animate domain chips on add/remove

Domain Chip:

  • Background: gray-100
  • Border: gray-300
  • Padding: 6px 12px
  • Border-radius: full
  • Delete icon: hover shows red

Accessibility:

  • Input: aria-label "Add email domain"
  • Chip list: role="list"
  • Delete buttons: aria-label "Remove {domain}"

8. SsoTestButton

Purpose: Test SSO configuration before saving.

Visual:

[Test Connection]

Test Results:
┌─────────────────────────────────────────┐
│ ✓ Metadata endpoint reachable           │
│ ✓ OIDC discovery document valid         │
│ ⏳ Testing authentication...             │
└─────────────────────────────────────────┘

Component Structure:

interface SsoTestButtonProps {
  onTest: () => Promise<SsoTestResult>;
  disabled?: boolean;
}

interface SsoTestResult {
  success: boolean;
  checks: TestCheck[];
  error?: string;
}

interface TestCheck {
  label: string;
  status: 'pending' | 'success' | 'error';
  message?: string;
  duration?: number; // ms
}

Test Checks:

  1. Metadata Endpoint Reachable

    • Request: GET metadata URL
    • Success: 200 status
    • Time: <500ms
  2. OIDC Discovery Valid (OIDC only)

    • Parse JSON
    • Validate required fields
    • Time: <100ms
  3. Certificate Valid (SAML only)

    • Parse X.509 certificate
    • Check expiration
    • Time: <100ms
  4. Callback URL Whitelisted

    • Attempt authorization request
    • Check for redirect_uri_mismatch
    • Time: <1000ms

States:

  1. Idle:

    • Button: secondary variant
    • Text: "Test Connection"
    • Icon: none
  2. Testing:

    • Button: loading spinner
    • Text: "Testing..."
    • Show progress: checks updating in real-time
  3. Success:

    • Button: disabled
    • Results: all checks green
    • Final message: "All checks passed ✓"
  4. Failure:

    • Button: enabled (can retry)
    • Results: failed checks red
    • Show error details
    • Link to troubleshooting docs

Results Display:

<div className="mt-4 space-y-2">
  {checks.map((check, idx) => (
    <div key={idx} className="flex items-center gap-2">
      {check.status === 'pending' && <Spinner className="h-4 w-4" />}
      {check.status === 'success' && <CheckIcon className="h-4 w-4 text-green-500" />}
      {check.status === 'error' && <XIcon className="h-4 w-4 text-red-500" />}
      <span className={check.status === 'error' ? 'text-red-700' : 'text-gray-700'}>
        {check.label}
      </span>
      {check.duration && (
        <span className="text-xs text-gray-500">({check.duration}ms)</span>
      )}
    </div>
  ))}
</div>

MCP Token Components

9. McpTokenCard

Purpose: Display token information in list view.

Visual:

┌──────────────────────────────────────────────────┐
│ Claude Desktop                          [Active] │
│ Permissions: Projects (R), Issues (R/W)          │
│ Created: 2025-11-01 • Last Used: 2 hours ago     │
│ Expires: In 30 days                              │
│                                                  │
│ [View Details]  [Revoke]                         │
└──────────────────────────────────────────────────┘

Component Structure:

interface McpTokenCardProps {
  token: McpToken;
  onViewDetails: (tokenId: string) => void;
  onRevoke: (tokenId: string) => void;
}

interface McpToken {
  id: string;
  name: string;
  permissions: TokenPermissions;
  createdAt: string;
  lastUsedAt?: string;
  expiresAt?: string;
  status: TokenStatus;
  usageCount: number;
}

type TokenStatus = 'Active' | 'Expired' | 'Revoked';

interface TokenPermissions {
  [resource: string]: string[]; // e.g., { projects: ['read'], issues: ['read', 'create'] }
}

Status Badge:

const statusConfig: Record<TokenStatus, { color: string; text: string }> = {
  Active: { color: 'green', text: 'Active' },
  Expired: { color: 'gray', text: 'Expired' },
  Revoked: { color: 'red', text: 'Revoked' },
};

Permissions Display:

Format: Resource (Operations)

  • R = Read
  • C = Create
  • U = Update
  • D = Delete
  • S = Search

Example: Projects (R), Issues (R/C/U), Documents (R/C)

Truncate if more than 3 resources, show "+X more"

Time Display:

const formatRelativeTime = (date: string): string => {
  const now = new Date();
  const then = new Date(date);
  const diffMs = now.getTime() - then.getTime();

  const seconds = Math.floor(diffMs / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (minutes < 1) return 'Just now';
  if (minutes < 60) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
  if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
  if (days < 30) return `${days} day${days > 1 ? 's' : ''} ago`;

  return then.toLocaleDateString();
};

Hover State:

  • Border: primary-500
  • Shadow: md
  • Background: gray-50

Actions:

  • "View Details" button: secondary variant
  • "Revoke" button: danger text button

10. PermissionMatrix

Purpose: Interactive matrix for selecting token permissions.

Visual:

┌─────────────────────────────────────────────────┐
│ Resource   │ Read │Create│Update│Delete│Search │
│────────────┼──────┼──────┼──────┼──────┼────── │
│ Projects   │  ✓   │      │      │      │   ✓   │
│ Issues     │  ✓   │  ✓   │  ✓   │      │   ✓   │
│ Documents  │  ✓   │  ✓   │      │      │   ✓   │
│ Reports    │  ✓   │      │      │      │       │
│ Sprints    │  ✓   │      │      │      │   ✓   │
│ Comments   │  ✓   │  ✓   │      │      │       │
└─────────────────────────────────────────────────┘

[Select All] [Clear All] [Templates ▼]

Component Structure:

interface PermissionMatrixProps {
  value: TokenPermissions;
  onChange: (permissions: TokenPermissions) => void;
  restrictions?: PermissionRestrictions;
}

interface PermissionRestrictions {
  [resource: string]: {
    allowedOperations: string[];
    disabledMessage?: string;
  };
}

interface PermissionCell {
  resource: string;
  operation: string;
  enabled: boolean;
  disabled?: boolean;
  disabledReason?: string;
}

Resources and Operations:

const resources = ['projects', 'issues', 'documents', 'reports', 'sprints', 'comments'];
const operations = ['read', 'create', 'update', 'delete', 'search'];

Restrictions (Business Rules):

const defaultRestrictions: PermissionRestrictions = {
  issues: {
    allowedOperations: ['read', 'create', 'update', 'search'], // No delete
    disabledMessage: 'Deleting issues is not allowed for safety',
  },
  users: {
    allowedOperations: [], // No user management via MCP
    disabledMessage: 'User management is not available via MCP tokens',
  },
};

Behavior:

  1. Click checkbox: Toggle permission
  2. Click row header: Select all operations for that resource
  3. Click column header: Select that operation for all resources
  4. Disabled cells: Show tooltip with reason
  5. Templates: Quick presets (Read Only, Read+Write, Custom)

Templates:

const templates = {
  'Read Only': {
    projects: ['read', 'search'],
    issues: ['read', 'search'],
    documents: ['read', 'search'],
    reports: ['read'],
  },
  'Read + Write': {
    projects: ['read', 'search'],
    issues: ['read', 'create', 'update', 'search'],
    documents: ['read', 'create', 'search'],
    reports: ['read'],
  },
};

Visual States:

  • Enabled checkbox:

    • Unchecked: border-gray-300
    • Checked: bg-primary-500, checkmark visible
  • Disabled checkbox:

    • Unchecked: border-gray-200, bg-gray-100
    • Tooltip on hover

Accessibility:

  • Table: role="table"
  • Row headers: scope="row"
  • Column headers: scope="col"
  • Checkboxes: aria-label "{operation} permission for {resource}"
  • Disabled cells: aria-disabled="true"

11. TokenDisplayModal

Purpose: One-time display of generated MCP token with copy functionality.

Visual:

┌────────────────────────────────────────────────┐
│ ✓ Token Created Successfully!           [× ] │
├────────────────────────────────────────────────┤
│                                                │
│ ⚠️ IMPORTANT: Save this token now!             │
│                                                │
│ This is the ONLY time you'll see this token.   │
│ If you lose it, you'll need to generate a new  │
│ one.                                           │
│                                                │
│ Your Token:                                    │
│ ┌──────────────────────────────────────────┐   │
│ │ mcp_acme_7f3d8a9c4e1b2f5a6d8c9e0f1a2b3  │   │
│ └──────────────────────────────────────────┘   │
│ [📋 Copy to Clipboard]                         │
│                                                │
│ [Download as .env file]                        │
│                                                │
│ [☐] I have saved this token securely           │
│                                                │
│ [Close]                                        │
└────────────────────────────────────────────────┘

Component Structure:

interface TokenDisplayModalProps {
  token: string;
  tokenName: string;
  onClose: () => void;
  open: boolean;
}

interface TokenDisplayState {
  copied: boolean;
  downloaded: boolean;
  acknowledged: boolean;
}

Token Display Box:

<div className="bg-gray-50 rounded-lg border border-gray-300 p-4 my-4">
  <code className="text-sm font-mono break-all select-all">
    {token}
  </code>
</div>

Copy Button Behavior:

const handleCopy = async () => {
  await navigator.clipboard.writeText(token);
  setCopied(true);
  toast.success('Token copied to clipboard!');

  // Reset after 3 seconds
  setTimeout(() => setCopied(false), 3000);
};

Download .env File:

const handleDownload = () => {
  const content = `# ColaFlow MCP Token
# Token Name: ${tokenName}
# Created: ${new Date().toISOString()}
# Expires: ${expiresAt || 'Never'}

COLAFLOW_MCP_TOKEN=${token}
`;

  const blob = new Blob([content], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'colaflow-mcp-token.env';
  link.click();
  URL.revokeObjectURL(url);

  setDownloaded(true);
};

Close Button State:

  • Disabled until checkbox is checked
  • Shows tooltip: "Please confirm you've saved the token"

Warning on Close (if not acknowledged):

Are you sure you want to close?

You won't be able to see this token again.

[Go Back]  [Close Anyway]

12. AuditLogTable

Purpose: Display MCP token usage audit log.

Visual:

┌──────────────────────────────────────────────────────────┐
│ Timestamp        Action   Resource    Result    IP       │
│──────────────────────────────────────────────────────────│
│ Nov 3, 08:15 AM  Read     Issue #123  Success  192.168… │
│ Nov 3, 08:10 AM  Create   Issue #456  Success  192.168… │
│ Nov 3, 08:05 AM  Update   Issue #456  Success  192.168… │
│ Nov 3, 08:00 AM  Read     Project #1  Success  192.168… │
│ Nov 3, 07:55 AM  Delete   Issue #789  Failed   192.168… │
└──────────────────────────────────────────────────────────┘

Showing 1-50 of 1,247 operations
[Load More]

Component Structure:

interface AuditLogTableProps {
  logs: AuditLog[];
  loading?: boolean;
  onLoadMore?: () => void;
  hasMore?: boolean;
}

interface AuditLog {
  id: string;
  timestamp: string;
  action: string; // read, create, update, delete, search
  resource: string; // Issue #123, Project #1
  result: 'Success' | 'Failed';
  ipAddress: string;
  userAgent?: string;
  errorMessage?: string;
  durationMs: number;
}

Result Badge:

const resultConfig = {
  Success: { color: 'green', icon: CheckCircleIcon },
  Failed: { color: 'red', icon: XCircleIcon },
};

Row Hover:

  • Background: gray-50
  • Cursor: pointer
  • Show details on click (expandable row)

Expandable Row Details:

┌──────────────────────────────────────────────────────┐
│ Request Details                                      │
│ Method: POST /api/mcp/issues                         │
│ Duration: 234ms                                      │
│ User Agent: Claude Desktop 1.2.3                     │
│ IP Address: 192.168.1.100                            │
│                                                      │
│ Error: Permission denied                             │
│ Message: Token does not have delete permission for   │
│          issues.                                     │
└──────────────────────────────────────────────────────┘

Pagination:

  • Load more button (not traditional pagination)
  • Infinite scroll (optional)
  • Show "X of Y operations" count

Filters (future):

  • Action type
  • Result (success/failed)
  • Date range
  • Resource type

Shared Components

13. ProgressIndicator

Purpose: Show progress through multi-step flows.

Visual:

● ○ ○     Step 1 of 3
✓ ● ○     Step 2 of 3
✓ ✓ ●     Step 3 of 3

Component Structure:

interface ProgressIndicatorProps {
  steps: number;
  currentStep: number;
  labels?: string[];
  variant?: 'dots' | 'line' | 'numbers';
}

Variants:

  1. Dots (default):

    • Completed: filled circle with checkmark
    • Current: filled circle (primary color)
    • Upcoming: outlined circle (gray)
    • Size: 32px
    • Gap: 16px
  2. Line:

    • Horizontal line connecting steps
    • Progress fills from left to right
  3. Numbers:

    • Numbered steps instead of dots
    • Better for >5 steps

Implementation:

<div className="flex items-center justify-center gap-4">
  {Array.from({ length: steps }, (_, i) => {
    const stepNumber = i + 1;
    const isCompleted = stepNumber < currentStep;
    const isCurrent = stepNumber === currentStep;

    return (
      <div key={i} className="flex items-center gap-2">
        <div className={cn(
          "w-8 h-8 rounded-full flex items-center justify-center",
          isCompleted && "bg-green-500 text-white",
          isCurrent && "bg-primary-500 text-white",
          !isCompleted && !isCurrent && "border-2 border-gray-300 text-gray-500"
        )}>
          {isCompleted ? <CheckIcon /> : stepNumber}
        </div>
        {i < steps - 1 && (
          <div className="w-12 h-0.5 bg-gray-300" />
        )}
      </div>
    );
  })}
</div>

14. InfoTooltip

Purpose: Contextual help icon with tooltip.

Visual:

Company Slug * []
              ↑
         Hover shows tooltip

Component Structure:

interface InfoTooltipProps {
  content: React.ReactNode | string;
  placement?: 'top' | 'right' | 'bottom' | 'left';
  maxWidth?: number;
}

Icon:

  • Component: Information Circle Icon
  • Size: 16px
  • Color: gray-400
  • Hover: gray-600

Tooltip:

  • Background: gray-900
  • Text: white, text-sm
  • Padding: 8px 12px
  • Border-radius: 6px
  • Max-width: 200px (default)
  • Arrow: pointing to icon
  • Animation: fade-in 200ms
  • Delay: 300ms (on hover)

Accessibility:

  • Icon button: role="button", aria-label="Show help"
  • Tooltip: role="tooltip"
  • Keyboard: Focus with Tab, show tooltip on Enter

15. EmptyState

Purpose: Placeholder when no data exists.

Visual:

┌────────────────────────────────────┐
│          [Icon/Illustration]       │
│                                    │
│         No tokens yet              │
│                                    │
│  Generate your first MCP token to  │
│  connect AI agents to ColaFlow.    │
│                                    │
│  [Generate Token]                  │
└────────────────────────────────────┘

Component Structure:

interface EmptyStateProps {
  icon?: React.ReactNode;
  title: string;
  description: string;
  action?: {
    label: string;
    onClick: () => void;
  };
  secondaryAction?: {
    label: string;
    href: string;
  };
}

Layout:

  • Center-aligned
  • Icon: 64x64px, gray-400
  • Title: text-xl, font-semibold, gray-900
  • Description: text-sm, gray-600, max-w-md
  • Spacing: 16px between elements
  • Action button: primary, large

Common Variations:

  1. No MCP Tokens:

    • Icon: Robot icon
    • Action: "Generate Token"
  2. No SSO Config:

    • Icon: Shield icon
    • Action: "Configure SSO"
  3. No Users:

    • Icon: Users icon
    • Action: "Invite Team"

16. ConfirmDialog

Purpose: Confirmation modal for destructive actions.

Visual:

┌────────────────────────────────────┐
│ Revoke Token?                      │
├────────────────────────────────────┤
│                                    │
│ Are you sure you want to revoke    │
│ this token?                        │
│                                    │
│ Token: Claude Desktop              │
│ Last used: 2 hours ago             │
│                                    │
│ This will:                         │
│ • Immediately stop all API access  │
│ • Cannot be undone                 │
│ • Require generating a new token   │
│                                    │
│ [Cancel]  [Revoke Token]           │
└────────────────────────────────────┘

Component Structure:

interface ConfirmDialogProps {
  open: boolean;
  title: string;
  description: string;
  details?: React.ReactNode;
  consequences?: string[];
  confirmLabel?: string;
  cancelLabel?: string;
  variant?: 'danger' | 'warning' | 'info';
  onConfirm: () => void | Promise<void>;
  onCancel: () => void;
  loading?: boolean;
}

Variants:

  1. Danger (red):

    • Confirm button: bg-red-500
    • Icon: Alert triangle (red)
    • Used for: delete, revoke, suspend
  2. Warning (yellow):

    • Confirm button: bg-yellow-500
    • Icon: Warning icon (yellow)
    • Used for: archival, downgrade
  3. Info (blue):

    • Confirm button: bg-primary-500
    • Icon: Info icon (blue)
    • Used for: non-destructive confirmations

Behavior:

  • Backdrop: semi-transparent black
  • Click outside: calls onCancel
  • Escape key: calls onCancel
  • Enter key: calls onConfirm (if confirm button focused)
  • Loading state: disable buttons, show spinner

Animation:

  • Enter: fade-in 200ms + scale from 0.95
  • Exit: fade-out 200ms + scale to 0.95

Component Props Reference

Common Props

All components accept these base props:

interface BaseComponentProps {
  className?: string;
  style?: React.CSSProperties;
  testId?: string; // for e2e testing
  'aria-label'?: string;
  'aria-describedby'?: string;
}

Form Field Props

All form fields accept:

interface FormFieldProps extends BaseComponentProps {
  label?: string;
  helperText?: string;
  error?: string;
  required?: boolean;
  disabled?: boolean;
  autoFocus?: boolean;
}

Button Props

interface ButtonProps extends BaseComponentProps {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  disabled?: boolean;
  fullWidth?: boolean;
  icon?: React.ReactNode;
  iconPosition?: 'left' | 'right';
  onClick?: () => void;
  type?: 'button' | 'submit' | 'reset';
}

Modal Props

interface ModalProps extends BaseComponentProps {
  open: boolean;
  onClose: () => void;
  title?: string;
  description?: string;
  size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
  closeOnBackdrop?: boolean;
  closeOnEscape?: boolean;
  showCloseButton?: boolean;
  footer?: React.ReactNode;
}

Implementation Guidelines

Component File Structure

src/components/
├── registration/
│   ├── TenantSlugInput.tsx
│   ├── PasswordStrengthInput.tsx
│   └── SubscriptionPlanCard.tsx
├── auth/
│   ├── SsoButton.tsx
│   ├── LoginDivider.tsx
│   └── LoginForm.tsx
├── sso/
│   ├── SsoProviderSelect.tsx
│   ├── DomainListInput.tsx
│   └── SsoTestButton.tsx
├── mcp-tokens/
│   ├── McpTokenCard.tsx
│   ├── PermissionMatrix.tsx
│   ├── TokenDisplayModal.tsx
│   └── AuditLogTable.tsx
└── shared/
    ├── ProgressIndicator.tsx
    ├── InfoTooltip.tsx
    ├── EmptyState.tsx
    └── ConfirmDialog.tsx

Naming Conventions

  • Components: PascalCase (e.g., McpTokenCard)
  • Files: Match component name (e.g., McpTokenCard.tsx)
  • Props interfaces: {ComponentName}Props
  • CSS classes: kebab-case with BEM (if needed)
  • Test files: {ComponentName}.test.tsx
  • Story files: {ComponentName}.stories.tsx (Storybook)

State Management

  • Local state: Use useState for component-specific state
  • Form state: Use react-hook-form for complex forms
  • Server state: Use TanStack Query for API data
  • Global state: Use Zustand for app-wide state

Testing Requirements

Each component must have:

  1. Unit tests: Component rendering and interaction
  2. Accessibility tests: ARIA, keyboard navigation
  3. Visual tests: Storybook stories for all variants
  4. Integration tests: E2E flows with Playwright

Conclusion

This component specification provides a complete blueprint for implementing the UI for multi-tenant, SSO, and MCP Token features. All components follow ColaFlow's design system and adhere to accessibility best practices.

Next Steps:

  1. Review with development team
  2. Create Storybook stories for each component
  3. Begin implementation in priority order:
    • Registration flow (M1)
    • Login + SSO (M1)
    • MCP Tokens (M2)
  4. Conduct accessibility audit
  5. User testing with prototypes

Questions: Contact UX/UI team at ux@colaflow.com