create-fluxstack 1.22.0 → 1.22.1

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 (38) hide show
  1. package/app/client/src/components/AppLayout.tsx +290 -290
  2. package/app/client/src/components/BackButton.tsx +16 -16
  3. package/app/client/src/components/DemoPage.tsx +135 -135
  4. package/app/client/src/framework/ClientOnly.tsx +14 -14
  5. package/app/client/src/framework/LivePage.tsx +55 -55
  6. package/app/client/src/framework/RscHomePage.tsx +125 -125
  7. package/app/client/src/framework/RscLink.tsx +31 -31
  8. package/app/client/src/framework/RscNav.tsx +42 -42
  9. package/app/client/src/framework/RscRoot.tsx +54 -54
  10. package/app/client/src/framework/csrf.ts +22 -22
  11. package/app/client/src/framework/entry.browser.tsx +51 -51
  12. package/app/client/src/framework/entry.rsc.tsx +41 -41
  13. package/app/client/src/framework/entry.ssr.tsx +12 -12
  14. package/app/client/src/framework/navigation.ts +29 -29
  15. package/app/client/src/framework/params.tsx +22 -22
  16. package/app/client/src/framework/routes.ts +143 -143
  17. package/app/client/src/framework/vite-rsc.d.ts +21 -21
  18. package/app/client/src/live/AuthDemo.tsx +270 -270
  19. package/app/client/src/live/CounterDemo.tsx +152 -152
  20. package/app/client/src/live/FormDemo.tsx +140 -140
  21. package/app/client/src/live/PingPongDemo.tsx +180 -180
  22. package/app/client/src/live/RoomChatDemo.tsx +397 -397
  23. package/app/client/src/pages/auth.tsx +13 -13
  24. package/app/client/src/pages/blog/[slug].tsx +23 -23
  25. package/app/client/src/pages/counter.tsx +15 -15
  26. package/app/client/src/pages/form.tsx +13 -13
  27. package/app/client/src/pages/index.tsx +9 -9
  28. package/app/client/src/pages/ping-pong.tsx +13 -13
  29. package/app/client/src/pages/room-chat.tsx +13 -13
  30. package/app/client/src/pages/shared-counter.tsx +13 -13
  31. package/app/client/src/pages/sobre.tsx +19 -19
  32. package/app/server/live/rooms/index.ts +14 -14
  33. package/core/plugins/built-in/rsc/index.ts +156 -156
  34. package/core/plugins/built-in/ssr/bun-asset-loader.ts +46 -46
  35. package/core/plugins/built-in/ssr/index.ts +143 -143
  36. package/core/plugins/built-in/ssr/registry.ts +38 -38
  37. package/core/utils/version.ts +1 -1
  38. package/package.json +1 -1
@@ -1,271 +1,271 @@
1
1
  'use client'
2
- import { useState } from 'react'
3
- import { Live, useLiveComponents } from '@/core/client'
4
- import { LiveCounter } from '@server/live/LiveCounter'
5
- import { LiveAdminPanel } from '@server/live/LiveAdminPanel'
6
- import { FaKey, FaLock, FaShieldHalved, FaUserPlus, FaUsers } from 'react-icons/fa6'
7
-
8
- function PublicCounter() {
9
- const counter = Live.use(LiveCounter, {
10
- room: 'public-counter',
11
- initialState: LiveCounter.defaultState,
12
- persistState: false,
13
- })
14
-
15
- return (
16
- <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
17
- <div className="mb-5 flex items-start justify-between gap-4">
18
- <div>
19
- <span className="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-xs text-emerald-200">
20
- Public
21
- </span>
22
- <h2 className="mt-4 text-xl font-semibold text-white">Public counter</h2>
23
- <p className="mt-2 text-sm leading-6 text-gray-500">Mounts without auth and exposes the proxy auth flag.</p>
24
- </div>
25
- <FaUsers className="text-theme" />
26
- </div>
27
-
28
- <div className="flex items-center justify-between rounded-lg border border-white/10 bg-black/30 p-4">
29
- <button onClick={() => counter.decrement()} className="h-10 w-10 rounded-lg border border-white/10 bg-white/[0.03] text-white">-</button>
30
- <span className="bg-theme-gradient bg-clip-text text-5xl font-semibold text-transparent">{counter.$state.count}</span>
31
- <button onClick={() => counter.increment()} className="h-10 w-10 rounded-lg bg-white text-black">+</button>
32
- </div>
33
-
34
- <div className="mt-4 rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2 text-xs text-gray-500">
35
- $authenticated: <code className="text-amber-200">{String(counter.$authenticated)}</code>
36
- </div>
37
- </section>
38
- )
39
- }
40
-
41
- function AdminPanel() {
42
- const [newUserName, setNewUserName] = useState('')
43
- const [error, setError] = useState<string | null>(null)
44
- const panel = Live.use(LiveAdminPanel, { persistState: false })
45
-
46
- if (panel.$error?.includes('AUTH_DENIED')) {
47
- return (
48
- <section className="rounded-lg border border-red-400/20 bg-red-400/10 p-5">
49
- <div className="mb-3 flex items-center gap-2 text-red-200">
50
- <FaLock />
51
- <h2 className="text-lg font-semibold">Admin panel locked</h2>
52
- </div>
53
- <p className="text-sm leading-6 text-red-100/80">{panel.$error}</p>
54
- <p className="mt-2 text-xs text-gray-400">Use <code className="text-red-100">admin-token</code> to mount this protected component.</p>
55
- </section>
56
- )
57
- }
58
-
59
- if (panel.$status === 'mounting' || panel.$loading) {
60
- return (
61
- <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5">
62
- <div className="flex items-center gap-3 text-sm text-gray-400">
63
- <div className="h-4 w-4 rounded-full border-2 border-theme border-t-transparent animate-spin" />
64
- Mounting protected component...
65
- </div>
66
- </section>
67
- )
68
- }
69
-
70
- const handleAddUser = async () => {
71
- if (!newUserName.trim()) return
72
- try {
73
- await panel.addUser({ name: newUserName.trim(), role: 'user' })
74
- setNewUserName('')
75
- setError(null)
76
- } catch (e: any) {
77
- setError(e.message)
78
- }
79
- }
80
-
81
- const handleDeleteUser = async (userId: string) => {
82
- try {
83
- await panel.deleteUser({ userId })
84
- setError(null)
85
- } catch (e: any) {
86
- setError(e.message)
87
- }
88
- }
89
-
90
- return (
91
- <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
92
- <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
93
- <div>
94
- <span className="rounded-full border border-theme-active bg-theme-muted px-2.5 py-1 text-xs text-theme">
95
- Protected
96
- </span>
97
- <h2 className="mt-4 text-xl font-semibold text-white">Admin panel</h2>
98
- <p className="mt-2 text-sm leading-6 text-gray-500">Requires <code className="text-theme">admin</code> role and action permissions.</p>
99
- </div>
100
- <div className="rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2 text-right text-xs text-gray-500">
101
- <div>User: <span className="text-emerald-200">{panel.$state.currentUser || '...'}</span></div>
102
- <div className="mt-1">Roles: <span className="text-amber-200">{panel.$state.currentRoles?.join(', ') || '...'}</span></div>
103
- </div>
104
- </div>
105
-
106
- {error && (
107
- <div className="mb-4 rounded-lg border border-red-400/20 bg-red-400/10 px-3 py-2 text-sm text-red-200">
108
- {error}
109
- </div>
110
- )}
111
-
112
- <div className="space-y-2">
113
- {(panel.$state.users ?? []).map(user => (
114
- <div key={user.id} className="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2">
115
- <div>
116
- <p className="text-sm font-medium text-white">{user.name}</p>
117
- <p className="text-xs text-gray-500">{user.role}</p>
118
- </div>
119
- <button
120
- onClick={() => handleDeleteUser(user.id)}
121
- className="rounded-lg border border-red-400/20 bg-red-400/10 px-3 py-1.5 text-xs font-medium text-red-200 transition hover:bg-red-400/15"
122
- title="Requires users.delete permission"
123
- >
124
- Delete
125
- </button>
126
- </div>
127
- ))}
128
- </div>
129
-
130
- <div className="mt-4 flex gap-2">
131
- <input
132
- value={newUserName}
133
- onChange={e => setNewUserName(e.target.value)}
134
- onKeyDown={e => e.key === 'Enter' && handleAddUser()}
135
- placeholder="User name..."
136
- className="min-w-0 flex-1 input-theme"
137
- />
138
- <button
139
- onClick={handleAddUser}
140
- className="inline-flex h-11 items-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition hover:bg-gray-200"
141
- >
142
- <FaUserPlus className="h-3.5 w-3.5" />
143
- Add
144
- </button>
145
- </div>
146
-
147
- {(panel.$state.audit?.length ?? 0) > 0 && (
148
- <div className="mt-5 border-t border-white/10 pt-4">
149
- <div className="mb-2 flex items-center justify-between">
150
- <h3 className="text-sm font-semibold text-gray-300">Audit log</h3>
151
- <button onClick={() => panel.clearAudit()} className="rounded bg-white/[0.04] px-2 py-1 text-xs text-gray-400">
152
- Clear
153
- </button>
154
- </div>
155
- <div className="max-h-32 space-y-1 overflow-auto">
156
- {(panel.$state.audit ?? []).map((entry, i) => (
157
- <div key={i} className="text-xs text-gray-500">
158
- <span className="text-gray-400">{new Date(entry.timestamp).toLocaleTimeString()}</span>
159
- {' '}<span className="text-blue-300">{entry.action}</span>
160
- {' '}by <span className="text-emerald-300">{entry.performedBy}</span>
161
- {entry.target && <> on <span className="text-amber-300">{entry.target}</span></>}
162
- </div>
163
- ))}
164
- </div>
165
- </div>
166
- )}
167
- </section>
168
- )
169
- }
170
-
171
- function AuthControls() {
172
- const { authenticated, authenticate, reconnect } = useLiveComponents()
173
- const [token, setToken] = useState('')
174
- const [isLoggingIn, setIsLoggingIn] = useState(false)
175
-
176
- const handleLogin = async () => {
177
- if (!token.trim()) return
178
- setIsLoggingIn(true)
179
- await authenticate({ token: token.trim() })
180
- setIsLoggingIn(false)
181
- }
182
-
183
- const handleLogout = () => {
184
- setToken('')
185
- reconnect()
186
- }
187
-
188
- return (
189
- <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
190
- <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
191
- <div>
192
- <div className="mb-3 inline-flex items-center gap-2 rounded-full border border-theme-active bg-theme-muted px-3 py-1 text-xs text-theme">
193
- <FaKey className="h-3 w-3" />
194
- WebSocket auth
195
- </div>
196
- <h2 className="text-2xl font-semibold tracking-tight text-white">Authenticate the Live connection</h2>
197
- <p className="mt-2 text-sm leading-6 text-gray-500">Pick a development token and remount protected components with the new auth context.</p>
198
- </div>
199
- <span className={`inline-flex w-fit items-center gap-2 rounded-full border px-3 py-1 text-xs ${
200
- authenticated
201
- ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
202
- : 'border-white/10 bg-white/[0.03] text-gray-400'
203
- }`}>
204
- <span className={`h-1.5 w-1.5 rounded-full ${authenticated ? 'bg-emerald-300' : 'bg-gray-500'}`} />
205
- {authenticated ? 'Authenticated' : 'Anonymous'}
206
- </span>
207
- </div>
208
-
209
- <div className="flex flex-col gap-3 sm:flex-row">
210
- <input
211
- value={token}
212
- onChange={e => setToken(e.target.value)}
213
- onKeyDown={e => e.key === 'Enter' && handleLogin()}
214
- placeholder="admin-token, user-token, mod-token"
215
- className="min-w-0 flex-1 input-theme"
216
- />
217
- <button onClick={handleLogin} disabled={isLoggingIn} className="h-11 rounded-lg bg-white px-5 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50">
218
- {isLoggingIn ? 'Authenticating...' : 'Login'}
219
- </button>
220
- {authenticated && (
221
- <button onClick={handleLogout} className="h-11 rounded-lg border border-red-400/20 bg-red-400/10 px-5 text-sm font-semibold text-red-200 transition hover:bg-red-400/15">
222
- Logout
223
- </button>
224
- )}
225
- </div>
226
-
227
- <div className="mt-4 grid gap-2 sm:grid-cols-3">
228
- {['admin-token', 'user-token', 'mod-token'].map(value => (
229
- <button
230
- key={value}
231
- onClick={() => setToken(value)}
232
- className="rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 text-xs font-medium text-gray-300 transition hover:bg-white/[0.06]"
233
- >
234
- {value}
235
- </button>
236
- ))}
237
- </div>
238
- </section>
239
- )
240
- }
241
-
242
- export function AuthDemo() {
243
- return (
244
- <div className="w-full max-w-5xl space-y-4">
245
- <AuthControls />
246
-
247
- <div className="grid gap-4 lg:grid-cols-[0.82fr_1.18fr]">
248
- <PublicCounter />
249
- <AdminPanel />
250
- </div>
251
-
252
- <section className="grid gap-3 rounded-lg border border-white/10 bg-black/25 p-4 text-xs text-gray-500 md:grid-cols-3">
253
- <div className="rounded-lg border border-white/10 bg-white/[0.025] p-3">
254
- <FaShieldHalved className="mb-3 text-theme" />
255
- <p className="font-semibold text-gray-300">Component guard</p>
256
- <code className="mt-2 block text-theme">static auth = {'{ required: true }'}</code>
257
- </div>
258
- <div className="rounded-lg border border-white/10 bg-white/[0.025] p-3">
259
- <FaLock className="mb-3 text-theme" />
260
- <p className="font-semibold text-gray-300">Action guard</p>
261
- <code className="mt-2 block text-theme">permissions: ['users.delete']</code>
262
- </div>
263
- <div className="rounded-lg border border-white/10 bg-white/[0.025] p-3">
264
- <FaKey className="mb-3 text-theme" />
265
- <p className="font-semibold text-gray-300">Client auth</p>
266
- <code className="mt-2 block text-theme">authenticate({'{ token }'})</code>
267
- </div>
268
- </section>
269
- </div>
270
- )
271
- }
2
+ import { useState } from 'react'
3
+ import { Live, useLiveComponents } from '@/core/client'
4
+ import { LiveCounter } from '@server/live/LiveCounter'
5
+ import { LiveAdminPanel } from '@server/live/LiveAdminPanel'
6
+ import { FaKey, FaLock, FaShieldHalved, FaUserPlus, FaUsers } from 'react-icons/fa6'
7
+
8
+ function PublicCounter() {
9
+ const counter = Live.use(LiveCounter, {
10
+ room: 'public-counter',
11
+ initialState: LiveCounter.defaultState,
12
+ persistState: false,
13
+ })
14
+
15
+ return (
16
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
17
+ <div className="mb-5 flex items-start justify-between gap-4">
18
+ <div>
19
+ <span className="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-xs text-emerald-200">
20
+ Public
21
+ </span>
22
+ <h2 className="mt-4 text-xl font-semibold text-white">Public counter</h2>
23
+ <p className="mt-2 text-sm leading-6 text-gray-500">Mounts without auth and exposes the proxy auth flag.</p>
24
+ </div>
25
+ <FaUsers className="text-theme" />
26
+ </div>
27
+
28
+ <div className="flex items-center justify-between rounded-lg border border-white/10 bg-black/30 p-4">
29
+ <button onClick={() => counter.decrement()} className="h-10 w-10 rounded-lg border border-white/10 bg-white/[0.03] text-white">-</button>
30
+ <span className="bg-theme-gradient bg-clip-text text-5xl font-semibold text-transparent">{counter.$state.count}</span>
31
+ <button onClick={() => counter.increment()} className="h-10 w-10 rounded-lg bg-white text-black">+</button>
32
+ </div>
33
+
34
+ <div className="mt-4 rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2 text-xs text-gray-500">
35
+ $authenticated: <code className="text-amber-200">{String(counter.$authenticated)}</code>
36
+ </div>
37
+ </section>
38
+ )
39
+ }
40
+
41
+ function AdminPanel() {
42
+ const [newUserName, setNewUserName] = useState('')
43
+ const [error, setError] = useState<string | null>(null)
44
+ const panel = Live.use(LiveAdminPanel, { persistState: false })
45
+
46
+ if (panel.$error?.includes('AUTH_DENIED')) {
47
+ return (
48
+ <section className="rounded-lg border border-red-400/20 bg-red-400/10 p-5">
49
+ <div className="mb-3 flex items-center gap-2 text-red-200">
50
+ <FaLock />
51
+ <h2 className="text-lg font-semibold">Admin panel locked</h2>
52
+ </div>
53
+ <p className="text-sm leading-6 text-red-100/80">{panel.$error}</p>
54
+ <p className="mt-2 text-xs text-gray-400">Use <code className="text-red-100">admin-token</code> to mount this protected component.</p>
55
+ </section>
56
+ )
57
+ }
58
+
59
+ if (panel.$status === 'mounting' || panel.$loading) {
60
+ return (
61
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5">
62
+ <div className="flex items-center gap-3 text-sm text-gray-400">
63
+ <div className="h-4 w-4 rounded-full border-2 border-theme border-t-transparent animate-spin" />
64
+ Mounting protected component...
65
+ </div>
66
+ </section>
67
+ )
68
+ }
69
+
70
+ const handleAddUser = async () => {
71
+ if (!newUserName.trim()) return
72
+ try {
73
+ await panel.addUser({ name: newUserName.trim(), role: 'user' })
74
+ setNewUserName('')
75
+ setError(null)
76
+ } catch (e: any) {
77
+ setError(e.message)
78
+ }
79
+ }
80
+
81
+ const handleDeleteUser = async (userId: string) => {
82
+ try {
83
+ await panel.deleteUser({ userId })
84
+ setError(null)
85
+ } catch (e: any) {
86
+ setError(e.message)
87
+ }
88
+ }
89
+
90
+ return (
91
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
92
+ <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
93
+ <div>
94
+ <span className="rounded-full border border-theme-active bg-theme-muted px-2.5 py-1 text-xs text-theme">
95
+ Protected
96
+ </span>
97
+ <h2 className="mt-4 text-xl font-semibold text-white">Admin panel</h2>
98
+ <p className="mt-2 text-sm leading-6 text-gray-500">Requires <code className="text-theme">admin</code> role and action permissions.</p>
99
+ </div>
100
+ <div className="rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2 text-right text-xs text-gray-500">
101
+ <div>User: <span className="text-emerald-200">{panel.$state.currentUser || '...'}</span></div>
102
+ <div className="mt-1">Roles: <span className="text-amber-200">{panel.$state.currentRoles?.join(', ') || '...'}</span></div>
103
+ </div>
104
+ </div>
105
+
106
+ {error && (
107
+ <div className="mb-4 rounded-lg border border-red-400/20 bg-red-400/10 px-3 py-2 text-sm text-red-200">
108
+ {error}
109
+ </div>
110
+ )}
111
+
112
+ <div className="space-y-2">
113
+ {(panel.$state.users ?? []).map(user => (
114
+ <div key={user.id} className="flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2">
115
+ <div>
116
+ <p className="text-sm font-medium text-white">{user.name}</p>
117
+ <p className="text-xs text-gray-500">{user.role}</p>
118
+ </div>
119
+ <button
120
+ onClick={() => handleDeleteUser(user.id)}
121
+ className="rounded-lg border border-red-400/20 bg-red-400/10 px-3 py-1.5 text-xs font-medium text-red-200 transition hover:bg-red-400/15"
122
+ title="Requires users.delete permission"
123
+ >
124
+ Delete
125
+ </button>
126
+ </div>
127
+ ))}
128
+ </div>
129
+
130
+ <div className="mt-4 flex gap-2">
131
+ <input
132
+ value={newUserName}
133
+ onChange={e => setNewUserName(e.target.value)}
134
+ onKeyDown={e => e.key === 'Enter' && handleAddUser()}
135
+ placeholder="User name..."
136
+ className="min-w-0 flex-1 input-theme"
137
+ />
138
+ <button
139
+ onClick={handleAddUser}
140
+ className="inline-flex h-11 items-center gap-2 rounded-lg bg-white px-4 text-sm font-semibold text-black transition hover:bg-gray-200"
141
+ >
142
+ <FaUserPlus className="h-3.5 w-3.5" />
143
+ Add
144
+ </button>
145
+ </div>
146
+
147
+ {(panel.$state.audit?.length ?? 0) > 0 && (
148
+ <div className="mt-5 border-t border-white/10 pt-4">
149
+ <div className="mb-2 flex items-center justify-between">
150
+ <h3 className="text-sm font-semibold text-gray-300">Audit log</h3>
151
+ <button onClick={() => panel.clearAudit()} className="rounded bg-white/[0.04] px-2 py-1 text-xs text-gray-400">
152
+ Clear
153
+ </button>
154
+ </div>
155
+ <div className="max-h-32 space-y-1 overflow-auto">
156
+ {(panel.$state.audit ?? []).map((entry, i) => (
157
+ <div key={i} className="text-xs text-gray-500">
158
+ <span className="text-gray-400">{new Date(entry.timestamp).toLocaleTimeString()}</span>
159
+ {' '}<span className="text-blue-300">{entry.action}</span>
160
+ {' '}by <span className="text-emerald-300">{entry.performedBy}</span>
161
+ {entry.target && <> on <span className="text-amber-300">{entry.target}</span></>}
162
+ </div>
163
+ ))}
164
+ </div>
165
+ </div>
166
+ )}
167
+ </section>
168
+ )
169
+ }
170
+
171
+ function AuthControls() {
172
+ const { authenticated, authenticate, reconnect } = useLiveComponents()
173
+ const [token, setToken] = useState('')
174
+ const [isLoggingIn, setIsLoggingIn] = useState(false)
175
+
176
+ const handleLogin = async () => {
177
+ if (!token.trim()) return
178
+ setIsLoggingIn(true)
179
+ await authenticate({ token: token.trim() })
180
+ setIsLoggingIn(false)
181
+ }
182
+
183
+ const handleLogout = () => {
184
+ setToken('')
185
+ reconnect()
186
+ }
187
+
188
+ return (
189
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
190
+ <div className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
191
+ <div>
192
+ <div className="mb-3 inline-flex items-center gap-2 rounded-full border border-theme-active bg-theme-muted px-3 py-1 text-xs text-theme">
193
+ <FaKey className="h-3 w-3" />
194
+ WebSocket auth
195
+ </div>
196
+ <h2 className="text-2xl font-semibold tracking-tight text-white">Authenticate the Live connection</h2>
197
+ <p className="mt-2 text-sm leading-6 text-gray-500">Pick a development token and remount protected components with the new auth context.</p>
198
+ </div>
199
+ <span className={`inline-flex w-fit items-center gap-2 rounded-full border px-3 py-1 text-xs ${
200
+ authenticated
201
+ ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
202
+ : 'border-white/10 bg-white/[0.03] text-gray-400'
203
+ }`}>
204
+ <span className={`h-1.5 w-1.5 rounded-full ${authenticated ? 'bg-emerald-300' : 'bg-gray-500'}`} />
205
+ {authenticated ? 'Authenticated' : 'Anonymous'}
206
+ </span>
207
+ </div>
208
+
209
+ <div className="flex flex-col gap-3 sm:flex-row">
210
+ <input
211
+ value={token}
212
+ onChange={e => setToken(e.target.value)}
213
+ onKeyDown={e => e.key === 'Enter' && handleLogin()}
214
+ placeholder="admin-token, user-token, mod-token"
215
+ className="min-w-0 flex-1 input-theme"
216
+ />
217
+ <button onClick={handleLogin} disabled={isLoggingIn} className="h-11 rounded-lg bg-white px-5 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50">
218
+ {isLoggingIn ? 'Authenticating...' : 'Login'}
219
+ </button>
220
+ {authenticated && (
221
+ <button onClick={handleLogout} className="h-11 rounded-lg border border-red-400/20 bg-red-400/10 px-5 text-sm font-semibold text-red-200 transition hover:bg-red-400/15">
222
+ Logout
223
+ </button>
224
+ )}
225
+ </div>
226
+
227
+ <div className="mt-4 grid gap-2 sm:grid-cols-3">
228
+ {['admin-token', 'user-token', 'mod-token'].map(value => (
229
+ <button
230
+ key={value}
231
+ onClick={() => setToken(value)}
232
+ className="rounded-lg border border-white/10 bg-white/[0.03] px-3 py-2 text-xs font-medium text-gray-300 transition hover:bg-white/[0.06]"
233
+ >
234
+ {value}
235
+ </button>
236
+ ))}
237
+ </div>
238
+ </section>
239
+ )
240
+ }
241
+
242
+ export function AuthDemo() {
243
+ return (
244
+ <div className="w-full max-w-5xl space-y-4">
245
+ <AuthControls />
246
+
247
+ <div className="grid gap-4 lg:grid-cols-[0.82fr_1.18fr]">
248
+ <PublicCounter />
249
+ <AdminPanel />
250
+ </div>
251
+
252
+ <section className="grid gap-3 rounded-lg border border-white/10 bg-black/25 p-4 text-xs text-gray-500 md:grid-cols-3">
253
+ <div className="rounded-lg border border-white/10 bg-white/[0.025] p-3">
254
+ <FaShieldHalved className="mb-3 text-theme" />
255
+ <p className="font-semibold text-gray-300">Component guard</p>
256
+ <code className="mt-2 block text-theme">static auth = {'{ required: true }'}</code>
257
+ </div>
258
+ <div className="rounded-lg border border-white/10 bg-white/[0.025] p-3">
259
+ <FaLock className="mb-3 text-theme" />
260
+ <p className="font-semibold text-gray-300">Action guard</p>
261
+ <code className="mt-2 block text-theme">permissions: ['users.delete']</code>
262
+ </div>
263
+ <div className="rounded-lg border border-white/10 bg-white/[0.025] p-3">
264
+ <FaKey className="mb-3 text-theme" />
265
+ <p className="font-semibold text-gray-300">Client auth</p>
266
+ <code className="mt-2 block text-theme">authenticate({'{ token }'})</code>
267
+ </div>
268
+ </section>
269
+ </div>
270
+ )
271
+ }