feat: add theme system from jarvis frontend

This commit is contained in:
Jason Woltje
2026-01-29 21:45:18 -06:00
parent 532f5a39a0
commit af8f5df111
6 changed files with 1929 additions and 18 deletions

View File

@@ -15,11 +15,17 @@
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^9.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@mosaic/shared": "workspace:*",
"@mosaic/ui": "workspace:*",
"@tanstack/react-query": "^5.90.20",
"@xyflow/react": "^12.5.3",
"date-fns": "^4.1.0",
"elkjs": "^0.9.3",
"lucide-react": "^0.563.0",
"mermaid": "^11.4.1",
"next": "^16.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",

View File

@@ -2,19 +2,747 @@
@tailwind components;
@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 {
--foreground-rgb: 0, 0, 0;
--background-rgb: 255, 255, 255;
/* Base colors - increased contrast from surfaces */
--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 {
--foreground-rgb: 255, 255, 255;
--background-rgb: 0, 0, 0;
}
/* -----------------------------------------------------------------------------
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 */
--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 {
color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb));
color: rgb(var(--text-primary));
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;
}
}

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import type { ReactNode } from "react";
import { AuthProvider } from "@/lib/auth/auth-context";
import { ErrorBoundary } from "@/components/error-boundary";
import { ThemeProvider } from "@/providers/ThemeProvider";
import "./globals.css";
export const metadata: Metadata = {
@@ -13,9 +14,11 @@ export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<ErrorBoundary>
<AuthProvider>{children}</AuthProvider>
</ErrorBoundary>
<ThemeProvider>
<ErrorBoundary>
<AuthProvider>{children}</AuthProvider>
</ErrorBoundary>
</ThemeProvider>
</body>
</html>
);

View 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>
);
}

View 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

File diff suppressed because it is too large Load Diff