CUB-21: Build tactical dark mode theme and design system #27
@@ -53,7 +53,12 @@
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
]
|
||||
],
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
||||
11
frontend/src/app/design/index.ts
Normal file
11
frontend/src/app/design/index.ts
Normal file
@@ -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';
|
||||
151
frontend/src/app/design/theme.service.ts
Normal file
151
frontend/src/app/design/theme.service.ts
Normal file
@@ -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<string, string> = {
|
||||
// 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<string, string> = {
|
||||
'--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<ThemeMode>(
|
||||
(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
379
frontend/src/app/design/tokens.ts
Normal file
379
frontend/src/app/design/tokens.ts
Normal file
@@ -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<string, { fg: string; bg: string; border: string }> = CcStatusColors;
|
||||
return statusMap[status] ?? CcStatusColors.offline;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
115
frontend/src/styles/_css-properties.scss
Normal file
115
frontend/src/styles/_css-properties.scss
Normal file
@@ -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};
|
||||
}
|
||||
}
|
||||
437
frontend/src/styles/_tokens.scss
Normal file
437
frontend/src/styles/_tokens.scss
Normal file
@@ -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;
|
||||
}
|
||||
170
frontend/src/styles/_utilities.scss
Normal file
170
frontend/src/styles/_utilities.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user