diff --git a/frontend/angular.json b/frontend/angular.json index 6d8bb04..8c5cf7c 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -53,7 +53,12 @@ ], "styles": [ "src/styles.scss" - ] + ], + "stylePreprocessorOptions": { + "includePaths": [ + "src" + ] + } }, "configurations": { "production": { diff --git a/frontend/src/app/design/index.ts b/frontend/src/app/design/index.ts new file mode 100644 index 0000000..3c94005 --- /dev/null +++ b/frontend/src/app/design/index.ts @@ -0,0 +1,11 @@ +// ============================================================================ +// OpenClaw Control Center — Design System Barrel Export +// ============================================================================ +// Import everything from '@app/design' for convenient access. +// +// Usage: +// import { CcTokens, CcThemeService, CcCssProps } from '@app/design'; +// ============================================================================ + +export * from './tokens'; +export * from './theme.service'; \ No newline at end of file diff --git a/frontend/src/app/design/theme.service.ts b/frontend/src/app/design/theme.service.ts new file mode 100644 index 0000000..3fe1ae3 --- /dev/null +++ b/frontend/src/app/design/theme.service.ts @@ -0,0 +1,151 @@ +// ============================================================================ +// OpenClaw Control Center — Theme Service +// ============================================================================ +// Angular service providing programmatic access to design tokens, theme +// mode switching (dark/light), and runtime CSS custom property manipulation. +// +// Usage: +// constructor(private theme: CcThemeService) {} +// +// // Read a token +// const primary = this.theme.getToken('--cc-color-primary'); +// +// // Set a token at runtime +// this.theme.setToken('--cc-color-primary', '#00ff00'); +// +// // Toggle theme +// this.theme.setMode('light'); +// ============================================================================ + +import { Injectable, signal, computed, effect } from '@angular/core'; +import { CcCssProps, getStatusColor, setCssToken, getCssToken } from './tokens'; + +// --------------------------------------------------------------------------- +// Theme Mode Types +// --------------------------------------------------------------------------- +export type ThemeMode = 'dark' | 'light'; + +// --------------------------------------------------------------------------- +// Light theme overrides (future use) +// --------------------------------------------------------------------------- +const LIGHT_THEME_OVERRIDES: Record = { + // Surface tokens + '--cc-surface-darkest': '#F8FAFC', + '--cc-surface-dark': '#FFFFFF', + '--cc-surface-medium': '#F1F5F9', + '--cc-surface-light': '#E2E8F0', + '--cc-surface-lighter': '#CBD5E1', + + // On-surface tokens + '--cc-on-surface': '#0F172A', + '--cc-on-surface-variant': '#475569', + '--cc-on-surface-muted': '#94A3B8', + + // Border + '--cc-surface-lighter-alt': '#E2E8F0', + + // M3 system overrides for light + '--mat-sys-surface': '#FFFFFF', + '--mat-sys-surface-container': '#F1F5F9', + '--mat-sys-surface-container-high': '#E2E8F0', + '--mat-sys-on-surface': '#0F172A', + '--mat-sys-on-surface-variant': '#475569', + '--mat-sys-outline': '#CBD5E1', + '--mat-sys-background': '#F8FAFC', +}; + +// --------------------------------------------------------------------------- +// Dark theme (matches the SCSS defaults) +// --------------------------------------------------------------------------- +const DARK_THEME_OVERRIDES: Record = { + '--cc-surface-darkest': '#0D0F12', + '--cc-surface-dark': '#13161A', + '--cc-surface-medium': '#1C2027', + '--cc-surface-light': '#252B33', + '--cc-surface-lighter': '#2D3748', + + '--cc-on-surface': '#E2E8F0', + '--cc-on-surface-variant': '#8A9BB0', + '--cc-on-surface-muted': '#64748B', + + '--mat-sys-surface': '#13161A', + '--mat-sys-surface-container': '#1C2027', + '--mat-sys-surface-container-high': '#252B33', + '--mat-sys-on-surface': '#E2E8F0', + '--mat-sys-on-surface-variant': '#8A9BB0', + '--mat-sys-outline': '#2D3748', + '--mat-sys-background': '#0D0F12', +}; + +@Injectable({ providedIn: 'root' }) +export class CcThemeService { + // --------------------------------------------------------------------------- + // Signals for reactive theme state + // --------------------------------------------------------------------------- + private readonly _mode = signal( + (localStorage.getItem('cc-theme') as ThemeMode) ?? 'dark' + ); + + /** Current theme mode */ + readonly mode = this._mode.asReadonly(); + + /** Computed: is the current mode dark? */ + readonly isDark = computed(() => this._mode() === 'dark'); + + /** Computed: is the current mode light? */ + readonly isLight = computed(() => this._mode() === 'light'); + + constructor() { + // Apply theme on init and whenever mode changes + effect(() => { + this.applyTheme(this._mode()); + }); + } + + // --------------------------------------------------------------------------- + // Public API + // --------------------------------------------------------------------------- + + /** Set the theme mode and persist to localStorage */ + setMode(mode: ThemeMode): void { + this._mode.set(mode); + localStorage.setItem('cc-theme', mode); + } + + /** Toggle between dark and light mode */ + toggle(): void { + this.setMode(this._mode() === 'dark' ? 'light' : 'dark'); + } + + /** Read a CSS custom property from the document root */ + getToken(property: string): string { + return getCssToken(property); + } + + /** Set a CSS custom property on the document root */ + setToken(property: string, value: string): void { + setCssToken(property, value); + } + + /** Get status color set by agent status */ + getStatusColors(status: string): { fg: string; bg: string; border: string } { + return getStatusColor(status); + } + + // --------------------------------------------------------------------------- + // Internal + // --------------------------------------------------------------------------- + + /** Apply a theme mode by setting all CSS custom properties */ + private applyTheme(mode: ThemeMode): void { + const overrides = mode === 'dark' ? DARK_THEME_OVERRIDES : LIGHT_THEME_OVERRIDES; + + // Set color-scheme for native form controls + document.documentElement.style.setProperty('color-scheme', mode); + + // Apply all overrides + for (const [prop, value] of Object.entries(overrides)) { + document.documentElement.style.setProperty(prop, value); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/design/tokens.ts b/frontend/src/app/design/tokens.ts new file mode 100644 index 0000000..1029c9b --- /dev/null +++ b/frontend/src/app/design/tokens.ts @@ -0,0 +1,379 @@ +// ============================================================================ +// OpenClaw Control Center — Design Tokens (TypeScript) +// ============================================================================ +// Typed representation of the design system tokens for programmatic access. +// These mirror the SCSS tokens in styles/_tokens.scss and the CSS custom +// properties emitted by styles/_css-properties.scss. +// +// Usage: +// import { CcTokens } from '@app/design/tokens'; +// const primary = CcTokens.color.primary; +// const surface = CcTokens.surface.dark; +// ============================================================================ + +// --------------------------------------------------------------------------- +// Color Palette +// --------------------------------------------------------------------------- +export const CcColors = { + primary: { + 50: '#ecfeff', + 100: '#cffafe', + 200: '#a5f3fc', + 300: '#67e8f9', + 400: '#22d3ee', + 500: '#38bdf8', + 600: '#0ea5e9', + 700: '#0284c7', + 800: '#0369a1', + 900: '#075985', + }, + secondary: { + 50: '#f0fdfa', + 100: '#ccfbf1', + 200: '#99f6e4', + 300: '#5eead4', + 400: '#2dd4bf', + 500: '#14b8a6', + 600: '#0d9488', + 700: '#0f766e', + 800: '#115e59', + 900: '#134e4a', + }, + accent: { + 50: '#f5f3ff', + 100: '#ede9fe', + 200: '#ddd6fe', + 300: '#c4b5fd', + 400: '#a78bfa', + 500: '#8b5cf6', + 600: '#7c3aed', + 700: '#6d28d9', + 800: '#5b21b6', + 900: '#4c1d95', + }, + danger: { + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + }, +} as const; + +// --------------------------------------------------------------------------- +// Semantic Colors (Tactical Dark) +// --------------------------------------------------------------------------- +export const CcSemanticColors = { + surface: { + darkest: '#0D0F12', + dark: '#13161A', + medium: '#1C2027', + light: '#252B33', + lighter: '#2D3748', + }, + onSurface: { + primary: '#E2E8F0', + variant: '#8A9BB0', + muted: '#64748B', + }, +} as const; + +// --------------------------------------------------------------------------- +// Status Colors +// --------------------------------------------------------------------------- +export const CcStatusColors = { + active: { fg: '#38bdf8', bg: 'rgba(56, 189, 248, 0.12)', border: 'rgba(56, 189, 248, 0.40)' }, + idle: { fg: '#2dd4bf', bg: 'rgba(45, 212, 191, 0.12)', border: 'rgba(45, 212, 191, 0.40)' }, + thinking: { fg: '#a78bfa', bg: 'rgba(167, 139, 250, 0.12)', border: 'rgba(167, 139, 250, 0.40)' }, + error: { fg: '#f87171', bg: 'rgba(248, 113, 113, 0.12)', border: 'rgba(248, 113, 113, 0.40)' }, + offline: { fg: '#64748b', bg: 'rgba(100, 116, 139, 0.12)', border: 'rgba(100, 116, 139, 0.40)' }, +} as const; + +// --------------------------------------------------------------------------- +// Typography +// --------------------------------------------------------------------------- +export const CcTypography = { + fontFamily: { + brand: "'Inter, Roboto, sans-serif'", + body: "'Inter, Roboto, sans-serif'", + mono: "'Roboto Mono, Cascadia Code, Fira Code, monospace'", + }, + size: { + displayLarge: '57px', + displayMedium: '45px', + displaySmall: '36px', + headlineLarge: '32px', + headlineMedium: '28px', + headlineSmall: '24px', + titleLarge: '22px', + titleMedium: '16px', + titleSmall: '14px', + bodyLarge: '16px', + bodyMedium: '14px', + bodySmall: '12px', + labelLarge: '14px', + labelMedium: '12px', + labelSmall: '11px', + }, + weight: { + regular: 400, + medium: 500, + bold: 600, + heavy: 700, + }, + lineHeight: { + tight: '1.2', + normal: '1.5', + relaxed: '1.6', + }, + letterSpacing: { + tight: '-0.01em', + normal: '0em', + wide: '0.02em', + mono: '0.05em', + }, +} as const; + +// --------------------------------------------------------------------------- +// Spacing (4px grid) +// --------------------------------------------------------------------------- +export const CcSpacing = { + 0: '0px', + 1: '4px', + 2: '8px', + 3: '12px', + 4: '16px', + 5: '20px', + 6: '24px', + 7: '28px', + 8: '32px', + 9: '36px', + 10: '40px', + 12: '48px', + 14: '56px', + 16: '64px', + 20: '80px', +} as const; + +// --------------------------------------------------------------------------- +// Layout +// --------------------------------------------------------------------------- +export const CcLayout = { + navRailCollapsedWidth: '72px', + navRailExpandedWidth: '256px', + headerHeight: '64px', + bottomNavHeight: '80px', + cardBorderRadius: '16px', + cardMinWidth: '320px', + badgeHeight: '24px', + badgeBorderRadius: '12px', + statusDotSize: '10px', +} as const; + +// --------------------------------------------------------------------------- +// Breakpoints (M3 canonical) +// --------------------------------------------------------------------------- +export const CcBreakpoints = { + compact: 599, + medium: 767, + expanded: 1023, + large: 1439, +} as const; + +// --------------------------------------------------------------------------- +// Border Radius +// --------------------------------------------------------------------------- +export const CcRadius = { + none: '0px', + xs: '4px', + sm: '8px', + md: '12px', + lg: '16px', + xl: '24px', + full: '9999px', +} as const; + +// --------------------------------------------------------------------------- +// Shadows (M3 elevation) +// --------------------------------------------------------------------------- +export const CcShadows = { + level0: 'none', + level1: '0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px -1px rgba(0, 0, 0, 0.3)', + level2: '0 2px 6px 0 rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.3)', + level3: '0 4px 12px 0 rgba(0, 0, 0, 0.3), 0 4px 8px -4px rgba(0, 0, 0, 0.3)', + level4: '0 8px 24px 0 rgba(0, 0, 0, 0.3), 0 8px 16px -8px rgba(0, 0, 0, 0.3)', +} as const; + +// --------------------------------------------------------------------------- +// Motion +// --------------------------------------------------------------------------- +export const CcMotion = { + duration: { + instant: 0, + fast: 100, + short: 150, + medium: 200, + standard: 300, + long: 500, + }, + easing: { + standard: 'cubic-bezier(0.4, 0, 0.2, 1)', + decelerate: 'cubic-bezier(0, 0, 0.2, 1)', + accelerate: 'cubic-bezier(0.4, 0, 1, 1)', + sharp: 'cubic-bezier(0.4, 0, 0.6, 1)', + }, +} as const; + +// --------------------------------------------------------------------------- +// Accessibility +// --------------------------------------------------------------------------- +export const CcA11y = { + focusRing: { + width: '2px', + offset: '2px', + color: '#38bdf8', + style: 'solid', + }, + minTouchTarget: 48, + minBodyFont: 16, +} as const; + +// --------------------------------------------------------------------------- +// Aggregate token object for convenient access +// --------------------------------------------------------------------------- +export const CcTokens = { + color: CcColors, + semantic: CcSemanticColors, + status: CcStatusColors, + typography: CcTypography, + spacing: CcSpacing, + layout: CcLayout, + breakpoints: CcBreakpoints, + radius: CcRadius, + shadows: CcShadows, + motion: CcMotion, + a11y: CcA11y, +} as const; + +// --------------------------------------------------------------------------- +// CSS Custom Property Names +// --------------------------------------------------------------------------- +// Use these constants when setting styles programmatically via Renderer2 +// or ElementRef.style, e.g.: el.style.setProperty(CcCssProps.colorPrimary, '#fff') +// --------------------------------------------------------------------------- +export const CcCssProps = { + // Color + colorPrimary: '--cc-color-primary', + colorSecondary: '--cc-color-secondary', + colorAccent: '--cc-color-accent', + colorDanger: '--cc-color-danger', + + // Surface + surfaceDarkest: '--cc-surface-darkest', + surfaceDark: '--cc-surface-dark', + surfaceMedium: '--cc-surface-medium', + surfaceLight: '--cc-surface-light', + surfaceLighter: '--cc-surface-lighter', + + // On-surface + onSurface: '--cc-on-surface', + onSurfaceVariant: '--cc-on-surface-variant', + onSurfaceMuted: '--cc-on-surface-muted', + + // Status + statusActive: '--cc-status-active', + statusIdle: '--cc-status-idle', + statusThinking: '--cc-status-thinking', + statusError: '--cc-status-error', + statusOffline: '--cc-status-offline', + statusActiveBg: '--cc-status-active-bg', + statusIdleBg: '--cc-status-idle-bg', + statusThinkingBg: '--cc-status-thinking-bg', + statusErrorBg: '--cc-status-error-bg', + statusOfflineBg: '--cc-status-offline-bg', + statusActiveBorder: '--cc-status-active-border', + statusIdleBorder: '--cc-status-idle-border', + statusThinkingBorder: '--cc-status-thinking-border', + statusErrorBorder: '--cc-status-error-border', + statusOfflineBorder: '--cc-status-offline-border', + + // Typography + fontBrand: '--cc-font-brand', + fontBody: '--cc-font-body', + fontMono: '--cc-font-mono', + + // Spacing + spacing2: '--cc-spacing-2', + spacing4: '--cc-spacing-4', + spacing6: '--cc-spacing-6', + spacing8: '--cc-spacing-8', + spacing12: '--cc-spacing-12', + spacing16: '--cc-spacing-16', + + // Layout + navRailCollapsed: '--cc-nav-rail-collapsed', + navRailExpanded: '--cc-nav-rail-expanded', + headerHeight: '--cc-header-height', + bottomNavHeight: '--cc-bottom-nav-height', + cardRadius: '--cc-card-radius', + cardMinWidth: '--cc-card-min-width', + + // Radius + radiusNone: '--cc-radius-none', + radiusXs: '--cc-radius-xs', + radiusSm: '--cc-radius-sm', + radiusMd: '--cc-radius-md', + radiusLg: '--cc-radius-lg', + radiusXl: '--cc-radius-xl', + radiusFull: '--cc-radius-full', + + // Shadows + shadow0: '--cc-shadow-0', + shadow1: '--cc-shadow-1', + shadow2: '--cc-shadow-2', + shadow3: '--cc-shadow-3', + shadow4: '--cc-shadow-4', + + // Motion + durationFast: '--cc-duration-fast', + durationShort: '--cc-duration-short', + durationMedium: '--cc-duration-medium', + durationStandard: '--cc-duration-standard', + durationLong: '--cc-duration-long', + easingStandard: '--cc-easing-standard', + easingDecelerate: '--cc-easing-decelerate', + easingAccelerate: '--cc-easing-accelerate', + + // Accessibility + focusWidth: '--cc-focus-width', + focusOffset: '--cc-focus-offset', + focusColor: '--cc-focus-color', + touchMin: '--cc-touch-min', +} as const; + +// --------------------------------------------------------------------------- +// Utility: Read a CSS custom property from the document +// --------------------------------------------------------------------------- +export function getCssToken(propertyName: string): string { + return getComputedStyle(document.documentElement).getPropertyValue(propertyName).trim(); +} + +// --------------------------------------------------------------------------- +// Utility: Set a CSS custom property on the document root +// --------------------------------------------------------------------------- +export function setCssToken(propertyName: string, value: string): void { + document.documentElement.style.setProperty(propertyName, value); +} + +// --------------------------------------------------------------------------- +// Utility: Get status color by agent status type +// --------------------------------------------------------------------------- +export function getStatusColor(status: string): { fg: string; bg: string; border: string } { + const statusMap: Record = CcStatusColors; + return statusMap[status] ?? CcStatusColors.offline; +} \ No newline at end of file diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index a581a28..5a06940 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1,10 +1,18 @@ // ============================================================================ // OpenClaw Control Center — M3 Tactical Dark Theme // ============================================================================ -// Material Design 3 theming with custom dark palette per design spec. -// Section 5.1: Color Palette, Section 5.2: Typography +// Main global stylesheet. Imports the design system token modules and +// applies the M3 dark theme. All tokens are defined once in +// styles/_tokens.scss — SCSS variables and mixins +// styles/_css-properties.scss — CSS custom property output +// styles/_utilities.scss — utility mixins for components +// +// Components should @use these modules rather than hardcoding values. // ============================================================================ +@use 'styles/tokens' as tokens; +@use 'styles/css-properties' as css-props; +@use 'styles/utilities' as utils; @use '@angular/material' as mat; // --------------------------------------------------------------------------- @@ -21,11 +29,11 @@ $dark-theme: mat.define-theme(( tertiary: mat.$violet-palette, ), typography: ( - brand-family: 'Inter, Roboto, sans-serif', - plain-family: 'Inter, Roboto, sans-serif', - bold-weight: 600, - medium-weight: 500, - regular-weight: 400, + brand-family: tokens.$font-family-brand, + plain-family: tokens.$font-family-body, + bold-weight: tokens.$font-weight-bold, + medium-weight: tokens.$font-weight-medium, + regular-weight: tokens.$font-weight-regular, ), density: ( scale: 0, @@ -42,68 +50,17 @@ html { } // --------------------------------------------------------------------------- -// Custom CSS Custom Properties — Status Colors +// Emit Design System CSS Custom Properties // --------------------------------------------------------------------------- -// Per spec Section 5.1 "Status Colors (Semantic — outside M3 tonal system)" -// These are NOT part of the M3 tonal palette; they are semantic overrides. -// --------------------------------------------------------------------------- -:root { - // --- Tactical Dark Mode color palette (CUB-47) --- - --color-surface: #0F172A; - --color-surface-light: #1E293B; - --color-primary: #38BDF8; - --color-secondary: #2DD4BF; - --color-accent: #A78BFA; - --color-danger: #F87171; - --color-text-primary: #FFFFFF; - --color-text-secondary: #94A3B8; - --color-border: #334155; - - // --- Status colors --- - --status-active: #38BDF8; - --status-idle: #2DD4BF; - --status-thinking: #A78BFA; - --status-error: #F87171; - --status-offline: #64748B; - - // --- Status background tints (12% opacity) --- - --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); - - // --- Surface overrides (tactical dark palette) --- - --cc-background: #0D0F12; - --cc-surface: #13161A; - --cc-surface-container: #1C2027; - --cc-surface-container-high: #252B33; - --cc-on-surface: #E2E8F0; - --cc-on-surface-variant: #8A9BB0; - --cc-outline: #2D3748; - - // --- Mono font stack --- - --cc-font-mono: 'Roboto Mono', 'Cascadia Code', 'Fira Code', monospace; - - // --- Layout constants --- - --cc-nav-rail-collapsed-width: 72px; - --cc-nav-rail-expanded-width: 256px; - --cc-header-height: 64px; - --cc-bottom-nav-height: 80px; - --cc-card-border-radius: 16px; - --cc-card-min-width: 320px; - --cc-card-gap: 16px; - --cc-card-padding: 20px; - --cc-section-padding: 24px; - --cc-spacing-unit: 8px; -} +@include css-props.emit-custom-properties; // --------------------------------------------------------------------------- // Global Body Styles // --------------------------------------------------------------------------- body { - background-color: var(--color-surface); + background-color: var(--cc-surface-darkest); color: var(--cc-on-surface); - font-family: 'Inter', 'Roboto', sans-serif; + font-family: var(--cc-font-body); margin: 0; height: 100%; min-height: 100vh; @@ -111,37 +68,60 @@ body { -moz-osx-font-smoothing: grayscale; } -// --------------------------------------------------------------------------- -// M3 Surface Overrides -// --------------------------------------------------------------------------- -// Override M3 surface tokens to match our tactical dark palette -// --------------------------------------------------------------------------- -:root { - // Override M3 system color tokens to match custom palette - --mat-sys-surface: var(--cc-surface); - --mat-sys-surface-container: var(--cc-surface-container); - --mat-sys-surface-container-high: var(--cc-surface-container-high); - --mat-sys-on-surface: var(--cc-on-surface); - --mat-sys-on-surface-variant: var(--cc-on-surface-variant); - --mat-sys-outline: var(--cc-outline); - --mat-sys-background: var(--cc-background); -} - // --------------------------------------------------------------------------- // Typography Helpers // --------------------------------------------------------------------------- .text-mono { font-family: var(--cc-font-mono); - font-size: 13px; - font-weight: 400; - letter-spacing: 0.02em; + font-size: tokens.$font-size-body-medium; + font-weight: tokens.$font-weight-regular; + letter-spacing: tokens.$letter-spacing-mono; +} + +.text-display-large { + font-size: tokens.$font-size-display-large; + font-weight: tokens.$font-weight-heavy; + line-height: tokens.$line-height-tight; + letter-spacing: tokens.$letter-spacing-tight; +} + +.text-headline-medium { + font-size: tokens.$font-size-headline-medium; + font-weight: tokens.$font-weight-bold; + line-height: tokens.$line-height-tight; +} + +.text-title-large { + font-size: tokens.$font-size-title-large; + font-weight: tokens.$font-weight-bold; +} + +.text-title-medium { + font-size: tokens.$font-size-title-medium; + font-weight: tokens.$font-weight-medium; +} + +.text-body-large { + font-size: tokens.$font-size-body-large; + font-weight: tokens.$font-weight-regular; + line-height: tokens.$line-height-normal; +} + +.text-body-medium { + font-size: tokens.$font-size-body-medium; + font-weight: tokens.$font-weight-regular; + line-height: tokens.$line-height-normal; +} + +.text-label-medium { + font-size: tokens.$font-size-label-medium; + font-weight: tokens.$font-weight-medium; + letter-spacing: tokens.$letter-spacing-wide; } // --------------------------------------------------------------------------- // Status Dot Pulse Animations // --------------------------------------------------------------------------- -// Per spec Section 7.5: Animation Specs -// --------------------------------------------------------------------------- @keyframes pulse-active { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.15); } @@ -158,35 +138,35 @@ body { } // --------------------------------------------------------------------------- -// Utility Classes +// Status Dot Utility Classes // --------------------------------------------------------------------------- .status-dot { - width: 10px; - height: 10px; - border-radius: 50%; + width: tokens.$status-dot-size; + height: tokens.$status-dot-size; + border-radius: tokens.$radius-full; display: inline-block; &--active { - background-color: var(--status-active); - animation: pulse-active 2s ease-in-out infinite; + background-color: var(--cc-status-active); + animation: pulse-active tokens.$duration-standard tokens.$easing-standard infinite; } &--idle { - background-color: var(--status-idle); + background-color: var(--cc-status-idle); } &--thinking { - background-color: var(--status-thinking); - animation: pulse-thinking 3s ease-in-out infinite; + background-color: var(--cc-status-thinking); + animation: pulse-thinking 3s tokens.$easing-standard infinite; } &--error { - background-color: var(--status-error); - animation: pulse-error 0.8s ease-in-out infinite; + background-color: var(--cc-status-error); + animation: pulse-error tokens.$duration-fast tokens.$easing-standard infinite; } &--offline { - background-color: var(--status-offline); + background-color: var(--cc-status-offline); } } @@ -201,6 +181,27 @@ body { } } +// --------------------------------------------------------------------------- +// Screen-reader-only utility +// --------------------------------------------------------------------------- +.sr-only { + @include tokens.sr-only; +} + +// --------------------------------------------------------------------------- +// Truncate utility +// --------------------------------------------------------------------------- +.truncate { + @include tokens.truncate; +} + +// --------------------------------------------------------------------------- +// Focus ring utility +// --------------------------------------------------------------------------- +.focus-ring { + @include tokens.focus-ring; +} + // --------------------------------------------------------------------------- // Scrollbar Styling (Tactical Dark) // --------------------------------------------------------------------------- @@ -210,11 +211,11 @@ body { } ::-webkit-scrollbar-track { - background: var(--cc-surface); + background: var(--cc-surface-dark); } ::-webkit-scrollbar-thumb { - background: var(--cc-outline); + background: var(--cc-surface-lighter); border-radius: 3px; &:hover { diff --git a/frontend/src/styles/_css-properties.scss b/frontend/src/styles/_css-properties.scss new file mode 100644 index 0000000..d067500 --- /dev/null +++ b/frontend/src/styles/_css-properties.scss @@ -0,0 +1,115 @@ +// ============================================================================ +// OpenClaw Control Center — CSS Custom Property Output +// ============================================================================ +// This module emits ALL design tokens as CSS custom properties on :root. +// Import this in styles.scss to make tokens available to all components. +// +// Tokens are namespaced with --cc- (Control Center) to avoid collisions +// with Angular Material's --mat- variables. +// ============================================================================ + +@use 'tokens' as *; + +// --------------------------------------------------------------------------- +// Emit all CSS custom properties +// --------------------------------------------------------------------------- +@mixin emit-custom-properties { + :root { + // --- Color tokens --- + --cc-color-primary: #{$color-primary-500}; + --cc-color-secondary: #{$color-secondary-400}; + --cc-color-accent: #{$color-accent-400}; + --cc-color-danger: #{$color-danger-400}; + + // --- Surface tokens --- + --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}; + + // --- On-surface tokens --- + --cc-on-surface: #{$color-on-surface}; + --cc-on-surface-variant: #{$color-on-surface-variant}; + --cc-on-surface-muted: #{$color-on-surface-muted}; + + // --- Status tokens --- + --cc-status-active: #{$status-active}; + --cc-status-idle: #{$status-idle}; + --cc-status-thinking: #{$status-thinking}; + --cc-status-error: #{$status-error}; + --cc-status-offline: #{$status-offline}; + + --cc-status-active-bg: #{$status-active-bg}; + --cc-status-idle-bg: #{$status-idle-bg}; + --cc-status-thinking-bg: #{$status-thinking-bg}; + --cc-status-error-bg: #{$status-error-bg}; + --cc-status-offline-bg: #{$status-offline-bg}; + + --cc-status-active-border: #{$status-active-border}; + --cc-status-idle-border: #{$status-idle-border}; + --cc-status-thinking-border: #{$status-thinking-border}; + --cc-status-error-border: #{$status-error-border}; + --cc-status-offline-border: #{$status-offline-border}; + + // --- Typography tokens --- + --cc-font-brand: #{$font-family-brand}; + --cc-font-body: #{$font-family-body}; + --cc-font-mono: #{$font-family-mono}; + + // --- Spacing tokens --- + --cc-spacing-0: #{$spacing-0}; + --cc-spacing-1: #{$spacing-1}; + --cc-spacing-2: #{$spacing-2}; + --cc-spacing-3: #{$spacing-3}; + --cc-spacing-4: #{$spacing-4}; + --cc-spacing-5: #{$spacing-5}; + --cc-spacing-6: #{$spacing-6}; + --cc-spacing-7: #{$spacing-7}; + --cc-spacing-8: #{$spacing-8}; + --cc-spacing-10: #{$spacing-10}; + --cc-spacing-12: #{$spacing-12}; + --cc-spacing-16: #{$spacing-16}; + + // --- Layout tokens --- + --cc-nav-rail-collapsed: #{$nav-rail-collapsed-width}; + --cc-nav-rail-expanded: #{$nav-rail-expanded-width}; + --cc-header-height: #{$header-height}; + --cc-bottom-nav-height: #{$bottom-nav-height}; + --cc-card-radius: #{$card-border-radius}; + --cc-card-min-width: #{$card-min-width}; + + // --- Radius tokens --- + --cc-radius-none: #{$radius-none}; + --cc-radius-xs: #{$radius-xs}; + --cc-radius-sm: #{$radius-sm}; + --cc-radius-md: #{$radius-md}; + --cc-radius-lg: #{$radius-lg}; + --cc-radius-xl: #{$radius-xl}; + --cc-radius-full: #{$radius-full}; + + // --- Shadow tokens --- + --cc-shadow-0: #{$shadow-level-0}; + --cc-shadow-1: #{$shadow-level-1}; + --cc-shadow-2: #{$shadow-level-2}; + --cc-shadow-3: #{$shadow-level-3}; + --cc-shadow-4: #{$shadow-level-4}; + + // --- Motion tokens --- + --cc-duration-fast: #{$duration-fast}; + --cc-duration-short: #{$duration-short}; + --cc-duration-medium: #{$duration-medium}; + --cc-duration-standard: #{$duration-standard}; + --cc-duration-long: #{$duration-long}; + + --cc-easing-standard: #{$easing-standard}; + --cc-easing-decelerate: #{$easing-decelerate}; + --cc-easing-accelerate: #{$easing-accelerate}; + + // --- Accessibility tokens --- + --cc-focus-width: #{$focus-ring-width}; + --cc-focus-offset: #{$focus-ring-offset}; + --cc-focus-color: #{$focus-ring-color}; + --cc-touch-min: #{$min-touch-target}; + } +} \ No newline at end of file diff --git a/frontend/src/styles/_tokens.scss b/frontend/src/styles/_tokens.scss new file mode 100644 index 0000000..a430661 --- /dev/null +++ b/frontend/src/styles/_tokens.scss @@ -0,0 +1,437 @@ +// ============================================================================ +// 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; +} \ No newline at end of file diff --git a/frontend/src/styles/_utilities.scss b/frontend/src/styles/_utilities.scss new file mode 100644 index 0000000..9901928 --- /dev/null +++ b/frontend/src/styles/_utilities.scss @@ -0,0 +1,170 @@ +// ============================================================================ +// OpenClaw Control Center — Utility Mixins +// ============================================================================ +// Reusable patterns that enforce design-system consistency. +// Components should @use this module and include mixins rather than +// writing repetitive CSS blocks. +// ============================================================================ + +@use 'tokens' as *; + +// --------------------------------------------------------------------------- +// Elevation / Surface Card +// Applies consistent card styling using design tokens. +// Usage: @include utils.card-surface(); +// --------------------------------------------------------------------------- +@mixin card-surface($elevation: 1) { + background-color: var(--cc-surface-medium); + border-radius: var(--cc-card-radius); + border: 1px solid var(--cc-surface-lighter); + box-shadow: var(--cc-shadow-#{$elevation}); + transition: box-shadow var(--cc-duration-short) var(--cc-easing-standard); +} + +// --------------------------------------------------------------------------- +// Elevated card on hover +// --------------------------------------------------------------------------- +@mixin card-hover($elevation: 2) { + &:hover { + box-shadow: var(--cc-shadow-#{$elevation}); + } +} + +// --------------------------------------------------------------------------- +// Status-aware left border +// Applies colored left border based on agent status. +// Usage: @include utils.status-border('active'); +// --------------------------------------------------------------------------- +@mixin status-border($status) { + $status-map: ( + 'active': var(--cc-status-active), + 'idle': var(--cc-status-idle), + 'thinking': var(--cc-status-thinking), + 'error': var(--cc-status-error), + 'offline': var(--cc-status-offline), + ); + $color: map-get($status-map, $status); + @if not $color { + $color: var(--cc-status-offline); + } + border-left: 4px solid $color; +} + +// --------------------------------------------------------------------------- +// Status badge / pill +// --------------------------------------------------------------------------- +@mixin status-badge($status) { + $fg-map: ( + 'active': var(--cc-status-active), + 'idle': var(--cc-status-idle), + 'thinking': var(--cc-status-thinking), + 'error': var(--cc-status-error), + 'offline': var(--cc-status-offline), + ); + $bg-map: ( + 'active': var(--cc-status-active-bg), + 'idle': var(--cc-status-idle-bg), + 'thinking': var(--cc-status-thinking-bg), + 'error': var(--cc-status-error-bg), + 'offline': var(--cc-status-offline-bg), + ); + + display: inline-flex; + align-items: center; + gap: 6px; + height: $badge-height; + padding: 0 8px; + border-radius: $badge-border-radius; + background-color: map-get($bg-map, $status); + color: map-get($fg-map, $status); + font-size: $font-size-label-medium; + font-weight: $font-weight-medium; + white-space: nowrap; +} + +// --------------------------------------------------------------------------- +// Glass surface (frosted glass effect) +// --------------------------------------------------------------------------- +@mixin glass-surface { + background-color: rgba(19, 22, 26, 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--cc-surface-lighter); +} + +// --------------------------------------------------------------------------- +// Responsive grid +// Creates a responsive grid that adapts from 1-col to 2-col. +// --------------------------------------------------------------------------- +@mixin responsive-grid($min-col-width: $card-min-width, $gap: $spacing-card-gap) { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($min-col-width, 1fr)); + gap: $gap; +} + +// --------------------------------------------------------------------------- +// Scroll container +// --------------------------------------------------------------------------- +@mixin scroll-container($direction: 'y') { + @if $direction == 'y' { + overflow-y: auto; + overflow-x: hidden; + } @else { + overflow-x: auto; + overflow-y: hidden; + } + -webkit-overflow-scrolling: touch; + + // Custom scrollbar (tactical dark) + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + &::-webkit-scrollbar-track { + background: var(--cc-surface-dark); + } + &::-webkit-scrollbar-thumb { + background: var(--cc-surface-lighter); + border-radius: 3px; + &:hover { + background: var(--cc-on-surface-variant); + } + } +} + +// --------------------------------------------------------------------------- +// Page container +// Standard page padding and layout +// --------------------------------------------------------------------------- +@mixin page-container { + padding: $spacing-section; + min-height: 400px; + overflow-x: hidden; +} + +// --------------------------------------------------------------------------- +// Transition helpers +// --------------------------------------------------------------------------- +@mixin transition-colors($duration: $duration-short) { + transition: color #{$duration} $easing-standard, + background-color #{$duration} $easing-standard, + border-color #{$duration} $easing-standard; +} + +@mixin transition-transform($duration: $duration-medium) { + transition: transform #{$duration} $easing-standard; +} + +@mixin transition-opacity($duration: $duration-short) { + transition: opacity #{$duration} $easing-standard; +} + +// --------------------------------------------------------------------------- +// Reduced motion +// Wraps content in a reduced-motion media query. +// --------------------------------------------------------------------------- +@mixin reduced-motion { + @media (prefers-reduced-motion: reduce) { + @content; + } +} \ No newline at end of file