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.
- package/CHANGELOG.md +132 -0
- 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/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +216 -107
- package/package.json +6 -5
- package/tsconfig.json +2 -1
- package/app/client/.live-stubs/LiveAdminPanel.js +0 -15
- package/app/client/.live-stubs/LiveCounter.js +0 -9
- package/app/client/.live-stubs/LiveForm.js +0 -11
- package/app/client/.live-stubs/LiveLocalCounter.js +0 -8
- package/app/client/.live-stubs/LivePingPong.js +0 -10
- package/app/client/.live-stubs/LiveRoomChat.js +0 -11
- package/app/client/.live-stubs/LiveSharedCounter.js +0 -10
- package/app/client/.live-stubs/LiveUpload.js +0 -15
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,138 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FluxStack will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.19.0] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
### Major Refactor: Plugin System Extracted to `@fluxstack/plugin-kit`
|
|
8
|
+
|
|
9
|
+
The plugin system (manager, registry, executor, discovery, dependency
|
|
10
|
+
manager, module resolver, types) has been extracted from `core/plugins/`
|
|
11
|
+
into the new standalone package `@fluxstack/plugin-kit`. This is the
|
|
12
|
+
same playbook we used for `@fluxstack/live` in 1.16.0 — implementation
|
|
13
|
+
lives in the lib, `core/plugins/` is now a thin re-export shim layer
|
|
14
|
+
for backwards compatibility with existing `@core/plugins/*` imports.
|
|
15
|
+
|
|
16
|
+
**Impact:** ~3,300 lines of plugin system implementation deleted from
|
|
17
|
+
the app, `core/plugins/` went from 9 runtime files (4017 lines) to
|
|
18
|
+
2 thin shims (types.ts + index.ts, ~200 lines combined). Single
|
|
19
|
+
source of truth for plugin types and runtime is now
|
|
20
|
+
`@fluxstack/plugin-kit`.
|
|
21
|
+
|
|
22
|
+
### Breaking Changes
|
|
23
|
+
|
|
24
|
+
- **Auto-discovery removed from `PluginManager.initialize()`** (plugin-kit 0.4.0).
|
|
25
|
+
The old code called `readdir('node_modules')` and `readdir('plugins')`
|
|
26
|
+
at startup to discover plugins. This silently broke in production
|
|
27
|
+
bundles where those directories don't exist, and prevented bundlers
|
|
28
|
+
from statically including plugin code. Host apps must now register
|
|
29
|
+
plugins explicitly via `framework.use(pluginObject)`. Dev and prod
|
|
30
|
+
are now identical; the bundler can tree-shake.
|
|
31
|
+
|
|
32
|
+
- **`PluginManager` constructor requires `settings` and `clientHooks`**
|
|
33
|
+
explicitly. These used to be read from the full config / imported
|
|
34
|
+
as module-level singletons. Now they're injected:
|
|
35
|
+
```ts
|
|
36
|
+
new PluginManager<FluxStackConfig>({
|
|
37
|
+
config: fullConfig,
|
|
38
|
+
settings: fullConfig.plugins,
|
|
39
|
+
logger: pluginLogger,
|
|
40
|
+
clientHooks: { register: (...) => pluginClientHooks.register(...) },
|
|
41
|
+
app: this.app,
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **`FluxStack.Plugin` generic over `TConfig`**. The legacy form
|
|
46
|
+
(no generic) still works and defaults to `unknown`. FluxStack's
|
|
47
|
+
shim specializes to `Plugin<FluxStackConfig>`.
|
|
48
|
+
|
|
49
|
+
- **Plugin classes discouraged, object literals are canonical**.
|
|
50
|
+
All built-in plugins and `@fluxstack/plugin-csrf-protection` use
|
|
51
|
+
`export const xxxPlugin: Plugin = { name, setup, ... }`. The
|
|
52
|
+
`class X implements Plugin` form is retired from the generators
|
|
53
|
+
but still technically accepted at runtime (it implements the
|
|
54
|
+
same interface).
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`@fluxstack/plugin-kit`** — new npm package. Types + runtime
|
|
59
|
+
for the plugin system. Published at 0.4.0 as of this release.
|
|
60
|
+
Used by FluxStack app and external plugin packages alike.
|
|
61
|
+
- **`tests/integration/framework/registered-plugins.test.ts`** (10 tests)
|
|
62
|
+
— end-to-end verification that the four plugins registered via
|
|
63
|
+
`framework.use()` in `app/server/index.ts` actually inject their
|
|
64
|
+
hooks at runtime. Catches the class of bug where plugin objects
|
|
65
|
+
are registered but no hook actually fires (the exact failure
|
|
66
|
+
mode of the old auto-discovery in prod bundles).
|
|
67
|
+
- **Static plugin registration everywhere**. `app/server/index.ts`
|
|
68
|
+
now imports `csrfProtectionPlugin` from `@fluxstack/plugin-csrf-protection`
|
|
69
|
+
directly and registers via `.use()`. Same pattern as the built-ins.
|
|
70
|
+
- **Startup banner reads from the PluginRegistry**. The banner line
|
|
71
|
+
`Plugins (N): ...` now lists exactly what `framework.getPluginRegistry().getAll()`
|
|
72
|
+
returns, instead of relying on each plugin manually pushing itself
|
|
73
|
+
to `globalThis.__fluxstackPlugins`. Backwards compatible — the old
|
|
74
|
+
global is still read as a fallback.
|
|
75
|
+
- **`@fluxstack/sdk` deprecated on npm** with a message pointing
|
|
76
|
+
users to `@fluxstack/plugin-kit`. The SDK was a static copy of
|
|
77
|
+
plugin types + a duplicate of `@fluxstack/config`; both have
|
|
78
|
+
canonical sources now.
|
|
79
|
+
- **`make:plugin` CLI** generates plain object literal plugins
|
|
80
|
+
importing from `@fluxstack/plugin-kit`. Identifier generation
|
|
81
|
+
fixed: `my-plugin` → `myPlugin` (not `myPluginPlugin`).
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
|
|
85
|
+
- **`@fluxstack/live` family bumped** from `^0.6.0` to `^0.7.1`.
|
|
86
|
+
Ships three follow-up bug fixes: opt-in `includeSelf` on `$room`
|
|
87
|
+
proxy emit (#15), `deepAssign` clones plain objects to break
|
|
88
|
+
external aliasing (#13), fail-loud protocol framing + telemetry (#7).
|
|
89
|
+
- **`create-fluxstack` README template**: removed the `loggerPlugin`
|
|
90
|
+
example (that plugin was deleted), replaced class-based plugin
|
|
91
|
+
example with object literal, added a hook reference table.
|
|
92
|
+
- **`plugins/README.md` template**: rewritten to reflect the static
|
|
93
|
+
`.use()` model. Explicitly calls out that plugins are NOT
|
|
94
|
+
auto-discovered. Points at `@fluxstack/plugin-csrf-protection`
|
|
95
|
+
as the living reference implementation.
|
|
96
|
+
- **Bundle prod size** grew from ~2.46 MB to ~3.34 MB because
|
|
97
|
+
`@fluxstack/plugin-csrf-protection` is now statically included.
|
|
98
|
+
Before, it was dynamically loaded via `readdir('node_modules')`
|
|
99
|
+
and the bundler couldn't see it.
|
|
100
|
+
- **Vite plugin startup banner label fixed** — `| Vite: embedded`
|
|
101
|
+
only shows in development now. In production the vite plugin
|
|
102
|
+
runs in static-fallback mode (serving `dist/client/`) and doesn't
|
|
103
|
+
actually run a Vite dev server, so the label was misleading.
|
|
104
|
+
|
|
105
|
+
### Removed
|
|
106
|
+
|
|
107
|
+
- **6934 lines of dead test code** across 24 test files. Orphaned
|
|
108
|
+
tests under `core/**/__tests__/*` never ran (vitest config was
|
|
109
|
+
`include: tests/**/*.test.ts`) and 14 of 18 were broken on
|
|
110
|
+
import when run directly. Also deleted 5 `describe.skip`'d test
|
|
111
|
+
suites under `tests/` with abandoned TODOs pointing at APIs
|
|
112
|
+
that were refactored away (Eden Treaty, ProjectCreator). Plus
|
|
113
|
+
`vitest.config.live.ts`, an orphan config pointing at a
|
|
114
|
+
directory that doesn't exist anymore.
|
|
115
|
+
- **`core/plugins/{manager,registry,executor,discovery,dependency-manager,module-resolver,config}.ts`**
|
|
116
|
+
deleted from FluxStack app. Implementation lives in
|
|
117
|
+
`@fluxstack/plugin-kit` now. `core/plugins/types.ts` and
|
|
118
|
+
`core/plugins/index.ts` kept as thin shim barrels that re-export
|
|
119
|
+
from the lib and specialize `<TConfig>` against `FluxStackConfig`.
|
|
120
|
+
- **Deprecated `configSchema` and `defaultConfig` fields** from the
|
|
121
|
+
Plugin interface. Were marked `@deprecated` and had no call sites.
|
|
122
|
+
Plugins use `@fluxstack/config` for declarative config instead.
|
|
123
|
+
- **`loggerPlugin`** — old built-in plugin that was already absent
|
|
124
|
+
from the real codebase but still referenced in generated templates.
|
|
125
|
+
Template references removed.
|
|
126
|
+
|
|
127
|
+
### Validation
|
|
128
|
+
|
|
129
|
+
- Typecheck (`bunx tsc --noEmit -p tsconfig.api-strict.json`) holds
|
|
130
|
+
at the 60 pre-existing errors baseline throughout every step —
|
|
131
|
+
zero regression across all four phases of the extraction.
|
|
132
|
+
- Full test suite: 42 test files, 652 passing, 5 skipped (all
|
|
133
|
+
intentional individual `it.skip` TODOs).
|
|
134
|
+
- Dev and prod both show `Plugins (4): swagger, live-components,
|
|
135
|
+
csrf-protection, vite` in the startup banner — identical output.
|
|
136
|
+
|
|
5
137
|
## [1.16.0] - 2026-03-13
|
|
6
138
|
|
|
7
139
|
### Major Refactor: Extract Live Components to Monorepo
|
package/app/client/src/App.tsx
CHANGED
|
@@ -21,11 +21,11 @@ function NotFoundPage() {
|
|
|
21
21
|
<h1 className="text-6xl font-black text-white mb-4">404</h1>
|
|
22
22
|
<p className="text-xl text-gray-400 mb-6">Pagina nao encontrada</p>
|
|
23
23
|
<p className="text-sm text-gray-500 mb-8">
|
|
24
|
-
O caminho <code className="text-
|
|
24
|
+
O caminho <code className="text-theme">{window.location.pathname}</code> nao existe.
|
|
25
25
|
</p>
|
|
26
26
|
<a
|
|
27
27
|
href="/"
|
|
28
|
-
className="px-6 py-3 rounded-xl bg-
|
|
28
|
+
className="px-6 py-3 rounded-xl bg-theme-muted border border-theme-active text-theme hover:bg-theme-muted transition-all"
|
|
29
29
|
>
|
|
30
30
|
Voltar ao inicio
|
|
31
31
|
</a>
|
|
@@ -129,7 +129,7 @@ function AppContent() {
|
|
|
129
129
|
path="/form"
|
|
130
130
|
element={
|
|
131
131
|
<DemoPage
|
|
132
|
-
note={<>? Este formul?rio usa <code className="text-
|
|
132
|
+
note={<>? Este formul?rio usa <code className="text-theme">Live.use()</code> - cada campo sincroniza automaticamente com o servidor!</>}
|
|
133
133
|
>
|
|
134
134
|
<FormDemo />
|
|
135
135
|
</DemoPage>
|
|
@@ -155,7 +155,7 @@ function AppContent() {
|
|
|
155
155
|
path="/shared-counter"
|
|
156
156
|
element={
|
|
157
157
|
<DemoPage
|
|
158
|
-
note={<>Contador compartilhado usando <code className="text-
|
|
158
|
+
note={<>Contador compartilhado usando <code className="text-theme">LiveRoom</code> - abra em varias abas!</>}
|
|
159
159
|
>
|
|
160
160
|
<SharedCounterDemo />
|
|
161
161
|
</DemoPage>
|
|
@@ -165,7 +165,7 @@ function AppContent() {
|
|
|
165
165
|
path="/room-chat"
|
|
166
166
|
element={
|
|
167
167
|
<DemoPage
|
|
168
|
-
note={<>Chat com múltiplas salas usando o sistema <code className="text-
|
|
168
|
+
note={<>Chat com múltiplas salas usando o sistema <code className="text-theme">$room</code>.</>}
|
|
169
169
|
>
|
|
170
170
|
<RoomChatDemo />
|
|
171
171
|
</DemoPage>
|
|
@@ -175,7 +175,7 @@ function AppContent() {
|
|
|
175
175
|
path="/auth"
|
|
176
176
|
element={
|
|
177
177
|
<DemoPage
|
|
178
|
-
note={<>🔒 Sistema de autenticação declarativo para Live Components com <code className="text-
|
|
178
|
+
note={<>🔒 Sistema de autenticação declarativo para Live Components com <code className="text-theme">$auth</code>!</>}
|
|
179
179
|
>
|
|
180
180
|
<AuthDemo />
|
|
181
181
|
</DemoPage>
|
|
@@ -185,7 +185,7 @@ function AppContent() {
|
|
|
185
185
|
path="/ping-pong"
|
|
186
186
|
element={
|
|
187
187
|
<DemoPage
|
|
188
|
-
note={<>Latency demo com <code className="text-
|
|
188
|
+
note={<>Latency demo com <code className="text-theme-secondary">msgpack</code> binary codec - mensagens binárias no WebSocket!</>}
|
|
189
189
|
>
|
|
190
190
|
<PingPongDemo />
|
|
191
191
|
</DemoPage>
|
|
@@ -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>
|