@object-ui/runner 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Object UI React Example</title>
8
+ <script type="module" crossorigin src="/assets/index-CW0j9o4j.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BAf0tooB.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Object UI React Example</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@object-ui/runner",
3
+ "private": false,
4
+ "version": "0.3.0",
5
+ "description": "Universal Object UI Application Runner",
6
+ "type": "module",
7
+ "dependencies": {
8
+ "class-variance-authority": "^0.7.1",
9
+ "clsx": "^2.1.0",
10
+ "lucide-react": "^0.562.0",
11
+ "react": "^18.2.0",
12
+ "react-dom": "^18.2.0",
13
+ "tailwind-merge": "^2.2.1",
14
+ "tailwindcss-animate": "^1.0.7",
15
+ "@object-ui/components": "0.3.0",
16
+ "@object-ui/core": "0.3.0",
17
+ "@object-ui/react": "0.3.0",
18
+ "@object-ui/types": "0.3.0",
19
+ "@object-ui/plugin-kanban": "0.3.0",
20
+ "@object-ui/plugin-charts": "0.3.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^18.2.66",
24
+ "@types/react-dom": "^18.2.22",
25
+ "@vitejs/plugin-react": "^4.2.1",
26
+ "autoprefixer": "^10.4.19",
27
+ "postcss": "^8.4.38",
28
+ "tailwindcss": "^3.4.1",
29
+ "typescript": "^5.2.2",
30
+ "vite": "^5.2.0"
31
+ },
32
+ "scripts": {
33
+ "dev": "vite",
34
+ "build": "vite build",
35
+ "preview": "vite preview"
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,149 @@
1
+ // Ensure this file is treated as a module
2
+ export {};
3
+
4
+ import { SchemaRenderer } from '@object-ui/react';
5
+ import '@object-ui/components';
6
+ import '@object-ui/plugin-kanban';
7
+ import '@object-ui/plugin-charts';
8
+ import { PageSchema, AppSchema } from '@object-ui/types';
9
+ import { useState, useEffect, useMemo, useCallback } from 'react';
10
+ import { LayoutRenderer } from './LayoutRenderer';
11
+ import { LocalBundleLoader, NetworkLoader, MetadataLoader } from './lib/MetadataLoader';
12
+
13
+ export default function App() {
14
+ const [appConfig, setAppConfig] = useState<AppSchema | null>(null);
15
+ const [pageSchema, setPageSchema] = useState<PageSchema | null>(null);
16
+ const [currentPath, setCurrentPath] = useState(window.location.pathname);
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [loading, setLoading] = useState(true);
19
+
20
+ // Initialize Loader Strategy
21
+ const loader = useMemo<MetadataLoader>(() => {
22
+ const params = new URLSearchParams(window.location.search);
23
+ const apiUrl = params.get('api');
24
+
25
+ // IF ?api=... is present, use Network Loader
26
+ if (apiUrl) {
27
+ console.log('🔌 Using Network Loader:', apiUrl);
28
+ return new NetworkLoader(apiUrl);
29
+ }
30
+
31
+ // ELSE use bundled files (Local Development)
32
+ console.log('📦 Using Local Bundle Loader');
33
+ return new LocalBundleLoader();
34
+ }, []);
35
+
36
+ // --- 1. Load Global Config (once) ---
37
+ useEffect(() => {
38
+ const loadApp = async () => {
39
+ try {
40
+ const config = await loader.loadAppConfig();
41
+ if (config) setAppConfig(config);
42
+ } catch (e) {
43
+ console.error("Error loading app config", e);
44
+ }
45
+ };
46
+ loadApp();
47
+ }, [loader]);
48
+
49
+ // --- 2. Route Handling ---
50
+ const handleNavigate = useCallback((to: string) => {
51
+ window.history.pushState({}, '', to);
52
+ setCurrentPath(to);
53
+ window.scrollTo(0, 0);
54
+ }, []);
55
+
56
+ useEffect(() => {
57
+ const onPopState = () => setCurrentPath(window.location.pathname);
58
+ window.addEventListener('popstate', onPopState);
59
+ return () => window.removeEventListener('popstate', onPopState);
60
+ }, []);
61
+
62
+ // --- 3. Page Loading Logic ---
63
+ useEffect(() => {
64
+ const loadPage = async () => {
65
+ setLoading(true);
66
+ setError(null);
67
+ try {
68
+ const schema = await loader.loadPage(currentPath);
69
+ if (schema) {
70
+ setPageSchema(schema);
71
+ } else {
72
+ // If 404
73
+ setError(`Page not found: ${currentPath}`);
74
+ if (currentPath === '/') {
75
+ setPageSchema({
76
+ type: 'page',
77
+ title: 'Welcome to Object UI',
78
+ body: [{ type: 'div', className: "p-10 text-center text-muted-foreground", body: 'No index page found.' }]
79
+ } as any);
80
+ } else {
81
+ setPageSchema(null);
82
+ }
83
+ }
84
+ } catch (err) {
85
+ console.error(err);
86
+ setError("Failed to load page.");
87
+ } finally {
88
+ setLoading(false);
89
+ }
90
+ };
91
+
92
+ loadPage();
93
+ }, [currentPath, loader]);
94
+
95
+ // --- Render ---
96
+
97
+ // 1. Initial App Boot: Show Full Loader (only if app config isn't ready)
98
+ if (!appConfig && loading) {
99
+ return (
100
+ <div className="flex h-screen items-center justify-center text-slate-400">
101
+ <div className="h-6 w-6 animate-spin rounded-full border-2 border-slate-200 border-t-slate-600 mr-2" />
102
+ Loading App...
103
+ </div>
104
+ );
105
+ }
106
+
107
+ // 2. Prepare Main Content (Page)
108
+ let mainContent;
109
+
110
+ if (loading) {
111
+ // Page transition loading - shows INSIDE layout
112
+ mainContent = (
113
+ <div className="flex flex-col items-center justify-center h-full text-slate-400 min-h-[50vh]">
114
+ <div className="h-6 w-6 animate-spin rounded-full border-2 border-slate-200 border-t-slate-600 mr-2" />
115
+ Loading Page...
116
+ </div>
117
+ );
118
+ } else if (error || !pageSchema) {
119
+ // Error State
120
+ mainContent = (
121
+ <div className="flex flex-col items-center justify-center h-full p-12 text-red-600">
122
+ <h1 className="text-2xl font-bold">404</h1>
123
+ <p className="mt-2 text-slate-600">{error || 'Page not found'}</p>
124
+ <button onClick={() => handleNavigate('/')} className="mt-4 text-blue-600 hover:underline">
125
+ Go Home
126
+ </button>
127
+ </div>
128
+ );
129
+ } else {
130
+ // Success State
131
+ mainContent = <SchemaRenderer schema={pageSchema} />;
132
+ }
133
+
134
+ // 3. Render Wrapper
135
+ if (appConfig) {
136
+ return (
137
+ <LayoutRenderer app={appConfig} currentPath={currentPath} onNavigate={handleNavigate}>
138
+ {mainContent}
139
+ </LayoutRenderer>
140
+ );
141
+ }
142
+
143
+ // Fallback if no appConfig but somehow done loading (e.g. error loading app.json)
144
+ return (
145
+ <div className="object-ui-app">
146
+ {mainContent}
147
+ </div>
148
+ );
149
+ }
@@ -0,0 +1,312 @@
1
+ import React from 'react';
2
+ import type { AppSchema } from '@object-ui/types';
3
+ import * as LucideIcons from 'lucide-react';
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuGroup,
8
+ DropdownMenuItem,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ DropdownMenuShortcut,
13
+ Avatar,
14
+ AvatarImage,
15
+ AvatarFallback,
16
+ Collapsible,
17
+ CollapsibleContent,
18
+ CollapsibleTrigger
19
+ } from '@object-ui/components';
20
+
21
+ interface LayoutRendererProps {
22
+ app: AppSchema;
23
+ children: React.ReactNode;
24
+ currentPath?: string;
25
+ onNavigate?: (path: string) => void;
26
+ }
27
+
28
+ // Helper to resolve icon from string name (e.g. "bar-chart" -> "BarChart")
29
+ const getIcon = (name?: string) => {
30
+ if (!name) return null;
31
+
32
+ // 1. Try direct match (e.g. "Home")
33
+ if ((LucideIcons as any)[name]) return (LucideIcons as any)[name];
34
+
35
+ // 2. Try PascalCase (e.g. "bar-chart" -> "BarChart")
36
+ const pascalName = name.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('');
37
+ if ((LucideIcons as any)[pascalName]) return (LucideIcons as any)[pascalName];
38
+
39
+ return LucideIcons.Circle; // Fallback
40
+ };
41
+
42
+ const NavItem = ({ item, currentPath, isSidebarOpen, onNavigate, level = 0 }: any) => {
43
+ const isActive = currentPath === item.path;
44
+ const hasActiveChild = item.children?.some((child: any) => child.path === currentPath);
45
+ const [isOpen, setIsOpen] = React.useState(hasActiveChild);
46
+ const Icon = getIcon(item.icon);
47
+
48
+ // Auto-expand if child is active
49
+ React.useEffect(() => {
50
+ if (hasActiveChild) setIsOpen(true);
51
+ }, [hasActiveChild]);
52
+
53
+ if (item.children && item.children.length > 0) {
54
+ return (
55
+ <Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full">
56
+ <CollapsibleTrigger className={`flex w-full items-center justify-between py-2 text-sm font-medium rounded-md transition-colors text-muted-foreground hover:bg-muted hover:text-foreground ${isSidebarOpen ? 'px-3' : 'justify-center px-2 cursor-pointer'}`}>
57
+ <div className="flex items-center overflow-hidden">
58
+ {Icon && <Icon className={`h-4 w-4 flex-shrink-0 ${isSidebarOpen ? 'mr-3' : ''}`} />}
59
+ <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isSidebarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden'}`}>
60
+ {item.label}
61
+ </span>
62
+ </div>
63
+ {isSidebarOpen && (
64
+ <LucideIcons.ChevronDown className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
65
+ )}
66
+ </CollapsibleTrigger>
67
+ <CollapsibleContent className="space-y-1 overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
68
+ {isSidebarOpen && item.children.map((child: any, idx: number) => (
69
+ <NavItem
70
+ key={idx}
71
+ item={child}
72
+ currentPath={currentPath}
73
+ isSidebarOpen={isSidebarOpen}
74
+ onNavigate={onNavigate}
75
+ level={level + 1}
76
+ />
77
+ ))}
78
+ </CollapsibleContent>
79
+ </Collapsible>
80
+ );
81
+ }
82
+
83
+ return (
84
+ <a
85
+ href={item.path || '#'}
86
+ onClick={(e) => item.path && onNavigate(e, item.path)}
87
+ title={!isSidebarOpen ? item.label : undefined}
88
+ className={`flex items-center py-2 text-sm font-medium rounded-md transition-colors ${
89
+ isActive
90
+ ? 'bg-primary text-primary-foreground'
91
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'
92
+ } ${isSidebarOpen ? 'px-3' : 'justify-center px-2'} ${level > 0 && isSidebarOpen ? 'pl-10' : ''}`}
93
+ >
94
+ {Icon && <Icon className={`h-4 w-4 flex-shrink-0 ${isSidebarOpen ? 'mr-3' : ''} ${isActive ? 'text-primary-foreground' : 'text-muted-foreground group-hover:text-foreground'}`} />}
95
+ <span className={`whitespace-nowrap overflow-hidden transition-all duration-300 ${isSidebarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden'}`}>
96
+ {item.label}
97
+ </span>
98
+ </a>
99
+ );
100
+ };
101
+
102
+ export const LayoutRenderer = ({ app, children, currentPath, onNavigate }: LayoutRendererProps) => {
103
+ const layout = app.layout || 'sidebar';
104
+ const [isSidbarOpen, setSidebarOpen] = React.useState(true);
105
+
106
+ // Theme management
107
+ const [theme, setTheme] = React.useState<"light" | "dark">("light");
108
+
109
+ React.useEffect(() => {
110
+ const isDark = localStorage.getItem('theme') === 'dark' ||
111
+ (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
112
+ setTheme(isDark ? 'dark' : 'light');
113
+ }, []);
114
+
115
+ React.useEffect(() => {
116
+ if (theme === 'dark') {
117
+ document.documentElement.classList.add('dark');
118
+ localStorage.setItem('theme', 'dark');
119
+ } else {
120
+ document.documentElement.classList.remove('dark');
121
+ localStorage.setItem('theme', 'light');
122
+ }
123
+ }, [theme]);
124
+
125
+ const toggleTheme = () => {
126
+ setTheme(prev => prev === 'dark' ? 'light' : 'dark');
127
+ };
128
+
129
+ const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, path: string) => {
130
+ e.preventDefault();
131
+ if (onNavigate) {
132
+ onNavigate(path);
133
+ } else {
134
+ window.location.href = path;
135
+ }
136
+ };
137
+
138
+ if (layout === 'empty') {
139
+ return <main className={app.className}>{children}</main>;
140
+ }
141
+
142
+ const LogoIcon = app.logo && !app.logo.includes('/') && !app.logo.includes('.') ? getIcon(app.logo) : null;
143
+
144
+ return (
145
+ <div className={`flex min-h-screen w-full bg-slate-50/50 dark:bg-zinc-950 ${app.className || ''}`}>
146
+ {/* Sidebar - Only if configured */}
147
+ {layout === 'sidebar' && (
148
+ <aside
149
+ className={`
150
+ flex-shrink-0 border-r bg-background hidden md:flex flex-col h-screen sticky top-0 z-30 transition-all duration-300 ease-in-out
151
+ ${isSidbarOpen ? 'w-64' : 'w-[70px]'}
152
+ `}
153
+ >
154
+ <div className={`h-14 flex items-center border-b font-semibold text-lg tracking-tight transition-all ${isSidbarOpen ? 'px-6' : 'justify-center px-0'}`}>
155
+ {LogoIcon ? (
156
+ <LogoIcon className="h-6 w-6" />
157
+ ) : app.logo ? (
158
+ <img src={app.logo} alt={app.title} className="h-6 w-auto" />
159
+ ) : <LucideIcons.Box className="h-6 w-6" />}
160
+
161
+ <span className={`ml-2 whitespace-nowrap overflow-hidden transition-all duration-300 ${isSidbarOpen ? 'opacity-100 w-auto' : 'opacity-0 w-0 hidden'}`}>
162
+ {app.title || app.name || 'Object UI'}
163
+ </span>
164
+ </div>
165
+ <nav className="flex-1 p-2 space-y-1 overflow-y-auto overflow-x-hidden">
166
+ {app.menu?.map((item, index) => (
167
+ <NavItem
168
+ key={index}
169
+ item={item}
170
+ currentPath={currentPath}
171
+ isSidebarOpen={isSidbarOpen}
172
+ onNavigate={handleNavClick}
173
+ />
174
+ ))}
175
+ </nav>
176
+ {app.version && isSidbarOpen && (
177
+ <div className="p-4 border-t text-xs text-muted-foreground">
178
+ v{app.version}
179
+ </div>
180
+ )}
181
+ </aside>
182
+ )}
183
+
184
+ {/* Main Content Area */}
185
+ <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
186
+ {/* Header - Always shown in sidebar/header layouts */}
187
+ <header className="h-14 flex items-center justify-between px-4 md:px-6 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b z-20 sticky top-0">
188
+ <div className="flex items-center gap-4">
189
+ {/* Toggle Sidebar Button */}
190
+ <button
191
+ onClick={() => setSidebarOpen(!isSidbarOpen)}
192
+ className="p-2 -ml-2 text-muted-foreground hover:bg-muted hover:text-foreground rounded-md transition-colors"
193
+ >
194
+ <LucideIcons.Menu className="h-5 w-5" />
195
+ </button>
196
+
197
+ {/* Breadcrumbs placeholder or Search */}
198
+ <div className="relative hidden md:block w-96">
199
+ <LucideIcons.Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
200
+ <input
201
+ type="text"
202
+ placeholder="Search..."
203
+ className="w-full h-9 pl-9 pr-4 rounded-md border border-input bg-background text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
204
+ />
205
+ </div>
206
+ </div>
207
+ <div className="flex items-center gap-2">
208
+ {/* Theme Toggle */}
209
+ <button
210
+ onClick={toggleTheme}
211
+ className="p-2 text-muted-foreground hover:text-foreground transition-colors rounded-md hover:bg-muted"
212
+ title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
213
+ >
214
+ {theme === 'dark' ? <LucideIcons.Sun className="h-5 w-5" /> : <LucideIcons.Moon className="h-5 w-5" />}
215
+ </button>
216
+
217
+ {/* Global Actions */}
218
+ {app.actions?.filter(a => a.type === 'button').map((action, i) => {
219
+ const Icon = action.icon ? getIcon(action.icon) : null;
220
+ return (
221
+ <button
222
+ key={i}
223
+ className={action.variant === 'ghost' ? "relative p-2 text-muted-foreground hover:text-foreground transition-colors hover:bg-muted rounded-md" : "p-2"}
224
+ title={action.label}
225
+ >
226
+ {Icon && <Icon className="h-5 w-5" />}
227
+ {action.label && !action.icon && <span>{action.label}</span>}
228
+ </button>
229
+ );
230
+ })}
231
+
232
+ {/* Fallback Bell if no actions defined, or keep it as specific logic?
233
+ The original code hardcoded a Bell button.
234
+ The app.json defines a 'Bell' button action.
235
+ So I should iterate app.actions for buttons as well.
236
+ */}
237
+
238
+ {/* Original Bell Logic (Hardcoded in user request? No, it was hardcoded in my previous edit, but app.json has it too)
239
+ Let's check app.json. It has:
240
+ { "type": "button", "variant": "ghost", "size": "icon", "icon": "Bell" }
241
+
242
+ If I render actions generically, I don't need the hardcoded Bell.
243
+ */}
244
+
245
+ {(!app.actions || !app.actions.some(a => a.type === 'button')) && (
246
+ <button className="relative p-2 text-muted-foreground hover:text-foreground transition-colors">
247
+ <LucideIcons.Bell className="h-5 w-5" />
248
+ <span className="absolute top-1.5 right-1.5 h-2 w-2 bg-red-600 rounded-full border-2 border-background"></span>
249
+ </button>
250
+ )}
251
+
252
+ {app.actions?.filter(a => a.type === 'user').map((userAction, i) => (
253
+ <DropdownMenu key={i}>
254
+ <DropdownMenuTrigger asChild>
255
+ <button className="relative h-8 w-8 rounded-full border bg-muted overflow-hidden focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 hover:opacity-90 transition-opacity">
256
+ <Avatar className="h-full w-full">
257
+ <AvatarImage
258
+ src={userAction.avatar}
259
+ alt={userAction.label || 'User'}
260
+ />
261
+ <AvatarFallback>
262
+ {userAction.label?.substring(0, 2).toUpperCase() || 'JD'}
263
+ </AvatarFallback>
264
+ </Avatar>
265
+ </button>
266
+ </DropdownMenuTrigger>
267
+ <DropdownMenuContent className="w-56" align="end" forceMount>
268
+ <DropdownMenuLabel className="font-normal">
269
+ <div className="flex flex-col space-y-1">
270
+ <p className="text-sm font-medium leading-none">{userAction.label || 'User'}</p>
271
+ <p className="text-xs leading-none text-muted-foreground">
272
+ {userAction.description || 'user@example.com'}
273
+ </p>
274
+ </div>
275
+ </DropdownMenuLabel>
276
+ <DropdownMenuSeparator />
277
+ <DropdownMenuGroup>
278
+ {userAction.items?.map((item, idx) => {
279
+ if (item.type === 'separator') {
280
+ return <DropdownMenuSeparator key={idx} />;
281
+ }
282
+ return (
283
+ <DropdownMenuItem key={idx} onSelect={() => {
284
+ if ((item as any).onClick) {
285
+ // Handle click logic
286
+ console.log('Clicked', item.label);
287
+ }
288
+ }}>
289
+ {item.label}
290
+ {(item as any).shortcut && (
291
+ <DropdownMenuShortcut>{(item as any).shortcut}</DropdownMenuShortcut>
292
+ )}
293
+ </DropdownMenuItem>
294
+ );
295
+ })}
296
+ </DropdownMenuGroup>
297
+ </DropdownMenuContent>
298
+ </DropdownMenu>
299
+ ))}
300
+ </div>
301
+ </header>
302
+
303
+ {/* Page Content */}
304
+ <main className="flex-1 overflow-auto p-4 md:p-8 scroll-smooth">
305
+ <div className="mx-auto max-w-7xl animate-in fade-in slide-in-from-bottom-4 duration-500">
306
+ {children}
307
+ </div>
308
+ </main>
309
+ </div>
310
+ </div>
311
+ );
312
+ };
package/src/index.css ADDED
@@ -0,0 +1,76 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+
10
+ --card: 0 0% 100%;
11
+ --card-foreground: 222.2 84% 4.9%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 222.2 84% 4.9%;
15
+
16
+ --primary: 222.2 47.4% 11.2%;
17
+ --primary-foreground: 210 40% 98%;
18
+
19
+ --secondary: 210 40% 96.1%;
20
+ --secondary-foreground: 222.2 47.4% 11.2%;
21
+
22
+ --muted: 210 40% 96.1%;
23
+ --muted-foreground: 215.4 16.3% 46.9%;
24
+
25
+ --accent: 210 40% 96.1%;
26
+ --accent-foreground: 222.2 47.4% 11.2%;
27
+
28
+ --destructive: 0 84.2% 60.2%;
29
+ --destructive-foreground: 210 40% 98%;
30
+
31
+ --border: 214.3 31.8% 91.4%;
32
+ --input: 214.3 31.8% 91.4%;
33
+ --ring: 222.2 84% 4.9%;
34
+
35
+ --radius: 0.5rem;
36
+ }
37
+
38
+ .dark {
39
+ --background: 222.2 84% 4.9%;
40
+ --foreground: 210 40% 98%;
41
+
42
+ --card: 222.2 84% 4.9%;
43
+ --card-foreground: 210 40% 98%;
44
+
45
+ --popover: 222.2 84% 4.9%;
46
+ --popover-foreground: 210 40% 98%;
47
+
48
+ --primary: 210 40% 98%;
49
+ --primary-foreground: 222.2 47.4% 11.2%;
50
+
51
+ --secondary: 217.2 32.6% 17.5%;
52
+ --secondary-foreground: 210 40% 98%;
53
+
54
+ --muted: 217.2 32.6% 17.5%;
55
+ --muted-foreground: 215 20.2% 65.1%;
56
+
57
+ --accent: 217.2 32.6% 17.5%;
58
+ --accent-foreground: 210 40% 98%;
59
+
60
+ --destructive: 0 62.8% 30.6%;
61
+ --destructive-foreground: 210 40% 98%;
62
+
63
+ --border: 217.2 32.6% 17.5%;
64
+ --input: 217.2 32.6% 17.5%;
65
+ --ring: 212.7 26.8% 83.9%;
66
+ }
67
+ }
68
+
69
+ @layer base {
70
+ * {
71
+ @apply border-border;
72
+ }
73
+ body {
74
+ @apply bg-background text-foreground;
75
+ }
76
+ }