feat: add theme system from jarvis frontend
This commit is contained in:
@@ -15,11 +15,17 @@
|
|||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^9.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@mosaic/shared": "workspace:*",
|
"@mosaic/shared": "workspace:*",
|
||||||
"@mosaic/ui": "workspace:*",
|
"@mosaic/ui": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.90.20",
|
"@tanstack/react-query": "^5.90.20",
|
||||||
|
"@xyflow/react": "^12.5.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"elkjs": "^0.9.3",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
|
"mermaid": "^11.4.1",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -2,19 +2,747 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* =============================================================================
|
||||||
|
DESIGN C: PROFESSIONAL/ENTERPRISE DESIGN SYSTEM
|
||||||
|
Philosophy: "Good design is as little design as possible." - Dieter Rams
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
CSS Custom Properties - Light Theme (Default)
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
:root {
|
:root {
|
||||||
--foreground-rgb: 0, 0, 0;
|
/* Base colors - increased contrast from surfaces */
|
||||||
--background-rgb: 255, 255, 255;
|
--color-background: 245 247 250;
|
||||||
|
--color-foreground: 15 23 42;
|
||||||
|
|
||||||
|
/* Surface hierarchy (elevation levels) - improved contrast */
|
||||||
|
--surface-0: 255 255 255;
|
||||||
|
--surface-1: 250 251 252;
|
||||||
|
--surface-2: 241 245 249;
|
||||||
|
--surface-3: 226 232 240;
|
||||||
|
|
||||||
|
/* Text hierarchy */
|
||||||
|
--text-primary: 15 23 42;
|
||||||
|
--text-secondary: 51 65 85;
|
||||||
|
--text-tertiary: 71 85 105;
|
||||||
|
--text-muted: 100 116 139;
|
||||||
|
|
||||||
|
/* Border colors - stronger borders for light mode */
|
||||||
|
--border-default: 203 213 225;
|
||||||
|
--border-subtle: 226 232 240;
|
||||||
|
--border-strong: 148 163 184;
|
||||||
|
|
||||||
|
/* Brand accent - Indigo (professional, trustworthy) */
|
||||||
|
--accent-primary: 79 70 229;
|
||||||
|
--accent-primary-hover: 67 56 202;
|
||||||
|
--accent-primary-light: 238 242 255;
|
||||||
|
--accent-primary-muted: 199 210 254;
|
||||||
|
|
||||||
|
/* Semantic colors - Success (Emerald) */
|
||||||
|
--semantic-success: 16 185 129;
|
||||||
|
--semantic-success-light: 209 250 229;
|
||||||
|
--semantic-success-dark: 6 95 70;
|
||||||
|
|
||||||
|
/* Semantic colors - Warning (Amber) */
|
||||||
|
--semantic-warning: 245 158 11;
|
||||||
|
--semantic-warning-light: 254 243 199;
|
||||||
|
--semantic-warning-dark: 146 64 14;
|
||||||
|
|
||||||
|
/* Semantic colors - Error (Rose) */
|
||||||
|
--semantic-error: 244 63 94;
|
||||||
|
--semantic-error-light: 255 228 230;
|
||||||
|
--semantic-error-dark: 159 18 57;
|
||||||
|
|
||||||
|
/* Semantic colors - Info (Sky) */
|
||||||
|
--semantic-info: 14 165 233;
|
||||||
|
--semantic-info-light: 224 242 254;
|
||||||
|
--semantic-info-dark: 3 105 161;
|
||||||
|
|
||||||
|
/* Focus ring */
|
||||||
|
--focus-ring: 99 102 241;
|
||||||
|
--focus-ring-offset: 255 255 255;
|
||||||
|
|
||||||
|
/* Shadows - visible but subtle */
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05), 0 1px 3px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.08), 0 2px 4px -2px rgb(0 0 0 / 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* -----------------------------------------------------------------------------
|
||||||
:root {
|
CSS Custom Properties - Dark Theme
|
||||||
--foreground-rgb: 255, 255, 255;
|
----------------------------------------------------------------------------- */
|
||||||
--background-rgb: 0, 0, 0;
|
.dark {
|
||||||
}
|
--color-background: 3 7 18;
|
||||||
|
--color-foreground: 248 250 252;
|
||||||
|
|
||||||
|
/* Surface hierarchy (elevation levels) */
|
||||||
|
--surface-0: 15 23 42;
|
||||||
|
--surface-1: 30 41 59;
|
||||||
|
--surface-2: 51 65 85;
|
||||||
|
--surface-3: 71 85 105;
|
||||||
|
|
||||||
|
/* Text hierarchy */
|
||||||
|
--text-primary: 248 250 252;
|
||||||
|
--text-secondary: 203 213 225;
|
||||||
|
--text-tertiary: 148 163 184;
|
||||||
|
--text-muted: 100 116 139;
|
||||||
|
|
||||||
|
/* Border colors */
|
||||||
|
--border-default: 51 65 85;
|
||||||
|
--border-subtle: 30 41 59;
|
||||||
|
--border-strong: 71 85 105;
|
||||||
|
|
||||||
|
/* Brand accent adjustments for dark mode */
|
||||||
|
--accent-primary: 129 140 248;
|
||||||
|
--accent-primary-hover: 165 180 252;
|
||||||
|
--accent-primary-light: 30 27 75;
|
||||||
|
--accent-primary-muted: 55 48 163;
|
||||||
|
|
||||||
|
/* Semantic colors adjustments */
|
||||||
|
--semantic-success: 52 211 153;
|
||||||
|
--semantic-success-light: 6 78 59;
|
||||||
|
--semantic-success-dark: 167 243 208;
|
||||||
|
|
||||||
|
--semantic-warning: 251 191 36;
|
||||||
|
--semantic-warning-light: 120 53 15;
|
||||||
|
--semantic-warning-dark: 253 230 138;
|
||||||
|
|
||||||
|
--semantic-error: 251 113 133;
|
||||||
|
--semantic-error-light: 136 19 55;
|
||||||
|
--semantic-error-dark: 253 164 175;
|
||||||
|
|
||||||
|
--semantic-info: 56 189 248;
|
||||||
|
--semantic-info-light: 12 74 110;
|
||||||
|
--semantic-info-dark: 186 230 253;
|
||||||
|
|
||||||
|
/* Focus ring */
|
||||||
|
--focus-ring: 129 140 248;
|
||||||
|
--focus-ring-offset: 15 23 42;
|
||||||
|
|
||||||
|
/* Shadows - subtle glow in dark mode */
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Base Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: rgb(var(--foreground-rgb));
|
color: rgb(var(--text-primary));
|
||||||
background: rgb(var(--background-rgb));
|
background: rgb(var(--color-background));
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
transition: background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Typography Utilities
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer utilities {
|
||||||
|
.text-display {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-2 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-caption {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text color utilities */
|
||||||
|
.text-primary {
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary {
|
||||||
|
color: rgb(var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-tertiary {
|
||||||
|
color: rgb(var(--text-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: rgb(var(--text-muted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Surface & Card Utilities
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer utilities {
|
||||||
|
.surface-0 {
|
||||||
|
background-color: rgb(var(--surface-0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-1 {
|
||||||
|
background-color: rgb(var(--surface-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-2 {
|
||||||
|
background-color: rgb(var(--surface-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.surface-3 {
|
||||||
|
background-color: rgb(var(--surface-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-default {
|
||||||
|
border-color: rgb(var(--border-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-subtle {
|
||||||
|
border-color: rgb(var(--border-subtle));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-strong {
|
||||||
|
border-color: rgb(var(--border-strong));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Focus States - Accessible & Visible
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer base {
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid rgb(var(--focus-ring));
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove default focus for mouse users */
|
||||||
|
:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Scrollbar Styling - Minimal & Professional
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgb(var(--text-muted) / 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgb(var(--text-muted) / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgb(var(--text-muted) / 0.4) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Button Component Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-all duration-150;
|
||||||
|
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
||||||
|
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn px-4 py-2;
|
||||||
|
background-color: rgb(var(--accent-primary));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: rgb(var(--accent-primary-hover));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply btn px-4 py-2;
|
||||||
|
background-color: rgb(var(--surface-2));
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
border: 1px solid rgb(var(--border-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background-color: rgb(var(--surface-3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
@apply btn px-3 py-2;
|
||||||
|
background-color: transparent;
|
||||||
|
color: rgb(var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
background-color: rgb(var(--surface-2));
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply btn px-4 py-2;
|
||||||
|
background-color: rgb(var(--semantic-error));
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
@apply px-3 py-1.5 text-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
@apply px-6 py-3 text-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Input Component Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.input {
|
||||||
|
@apply w-full rounded-md px-3 py-2 text-sm transition-all duration-150;
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-offset-0;
|
||||||
|
background-color: rgb(var(--surface-0));
|
||||||
|
border: 1px solid rgb(var(--border-default));
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.input::placeholder {
|
||||||
|
color: rgb(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
border-color: rgb(var(--accent-primary));
|
||||||
|
box-shadow: 0 0 0 3px rgb(var(--accent-primary) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled {
|
||||||
|
@apply opacity-50 cursor-not-allowed;
|
||||||
|
background-color: rgb(var(--surface-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
border-color: rgb(var(--semantic-error));
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error:focus {
|
||||||
|
border-color: rgb(var(--semantic-error));
|
||||||
|
box-shadow: 0 0 0 3px rgb(var(--semantic-error) / 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Card Component Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.card {
|
||||||
|
@apply rounded-lg p-4;
|
||||||
|
background-color: rgb(var(--surface-0));
|
||||||
|
border: 1px solid rgb(var(--border-default));
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-elevated {
|
||||||
|
@apply card;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-interactive {
|
||||||
|
@apply card transition-all duration-150;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-interactive:hover {
|
||||||
|
border-color: rgb(var(--border-strong));
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Badge Component Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: rgb(var(--semantic-success-light));
|
||||||
|
color: rgb(var(--semantic-success-dark));
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background-color: rgb(var(--semantic-warning-light));
|
||||||
|
color: rgb(var(--semantic-warning-dark));
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-error {
|
||||||
|
background-color: rgb(var(--semantic-error-light));
|
||||||
|
color: rgb(var(--semantic-error-dark));
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-info {
|
||||||
|
background-color: rgb(var(--semantic-info-light));
|
||||||
|
color: rgb(var(--semantic-info-dark));
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-neutral {
|
||||||
|
background-color: rgb(var(--surface-2));
|
||||||
|
color: rgb(var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary {
|
||||||
|
background-color: rgb(var(--accent-primary-light));
|
||||||
|
color: rgb(var(--accent-primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Status Indicator Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.status-dot {
|
||||||
|
@apply inline-block h-2 w-2 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-success {
|
||||||
|
background-color: rgb(var(--semantic-success));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-warning {
|
||||||
|
background-color: rgb(var(--semantic-warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-error {
|
||||||
|
background-color: rgb(var(--semantic-error));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-info {
|
||||||
|
background-color: rgb(var(--semantic-info));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-neutral {
|
||||||
|
background-color: rgb(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulsing indicator for live/active status */
|
||||||
|
.status-dot-pulse {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot-pulse::before {
|
||||||
|
content: "";
|
||||||
|
@apply absolute inset-0 rounded-full animate-ping;
|
||||||
|
background-color: inherit;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Keyboard Shortcut Styling
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.kbd {
|
||||||
|
@apply inline-flex items-center justify-center rounded px-1.5 py-0.5 text-xs font-medium;
|
||||||
|
background-color: rgb(var(--surface-2));
|
||||||
|
border: 1px solid rgb(var(--border-default));
|
||||||
|
color: rgb(var(--text-tertiary));
|
||||||
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
box-shadow: 0 1px 0 rgb(var(--border-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
.kbd-group {
|
||||||
|
@apply inline-flex items-center gap-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Table Styles - Dense & Professional
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.table-pro {
|
||||||
|
@apply w-full text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro thead {
|
||||||
|
@apply sticky top-0;
|
||||||
|
background-color: rgb(var(--surface-1));
|
||||||
|
border-bottom: 1px solid rgb(var(--border-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro th {
|
||||||
|
@apply px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider;
|
||||||
|
color: rgb(var(--text-tertiary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro th.sortable {
|
||||||
|
@apply cursor-pointer select-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro th.sortable:hover {
|
||||||
|
color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro tbody tr {
|
||||||
|
border-bottom: 1px solid rgb(var(--border-subtle));
|
||||||
|
transition: background-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro tbody tr:hover {
|
||||||
|
background-color: rgb(var(--surface-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro td {
|
||||||
|
@apply px-4 py-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-pro-dense td {
|
||||||
|
@apply py-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Skeleton Loading Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.skeleton {
|
||||||
|
@apply animate-pulse rounded;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgb(var(--surface-2)) 0%,
|
||||||
|
rgb(var(--surface-1)) 50%,
|
||||||
|
rgb(var(--surface-2)) 100%
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
@apply skeleton h-4 w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text-sm {
|
||||||
|
@apply skeleton h-3 w-3/4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
@apply skeleton h-10 w-10 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
@apply skeleton h-32 w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Modal & Dialog Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.modal-backdrop {
|
||||||
|
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
|
||||||
|
background-color: rgb(0 0 0 / 0.5);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
@apply relative max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-lg;
|
||||||
|
background-color: rgb(var(--surface-0));
|
||||||
|
border: 1px solid rgb(var(--border-default));
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
@apply flex items-center justify-between p-4 border-b;
|
||||||
|
border-color: rgb(var(--border-default));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
@apply flex items-center justify-end gap-3 p-4 border-t;
|
||||||
|
border-color: rgb(var(--border-default));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Tooltip Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@layer components {
|
||||||
|
.tooltip {
|
||||||
|
@apply absolute z-50 rounded px-2 py-1 text-xs font-medium;
|
||||||
|
background-color: rgb(var(--text-primary));
|
||||||
|
color: rgb(var(--color-background));
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip::before {
|
||||||
|
content: "";
|
||||||
|
@apply absolute;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-top::before {
|
||||||
|
@apply left-1/2 top-full -translate-x-1/2;
|
||||||
|
border-top-color: rgb(var(--text-primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Animations - Functional Only
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in {
|
||||||
|
animation: slideIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-in {
|
||||||
|
animation: scaleIn 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message animation - subtle for chat */
|
||||||
|
.message-animate {
|
||||||
|
animation: slideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu dropdown animation */
|
||||||
|
.animate-menu-enter {
|
||||||
|
animation: scaleIn 0.1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Responsive Typography Adjustments
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.text-display {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-heading-1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
High Contrast Mode Support
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
:root {
|
||||||
|
--border-default: 100 116 139;
|
||||||
|
--border-strong: 71 85 105;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--border-default: 148 163 184;
|
||||||
|
--border-strong: 203 213 225;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Reduced Motion Support
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------------------
|
||||||
|
Print Styles
|
||||||
|
----------------------------------------------------------------------------- */
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-print {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { AuthProvider } from "@/lib/auth/auth-context";
|
import { AuthProvider } from "@/lib/auth/auth-context";
|
||||||
import { ErrorBoundary } from "@/components/error-boundary";
|
import { ErrorBoundary } from "@/components/error-boundary";
|
||||||
|
import { ThemeProvider } from "@/providers/ThemeProvider";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -13,9 +14,11 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<ErrorBoundary>
|
<ThemeProvider>
|
||||||
<AuthProvider>{children}</AuthProvider>
|
<ErrorBoundary>
|
||||||
</ErrorBoundary>
|
<AuthProvider>{children}</AuthProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
47
apps/web/src/components/layout/ThemeToggle.tsx
Normal file
47
apps/web/src/components/layout/ThemeToggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
|
|
||||||
|
interface ThemeToggleProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeToggle({ className = "" }: ThemeToggleProps) {
|
||||||
|
const { resolvedTheme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className={`btn-ghost rounded-md p-2 ${className}`}
|
||||||
|
title={`Switch to ${resolvedTheme === "dark" ? "light" : "dark"} mode`}
|
||||||
|
aria-label={`Switch to ${resolvedTheme === "dark" ? "light" : "dark"} mode`}
|
||||||
|
>
|
||||||
|
{resolvedTheme === "dark" ? (
|
||||||
|
// Sun icon for dark mode (click to switch to light)
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
style={{ color: "rgb(var(--semantic-warning))" }}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
// Moon icon for light mode (click to switch to dark)
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
style={{ color: "rgb(var(--text-secondary))" }}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
apps/web/src/providers/ThemeProvider.tsx
Normal file
131
apps/web/src/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
type Theme = "light" | "dark" | "system";
|
||||||
|
|
||||||
|
interface ThemeContextValue {
|
||||||
|
theme: Theme;
|
||||||
|
resolvedTheme: "light" | "dark";
|
||||||
|
setTheme: (theme: Theme) => void;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
|
|
||||||
|
const STORAGE_KEY = "jarvis-theme";
|
||||||
|
|
||||||
|
function getSystemTheme(): "light" | "dark" {
|
||||||
|
if (typeof window === "undefined") return "dark";
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredTheme(): Theme {
|
||||||
|
if (typeof window === "undefined") return "system";
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored === "light" || stored === "dark" || stored === "system") {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
defaultTheme?: Theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setThemeState] = useState<Theme>(defaultTheme);
|
||||||
|
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("dark");
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
// Initialize theme from storage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
const storedTheme = getStoredTheme();
|
||||||
|
setThemeState(storedTheme);
|
||||||
|
setResolvedTheme(
|
||||||
|
storedTheme === "system" ? getSystemTheme() : storedTheme
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Apply theme class to html element
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
const root = document.documentElement;
|
||||||
|
const resolved = theme === "system" ? getSystemTheme() : theme;
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark");
|
||||||
|
root.classList.add(resolved);
|
||||||
|
setResolvedTheme(resolved);
|
||||||
|
}, [theme, mounted]);
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted || theme !== "system") return;
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const handleChange = (e: MediaQueryListEvent) => {
|
||||||
|
setResolvedTheme(e.matches ? "dark" : "light");
|
||||||
|
document.documentElement.classList.remove("light", "dark");
|
||||||
|
document.documentElement.classList.add(e.matches ? "dark" : "light");
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaQuery.addEventListener("change", handleChange);
|
||||||
|
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||||
|
}, [theme, mounted]);
|
||||||
|
|
||||||
|
const setTheme = useCallback((newTheme: Theme) => {
|
||||||
|
setThemeState(newTheme);
|
||||||
|
localStorage.setItem(STORAGE_KEY, newTheme);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
||||||
|
}, [resolvedTheme, setTheme]);
|
||||||
|
|
||||||
|
// Prevent flash by not rendering until mounted
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider
|
||||||
|
value={{
|
||||||
|
theme: defaultTheme,
|
||||||
|
resolvedTheme: "dark",
|
||||||
|
setTheme: () => {},
|
||||||
|
toggleTheme: () => {},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider
|
||||||
|
value={{ theme, resolvedTheme, setTheme, toggleTheme }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
1008
pnpm-lock.yaml
generated
1008
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user