create-fluxstack 1.18.1 → 1.19.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/app/client/src/App.tsx +7 -7
  3. package/app/client/src/components/AppLayout.tsx +60 -23
  4. package/app/client/src/components/ColorWheel.tsx +195 -0
  5. package/app/client/src/components/DemoPage.tsx +5 -3
  6. package/app/client/src/components/LiveUploadWidget.tsx +1 -1
  7. package/app/client/src/components/ThemePicker.tsx +307 -0
  8. package/app/client/src/config/theme.config.ts +127 -0
  9. package/app/client/src/hooks/useThemeClock.ts +66 -0
  10. package/app/client/src/index.css +193 -0
  11. package/app/client/src/lib/theme-clock.ts +201 -0
  12. package/app/client/src/live/AuthDemo.tsx +9 -9
  13. package/app/client/src/live/CounterDemo.tsx +10 -10
  14. package/app/client/src/live/FormDemo.tsx +8 -8
  15. package/app/client/src/live/PingPongDemo.tsx +10 -10
  16. package/app/client/src/live/RoomChatDemo.tsx +10 -10
  17. package/app/client/src/live/SharedCounterDemo.tsx +5 -5
  18. package/app/client/src/pages/ApiTestPage.tsx +5 -5
  19. package/app/client/src/pages/HomePage.tsx +12 -12
  20. package/app/server/index.ts +8 -0
  21. package/app/server/live/auto-generated-components.ts +1 -1
  22. package/core/build/index.ts +1 -1
  23. package/core/cli/command-registry.ts +1 -1
  24. package/core/cli/commands/build.ts +25 -6
  25. package/core/cli/commands/plugin-deps.ts +1 -2
  26. package/core/cli/generators/plugin.ts +433 -581
  27. package/core/framework/server.ts +22 -8
  28. package/core/index.ts +6 -5
  29. package/core/plugins/index.ts +71 -199
  30. package/core/plugins/types.ts +76 -461
  31. package/core/server/index.ts +1 -1
  32. package/core/utils/logger/startup-banner.ts +26 -4
  33. package/core/utils/version.ts +6 -6
  34. package/create-fluxstack.ts +216 -107
  35. package/package.json +6 -5
  36. package/tsconfig.json +2 -1
  37. package/app/client/.live-stubs/LiveAdminPanel.js +0 -15
  38. package/app/client/.live-stubs/LiveCounter.js +0 -9
  39. package/app/client/.live-stubs/LiveForm.js +0 -11
  40. package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
  41. package/app/client/.live-stubs/LivePingPong.js +0 -10
  42. package/app/client/.live-stubs/LiveRoomChat.js +0 -11
  43. package/app/client/.live-stubs/LiveSharedCounter.js +0 -10
  44. package/app/client/.live-stubs/LiveUpload.js +0 -15
  45. package/core/plugins/config.ts +0 -356
  46. package/core/plugins/dependency-manager.ts +0 -481
  47. package/core/plugins/discovery.ts +0 -379
  48. package/core/plugins/executor.ts +0 -353
  49. package/core/plugins/manager.ts +0 -645
  50. package/core/plugins/module-resolver.ts +0 -227
  51. package/core/plugins/registry.ts +0 -913
  52. package/vitest.config.live.ts +0 -69
@@ -0,0 +1,307 @@
1
+ /**
2
+ * ThemePicker — floating panel with interactive color wheel
3
+ */
4
+ import { useState, useCallback, useRef } from 'react'
5
+ import { buildPaletteFromHues, applyPalette, type ColorPalette } from '../lib/theme-clock'
6
+ import { ColorWheel, generateHuesFromMode, type HarmonyMode } from './ColorWheel'
7
+ import { createPortal } from 'react-dom'
8
+
9
+ interface ThemePickerProps {
10
+ palette: ColorPalette
11
+ onOverride: (palette: ColorPalette | null) => void
12
+ }
13
+
14
+ const harmonyModes: { mode: HarmonyMode; label: string; icon: string }[] = [
15
+ { mode: 'analogous', label: 'Análogo', icon: '◐' },
16
+ { mode: 'complementary', label: 'Complementar', icon: '◑' },
17
+ { mode: 'triadic', label: 'Triádico', icon: '△' },
18
+ { mode: 'split-complementary', label: 'Split', icon: '⋔' },
19
+ { mode: 'square', label: 'Quadrado', icon: '◻' },
20
+ { mode: 'custom', label: 'Custom', icon: '✦' },
21
+ ]
22
+
23
+ function getHueName(hue: number): string {
24
+ const h = ((hue % 360) + 360) % 360
25
+ if (h < 15 || h >= 345) return 'red'
26
+ if (h < 45) return 'orange'
27
+ if (h < 75) return 'yellow'
28
+ if (h < 105) return 'lime'
29
+ if (h < 135) return 'green'
30
+ if (h < 165) return 'teal'
31
+ if (h < 195) return 'cyan'
32
+ if (h < 225) return 'blue'
33
+ if (h < 255) return 'indigo'
34
+ if (h < 285) return 'purple'
35
+ if (h < 315) return 'pink'
36
+ return 'magenta'
37
+ }
38
+
39
+ function huesToPalette(hues: number[]): ColorPalette {
40
+ return buildPaletteFromHues(hues[0], 'midday', {
41
+ secondary: hues[1],
42
+ tertiary: hues[2],
43
+ complement: hues[3],
44
+ accent: hues[4],
45
+ })
46
+ }
47
+
48
+ function ConfigModal({ config, palette, onClose }: { config: string; palette: ColorPalette; onClose: () => void }) {
49
+ const [copied, setCopied] = useState(false)
50
+
51
+ const handleCopy = () => {
52
+ navigator.clipboard.writeText(config)
53
+ setCopied(true)
54
+ setTimeout(() => setCopied(false), 2000)
55
+ }
56
+
57
+ return createPortal(
58
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4" onClick={onClose}>
59
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
60
+ <div
61
+ className="relative w-full max-w-lg rounded-2xl p-6 shadow-2xl"
62
+ style={{ backgroundColor: `oklch(12% 0.015 ${palette.baseHue})`, border: `1px solid ${palette.border}` }}
63
+ onClick={e => e.stopPropagation()}
64
+ >
65
+ <h2 className="text-lg font-bold text-white mb-1">🎨 Fixar Tema</h2>
66
+ <p className="text-sm text-gray-400 mb-4">
67
+ Cole este código em <code className="text-theme text-xs px-1.5 py-0.5 rounded bg-white/5">app/client/src/config/theme.config.ts</code>
68
+ </p>
69
+
70
+ <pre
71
+ className="text-sm font-mono p-4 rounded-xl overflow-x-auto mb-4 leading-relaxed"
72
+ style={{ backgroundColor: 'rgba(0,0,0,0.4)', border: `1px solid ${palette.border}` }}
73
+ >
74
+ <code className="text-gray-300">{config}</code>
75
+ </pre>
76
+
77
+ <div className="flex items-center gap-2 text-xs text-gray-500 mb-4 p-3 rounded-xl" style={{ backgroundColor: palette.bgAccent }}>
78
+ <span>💡</span>
79
+ <span>Após colar, o Vite hot reload aplica o tema automaticamente. Para esconder o picker em produção, mantenha <code className="text-theme">showPicker: false</code>.</span>
80
+ </div>
81
+
82
+ <div className="flex gap-2">
83
+ <button
84
+ onClick={handleCopy}
85
+ className="flex-1 py-2.5 rounded-xl text-sm font-medium transition-all"
86
+ style={copied
87
+ ? { backgroundColor: 'oklch(70% 0.2 150)', color: 'white' }
88
+ : { backgroundColor: palette.primary, color: 'white' }
89
+ }
90
+ >
91
+ {copied ? '✓ Copiado!' : '📋 Copiar Código'}
92
+ </button>
93
+ <button
94
+ onClick={onClose}
95
+ className="px-4 py-2.5 rounded-xl text-sm font-medium btn-theme-ghost"
96
+ >
97
+ Fechar
98
+ </button>
99
+ </div>
100
+ </div>
101
+ </div>,
102
+ document.body
103
+ )
104
+ }
105
+
106
+ export function ThemePicker({ palette, onOverride }: ThemePickerProps) {
107
+ const [open, setOpen] = useState(false)
108
+ const [harmonyMode, setHarmonyMode] = useState<HarmonyMode>('analogous')
109
+ const [hues, setHues] = useState<number[]>(() => generateHuesFromMode(palette.baseHue, 'analogous'))
110
+ const [isManual, setIsManual] = useState(false)
111
+ const [showConfigModal, setShowConfigModal] = useState(false)
112
+ const configRef = useRef('')
113
+
114
+ const handleHueChange = useCallback((index: number, hue: number) => {
115
+ setIsManual(true)
116
+ setHues(prev => {
117
+ const next = [...prev]
118
+ next[index] = hue
119
+ return next
120
+ })
121
+ // Build palette from current hues with the new one
122
+ const updated = [...hues]
123
+ updated[index] = hue
124
+ // Pad to 5 hues
125
+ while (updated.length < 5) updated.push(updated[0])
126
+ const p = huesToPalette(updated)
127
+ applyPalette(p)
128
+ onOverride(p)
129
+ }, [hues, onOverride])
130
+
131
+ const handleModeChange = useCallback((mode: HarmonyMode) => {
132
+ setHarmonyMode(mode)
133
+ const baseHue = hues[0] ?? palette.baseHue
134
+ const newHues = generateHuesFromMode(baseHue, mode)
135
+ // Pad to 5 for custom
136
+ while (newHues.length < 5) newHues.push(baseHue)
137
+ setHues(newHues)
138
+ if (isManual) {
139
+ const p = huesToPalette(newHues)
140
+ applyPalette(p)
141
+ onOverride(p)
142
+ }
143
+ }, [hues, palette.baseHue, isManual, onOverride])
144
+
145
+ const handleAuto = useCallback(() => {
146
+ setIsManual(false)
147
+ onOverride(null)
148
+ }, [onOverride])
149
+
150
+ const showConfig = useCallback(() => {
151
+ const h = hues.map(h => Math.round(h))
152
+ // Calculate relative offset from primary, normalized to -180..+180
153
+ const offset = (target: number) => {
154
+ let d = ((target - h[0]) % 360 + 540) % 360 - 180
155
+ return d >= 0 ? `+${d}°` : `${d}°`
156
+ }
157
+
158
+ configRef.current = harmonyMode === 'custom'
159
+ ? `export const themeConfig: ThemeConfig = {
160
+ mode: 'custom',
161
+ palette: {
162
+ primary: ${h[0]}, // ${getHueName(h[0])} (base)
163
+ secondary: ${h[1]}, // ${getHueName(h[1])} (${offset(h[1])} from primary)
164
+ tertiary: ${h[2]}, // ${getHueName(h[2])} (${offset(h[2])} from primary)
165
+ complement: ${h[3]}, // ${getHueName(h[3])} (${offset(h[3])} from primary)
166
+ accent: ${h[4]}, // ${getHueName(h[4])} (${offset(h[4])} from primary)
167
+ },
168
+ showPicker: false,
169
+ }`
170
+ : `export const themeConfig: ThemeConfig = {
171
+ mode: 'fixed',
172
+ hue: ${h[0]}, // ${getHueName(h[0])}
173
+ showPicker: false,
174
+ }`
175
+ setShowConfigModal(true)
176
+ }, [hues, harmonyMode])
177
+
178
+ const swatchLabels = ['Primary', 'Secondary', 'Tertiary', 'Complement', 'Accent']
179
+ const displayHues = [...hues]
180
+ while (displayHues.length < 5) displayHues.push(displayHues[0] ?? 0)
181
+ const displayPalette = isManual ? huesToPalette(displayHues) : palette
182
+
183
+ if (!open) {
184
+ return (
185
+ <button
186
+ onClick={() => setOpen(true)}
187
+ className="fixed bottom-4 right-4 z-50 w-12 h-12 rounded-full shadow-lg flex items-center justify-center text-white text-lg transition-all hover:scale-110 border border-white/10"
188
+ style={{ background: palette.gradientPrimary }}
189
+ title="Theme Picker"
190
+ >
191
+ 🎨
192
+ </button>
193
+ )
194
+ }
195
+
196
+ return (
197
+ <div
198
+ className="fixed bottom-4 right-4 z-50 rounded-2xl shadow-2xl overflow-hidden"
199
+ style={{
200
+ backgroundColor: `oklch(10% 0.015 ${displayPalette.baseHue})`,
201
+ border: `1px solid ${displayPalette.border}`,
202
+ width: '320px',
203
+ }}
204
+ >
205
+ {/* Header */}
206
+ <div className="flex items-center justify-between px-4 py-2.5"
207
+ style={{ borderBottom: `1px solid ${displayPalette.border}` }}
208
+ >
209
+ <div className="flex items-center gap-2">
210
+ <span>🎨</span>
211
+ <span className="text-white text-sm font-semibold">Color Wheel</span>
212
+ {isManual && (
213
+ <span className="text-[10px] px-1.5 py-0.5 rounded-full"
214
+ style={{ backgroundColor: displayPalette.primaryMuted, color: displayPalette.textPrimary }}
215
+ >
216
+ {harmonyMode === 'custom' ? 'custom' : Math.round(hues[0]) + '°'}
217
+ </span>
218
+ )}
219
+ </div>
220
+ <button onClick={() => setOpen(false)}
221
+ className="text-gray-500 hover:text-white transition-colors text-lg leading-none w-6 h-6 flex items-center justify-center rounded-full hover:bg-white/10"
222
+ >×</button>
223
+ </div>
224
+
225
+ {/* Color Wheel */}
226
+ <div className="flex justify-center py-3">
227
+ <ColorWheel
228
+ hues={displayHues}
229
+ mode={harmonyMode}
230
+ size={210}
231
+ onChange={handleHueChange}
232
+ />
233
+ </div>
234
+
235
+ {/* Harmony Mode Selector */}
236
+ <div className="px-3 pb-2">
237
+ <div className="flex gap-0.5">
238
+ {harmonyModes.map(({ mode, label, icon }) => (
239
+ <button
240
+ key={mode}
241
+ onClick={() => handleModeChange(mode)}
242
+ className={`flex-1 py-1.5 rounded-lg text-xs transition-all ${
243
+ harmonyMode === mode ? 'font-medium' : 'text-gray-500 hover:text-white'
244
+ }`}
245
+ style={harmonyMode === mode ? {
246
+ backgroundColor: displayPalette.primaryMuted,
247
+ color: displayPalette.textPrimary,
248
+ } : undefined}
249
+ title={label}
250
+ >
251
+ {icon}
252
+ </button>
253
+ ))}
254
+ </div>
255
+ <div className="text-center text-[10px] text-gray-500 mt-0.5">
256
+ {harmonyModes.find(m => m.mode === harmonyMode)?.label}
257
+ {harmonyMode === 'custom' && ' — dot = move one · ring = rotate all'}
258
+ </div>
259
+ </div>
260
+
261
+ {/* Swatches with hue labels */}
262
+ <div className="px-3 pb-2">
263
+ <div className="flex gap-1">
264
+ {displayHues.slice(0, 5).map((h, i) => (
265
+ <div key={i} className="flex-1 flex flex-col items-center gap-0.5">
266
+ <div className="w-full h-6 rounded-lg" style={{ backgroundColor: `oklch(65% 0.25 ${h})` }} title={swatchLabels[i]} />
267
+ <span className="text-[8px] text-gray-600">{Math.round(h)}°</span>
268
+ </div>
269
+ ))}
270
+ </div>
271
+ </div>
272
+
273
+ {/* Gradient preview */}
274
+ <div className="px-3 pb-2">
275
+ <div className="h-4 rounded-lg" style={{ background: displayPalette.gradientPrimary }} />
276
+ </div>
277
+
278
+ {/* Actions — always visible */}
279
+ <div className="px-3 pb-3 flex gap-2">
280
+ <button
281
+ onClick={showConfig}
282
+ className="flex-1 py-1.5 rounded-xl text-xs transition-all hover:opacity-80"
283
+ style={{ backgroundColor: displayPalette.primaryMuted, color: displayPalette.textPrimary }}
284
+ >
285
+ 📋 Fixar Tema
286
+ </button>
287
+ {isManual && (
288
+ <button
289
+ onClick={handleAuto}
290
+ className="px-3 py-1.5 rounded-xl text-xs transition-all hover:opacity-80 border"
291
+ style={{ borderColor: displayPalette.border, color: displayPalette.textSecondary }}
292
+ >
293
+
294
+ </button>
295
+ )}
296
+ </div>
297
+
298
+ {showConfigModal && (
299
+ <ConfigModal
300
+ config={configRef.current}
301
+ palette={displayPalette}
302
+ onClose={() => setShowConfigModal(false)}
303
+ />
304
+ )}
305
+ </div>
306
+ )
307
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Theme Configuration
3
+ *
4
+ * Control how the app's color palette behaves.
5
+ *
6
+ * === Modes ===
7
+ *
8
+ * 'auto' → colors shift with the time of day
9
+ * 'fixed' → fixed base hue with default harmony offsets
10
+ * 'custom' → full control over every color position
11
+ *
12
+ * === Examples ===
13
+ *
14
+ * // Auto mode (default)
15
+ * export const themeConfig = { mode: 'auto' }
16
+ *
17
+ * // Fixed purple
18
+ * export const themeConfig = { mode: 'fixed', hue: 270 }
19
+ *
20
+ * // Custom palette — you define every color offset
21
+ * export const themeConfig = {
22
+ * mode: 'custom',
23
+ * palette: {
24
+ * primary: 270, // purple
25
+ * secondary: 310, // pink (270 + 40)
26
+ * tertiary: 240, // indigo (270 - 30)
27
+ * complement: 90, // yellow (270 + 180)
28
+ * accent: 30, // orange (270 + 120)
29
+ * }
30
+ * }
31
+ *
32
+ * // Custom with offsets from base — easier to tweak
33
+ * export const themeConfig = {
34
+ * mode: 'custom',
35
+ * hue: 270,
36
+ * offsets: {
37
+ * secondary: +50, // default is +40
38
+ * tertiary: -45, // default is -30
39
+ * complement: +160, // default is +180
40
+ * accent: +100, // default is +120
41
+ * }
42
+ * }
43
+ *
44
+ * === Hue Reference ===
45
+ * 0° = Red 180° = Cyan
46
+ * 30° = Orange 210° = Blue
47
+ * 60° = Yellow 270° = Purple
48
+ * 120° = Green 330° = Magenta
49
+ */
50
+
51
+ export type ThemeMode = 'auto' | 'fixed' | 'custom'
52
+
53
+ export interface HarmonyOffsets {
54
+ /** Offset from base hue for secondary color (default: +40) */
55
+ secondary?: number
56
+ /** Offset from base hue for tertiary color (default: -30) */
57
+ tertiary?: number
58
+ /** Offset from base hue for complement color (default: +180) */
59
+ complement?: number
60
+ /** Offset from base hue for accent color (default: +120) */
61
+ accent?: number
62
+ }
63
+
64
+ export interface CustomPalette {
65
+ /** Absolute hue for primary (0-360) */
66
+ primary: number
67
+ /** Absolute hue for secondary (0-360) */
68
+ secondary: number
69
+ /** Absolute hue for tertiary (0-360) */
70
+ tertiary: number
71
+ /** Absolute hue for complement (0-360) */
72
+ complement: number
73
+ /** Absolute hue for accent (0-360) */
74
+ accent: number
75
+ }
76
+
77
+ export interface ThemeConfig {
78
+ /** 'auto' = shifts with time, 'fixed' = one hue, 'custom' = full control */
79
+ mode: ThemeMode
80
+ /** Base hue (0-360) — used in 'fixed' and 'custom' with offsets */
81
+ hue?: number
82
+ /** Custom harmony offsets from base hue — used in 'custom' mode */
83
+ offsets?: HarmonyOffsets
84
+ /** Absolute hue positions — used in 'custom' mode (overrides offsets) */
85
+ palette?: CustomPalette
86
+ /** Show the floating theme picker (default: true in dev) */
87
+ showPicker?: boolean
88
+ }
89
+
90
+ // ===== Default harmony offsets =====
91
+ export const DEFAULT_OFFSETS: Required<HarmonyOffsets> = {
92
+ secondary: 40,
93
+ tertiary: -30,
94
+ complement: 180,
95
+ accent: 120,
96
+ }
97
+
98
+ export const themeConfig: ThemeConfig = {
99
+ mode: 'fixed',
100
+ hue: 270, // purple
101
+
102
+ // ── Auto mode — colors shift with the time of day ─────
103
+ // mode: 'auto',
104
+
105
+ // ── Custom mode with offsets from base hue ────────────
106
+ // mode: 'custom',
107
+ // hue: 270,
108
+ // offsets: {
109
+ // secondary: +40,
110
+ // tertiary: -30,
111
+ // complement: +180,
112
+ // accent: +120,
113
+ // },
114
+
115
+ // ── Custom mode with absolute hue positions ───────────
116
+ // mode: 'custom',
117
+ // palette: {
118
+ // primary: 270, // purple
119
+ // secondary: 310, // pink
120
+ // tertiary: 240, // indigo
121
+ // complement: 90, // yellow
122
+ // accent: 30, // orange
123
+ // },
124
+
125
+ // Show color wheel picker (🎨) — visible in dev only
126
+ showPicker: import.meta.env.DEV,
127
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * React hook for the palette system — respects theme.config.ts
3
+ *
4
+ * Modes:
5
+ * - 'auto' → palette shifts with time (updates every 30s)
6
+ * - 'fixed' → palette stays on configured hue with default offsets
7
+ * - 'custom' → palette uses custom offsets or absolute hue positions
8
+ */
9
+ import { useState, useEffect } from 'react'
10
+ import { generatePalette, buildPaletteFromHues, applyPalette, type ColorPalette } from '../lib/theme-clock'
11
+ import { themeConfig, DEFAULT_OFFSETS } from '../config/theme.config'
12
+
13
+ function getConfiguredPalette(): ColorPalette {
14
+ const { mode, hue, offsets, palette } = themeConfig
15
+
16
+ if (mode === 'custom') {
17
+ // Absolute palette — each hue is defined explicitly
18
+ if (palette) {
19
+ return buildPaletteFromHues(palette.primary, 'midday', {
20
+ secondary: palette.secondary,
21
+ tertiary: palette.tertiary,
22
+ complement: palette.complement,
23
+ accent: palette.accent,
24
+ })
25
+ }
26
+
27
+ // Offsets from base hue
28
+ if (hue !== undefined) {
29
+ const o = { ...DEFAULT_OFFSETS, ...offsets }
30
+ return buildPaletteFromHues(hue, 'midday', {
31
+ secondary: hue + o.secondary,
32
+ tertiary: hue + o.tertiary,
33
+ complement: hue + o.complement,
34
+ accent: hue + o.accent,
35
+ })
36
+ }
37
+ }
38
+
39
+ if (mode === 'fixed' && hue !== undefined) {
40
+ return buildPaletteFromHues(hue, 'midday')
41
+ }
42
+
43
+ // Auto mode
44
+ return generatePalette()
45
+ }
46
+
47
+ export function useThemeClock(): ColorPalette {
48
+ const [palette, setPalette] = useState<ColorPalette>(getConfiguredPalette)
49
+
50
+ useEffect(() => {
51
+ const initial = getConfiguredPalette()
52
+ setPalette(initial)
53
+ applyPalette(initial)
54
+
55
+ if (themeConfig.mode === 'auto') {
56
+ const interval = setInterval(() => {
57
+ const p = generatePalette()
58
+ setPalette(p)
59
+ applyPalette(p)
60
+ }, 30_000)
61
+ return () => clearInterval(interval)
62
+ }
63
+ }, [])
64
+
65
+ return palette
66
+ }
@@ -1,5 +1,198 @@
1
1
  @import "tailwindcss";
2
2
 
3
+ /* ===== Theme Clock CSS Variables ===== */
4
+ /* Default values (overridden at runtime by theme-clock.ts) */
5
+ :root {
6
+ --theme-hue: 270;
7
+ --theme-primary: oklch(65% 0.25 270);
8
+ --theme-secondary: oklch(70% 0.20 310);
9
+ --theme-tertiary: oklch(60% 0.22 240);
10
+ --theme-complement: oklch(68% 0.18 90);
11
+ --theme-accent: oklch(72% 0.20 30);
12
+ --theme-primary-muted: oklch(65% 0.25 270 / 0.15);
13
+ --theme-secondary-muted: oklch(70% 0.20 310 / 0.10);
14
+ --theme-primary-glow: oklch(65% 0.25 270 / 0.3);
15
+ --theme-secondary-glow: oklch(70% 0.20 310 / 0.2);
16
+ --theme-bg-accent: oklch(65% 0.25 270 / 0.08);
17
+ --theme-bg-accent2: oklch(70% 0.20 310 / 0.05);
18
+ --theme-text-primary: oklch(80% 0.15 270);
19
+ --theme-text-secondary: oklch(75% 0.12 310);
20
+ --theme-text-muted: oklch(55% 0.05 270);
21
+ --theme-border: oklch(65% 0.25 270 / 0.12);
22
+ --theme-border-active: oklch(65% 0.25 270 / 0.3);
23
+ }
24
+
25
+ /* ===== Theme utility classes ===== */
26
+ /* Use these instead of fixed purple-500 etc. for dynamic theming */
27
+ .text-theme { color: var(--theme-primary); }
28
+ .text-theme-secondary { color: var(--theme-secondary); }
29
+ .text-theme-muted { color: var(--theme-text-muted); }
30
+ .bg-theme { background-color: var(--theme-primary); }
31
+ .bg-theme-muted { background-color: var(--theme-primary-muted); }
32
+ .bg-theme-accent { background-color: var(--theme-bg-accent); }
33
+ .border-theme { border-color: var(--theme-border); }
34
+ .border-theme-active { border-color: var(--theme-border-active); }
35
+ .shadow-theme { box-shadow: 0 4px 14px var(--theme-primary-glow); }
36
+ .glow-theme { filter: drop-shadow(0 0 12px var(--theme-primary-glow)); }
37
+
38
+ .bg-theme-gradient {
39
+ background-image: var(--theme-gradient-primary,
40
+ linear-gradient(to right, var(--theme-primary), var(--theme-secondary)));
41
+ }
42
+
43
+ /* Hover variants */
44
+ .hover\:bg-theme-muted:hover { background-color: var(--theme-primary-muted); }
45
+ .hover\:border-theme:hover { border-color: var(--theme-border-active); }
46
+ .hover\:text-theme:hover { color: var(--theme-primary); }
47
+ .hover\:bg-theme:hover { background-color: var(--theme-primary); }
48
+ .hover\:shadow-theme:hover { box-shadow: 0 8px 24px var(--theme-primary-glow); }
49
+
50
+ /* ===== Themed component classes ===== */
51
+
52
+ /* Primary button — filled with theme color */
53
+ .btn-theme {
54
+ background-color: var(--theme-primary);
55
+ color: white;
56
+ border-radius: 0.75rem;
57
+ padding: 0.5rem 1.25rem;
58
+ font-weight: 500;
59
+ font-size: 0.875rem;
60
+ transition: all 0.15s;
61
+ box-shadow: 0 4px 14px var(--theme-primary-glow);
62
+ }
63
+ .btn-theme:hover {
64
+ filter: brightness(1.1);
65
+ box-shadow: 0 8px 24px var(--theme-primary-glow);
66
+ }
67
+ .btn-theme:disabled {
68
+ opacity: 0.5;
69
+ cursor: not-allowed;
70
+ }
71
+
72
+ /* Outline button — border with theme color */
73
+ .btn-theme-outline {
74
+ background-color: var(--theme-primary-muted);
75
+ color: var(--theme-text-primary);
76
+ border: 1px solid var(--theme-border-active);
77
+ border-radius: 0.75rem;
78
+ padding: 0.5rem 1.25rem;
79
+ font-weight: 500;
80
+ font-size: 0.875rem;
81
+ transition: all 0.15s;
82
+ }
83
+ .btn-theme-outline:hover {
84
+ background-color: var(--theme-primary);
85
+ color: white;
86
+ }
87
+
88
+ /* Ghost button — subtle theme tint */
89
+ .btn-theme-ghost {
90
+ background-color: transparent;
91
+ color: var(--theme-text-primary);
92
+ border-radius: 0.75rem;
93
+ padding: 0.5rem 1.25rem;
94
+ font-weight: 500;
95
+ font-size: 0.875rem;
96
+ transition: all 0.15s;
97
+ }
98
+ .btn-theme-ghost:hover {
99
+ background-color: var(--theme-primary-muted);
100
+ }
101
+
102
+ /* Themed input */
103
+ .input-theme {
104
+ background-color: rgba(255, 255, 255, 0.03);
105
+ border: 1px solid var(--theme-border);
106
+ border-radius: 0.75rem;
107
+ padding: 0.625rem 1rem;
108
+ color: white;
109
+ font-size: 0.875rem;
110
+ transition: border-color 0.15s;
111
+ outline: none;
112
+ }
113
+ .input-theme:focus {
114
+ border-color: var(--theme-primary);
115
+ box-shadow: 0 0 0 2px var(--theme-primary-glow);
116
+ }
117
+ .input-theme::placeholder {
118
+ color: rgba(255, 255, 255, 0.3);
119
+ }
120
+
121
+ /* Themed card */
122
+ .card-theme {
123
+ background-color: rgba(255, 255, 255, 0.02);
124
+ border: 1px solid var(--theme-border);
125
+ border-radius: 0.75rem;
126
+ transition: all 0.2s;
127
+ }
128
+ .card-theme:hover {
129
+ background-color: rgba(255, 255, 255, 0.04);
130
+ border-color: var(--theme-border-active);
131
+ }
132
+
133
+ /* Themed badge/tag */
134
+ .badge-theme {
135
+ background-color: var(--theme-primary-muted);
136
+ color: var(--theme-text-primary);
137
+ border-radius: 9999px;
138
+ padding: 0.125rem 0.625rem;
139
+ font-size: 0.75rem;
140
+ font-weight: 500;
141
+ }
142
+
143
+ /* Complement/accent colored buttons */
144
+ .btn-complement {
145
+ background-color: color-mix(in oklch, var(--theme-complement) 20%, transparent);
146
+ color: var(--theme-complement);
147
+ border: 1px solid color-mix(in oklch, var(--theme-complement) 30%, transparent);
148
+ border-radius: 0.75rem;
149
+ transition: all 0.15s;
150
+ }
151
+ .btn-complement:hover {
152
+ background-color: color-mix(in oklch, var(--theme-complement) 30%, transparent);
153
+ }
154
+
155
+ .btn-accent {
156
+ background-color: color-mix(in oklch, var(--theme-accent) 20%, transparent);
157
+ color: var(--theme-accent);
158
+ border: 1px solid color-mix(in oklch, var(--theme-accent) 30%, transparent);
159
+ border-radius: 0.75rem;
160
+ transition: all 0.15s;
161
+ }
162
+ .btn-accent:hover {
163
+ background-color: color-mix(in oklch, var(--theme-accent) 30%, transparent);
164
+ }
165
+
166
+ .btn-secondary {
167
+ background-color: color-mix(in oklch, var(--theme-secondary) 20%, transparent);
168
+ color: var(--theme-secondary);
169
+ border: 1px solid color-mix(in oklch, var(--theme-secondary) 30%, transparent);
170
+ border-radius: 0.75rem;
171
+ transition: all 0.15s;
172
+ }
173
+ .btn-secondary:hover {
174
+ background-color: color-mix(in oklch, var(--theme-secondary) 30%, transparent);
175
+ }
176
+
177
+ .btn-tertiary {
178
+ background-color: color-mix(in oklch, var(--theme-tertiary) 20%, transparent);
179
+ color: var(--theme-tertiary);
180
+ border: 1px solid color-mix(in oklch, var(--theme-tertiary) 30%, transparent);
181
+ border-radius: 0.75rem;
182
+ transition: all 0.15s;
183
+ }
184
+ .btn-tertiary:hover {
185
+ background-color: color-mix(in oklch, var(--theme-tertiary) 30%, transparent);
186
+ }
187
+
188
+ /* Themed scrollbar */
189
+ ::-webkit-scrollbar-thumb {
190
+ background: var(--theme-border) !important;
191
+ }
192
+ ::-webkit-scrollbar-thumb:hover {
193
+ background: var(--theme-border-active) !important;
194
+ }
195
+
3
196
  /* Base styles */
4
197
  * {
5
198
  box-sizing: border-box;