// ============================================================================ // OpenClaw Control Center — M3 Design Tokens // ============================================================================ // Single source of truth for all design tokens. // Components should @use this module and reference tokens via variables or // the theme() mixin rather than hardcoding values. // // Token structure: // 1. Color tokens — palette, semantic, status, surface // 2. Typography — families, sizes, weights, line-heights // 3. Spacing — 4px base grid, named steps // 4. Layout — dimensions, breakpoints, radii, shadows // 5. Motion — durations, easing curves // 6. Accessibility — focus, reduced-motion // ============================================================================ @use 'sass:map'; @use 'sass:meta'; // ============================================================================ // 1. COLOR TOKENS // ============================================================================ // --------------------------------------------------------------------------- // 1a. Primary Palette (M3 cyan-based) // --------------------------------------------------------------------------- $color-primary-50: #ecfeff; $color-primary-100: #cffafe; $color-primary-200: #a5f3fc; $color-primary-300: #67e8f9; $color-primary-400: #22d3ee; $color-primary-500: #38bdf8; // Brand primary $color-primary-600: #0ea5e9; $color-primary-700: #0284c7; $color-primary-800: #0369a1; $color-primary-900: #075985; // --------------------------------------------------------------------------- // 1b. Secondary Palette (M3 teal-based) // --------------------------------------------------------------------------- $color-secondary-50: #f0fdfa; $color-secondary-100: #ccfbf1; $color-secondary-200: #99f6e4; $color-secondary-300: #5eead4; $color-secondary-400: #2dd4bf; // Brand secondary $color-secondary-500: #14b8a6; $color-secondary-600: #0d9488; $color-secondary-700: #0f766e; $color-secondary-800: #115e59; $color-secondary-900: #134e4a; // --------------------------------------------------------------------------- // 1c. Accent / Tertiary Palette (M3 violet-based) // --------------------------------------------------------------------------- $color-accent-50: #f5f3ff; $color-accent-100: #ede9fe; $color-accent-200: #ddd6fe; $color-accent-300: #c4b5fd; $color-accent-400: #a78bfa; // Brand accent $color-accent-500: #8b5cf6; $color-accent-600: #7c3aed; $color-accent-700: #6d28d9; $color-accent-800: #5b21b6; $color-accent-900: #4c1d95; // --------------------------------------------------------------------------- // 1d. Danger / Error Palette // --------------------------------------------------------------------------- $color-danger-50: #fef2f2; $color-danger-100: #fee2e2; $color-danger-200: #fecaca; $color-danger-300: #fca5a5; $color-danger-400: #f87171; // Brand danger $color-danger-500: #ef4444; $color-danger-600: #dc2626; $color-danger-700: #b91c1c; $color-danger-800: #991b1b; $color-danger-900: #7f1d1d; // --------------------------------------------------------------------------- // 1e. Semantic Surface Tokens (Tactical Dark) // --------------------------------------------------------------------------- $color-surface-darkest: #0D0F12; // Page background $color-surface-dark: #13161A; // Card / container surface $color-surface-medium: #1C2027; // Container-elevated $color-surface-light: #252B33; // Container-high / hover $color-surface-lighter: #2D3748; // Border / divider zone $color-on-surface: #E2E8F0; // Primary text on dark surfaces $color-on-surface-variant: #8A9BB0; // Secondary / muted text $color-on-surface-muted: #64748B; // Disabled / hint text // --------------------------------------------------------------------------- // 1f. Status Colors (Semantic — outside M3 tonal system) // --------------------------------------------------------------------------- $status-active: #38bdf8; $status-idle: #2dd4bf; $status-thinking: #a78bfa; $status-error: #f87171; $status-offline: #64748b; // Status background tints (12% opacity for badges, pills, backgrounds) $status-active-bg: rgba(56, 189, 248, 0.12); $status-idle-bg: rgba(45, 212, 191, 0.12); $status-thinking-bg: rgba(167, 139, 250, 0.12); $status-error-bg: rgba(248, 113, 113, 0.12); $status-offline-bg: rgba(100, 116, 139, 0.12); // Status border colors (40% opacity) $status-active-border: rgba(56, 189, 248, 0.40); $status-idle-border: rgba(45, 212, 191, 0.40); $status-thinking-border: rgba(167, 139, 250, 0.40); $status-error-border: rgba(248, 113, 113, 0.40); $status-offline-border: rgba(100, 116, 139, 0.40); // Map for iteration $status-colors: ( 'active': ('fg': $status-active, 'bg': $status-active-bg, 'border': $status-active-border), 'idle': ('fg': $status-idle, 'bg': $status-idle-bg, 'border': $status-idle-border), 'thinking': ('fg': $status-thinking, 'bg': $status-thinking-bg, 'border': $status-thinking-border), 'error': ('fg': $status-error, 'bg': $status-error-bg, 'border': $status-error-border), 'offline': ('fg': $status-offline, 'bg': $status-offline-bg, 'border': $status-offline-border), ); // --------------------------------------------------------------------------- // 1g. Full color map for programmatic access // --------------------------------------------------------------------------- $colors: ( 'primary': $color-primary-500, 'secondary': $color-secondary-400, 'accent': $color-accent-400, 'danger': $color-danger-400, 'surface': $color-surface-dark, 'surface-light': $color-surface-light, 'on-surface': $color-on-surface, 'on-surface-variant': $color-on-surface-variant, 'border': $color-surface-lighter, ); // ============================================================================ // 2. TYPOGRAPHY TOKENS // ============================================================================ // --------------------------------------------------------------------------- // 2a. Font Families // --------------------------------------------------------------------------- $font-family-brand: 'Inter, Roboto, sans-serif'; $font-family-body: 'Inter, Roboto, sans-serif'; $font-family-mono: 'Roboto Mono, Cascadia Code, Fira Code, monospace'; // --------------------------------------------------------------------------- // 2b. Font Sizes (M3 type scale) // --------------------------------------------------------------------------- $font-size-display-large: 57px; $font-size-display-medium: 45px; $font-size-display-small: 36px; $font-size-headline-large: 32px; $font-size-headline-medium: 28px; $font-size-headline-small: 24px; $font-size-title-large: 22px; $font-size-title-medium: 16px; $font-size-title-small: 14px; $font-size-body-large: 16px; $font-size-body-medium: 14px; $font-size-body-small: 12px; $font-size-label-large: 14px; $font-size-label-medium: 12px; $font-size-label-small: 11px; // --------------------------------------------------------------------------- // 2c. Font Weights // --------------------------------------------------------------------------- $font-weight-regular: 400; $font-weight-medium: 500; $font-weight-bold: 600; $font-weight-heavy: 700; // --------------------------------------------------------------------------- // 2d. Line Heights // --------------------------------------------------------------------------- $line-height-tight: 1.2; $line-height-normal: 1.5; $line-height-relaxed: 1.6; // --------------------------------------------------------------------------- // 2e. Letter Spacing // --------------------------------------------------------------------------- $letter-spacing-tight: -0.01em; $letter-spacing-normal: 0em; $letter-spacing-wide: 0.02em; $letter-spacing-mono: 0.05em; // --------------------------------------------------------------------------- // 2f. Typography map // --------------------------------------------------------------------------- $typography: ( 'font-family-brand': $font-family-brand, 'font-family-body': $font-family-body, 'font-family-mono': $font-family-mono, 'size-display-large': $font-size-display-large, 'size-headline-medium': $font-size-headline-medium, 'size-title-large': $font-size-title-large, 'size-title-medium': $font-size-title-medium, 'size-body-large': $font-size-body-large, 'size-body-medium': $font-size-body-medium, 'size-body-small': $font-size-body-small, 'size-label-large': $font-size-label-large, 'size-label-medium': $font-size-label-medium, 'weight-regular': $font-weight-regular, 'weight-medium': $font-weight-medium, 'weight-bold': $font-weight-bold, ); // ============================================================================ // 3. SPACING TOKENS (4px grid) // ============================================================================ $spacing-0: 0px; $spacing-1: 4px; $spacing-2: 8px; $spacing-3: 12px; $spacing-4: 16px; $spacing-5: 20px; $spacing-6: 24px; $spacing-7: 28px; $spacing-8: 32px; $spacing-9: 36px; $spacing-10: 40px; $spacing-12: 48px; $spacing-14: 56px; $spacing-16: 64px; $spacing-20: 80px; // Named semantic spacing $spacing-unit: $spacing-2; // 8px — base grid unit $spacing-card-gap: $spacing-4; // 16px $spacing-card-pad: $spacing-5; // 20px $spacing-section: $spacing-6; // 24px // ============================================================================ // 4. LAYOUT TOKENS // ============================================================================ // --------------------------------------------------------------------------- // 4a. Dimensions // --------------------------------------------------------------------------- $nav-rail-collapsed-width: 72px; $nav-rail-expanded-width: 256px; $header-height: 64px; $bottom-nav-height: 80px; $card-border-radius: 16px; $card-min-width: 320px; $badge-height: 24px; $badge-border-radius: 12px; $status-dot-size: 10px; // --------------------------------------------------------------------------- // 4b. Breakpoints (M3 canonical) // --------------------------------------------------------------------------- $breakpoint-compact: 599px; // Mobile phone $breakpoint-medium: 767px; // Tablet portrait $breakpoint-expanded: 1023px; // Tablet landscape $breakpoint-large: 1439px; // Desktop // Named breakpoint map for @media mixins $breakpoints: ( 'compact': $breakpoint-compact, 'medium': $breakpoint-medium, 'expanded': $breakpoint-expanded, 'large': $breakpoint-large, ); // --------------------------------------------------------------------------- // 4c. Border Radius // --------------------------------------------------------------------------- $radius-none: 0px; $radius-xs: 4px; $radius-sm: 8px; $radius-md: 12px; $radius-lg: 16px; $radius-xl: 24px; $radius-full: 9999px; // --------------------------------------------------------------------------- // 4d. Shadows (M3 elevation) // --------------------------------------------------------------------------- $shadow-level-0: none; $shadow-level-1: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px -1px rgba(0, 0, 0, 0.3); $shadow-level-2: 0 2px 6px 0 rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.3); $shadow-level-3: 0 4px 12px 0 rgba(0, 0, 0, 0.3), 0 4px 8px -4px rgba(0, 0, 0, 0.3); $shadow-level-4: 0 8px 24px 0 rgba(0, 0, 0, 0.3), 0 8px 16px -8px rgba(0, 0, 0, 0.3); // ============================================================================ // 5. MOTION TOKENS // ============================================================================ $duration-instant: 0ms; // No animation (reduced motion fallback) $duration-fast: 100ms; $duration-short: 150ms; $duration-medium: 200ms; $duration-standard: 300ms; $duration-long: 500ms; $easing-standard: cubic-bezier(0.4, 0, 0.2, 1); // M3 standard $easing-decelerate: cubic-bezier(0, 0, 0.2, 1); // M3 decelerate (entering) $easing-accelerate: cubic-bezier(0.4, 0, 1, 1); // M3 accelerate (exiting) $easing-sharp: cubic-bezier(0.4, 0, 0.6, 1); // M3 sharp // ============================================================================ // 6. ACCESSIBILITY TOKENS // ============================================================================ $focus-ring-width: 2px; $focus-ring-offset: 2px; $focus-ring-color: $status-active; $focus-ring-style: solid; $min-touch-target: 48px; $min-body-font: 16px; // ============================================================================ // MIXINS // ============================================================================ // --------------------------------------------------------------------------- // Responsive breakpoint mixin // Usage: @include tokens.respond-to('expanded') { ... } // --------------------------------------------------------------------------- @mixin respond-to($breakpoint) { $value: map.get($breakpoints, $breakpoint); @if $value { @media (min-width: $value + 1) { @content; } } @else { @error "Unknown breakpoint: `#{$breakpoint}`. Valid: compact, medium, expanded, large"; } } // --------------------------------------------------------------------------- // Below-breakpoint mixin (max-width) // Usage: @include tokens.below('expanded') { ... } // --------------------------------------------------------------------------- @mixin below($breakpoint) { $value: map.get($breakpoints, $breakpoint); @if $value { @media (max-width: $value) { @content; } } @else { @error "Unknown breakpoint: `#{$breakpoint}`. Valid: compact, medium, expanded, large"; } } // --------------------------------------------------------------------------- // Focus ring mixin // --------------------------------------------------------------------------- @mixin focus-ring($color: $focus-ring-color) { &:focus-visible { outline: $focus-ring-width $focus-ring-style $color; outline-offset: $focus-ring-offset; } } // --------------------------------------------------------------------------- // Card surface mixin // --------------------------------------------------------------------------- @mixin card-surface { background-color: $color-surface-medium; border-radius: $card-border-radius; border: 1px solid $color-surface-lighter; } // --------------------------------------------------------------------------- // Mono text mixin // --------------------------------------------------------------------------- @mixin mono-text($size: $font-size-body-medium) { font-family: $font-family-mono; font-size: $size; letter-spacing: $letter-spacing-mono; } // --------------------------------------------------------------------------- // Status dot mixin // --------------------------------------------------------------------------- @mixin status-dot($status) { $colors: map.get($status-colors, $status); @if not $colors { @error "Unknown status: `#{$status}`. Valid: active, idle, thinking, error, offline"; } $fg: map.get($colors, 'fg'); width: $status-dot-size; height: $status-dot-size; border-radius: $radius-full; background-color: $fg; @if $status == 'active' { animation: pulse-active $duration-standard $easing-standard infinite; } @else if $status == 'thinking' { animation: pulse-thinking 3s $easing-standard infinite; } @else if $status == 'error' { animation: pulse-error $duration-fast $easing-standard infinite; } } // --------------------------------------------------------------------------- // Truncate text mixin (single line) // --------------------------------------------------------------------------- @mixin truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } // --------------------------------------------------------------------------- // Screen-reader-only mixin // --------------------------------------------------------------------------- @mixin sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } // --------------------------------------------------------------------------- // Touch target mixin — ensures minimum 48px touch area // --------------------------------------------------------------------------- @mixin touch-target($min-size: $min-touch-target) { min-width: $min-size; min-height: $min-size; } // ============================================================================ // CUB-20 / CUB-27 CONVENIENCE ALIASES // ============================================================================ // Short aliases used by agent-card, hub-page, and other components. // These map to the canonical M3 tokens above. $cc-background: $color-surface-darkest; $cc-surface: $color-surface-dark; $cc-surface-container: $color-surface-medium; $cc-surface-container-high: $color-surface-light; $cc-on-surface: $color-on-surface; $cc-on-surface-variant: $color-on-surface-variant; $cc-outline: $color-surface-lighter; $cc-font-mono: $font-family-mono; $cc-font-family: $font-family-brand; $cc-nav-rail-collapsed-width: $nav-rail-collapsed-width; $cc-nav-rail-expanded-width: $nav-rail-expanded-width; $cc-header-height: $header-height; $cc-bottom-nav-height: $bottom-nav-height; $cc-card-border-radius: $card-border-radius; $cc-card-min-width: $card-min-width; $cc-card-gap: $spacing-card-gap; $cc-card-padding: $spacing-card-pad; $cc-section-padding: $spacing-section; $cc-spacing-unit: $spacing-unit; $cc-breakpoint-mobile: $breakpoint-compact; $cc-breakpoint-tablet: $breakpoint-medium; $cc-breakpoint-desktop: $breakpoint-expanded; $cc-surface-darkest: $color-surface-darkest; $cc-surface-dark: $color-surface-dark; $cc-surface-medium: $color-surface-medium; $cc-surface-light: $color-surface-light; $cc-surface-lighter: $color-surface-lighter;