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 +71 -12
- package/dist/client/app.d.ts +7 -7
- package/dist/client/i18n.d.ts +5 -1
- package/dist/client/index.d.ts +7 -4
- package/dist/client/index.js +17 -1
- package/dist/client/middleware/api.d.ts +5 -1
- package/dist/client/middleware/auth.d.ts +5 -1
- package/dist/client/middleware/ws.d.ts +11 -1
- package/dist/client/router.d.ts +14 -1
- package/dist/client/type-flow.test.d.ts +7 -0
- package/dist/client/types.d.ts +6 -1
- package/dist/client/vnode.d.ts +7 -1
- package/dist/components/Table/Table.d.ts +2 -0
- package/dist/components/ThemeSwitch/ThemeSwitch.d.ts +27 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +80 -1
- package/dist/components/style.css +285 -44
- package/dist/index.d.ts +1 -1
- package/dist/index.js +20 -5
- package/dist/layout/weifuwu-layout.css +180 -15
- package/package.json +2 -2
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
|
|
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` |
|
|
153
|
-
| 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS +
|
|
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` | **
|
|
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` | **
|
|
182
|
-
| `weifuwu/layout` | **CSS 布局** | 35 个布局原语 +
|
|
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
|
-
|
|
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 布局原语 +
|
|
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
|
-
##
|
|
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
|
-
|
|
2251
|
-
//
|
|
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
|
-
|
|
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
|
## 组件级覆盖
|
package/dist/client/app.d.ts
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
* 并设置 childCtx.ui._selfId = 组件 ID
|
|
15
15
|
* render() 无参时从 this._selfId 取当前组件 ID
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
17
|
+
import type { AppMiddleware } from './types.ts';
|
|
18
18
|
import type { Component } from './vnode.ts';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
use(mw: AppMiddleware):
|
|
22
|
-
mount(rootSelector: string,
|
|
23
|
-
|
|
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>;
|
package/dist/client/i18n.d.ts
CHANGED
|
@@ -24,4 +24,8 @@ export interface I18nState {
|
|
|
24
24
|
setLocale: (lang: string) => void;
|
|
25
25
|
components: Record<string, Record<string, string>>;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** i18n 中间件注入到 ctx 的字段 */
|
|
28
|
+
export interface I18nInjected {
|
|
29
|
+
i18n: I18nState;
|
|
30
|
+
}
|
|
31
|
+
export declare function i18n(opts?: I18nOptions): AppMiddleware<{}, I18nInjected>;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/client/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/client/router.d.ts
CHANGED
|
@@ -9,5 +9,18 @@ export interface RouterOptions {
|
|
|
9
9
|
routes: RouteDef[];
|
|
10
10
|
notFound?: (props: any, ctx: WfuiContext) => any;
|
|
11
11
|
}
|
|
12
|
-
|
|
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;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -81,7 +81,12 @@ export interface WfuiContext {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
/** 中间件签名 */
|
|
84
|
-
|
|
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;
|
package/dist/client/vnode.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -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';
|
package/dist/components/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|