weifuwu 0.33.4 → 0.33.6

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/README.md CHANGED
@@ -66,7 +66,8 @@ app.mount('#root', AppShell)
66
66
  ```
67
67
 
68
68
  ```js
69
- // build.mjs
69
+ // build.mjs — 传统构建方式(可选)
70
+ // 推荐:使用 ctx.ui.js() 服务端动态编译,无需独立构建脚本
70
71
  esbuild.build({
71
72
  entryPoints: ['src/main.tsx'],
72
73
  jsx: 'automatic',
@@ -75,6 +76,12 @@ esbuild.build({
75
76
  })
76
77
  ```
77
78
 
79
+ 或用服务端动态编译——一行代码,无需构建步骤、无需 watch 模式:
80
+ ```ts
81
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
82
+ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
83
+ ```
84
+
78
85
  ### Environment Variables
79
86
 
80
87
  | Variable | Default | Used by |
@@ -126,7 +133,7 @@ esbuild.build({
126
133
  | `redis()` | Redis client (`ctx.redis`) |
127
134
  | `queue()` | Job queue + cron |
128
135
  | `createHub()` | WebSocket pub/sub |
129
- | `ui()` | SPA HTML shell (`ctx.ui.html()`) |
136
+ | `ui()` | SSR + SPA rendering (`ctx.ui.html`, `ctx.ui.js`, `ctx.ui.css`) |
130
137
 
131
138
  ### Frontend (weifuwu/client)
132
139
 
@@ -146,6 +153,10 @@ esbuild.build({
146
153
  | `LoginForm` | Login/register form component |
147
154
  | `Chat` | Real-time messaging component |
148
155
  | `domMount()` | Direct DOM mounting |
156
+ | `wrap()` | Third-party library integration |
157
+ | `useForm()` | Form state management (validation, submit, reset) |
158
+ | `createPortal()` | Render outside parent DOM hierarchy |
159
+ | `<ErrorBoundary>` | Catch render errors, show fallback |
149
160
 
150
161
  ### Utilities
151
162
 
@@ -224,6 +235,29 @@ ctx.route.path // "/chat/123"
224
235
  ctx.route.params // { id: "123" }
225
236
  ctx.route.query // { tab: "settings" }
226
237
 
238
+ #### Route Loader — 数据预取
239
+
240
+ ```tsx
241
+ const routes: RouteDef[] = [
242
+ {
243
+ path: '/post/:id',
244
+ component: PostPage,
245
+ loader: async (ctx) => ({
246
+ post: await ctx.api.get(`/api/posts/${ctx.route.params.id}`),
247
+ }),
248
+ },
249
+ ]
250
+
251
+ // In component:
252
+ function PostPage(_, ctx) {
253
+ const post = ctx.route.data.post
254
+ if (!post) return <p class="text-gray-400">加载中...</p>
255
+ return <h1 class="text-2xl font-bold">{post.title}</h1>
256
+ }
257
+ ```
258
+
259
+ 组件先渲染(显示 loading),loader 完成后自动重渲染。
260
+
227
261
  ### Middleware: api / auth / ws
228
262
 
229
263
  ```tsx
@@ -255,19 +289,147 @@ function ChatPage(_, ctx) {
255
289
  }
256
290
  ```
257
291
 
258
- ### Backend: ctx.ui.html()
292
+ ### SSR & SPA (ctx.ui.html / ctx.ui.js / ctx.ui.css)
259
293
 
260
294
  ```ts
261
- import { ui, serveStatic } from 'weifuwu'
295
+ import { ui } from 'weifuwu'
296
+
297
+ app.use(ui())
298
+
299
+ // SSR page — ctx.ui.html`` returns complete HTML Response
300
+ app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
301
+ <!DOCTYPE html>
302
+ <html>
303
+ <head>
304
+ <title>${post.title}</title>
305
+ <link rel="stylesheet" href="/static/style.css">
306
+ </head>
307
+ <body>
308
+ <div id="root">
309
+ <h1>${post.title}</h1>
310
+ <div>${ctx.ui.html.unsafe(post.body)}</div>
311
+ <div data-hydrate="like"></div>
312
+ </div>
313
+ <script>window.__WFUI_PROPS__=${ctx.ui.html.unsafe(JSON.stringify({ post }))}</script>
314
+ <script src="/static/app.js"></script>
315
+ </body>
316
+ </html>
317
+ `)
318
+
319
+ // Dynamic JS compilation — ctx.ui.js() compiles TSX on demand
320
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
321
+
322
+ // Dynamic CSS serving — ctx.ui.css() reads and serves CSS
323
+ app.get('/static/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
324
+
325
+ // Client hydrates interactive sections, skips SSR content
326
+ const app = createApp()
327
+ app.use(api())
328
+
329
+ const root = document.getElementById('root')
330
+ if (root && root.children.length > 0) {
331
+ // SSR page — hydrate only interactive areas
332
+ app.hydrate('[data-hydrate="like"]', LikeButton)
333
+ } else {
334
+ // SPA page — full mount
335
+ app.mount('#root', AppShell)
336
+ }
337
+ ```
338
+
339
+ ### wrap() — Third-party Library Integration
340
+
341
+ ```tsx
342
+ import { wrap, effect } from 'weifuwu/client'
343
+ import * as echarts from 'echarts'
344
+
345
+ // wrap(tagName, setup) creates a component:
346
+ // - Creates a <div> container
347
+ // - Calls setup(el, props, ctx) when element enters the document
348
+ // - Runs cleanup when element is removed
349
+ const PieChart = wrap('div', (el, props: { data: any[] }, ctx) => {
350
+ const chart = echarts.init(el)
351
+ chart.setOption({ series: [{ type: 'pie', data: props.data }] })
352
+ effect(() => chart.setOption({ series: [{ type: 'pie', data: props.data }] }))
353
+ return () => chart.dispose()
354
+ })
355
+
356
+ // Use in JSX like any component
357
+ <Dashboard>
358
+ <PieChart data={salesData} />
359
+ </Dashboard>
360
+ ```
262
361
 
263
- app.use(ui({ title: 'My App', script: '/static/app.js' }))
264
- app.get('/static/*', serveStatic('./dist/client'))
362
+ ### useForm() 表单状态管理
265
363
 
266
- // SPA route
267
- app.get('/', async (req, ctx) => ctx.ui.html())
364
+ ```tsx
365
+ import { useForm } from 'weifuwu/client'
366
+
367
+ const form = useForm({
368
+ initial: { email: '', password: '' },
369
+ validate: {
370
+ email: (v) => !v.includes('@') && '请输入有效邮箱',
371
+ password: (v) => v.length < 6 && '至少 6 位',
372
+ },
373
+ })
374
+
375
+ // 绑定到 input:{...form.field('name')} 自动设置 value + onInput
376
+ <input {...form.field('email')} placeholder="邮箱" />
377
+ {form.errors.email && <span class="text-red-500">{form.errors.email}</span>}
378
+
379
+ // 提交时自动验证所有字段
380
+ <button onClick={() => form.submit((data) => ctx.login(data.email, data.password))}>
381
+ 登录
382
+ </button>
383
+
384
+ // 重置
385
+ <button onClick={form.reset}>重置</button>
268
386
 
269
- // With initial props (accessible via window.__WFUI_PROPS__):
270
- app.get('/', async (req, ctx) => ctx.ui.html({ title: 'Custom', props: { user: userData } }))
387
+ // 编程设置
388
+ form.setValue('email', 'a@b.com')
389
+ form.setValues({ email: 'a@b.com', password: '123' })
390
+ ```
391
+
392
+ ### createPortal() — 渲染到父容器外
393
+
394
+ 适用于 Modal、Dropdown、Tooltip 等需突破 `overflow: hidden` 或 z-index 层级的情况。
395
+
396
+ ```tsx
397
+ import { createPortal, Show } from 'weifuwu/client'
398
+
399
+ function Modal({ show, title, children }) {
400
+ return <Show when={show}>
401
+ {createPortal(
402
+ <div class="fixed inset-0 bg-black/50 flex items-center justify-center">
403
+ <div class="bg-white rounded-xl p-6 min-w-[400px]">
404
+ <h2 class="text-lg font-bold mb-4">{title}</h2>
405
+ {children}
406
+ </div>
407
+ </div>,
408
+ document.body
409
+ )}
410
+ </Show>
411
+ }
412
+ ```
413
+
414
+ ### ErrorBoundary — 错误边界
415
+
416
+ 子组件渲染异常时捕获,显示 fallback 而非白屏。children 必须是 thunk(延迟执行)。
417
+
418
+ ```tsx
419
+ import { ErrorBoundary } from 'weifuwu/client'
420
+
421
+ function AppShell(_, ctx) {
422
+ return (
423
+ <div>
424
+ <nav>...</nav>
425
+ <main>
426
+ <ErrorBoundary fallback={(e) => <p>出错了: {e.message}</p>}>
427
+ {() => <RouteView />}
428
+ </ErrorBoundary>
429
+ </main>
430
+ </div>
431
+ )
432
+ }
271
433
  ```
272
434
 
273
435
  ### Show / For
@@ -796,14 +958,16 @@ src/
796
958
  ├── queue/ ← Job queue + cron
797
959
  ├── graphql.ts ← GraphQL
798
960
  ├── hub.ts ← WebSocket hub
799
- ├── ui/ ← SPA HTML shell (`ctx.ui.html()`)
800
- ├── client/ ← Frontend framework (~600 lines)
801
- │ ├── index.ts ← Entry
961
+ ├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
962
+ ├── client/ ← Frontend framework
963
+ │ ├── index.ts ← Entry (exports signal, useForm, wrap, createApp, ...)
802
964
  │ ├── signal.ts ← Signal / effect / computed
803
- │ ├── jsx-runtime.ts ← JSX → DOM / Show / For
804
- │ ├── app.ts ← createApp / middleware chain
805
- │ ├── router.ts ← Route matching / RouteView
965
+ │ ├── jsx-runtime.ts ← JSX → DOM / Show / For / wrap / ErrorBoundary / createPortal
966
+ │ ├── app.ts ← createApp / hydrate / middleware chain
967
+ │ ├── router.ts ← Route matching / RouteView / loader
806
968
  │ ├── types.ts ← WfuiContext / RouteDef
969
+ │ ├── lib/
970
+ │ │ └── form.ts ← useForm
807
971
  │ ├── middleware/
808
972
  │ │ ├── api.ts ← HTTP client
809
973
  │ │ ├── auth.ts ← Login / logout / token
@@ -811,14 +975,15 @@ src/
811
975
  │ └── components/
812
976
  │ ├── LoginForm.ts ← Login / register form
813
977
  │ └── Chat.ts ← Real-time messaging
814
- └── test/ ← 281 tests
978
+ └── test/ ← Tests
815
979
 
816
980
  apps/demo/ ← Full-stack demo
817
- ├── src/main.tsx ← SPA demo pages
981
+ ├── src/main.tsx ← SPA + SSR hydrate demo pages
818
982
  ├── server.ts ← weifuwu server
819
- ├── public/index.html
820
- ├── tsconfig.json
821
- └── scripts/build.mjs
983
+ ├── public/
984
+ ├── index.html ← HTML skeleton with placeholders
985
+ └── style.css ← Demo styles
986
+ └── tsconfig.json
822
987
 
823
988
  docker-compose.yml ← postgres (pgvector) + redis
824
989
  ```
@@ -21,4 +21,16 @@ export declare function createApp(): {
21
21
  ctx: WfuiContext;
22
22
  use: (mw: AppMiddleware) => any;
23
23
  mount: (rootSelector: string, RootComponent: Component) => Promise<void>;
24
+ /**
25
+ * Hydrate 一个已由 SSR 渲染的区域。
26
+ * 不清除目标容器内的内容,只附加组件输出。
27
+ *
28
+ * ```ts
29
+ * const app = createApp()
30
+ * app.use(api())
31
+ * app.use(auth())
32
+ * app.hydrate('#comments', CommentSection, { postId: '123' })
33
+ * ```
34
+ */
35
+ hydrate: (selector: string, Component: Component, props?: Record<string, unknown>) => void;
24
36
  };
@@ -15,7 +15,7 @@
15
15
  */
16
16
  export { signal, computed, effect, isSignal } from './signal.ts';
17
17
  export type { Signal } from './signal.ts';
18
- export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount } from './jsx-runtime.ts';
18
+ export { jsx, jsxs, jsxDEV, Fragment, Show, For, domMount, wrap, createPortal, ErrorBoundary } from './jsx-runtime.ts';
19
19
  export type { Component } from './jsx-runtime.ts';
20
20
  export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
21
21
  export { createApp } from './app.ts';
@@ -24,5 +24,6 @@ export { api, ApiClient, ApiError } from './middleware/api.ts';
24
24
  export { auth } from './middleware/auth.ts';
25
25
  export type { UserRecord } from './middleware/auth.ts';
26
26
  export { ws } from './middleware/ws.ts';
27
+ export { useForm } from './lib/form.ts';
27
28
  export { LoginForm } from './components/LoginForm.ts';
28
29
  export { Chat } from './components/Chat.ts';
@@ -51,6 +51,60 @@ var currentCtx = null;
51
51
  function setCtx(ctx) {
52
52
  currentCtx = ctx;
53
53
  }
54
+ var _entries = /* @__PURE__ */ new Map();
55
+ function _ensure(el) {
56
+ let entry = _entries.get(el);
57
+ if (entry) return entry;
58
+ entry = {
59
+ mounted: document.contains(el),
60
+ observer: null,
61
+ mountFns: [],
62
+ disposeFns: []
63
+ };
64
+ _entries.set(el, entry);
65
+ const obs = new MutationObserver(() => {
66
+ const now = document.contains(el);
67
+ if (now && !entry.mounted) {
68
+ entry.mounted = true;
69
+ const fns = entry.mountFns.slice();
70
+ entry.mountFns = [];
71
+ for (const fn of fns) {
72
+ const dispose = fn();
73
+ if (typeof dispose === "function") {
74
+ entry.disposeFns.push(dispose);
75
+ }
76
+ }
77
+ } else if (!now && entry.mounted) {
78
+ entry.mounted = false;
79
+ for (const fn of entry.disposeFns) fn();
80
+ entry.disposeFns = [];
81
+ entry.mountFns = [];
82
+ obs.disconnect();
83
+ _entries.delete(el);
84
+ }
85
+ });
86
+ obs.observe(document.body, { childList: true, subtree: true });
87
+ entry.observer = obs;
88
+ return entry;
89
+ }
90
+ function onMount(el, fn) {
91
+ const entry = _ensure(el);
92
+ if (entry.mounted) {
93
+ const dispose = fn();
94
+ if (typeof dispose === "function") {
95
+ entry.disposeFns.push(dispose);
96
+ }
97
+ } else {
98
+ entry.mountFns.push(fn);
99
+ }
100
+ }
101
+ function wrap(tagName, setup) {
102
+ return (props, ctx) => {
103
+ const el = document.createElement(tagName);
104
+ onMount(el, () => setup(el, props, ctx));
105
+ return el;
106
+ };
107
+ }
54
108
  function setProp(el, key, value) {
55
109
  if (key === "class" || key === "className") {
56
110
  if (isSignal(value)) {
@@ -130,6 +184,17 @@ function domMount(root, app) {
130
184
  container.innerHTML = "";
131
185
  container.appendChild(app);
132
186
  }
187
+ function ErrorBoundary({ fallback, children }, _ctx) {
188
+ try {
189
+ return children();
190
+ } catch (e) {
191
+ return fallback(e);
192
+ }
193
+ }
194
+ function createPortal(node, target) {
195
+ target.appendChild(node);
196
+ return document.createDocumentFragment();
197
+ }
133
198
  function toNode(v) {
134
199
  if (v instanceof Node) return v;
135
200
  if (typeof v === "function") return toNode(v());
@@ -178,7 +243,8 @@ function createApp() {
178
243
  params: {},
179
244
  query: Object.fromEntries(new URLSearchParams(window.location.search)),
180
245
  hash: window.location.hash,
181
- component: null
246
+ component: null,
247
+ data: {}
182
248
  },
183
249
  app: {
184
250
  navigate(path) {
@@ -223,6 +289,18 @@ function createApp() {
223
289
  const app = jsx(RootComponent, {});
224
290
  domMount(rootSelector, app);
225
291
  setCtx(null);
292
+ },
293
+ hydrate(selector, Component, props) {
294
+ const root = document.querySelector(selector);
295
+ if (!root) {
296
+ console.warn(`hydrate target not found: ${selector}`);
297
+ return;
298
+ }
299
+ const mergedProps = props ?? window.__WFUI_PROPS__ ?? {};
300
+ setCtx(ctx);
301
+ const vnode = jsx(Component, mergedProps);
302
+ root.appendChild(vnode);
303
+ setCtx(null);
226
304
  }
227
305
  };
228
306
  }
@@ -242,56 +320,79 @@ function router(opts) {
242
320
  }).join("/") + "$";
243
321
  return { re: new RegExp(reStr), keys, route };
244
322
  });
245
- function matchPath(path) {
246
- for (const { re, keys, route } of matchers) {
247
- const m = path.match(re);
248
- if (m) {
249
- const params = {};
250
- keys.forEach((k, i) => {
251
- params[k] = decodeURIComponent(m[i + 1]);
252
- });
253
- return { component: route.component, params, title: route.title, auth: route.auth };
254
- }
323
+ function matchRoute(path) {
324
+ for (const m of matchers) {
325
+ const result = path.match(m.re);
326
+ if (!result) continue;
327
+ const params = {};
328
+ m.keys.forEach((k, i) => {
329
+ params[k] = decodeURIComponent(result[i + 1]);
330
+ });
331
+ return {
332
+ matched: { component: m.route.component, params, title: m.route.title, auth: m.route.auth },
333
+ routeDef: m.route
334
+ };
255
335
  }
256
336
  return null;
257
337
  }
258
338
  return (ctx) => {
259
- function handleRoute() {
260
- const raw = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
261
- const [path, qs] = raw.split("?");
339
+ function emit(path) {
340
+ window.dispatchEvent(new CustomEvent("wefu:route", { detail: { path } }));
341
+ }
342
+ function resolve(path) {
343
+ const raw = mode === "hash" ? path || "/" : path;
344
+ const [cleanPath, qs] = raw.split("?");
262
345
  ctx.route.query = Object.fromEntries(new URLSearchParams(qs ?? ""));
263
- const matched = matchPath(path);
264
- if (matched) {
265
- ctx.route.path = path;
266
- ctx.route.params = matched.params;
267
- ctx.route.component = matched.component;
268
- ctx.route.title = matched.title;
269
- ctx.route.auth = matched.auth;
270
- if (matched.title) document.title = matched.title;
271
- if (matched.auth && !ctx.user) {
272
- setTimeout(() => ctx.app.navigate("/login"), 0);
273
- return;
274
- }
275
- } else {
346
+ ctx.route.path = cleanPath;
347
+ const r = matchRoute(cleanPath);
348
+ if (!r) {
276
349
  ctx.route.component = opts.notFound ?? null;
350
+ ctx.route.data = {};
351
+ return { component: null };
352
+ }
353
+ ctx.route.params = r.matched.params;
354
+ ctx.route.component = r.matched.component;
355
+ ctx.route.title = r.matched.title;
356
+ ctx.route.auth = r.matched.auth;
357
+ if (r.matched.title) document.title = r.matched.title;
358
+ return { component: r.matched.component, routeDef: r.routeDef };
359
+ }
360
+ function navigateAndLoad(path) {
361
+ const { routeDef } = resolve(path);
362
+ if (ctx.route.auth && !ctx.user) {
363
+ setTimeout(() => ctx.app.navigate("/login"), 0);
364
+ return;
365
+ }
366
+ emit(ctx.route.path);
367
+ if (routeDef?.loader) {
368
+ routeDef.loader(ctx).then((data) => {
369
+ ctx.route.data = data;
370
+ emit(ctx.route.path);
371
+ }).catch(() => {
372
+ ctx.route.data = {};
373
+ emit(ctx.route.path);
374
+ });
277
375
  }
278
- window.dispatchEvent(new CustomEvent("wefu:route", { detail: { path } }));
279
376
  }
280
- const origNavigate = ctx.app.navigate;
281
377
  ctx.app.navigate = (path) => {
282
378
  if (mode === "hash") {
283
379
  window.location.hash = "#" + path;
284
380
  } else {
285
381
  window.history.pushState({}, "", path);
286
- handleRoute();
382
+ navigateAndLoad(path);
287
383
  }
288
384
  };
289
385
  if (mode === "hash") {
290
- window.addEventListener("hashchange", handleRoute);
386
+ window.addEventListener("hashchange", () => {
387
+ navigateAndLoad(window.location.hash.slice(1) || "/");
388
+ });
291
389
  } else {
292
- window.addEventListener("popstate", handleRoute);
390
+ window.addEventListener("popstate", () => {
391
+ navigateAndLoad(window.location.pathname + window.location.search);
392
+ });
293
393
  }
294
- handleRoute();
394
+ const initialPath = mode === "hash" ? window.location.hash.slice(1) || "/" : window.location.pathname + window.location.search;
395
+ navigateAndLoad(initialPath);
295
396
  return ctx;
296
397
  };
297
398
  }
@@ -517,6 +618,112 @@ function ws(opts = {}) {
517
618
  };
518
619
  }
519
620
 
621
+ // src/client/lib/form.ts
622
+ function useForm(opts) {
623
+ const fields = /* @__PURE__ */ new Map();
624
+ const errors = {};
625
+ const touched = {};
626
+ for (const key of Object.keys(opts.initial)) {
627
+ fields.set(key, signal(opts.initial[key]));
628
+ errors[key] = null;
629
+ touched[key] = false;
630
+ }
631
+ const valid = signal(true);
632
+ const values = signal({ ...opts.initial });
633
+ function snapshot() {
634
+ const data = {};
635
+ for (const [key, sig] of fields) {
636
+ data[key] = sig.value;
637
+ }
638
+ values.value = data;
639
+ }
640
+ function validateField(name) {
641
+ if (!opts.validate) return null;
642
+ const rule = opts.validate[name];
643
+ if (!rule) return null;
644
+ const sig = fields.get(name);
645
+ const result = rule(sig.value, values.value);
646
+ const error = result && typeof result === "string" ? result : null;
647
+ errors[name] = error;
648
+ return error;
649
+ }
650
+ function validateAll() {
651
+ if (!opts.validate) return true;
652
+ let allValid = true;
653
+ for (const key of Object.keys(opts.initial)) {
654
+ const err = validateField(key);
655
+ if (err) allValid = false;
656
+ }
657
+ valid.value = allValid;
658
+ return allValid;
659
+ }
660
+ return {
661
+ field(name) {
662
+ let sig = fields.get(name);
663
+ if (!sig) {
664
+ sig = signal(opts.initial[name]);
665
+ fields.set(name, sig);
666
+ }
667
+ return {
668
+ value: sig,
669
+ onInput(e) {
670
+ const target = e.target;
671
+ touched[name] = true;
672
+ if (target.type === "checkbox") {
673
+ sig.value = target.checked;
674
+ } else {
675
+ sig.value = target.value;
676
+ }
677
+ snapshot();
678
+ validateField(name);
679
+ }
680
+ };
681
+ },
682
+ get errors() {
683
+ return errors;
684
+ },
685
+ get touched() {
686
+ return touched;
687
+ },
688
+ valid,
689
+ get values() {
690
+ return values;
691
+ },
692
+ async submit(handler) {
693
+ if (!validateAll()) return;
694
+ const data = {};
695
+ for (const [key, sig] of fields) {
696
+ data[key] = sig.value;
697
+ }
698
+ await handler(data);
699
+ },
700
+ reset() {
701
+ for (const key of Object.keys(opts.initial)) {
702
+ const sig = fields.get(key);
703
+ if (sig) sig.value = opts.initial[key];
704
+ errors[key] = null;
705
+ touched[key] = false;
706
+ }
707
+ valid.value = true;
708
+ snapshot();
709
+ },
710
+ setValue(name, value) {
711
+ const sig = fields.get(name);
712
+ if (sig) sig.value = value;
713
+ snapshot();
714
+ validateField(name);
715
+ },
716
+ setValues(partial) {
717
+ for (const [key, value] of Object.entries(partial)) {
718
+ const sig = fields.get(key);
719
+ if (sig) sig.value = value;
720
+ }
721
+ snapshot();
722
+ validateAll();
723
+ }
724
+ };
725
+ }
726
+
520
727
  // src/client/components/LoginForm.ts
521
728
  var h = (tag, props, ...children) => jsx(tag, props ?? {}, ...children);
522
729
  function LoginForm(_props, ctx) {
@@ -673,6 +880,7 @@ export {
673
880
  ApiClient,
674
881
  ApiError,
675
882
  Chat,
883
+ ErrorBoundary,
676
884
  For,
677
885
  Fragment,
678
886
  LoginForm,
@@ -682,6 +890,7 @@ export {
682
890
  auth,
683
891
  computed,
684
892
  createApp,
893
+ createPortal,
685
894
  domMount,
686
895
  effect,
687
896
  isSignal,
@@ -690,5 +899,7 @@ export {
690
899
  jsxs,
691
900
  router,
692
901
  signal,
902
+ useForm,
903
+ wrap,
693
904
  ws
694
905
  };