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,152 +1,152 @@
1
- 'use client'
2
- import { useMemo } from 'react'
3
- import { Live } from '@/core/client'
4
- import { LiveCounter } from '@server/live/LiveCounter'
5
- import { LiveLocalCounter } from '@server/live/LiveLocalCounter'
6
- import { FaMinus, FaPlus, FaRotateRight, FaUsers } from 'react-icons/fa6'
7
-
8
- type CounterProxy = ReturnType<typeof Live.use>
9
-
10
- function ConnectionPill({ connected }: { connected: boolean }) {
11
- return (
12
- <span className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs ${
13
- connected
14
- ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
15
- : 'border-red-400/25 bg-red-400/10 text-red-200'
16
- }`}>
17
- <span className={`h-1.5 w-1.5 rounded-full ${connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
18
- {connected ? 'Connected' : 'Offline'}
19
- </span>
20
- )
21
- }
22
-
23
- function CounterCard({
24
- title,
25
- description,
26
- mode,
27
- counter,
28
- accent = 'theme',
29
- }: {
30
- title: string
31
- description: string
32
- mode: string
33
- counter: CounterProxy
34
- accent?: 'theme' | 'warm'
35
- }) {
36
- const valueClass = accent === 'warm'
37
- ? 'bg-gradient-to-r from-amber-300 via-orange-300 to-rose-300'
38
- : 'bg-theme-gradient'
39
-
40
- return (
41
- <article className="flex min-h-[430px] w-full flex-col rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
42
- <div className="flex items-start justify-between gap-4">
43
- <div>
44
- <span className="rounded-full border border-white/10 bg-white/[0.03] px-2.5 py-1 text-xs text-gray-400">
45
- {mode}
46
- </span>
47
- <h2 className="mt-4 text-xl font-semibold tracking-tight text-white">{title}</h2>
48
- <p className="mt-2 text-sm leading-6 text-gray-500">{description}</p>
49
- </div>
50
- {'$connected' in counter && <ConnectionPill connected={counter.$connected} />}
51
- </div>
52
-
53
- <div className="flex flex-1 items-center justify-center py-8">
54
- <div className={`bg-clip-text text-7xl font-semibold tabular-nums tracking-tight text-transparent sm:text-8xl ${valueClass}`}>
55
- {counter.$state.count}
56
- </div>
57
- </div>
58
-
59
- {'connectedUsers' in counter.$state && (
60
- <div className="mb-5 flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2 text-xs text-gray-400">
61
- <span className="inline-flex items-center gap-2">
62
- <FaUsers className="text-theme" />
63
- Users in room
64
- </span>
65
- <span className="font-mono text-white">{counter.$state.connectedUsers}</span>
66
- </div>
67
- )}
68
-
69
- {counter.$state.lastUpdatedBy && (
70
- <p className="mb-5 truncate text-xs text-gray-500">
71
- Last update: <span className="text-gray-300">{counter.$state.lastUpdatedBy}</span>
72
- </p>
73
- )}
74
-
75
- <div className="grid grid-cols-[56px_1fr_56px] gap-3">
76
- <button
77
- onClick={() => counter.decrement()}
78
- disabled={counter.$loading}
79
- className="flex h-12 items-center justify-center rounded-lg border border-white/10 bg-white/[0.03] text-gray-200 transition hover:bg-white/[0.07] disabled:opacity-50"
80
- aria-label={`Decrease ${title}`}
81
- >
82
- <FaMinus />
83
- </button>
84
- <button
85
- onClick={() => counter.reset()}
86
- disabled={counter.$loading}
87
- className="inline-flex h-12 items-center justify-center gap-2 rounded-lg border border-theme-active bg-theme-muted px-4 text-sm font-semibold text-theme transition hover:shadow-theme disabled:opacity-50"
88
- >
89
- <FaRotateRight className="h-3.5 w-3.5" />
90
- Reset
91
- </button>
92
- <button
93
- onClick={() => counter.increment()}
94
- disabled={counter.$loading}
95
- className="flex h-12 items-center justify-center rounded-lg bg-white text-black transition hover:bg-gray-200 disabled:opacity-50"
96
- aria-label={`Increase ${title}`}
97
- >
98
- <FaPlus />
99
- </button>
100
- </div>
101
- </article>
102
- )
103
- }
104
-
105
- export function CounterDemo() {
106
- const isolatedRoom = useMemo(() => {
107
- if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
108
- return `local-${crypto.randomUUID()}`
109
- }
110
- return `local-${Math.random().toString(36).slice(2)}`
111
- }, [])
112
-
113
- const localCounter = Live.use(LiveLocalCounter, {
114
- initialState: LiveLocalCounter.defaultState,
115
- persistState: false,
116
- })
117
-
118
- const isolatedCounter = Live.use(LiveCounter, {
119
- room: isolatedRoom,
120
- initialState: LiveCounter.defaultState,
121
- persistState: false,
122
- })
123
-
124
- const sharedCounter = Live.use(LiveCounter, {
125
- room: 'global-counter',
126
- initialState: LiveCounter.defaultState,
127
- })
128
-
129
- return (
130
- <div className="grid w-full gap-4 lg:grid-cols-3">
131
- <CounterCard
132
- title="Local state"
133
- description="A component-local counter for quick state updates without a shared room."
134
- mode="No room"
135
- counter={localCounter}
136
- accent="warm"
137
- />
138
- <CounterCard
139
- title="Isolated room"
140
- description="A private room per tab. The server owns the state, but the session is isolated."
141
- mode="Private room"
142
- counter={isolatedCounter}
143
- />
144
- <CounterCard
145
- title="Shared room"
146
- description="One global room. Open another tab and every client sees the same counter."
147
- mode="Global room"
148
- counter={sharedCounter}
149
- />
150
- </div>
151
- )
152
- }
1
+ 'use client'
2
+ import { useMemo } from 'react'
3
+ import { Live } from '@/core/client'
4
+ import { LiveCounter } from '@server/live/LiveCounter'
5
+ import { LiveLocalCounter } from '@server/live/LiveLocalCounter'
6
+ import { FaMinus, FaPlus, FaRotateRight, FaUsers } from 'react-icons/fa6'
7
+
8
+ type CounterProxy = ReturnType<typeof Live.use>
9
+
10
+ function ConnectionPill({ connected }: { connected: boolean }) {
11
+ return (
12
+ <span className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs ${
13
+ connected
14
+ ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
15
+ : 'border-red-400/25 bg-red-400/10 text-red-200'
16
+ }`}>
17
+ <span className={`h-1.5 w-1.5 rounded-full ${connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
18
+ {connected ? 'Connected' : 'Offline'}
19
+ </span>
20
+ )
21
+ }
22
+
23
+ function CounterCard({
24
+ title,
25
+ description,
26
+ mode,
27
+ counter,
28
+ accent = 'theme',
29
+ }: {
30
+ title: string
31
+ description: string
32
+ mode: string
33
+ counter: CounterProxy
34
+ accent?: 'theme' | 'warm'
35
+ }) {
36
+ const valueClass = accent === 'warm'
37
+ ? 'bg-gradient-to-r from-amber-300 via-orange-300 to-rose-300'
38
+ : 'bg-theme-gradient'
39
+
40
+ return (
41
+ <article className="flex min-h-[430px] w-full flex-col rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20">
42
+ <div className="flex items-start justify-between gap-4">
43
+ <div>
44
+ <span className="rounded-full border border-white/10 bg-white/[0.03] px-2.5 py-1 text-xs text-gray-400">
45
+ {mode}
46
+ </span>
47
+ <h2 className="mt-4 text-xl font-semibold tracking-tight text-white">{title}</h2>
48
+ <p className="mt-2 text-sm leading-6 text-gray-500">{description}</p>
49
+ </div>
50
+ {'$connected' in counter && <ConnectionPill connected={counter.$connected} />}
51
+ </div>
52
+
53
+ <div className="flex flex-1 items-center justify-center py-8">
54
+ <div className={`bg-clip-text text-7xl font-semibold tabular-nums tracking-tight text-transparent sm:text-8xl ${valueClass}`}>
55
+ {counter.$state.count}
56
+ </div>
57
+ </div>
58
+
59
+ {'connectedUsers' in counter.$state && (
60
+ <div className="mb-5 flex items-center justify-between rounded-lg border border-white/10 bg-white/[0.025] px-3 py-2 text-xs text-gray-400">
61
+ <span className="inline-flex items-center gap-2">
62
+ <FaUsers className="text-theme" />
63
+ Users in room
64
+ </span>
65
+ <span className="font-mono text-white">{counter.$state.connectedUsers}</span>
66
+ </div>
67
+ )}
68
+
69
+ {counter.$state.lastUpdatedBy && (
70
+ <p className="mb-5 truncate text-xs text-gray-500">
71
+ Last update: <span className="text-gray-300">{counter.$state.lastUpdatedBy}</span>
72
+ </p>
73
+ )}
74
+
75
+ <div className="grid grid-cols-[56px_1fr_56px] gap-3">
76
+ <button
77
+ onClick={() => counter.decrement()}
78
+ disabled={counter.$loading}
79
+ className="flex h-12 items-center justify-center rounded-lg border border-white/10 bg-white/[0.03] text-gray-200 transition hover:bg-white/[0.07] disabled:opacity-50"
80
+ aria-label={`Decrease ${title}`}
81
+ >
82
+ <FaMinus />
83
+ </button>
84
+ <button
85
+ onClick={() => counter.reset()}
86
+ disabled={counter.$loading}
87
+ className="inline-flex h-12 items-center justify-center gap-2 rounded-lg border border-theme-active bg-theme-muted px-4 text-sm font-semibold text-theme transition hover:shadow-theme disabled:opacity-50"
88
+ >
89
+ <FaRotateRight className="h-3.5 w-3.5" />
90
+ Reset
91
+ </button>
92
+ <button
93
+ onClick={() => counter.increment()}
94
+ disabled={counter.$loading}
95
+ className="flex h-12 items-center justify-center rounded-lg bg-white text-black transition hover:bg-gray-200 disabled:opacity-50"
96
+ aria-label={`Increase ${title}`}
97
+ >
98
+ <FaPlus />
99
+ </button>
100
+ </div>
101
+ </article>
102
+ )
103
+ }
104
+
105
+ export function CounterDemo() {
106
+ const isolatedRoom = useMemo(() => {
107
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
108
+ return `local-${crypto.randomUUID()}`
109
+ }
110
+ return `local-${Math.random().toString(36).slice(2)}`
111
+ }, [])
112
+
113
+ const localCounter = Live.use(LiveLocalCounter, {
114
+ initialState: LiveLocalCounter.defaultState,
115
+ persistState: false,
116
+ })
117
+
118
+ const isolatedCounter = Live.use(LiveCounter, {
119
+ room: isolatedRoom,
120
+ initialState: LiveCounter.defaultState,
121
+ persistState: false,
122
+ })
123
+
124
+ const sharedCounter = Live.use(LiveCounter, {
125
+ room: 'global-counter',
126
+ initialState: LiveCounter.defaultState,
127
+ })
128
+
129
+ return (
130
+ <div className="grid w-full gap-4 lg:grid-cols-3">
131
+ <CounterCard
132
+ title="Local state"
133
+ description="A component-local counter for quick state updates without a shared room."
134
+ mode="No room"
135
+ counter={localCounter}
136
+ accent="warm"
137
+ />
138
+ <CounterCard
139
+ title="Isolated room"
140
+ description="A private room per tab. The server owns the state, but the session is isolated."
141
+ mode="Private room"
142
+ counter={isolatedCounter}
143
+ />
144
+ <CounterCard
145
+ title="Shared room"
146
+ description="One global room. Open another tab and every client sees the same counter."
147
+ mode="Global room"
148
+ counter={sharedCounter}
149
+ />
150
+ </div>
151
+ )
152
+ }
@@ -1,141 +1,141 @@
1
1
  'use client'
2
- import { Live } from '@/core/client'
3
- import { LiveForm } from '@server/live/LiveForm'
4
- import { FaCheck, FaCode, FaEnvelope, FaRegMessage, FaUser } from 'react-icons/fa6'
5
-
6
- function FieldHint({ children }: { children: string }) {
7
- return (
8
- <span className="rounded-full border border-white/10 bg-white/[0.03] px-2 py-0.5 text-[11px] text-gray-500">
9
- {children}
10
- </span>
11
- )
12
- }
13
-
14
- export function FormDemo() {
15
- const form = Live.use(LiveForm)
16
-
17
- if (form.submitted) {
18
- return (
19
- <div className="w-full max-w-2xl rounded-lg border border-emerald-400/20 bg-emerald-400/10 p-6 text-center shadow-2xl shadow-black/20">
20
- <div className="mx-auto mb-5 flex h-12 w-12 items-center justify-center rounded-lg bg-emerald-400 text-black">
21
- <FaCheck />
22
- </div>
23
- <h2 className="text-2xl font-semibold text-white">Message received</h2>
24
- <p className="mt-2 text-sm leading-6 text-gray-300">
25
- Thanks, <span className="text-emerald-200">{form.name || 'there'}</span>. The server state was updated and submitted.
26
- </p>
27
- <p className="mt-3 text-xs text-gray-500">
28
- Submitted at {form.submittedAt ? new Date(form.submittedAt).toLocaleString() : '-'}
29
- </p>
30
- <button onClick={() => form.reset()} className="mt-6 h-11 rounded-lg bg-white px-5 text-sm font-semibold text-black transition hover:bg-gray-200">
31
- Start another form
32
- </button>
33
- </div>
34
- )
35
- }
36
-
37
- return (
38
- <div className="grid w-full max-w-5xl gap-4 lg:grid-cols-[1fr_360px]">
39
- <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20 sm:p-6">
40
- <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
41
- <div>
42
- <h2 className="text-2xl font-semibold tracking-tight text-white">Contact workflow</h2>
43
- <p className="mt-2 text-sm leading-6 text-gray-500">
44
- Fields sync with the server using different strategies so the UI stays responsive.
45
- </p>
46
- </div>
47
- <span className={`inline-flex w-fit items-center gap-2 rounded-full border px-3 py-1 text-xs ${
48
- form.$connected
49
- ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
50
- : 'border-red-400/25 bg-red-400/10 text-red-200'
51
- }`}>
52
- <span className={`h-1.5 w-1.5 rounded-full ${form.$connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
53
- {form.$connected ? 'Connected' : 'Offline'}
54
- </span>
55
- </div>
56
-
57
- <div className="space-y-4">
58
- <label className="block">
59
- <div className="mb-2 flex items-center justify-between gap-3">
60
- <span className="inline-flex items-center gap-2 text-sm font-medium text-gray-200">
61
- <FaUser className="text-theme" />
62
- Name
63
- </span>
64
- <FieldHint>sync on blur</FieldHint>
65
- </div>
66
- <input
67
- {...form.$field('name', { syncOn: 'blur' })}
68
- placeholder="Ada Lovelace"
69
- className="w-full input-theme"
70
- />
71
- </label>
72
-
73
- <label className="block">
74
- <div className="mb-2 flex items-center justify-between gap-3">
75
- <span className="inline-flex items-center gap-2 text-sm font-medium text-gray-200">
76
- <FaEnvelope className="text-theme-secondary" />
77
- Email
78
- </span>
79
- <FieldHint>debounce 500ms</FieldHint>
80
- </div>
81
- <input
82
- {...form.$field('email', { syncOn: 'change', debounce: 500 })}
83
- type="email"
84
- placeholder="ada@company.dev"
85
- className="w-full input-theme"
86
- />
87
- </label>
88
-
89
- <label className="block">
90
- <div className="mb-2 flex items-center justify-between gap-3">
91
- <span className="inline-flex items-center gap-2 text-sm font-medium text-gray-200">
92
- <FaRegMessage className="text-theme" />
93
- Message
94
- </span>
95
- <FieldHint>sync on blur</FieldHint>
96
- </div>
97
- <textarea
98
- {...form.$field('message', { syncOn: 'blur' })}
99
- rows={5}
100
- placeholder="Tell us what you want to build..."
101
- className="w-full resize-none input-theme"
102
- />
103
- </label>
104
-
105
- <div className="flex flex-col gap-3 pt-2 sm:flex-row">
106
- <button
107
- onClick={async () => {
108
- try {
109
- await form.$sync()
110
- await form.submit()
111
- } catch (err: any) {
112
- alert(err.message || 'Submit failed')
113
- }
114
- }}
115
- disabled={!form.$connected || form.$loading}
116
- className="h-11 flex-1 rounded-lg bg-white px-5 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50"
117
- >
118
- {form.$loading ? 'Sending...' : 'Submit'}
119
- </button>
120
- <button
121
- onClick={() => form.reset()}
122
- className="h-11 rounded-lg border border-white/10 bg-white/[0.03] px-5 text-sm font-semibold text-white transition hover:bg-white/[0.06]"
123
- >
124
- Clear
125
- </button>
126
- </div>
127
- </div>
128
- </section>
129
-
130
- <aside className="rounded-lg border border-white/10 bg-black/30 p-5">
131
- <div className="mb-4 flex items-center gap-2 text-sm font-semibold text-white">
132
- <FaCode className="text-theme" />
133
- Server state
134
- </div>
135
- <pre className="max-h-[460px] overflow-auto rounded-lg border border-white/10 bg-black/50 p-4 text-xs leading-6 text-emerald-300">
136
- <code>{JSON.stringify(form.$state, null, 2)}</code>
137
- </pre>
138
- </aside>
139
- </div>
140
- )
141
- }
2
+ import { Live } from '@/core/client'
3
+ import { LiveForm } from '@server/live/LiveForm'
4
+ import { FaCheck, FaCode, FaEnvelope, FaRegMessage, FaUser } from 'react-icons/fa6'
5
+
6
+ function FieldHint({ children }: { children: string }) {
7
+ return (
8
+ <span className="rounded-full border border-white/10 bg-white/[0.03] px-2 py-0.5 text-[11px] text-gray-500">
9
+ {children}
10
+ </span>
11
+ )
12
+ }
13
+
14
+ export function FormDemo() {
15
+ const form = Live.use(LiveForm)
16
+
17
+ if (form.submitted) {
18
+ return (
19
+ <div className="w-full max-w-2xl rounded-lg border border-emerald-400/20 bg-emerald-400/10 p-6 text-center shadow-2xl shadow-black/20">
20
+ <div className="mx-auto mb-5 flex h-12 w-12 items-center justify-center rounded-lg bg-emerald-400 text-black">
21
+ <FaCheck />
22
+ </div>
23
+ <h2 className="text-2xl font-semibold text-white">Message received</h2>
24
+ <p className="mt-2 text-sm leading-6 text-gray-300">
25
+ Thanks, <span className="text-emerald-200">{form.name || 'there'}</span>. The server state was updated and submitted.
26
+ </p>
27
+ <p className="mt-3 text-xs text-gray-500">
28
+ Submitted at {form.submittedAt ? new Date(form.submittedAt).toLocaleString() : '-'}
29
+ </p>
30
+ <button onClick={() => form.reset()} className="mt-6 h-11 rounded-lg bg-white px-5 text-sm font-semibold text-black transition hover:bg-gray-200">
31
+ Start another form
32
+ </button>
33
+ </div>
34
+ )
35
+ }
36
+
37
+ return (
38
+ <div className="grid w-full max-w-5xl gap-4 lg:grid-cols-[1fr_360px]">
39
+ <section className="rounded-lg border border-white/10 bg-[#07070b]/85 p-5 shadow-2xl shadow-black/20 sm:p-6">
40
+ <div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
41
+ <div>
42
+ <h2 className="text-2xl font-semibold tracking-tight text-white">Contact workflow</h2>
43
+ <p className="mt-2 text-sm leading-6 text-gray-500">
44
+ Fields sync with the server using different strategies so the UI stays responsive.
45
+ </p>
46
+ </div>
47
+ <span className={`inline-flex w-fit items-center gap-2 rounded-full border px-3 py-1 text-xs ${
48
+ form.$connected
49
+ ? 'border-emerald-400/25 bg-emerald-400/10 text-emerald-200'
50
+ : 'border-red-400/25 bg-red-400/10 text-red-200'
51
+ }`}>
52
+ <span className={`h-1.5 w-1.5 rounded-full ${form.$connected ? 'bg-emerald-300' : 'bg-red-300'}`} />
53
+ {form.$connected ? 'Connected' : 'Offline'}
54
+ </span>
55
+ </div>
56
+
57
+ <div className="space-y-4">
58
+ <label className="block">
59
+ <div className="mb-2 flex items-center justify-between gap-3">
60
+ <span className="inline-flex items-center gap-2 text-sm font-medium text-gray-200">
61
+ <FaUser className="text-theme" />
62
+ Name
63
+ </span>
64
+ <FieldHint>sync on blur</FieldHint>
65
+ </div>
66
+ <input
67
+ {...form.$field('name', { syncOn: 'blur' })}
68
+ placeholder="Ada Lovelace"
69
+ className="w-full input-theme"
70
+ />
71
+ </label>
72
+
73
+ <label className="block">
74
+ <div className="mb-2 flex items-center justify-between gap-3">
75
+ <span className="inline-flex items-center gap-2 text-sm font-medium text-gray-200">
76
+ <FaEnvelope className="text-theme-secondary" />
77
+ Email
78
+ </span>
79
+ <FieldHint>debounce 500ms</FieldHint>
80
+ </div>
81
+ <input
82
+ {...form.$field('email', { syncOn: 'change', debounce: 500 })}
83
+ type="email"
84
+ placeholder="ada@company.dev"
85
+ className="w-full input-theme"
86
+ />
87
+ </label>
88
+
89
+ <label className="block">
90
+ <div className="mb-2 flex items-center justify-between gap-3">
91
+ <span className="inline-flex items-center gap-2 text-sm font-medium text-gray-200">
92
+ <FaRegMessage className="text-theme" />
93
+ Message
94
+ </span>
95
+ <FieldHint>sync on blur</FieldHint>
96
+ </div>
97
+ <textarea
98
+ {...form.$field('message', { syncOn: 'blur' })}
99
+ rows={5}
100
+ placeholder="Tell us what you want to build..."
101
+ className="w-full resize-none input-theme"
102
+ />
103
+ </label>
104
+
105
+ <div className="flex flex-col gap-3 pt-2 sm:flex-row">
106
+ <button
107
+ onClick={async () => {
108
+ try {
109
+ await form.$sync()
110
+ await form.submit()
111
+ } catch (err: any) {
112
+ alert(err.message || 'Submit failed')
113
+ }
114
+ }}
115
+ disabled={!form.$connected || form.$loading}
116
+ className="h-11 flex-1 rounded-lg bg-white px-5 text-sm font-semibold text-black transition hover:bg-gray-200 disabled:opacity-50"
117
+ >
118
+ {form.$loading ? 'Sending...' : 'Submit'}
119
+ </button>
120
+ <button
121
+ onClick={() => form.reset()}
122
+ className="h-11 rounded-lg border border-white/10 bg-white/[0.03] px-5 text-sm font-semibold text-white transition hover:bg-white/[0.06]"
123
+ >
124
+ Clear
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </section>
129
+
130
+ <aside className="rounded-lg border border-white/10 bg-black/30 p-5">
131
+ <div className="mb-4 flex items-center gap-2 text-sm font-semibold text-white">
132
+ <FaCode className="text-theme" />
133
+ Server state
134
+ </div>
135
+ <pre className="max-h-[460px] overflow-auto rounded-lg border border-white/10 bg-black/50 p-4 text-xs leading-6 text-emerald-300">
136
+ <code>{JSON.stringify(form.$state, null, 2)}</code>
137
+ </pre>
138
+ </aside>
139
+ </div>
140
+ )
141
+ }