create-fluxstack 1.21.1 → 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.
- package/app/client/src/App.tsx +9 -11
- package/app/client/src/components/AppLayout.tsx +291 -290
- package/app/client/src/components/BackButton.tsx +17 -16
- package/app/client/src/components/ColorWheel.tsx +1 -0
- package/app/client/src/components/DemoPage.tsx +136 -135
- package/app/client/src/components/ErrorBoundary.tsx +2 -1
- package/app/client/src/components/LiveErrorBoundary.tsx +2 -1
- package/app/client/src/components/ThemePicker.tsx +1 -0
- package/app/client/src/framework/ClientOnly.tsx +14 -0
- package/app/client/src/framework/LivePage.tsx +55 -0
- package/app/client/src/framework/RscHomePage.tsx +125 -0
- package/app/client/src/framework/RscLink.tsx +31 -0
- package/app/client/src/framework/RscNav.tsx +42 -0
- package/app/client/src/framework/RscRoot.tsx +54 -0
- package/app/client/src/framework/csrf.ts +22 -0
- package/app/client/src/framework/entry.browser.tsx +51 -0
- package/app/client/src/framework/entry.rsc.tsx +41 -0
- package/app/client/src/framework/entry.ssr.tsx +12 -0
- package/app/client/src/framework/navigation.ts +29 -0
- package/app/client/src/framework/params.tsx +22 -0
- package/app/client/src/framework/routes.ts +143 -0
- package/app/client/src/framework/vite-rsc.d.ts +21 -0
- package/app/client/src/hooks/useThemeClock.ts +16 -1
- package/app/client/src/live/AuthDemo.tsx +271 -270
- package/app/client/src/live/CounterDemo.tsx +152 -151
- package/app/client/src/live/FormDemo.tsx +141 -140
- package/app/client/src/live/PingPongDemo.tsx +181 -180
- package/app/client/src/live/RoomChatDemo.tsx +398 -397
- package/app/client/src/live/SharedCounterDemo.tsx +2 -1
- package/app/client/src/pages/ApiTestPage.tsx +1 -0
- package/app/client/src/pages/auth.tsx +13 -0
- package/app/client/src/pages/blog/[slug].tsx +23 -0
- package/app/client/src/pages/counter.tsx +15 -0
- package/app/client/src/pages/form.tsx +13 -0
- package/app/client/src/pages/index.tsx +9 -0
- package/app/client/src/pages/ping-pong.tsx +13 -0
- package/app/client/src/pages/room-chat.tsx +13 -0
- package/app/client/src/pages/shared-counter.tsx +13 -0
- package/app/client/src/pages/sobre.tsx +19 -0
- package/app/server/index.ts +5 -1
- package/app/server/live/rooms/index.ts +14 -0
- package/config/system/plugins.config.ts +12 -2
- package/core/cli/commands/dev.ts +9 -1
- package/core/plugins/built-in/rsc/index.ts +156 -0
- package/core/plugins/built-in/ssr/bun-asset-loader.ts +46 -0
- package/core/plugins/built-in/ssr/index.ts +143 -0
- package/core/plugins/built-in/ssr/registry.ts +38 -0
- package/core/plugins/built-in/vite/index.ts +7 -0
- package/core/server/live/websocket-plugin.ts +29 -10
- package/core/utils/version.ts +6 -6
- package/create-fluxstack.ts +64 -5
- package/package.json +112 -108
- package/playwright.config.ts +6 -2
- package/vite.config.ts +22 -0
- package/app/client/.live-stubs/LiveUpload.js +0 -15
|
@@ -1,151 +1,152 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{connected ? '
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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,140 +1,141 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{form.$connected ? '
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await form
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|