diff --git a/THEME_JSON_SYSTEM.md b/THEME_JSON_SYSTEM.md
new file mode 100644
index 0000000..f7d258a
--- /dev/null
+++ b/THEME_JSON_SYSTEM.md
@@ -0,0 +1,195 @@
+# Theme JSON System Implementation
+
+## Overview
+Implemented a comprehensive theme configuration system that loads styling from `theme.json`, making the application's visual appearance highly configurable without code changes.
+
+## What Was Fixed
+1. **Sidebar Styling Issues**: Fixed messed up sidebar styling by implementing proper theme loading and CSS variable management
+2. **Theme JSON Loading**: Created a robust theme loading system that reads from `theme.json`
+3. **CSS Variables**: Properly configured CSS custom properties for sidebar and application theming
+4. **Type Safety**: Added TypeScript interfaces for theme configuration
+
+## Files Changed
+
+### New Files
+- `/src/hooks/use-theme-config.ts` - Hook for loading and applying theme configuration
+- Updated `/theme.json` - Comprehensive theme configuration with sidebar settings
+
+### Modified Files
+- `/src/components/ui/sidebar.tsx` - Now uses theme config for sidebar dimensions
+- `/src/index.css` - Added sidebar CSS variables and component classes
+- `/src/hooks/index.ts` - Exported the new `useThemeConfig` hook
+
+## Theme Configuration Structure
+
+The `theme.json` file supports the following structure:
+
+```json
+{
+ "sidebar": {
+ "width": "16rem",
+ "widthMobile": "18rem",
+ "widthIcon": "3rem",
+ "backgroundColor": "oklch(0.19 0.02 265)",
+ "foregroundColor": "oklch(0.95 0.01 265)",
+ "borderColor": "oklch(0.28 0.03 265)",
+ "accentColor": "oklch(0.58 0.24 265)",
+ "accentForeground": "oklch(1 0 0)",
+ "hoverBackground": "oklch(0.25 0.03 265)",
+ "activeBackground": "oklch(0.30 0.04 265)",
+ "headerHeight": "4rem",
+ "transitionDuration": "200ms",
+ "zIndex": 40
+ },
+ "colors": {
+ "background": "oklch(...)",
+ "foreground": "oklch(...)",
+ "primary": "oklch(...)",
+ ...
+ },
+ "spacing": {
+ "radius": "0.5rem"
+ },
+ "typography": {
+ "fontFamily": {
+ "body": "'IBM Plex Sans', sans-serif",
+ "heading": "'JetBrains Mono', monospace",
+ "code": "'JetBrains Mono', monospace"
+ }
+ }
+}
+```
+
+## How It Works
+
+### 1. Theme Loading
+The `useThemeConfig` hook:
+- Fetches `/theme.json` on mount
+- Falls back to sensible defaults if loading fails
+- Returns loading state for conditional rendering
+
+### 2. CSS Variable Application
+When theme config loads, it automatically sets CSS custom properties:
+- `--sidebar-width`
+- `--sidebar-bg`
+- `--sidebar-fg`
+- `--sidebar-border`
+- And many more...
+
+### 3. Component Usage
+The sidebar component uses these CSS variables:
+```tsx
+const { themeConfig } = useThemeConfig()
+const sidebarWidth = themeConfig.sidebar?.width || '16rem'
+```
+
+## Benefits
+
+1. **Easy Customization**: Change colors, sizes, and spacing without touching code
+2. **Consistent Theming**: Single source of truth for design tokens
+3. **Runtime Updates**: Theme can be modified without rebuilding
+4. **Type Safety**: Full TypeScript support with interfaces
+5. **Graceful Fallbacks**: Defaults ensure app works even if theme.json is missing
+
+## Usage Examples
+
+### Changing Sidebar Width
+Edit `theme.json`:
+```json
+{
+ "sidebar": {
+ "width": "20rem"
+ }
+}
+```
+
+### Changing Color Scheme
+```json
+{
+ "sidebar": {
+ "backgroundColor": "oklch(0.25 0.05 280)",
+ "accentColor": "oklch(0.65 0.25 200)"
+ }
+}
+```
+
+### Using in Custom Components
+```tsx
+import { useThemeConfig } from '@/hooks/use-theme-config'
+
+function MyComponent() {
+ const { themeConfig, isLoading } = useThemeConfig()
+
+ if (isLoading) return
+
+ return (
+
(
@@ -133,8 +128,8 @@ function SidebarProvider({
data-slot="sidebar-wrapper"
style={
{
- "--sidebar-width": SIDEBAR_WIDTH,
- "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
+ "--sidebar-width": sidebarWidth,
+ "--sidebar-width-icon": sidebarWidthIcon,
...style,
} as CSSProperties
}
@@ -164,6 +159,9 @@ function Sidebar({
collapsible?: "offcanvas" | "icon" | "none"
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+ const { themeConfig } = useThemeConfig()
+
+ const sidebarWidthMobile = themeConfig.sidebar?.widthMobile || '18rem'
if (collapsible === "none") {
return (
@@ -190,7 +188,7 @@ function Sidebar({
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
- "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
+ "--sidebar-width": sidebarWidthMobile,
} as CSSProperties
}
side={side}
@@ -214,7 +212,6 @@ function Sidebar({
data-side={side}
data-slot="sidebar"
>
- {/* This is what handles the sidebar gap on desktop */}
+ spacing?: {
+ radius?: string
+ }
+ typography?: {
+ fontFamily?: {
+ body?: string
+ heading?: string
+ code?: string
+ }
+ }
+}
+
+const DEFAULT_THEME: ThemeConfig = {
+ sidebar: {
+ width: '16rem',
+ widthMobile: '18rem',
+ widthIcon: '3rem',
+ backgroundColor: 'oklch(0.19 0.02 265)',
+ foregroundColor: 'oklch(0.95 0.01 265)',
+ borderColor: 'oklch(0.28 0.03 265)',
+ accentColor: 'oklch(0.58 0.24 265)',
+ accentForeground: 'oklch(1 0 0)',
+ hoverBackground: 'oklch(0.25 0.03 265)',
+ activeBackground: 'oklch(0.30 0.04 265)',
+ headerHeight: '4rem',
+ transitionDuration: '200ms',
+ zIndex: 40,
+ },
+ colors: {},
+ spacing: {
+ radius: '0.5rem',
+ },
+ typography: {
+ fontFamily: {
+ body: "'IBM Plex Sans', sans-serif",
+ heading: "'JetBrains Mono', monospace",
+ code: "'JetBrains Mono', monospace",
+ },
+ },
+}
+
+export function useThemeConfig() {
+ const [themeConfig, setThemeConfig] = useState(DEFAULT_THEME)
+ const [isLoading, setIsLoading] = useState(true)
+
+ useEffect(() => {
+ async function loadTheme() {
+ try {
+ const response = await fetch('/theme.json')
+ if (response.ok) {
+ const config = await response.json()
+ setThemeConfig({ ...DEFAULT_THEME, ...config })
+ } else {
+ console.warn('Failed to load theme.json, using defaults')
+ setThemeConfig(DEFAULT_THEME)
+ }
+ } catch (error) {
+ console.error('Error loading theme config:', error)
+ setThemeConfig(DEFAULT_THEME)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadTheme()
+ }, [])
+
+ useEffect(() => {
+ if (themeConfig.sidebar && !isLoading) {
+ const root = document.documentElement
+ const sidebar = themeConfig.sidebar
+
+ if (sidebar.width) root.style.setProperty('--sidebar-width', sidebar.width)
+ if (sidebar.widthMobile) root.style.setProperty('--sidebar-width-mobile', sidebar.widthMobile)
+ if (sidebar.widthIcon) root.style.setProperty('--sidebar-width-icon', sidebar.widthIcon)
+ if (sidebar.backgroundColor) root.style.setProperty('--sidebar-bg', sidebar.backgroundColor)
+ if (sidebar.foregroundColor) root.style.setProperty('--sidebar-fg', sidebar.foregroundColor)
+ if (sidebar.borderColor) root.style.setProperty('--sidebar-border', sidebar.borderColor)
+ if (sidebar.accentColor) root.style.setProperty('--sidebar-accent', sidebar.accentColor)
+ if (sidebar.accentForeground) root.style.setProperty('--sidebar-accent-fg', sidebar.accentForeground)
+ if (sidebar.hoverBackground) root.style.setProperty('--sidebar-hover-bg', sidebar.hoverBackground)
+ if (sidebar.activeBackground) root.style.setProperty('--sidebar-active-bg', sidebar.activeBackground)
+ if (sidebar.headerHeight) root.style.setProperty('--sidebar-header-height', sidebar.headerHeight)
+ if (sidebar.transitionDuration) root.style.setProperty('--sidebar-transition', sidebar.transitionDuration)
+ if (sidebar.zIndex !== undefined) root.style.setProperty('--sidebar-z-index', sidebar.zIndex.toString())
+ }
+ }, [themeConfig, isLoading])
+
+ return { themeConfig, isLoading }
+}
diff --git a/src/index.css b/src/index.css
index cd86046..a22612c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -20,6 +20,20 @@
}
:root {
+ --sidebar-width: 16rem;
+ --sidebar-width-mobile: 18rem;
+ --sidebar-width-icon: 3rem;
+ --sidebar-bg: oklch(0.19 0.02 265);
+ --sidebar-fg: oklch(0.95 0.01 265);
+ --sidebar-border: oklch(0.28 0.03 265);
+ --sidebar-accent: oklch(0.58 0.24 265);
+ --sidebar-accent-fg: oklch(1 0 0);
+ --sidebar-hover-bg: oklch(0.25 0.03 265);
+ --sidebar-active-bg: oklch(0.30 0.04 265);
+ --sidebar-header-height: 4rem;
+ --sidebar-transition: 200ms;
+ --sidebar-z-index: 40;
+
--background: oklch(0.15 0.02 265);
--foreground: oklch(0.95 0.01 265);
@@ -52,6 +66,14 @@
}
@theme {
+ --color-sidebar: var(--sidebar-bg);
+ --color-sidebar-foreground: var(--sidebar-fg);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-fg);
+ --color-sidebar-hover: var(--sidebar-hover-bg);
+ --color-sidebar-active: var(--sidebar-active-bg);
+
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -79,3 +101,17 @@
--radius-2xl: calc(var(--radius) * 3);
--radius-full: 9999px;
}
+
+@layer components {
+ .bg-sidebar {
+ background-color: var(--sidebar-bg);
+ }
+
+ .text-sidebar-foreground {
+ color: var(--sidebar-fg);
+ }
+
+ .border-sidebar-border {
+ border-color: var(--sidebar-border);
+ }
+}
diff --git a/theme.json b/theme.json
index 9e26dfe..f1cd45d 100644
--- a/theme.json
+++ b/theme.json
@@ -1 +1,48 @@
-{}
\ No newline at end of file
+{
+ "sidebar": {
+ "width": "16rem",
+ "widthMobile": "18rem",
+ "widthIcon": "3rem",
+ "backgroundColor": "oklch(0.19 0.02 265)",
+ "foregroundColor": "oklch(0.95 0.01 265)",
+ "borderColor": "oklch(0.28 0.03 265)",
+ "accentColor": "oklch(0.58 0.24 265)",
+ "accentForeground": "oklch(1 0 0)",
+ "hoverBackground": "oklch(0.25 0.03 265)",
+ "activeBackground": "oklch(0.30 0.04 265)",
+ "headerHeight": "4rem",
+ "transitionDuration": "200ms",
+ "zIndex": 40
+ },
+ "colors": {
+ "background": "oklch(0.15 0.02 265)",
+ "foreground": "oklch(0.95 0.01 265)",
+ "card": "oklch(0.19 0.02 265)",
+ "cardForeground": "oklch(0.95 0.01 265)",
+ "popover": "oklch(0.19 0.02 265)",
+ "popoverForeground": "oklch(0.95 0.01 265)",
+ "primary": "oklch(0.58 0.24 265)",
+ "primaryForeground": "oklch(1 0 0)",
+ "secondary": "oklch(0.25 0.03 265)",
+ "secondaryForeground": "oklch(0.95 0.01 265)",
+ "muted": "oklch(0.25 0.03 265)",
+ "mutedForeground": "oklch(0.60 0.02 265)",
+ "accent": "oklch(0.75 0.20 145)",
+ "accentForeground": "oklch(0.15 0.02 265)",
+ "destructive": "oklch(0.60 0.25 25)",
+ "destructiveForeground": "oklch(1 0 0)",
+ "border": "oklch(0.28 0.03 265)",
+ "input": "oklch(0.28 0.03 265)",
+ "ring": "oklch(0.75 0.20 145)"
+ },
+ "spacing": {
+ "radius": "0.5rem"
+ },
+ "typography": {
+ "fontFamily": {
+ "body": "'IBM Plex Sans', sans-serif",
+ "heading": "'JetBrains Mono', monospace",
+ "code": "'JetBrains Mono', monospace"
+ }
+ }
+}
\ No newline at end of file