kayforms 0.1.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/LICENSE +21 -0
- package/README.md +337 -0
- package/examples/react-demo/README.md +337 -0
- package/examples/react-demo/eslint.config.js +22 -0
- package/examples/react-demo/index.html +13 -0
- package/examples/react-demo/package.json +33 -0
- package/examples/react-demo/public/apple-touch-icon.png +0 -0
- package/examples/react-demo/public/favicon-96x96.png +0 -0
- package/examples/react-demo/public/favicon.ico +0 -0
- package/examples/react-demo/public/favicon.svg +17 -0
- package/examples/react-demo/public/icons.svg +24 -0
- package/examples/react-demo/public/site.webmanifest +21 -0
- package/examples/react-demo/public/web-app-manifest-192x192.png +0 -0
- package/examples/react-demo/public/web-app-manifest-512x512.png +0 -0
- package/examples/react-demo/src/App.css +184 -0
- package/examples/react-demo/src/App.tsx +825 -0
- package/examples/react-demo/src/assets/hero.png +0 -0
- package/examples/react-demo/src/assets/react.svg +1 -0
- package/examples/react-demo/src/assets/vite.svg +1 -0
- package/examples/react-demo/src/index.css +627 -0
- package/examples/react-demo/src/main.tsx +10 -0
- package/examples/react-demo/tsconfig.app.json +25 -0
- package/examples/react-demo/tsconfig.json +7 -0
- package/examples/react-demo/tsconfig.node.json +24 -0
- package/examples/react-demo/vite.config.ts +7 -0
- package/kayforms.jpg +0 -0
- package/package.json +26 -0
- package/packages/angular/package.json +43 -0
- package/packages/angular/src/index.ts +198 -0
- package/packages/angular/tsconfig.json +8 -0
- package/packages/angular/tsup.config.ts +17 -0
- package/packages/core/README.md +337 -0
- package/packages/core/package.json +37 -0
- package/packages/core/src/batch.ts +106 -0
- package/packages/core/src/devtools.ts +329 -0
- package/packages/core/src/field.ts +167 -0
- package/packages/core/src/form.ts +448 -0
- package/packages/core/src/index.ts +71 -0
- package/packages/core/src/registry.ts +126 -0
- package/packages/core/src/signal.ts +399 -0
- package/packages/core/src/time-travel.ts +275 -0
- package/packages/core/src/validation.ts +243 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/core/tsup.config.ts +16 -0
- package/packages/devtools/extension/background.js +35 -0
- package/packages/devtools/extension/content-script.js +10 -0
- package/packages/devtools/extension/devtools.html +9 -0
- package/packages/devtools/extension/devtools.js +8 -0
- package/packages/devtools/extension/manifest.json +19 -0
- package/packages/devtools/extension/panel.css +505 -0
- package/packages/devtools/extension/panel.html +108 -0
- package/packages/devtools/extension/panel.js +354 -0
- package/packages/devtools/package.json +38 -0
- package/packages/devtools/src/index.ts +95 -0
- package/packages/devtools/src/panel.ts +226 -0
- package/packages/devtools/src/styles.ts +422 -0
- package/packages/devtools/src/timeline.ts +283 -0
- package/packages/devtools/tsconfig.json +8 -0
- package/packages/devtools/tsup.config.ts +17 -0
- package/packages/react/package.json +46 -0
- package/packages/react/src/index.ts +279 -0
- package/packages/react/tsconfig.json +8 -0
- package/packages/react/tsup.config.ts +17 -0
- package/packages/solid/package.json +42 -0
- package/packages/solid/src/index.ts +206 -0
- package/packages/solid/tsconfig.json +8 -0
- package/packages/solid/tsup.config.ts +17 -0
- package/packages/svelte/package.json +42 -0
- package/packages/svelte/src/index.ts +199 -0
- package/packages/svelte/tsconfig.json +8 -0
- package/packages/svelte/tsup.config.ts +17 -0
- package/packages/vanilla/package.json +38 -0
- package/packages/vanilla/src/index.ts +254 -0
- package/packages/vanilla/tsconfig.json +8 -0
- package/packages/vanilla/tsup.config.ts +17 -0
- package/packages/vue/package.json +42 -0
- package/packages/vue/src/index.ts +217 -0
- package/packages/vue/tsconfig.json +8 -0
- package/packages/vue/tsup.config.ts +17 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +21 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
2
|
+
import { useForm, useField, FormProvider, useSignalValue } from "@kayforms/react";
|
|
3
|
+
import { validators, getFormRegistry, createComputed, batch } from "@kayforms/core";
|
|
4
|
+
import { connectDevTools } from "@kayforms/devtools";
|
|
5
|
+
|
|
6
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
7
|
+
const css = `
|
|
8
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=DM+Mono:wght@400;500&display=swap');
|
|
9
|
+
|
|
10
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #0b0c0e;
|
|
14
|
+
--bg-surface: #111214;
|
|
15
|
+
--bg-raised: #18191d;
|
|
16
|
+
--bg-input: #1e1f24;
|
|
17
|
+
--border: rgba(255,255,255,0.07);
|
|
18
|
+
--border-md: rgba(255,255,255,0.12);
|
|
19
|
+
--accent: #5b6af7;
|
|
20
|
+
--accent-dim: rgba(91,106,247,0.15);
|
|
21
|
+
--accent-glow:rgba(91,106,247,0.35);
|
|
22
|
+
--success: #22c55e;
|
|
23
|
+
--danger: #f87171;
|
|
24
|
+
--text-1: #f0f0f2;
|
|
25
|
+
--text-2: #9a9ba8;
|
|
26
|
+
--text-3: #5a5b68;
|
|
27
|
+
--mono: 'DM Mono', monospace;
|
|
28
|
+
--sans: 'DM Sans', sans-serif;
|
|
29
|
+
--radius-sm: 6px;
|
|
30
|
+
--radius-md: 10px;
|
|
31
|
+
--radius-lg: 16px;
|
|
32
|
+
--radius-xl: 22px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
body {
|
|
36
|
+
font-family: var(--sans);
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text-1);
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
-webkit-font-smoothing: antialiased;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ─── Layout ─────────────────────────────────── */
|
|
44
|
+
.shell {
|
|
45
|
+
max-width: 1200px;
|
|
46
|
+
margin: 0 auto;
|
|
47
|
+
padding: 48px 24px 80px;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: 24px;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ─── Header ─────────────────────────────────── */
|
|
54
|
+
.header {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
padding-bottom: 32px;
|
|
59
|
+
border-bottom: 1px solid var(--border);
|
|
60
|
+
}
|
|
61
|
+
.header-left { display: flex; align-items: center; gap: 14px; }
|
|
62
|
+
.logo {
|
|
63
|
+
width: 38px; height: 38px;
|
|
64
|
+
background: var(--accent);
|
|
65
|
+
border-radius: var(--radius-md);
|
|
66
|
+
display: flex; align-items: center; justify-content: center;
|
|
67
|
+
font-size: 18px; font-weight: 600; color: #fff;
|
|
68
|
+
letter-spacing: -0.5px;
|
|
69
|
+
}
|
|
70
|
+
.brand-name {
|
|
71
|
+
font-size: 17px; font-weight: 600; letter-spacing: -0.3px; color: var(--text-1);
|
|
72
|
+
}
|
|
73
|
+
.brand-sub { font-size: 12px; color: var(--text-3); margin-top: 1px; }
|
|
74
|
+
.header-badges { display: flex; gap: 8px; }
|
|
75
|
+
.badge {
|
|
76
|
+
font-size: 11px; font-family: var(--mono);
|
|
77
|
+
padding: 4px 10px; border-radius: 20px;
|
|
78
|
+
border: 1px solid var(--border-md);
|
|
79
|
+
color: var(--text-2);
|
|
80
|
+
background: var(--bg-raised);
|
|
81
|
+
letter-spacing: 0.2px;
|
|
82
|
+
}
|
|
83
|
+
.badge.accent { border-color: var(--accent-glow); color: var(--accent); background: var(--accent-dim); }
|
|
84
|
+
|
|
85
|
+
/* ─── Content grid ───────────────────────────── */
|
|
86
|
+
.content-grid {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: 1fr 380px;
|
|
89
|
+
gap: 16px;
|
|
90
|
+
align-items: start;
|
|
91
|
+
}
|
|
92
|
+
@media (max-width: 900px) { .content-grid { grid-template-columns: 1fr; } }
|
|
93
|
+
|
|
94
|
+
/* ─── Cards ──────────────────────────────────── */
|
|
95
|
+
.card {
|
|
96
|
+
background: var(--bg-surface);
|
|
97
|
+
border: 1px solid var(--border);
|
|
98
|
+
border-radius: var(--radius-xl);
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
}
|
|
101
|
+
.card-inner { padding: 28px; }
|
|
102
|
+
|
|
103
|
+
/* ─── Wizard steps ───────────────────────────── */
|
|
104
|
+
.wizard-nav {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 0;
|
|
108
|
+
padding: 20px 28px;
|
|
109
|
+
border-bottom: 1px solid var(--border);
|
|
110
|
+
}
|
|
111
|
+
.step-item {
|
|
112
|
+
display: flex; align-items: center; gap: 10px;
|
|
113
|
+
cursor: pointer; padding: 6px 0;
|
|
114
|
+
}
|
|
115
|
+
.step-dot {
|
|
116
|
+
width: 26px; height: 26px;
|
|
117
|
+
border-radius: 50%;
|
|
118
|
+
border: 1.5px solid var(--border-md);
|
|
119
|
+
display: flex; align-items: center; justify-content: center;
|
|
120
|
+
font-size: 11px; font-weight: 600; font-family: var(--mono);
|
|
121
|
+
color: var(--text-3);
|
|
122
|
+
transition: all 0.2s;
|
|
123
|
+
flex-shrink: 0;
|
|
124
|
+
}
|
|
125
|
+
.step-item.active .step-dot {
|
|
126
|
+
border-color: var(--accent);
|
|
127
|
+
color: var(--accent);
|
|
128
|
+
background: var(--accent-dim);
|
|
129
|
+
box-shadow: 0 0 0 4px var(--accent-dim);
|
|
130
|
+
}
|
|
131
|
+
.step-item.done .step-dot {
|
|
132
|
+
border-color: var(--success);
|
|
133
|
+
color: var(--success);
|
|
134
|
+
background: rgba(34,197,94,0.1);
|
|
135
|
+
}
|
|
136
|
+
.step-label {
|
|
137
|
+
font-size: 12px; font-weight: 500; color: var(--text-3);
|
|
138
|
+
transition: color 0.2s;
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
}
|
|
141
|
+
.step-item.active .step-label { color: var(--text-1); }
|
|
142
|
+
.step-item.done .step-label { color: var(--text-2); }
|
|
143
|
+
.step-divider {
|
|
144
|
+
flex: 1; height: 1px; background: var(--border); margin: 0 10px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ─── Form elements ──────────────────────────── */
|
|
148
|
+
.form-body { padding: 28px; }
|
|
149
|
+
.form-group { margin-bottom: 20px; }
|
|
150
|
+
.form-label {
|
|
151
|
+
display: block;
|
|
152
|
+
font-size: 12px; font-weight: 500; letter-spacing: 0.4px;
|
|
153
|
+
color: var(--text-2); text-transform: uppercase;
|
|
154
|
+
margin-bottom: 8px;
|
|
155
|
+
}
|
|
156
|
+
.form-label .req { color: var(--accent); margin-left: 2px; }
|
|
157
|
+
|
|
158
|
+
.input-wrap { position: relative; }
|
|
159
|
+
.input-wrap input,
|
|
160
|
+
.input-wrap select {
|
|
161
|
+
width: 100%;
|
|
162
|
+
padding: 10px 14px;
|
|
163
|
+
background: var(--bg-input);
|
|
164
|
+
border: 1px solid var(--border-md);
|
|
165
|
+
border-radius: var(--radius-md);
|
|
166
|
+
color: var(--text-1);
|
|
167
|
+
font-family: var(--sans);
|
|
168
|
+
font-size: 14px;
|
|
169
|
+
outline: none;
|
|
170
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
171
|
+
appearance: none;
|
|
172
|
+
-webkit-appearance: none;
|
|
173
|
+
}
|
|
174
|
+
.input-wrap input::placeholder { color: var(--text-3); }
|
|
175
|
+
.input-wrap input:focus,
|
|
176
|
+
.input-wrap select:focus {
|
|
177
|
+
border-color: var(--accent);
|
|
178
|
+
box-shadow: 0 0 0 3px var(--accent-dim);
|
|
179
|
+
}
|
|
180
|
+
.input-wrap.has-error input,
|
|
181
|
+
.input-wrap.has-error select {
|
|
182
|
+
border-color: var(--danger);
|
|
183
|
+
box-shadow: 0 0 0 3px rgba(248,113,113,0.12);
|
|
184
|
+
}
|
|
185
|
+
.input-wrap.is-valid input,
|
|
186
|
+
.input-wrap.is-valid select {
|
|
187
|
+
border-color: rgba(34,197,94,0.4);
|
|
188
|
+
}
|
|
189
|
+
.input-wrap select {
|
|
190
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%236b7280' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
|
|
191
|
+
background-repeat: no-repeat;
|
|
192
|
+
background-position: right 12px center;
|
|
193
|
+
padding-right: 36px;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
}
|
|
196
|
+
.input-wrap select option { background: #18191d; }
|
|
197
|
+
|
|
198
|
+
.field-error {
|
|
199
|
+
display: flex; align-items: center; gap: 5px;
|
|
200
|
+
font-size: 11px; color: var(--danger);
|
|
201
|
+
margin-top: 6px; font-weight: 500;
|
|
202
|
+
}
|
|
203
|
+
.field-error::before { content: '●'; font-size: 6px; }
|
|
204
|
+
|
|
205
|
+
.checkbox-row {
|
|
206
|
+
display: flex; align-items: center; gap: 10px;
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
padding: 12px 14px;
|
|
209
|
+
background: var(--bg-input);
|
|
210
|
+
border: 1px solid var(--border-md);
|
|
211
|
+
border-radius: var(--radius-md);
|
|
212
|
+
transition: border-color 0.15s;
|
|
213
|
+
}
|
|
214
|
+
.checkbox-row:hover { border-color: var(--border-md); }
|
|
215
|
+
.checkbox-row input[type=checkbox] {
|
|
216
|
+
width: 16px; height: 16px; flex-shrink: 0;
|
|
217
|
+
accent-color: var(--accent);
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
}
|
|
220
|
+
.checkbox-row span { font-size: 13px; color: var(--text-2); }
|
|
221
|
+
|
|
222
|
+
/* ─── Inline info box ────────────────────────── */
|
|
223
|
+
.info-box {
|
|
224
|
+
padding: 12px 14px;
|
|
225
|
+
background: var(--bg-input);
|
|
226
|
+
border: 1px solid var(--border-md);
|
|
227
|
+
border-radius: var(--radius-md);
|
|
228
|
+
font-size: 13px;
|
|
229
|
+
color: var(--text-2);
|
|
230
|
+
line-height: 1.5;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* ─── Form footer / buttons ──────────────────── */
|
|
234
|
+
.form-footer {
|
|
235
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
236
|
+
padding: 20px 28px;
|
|
237
|
+
border-top: 1px solid var(--border);
|
|
238
|
+
}
|
|
239
|
+
.btn {
|
|
240
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
241
|
+
padding: 9px 20px;
|
|
242
|
+
font-family: var(--sans); font-size: 13px; font-weight: 500;
|
|
243
|
+
border-radius: var(--radius-md);
|
|
244
|
+
border: none; cursor: pointer;
|
|
245
|
+
transition: all 0.15s;
|
|
246
|
+
letter-spacing: 0.1px;
|
|
247
|
+
}
|
|
248
|
+
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
|
249
|
+
.btn-ghost {
|
|
250
|
+
background: transparent;
|
|
251
|
+
color: var(--text-2);
|
|
252
|
+
border: 1px solid var(--border-md);
|
|
253
|
+
}
|
|
254
|
+
.btn-ghost:hover:not(:disabled) { background: var(--bg-raised); color: var(--text-1); }
|
|
255
|
+
.btn-primary {
|
|
256
|
+
background: var(--accent);
|
|
257
|
+
color: #fff;
|
|
258
|
+
}
|
|
259
|
+
.btn-primary:hover:not(:disabled) {
|
|
260
|
+
background: #6b7af9;
|
|
261
|
+
box-shadow: 0 4px 16px var(--accent-glow);
|
|
262
|
+
transform: translateY(-1px);
|
|
263
|
+
}
|
|
264
|
+
.btn-success {
|
|
265
|
+
background: var(--success);
|
|
266
|
+
color: #0b1a0f;
|
|
267
|
+
}
|
|
268
|
+
.btn-success:hover { filter: brightness(1.1); }
|
|
269
|
+
.btn-danger { background: var(--danger); color: #2a0a0a; }
|
|
270
|
+
.btn-danger:hover { filter: brightness(1.1); }
|
|
271
|
+
|
|
272
|
+
/* ─── Success screen ─────────────────────────── */
|
|
273
|
+
.success-screen {
|
|
274
|
+
padding: 48px 28px;
|
|
275
|
+
display: flex; flex-direction: column; align-items: center;
|
|
276
|
+
text-align: center; gap: 16px;
|
|
277
|
+
}
|
|
278
|
+
.success-icon {
|
|
279
|
+
width: 56px; height: 56px;
|
|
280
|
+
background: rgba(34,197,94,0.12);
|
|
281
|
+
border: 1.5px solid rgba(34,197,94,0.35);
|
|
282
|
+
border-radius: 50%;
|
|
283
|
+
display: flex; align-items: center; justify-content: center;
|
|
284
|
+
font-size: 22px;
|
|
285
|
+
color: var(--success);
|
|
286
|
+
animation: pop 0.3s ease;
|
|
287
|
+
}
|
|
288
|
+
@keyframes pop {
|
|
289
|
+
0% { transform: scale(0.6); opacity: 0; }
|
|
290
|
+
80% { transform: scale(1.08); }
|
|
291
|
+
100% { transform: scale(1); opacity: 1; }
|
|
292
|
+
}
|
|
293
|
+
.success-title { font-size: 22px; font-weight: 600; letter-spacing: -0.4px; }
|
|
294
|
+
.success-sub { font-size: 13px; color: var(--text-2); max-width: 380px; line-height: 1.6; }
|
|
295
|
+
.json-preview {
|
|
296
|
+
width: 100%; text-align: left;
|
|
297
|
+
background: var(--bg-input);
|
|
298
|
+
border: 1px solid var(--border);
|
|
299
|
+
border-radius: var(--radius-md);
|
|
300
|
+
padding: 16px;
|
|
301
|
+
font-family: var(--mono); font-size: 11.5px;
|
|
302
|
+
color: var(--text-2);
|
|
303
|
+
white-space: pre; overflow-x: auto;
|
|
304
|
+
max-height: 240px; overflow-y: auto;
|
|
305
|
+
line-height: 1.6;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ─── Right panel: State inspector ───────────── */
|
|
309
|
+
.inspector-header {
|
|
310
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
311
|
+
padding: 18px 22px;
|
|
312
|
+
border-bottom: 1px solid var(--border);
|
|
313
|
+
}
|
|
314
|
+
.inspector-title {
|
|
315
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.6px;
|
|
316
|
+
color: var(--text-3); text-transform: uppercase;
|
|
317
|
+
}
|
|
318
|
+
.live-dot {
|
|
319
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
320
|
+
background: var(--success);
|
|
321
|
+
box-shadow: 0 0 6px var(--success);
|
|
322
|
+
animation: pulse 1.6s ease-in-out infinite;
|
|
323
|
+
}
|
|
324
|
+
@keyframes pulse {
|
|
325
|
+
0%, 100% { opacity: 1; }
|
|
326
|
+
50% { opacity: 0.3; }
|
|
327
|
+
}
|
|
328
|
+
.inspector-body { padding: 14px; display: flex; flex-direction: column; gap: 10px; }
|
|
329
|
+
.form-block {
|
|
330
|
+
border-radius: var(--radius-md);
|
|
331
|
+
border: 1px solid var(--border);
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
}
|
|
334
|
+
.form-block-head {
|
|
335
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
336
|
+
padding: 9px 12px;
|
|
337
|
+
background: var(--bg-raised);
|
|
338
|
+
border-bottom: 1px solid var(--border);
|
|
339
|
+
}
|
|
340
|
+
.form-id {
|
|
341
|
+
font-family: var(--mono); font-size: 11px; color: var(--text-2);
|
|
342
|
+
}
|
|
343
|
+
.validity-tag {
|
|
344
|
+
font-size: 10px; font-weight: 600; letter-spacing: 0.3px;
|
|
345
|
+
padding: 2px 8px; border-radius: 10px;
|
|
346
|
+
}
|
|
347
|
+
.validity-tag.valid { background: rgba(34,197,94,0.1); color: var(--success); }
|
|
348
|
+
.validity-tag.invalid { background: rgba(248,113,113,0.1); color: var(--danger); }
|
|
349
|
+
.form-block pre {
|
|
350
|
+
padding: 10px 12px;
|
|
351
|
+
font-family: var(--mono); font-size: 10.5px;
|
|
352
|
+
color: var(--text-3);
|
|
353
|
+
line-height: 1.65; overflow-x: auto;
|
|
354
|
+
white-space: pre;
|
|
355
|
+
max-height: 180px; overflow-y: auto;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* ─── Benchmark section ──────────────────────── */
|
|
359
|
+
.bench-card { padding: 24px 28px; }
|
|
360
|
+
.bench-header {
|
|
361
|
+
display: flex; align-items: flex-start; justify-content: space-between;
|
|
362
|
+
gap: 16px; margin-bottom: 24px;
|
|
363
|
+
}
|
|
364
|
+
.bench-title { font-size: 15px; font-weight: 600; letter-spacing: -0.2px; margin-bottom: 4px; }
|
|
365
|
+
.bench-sub { font-size: 12px; color: var(--text-3); line-height: 1.5; max-width: 480px; }
|
|
366
|
+
.bench-controls { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
|
|
367
|
+
.fps-box {
|
|
368
|
+
text-align: center;
|
|
369
|
+
background: var(--bg-raised);
|
|
370
|
+
border: 1px solid var(--border);
|
|
371
|
+
border-radius: var(--radius-md);
|
|
372
|
+
padding: 6px 16px;
|
|
373
|
+
min-width: 70px;
|
|
374
|
+
}
|
|
375
|
+
.fps-val { font-family: var(--mono); font-size: 22px; font-weight: 500; color: var(--text-1); }
|
|
376
|
+
.fps-label { font-size: 10px; color: var(--text-3); letter-spacing: 0.5px; text-transform: uppercase; }
|
|
377
|
+
|
|
378
|
+
/* ─── Benchmark grid ─────────────────────────── */
|
|
379
|
+
.bench-grid {
|
|
380
|
+
display: grid;
|
|
381
|
+
grid-template-columns: repeat(50, 1fr);
|
|
382
|
+
gap: 2px;
|
|
383
|
+
}
|
|
384
|
+
.cell {
|
|
385
|
+
aspect-ratio: 1;
|
|
386
|
+
border-radius: 2px;
|
|
387
|
+
background: var(--bg-raised);
|
|
388
|
+
border: 1px solid var(--border);
|
|
389
|
+
transition: background 0.06s, border-color 0.06s;
|
|
390
|
+
}
|
|
391
|
+
.cell.err { background: rgba(248,113,113,0.5); border-color: rgba(248,113,113,0.7); }
|
|
392
|
+
.cell.a0 { background: rgba(91,106,247,0.15); border-color: rgba(91,106,247,0.25); }
|
|
393
|
+
.cell.a1 { background: rgba(91,106,247,0.35); border-color: rgba(91,106,247,0.5); }
|
|
394
|
+
.cell.a2 { background: rgba(91,106,247,0.6); border-color: rgba(91,106,247,0.7); }
|
|
395
|
+
.cell.a3 { background: rgba(91,106,247,0.85); border-color: var(--accent); }
|
|
396
|
+
|
|
397
|
+
/* ─── Section label ──────────────────────────── */
|
|
398
|
+
.section-label {
|
|
399
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.6px;
|
|
400
|
+
color: var(--text-3); text-transform: uppercase; margin-bottom: 10px;
|
|
401
|
+
}
|
|
402
|
+
`;
|
|
403
|
+
|
|
404
|
+
// ─── Profile Fields ───────────────────────────────────────────────────────────
|
|
405
|
+
function ProfileFields() {
|
|
406
|
+
const name = useField("name");
|
|
407
|
+
const email = useField("email");
|
|
408
|
+
const age = useField("age");
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<div className="form-body">
|
|
412
|
+
<FieldRow label="Full name" required field={name} type="text" placeholder="Your full name" />
|
|
413
|
+
<FieldRow label="Email address" required field={email} type="email" placeholder="you@example.com" />
|
|
414
|
+
<FieldRow label="Age" required field={age} type="number" placeholder="Must be 18 or older" />
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ─── Preference Fields ────────────────────────────────────────────────────────
|
|
420
|
+
function PreferenceFields() {
|
|
421
|
+
const country = useField("country");
|
|
422
|
+
const newsletter = useField("newsletter");
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div className="form-body">
|
|
426
|
+
<div className="form-group">
|
|
427
|
+
<label className="form-label">Country of residence</label>
|
|
428
|
+
<div className="input-wrap">
|
|
429
|
+
<select {...country.inputProps}>
|
|
430
|
+
<option value="US">United States — USD</option>
|
|
431
|
+
<option value="GB">United Kingdom — GBP</option>
|
|
432
|
+
<option value="GH">Ghana — GHS · Momo enabled</option>
|
|
433
|
+
<option value="NG">Nigeria — NGN · Bank enabled</option>
|
|
434
|
+
</select>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
<div className="form-group">
|
|
438
|
+
<label className="form-label">Notifications</label>
|
|
439
|
+
<label className="checkbox-row">
|
|
440
|
+
<input
|
|
441
|
+
type="checkbox"
|
|
442
|
+
checked={!!newsletter.value}
|
|
443
|
+
onChange={(e) => newsletter.onChange(e.target.checked)}
|
|
444
|
+
onBlur={newsletter.onBlur}
|
|
445
|
+
/>
|
|
446
|
+
<span>Subscribe to developer updates & changelogs</span>
|
|
447
|
+
</label>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ─── Payment Fields ───────────────────────────────────────────────────────────
|
|
454
|
+
function PaymentFields() {
|
|
455
|
+
const payMethod = useField("payMethod");
|
|
456
|
+
const cardNumber = useField("cardNumber");
|
|
457
|
+
const cardExpiry = useField("cardExpiry");
|
|
458
|
+
const momoProvider = useField("momoProvider");
|
|
459
|
+
const momoNumber = useField("momoNumber");
|
|
460
|
+
|
|
461
|
+
const registry = getFormRegistry();
|
|
462
|
+
const selectedCountry = useSignalValue(
|
|
463
|
+
useMemo(
|
|
464
|
+
() => createComputed(() => (registry.get("preferences")?.values.value?.country) || "US"),
|
|
465
|
+
[registry]
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
if (selectedCountry === "GH") payMethod.onChange("momo");
|
|
471
|
+
else if (selectedCountry === "NG") payMethod.onChange("bank");
|
|
472
|
+
else payMethod.onChange("card");
|
|
473
|
+
}, [selectedCountry]);
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<div className="form-body">
|
|
477
|
+
<div className="form-group">
|
|
478
|
+
<label className="form-label">Payment method</label>
|
|
479
|
+
<div className="input-wrap">
|
|
480
|
+
<select {...payMethod.inputProps}>
|
|
481
|
+
{selectedCountry === "GH" && <option value="momo">Mobile Money (Momo)</option>}
|
|
482
|
+
{selectedCountry === "NG" && <option value="bank">Bank transfer</option>}
|
|
483
|
+
<option value="card">Credit / debit card</option>
|
|
484
|
+
</select>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{payMethod.value === "momo" && (
|
|
489
|
+
<>
|
|
490
|
+
<div className="form-group">
|
|
491
|
+
<label className="form-label">Momo provider</label>
|
|
492
|
+
<div className="input-wrap">
|
|
493
|
+
<select {...momoProvider.inputProps}>
|
|
494
|
+
<option value="mtn">MTN Mobile Money</option>
|
|
495
|
+
<option value="telecel">Telecel Cash</option>
|
|
496
|
+
<option value="at">AT Money</option>
|
|
497
|
+
</select>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
<FieldRow label="Momo number" required field={momoNumber} type="text" placeholder="024XXXXXXX — 10 digits" />
|
|
501
|
+
</>
|
|
502
|
+
)}
|
|
503
|
+
|
|
504
|
+
{payMethod.value === "bank" && (
|
|
505
|
+
<div className="form-group">
|
|
506
|
+
<div className="info-box">
|
|
507
|
+
Bank details are generated on checkout. Pay via OPay or GTBank after submission.
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
)}
|
|
511
|
+
|
|
512
|
+
{payMethod.value === "card" && (
|
|
513
|
+
<>
|
|
514
|
+
<FieldRow label="Card number" required field={cardNumber} type="text" placeholder="4111 2222 3333 4444" />
|
|
515
|
+
<FieldRow label="Expiration" required field={cardExpiry} type="text" placeholder="MM / YY" />
|
|
516
|
+
</>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ─── Shared FieldRow ──────────────────────────────────────────────────────────
|
|
523
|
+
function FieldRow({ label, required, field, type, placeholder }) {
|
|
524
|
+
const hasError = field.touched && field.error;
|
|
525
|
+
const isValid = field.touched && !field.error;
|
|
526
|
+
return (
|
|
527
|
+
<div className="form-group">
|
|
528
|
+
<label className="form-label">
|
|
529
|
+
{label}{required && <span className="req"> *</span>}
|
|
530
|
+
</label>
|
|
531
|
+
<div className={`input-wrap ${hasError ? "has-error" : isValid ? "is-valid" : ""}`}>
|
|
532
|
+
<input type={type} placeholder={placeholder} {...field.inputProps} />
|
|
533
|
+
</div>
|
|
534
|
+
{hasError && <p className="field-error">{field.error}</p>}
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ─── Benchmark Cell ───────────────────────────────────────────────────────────
|
|
540
|
+
function BenchmarkCell({ index }) {
|
|
541
|
+
const cell = useField(`cell_${index}`);
|
|
542
|
+
let cls = "cell";
|
|
543
|
+
if (cell.error) cls += " err";
|
|
544
|
+
else if (cell.value === 0) cls += " a0";
|
|
545
|
+
else if (cell.value === 1) cls += " a1";
|
|
546
|
+
else if (cell.value === 2) cls += " a2";
|
|
547
|
+
else if (cell.value === 3) cls += " a3";
|
|
548
|
+
return <div className={cls} />;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ─── App ──────────────────────────────────────────────────────────────────────
|
|
552
|
+
export default function App() {
|
|
553
|
+
const [step, setStep] = useState(1);
|
|
554
|
+
const [submittedData, setSubmittedData] = useState(null);
|
|
555
|
+
const [benchRunning, setBenchRunning] = useState(false);
|
|
556
|
+
const [fps, setFps] = useState(60);
|
|
557
|
+
const frameRef = useRef(0);
|
|
558
|
+
const lastRef = useRef(performance.now());
|
|
559
|
+
const rafRef = useRef(null);
|
|
560
|
+
|
|
561
|
+
const profileForm = useForm({
|
|
562
|
+
id: "profile",
|
|
563
|
+
initialValues: { name: "", email: "", age: "" },
|
|
564
|
+
fieldValidators: {
|
|
565
|
+
name: [validators.required("Name is required"), validators.minLength(3, "Minimum 3 characters")],
|
|
566
|
+
email: [validators.required("Email is required"), validators.email("Enter a valid email")],
|
|
567
|
+
age: [validators.required("Age is required"), validators.custom((v) => {
|
|
568
|
+
const n = Number(v);
|
|
569
|
+
return isNaN(n) ? "Must be a number" : n >= 18 ? undefined : "Must be 18 or older";
|
|
570
|
+
})],
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const preferencesForm = useForm({
|
|
575
|
+
id: "preferences",
|
|
576
|
+
initialValues: { country: "GH", newsletter: false },
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const paymentForm = useForm({
|
|
580
|
+
id: "payment",
|
|
581
|
+
initialValues: { payMethod: "card", cardNumber: "", cardExpiry: "", momoProvider: "mtn", momoNumber: "" },
|
|
582
|
+
fieldValidators: {
|
|
583
|
+
cardNumber: [validators.custom((v) => {
|
|
584
|
+
const m = getFormRegistry().get("payment")?.values.peek()?.payMethod;
|
|
585
|
+
if (m !== "card") return undefined;
|
|
586
|
+
if (!v) return "Card number required";
|
|
587
|
+
return v.replace(/\D/g, "").length >= 12 ? undefined : "Invalid card number";
|
|
588
|
+
})],
|
|
589
|
+
cardExpiry: [validators.custom((v) => {
|
|
590
|
+
const m = getFormRegistry().get("payment")?.values.peek()?.payMethod;
|
|
591
|
+
if (m !== "card") return undefined;
|
|
592
|
+
if (!v) return "Expiry required";
|
|
593
|
+
return /^\d{2}\/\d{2}$/.test(v) ? undefined : "Format MM/YY";
|
|
594
|
+
})],
|
|
595
|
+
momoNumber: [validators.custom((v) => {
|
|
596
|
+
const m = getFormRegistry().get("payment")?.values.peek()?.payMethod;
|
|
597
|
+
if (m !== "momo") return undefined;
|
|
598
|
+
if (!v) return "Momo number required";
|
|
599
|
+
return v.replace(/\D/g, "").length === 10 ? undefined : "Must be 10 digits";
|
|
600
|
+
})],
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const benchmarkInitialValues = useMemo(() => {
|
|
605
|
+
const v = {};
|
|
606
|
+
for (let i = 0; i < 1000; i++) v[`cell_${i}`] = -1;
|
|
607
|
+
return v;
|
|
608
|
+
}, []);
|
|
609
|
+
|
|
610
|
+
const benchmarkForm = useForm({ id: "benchmark", initialValues: benchmarkInitialValues });
|
|
611
|
+
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
const dt = connectDevTools(profileForm.store, preferencesForm.store, paymentForm.store, { minimized: true });
|
|
614
|
+
return () => dt.destroy();
|
|
615
|
+
}, []);
|
|
616
|
+
|
|
617
|
+
const handleNext = async () => {
|
|
618
|
+
if (step === 1) {
|
|
619
|
+
const e = await profileForm.store.validateAll();
|
|
620
|
+
if (!Object.keys(e).length) setStep(2);
|
|
621
|
+
} else if (step === 2) {
|
|
622
|
+
const e = await preferencesForm.store.validateAll();
|
|
623
|
+
if (!Object.keys(e).length) setStep(3);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const handleSubmit = async (e) => {
|
|
628
|
+
e.preventDefault();
|
|
629
|
+
const pe = await profileForm.store.validateAll();
|
|
630
|
+
const pre = await preferencesForm.store.validateAll();
|
|
631
|
+
const paye = await paymentForm.store.validateAll();
|
|
632
|
+
if (!Object.keys(pe).length && !Object.keys(pre).length && !Object.keys(paye).length) {
|
|
633
|
+
setSubmittedData({ profile: profileForm.values, preferences: preferencesForm.values, payment: paymentForm.values });
|
|
634
|
+
setStep(4);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const handleReset = () => {
|
|
639
|
+
profileForm.reset(); preferencesForm.reset(); paymentForm.reset();
|
|
640
|
+
setSubmittedData(null); setStep(1);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// Bench loop
|
|
644
|
+
const benchLoop = () => {
|
|
645
|
+
frameRef.current++;
|
|
646
|
+
const now = performance.now();
|
|
647
|
+
if (now - lastRef.current >= 1000) {
|
|
648
|
+
setFps(Math.round((frameRef.current * 1000) / (now - lastRef.current)));
|
|
649
|
+
frameRef.current = 0;
|
|
650
|
+
lastRef.current = now;
|
|
651
|
+
}
|
|
652
|
+
batch(() => {
|
|
653
|
+
for (let k = 0; k < 50; k++) {
|
|
654
|
+
const i = Math.floor(Math.random() * 1000);
|
|
655
|
+
const v = Math.random() > 0.08 ? Math.floor(Math.random() * 4) : -1;
|
|
656
|
+
benchmarkForm.store.setFieldValue(`cell_${i}`, v);
|
|
657
|
+
const f = benchmarkForm.store.getField(`cell_${i}`);
|
|
658
|
+
if (Math.random() > 0.95) f.error.set("err");
|
|
659
|
+
else f.error.set(undefined);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
rafRef.current = requestAnimationFrame(benchLoop);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
useEffect(() => {
|
|
666
|
+
if (benchRunning) {
|
|
667
|
+
lastRef.current = performance.now();
|
|
668
|
+
frameRef.current = 0;
|
|
669
|
+
rafRef.current = requestAnimationFrame(benchLoop);
|
|
670
|
+
} else if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
671
|
+
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
|
|
672
|
+
}, [benchRunning]);
|
|
673
|
+
|
|
674
|
+
const stepConfig = [
|
|
675
|
+
{ n: 1, label: "Profile" },
|
|
676
|
+
{ n: 2, label: "Preferences" },
|
|
677
|
+
{ n: 3, label: "Payment" },
|
|
678
|
+
];
|
|
679
|
+
|
|
680
|
+
return (
|
|
681
|
+
<>
|
|
682
|
+
<style>{css}</style>
|
|
683
|
+
<div className="shell">
|
|
684
|
+
{/* Header */}
|
|
685
|
+
<header className="header">
|
|
686
|
+
<div className="header-left">
|
|
687
|
+
<div className="logo">K</div>
|
|
688
|
+
<div>
|
|
689
|
+
<div className="brand-name">Kayforms</div>
|
|
690
|
+
<div className="brand-sub">Reactive form library with time-travel debugging</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
<div className="header-badges">
|
|
694
|
+
<span className="badge">< 3 kb gzip</span>
|
|
695
|
+
<span className="badge accent">60 fps</span>
|
|
696
|
+
</div>
|
|
697
|
+
</header>
|
|
698
|
+
|
|
699
|
+
{/* Main grid */}
|
|
700
|
+
<div className="content-grid">
|
|
701
|
+
{/* Wizard card */}
|
|
702
|
+
<div className="card">
|
|
703
|
+
{step < 4 ? (
|
|
704
|
+
<>
|
|
705
|
+
{/* Step nav */}
|
|
706
|
+
<nav className="wizard-nav">
|
|
707
|
+
{stepConfig.map(({ n, label }, idx) => (
|
|
708
|
+
<>
|
|
709
|
+
<div
|
|
710
|
+
key={n}
|
|
711
|
+
className={`step-item ${step === n ? "active" : step > n ? "done" : ""}`}
|
|
712
|
+
onClick={() => {
|
|
713
|
+
if (n < step) setStep(n);
|
|
714
|
+
else if (n === 2 && profileForm.valid) setStep(2);
|
|
715
|
+
else if (n === 3 && profileForm.valid && preferencesForm.valid) setStep(3);
|
|
716
|
+
}}
|
|
717
|
+
>
|
|
718
|
+
<div className="step-dot">
|
|
719
|
+
{step > n ? "✓" : n}
|
|
720
|
+
</div>
|
|
721
|
+
<span className="step-label">{label}</span>
|
|
722
|
+
</div>
|
|
723
|
+
{idx < stepConfig.length - 1 && <div className="step-divider" key={`d${n}`} />}
|
|
724
|
+
</>
|
|
725
|
+
))}
|
|
726
|
+
</nav>
|
|
727
|
+
|
|
728
|
+
{/* Form content */}
|
|
729
|
+
<form onSubmit={handleSubmit}>
|
|
730
|
+
{step === 1 && <FormProvider form={profileForm.store}><ProfileFields /></FormProvider>}
|
|
731
|
+
{step === 2 && <FormProvider form={preferencesForm.store}><PreferenceFields /></FormProvider>}
|
|
732
|
+
{step === 3 && <FormProvider form={paymentForm.store}><PaymentFields /></FormProvider>}
|
|
733
|
+
|
|
734
|
+
<div className="form-footer">
|
|
735
|
+
<button
|
|
736
|
+
type="button" className="btn btn-ghost"
|
|
737
|
+
disabled={step === 1}
|
|
738
|
+
onClick={() => setStep(step - 1)}
|
|
739
|
+
>
|
|
740
|
+
← Back
|
|
741
|
+
</button>
|
|
742
|
+
{step < 3
|
|
743
|
+
? <button type="button" className="btn btn-primary" onClick={handleNext}>Continue →</button>
|
|
744
|
+
: <button type="submit" className="btn btn-success">Complete checkout ✓</button>
|
|
745
|
+
}
|
|
746
|
+
</div>
|
|
747
|
+
</form>
|
|
748
|
+
</>
|
|
749
|
+
) : (
|
|
750
|
+
<div className="success-screen">
|
|
751
|
+
<div className="success-icon">✓</div>
|
|
752
|
+
<p className="success-title">Order placed successfully</p>
|
|
753
|
+
<p className="success-sub">
|
|
754
|
+
Your form state was batched and submitted atomically. Use the DevTools timeline (bottom right) to time-travel through history.
|
|
755
|
+
</p>
|
|
756
|
+
<pre className="json-preview">{JSON.stringify(submittedData, null, 2)}</pre>
|
|
757
|
+
<button type="button" className="btn btn-ghost" onClick={handleReset}>
|
|
758
|
+
← Submit another
|
|
759
|
+
</button>
|
|
760
|
+
</div>
|
|
761
|
+
)}
|
|
762
|
+
</div>
|
|
763
|
+
|
|
764
|
+
{/* State inspector */}
|
|
765
|
+
<div className="card">
|
|
766
|
+
<div className="inspector-header">
|
|
767
|
+
<span className="inspector-title">State Inspector</span>
|
|
768
|
+
<div className="live-dot" />
|
|
769
|
+
</div>
|
|
770
|
+
<div className="inspector-body">
|
|
771
|
+
{[
|
|
772
|
+
{ id: "profileForm", form: profileForm },
|
|
773
|
+
{ id: "preferencesForm", form: preferencesForm },
|
|
774
|
+
{ id: "paymentForm", form: paymentForm },
|
|
775
|
+
].map(({ id, form }) => (
|
|
776
|
+
<div className="form-block" key={id}>
|
|
777
|
+
<div className="form-block-head">
|
|
778
|
+
<span className="form-id">{id}</span>
|
|
779
|
+
<span className={`validity-tag ${form.valid ? "valid" : "invalid"}`}>
|
|
780
|
+
{form.valid ? "valid" : "invalid"}
|
|
781
|
+
</span>
|
|
782
|
+
</div>
|
|
783
|
+
<pre>{JSON.stringify({ values: form.values, errors: form.errors, dirty: form.dirty }, null, 2)}</pre>
|
|
784
|
+
</div>
|
|
785
|
+
))}
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
{/* Benchmark */}
|
|
791
|
+
<div className="card">
|
|
792
|
+
<div className="bench-card">
|
|
793
|
+
<div className="bench-header">
|
|
794
|
+
<div>
|
|
795
|
+
<p className="bench-title">Fine-grained signal rendering — 1,000 fields</p>
|
|
796
|
+
<p className="bench-sub">
|
|
797
|
+
50 fields update per frame via granular signals. Only the changed cell re-renders — no virtual DOM diffing, no full tree reconciliation.
|
|
798
|
+
</p>
|
|
799
|
+
</div>
|
|
800
|
+
<div className="bench-controls">
|
|
801
|
+
<div className="fps-box">
|
|
802
|
+
<div className="fps-val">{fps}</div>
|
|
803
|
+
<div className="fps-label">fps</div>
|
|
804
|
+
</div>
|
|
805
|
+
<button
|
|
806
|
+
type="button"
|
|
807
|
+
className={`btn ${benchRunning ? "btn-danger" : "btn-primary"}`}
|
|
808
|
+
onClick={() => setBenchRunning(!benchRunning)}
|
|
809
|
+
>
|
|
810
|
+
{benchRunning ? "Stop" : "Run benchmark"}
|
|
811
|
+
</button>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
<FormProvider form={benchmarkForm.store}>
|
|
816
|
+
<div className="bench-grid">
|
|
817
|
+
{Array.from({ length: 1000 }, (_, i) => <BenchmarkCell key={i} index={i} />)}
|
|
818
|
+
</div>
|
|
819
|
+
</FormProvider>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
</>
|
|
824
|
+
);
|
|
825
|
+
}
|