weifuwu 0.55.1 → 0.55.3

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
@@ -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
 
@@ -149,8 +149,8 @@ createApp()
149
149
  | 资源 | CDN 地址 | 说明 |
150
150
  |------|---------|------|
151
151
  | `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 个布局原语 |
152
+ | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 43 个 UI 组件(Button, Card, Table, Modal 等) |
153
+ | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 91 个主题 Token + 35 个布局原语 |
154
154
  | 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
155
155
 
156
156
 
@@ -169,8 +169,9 @@ createApp()
169
169
  | `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
170
170
  | `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
171
171
  | `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
172
- | `weifuwu` | **response** | HTTP 响应辅助函数(ok/badRequest/...) | — |
172
+ | `weifuwu` | **ok / badRequest / …** | HTTP 响应辅助函数(ok/badRequest/... 等 12 个) | — |
173
173
  | `weifuwu` | **parseBody** | JSON 请求体安全解析 | — |
174
+ | Router 方法 | **app.graphql()** | GraphQL 端点(支持 GraphiQL),Router 实例方法(无需单独 import) | Router |
174
175
  | `weifuwu/client` | **createApp** | 应用引导 + VDOM 渲染引擎 | — |
175
176
  | `weifuwu/client` | **router / RouteView** | 前端路由(history/hash 模式) | createApp |
176
177
  | `weifuwu/client` | **api / auth / ws** | HTTP 客户端 / 认证 / WebSocket 中间件 | createApp |
@@ -178,8 +179,8 @@ createApp()
178
179
  | `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
179
180
  | `weifuwu/client` | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
180
181
  | `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`) | — |
182
+ | `weifuwu/components` | **43 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/client |
183
+ | `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 91 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
183
184
 
184
185
  ---
185
186
 
@@ -991,6 +992,35 @@ const Badge: Component = () =>
991
992
  (props) => h('span', { class: `badge-${props.variant}` }, props.children)
992
993
  ```
993
994
 
995
+ ### 类型流(props 泛型 + ctx 注入)
996
+
997
+ ```tsx
998
+ import type { Component } from 'weifuwu/client'
999
+ import type { ApiInjected, RouteInjected } from 'weifuwu/client'
1000
+
1001
+ // ① props 泛型:JSX 使用时自动类型检查(传错类型编译期报错)
1002
+ interface DeckCardProps { title: string; pages: number }
1003
+ const DeckCard: Component<DeckCardProps> = (_init, ctx) =>
1004
+ (props) => <div>{props.title} / {props.pages} 页</div>
1005
+ // <DeckCard title="x" pages={8} /> ✓
1006
+ // <DeckCard title="x" pages="8" /> ✗ 编译期报错
1007
+
1008
+ // ② ctx 注入声明:use(api()).use(router()) 后组件声明依赖,ctx 直接访问
1009
+ const Home: Component<{}, ApiInjected & RouteInjected> = (_init, ctx) => {
1010
+ ctx.api.get('/users') // ✓ 有类型
1011
+ ctx.app.navigate('/x') // ✓ 有类型
1012
+ return () => <h1>Home</h1>
1013
+ }
1014
+ // 未声明的注入字段编译期报错——注入从"文档约定"变成"类型保证"
1015
+
1016
+ createApp()
1017
+ .use(api()) // 注入 ctx.api
1018
+ .use(router({ routes })) // 注入 ctx.route / ctx.app
1019
+ .mount('#root', Home) // mount 时类型累积完整
1020
+ ```
1021
+
1022
+ > 各中间件的注入接口:`api()` → `ApiInjected`、`auth()` → `AuthInjected`、`ws()` → `WsInjected`、`i18n()` → `I18nInjected`、`router()` → `RouteInjected`(均可从 `weifuwu/client` 导入)。
1023
+
994
1024
  | 规则 | 说明 |
995
1025
  |------|------|
996
1026
  | 组件签名 | `(initProps: P, ctx: WfuiContext) => (props: P) => VNode \| null` |
@@ -1907,7 +1937,7 @@ import type { RouterOptions } from 'weifuwu/client'
1907
1937
 
1908
1938
  # 组件库 (`weifuwu/components`)
1909
1939
 
1910
- 42 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
1940
+ 43 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
1911
1941
 
1912
1942
  ```ts
1913
1943
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
@@ -2123,11 +2153,17 @@ props 变化 ──────────────────────
2123
2153
  |-----|--------|-----------|------|
2124
2154
  | Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
2125
2155
 
2156
+ ### 全局工具
2157
+
2158
+ | 组件 | 导入名 | 关键 Props | 说明 |
2159
+ |-----|--------|-----------|------|
2160
+ | ThemeSwitch | `ThemeSwitch` | `mode: 'auto'\|'light'\|'dark'`, `onChange`, `storageKey` | 主题切换(auto/light/dark,localStorage 持久化);另有 `applyTheme()` / `getTheme()` 命令式工具 |
2161
+
2126
2162
  ---
2127
2163
 
2128
2164
  # 布局系统 (`weifuwu/layout`)
2129
2165
 
2130
- 纯 CSS 布局原语 + 82 个主题 Token。不绑定任何 JS 框架。
2166
+ 纯 CSS 布局原语 + 91 个主题 Token。不绑定任何 JS 框架。
2131
2167
 
2132
2168
  > **全栈 weifuwu 项目**:`weifuwu/components/style.css` 已包含布局系统,一条 import 就够了,无需单独引用本页。
2133
2169
  > 本页仅适用于**非 weifuwu 项目**或**只需 CSS 布局**的场景。
@@ -2187,7 +2223,7 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2187
2223
  | | `wf-inline-block` | display: inline-block |
2188
2224
  | | `wf-contents` | display: contents |
2189
2225
 
2190
- ## 82 个主题 Token
2226
+ ## 91 个主题 Token
2191
2227
 
2192
2228
  ```css
2193
2229
  /* 品牌色 */
@@ -2246,9 +2282,15 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2246
2282
 
2247
2283
  ### 暗色模式
2248
2284
 
2285
+ 两种激活方式(显式 `data-theme` 优先级更高):
2286
+
2249
2287
  ```ts
2250
- document.documentElement.setAttribute('data-theme', 'dark')
2251
- // 所有 var(--wf-*) 自动切换
2288
+ // 1. 手动切换
2289
+ // document.documentElement.setAttribute('data-theme', 'dark')
2290
+ // document.documentElement.setAttribute('data-theme', 'light') // 强制亮色
2291
+
2292
+ // 2. 自动:系统暗色偏好(无需任何代码)
2293
+ // 系统为暗色时自动生效;加 data-theme="light" 可强制亮色
2252
2294
  ```
2253
2295
 
2254
2296
  ---
@@ -2270,11 +2312,19 @@ document.documentElement.setAttribute('data-theme', 'dark')
2270
2312
 
2271
2313
  ## 暗色模式
2272
2314
 
2315
+ 两种激活方式(显式 `data-theme` 优先级高于系统偏好):
2316
+
2273
2317
  ```ts
2318
+ // 手动切换
2274
2319
  document.documentElement.setAttribute('data-theme', 'dark')
2320
+
2321
+ // 强制亮色(系统为暗色时也保持亮色)
2322
+ document.documentElement.setAttribute('data-theme', 'light')
2275
2323
  ```
2276
2324
 
2277
- 所有 `--wf-*` 变量在 `[data-theme="dark"]` 下自动切换。可自定义暗色变量:
2325
+ 未设置 `data-theme` 时,自动跟随系统偏好:`@media (prefers-color-scheme: dark)` 下自动切换暗色。
2326
+
2327
+ 所有 `--wf-*` 变量在暗色下自动切换。可自定义暗色变量:
2278
2328
 
2279
2329
  ```css
2280
2330
  [data-theme="dark"] {
@@ -2282,6 +2332,15 @@ document.documentElement.setAttribute('data-theme', 'dark')
2282
2332
  --wf-color-text: #e0e0e0;
2283
2333
  --wf-color-border: #2a2a4a;
2284
2334
  }
2335
+
2336
+ /* 自定义系统自动暗色的变量(需与上面同步) */
2337
+ @media (prefers-color-scheme: dark) {
2338
+ :root:not([data-theme="light"]) {
2339
+ --wf-color-bg: #1a1a2e;
2340
+ --wf-color-text: #e0e0e0;
2341
+ --wf-color-border: #2a2a4a;
2342
+ }
2343
+ }
2285
2344
  ```
2286
2345
 
2287
2346
  ## 组件级覆盖
@@ -14,11 +14,11 @@
14
14
  * 并设置 childCtx.ui._selfId = 组件 ID
15
15
  * render() 无参时从 this._selfId 取当前组件 ID
16
16
  */
17
- import type { WfuiContext, AppMiddleware } from './types.ts';
17
+ import type { AppMiddleware } from './types.ts';
18
18
  import type { Component } from './vnode.ts';
19
- export declare function createApp(): {
20
- readonly ctx: WfuiContext;
21
- use(mw: AppMiddleware): /*elided*/ any;
22
- mount(rootSelector: string, RootComponent: Component): Promise<void>;
23
- destroy(): void;
24
- };
19
+ /** 应用句柄:use() 链式累积中间件注入的 ctx 类型,mount() 时组件拿到完整注入 */
20
+ export interface App<C extends object = {}> {
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>;
23
+ }
24
+ export declare function createApp<C extends object = {}>(): App<C>;
@@ -24,4 +24,8 @@ export interface I18nState {
24
24
  setLocale: (lang: string) => void;
25
25
  components: Record<string, Record<string, string>>;
26
26
  }
27
- export declare function i18n(opts?: I18nOptions): AppMiddleware;
27
+ /** i18n 中间件注入到 ctx 的字段 */
28
+ export interface I18nInjected {
29
+ i18n: I18nState;
30
+ }
31
+ export declare function i18n(opts?: I18nOptions): AppMiddleware<{}, I18nInjected>;
@@ -18,22 +18,25 @@
18
18
  export { h, jsx, jsxs, jsxDEV, Fragment, Portal, createPortal } from './vnode.ts';
19
19
  export type { VNode, VNodeType, Component } from './vnode.ts';
20
20
  export { createApp } from './app.ts';
21
+ export type { App } from './app.ts';
21
22
  export { router, RouteView } from './router.ts';
23
+ export type { RouteInjected } from './router.ts';
22
24
  export { ws } from './middleware/ws.ts';
25
+ export type { WsClient, WsInjected } from './middleware/ws.ts';
23
26
  export { api } from './middleware/api.ts';
24
27
  export { auth } from './middleware/auth.ts';
25
- export type { ApiClient, ApiOptions, ApiRequestOptions } from './middleware/api.ts';
28
+ export type { ApiClient, ApiOptions, ApiRequestOptions, ApiInjected } from './middleware/api.ts';
26
29
  export { ApiError } from './middleware/api.ts';
27
- export type { AuthClient, AuthOptions } from './middleware/auth.ts';
30
+ export type { AuthClient, AuthOptions, AuthInjected } from './middleware/auth.ts';
28
31
  export { extendCtx } from './types.ts';
29
32
  export type { WfuiContext, AppMiddleware, RouteDef } from './types.ts';
30
33
  export { ErrorBoundary } from './error-boundary.ts';
31
34
  export type { ErrorBoundaryProps } from './error-boundary.ts';
32
35
  export { i18n } from './i18n.ts';
33
- export type { I18nOptions, I18nState } from './i18n.ts';
36
+ export type { I18nOptions, I18nState, I18nInjected } from './i18n.ts';
34
37
  export { lockScroll, unlockScroll } from './scroll-lock.ts';
35
38
  export { trapFocus } from './focus-trap.ts';
36
- export { computeFixedPos } from './popup.ts';
39
+ export { computeFixedPos, computeFixedPosRect } from './popup.ts';
37
40
  export type { FixedPos, Placement } from './popup.ts';
38
41
  export { zhCN } from './locale/zh_CN.ts';
39
42
  export { enUS } from './locale/en_US.ts';
@@ -596,6 +596,15 @@ function cleanupPortalChildren(vnode) {
596
596
  function callRefCleanup(input) {
597
597
  if (input == null || typeof input !== "object") return;
598
598
  const vnode = input;
599
+ if (vnode._id) {
600
+ if (vnode._customId) idRegistry.delete(vnode._customId);
601
+ idRegistry.delete(vnode._id);
602
+ vnode._id = void 0;
603
+ vnode._customId = void 0;
604
+ vnode._render = void 0;
605
+ vnode._parentNode = void 0;
606
+ vnode._refNode = void 0;
607
+ }
599
608
  if (vnode._child != null) {
600
609
  if (Array.isArray(vnode._child)) {
601
610
  for (const child of vnode._child) {
@@ -1010,7 +1019,13 @@ function router(opts) {
1010
1019
  title: last?.title ?? ""
1011
1020
  };
1012
1021
  }
1013
- return { path, params: {}, query: {}, chain: [], title: "" };
1022
+ return {
1023
+ path,
1024
+ params: {},
1025
+ query: Object.fromEntries(new URLSearchParams(window.location.search)),
1026
+ chain: opts.notFound ? [{ component: opts.notFound, title: "Not Found" }] : [],
1027
+ title: ""
1028
+ };
1014
1029
  }
1015
1030
  return (ctx) => {
1016
1031
  const resolved = resolve(getPath());
@@ -1582,6 +1597,7 @@ export {
1582
1597
  api,
1583
1598
  auth,
1584
1599
  computeFixedPos,
1600
+ computeFixedPosRect,
1585
1601
  createApp,
1586
1602
  createPortal,
1587
1603
  enUS,
@@ -61,7 +61,11 @@ export interface ApiRequestOptions {
61
61
  * await ctx.api.delete('/users/1')
62
62
  * ```
63
63
  */
64
- export declare function api(options?: ApiOptions): AppMiddleware;
64
+ /** api 中间件注入到 ctx 的字段 */
65
+ export interface ApiInjected {
66
+ api: ApiClient;
67
+ }
68
+ export declare function api(options?: ApiOptions): AppMiddleware<{}, ApiInjected>;
65
69
  /**
66
70
  * API 错误 — 包含 HTTP 状态码和响应文本。
67
71
  *
@@ -21,4 +21,8 @@ export interface AuthClient {
21
21
  setUser: (user: any) => void;
22
22
  refresh: () => Promise<boolean>;
23
23
  }
24
- export declare function auth(options?: AuthOptions): AppMiddleware;
24
+ /** auth 中间件注入到 ctx 的字段 */
25
+ export interface AuthInjected {
26
+ auth: AuthClient;
27
+ }
28
+ export declare function auth(options?: AuthOptions): AppMiddleware<{}, AuthInjected>;
@@ -12,4 +12,14 @@ export interface WsOptions {
12
12
  pingInterval?: number;
13
13
  pingTimeout?: number;
14
14
  }
15
- export declare function ws(options?: WsOptions): AppMiddleware;
15
+ /** ws 中间件注入到 ctx 的字段 */
16
+ /** ws 中间件注入的客户端形状(与 WfuiContext.ws 一致) */
17
+ export interface WsClient {
18
+ send: (msg: unknown) => void;
19
+ onMessage: (fn: (data: unknown) => void) => () => void;
20
+ isConnected: boolean;
21
+ }
22
+ export interface WsInjected {
23
+ ws: WsClient;
24
+ }
25
+ export declare function ws(options?: WsOptions): AppMiddleware<{}, WsInjected>;
@@ -9,5 +9,18 @@ export interface RouterOptions {
9
9
  routes: RouteDef[];
10
10
  notFound?: (props: any, ctx: WfuiContext) => any;
11
11
  }
12
- export declare function router(opts: RouterOptions): AppMiddleware;
12
+ /** router 中间件注入到 ctx 的字段 */
13
+ export interface RouteInjected {
14
+ route: {
15
+ path: string;
16
+ params: Record<string, string>;
17
+ query: Record<string, string>;
18
+ title?: string;
19
+ };
20
+ /** 编程式导航 */
21
+ app: {
22
+ navigate: (path: string) => void;
23
+ };
24
+ }
25
+ export declare function router(opts: RouterOptions): AppMiddleware<{}, RouteInjected>;
13
26
  export declare function RouteView(_props: {}, ctx: WfuiContext): () => any;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 类型流测试(编译期验证)——组件 props 泛型 + ctx 注入链式累积。
3
+ *
4
+ * 运行方式:这些断言是类型层面的,由 tsc --noEmit 保证。
5
+ * 运行时测试仅验证 createApp().use() 链式调用不抛错。
6
+ */
7
+ export {};
@@ -81,7 +81,12 @@ export interface WfuiContext {
81
81
  };
82
82
  }
83
83
  /** 中间件签名 */
84
- export type AppMiddleware = (ctx: WfuiContext) => WfuiContext;
84
+ /**
85
+ * 前端中间件:输入 ctx 需要 I,输出 ctx 注入 O(链式累积,createApp().use() 类型自动合并)
86
+ * api() → AppMiddleware<{}, ApiInjected> 注入 ctx.api
87
+ * router()→ AppMiddleware<{}, RouteInjected> 注入 ctx.route / ctx.app
88
+ */
89
+ export type AppMiddleware<I extends object = {}, O extends object = I> = (ctx: WfuiContext & I) => (WfuiContext & O) | Promise<WfuiContext & O>;
85
90
  /** 路由定义 */
86
91
  export interface RouteDef {
87
92
  path: string;
@@ -23,6 +23,8 @@ export interface VNode {
23
23
  _render?: (props: any) => VNode | null;
24
24
  /** 组件实例 ID(如 '_wf_0') */
25
25
  _id?: string;
26
+ /** 自定义组件 ID(ctx.ui.selfId() 注册,跨组件精准刷新) */
27
+ _customId?: string;
26
28
  /** 组件输出的 DOM 父节点 */
27
29
  _parentNode?: Node;
28
30
  /** 组件输出的第一个 DOM 节点 */
@@ -30,7 +32,11 @@ export interface VNode {
30
32
  /** 组件 mount/render 时的 ctx 版本号(供三态 skip 判定) */
31
33
  _ctxVersion?: number;
32
34
  }
33
- export type Component<P = {}> = (initProps: P, ctx: WfuiContext) => ((props: P) => VNode | null) | null;
35
+ /**
36
+ * 两阶段组件:外层 = mount(一次),内层 = render(每次 dirty/props 变化)。
37
+ * P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
38
+ */
39
+ export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => ((props: P) => VNode | null) | null;
34
40
  export declare const Fragment: unique symbol;
35
41
  /** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
36
42
  export declare const Portal: unique symbol;
@@ -22,5 +22,7 @@ export interface TableProps {
22
22
  onSort?: (key: string, order: 'asc' | 'desc') => void;
23
23
  /** 数据为空时显示的文本 */
24
24
  emptyText?: string;
25
+ /** 表格最小宽度(窄屏横向滚动,如 '720px') */
26
+ minWidth?: string;
25
27
  }
26
28
  export declare const Table: Component<TableProps>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * weifuwu/components — ThemeSwitch 主题切换器
3
+ *
4
+ * 三段式切换:auto(跟随系统偏好)/ light(强制亮色)/ dark(强制暗色)。
5
+ * 对应 layout 的暗色双段激活机制:
6
+ * - auto → 移除 data-theme,由 @media (prefers-color-scheme) 决定
7
+ * - light → <html data-theme="light">(系统暗色也强制亮色)
8
+ * - dark → <html data-theme="dark">
9
+ *
10
+ * mount 时读取 localStorage 并立即应用(避免闪白);
11
+ * 用户选择后持久化。
12
+ */
13
+ import type { Component } from '../../client/vnode.ts';
14
+ export type ThemeMode = 'auto' | 'light' | 'dark';
15
+ export interface ThemeSwitchProps {
16
+ /** 初始模式(默认从 localStorage 读取,无记录时为 auto) */
17
+ mode?: ThemeMode;
18
+ /** 切换回调 */
19
+ onChange?: (mode: ThemeMode) => void;
20
+ /** localStorage 存储 key */
21
+ storageKey?: string;
22
+ }
23
+ /** 应用主题:auto 移除属性,light/dark 显式设置 */
24
+ export declare function applyTheme(mode: ThemeMode): void;
25
+ /** 读取当前生效主题(localStorage 优先,其次系统偏好) */
26
+ export declare function getTheme(): ThemeMode;
27
+ export declare const ThemeSwitch: Component<ThemeSwitchProps>;
@@ -89,3 +89,6 @@ export { Chart } from './Chart/Chart.ts';
89
89
  export type { ChartProps, ChartType, DataPoint, ChartOptions } from './Chart/Chart.ts';
90
90
  export { Editor } from './Editor/Editor.ts';
91
91
  export type { EditorProps, ToolbarItem } from './Editor/Editor.ts';
92
+ export { ThemeSwitch } from './ThemeSwitch/ThemeSwitch.ts';
93
+ export type { ThemeSwitchProps, ThemeMode } from './ThemeSwitch/ThemeSwitch.ts';
94
+ export { applyTheme, getTheme } from './ThemeSwitch/ThemeSwitch.ts';
@@ -296,6 +296,7 @@ var Table = (_init, _ctx) => (props) => {
296
296
  return h("th", {
297
297
  class: `wf-table-th${col.sortable ? " wf-table-th--sortable" : ""}${isSorted ? " wf-table-th--sorted" : ""}`,
298
298
  scope: "col",
299
+ tabindex: col.sortable ? 0 : void 0,
299
300
  style: col.width ? { width: col.width } : void 0,
300
301
  onClick: col.sortable && onSort ? () => onSort(col.key, isSorted && isAsc ? "desc" : "asc") : void 0
301
302
  }, label);
@@ -327,7 +328,11 @@ var Table = (_init, _ctx) => (props) => {
327
328
  });
328
329
  }
329
330
  const tbody = h("tbody", { class: "wf-table-tbody" }, bodyRows);
330
- return h("table", { class: "wf-table" }, [thead, tbody]);
331
+ const table = h("table", {
332
+ class: "wf-table",
333
+ style: props.minWidth ? { minWidth: props.minWidth } : void 0
334
+ }, [thead, tbody]);
335
+ return h("div", { class: "wf-table-wrap" }, table);
331
336
  };
332
337
 
333
338
  // src/client/scroll-lock.ts
@@ -655,6 +660,15 @@ function cleanupPortalChildren(vnode) {
655
660
  function callRefCleanup(input) {
656
661
  if (input == null || typeof input !== "object") return;
657
662
  const vnode = input;
663
+ if (vnode._id) {
664
+ if (vnode._customId) idRegistry.delete(vnode._customId);
665
+ idRegistry.delete(vnode._id);
666
+ vnode._id = void 0;
667
+ vnode._customId = void 0;
668
+ vnode._render = void 0;
669
+ vnode._parentNode = void 0;
670
+ vnode._refNode = void 0;
671
+ }
658
672
  if (vnode._child != null) {
659
673
  if (Array.isArray(vnode._child)) {
660
674
  for (const child of vnode._child) {
@@ -2970,6 +2984,68 @@ var Editor = (_props, ctx) => {
2970
2984
  ]);
2971
2985
  };
2972
2986
  };
2987
+
2988
+ // src/components/ThemeSwitch/ThemeSwitch.ts
2989
+ var DEFAULT_KEY = "wf_theme";
2990
+ function readStored(key) {
2991
+ try {
2992
+ const v = localStorage.getItem(key);
2993
+ return v === "light" || v === "dark" || v === "auto" ? v : null;
2994
+ } catch {
2995
+ return null;
2996
+ }
2997
+ }
2998
+ function writeStored(key, mode) {
2999
+ try {
3000
+ localStorage.setItem(key, mode);
3001
+ } catch {
3002
+ }
3003
+ }
3004
+ function applyTheme(mode) {
3005
+ if (typeof document === "undefined") return;
3006
+ if (mode === "auto") document.documentElement.removeAttribute("data-theme");
3007
+ else document.documentElement.setAttribute("data-theme", mode);
3008
+ }
3009
+ function getTheme() {
3010
+ const stored = readStored(DEFAULT_KEY);
3011
+ if (stored) return stored;
3012
+ return "auto";
3013
+ }
3014
+ var ThemeSwitch = (initProps, ctx) => {
3015
+ const storageKey = initProps.storageKey ?? DEFAULT_KEY;
3016
+ let mode = initProps.mode ?? readStored(storageKey) ?? "auto";
3017
+ applyTheme(mode);
3018
+ return (props) => {
3019
+ const SL = ctx?.i18n?.components?.ThemeSwitch ?? {};
3020
+ const modes = [
3021
+ { value: "auto", label: SL.auto ?? "\u81EA\u52A8" },
3022
+ { value: "light", label: SL.light ?? "\u4EAE\u8272" },
3023
+ { value: "dark", label: SL.dark ?? "\u6697\u8272" }
3024
+ ];
3025
+ const segments = modes.map(
3026
+ (m) => h("button", {
3027
+ type: "button",
3028
+ class: `wf-theme-seg${mode === m.value ? " wf-theme-seg--active" : ""}`,
3029
+ role: "radio",
3030
+ "aria-checked": String(mode === m.value),
3031
+ "aria-label": m.label,
3032
+ onClick: () => {
3033
+ if (mode === m.value) return;
3034
+ mode = m.value;
3035
+ applyTheme(mode);
3036
+ writeStored(storageKey, mode);
3037
+ props.onChange?.(mode);
3038
+ ctx.ui.render();
3039
+ }
3040
+ }, m.label)
3041
+ );
3042
+ return h("div", {
3043
+ class: "wf-theme-switch",
3044
+ role: "radiogroup",
3045
+ "aria-label": SL.label ?? "\u4E3B\u9898\u5207\u6362"
3046
+ }, segments);
3047
+ };
3048
+ };
2973
3049
  export {
2974
3050
  Accordion,
2975
3051
  Alert,
@@ -3011,8 +3087,11 @@ export {
3011
3087
  Tabs,
3012
3088
  Tag,
3013
3089
  Textarea,
3090
+ ThemeSwitch,
3014
3091
  Toast,
3015
3092
  Tooltip,
3093
+ applyTheme,
3016
3094
  confirm,
3095
+ getTheme,
3017
3096
  toast
3018
3097
  };