1655 lines
39 KiB
Markdown
1655 lines
39 KiB
Markdown
# 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](#design-system-foundation)
|
||
2. [Registration Components](#registration-components)
|
||
3. [Login Components](#login-components)
|
||
4. [SSO Configuration Components](#sso-configuration-components)
|
||
5. [MCP Token Components](#mcp-token-components)
|
||
6. [Shared Components](#shared-components)
|
||
7. [Component Props Reference](#component-props-reference)
|
||
|
||
---
|
||
|
||
## Design System Foundation
|
||
|
||
### Color System
|
||
|
||
Based on ColaFlow's existing design system:
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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)
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
interface LoginDividerProps {
|
||
text?: string; // Default: "OR"
|
||
className?: string;
|
||
}
|
||
```
|
||
|
||
**Implementation:**
|
||
|
||
```tsx
|
||
<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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
interface DomainListInputProps {
|
||
domains: string[];
|
||
onChange: (domains: string[]) => void;
|
||
placeholder?: string;
|
||
helperText?: string;
|
||
}
|
||
|
||
interface ValidationResult {
|
||
isValid: boolean;
|
||
error?: string;
|
||
}
|
||
```
|
||
|
||
**Validation:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```tsx
|
||
<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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
const resources = ['projects', 'issues', 'documents', 'reports', 'sprints', 'comments'];
|
||
const operations = ['read', 'create', 'update', 'delete', 'search'];
|
||
```
|
||
|
||
**Restrictions (Business Rules):**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
interface TokenDisplayModalProps {
|
||
token: string;
|
||
tokenName: string;
|
||
onClose: () => void;
|
||
open: boolean;
|
||
}
|
||
|
||
interface TokenDisplayState {
|
||
copied: boolean;
|
||
downloaded: boolean;
|
||
acknowledged: boolean;
|
||
}
|
||
```
|
||
|
||
**Token Display Box:**
|
||
|
||
```tsx
|
||
<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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```tsx
|
||
<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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:**
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
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:
|
||
|
||
```typescript
|
||
interface FormFieldProps extends BaseComponentProps {
|
||
label?: string;
|
||
helperText?: string;
|
||
error?: string;
|
||
required?: boolean;
|
||
disabled?: boolean;
|
||
autoFocus?: boolean;
|
||
}
|
||
```
|
||
|
||
### Button Props
|
||
|
||
```typescript
|
||
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
|
||
|
||
```typescript
|
||
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
|