create-fluxstack 1.18.1 → 1.20.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.
- package/CHANGELOG.md +132 -0
- package/LLMD/INDEX.md +1 -1
- package/LLMD/MAINTENANCE.md +197 -197
- package/LLMD/MIGRATION.md +44 -1
- package/LLMD/agent.md +20 -7
- package/LLMD/config/declarative-system.md +268 -268
- package/LLMD/config/environment-vars.md +3 -6
- package/LLMD/config/runtime-reload.md +401 -401
- package/LLMD/core/build-system.md +599 -599
- package/LLMD/core/framework-lifecycle.md +249 -229
- package/LLMD/core/plugin-system.md +154 -100
- package/LLMD/patterns/anti-patterns.md +397 -397
- package/LLMD/patterns/project-structure.md +264 -264
- package/LLMD/patterns/type-safety.md +61 -5
- package/LLMD/reference/cli-commands.md +31 -7
- package/LLMD/reference/plugin-hooks.md +4 -2
- package/LLMD/reference/troubleshooting.md +364 -364
- package/LLMD/resources/controllers.md +465 -465
- package/LLMD/resources/live-auth.md +178 -1
- package/LLMD/resources/live-binary-delta.md +3 -1
- package/LLMD/resources/live-components.md +1192 -1041
- package/LLMD/resources/live-logging.md +3 -1
- package/LLMD/resources/live-rooms.md +1 -1
- package/LLMD/resources/live-upload.md +228 -181
- package/LLMD/resources/plugins-external.md +8 -7
- package/LLMD/resources/rest-auth.md +290 -290
- package/LLMD/resources/routes-eden.md +254 -254
- package/app/client/src/App.tsx +7 -7
- package/app/client/src/components/AppLayout.tsx +60 -23
- package/app/client/src/components/ColorWheel.tsx +195 -0
- package/app/client/src/components/DemoPage.tsx +5 -3
- package/app/client/src/components/LiveUploadWidget.tsx +1 -1
- package/app/client/src/components/ThemePicker.tsx +307 -0
- package/app/client/src/config/theme.config.ts +127 -0
- package/app/client/src/hooks/useThemeClock.ts +66 -0
- package/app/client/src/index.css +193 -0
- package/app/client/src/lib/theme-clock.ts +201 -0
- package/app/client/src/live/AuthDemo.tsx +9 -9
- package/app/client/src/live/CounterDemo.tsx +10 -10
- package/app/client/src/live/FormDemo.tsx +8 -8
- package/app/client/src/live/PingPongDemo.tsx +10 -10
- package/app/client/src/live/RoomChatDemo.tsx +10 -10
- package/app/client/src/live/SharedCounterDemo.tsx +5 -5
- package/app/client/src/pages/ApiTestPage.tsx +5 -5
- package/app/client/src/pages/HomePage.tsx +12 -12
- package/app/server/index.ts +8 -0
- package/app/server/live/auto-generated-components.ts +1 -1
- package/core/build/index.ts +1 -1
- package/core/cli/command-registry.ts +1 -1
- package/core/cli/commands/build.ts +25 -6
- package/core/cli/commands/plugin-deps.ts +1 -2
- package/core/cli/generators/plugin.ts +433 -581
- package/core/framework/server.ts +22 -8
- package/core/index.ts +6 -5
- package/core/plugins/index.ts +71 -199
- package/core/plugins/types.ts +76 -461
- package/core/server/index.ts +1 -1
- package/core/utils/logger/startup-banner.ts +26 -4
- package/create-fluxstack.ts +216 -107
- package/package.json +108 -107
- package/tsconfig.json +2 -1
- package/core/plugins/config.ts +0 -356
- package/core/plugins/dependency-manager.ts +0 -481
- package/core/plugins/discovery.ts +0 -379
- package/core/plugins/executor.ts +0 -353
- package/core/plugins/manager.ts +0 -645
- package/core/plugins/module-resolver.ts +0 -227
- package/core/plugins/registry.ts +0 -913
- package/vitest.config.live.ts +0 -69
|
@@ -3,6 +3,10 @@ import { Link, Outlet, useLocation } from 'react-router'
|
|
|
3
3
|
import { FaBook, FaGithub, FaBars, FaTimes } from 'react-icons/fa'
|
|
4
4
|
import FluxStackLogo from '@client/src/assets/fluxstack.svg'
|
|
5
5
|
import faviconSvg from '@client/src/assets/fluxstack-static.svg?raw'
|
|
6
|
+
import { useThemeClock } from '../hooks/useThemeClock'
|
|
7
|
+
import { ThemePicker } from './ThemePicker'
|
|
8
|
+
import type { ColorPalette } from '../lib/theme-clock'
|
|
9
|
+
import { themeConfig } from '../config/theme.config'
|
|
6
10
|
|
|
7
11
|
const navItems = [
|
|
8
12
|
{ to: '/', label: 'Home' },
|
|
@@ -36,13 +40,16 @@ const faviconUrlCache = new Map<string, string>()
|
|
|
36
40
|
export function AppLayout() {
|
|
37
41
|
const location = useLocation()
|
|
38
42
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
43
|
+
const autoTheme = useThemeClock()
|
|
44
|
+
const [overrideTheme, setOverrideTheme] = useState<ColorPalette | null>(null)
|
|
45
|
+
const theme = overrideTheme || autoTheme
|
|
39
46
|
|
|
40
47
|
useEffect(() => {
|
|
41
48
|
const current = navItems.find(item => item.to === location.pathname)
|
|
42
49
|
document.title = current ? `${current.label} - FluxStack` : 'FluxStack'
|
|
43
50
|
|
|
44
|
-
// Dynamic favicon with hue-rotate
|
|
45
|
-
const hue =
|
|
51
|
+
// Dynamic favicon with hue-rotate based on theme clock
|
|
52
|
+
const hue = `${Math.round(theme.baseHue - 270)}deg`
|
|
46
53
|
let url = faviconUrlCache.get(hue)
|
|
47
54
|
if (!url) {
|
|
48
55
|
// Evict oldest entry if cache is full, revoking blob URL to free memory
|
|
@@ -68,20 +75,27 @@ export function AppLayout() {
|
|
|
68
75
|
}
|
|
69
76
|
link.type = 'image/svg+xml'
|
|
70
77
|
link.href = url
|
|
71
|
-
}, [location.pathname])
|
|
78
|
+
}, [location.pathname, theme.baseHue])
|
|
72
79
|
|
|
73
80
|
return (
|
|
74
|
-
<div className="min-h-screen
|
|
75
|
-
<header className="sticky top-0 z-50 backdrop-blur-
|
|
81
|
+
<div className="min-h-screen text-white flex flex-col" style={{ backgroundColor: `oklch(8% 0.02 ${theme.baseHue})` }}>
|
|
82
|
+
<header className="sticky top-0 z-50 backdrop-blur-xl bg-[#0a0a1a]/80 border-b border-white/[0.06]">
|
|
76
83
|
<div className="container mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between gap-4">
|
|
77
|
-
<Link to="/" className="flex items-center gap-2
|
|
84
|
+
<Link to="/" className="flex items-center gap-2 font-semibold tracking-wide">
|
|
78
85
|
<img
|
|
79
86
|
src={FluxStackLogo}
|
|
80
87
|
alt="FluxStack"
|
|
81
|
-
className="w-9 h-9 transition-[filter] duration-500
|
|
82
|
-
style={{
|
|
88
|
+
className="w-9 h-9 transition-[filter] duration-500"
|
|
89
|
+
style={{
|
|
90
|
+
filter: `hue-rotate(${theme.baseHue - 270}deg) drop-shadow(0 0 8px ${theme.primaryGlow})`,
|
|
91
|
+
}}
|
|
83
92
|
/>
|
|
84
|
-
|
|
93
|
+
<span
|
|
94
|
+
className="bg-clip-text text-transparent"
|
|
95
|
+
style={{ backgroundImage: theme.gradientPrimary }}
|
|
96
|
+
>
|
|
97
|
+
FluxStack
|
|
98
|
+
</span>
|
|
85
99
|
</Link>
|
|
86
100
|
|
|
87
101
|
{/* Desktop nav */}
|
|
@@ -94,9 +108,13 @@ export function AppLayout() {
|
|
|
94
108
|
to={item.to}
|
|
95
109
|
className={`px-3 py-1.5 rounded-lg text-sm transition-all ${
|
|
96
110
|
active
|
|
97
|
-
? '
|
|
98
|
-
: 'text-gray-
|
|
111
|
+
? 'font-medium'
|
|
112
|
+
: 'text-gray-400 hover:text-white hover:bg-white/[0.05]'
|
|
99
113
|
}`}
|
|
114
|
+
style={active ? {
|
|
115
|
+
backgroundColor: theme.primaryMuted,
|
|
116
|
+
color: theme.textPrimary,
|
|
117
|
+
} : undefined}
|
|
100
118
|
>
|
|
101
119
|
{item.label}
|
|
102
120
|
</Link>
|
|
@@ -109,7 +127,7 @@ export function AppLayout() {
|
|
|
109
127
|
href="https://live-docs.marcosbrendon.com/"
|
|
110
128
|
target="_blank"
|
|
111
129
|
rel="noopener noreferrer"
|
|
112
|
-
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 bg-purple-500/20 border border-purple-500/
|
|
130
|
+
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 bg-purple-500/20 border border-purple-500/20 text-purple-300 rounded-xl text-sm hover:bg-purple-500/30 transition-all"
|
|
113
131
|
>
|
|
114
132
|
<FaBook />
|
|
115
133
|
Live Docs
|
|
@@ -118,7 +136,7 @@ export function AppLayout() {
|
|
|
118
136
|
href="/swagger"
|
|
119
137
|
target="_blank"
|
|
120
138
|
rel="noopener noreferrer"
|
|
121
|
-
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 bg-white/
|
|
139
|
+
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 bg-white/[0.03] border border-white/[0.06] text-gray-400 rounded-xl text-sm hover:bg-white/[0.06] hover:text-white transition-all"
|
|
122
140
|
>
|
|
123
141
|
<FaBook />
|
|
124
142
|
API Docs
|
|
@@ -127,7 +145,7 @@ export function AppLayout() {
|
|
|
127
145
|
href="https://github.com/MarcosBrendonDePaula/FluxStack"
|
|
128
146
|
target="_blank"
|
|
129
147
|
rel="noopener noreferrer"
|
|
130
|
-
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 bg-white/
|
|
148
|
+
className="hidden sm:inline-flex items-center gap-2 px-3 py-1.5 bg-white/[0.03] border border-white/[0.06] text-gray-400 rounded-xl text-sm hover:bg-white/[0.06] hover:text-white transition-all"
|
|
131
149
|
>
|
|
132
150
|
<FaGithub />
|
|
133
151
|
GitHub
|
|
@@ -136,7 +154,7 @@ export function AppLayout() {
|
|
|
136
154
|
{/* Mobile menu toggle */}
|
|
137
155
|
<button
|
|
138
156
|
onClick={() => setMenuOpen(!menuOpen)}
|
|
139
|
-
className="md:hidden p-2 text-gray-
|
|
157
|
+
className="md:hidden p-2 text-gray-400 hover:text-white transition-colors"
|
|
140
158
|
aria-label="Toggle menu"
|
|
141
159
|
>
|
|
142
160
|
{menuOpen ? <FaTimes size={20} /> : <FaBars size={20} />}
|
|
@@ -146,7 +164,7 @@ export function AppLayout() {
|
|
|
146
164
|
|
|
147
165
|
{/* Mobile nav */}
|
|
148
166
|
{menuOpen && (
|
|
149
|
-
<div className="md:hidden border-t border-white/
|
|
167
|
+
<div className="md:hidden border-t border-white/[0.06] bg-[#0a0a1a]/90 backdrop-blur-xl">
|
|
150
168
|
<nav className="container mx-auto px-4 py-3 flex gap-4 relative">
|
|
151
169
|
<div className="flex flex-col gap-1 flex-1">
|
|
152
170
|
{navItems.map((item) => {
|
|
@@ -158,20 +176,24 @@ export function AppLayout() {
|
|
|
158
176
|
onClick={() => setMenuOpen(false)}
|
|
159
177
|
className={`px-3 py-2 rounded-lg text-sm transition-all ${
|
|
160
178
|
active
|
|
161
|
-
? '
|
|
162
|
-
: 'text-gray-
|
|
179
|
+
? 'font-medium'
|
|
180
|
+
: 'text-gray-400 hover:text-white hover:bg-white/[0.05]'
|
|
163
181
|
}`}
|
|
182
|
+
style={active ? {
|
|
183
|
+
backgroundColor: theme.primaryMuted,
|
|
184
|
+
color: theme.textPrimary,
|
|
185
|
+
} : undefined}
|
|
164
186
|
>
|
|
165
187
|
{item.label}
|
|
166
188
|
</Link>
|
|
167
189
|
)
|
|
168
190
|
})}
|
|
169
|
-
<div className="flex flex-wrap gap-2 mt-2 pt-2 border-t border-white/
|
|
191
|
+
<div className="flex flex-wrap gap-2 mt-2 pt-2 border-t border-white/[0.06]">
|
|
170
192
|
<a
|
|
171
193
|
href="https://live-docs.marcosbrendon.com/"
|
|
172
194
|
target="_blank"
|
|
173
195
|
rel="noopener noreferrer"
|
|
174
|
-
className="inline-flex items-center gap-2 px-3 py-2 bg-purple-500/20 border border-purple-500/
|
|
196
|
+
className="inline-flex items-center gap-2 px-3 py-2 bg-purple-500/20 border border-purple-500/20 text-purple-300 rounded-xl text-sm hover:bg-purple-500/30 transition-all"
|
|
175
197
|
>
|
|
176
198
|
<FaBook />
|
|
177
199
|
Live Docs
|
|
@@ -180,7 +202,7 @@ export function AppLayout() {
|
|
|
180
202
|
href="/swagger"
|
|
181
203
|
target="_blank"
|
|
182
204
|
rel="noopener noreferrer"
|
|
183
|
-
className="inline-flex items-center gap-2 px-3 py-2 bg-white/
|
|
205
|
+
className="inline-flex items-center gap-2 px-3 py-2 bg-white/[0.03] border border-white/[0.06] text-gray-400 rounded-xl text-sm hover:bg-white/[0.06] hover:text-white transition-all"
|
|
184
206
|
>
|
|
185
207
|
<FaBook />
|
|
186
208
|
API Docs
|
|
@@ -189,7 +211,7 @@ export function AppLayout() {
|
|
|
189
211
|
href="https://github.com/MarcosBrendonDePaula/FluxStack"
|
|
190
212
|
target="_blank"
|
|
191
213
|
rel="noopener noreferrer"
|
|
192
|
-
className="inline-flex items-center gap-2 px-3 py-2 bg-white/
|
|
214
|
+
className="inline-flex items-center gap-2 px-3 py-2 bg-white/[0.03] border border-white/[0.06] text-gray-400 rounded-xl text-sm hover:bg-white/[0.06] hover:text-white transition-all"
|
|
193
215
|
>
|
|
194
216
|
<FaGithub />
|
|
195
217
|
GitHub
|
|
@@ -209,7 +231,22 @@ export function AppLayout() {
|
|
|
209
231
|
)}
|
|
210
232
|
</header>
|
|
211
233
|
|
|
212
|
-
<
|
|
234
|
+
<main className="flex-1">
|
|
235
|
+
<Outlet />
|
|
236
|
+
</main>
|
|
237
|
+
|
|
238
|
+
<footer className="border-t border-white/[0.06] py-6 mt-auto">
|
|
239
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
240
|
+
<p className="text-gray-500 text-sm">
|
|
241
|
+
Built with <span style={{ color: theme.primary }}>FluxStack</span> — Bun + Elysia + React
|
|
242
|
+
</p>
|
|
243
|
+
<p className="text-gray-600 text-xs mt-1">
|
|
244
|
+
🎨 <span style={{ color: theme.primary }}>{theme.period}</span> palette — colors shift with the time of day
|
|
245
|
+
</p>
|
|
246
|
+
</div>
|
|
247
|
+
</footer>
|
|
248
|
+
|
|
249
|
+
{themeConfig.showPicker && <ThemePicker palette={theme} onOverride={setOverrideTheme} />}
|
|
213
250
|
</div>
|
|
214
251
|
)
|
|
215
252
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorWheel — Interactive color wheel like Adobe Color
|
|
3
|
+
*
|
|
4
|
+
* Modes:
|
|
5
|
+
* - Preset harmonies: all dots move together based on base hue
|
|
6
|
+
* - Custom: each dot is independently draggable
|
|
7
|
+
*/
|
|
8
|
+
import { useRef, useState, useCallback, useEffect } from 'react'
|
|
9
|
+
|
|
10
|
+
export type HarmonyMode = 'analogous' | 'complementary' | 'triadic' | 'split-complementary' | 'square' | 'monochromatic' | 'custom'
|
|
11
|
+
|
|
12
|
+
interface ColorWheelProps {
|
|
13
|
+
/** Hues for each point: [primary, secondary, tertiary, complement, accent] */
|
|
14
|
+
hues: number[]
|
|
15
|
+
mode: HarmonyMode
|
|
16
|
+
size?: number
|
|
17
|
+
/** Called when any hue changes. index = which point, hue = new value */
|
|
18
|
+
onChange: (index: number, hue: number) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const POINT_LABELS = ['P', 'S', 'T', 'C', 'A']
|
|
22
|
+
|
|
23
|
+
/** Get harmony offsets for preset modes */
|
|
24
|
+
function getHarmonyOffsets(mode: HarmonyMode): number[] {
|
|
25
|
+
switch (mode) {
|
|
26
|
+
case 'analogous': return [0, -30, 30, -60, 60]
|
|
27
|
+
case 'complementary': return [0, 180]
|
|
28
|
+
case 'triadic': return [0, 120, 240]
|
|
29
|
+
case 'split-complementary': return [0, 150, 210]
|
|
30
|
+
case 'square': return [0, 90, 180, 270]
|
|
31
|
+
case 'monochromatic': return [0]
|
|
32
|
+
case 'custom': return [0, 40, -30, 180, 120] // defaults, but each is independent
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Get number of points for each mode */
|
|
37
|
+
export function getPointCount(mode: HarmonyMode): number {
|
|
38
|
+
return getHarmonyOffsets(mode).length
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Generate initial hues from base hue and mode */
|
|
42
|
+
export function generateHuesFromMode(baseHue: number, mode: HarmonyMode): number[] {
|
|
43
|
+
return getHarmonyOffsets(mode).map(o => ((baseHue + o) % 360 + 360) % 360)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hueToXY(hue: number, radius: number, cx: number, cy: number) {
|
|
47
|
+
const rad = ((hue - 90) * Math.PI) / 180
|
|
48
|
+
return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function xyToHue(x: number, y: number, cx: number, cy: number): number {
|
|
52
|
+
return ((Math.atan2(y - cy, x - cx) * 180) / Math.PI + 90 + 360) % 360
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ColorWheel({ hues, mode, size = 220, onChange }: ColorWheelProps) {
|
|
56
|
+
const svgRef = useRef<SVGSVGElement>(null)
|
|
57
|
+
const [dragging, setDragging] = useState<number | null>(null) // index of dragged point
|
|
58
|
+
|
|
59
|
+
const cx = size / 2
|
|
60
|
+
const cy = size / 2
|
|
61
|
+
const outerR = size / 2 - 8
|
|
62
|
+
const innerR = outerR - 24
|
|
63
|
+
const dotR = outerR - 12
|
|
64
|
+
|
|
65
|
+
const isCustom = mode === 'custom'
|
|
66
|
+
|
|
67
|
+
const getMouseHue = useCallback((e: MouseEvent | React.MouseEvent) => {
|
|
68
|
+
const svg = svgRef.current
|
|
69
|
+
if (!svg) return 0
|
|
70
|
+
const rect = svg.getBoundingClientRect()
|
|
71
|
+
return xyToHue(e.clientX - rect.left, e.clientY - rect.top, cx, cy)
|
|
72
|
+
}, [cx, cy])
|
|
73
|
+
|
|
74
|
+
// Mouse down on a specific point
|
|
75
|
+
const handlePointDown = useCallback((e: React.MouseEvent, index: number) => {
|
|
76
|
+
e.preventDefault()
|
|
77
|
+
e.stopPropagation()
|
|
78
|
+
setDragging(index)
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
// Mouse down on wheel background — moves the base (index 0)
|
|
82
|
+
// Click on wheel background = rotate ALL points together (preserving relative positions)
|
|
83
|
+
const handleWheelDown = useCallback((e: React.MouseEvent) => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
setDragging(-1) // -1 = rotate all
|
|
86
|
+
const hue = getMouseHue(e)
|
|
87
|
+
const delta = hue - hues[0]
|
|
88
|
+
for (let i = 0; i < hues.length; i++) {
|
|
89
|
+
onChange(i, ((hues[i] + delta) % 360 + 360) % 360)
|
|
90
|
+
}
|
|
91
|
+
}, [getMouseHue, hues, onChange])
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (dragging === null) return
|
|
95
|
+
|
|
96
|
+
const handleMove = (e: MouseEvent) => {
|
|
97
|
+
const hue = getMouseHue(e)
|
|
98
|
+
if (dragging === -1) {
|
|
99
|
+
// Rotate all points together (background drag)
|
|
100
|
+
const delta = hue - hues[0]
|
|
101
|
+
for (let i = 0; i < hues.length; i++) {
|
|
102
|
+
onChange(i, ((hues[i] + delta) % 360 + 360) % 360)
|
|
103
|
+
}
|
|
104
|
+
} else if (isCustom) {
|
|
105
|
+
// Custom mode: move only the dragged point
|
|
106
|
+
onChange(dragging, hue)
|
|
107
|
+
} else {
|
|
108
|
+
// Preset mode: move all points relative to base
|
|
109
|
+
const delta = hue - hues[0]
|
|
110
|
+
for (let i = 0; i < hues.length; i++) {
|
|
111
|
+
onChange(i, ((hues[i] + delta) % 360 + 360) % 360)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const handleUp = () => setDragging(null)
|
|
116
|
+
|
|
117
|
+
window.addEventListener('mousemove', handleMove)
|
|
118
|
+
window.addEventListener('mouseup', handleUp)
|
|
119
|
+
return () => {
|
|
120
|
+
window.removeEventListener('mousemove', handleMove)
|
|
121
|
+
window.removeEventListener('mouseup', handleUp)
|
|
122
|
+
}
|
|
123
|
+
}, [dragging, getMouseHue, hues, isCustom, onChange])
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<svg
|
|
127
|
+
ref={svgRef}
|
|
128
|
+
width={size}
|
|
129
|
+
height={size}
|
|
130
|
+
onMouseDown={handleWheelDown}
|
|
131
|
+
style={{ cursor: dragging !== null ? 'grabbing' : 'pointer', userSelect: 'none' }}
|
|
132
|
+
>
|
|
133
|
+
{/* Hue ring segments */}
|
|
134
|
+
{Array.from({ length: 72 }, (_, i) => {
|
|
135
|
+
const a1 = (i / 72) * 360
|
|
136
|
+
const a2 = ((i + 1) / 72) * 360
|
|
137
|
+
const r1 = ((a1 - 90) * Math.PI) / 180
|
|
138
|
+
const r2 = ((a2 - 90) * Math.PI) / 180
|
|
139
|
+
return (
|
|
140
|
+
<path
|
|
141
|
+
key={i}
|
|
142
|
+
d={`M ${cx + outerR * Math.cos(r1)} ${cy + outerR * Math.sin(r1)} A ${outerR} ${outerR} 0 0 1 ${cx + outerR * Math.cos(r2)} ${cy + outerR * Math.sin(r2)} L ${cx + innerR * Math.cos(r2)} ${cy + innerR * Math.sin(r2)} A ${innerR} ${innerR} 0 0 0 ${cx + innerR * Math.cos(r1)} ${cy + innerR * Math.sin(r1)} Z`}
|
|
143
|
+
fill={`oklch(70% 0.22 ${a1})`}
|
|
144
|
+
/>
|
|
145
|
+
)
|
|
146
|
+
})}
|
|
147
|
+
|
|
148
|
+
{/* Inner dark circle */}
|
|
149
|
+
<circle cx={cx} cy={cy} r={innerR - 2} fill="#0a0a1a" />
|
|
150
|
+
|
|
151
|
+
{/* Connection lines */}
|
|
152
|
+
{hues.length > 1 && (
|
|
153
|
+
<polygon
|
|
154
|
+
points={hues.map(h => {
|
|
155
|
+
const p = hueToXY(h, dotR * 0.55, cx, cy)
|
|
156
|
+
return `${p.x},${p.y}`
|
|
157
|
+
}).join(' ')}
|
|
158
|
+
fill={`oklch(65% 0.15 ${hues[0]} / 0.08)`}
|
|
159
|
+
stroke="rgba(255,255,255,0.12)"
|
|
160
|
+
strokeWidth="1"
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Center color preview */}
|
|
165
|
+
<circle cx={cx} cy={cy} r={innerR * 0.35} fill={`oklch(65% 0.25 ${hues[0]})`} opacity="0.8" />
|
|
166
|
+
<text x={cx} y={cy - 6} textAnchor="middle" dominantBaseline="central" fill="white" fontSize="11" fontWeight="700">
|
|
167
|
+
{Math.round(hues[0])}°
|
|
168
|
+
</text>
|
|
169
|
+
<text x={cx} y={cy + 8} textAnchor="middle" dominantBaseline="central" fill="rgba(255,255,255,0.5)" fontSize="8">
|
|
170
|
+
{isCustom ? 'CUSTOM' : mode.toUpperCase()}
|
|
171
|
+
</text>
|
|
172
|
+
|
|
173
|
+
{/* Harmony dots — each draggable in custom mode */}
|
|
174
|
+
{hues.map((hue, i) => {
|
|
175
|
+
const pos = hueToXY(hue, dotR, cx, cy)
|
|
176
|
+
const isBase = i === 0
|
|
177
|
+
const isDraggable = isCustom || isBase
|
|
178
|
+
const r = isBase ? 9 : 7
|
|
179
|
+
return (
|
|
180
|
+
<g
|
|
181
|
+
key={i}
|
|
182
|
+
onMouseDown={isDraggable ? (e) => handlePointDown(e, i) : undefined}
|
|
183
|
+
style={{ cursor: isDraggable ? 'grab' : 'default' }}
|
|
184
|
+
>
|
|
185
|
+
<circle cx={pos.x} cy={pos.y} r={r + 3} fill={`oklch(65% 0.25 ${hue} / 0.3)`} />
|
|
186
|
+
<circle cx={pos.x} cy={pos.y} r={r} fill={`oklch(65% 0.25 ${hue})`} stroke="white" strokeWidth={isBase ? 2.5 : 1.5} />
|
|
187
|
+
<text x={pos.x} y={pos.y} textAnchor="middle" dominantBaseline="central" fill="white" fontSize="7" fontWeight="600" pointerEvents="none">
|
|
188
|
+
{POINT_LABELS[i] || ''}
|
|
189
|
+
</text>
|
|
190
|
+
</g>
|
|
191
|
+
)
|
|
192
|
+
})}
|
|
193
|
+
</svg>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
@@ -11,9 +11,11 @@ export function DemoPage({ children, note }: { children: ReactNode; note?: React
|
|
|
11
11
|
{children}
|
|
12
12
|
</div>
|
|
13
13
|
{note && (
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
<div className="mt-4 sm:mt-6 bg-theme-accent border border-theme rounded-xl px-4 py-3 max-w-md text-center">
|
|
15
|
+
<p className="text-gray-400 text-xs sm:text-sm">
|
|
16
|
+
{note}
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
17
19
|
)}
|
|
18
20
|
</div>
|
|
19
21
|
)
|
|
@@ -168,7 +168,7 @@ export function LiveUploadWidget({
|
|
|
168
168
|
</div>
|
|
169
169
|
<div className="w-full h-3 bg-white/10 rounded-full overflow-hidden">
|
|
170
170
|
<div
|
|
171
|
-
className="h-full bg-gradient
|
|
171
|
+
className="h-full bg-theme-gradient transition-all"
|
|
172
172
|
style={{ width: `${live.$state.progress}%` }}
|
|
173
173
|
/>
|
|
174
174
|
</div>
|