feat(web): MS15 Phase 1 — Design System & App Shell (#451)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #451.
This commit is contained in:
@@ -4,10 +4,79 @@ import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/lib/auth/auth-context";
|
||||
import { IS_MOCK_AUTH_MODE } from "@/lib/config";
|
||||
import { Navigation } from "@/components/layout/Navigation";
|
||||
import { AppHeader } from "@/components/layout/AppHeader";
|
||||
import { AppSidebar } from "@/components/layout/AppSidebar";
|
||||
import { SidebarProvider, useSidebar } from "@/components/layout/SidebarContext";
|
||||
import { ChatOverlay } from "@/components/chat";
|
||||
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SIDEBAR_EXPANDED_WIDTH = "240px";
|
||||
const SIDEBAR_COLLAPSED_WIDTH = "60px";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inner shell — must be a child of SidebarProvider to use useSidebar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface AppShellProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function AppShell({ children }: AppShellProps): React.JSX.Element {
|
||||
const { collapsed, isMobile } = useSidebar();
|
||||
|
||||
// On tablet (md–lg), hide sidebar from the grid when the sidebar is collapsed.
|
||||
// On mobile, the sidebar is fixed-position so the grid is always single-column.
|
||||
const sidebarHidden = !isMobile && collapsed;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="app-shell"
|
||||
data-sidebar-hidden={sidebarHidden ? "true" : undefined}
|
||||
style={
|
||||
{
|
||||
"--sidebar-w": collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_EXPANDED_WIDTH,
|
||||
transition: "grid-template-columns 0.2s var(--ease, ease)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{/* Full-width header — grid-column: 1 / -1 via .app-header CSS class */}
|
||||
<AppHeader />
|
||||
|
||||
{/* Sidebar — left column, row 2, via .app-sidebar CSS class */}
|
||||
<AppSidebar />
|
||||
|
||||
{/* Main content — right column, row 2, via .app-main CSS class */}
|
||||
<main className="app-main" id="main-content">
|
||||
{IS_MOCK_AUTH_MODE && (
|
||||
<div
|
||||
className="border-b px-4 py-2 text-xs font-medium flex-shrink-0"
|
||||
style={{
|
||||
borderColor: "var(--ms-amber-500)",
|
||||
background: "rgba(245, 158, 11, 0.08)",
|
||||
color: "var(--ms-amber-400)",
|
||||
}}
|
||||
data-testid="mock-auth-banner"
|
||||
>
|
||||
Mock Auth Mode (Local Only): Real authentication is bypassed for frontend development.
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto p-5">{children}</div>
|
||||
</main>
|
||||
|
||||
{!IS_MOCK_AUTH_MODE && <ChatOverlay />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Authenticated layout — handles auth guard + provides sidebar context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function AuthenticatedLayout({
|
||||
children,
|
||||
}: {
|
||||
@@ -23,11 +92,7 @@ export default function AuthenticatedLayout({
|
||||
}, [isAuthenticated, isLoading, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
|
||||
</div>
|
||||
);
|
||||
return <MosaicSpinner size={48} fullPage />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
@@ -35,20 +100,8 @@ export default function AuthenticatedLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navigation />
|
||||
<div className="pt-16">
|
||||
{IS_MOCK_AUTH_MODE && (
|
||||
<div
|
||||
className="border-b border-amber-300 bg-amber-50 px-4 py-2 text-sm text-amber-900"
|
||||
data-testid="mock-auth-banner"
|
||||
>
|
||||
Mock Auth Mode (Local Only): Real authentication is bypassed for frontend development.
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
{!IS_MOCK_AUTH_MODE && <ChatOverlay />}
|
||||
</div>
|
||||
<SidebarProvider>
|
||||
<AppShell>{children}</AppShell>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,147 +3,303 @@
|
||||
@tailwind utilities;
|
||||
|
||||
/* =============================================================================
|
||||
DESIGN C: PROFESSIONAL/ENTERPRISE DESIGN SYSTEM
|
||||
Philosophy: "Good design is as little design as possible." - Dieter Rams
|
||||
MOSAIC DESIGN SYSTEM — Reference token system from dashboard design
|
||||
============================================================================= */
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
CSS Custom Properties - Light Theme (Default)
|
||||
Primitive Tokens (Dark-first — dark is the default theme)
|
||||
----------------------------------------------------------------------------- */
|
||||
:root {
|
||||
/* Base colors - increased contrast from surfaces */
|
||||
--color-background: 245 247 250;
|
||||
--color-foreground: 15 23 42;
|
||||
/* Mosaic design tokens — dark palette (default) */
|
||||
--ms-bg-950: #080b12;
|
||||
--ms-bg-900: #0f141d;
|
||||
--ms-bg-850: #151b26;
|
||||
--ms-surface-800: #1b2331;
|
||||
--ms-surface-750: #232d3f;
|
||||
--ms-border-700: #2f3b52;
|
||||
--ms-text-100: #eef3ff;
|
||||
--ms-text-300: #c5d0e6;
|
||||
--ms-text-500: #8f9db7;
|
||||
--ms-blue-500: #2f80ff;
|
||||
--ms-blue-400: #56a0ff;
|
||||
--ms-red-500: #e5484d;
|
||||
--ms-red-400: #f06a6f;
|
||||
--ms-purple-500: #8b5cf6;
|
||||
--ms-purple-400: #a78bfa;
|
||||
--ms-teal-500: #14b8a6;
|
||||
--ms-teal-400: #2dd4bf;
|
||||
--ms-amber-500: #f59e0b;
|
||||
--ms-amber-400: #fbbf24;
|
||||
--ms-pink-500: #ec4899;
|
||||
--ms-emerald-500: #10b981;
|
||||
--ms-orange-500: #f97316;
|
||||
--ms-cyan-500: #06b6d4;
|
||||
--ms-indigo-500: #6366f1;
|
||||
|
||||
/* 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;
|
||||
/* Semantic aliases — dark theme is default */
|
||||
--bg: var(--ms-bg-900);
|
||||
--bg-deep: var(--ms-bg-950);
|
||||
--bg-mid: var(--ms-bg-850);
|
||||
--surface: var(--ms-surface-800);
|
||||
--surface-2: var(--ms-surface-750);
|
||||
--border: var(--ms-border-700);
|
||||
--text: var(--ms-text-100);
|
||||
--text-2: var(--ms-text-300);
|
||||
--muted: var(--ms-text-500);
|
||||
--primary: var(--ms-blue-500);
|
||||
--primary-l: var(--ms-blue-400);
|
||||
--danger: var(--ms-red-500);
|
||||
--success: var(--ms-teal-500);
|
||||
--warn: var(--ms-amber-500);
|
||||
--purple: var(--ms-purple-500);
|
||||
|
||||
/* Text hierarchy */
|
||||
--text-primary: 15 23 42;
|
||||
--text-secondary: 51 65 85;
|
||||
--text-tertiary: 71 85 105;
|
||||
--text-muted: 100 116 139;
|
||||
/* Typography */
|
||||
--font: var(--font-outfit, 'Outfit'), system-ui, sans-serif;
|
||||
--mono: var(--font-fira-code, 'Fira Code'), 'Cascadia Code', monospace;
|
||||
|
||||
/* Border colors - stronger borders for light mode */
|
||||
--border-default: 203 213 225;
|
||||
--border-subtle: 226 232 240;
|
||||
--border-strong: 148 163 184;
|
||||
/* Radius scale */
|
||||
--r: 8px;
|
||||
--r-sm: 5px;
|
||||
--r-lg: 12px;
|
||||
--r-xl: 16px;
|
||||
|
||||
/* 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;
|
||||
/* Layout dimensions */
|
||||
--sidebar-w: 260px;
|
||||
--topbar-h: 56px;
|
||||
--terminal-h: 220px;
|
||||
|
||||
/* Semantic colors - Success (Emerald) */
|
||||
--semantic-success: 16 185 129;
|
||||
--semantic-success-light: 209 250 229;
|
||||
--semantic-success-dark: 6 95 70;
|
||||
/* Easing */
|
||||
--ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
CSS Custom Properties - Dark Theme
|
||||
----------------------------------------------------------------------------- */
|
||||
.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 */
|
||||
/* Legacy shadow tokens (retained for component compat) */
|
||||
--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);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Light Theme Override — applied via data-theme attribute on <html>
|
||||
----------------------------------------------------------------------------- */
|
||||
[data-theme="light"] {
|
||||
--ms-bg-950: #f8faff;
|
||||
--ms-bg-900: #f0f4fc;
|
||||
--ms-bg-850: #e8edf8;
|
||||
--ms-surface-800: #dde4f2;
|
||||
--ms-surface-750: #d0d9ec;
|
||||
--ms-border-700: #b8c4de;
|
||||
--ms-text-100: #0f141d;
|
||||
--ms-text-300: #2f3b52;
|
||||
--ms-text-500: #5a6a87;
|
||||
|
||||
/* Re-alias semantics for light — identical structure, primitive tokens differ */
|
||||
--bg: var(--ms-bg-900);
|
||||
--bg-deep: var(--ms-bg-950);
|
||||
--bg-mid: var(--ms-bg-850);
|
||||
--surface: var(--ms-surface-800);
|
||||
--surface-2: var(--ms-surface-750);
|
||||
--border: var(--ms-border-700);
|
||||
--text: var(--ms-text-100);
|
||||
--text-2: var(--ms-text-300);
|
||||
--muted: var(--ms-text-500);
|
||||
|
||||
/* Lighter shadows for light mode */
|
||||
--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);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Base Styles
|
||||
----------------------------------------------------------------------------- */
|
||||
* {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 15px;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--text-primary));
|
||||
background: rgb(var(--color-background));
|
||||
font-size: 14px;
|
||||
font-family: var(--font);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Subtle grain/noise overlay for texture */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='1'/%3E%3C/svg%3E");
|
||||
opacity: 0.025;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Focus States - Accessible & Visible
|
||||
----------------------------------------------------------------------------- */
|
||||
@layer base {
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--ms-blue-400);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
: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: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
App Shell Grid Layout
|
||||
----------------------------------------------------------------------------- */
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: var(--sidebar-w) 1fr;
|
||||
grid-template-rows: var(--topbar-h) 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
background: var(--bg-deep);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
gap: 12px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
background: var(--bg-deep);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Responsive App Shell — Mobile (< 768px): single-column, sidebar as overlay
|
||||
----------------------------------------------------------------------------- */
|
||||
@media (max-width: 767px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: var(--topbar-h);
|
||||
bottom: 0;
|
||||
width: 240px;
|
||||
z-index: 150;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.app-sidebar[data-mobile-open="true"] {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.app-main {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
Responsive App Shell — Tablet (768px–1023px): sidebar toggleable, pushes content
|
||||
----------------------------------------------------------------------------- */
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
.app-shell[data-sidebar-hidden="true"] {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-shell[data-sidebar-hidden="true"] .app-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-shell[data-sidebar-hidden="true"] .app-main {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.app-shell[data-sidebar-hidden="true"] .app-header {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
@@ -182,102 +338,10 @@ body {
|
||||
}
|
||||
|
||||
.text-mono {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-family: var(--mono);
|
||||
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;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
@@ -292,40 +356,46 @@ body {
|
||||
|
||||
.btn-primary {
|
||||
@apply btn px-4 py-2;
|
||||
background-color: rgb(var(--accent-primary));
|
||||
background: linear-gradient(135deg, var(--ms-blue-500), var(--ms-purple-500));
|
||||
color: white;
|
||||
border-radius: var(--r);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: rgb(var(--accent-primary-hover));
|
||||
box-shadow: 0 8px 28px rgba(47, 128, 255, 0.38);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.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));
|
||||
background-color: var(--surface);
|
||||
color: var(--text-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: rgb(var(--surface-3));
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply btn px-3 py-2;
|
||||
background-color: transparent;
|
||||
color: rgb(var(--text-secondary));
|
||||
color: var(--muted);
|
||||
border-radius: var(--r);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background-color: rgb(var(--surface-2));
|
||||
color: rgb(var(--text-primary));
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
@apply btn px-4 py-2;
|
||||
background-color: rgb(var(--semantic-error));
|
||||
background-color: var(--danger);
|
||||
color: white;
|
||||
border-radius: var(--r);
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
@@ -346,34 +416,36 @@ body {
|
||||
----------------------------------------------------------------------------- */
|
||||
@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));
|
||||
@apply w-full text-sm transition-all duration-150;
|
||||
@apply focus:outline-none;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
color: var(--text);
|
||||
padding: 11px 14px;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: rgb(var(--text-muted));
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: rgb(var(--accent-primary));
|
||||
box-shadow: 0 0 0 3px rgb(var(--accent-primary) / 0.1);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(47, 128, 255, 0.12);
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
background-color: rgb(var(--surface-1));
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
border-color: rgb(var(--semantic-error));
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.input-error:focus {
|
||||
border-color: rgb(var(--semantic-error));
|
||||
box-shadow: 0 0 0 3px rgb(var(--semantic-error) / 0.1);
|
||||
border-color: var(--danger);
|
||||
box-shadow: 0 0 0 3px rgba(229, 72, 77, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,8 +455,8 @@ body {
|
||||
@layer components {
|
||||
.card {
|
||||
@apply rounded-lg p-4;
|
||||
background-color: rgb(var(--surface-0));
|
||||
border: 1px solid rgb(var(--border-default));
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
@@ -398,7 +470,7 @@ body {
|
||||
}
|
||||
|
||||
.card-interactive:hover {
|
||||
border-color: rgb(var(--border-strong));
|
||||
border-color: var(--muted);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
}
|
||||
@@ -412,33 +484,33 @@ body {
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: rgb(var(--semantic-success-light));
|
||||
color: rgb(var(--semantic-success-dark));
|
||||
background-color: rgba(20, 184, 166, 0.15);
|
||||
color: var(--ms-teal-400);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: rgb(var(--semantic-warning-light));
|
||||
color: rgb(var(--semantic-warning-dark));
|
||||
background-color: rgba(245, 158, 11, 0.15);
|
||||
color: var(--ms-amber-400);
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background-color: rgb(var(--semantic-error-light));
|
||||
color: rgb(var(--semantic-error-dark));
|
||||
background-color: rgba(229, 72, 77, 0.15);
|
||||
color: var(--ms-red-400);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: rgb(var(--semantic-info-light));
|
||||
color: rgb(var(--semantic-info-dark));
|
||||
background-color: rgba(47, 128, 255, 0.15);
|
||||
color: var(--ms-blue-400);
|
||||
}
|
||||
|
||||
.badge-neutral {
|
||||
background-color: rgb(var(--surface-2));
|
||||
color: rgb(var(--text-secondary));
|
||||
background-color: var(--surface-2);
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background-color: rgb(var(--accent-primary-light));
|
||||
color: rgb(var(--accent-primary));
|
||||
background-color: rgba(47, 128, 255, 0.15);
|
||||
color: var(--primary-l);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,26 +523,29 @@ body {
|
||||
}
|
||||
|
||||
.status-dot-success {
|
||||
background-color: rgb(var(--semantic-success));
|
||||
background-color: var(--success);
|
||||
box-shadow: 0 0 5px var(--success);
|
||||
}
|
||||
|
||||
.status-dot-warning {
|
||||
background-color: rgb(var(--semantic-warning));
|
||||
background-color: var(--warn);
|
||||
box-shadow: 0 0 5px var(--warn);
|
||||
}
|
||||
|
||||
.status-dot-error {
|
||||
background-color: rgb(var(--semantic-error));
|
||||
background-color: var(--danger);
|
||||
box-shadow: 0 0 5px var(--danger);
|
||||
}
|
||||
|
||||
.status-dot-info {
|
||||
background-color: rgb(var(--semantic-info));
|
||||
background-color: var(--primary);
|
||||
box-shadow: 0 0 5px var(--primary);
|
||||
}
|
||||
|
||||
.status-dot-neutral {
|
||||
background-color: rgb(var(--text-muted));
|
||||
background-color: var(--muted);
|
||||
}
|
||||
|
||||
/* Pulsing indicator for live/active status */
|
||||
.status-dot-pulse {
|
||||
@apply relative;
|
||||
}
|
||||
@@ -489,12 +564,12 @@ body {
|
||||
@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;
|
||||
background-color: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
min-width: 1.5rem;
|
||||
box-shadow: 0 1px 0 rgb(var(--border-strong));
|
||||
box-shadow: 0 1px 0 var(--border);
|
||||
}
|
||||
|
||||
.kbd-group {
|
||||
@@ -512,13 +587,13 @@ body {
|
||||
|
||||
.table-pro thead {
|
||||
@apply sticky top-0;
|
||||
background-color: rgb(var(--surface-1));
|
||||
border-bottom: 1px solid rgb(var(--border-default));
|
||||
background-color: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table-pro th {
|
||||
@apply px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider;
|
||||
color: rgb(var(--text-tertiary));
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table-pro th.sortable {
|
||||
@@ -526,16 +601,16 @@ body {
|
||||
}
|
||||
|
||||
.table-pro th.sortable:hover {
|
||||
color: rgb(var(--text-primary));
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.table-pro tbody tr {
|
||||
border-bottom: 1px solid rgb(var(--border-subtle));
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.table-pro tbody tr:hover {
|
||||
background-color: rgb(var(--surface-1));
|
||||
background-color: var(--surface);
|
||||
}
|
||||
|
||||
.table-pro td {
|
||||
@@ -555,9 +630,9 @@ body {
|
||||
@apply animate-pulse rounded;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(var(--surface-2)) 0%,
|
||||
rgb(var(--surface-1)) 50%,
|
||||
rgb(var(--surface-2)) 100%
|
||||
var(--surface) 0%,
|
||||
var(--surface-2) 50%,
|
||||
var(--surface) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
@@ -590,15 +665,16 @@ body {
|
||||
}
|
||||
|
||||
.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));
|
||||
@apply relative max-h-[90vh] w-full max-w-lg overflow-y-auto;
|
||||
background-color: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@apply flex items-center justify-between p-4 border-b;
|
||||
border-color: rgb(var(--border-default));
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
@@ -607,7 +683,7 @@ body {
|
||||
|
||||
.modal-footer {
|
||||
@apply flex items-center justify-end gap-3 p-4 border-t;
|
||||
border-color: rgb(var(--border-default));
|
||||
border-color: var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,9 +693,10 @@ body {
|
||||
@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));
|
||||
background-color: var(--text);
|
||||
color: var(--bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
|
||||
.tooltip::before {
|
||||
@@ -630,7 +707,7 @@ body {
|
||||
|
||||
.tooltip-top::before {
|
||||
@apply left-1/2 top-full -translate-x-1/2;
|
||||
border-top-color: rgb(var(--text-primary));
|
||||
border-top-color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,12 +757,10 @@ body {
|
||||
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;
|
||||
}
|
||||
@@ -710,13 +785,8 @@ body {
|
||||
----------------------------------------------------------------------------- */
|
||||
@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;
|
||||
--border: #4a5a78;
|
||||
--muted: #a0b0cc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import { Outfit, Fira_Code } from "next/font/google";
|
||||
import { AuthProvider } from "@/lib/auth/auth-context";
|
||||
import { ErrorBoundary } from "@/components/error-boundary";
|
||||
import { ThemeProvider } from "@/providers/ThemeProvider";
|
||||
@@ -12,6 +13,18 @@ export const metadata: Metadata = {
|
||||
description: "Mosaic Stack Web Application",
|
||||
};
|
||||
|
||||
const outfit = Outfit({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-outfit",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const firaCode = Fira_Code({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-fira-code",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
/**
|
||||
* Runtime env vars injected as a synchronous script so client-side modules
|
||||
* can read them before React hydration. This allows Docker env vars to
|
||||
@@ -34,7 +47,7 @@ function runtimeEnvScript(): string {
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }): React.JSX.Element {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" className={`${outfit.variable} ${firaCode.variable}`}>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: runtimeEnvScript() }} />
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user