weifuwu 0.55.2 → 0.56.0

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
@@ -9,7 +9,7 @@ npm install weifuwu
9
9
  一个包 = 后端 (`weifuwu`) + 前端 (`weifuwu/client`) + 组件库 (`weifuwu/components`) + 布局系统 (`weifuwu/layout`)。
10
10
 
11
11
  > ⚠️ **注意:前后端都有 `ctx.ui`,但用途完全不同**
12
- > - **后端** `ctx.ui`(SSR/编译):`ctx.ui.html`(HTML 模板)、`ctx.ui.js`(TSX→JS 动态编译)、`ctx.ui.css`(CSS 编译)
12
+ > - **后端** `ctx.ui`(SSR/编译):`ctx.ui.html`(HTML 模板)、`ctx.ui.js`(TSX→JS 动态编译)、`ctx.ui.css`(CSS 编译)、`ctx.ui.ssr`(组件 SSR)、`ctx.ui.ssrData`(数据序列化)
13
13
  > - **前端** `ctx.ui`(渲染引擎):`ctx.ui.$()`(响应式状态)、`ctx.ui.render()` / `dirty()`(渲染控制)、`useMedia()` / `useBreakpoint()` / `usePopupPosition()`(浏览器事件监听)
14
14
  > 后端的是「把页面和代码交给浏览器」,前端的是「在浏览器里驱动 UI」。
15
15
 
@@ -17,7 +17,7 @@ npm install weifuwu
17
17
 
18
18
  ## 设计理念
19
19
 
20
- **零运行时依赖** — 前端无 npm 运行时依赖(自研 VDOM,不引入 Virtual DOM 库、rxjs、immer 等)。后端仅依赖 `graphql` + `ws`(语言/协议本身)——**数据库客户端(PostgreSQL/Redis 协议)、GraphQL schema 工具全部自研**。esbuild 编译 TSX 的结果即可直接运行。
20
+ **零运行时依赖** — 前端无 npm 运行时依赖(自研 VDOM,不引入 Virtual DOM 库、rxjs、immer 等)。后端仅依赖 `esbuild`(TSX→JS 编译)+ `graphql` + `ws`(语言/协议本身)——**数据库客户端(PostgreSQL/Redis 协议)、GraphQL schema 工具全部自研**。esbuild 作为运行时依赖随 `npm install weifuwu` 自动安装,`ctx.ui.js()` 开箱即用。
21
21
 
22
22
  **两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook。
23
23
 
@@ -29,49 +29,111 @@ npm install weifuwu
29
29
 
30
30
  **SSR + 动态编译** — 后端 `ctx.ui.js()` 用 esbuild 实时编译 TSX,开发时改代码即刷即用,零构建步骤。
31
31
 
32
+ **async 工厂组件** — `async (ctx) => (initProps, ctx) => (props) => VNode`:工厂层声明数据(`await ctx.data.get`)、mount 初始化状态(`$`)、render 输出视图。异步只在工厂边界,mount/render 保持同步;数据经闭包注入,写数据像写同步代码。
33
+
34
+ **SPA/SSR/Hydration 统一透明** — 同一份路由定义(`routes`)一个组件形态三场景自动适配:后端 `uiSsr({ routes })` 匹配即自动 SSR(完整 HTML + `__DATA__`),客户端 `router({ routes })` + `RouteView` + `mount(..., { hydrate: true })` 按 URL 同源匹配并收养服务端 HTML(不重建、无闪跳)。`ctx.data.get` 一个 API:SSR 预取 / hydration 命中(不重复请求)/ SPA 触发 fetch。服务端直接用 `.tsx`(`weifuwu/dev` Node loader),前后端同一 JSX 运行时。
35
+
32
36
  ---
33
37
 
34
38
  ## 快速开始
35
39
 
40
+ 两种模式,**组件和路由的写法完全一样**,差异只有后端/客户端入口两行:
41
+
42
+ | 模式 | 适用场景 | 后端 | 客户端入口 |
43
+ |------|---------|------|-----------|
44
+ | **SPA** | 应用页(Dashboard、工具、后台) | HTML 外壳 | `mount('#root', RouteView)` |
45
+ | **SSR + Hydration** | 内容页(博客、营销,需要 SEO/首屏) | `uiSsr` 一行 | `mount('#root', RouteView, { hydrate: true })` |
46
+
47
+ ### 先写共享部分(两种模式都一样)
48
+
49
+ ```tsx
50
+ // routes.tsx —— 页面声明(前后端共用)
51
+ import type { RouteDef } from 'weifuwu/client'
52
+ import { asyncComponent } from 'weifuwu/client'
53
+
54
+ // async 工厂组件:await 数据 → 返回视图(两阶段:外层初始化,内层渲染)
55
+ const Home = asyncComponent(async (ctx) => {
56
+ const msg = await ctx.data.get('/api/hello') // 数据管道:一个 API 三场景
57
+ return (_init, ctx) =>
58
+ (props) => <h1>{msg.msg}</h1>
59
+ })
60
+
61
+ export const routes: RouteDef[] = [{ path: '/', component: Home }]
62
+ ```
63
+
64
+ ### 模式 A:纯 SPA
65
+
36
66
  ```ts
37
67
  // server.ts
38
- import { serve, Router, ui, cors, serveStatic } from 'weifuwu'
68
+ import { serve, Router, ui, cors } from 'weifuwu'
39
69
 
40
70
  const app = new Router()
41
71
  app.use(cors())
42
- app.use(ui())
72
+ app.use(ui()) // 注入 ctx.ui.html / ctx.ui.js / ctx.ui.css
43
73
 
44
- // SPA 入口
74
+ // SPA 外壳(空 root + 前端 bundle)
45
75
  app.get('/', (req, ctx) => ctx.ui.html`
46
76
  <!doctype html><html><body>
47
77
  <div id="root"></div>
48
- <script src="/app.js"></script>
78
+ <script src="/static/app.js"></script>
49
79
  </body></html>
50
80
  `)
81
+ app.get('/static/app.js', (req, ctx) => ctx.ui.js('./src/client.ts'))
82
+ app.get('/api/hello', () => Response.json({ msg: 'world' }))
83
+
84
+ serve(app, { port: 3000 })
85
+ ```
86
+
87
+ ```ts
88
+ // src/client.ts —— 纯客户端渲染
89
+ import { createApp, router, RouteView } from 'weifuwu/client'
90
+ import { routes } from './routes.tsx'
91
+
92
+ createApp().use(router({ routes })).mount('#root', RouteView)
93
+ ```
94
+
95
+ ### 模式 B:SSR + Hydration(内容页/SEO)
96
+
97
+ 同一份 `routes`、同一个组件,差异只在**后端加 `uiSsr` 一行、客户端加 `hydrate` 参数**:
98
+
99
+ ```ts
100
+ // server.ts —— 完整版(与模式 A 的差异:uiSsr 中间件 + 一条样式路由)
101
+ import { serve, Router, ui, uiSsr, cors } from 'weifuwu'
102
+ import { routes } from './routes.tsx'
103
+
104
+ const app = new Router()
105
+ app.use(cors())
106
+ app.use(ui())
51
107
 
52
- // 动态编译前端 TSX(零构建步骤)
53
- app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx'))
54
- app.get('/style.css', (req, ctx) => ctx.ui.css('./src/style.css'))
108
+ // 路由级 SSR:GET 匹配 routes → 注入 ctx.route.params → await 组件工厂
109
+ // 完整 HTML + __DATA__ + bundle/styles 引用(无需手写页面 handler)
110
+ app.use(uiSsr({ routes, bundle: '/static/app.js', styles: ['/static/style.css'] }))
55
111
 
56
- // API
112
+ app.get('/static/app.js', (req, ctx) => ctx.ui.js('./src/client.ts'))
113
+ app.get('/static/style.css', (req, ctx) => ctx.ui.css('./src/style.css'))
57
114
  app.get('/api/hello', () => Response.json({ msg: 'world' }))
58
115
 
59
116
  serve(app, { port: 3000 })
60
117
  ```
61
118
 
62
- ```tsx
63
- // src/main.tsx
119
+ ```ts
120
+ // src/client.ts —— 与模式 A 的唯一差异:hydrate: true(收养服务端 HTML,无闪跳)
64
121
  import { createApp, router, RouteView } from 'weifuwu/client'
65
- import type { Component } from 'weifuwu/client'
122
+ import { routes } from './routes.tsx'
66
123
 
67
- const Home: Component = () => () => <h1>Hello weifuwu</h1>
124
+ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
125
+ ```
68
126
 
69
- createApp()
70
- .use(router({ routes: [{ path: '/', component: Home }] }))
71
- .mount('#root', () => () => <RouteView />) // 根组件也要两阶段:外层返回 render 函数
127
+ ### 启动(两种模式都一样)
128
+
129
+ ```json
130
+ // package.json —— 服务端直接跑 .tsx(零构建)
131
+ { "scripts": { "dev": "node --import weifuwu/dev server.ts" } }
72
132
  ```
73
133
 
74
- 运行 `node server.ts`,访问 `http://localhost:3000` 即可看到页面。后端 `ctx.ui.js()` 会实时编译 `src/main.tsx`,改代码刷新即生效,无需任何构建步骤。
134
+ - 访问页面:SPA 客户端渲染;SSR 页面内容直接进 HTML(`curl /` 可见,SEO)
135
+ - 改组件刷新即生效,无需构建步骤
136
+ - 完整可运行示例见 `apps/demo`(博客页 = SSR + Hydration,SPA 页 = 纯客户端)
75
137
 
76
138
  > 想**零后端、零构建**最快跑起来?直接跳到下面的「CDN 快速原型」。
77
139
 
@@ -149,8 +211,8 @@ createApp()
149
211
  | 资源 | CDN 地址 | 说明 |
150
212
  |------|---------|------|
151
213
  | `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
152
- | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
153
- | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 82 个主题 Token + 35 个布局原语 |
214
+ | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 43 个 UI 组件(Button, Card, Table, Modal 等) |
215
+ | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 91 个主题 Token + 35 个布局原语 |
154
216
  | 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
155
217
 
156
218
 
@@ -167,24 +229,47 @@ createApp()
167
229
  | `weifuwu` | **postgres** | PostgreSQL 客户端(自研 PG v3 协议)→ `ctx.sql` | Router, DATABASE_URL |
168
230
  | `weifuwu` | **redis** | Redis 客户端(自研 RESP2 协议)→ `ctx.redis` | Router, REDIS_URL |
169
231
  | `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
232
+ | `weifuwu` | **uiSsr** | 路由级 SSR:匹配 routes → 自动完整 HTML + `__DATA__` + bundle | Router, ui |
233
+ | `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
170
234
  | `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
171
235
  | `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
172
- | `weifuwu` | **response** | HTTP 响应辅助函数(ok/badRequest/...) | — |
236
+ | `weifuwu` | **ok / badRequest / …** | HTTP 响应辅助函数(ok/badRequest/... 等 12 个) | — |
173
237
  | `weifuwu` | **parseBody** | JSON 请求体安全解析 | — |
238
+ | Router 方法 | **app.graphql()** | GraphQL 端点(支持 GraphiQL),Router 实例方法(无需单独 import) | Router |
174
239
  | `weifuwu/client` | **createApp** | 应用引导 + VDOM 渲染引擎 | — |
175
240
  | `weifuwu/client` | **router / RouteView** | 前端路由(history/hash 模式) | createApp |
241
+ | `weifuwu/client` | **asyncComponent** | async 工厂组件(形态 C):工厂层声明数据,mount/render 同步 | — |
242
+ | `weifuwu/client` | **ctx.data** | 数据管道:SSR 预取 / hydration 命中 / SPA fetch(`ctx.data.get`) | createApp |
176
243
  | `weifuwu/client` | **api / auth / ws** | HTTP 客户端 / 认证 / WebSocket 中间件 | createApp |
177
244
  | `weifuwu/client` | **i18n** | 国际化中间件(运行时切换语言) | createApp |
178
245
  | `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
179
246
  | `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
180
247
  | `weifuwu/client` | **popup** | 弹层 fixed 定位工具(`computeFixedPos` / `computeFixedPosRect`) | — |
181
- | `weifuwu/components` | **42 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
182
- | `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 82 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
248
+ | `weifuwu/components` | **43 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
249
+ | `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 91 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
183
250
 
184
251
  ---
185
252
 
186
253
  ## 核心概念
187
254
 
255
+ ### 两阶段组件(新手必读:为什么是两层)
256
+
257
+ 组件 = `(initProps, ctx) => (props) => VNode`——**外层 = 初始化(只执行一次),内层 = 渲染(每次状态/props 变化时执行)**。类比:外层是对象的构造函数,内层是它的 render 方法。
258
+
259
+ ```tsx
260
+ const Counter = (_init, ctx) => {
261
+ // 外层(mount):只跑一次——初始化状态、订阅、定时器
262
+ const $ = ctx.ui.$()
263
+ $.count = 0
264
+ return (props) =>
265
+ // 内层(render):每次变化执行——读状态输出视图
266
+ <button onClick={() => $.count++}>{$.count}</button>
267
+ }
268
+ ```
269
+
270
+ > 为什么不是单层函数(React 风格)?单层函数每次渲染都执行整个函数体,需要 hooks 记忆机制来区分"初始化"和"渲染";两阶段用**位置即语义**——外层天生只跑一次,没有 hooks 规则、没有依赖数组、没有闭包陷阱。
271
+ > 异步数据用 `asyncComponent` 工厂(见下文):`async (ctx) => await ctx.data.get(...)` → 返回两阶段组件,数据经闭包注入。
272
+
188
273
  ### 中间件模式(前后端一致)
189
274
 
190
275
  ```
@@ -678,6 +763,8 @@ app.use(ui())
678
763
  | `ctx.ui.html.unsafe(str)` | `(string) => string` | 插入原始 HTML |
679
764
  | `ctx.ui.js(entryPath)` | `(string) => Promise<Response>` | esbuild 编译 TSX → JS bundle |
680
765
  | `ctx.ui.css(entryPath)` | `(string) => Promise<Response>` | 读取 CSS 文件 → CSS Response(如安装 postcss + @tailwindcss/postcss 则自动编译) |
766
+ | `ctx.ui.ssr(Comp, props?, { data })` | `(Component, props, opts?) => Promise<string>` | 服务端渲染组件 → HTML 片段(async 工厂自动 await;HtmlSafe 内联不二次转义) |
767
+ | `ctx.ui.ssrData(data)` | `(Map) => string` | 序列化 SSR 数据 → `<script>window.__DATA__=...</script>`(`<` 转义防 XSS) |
681
768
 
682
769
  ### ctx.ui.html — HTML 模板
683
770
 
@@ -714,6 +801,98 @@ app.get('/style.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
714
801
  - 支持包名(`weifuwu/layout/style.css`, `weifuwu/components/style.css`)或文件路径
715
802
  - 带 mtime 缓存验证(开发时编辑文件后自动失效)
716
803
 
804
+ ### ctx.ui.ssr — SSR 渲染组件 → HTML
805
+
806
+ 将组件(含 async 工厂组件)在服务端渲染为完整 HTML 片段,数据经 `ctx.data` 预取并序列化进 `window.__DATA__`(客户端 hydration 时同步命中,不重跑请求):
807
+
808
+ ```ts
809
+ const BlogPage = asyncComponent(async (ctx) => {
810
+ const post = await ctx.data.get(`/api/posts/${ctx.params.slug}`, fetchPost)
811
+ return (_init, ctx) => () =>
812
+ h('article', {},
813
+ h('h1', {}, post.title),
814
+ h('div', { innerHTML: post.body }),
815
+ )
816
+ })
817
+
818
+ app.get('/blog/:slug', async (req, ctx) => {
819
+ const data = new Map()
820
+ const html = await ctx.ui.ssr(BlogPage, {}, { data }) // HtmlSafe:模板内联不二次转义
821
+ return ctx.ui.html`
822
+ <!DOCTYPE html>
823
+ <html><body>
824
+ <div id="root">${html}</div>
825
+ ${ctx.ui.ssrData(data)}
826
+ <script src="/static/app.js"></script>
827
+ </body></html>
828
+ `
829
+ })
830
+ ```
831
+
832
+ - 事件处理器/ref 剥离,文本自动转义(XSS),`class`/`style` 对象序列化,`innerHTML` 原样输出
833
+ - Fragment/Portal 子节点就地内联
834
+ - `ctx.ui.ssrData(data)` 输出 `<script>window.__DATA__=...</script>`(JSON `<` 转义防 XSS)
835
+ - 服务端 ctx shim:`$`(dirty no-op)、`ctx.data` 预取去重、`selfId` 请求级隔离
836
+
837
+ ### Hydration — 客户端收养服务端 HTML
838
+
839
+ 服务端 HTML + `window.__DATA__`(ctx.data 种子)到达客户端后,`mount(..., { hydrate: true })` **收养现有 DOM**(不重建、不闪跳),只接线事件/ref/$:
840
+
841
+ ```ts
842
+ import { createApp } from 'weifuwu/client'
843
+
844
+ createApp()
845
+ .mount('#root', BlogPage, { hydrate: true }) // 容器已有服务端 HTML
846
+ ```
847
+
848
+ - **游标收养**:元素/文本按位置匹配现有 DOM;tag 不匹配 → 局部替换;文本不一致 → 就地修正;服务端多余节点 → 收尾清理
849
+ - **async 工厂 hydration**:工厂 `ctx.data.get` 从 `__DATA__` 同步命中(不重跑请求)→ 渲染与服务端一致 → 收养
850
+ - hydration 后 `$`/dirty/事件全量可用(与纯 SPA 无差别)
851
+ - 诚实裁剪:Portal 内容就地收养(不移动到 `#__wf_portal`);渲染期非确定性(Date/random)会导致 mismatch(dev 警告)
852
+
853
+ ### uiSsr — 路由级 SSR(声明即渲染)
854
+
855
+ 共享路由定义,前后端同一份声明——后端匹配即自动 SSR,无需手写 handler/模板/序列化:
856
+
857
+ ```tsx
858
+ // routes.tsx —— 前后端共用
859
+ import type { RouteDef } from 'weifuwu/client'
860
+ import { BlogPage } from './pages/BlogPage.tsx'
861
+
862
+ export const routes: RouteDef[] = [
863
+ { path: '/blog/:slug', component: BlogPage, title: '博客' },
864
+ ]
865
+
866
+ // server.ts —— 一行中间件:GET 匹配 → 注入 ctx.route.params → await 组件工厂 → 完整 HTML + __DATA__ + bundle
867
+ import { uiSsr } from 'weifuwu'
868
+ app.use(uiSsr({ routes, bundle: '/static/blog.js' }))
869
+
870
+ // blog-hydrate.ts —— 客户端:同一份 routes,router() 注入 ctx.route.params(两端同源)
871
+ createApp()
872
+ .use(router({ routes }))
873
+ .mount('#root', routes[0].component, { hydrate: true })
874
+ ```
875
+
876
+ - 组件工厂读 `ctx.route.params`(`/blog/:slug` → `ctx.route.params.slug`)——后端 uiSsr / 前端 router **同源注入**
877
+ - 未匹配 → next()(交给 API/静态/404);非 GET → next()
878
+ - 可自定义 `title` / `template`
879
+
880
+ ### weifuwu/dev — 服务端直接跑 .tsx
881
+
882
+ Node 原生 TS 只剥离类型(不支持 JSX)。`weifuwu/dev` 注册 esbuild loader,服务端直接跑 `.tsx`(零构建):
883
+
884
+ ```json
885
+ {
886
+ "scripts": {
887
+ "dev": "node --import weifuwu/dev server.ts",
888
+ "start": "node --import weifuwu/dev server.ts"
889
+ }
890
+ }
891
+ ```
892
+
893
+ - 前后端同一 JSX 运行时(`jsxImportSource: weifuwu/client`)→ 两端 VNode 一致 → hydration 可靠
894
+ - 与 `ctx.ui.js` 前端动态编译同一理念:无构建、无产物、改代码即生效
895
+
717
896
  ---
718
897
 
719
898
  ## graphql — GraphQL 端点
@@ -1465,6 +1644,32 @@ const UserProfile: Component = (initProps, ctx) => {
1465
1644
  }
1466
1645
  ```
1467
1646
 
1647
+ ### asyncComponent 工厂(形态 C)— 同步式数据声明
1648
+
1649
+ `async (ctx) => (initProps, ctx) => (props) => VNode` — 工厂层(async,只执行一次并缓存)声明数据/加载代码,mount/render 保持同步。数据经闭包注入组件,渲染无 loading 分支:
1650
+
1651
+ ```tsx
1652
+ import { asyncComponent } from 'weifuwu/client'
1653
+
1654
+ const UserProfile = asyncComponent(async (ctx) => {
1655
+ const user = await ctx.data.get(`/api/user/${ctx.params.id}`)
1656
+ return (_init, ctx) => {
1657
+ const $ = ctx.ui.$()
1658
+ $.liked = false // 客户端状态(交互后变化)
1659
+ return (props) =>
1660
+ h('div', {},
1661
+ h('p', {}, user.name), // 服务端状态(闭包,SSR 进 HTML)
1662
+ h('button', { onClick: () => $.liked = !$.liked }, $.liked ? '❤️' : '🤍'),
1663
+ )
1664
+ }
1665
+ })
1666
+ ```
1667
+
1668
+ - **客户端**:首次渲染占位 → 工厂 resolve 后整树重渲染补全(SPA);数据经 `ctx.data` 缓存(hydration 时从 `__DATA__` 同步命中,不重跑请求)
1669
+ - **服务端**:`ctx.ui.ssr()` 直接 await 工厂 → 数据进 HTML(无占位)
1670
+ - 工厂缓存绑定页面上下文:路由导航/登录登出时自动失效,工厂以新 ctx 重新执行
1671
+ - 会变的数据:初始值 seed 自服务端数据(`$.count = data.count`),交互改 `$`;初始状态必须确定性(禁止 `window.innerWidth` 直接初始化 → SSR/hydration mismatch)
1672
+
1468
1673
  ---
1469
1674
 
1470
1675
  ## router + RouteView — 前端路由
@@ -1936,7 +2141,7 @@ import type { RouterOptions } from 'weifuwu/client'
1936
2141
 
1937
2142
  # 组件库 (`weifuwu/components`)
1938
2143
 
1939
- 42 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
2144
+ 43 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
1940
2145
 
1941
2146
  ```ts
1942
2147
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
@@ -2152,11 +2357,17 @@ props 变化 ──────────────────────
2152
2357
  |-----|--------|-----------|------|
2153
2358
  | Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
2154
2359
 
2360
+ ### 全局工具
2361
+
2362
+ | 组件 | 导入名 | 关键 Props | 说明 |
2363
+ |-----|--------|-----------|------|
2364
+ | ThemeSwitch | `ThemeSwitch` | `mode: 'auto'\|'light'\|'dark'`, `onChange`, `storageKey` | 主题切换(auto/light/dark,localStorage 持久化);另有 `applyTheme()` / `getTheme()` 命令式工具 |
2365
+
2155
2366
  ---
2156
2367
 
2157
2368
  # 布局系统 (`weifuwu/layout`)
2158
2369
 
2159
- 纯 CSS 布局原语 + 82 个主题 Token。不绑定任何 JS 框架。
2370
+ 纯 CSS 布局原语 + 91 个主题 Token。不绑定任何 JS 框架。
2160
2371
 
2161
2372
  > **全栈 weifuwu 项目**:`weifuwu/components/style.css` 已包含布局系统,一条 import 就够了,无需单独引用本页。
2162
2373
  > 本页仅适用于**非 weifuwu 项目**或**只需 CSS 布局**的场景。
@@ -2216,7 +2427,7 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2216
2427
  | | `wf-inline-block` | display: inline-block |
2217
2428
  | | `wf-contents` | display: contents |
2218
2429
 
2219
- ## 82 个主题 Token
2430
+ ## 91 个主题 Token
2220
2431
 
2221
2432
  ```css
2222
2433
  /* 品牌色 */
@@ -2275,9 +2486,15 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2275
2486
 
2276
2487
  ### 暗色模式
2277
2488
 
2489
+ 两种激活方式(显式 `data-theme` 优先级更高):
2490
+
2278
2491
  ```ts
2279
- document.documentElement.setAttribute('data-theme', 'dark')
2280
- // 所有 var(--wf-*) 自动切换
2492
+ // 1. 手动切换
2493
+ // document.documentElement.setAttribute('data-theme', 'dark')
2494
+ // document.documentElement.setAttribute('data-theme', 'light') // 强制亮色
2495
+
2496
+ // 2. 自动:系统暗色偏好(无需任何代码)
2497
+ // 系统为暗色时自动生效;加 data-theme="light" 可强制亮色
2281
2498
  ```
2282
2499
 
2283
2500
  ---
@@ -2299,11 +2516,19 @@ document.documentElement.setAttribute('data-theme', 'dark')
2299
2516
 
2300
2517
  ## 暗色模式
2301
2518
 
2519
+ 两种激活方式(显式 `data-theme` 优先级高于系统偏好):
2520
+
2302
2521
  ```ts
2522
+ // 手动切换
2303
2523
  document.documentElement.setAttribute('data-theme', 'dark')
2524
+
2525
+ // 强制亮色(系统为暗色时也保持亮色)
2526
+ document.documentElement.setAttribute('data-theme', 'light')
2304
2527
  ```
2305
2528
 
2306
- 所有 `--wf-*` 变量在 `[data-theme="dark"]` 下自动切换。可自定义暗色变量:
2529
+ 未设置 `data-theme` 时,自动跟随系统偏好:`@media (prefers-color-scheme: dark)` 下自动切换暗色。
2530
+
2531
+ 所有 `--wf-*` 变量在暗色下自动切换。可自定义暗色变量:
2307
2532
 
2308
2533
  ```css
2309
2534
  [data-theme="dark"] {
@@ -2311,6 +2536,15 @@ document.documentElement.setAttribute('data-theme', 'dark')
2311
2536
  --wf-color-text: #e0e0e0;
2312
2537
  --wf-color-border: #2a2a4a;
2313
2538
  }
2539
+
2540
+ /* 自定义系统自动暗色的变量(需与上面同步) */
2541
+ @media (prefers-color-scheme: dark) {
2542
+ :root:not([data-theme="light"]) {
2543
+ --wf-color-bg: #1a1a2e;
2544
+ --wf-color-text: #e0e0e0;
2545
+ --wf-color-border: #2a2a4a;
2546
+ }
2547
+ }
2314
2548
  ```
2315
2549
 
2316
2550
  ## 组件级覆盖
@@ -19,6 +19,8 @@ import type { Component } from './vnode.ts';
19
19
  /** 应用句柄:use() 链式累积中间件注入的 ctx 类型,mount() 时组件拿到完整注入 */
20
20
  export interface App<C extends object = {}> {
21
21
  use<I extends object, O extends object>(mw: AppMiddleware<I, O>): App<C & O>;
22
- mount(rootSelector: string, root: Component<any, C>): Promise<void>;
22
+ mount(rootSelector: string, root: Component<any, C>, options?: {
23
+ hydrate?: boolean;
24
+ }): Promise<void>;
23
25
  }
24
26
  export declare function createApp<C extends object = {}>(): App<C>;
@@ -16,7 +16,8 @@
16
16
  * WfuiContext / AppMiddleware / RouteDef → 类型
17
17
  */
18
18
  export { h, jsx, jsxs, jsxDEV, Fragment, Portal, createPortal } from './vnode.ts';
19
- export type { VNode, VNodeType, Component } from './vnode.ts';
19
+ export type { VNode, VNodeType, Component, AsyncComponent } from './vnode.ts';
20
+ export { asyncComponent, isAsyncComponent } from './vnode.ts';
20
21
  export { createApp } from './app.ts';
21
22
  export type { App } from './app.ts';
22
23
  export { router, RouteView } from './router.ts';
@@ -36,7 +37,7 @@ export { i18n } from './i18n.ts';
36
37
  export type { I18nOptions, I18nState, I18nInjected } from './i18n.ts';
37
38
  export { lockScroll, unlockScroll } from './scroll-lock.ts';
38
39
  export { trapFocus } from './focus-trap.ts';
39
- export { computeFixedPos } from './popup.ts';
40
+ export { computeFixedPos, computeFixedPosRect } from './popup.ts';
40
41
  export type { FixedPos, Placement } from './popup.ts';
41
42
  export { zhCN } from './locale/zh_CN.ts';
42
43
  export { enUS } from './locale/en_US.ts';