weifuwu 0.55.2 → 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
 
@@ -1936,7 +1937,7 @@ import type { RouterOptions } from 'weifuwu/client'
1936
1937
 
1937
1938
  # 组件库 (`weifuwu/components`)
1938
1939
 
1939
- 42 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
1940
+ 43 个 HTML 原语组件。每个是 `(_init, ctx) => (props) => VNode`(两阶段组件,与前端框架同一模型),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
1940
1941
 
1941
1942
  ```ts
1942
1943
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
@@ -2152,11 +2153,17 @@ props 变化 ──────────────────────
2152
2153
  |-----|--------|-----------|------|
2153
2154
  | Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
2154
2155
 
2156
+ ### 全局工具
2157
+
2158
+ | 组件 | 导入名 | 关键 Props | 说明 |
2159
+ |-----|--------|-----------|------|
2160
+ | ThemeSwitch | `ThemeSwitch` | `mode: 'auto'\|'light'\|'dark'`, `onChange`, `storageKey` | 主题切换(auto/light/dark,localStorage 持久化);另有 `applyTheme()` / `getTheme()` 命令式工具 |
2161
+
2155
2162
  ---
2156
2163
 
2157
2164
  # 布局系统 (`weifuwu/layout`)
2158
2165
 
2159
- 纯 CSS 布局原语 + 82 个主题 Token。不绑定任何 JS 框架。
2166
+ 纯 CSS 布局原语 + 91 个主题 Token。不绑定任何 JS 框架。
2160
2167
 
2161
2168
  > **全栈 weifuwu 项目**:`weifuwu/components/style.css` 已包含布局系统,一条 import 就够了,无需单独引用本页。
2162
2169
  > 本页仅适用于**非 weifuwu 项目**或**只需 CSS 布局**的场景。
@@ -2216,7 +2223,7 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2216
2223
  | | `wf-inline-block` | display: inline-block |
2217
2224
  | | `wf-contents` | display: contents |
2218
2225
 
2219
- ## 82 个主题 Token
2226
+ ## 91 个主题 Token
2220
2227
 
2221
2228
  ```css
2222
2229
  /* 品牌色 */
@@ -2275,9 +2282,15 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
2275
2282
 
2276
2283
  ### 暗色模式
2277
2284
 
2285
+ 两种激活方式(显式 `data-theme` 优先级更高):
2286
+
2278
2287
  ```ts
2279
- document.documentElement.setAttribute('data-theme', 'dark')
2280
- // 所有 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" 可强制亮色
2281
2294
  ```
2282
2295
 
2283
2296
  ---
@@ -2299,11 +2312,19 @@ document.documentElement.setAttribute('data-theme', 'dark')
2299
2312
 
2300
2313
  ## 暗色模式
2301
2314
 
2315
+ 两种激活方式(显式 `data-theme` 优先级高于系统偏好):
2316
+
2302
2317
  ```ts
2318
+ // 手动切换
2303
2319
  document.documentElement.setAttribute('data-theme', 'dark')
2320
+
2321
+ // 强制亮色(系统为暗色时也保持亮色)
2322
+ document.documentElement.setAttribute('data-theme', 'light')
2304
2323
  ```
2305
2324
 
2306
- 所有 `--wf-*` 变量在 `[data-theme="dark"]` 下自动切换。可自定义暗色变量:
2325
+ 未设置 `data-theme` 时,自动跟随系统偏好:`@media (prefers-color-scheme: dark)` 下自动切换暗色。
2326
+
2327
+ 所有 `--wf-*` 变量在暗色下自动切换。可自定义暗色变量:
2307
2328
 
2308
2329
  ```css
2309
2330
  [data-theme="dark"] {
@@ -2311,6 +2332,15 @@ document.documentElement.setAttribute('data-theme', 'dark')
2311
2332
  --wf-color-text: #e0e0e0;
2312
2333
  --wf-color-border: #2a2a4a;
2313
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
+ }
2314
2344
  ```
2315
2345
 
2316
2346
  ## 组件级覆盖
@@ -36,7 +36,7 @@ export { i18n } from './i18n.ts';
36
36
  export type { I18nOptions, I18nState, I18nInjected } from './i18n.ts';
37
37
  export { lockScroll, unlockScroll } from './scroll-lock.ts';
38
38
  export { trapFocus } from './focus-trap.ts';
39
- export { computeFixedPos } from './popup.ts';
39
+ export { computeFixedPos, computeFixedPosRect } from './popup.ts';
40
40
  export type { FixedPos, Placement } from './popup.ts';
41
41
  export { zhCN } from './locale/zh_CN.ts';
42
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,
@@ -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 节点 */
@@ -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
  };
@@ -35,8 +35,20 @@
35
35
  --wf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
36
36
 
37
37
  /* ── 层级 ── */
38
- --wf-pop-z: 50;
39
- --wf-cover-z: 100;
38
+ /* 浮层全部 createPortal 到 body,全局参与堆叠。
39
+ 顺序:sticky < overlay < modal/drawer < 弹出面板族 < toast < tooltip。
40
+ modal 与 drawer 互斥出现;面板族同级(dropdown/select/datepicker/popover)用 DOM 顺序裁决。 */
41
+ --wf-z-sticky: 40;
42
+ --wf-z-overlay: 80;
43
+ --wf-z-modal: 100;
44
+ --wf-z-popover: 120; /* dropdown/select/datepicker 面板与 popover 同层 */
45
+ --wf-z-toast: 140;
46
+ --wf-z-tooltip: 160;
47
+ --wf-z-dropdown: var(--wf-z-popover); /* 语义别名:下拉菜单属弹出面板族 */
48
+
49
+ /* ── 兼容别名(旧 token 名,语义近似映射) ── */
50
+ --wf-pop-z: var(--wf-z-popover);
51
+ --wf-cover-z: var(--wf-z-modal);
40
52
 
41
53
  /* ── 色彩 — 品牌 ── */
42
54
  --wf-color-primary: #4f6ef7;
@@ -71,15 +83,15 @@
71
83
  --wf-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
72
84
  --wf-font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
73
85
 
74
- --wf-font-size-xs: 11px;
75
- --wf-font-size-sm: 12px;
76
- --wf-font-size-base: 13px;
77
- --wf-font-size-lg: 14px;
78
- --wf-font-size-xl: 15px;
79
- --wf-font-size-2xl: 20px;
86
+ --wf-font-size-xs: 12px;
87
+ --wf-font-size-sm: 13px;
88
+ --wf-font-size-base: 14px;
89
+ --wf-font-size-lg: 15px;
90
+ --wf-font-size-xl: 16px;
91
+ --wf-font-size-2xl: 21px;
80
92
  --wf-font-size-3xl: 24px;
81
- --wf-font-size-4xl: 28px;
82
- --wf-font-size-5xl: 32px;
93
+ --wf-font-size-4xl: 30px;
94
+ --wf-font-size-5xl: 36px;
83
95
 
84
96
  --wf-font-weight-normal: 400;
85
97
  --wf-font-weight-medium: 500;
@@ -110,6 +122,10 @@
110
122
  --wf-accent-color: var(--wf-color-primary);
111
123
  --wf-caret-color: var(--wf-color-primary);
112
124
 
125
+ /* 控件尺寸:垂直内边距 + 目标高度(移动端在 _base.css 提升到 44px) */
126
+ --wf-control-pad-y: 10px;
127
+ --wf-control-height: 36px;
128
+
113
129
  /* ── 断点 ── */
114
130
  --wf-bp-sm: 640px;
115
131
  --wf-bp-md: 768px;
@@ -126,8 +142,21 @@
126
142
  --wf-letter-spacing-wider: 1px;
127
143
  }
128
144
 
129
- /* weifuwu/theme — 暗色模式 */
130
- [data-theme="dark"] {
145
+ /* weifuwu/theme — 暗色模式
146
+ *
147
+ * 两种激活方式(优先级:显式 data-theme > 系统偏好):
148
+ * 1. 手动:<html data-theme="dark">(或 data-theme="light" 强制亮色)
149
+ * 2. 自动:系统偏好 prefers-color-scheme: dark,无显式覆盖时生效
150
+ *
151
+ * ⚠️ 两段暗色覆盖值必须保持一致(静态 CSS 无法共享选择器列表,改动需同步)。
152
+ * 每段末尾都设置 color-scheme: dark,让原生控件/滚动条跟随。
153
+ */
154
+
155
+ /* ── 激活方式一:手动 data-theme="dark" ── */
156
+ /* color-scheme 用 :root[data-theme] 提升优先级,压过 _base.css 的 :root 定义(导入顺序在后者) */
157
+ :root[data-theme="dark"] {
158
+ color-scheme: dark;
159
+
131
160
  --wf-color-primary: #6b8aff;
132
161
  --wf-color-primary-hover: #8ba3ff;
133
162
  --wf-color-primary-bg: #1e293b;
@@ -166,6 +195,50 @@
166
195
  --wf-surface-bg: var(--wf-color-bg);
167
196
  }
168
197
 
198
+ /* ── 激活方式二:系统暗色偏好(data-theme="light" 可强制亮色) ── */
199
+ @media (prefers-color-scheme: dark) {
200
+ :root:not([data-theme="light"]) {
201
+ color-scheme: dark;
202
+
203
+ --wf-color-primary: #6b8aff;
204
+ --wf-color-primary-hover: #8ba3ff;
205
+ --wf-color-primary-bg: #1e293b;
206
+
207
+ --wf-color-success: #4ade80;
208
+ --wf-color-success-bg: #14532d;
209
+ --wf-color-warning: #fbbf24;
210
+ --wf-color-warning-bg: #451a03;
211
+ --wf-color-error: #f87171;
212
+ --wf-color-error-bg: #451a1a;
213
+
214
+ --wf-color-info: #60a5fa;
215
+ --wf-color-info-bg: #172554;
216
+
217
+ --wf-color-text: #f1f5f9;
218
+ --wf-color-text-secondary: #94a3b8;
219
+ --wf-color-text-tertiary: #64748b;
220
+ --wf-color-text-disabled: #475569;
221
+
222
+ --wf-color-bg: #0f172a;
223
+ --wf-color-bg-secondary: #1e293b;
224
+ --wf-color-bg-tertiary: #334155;
225
+
226
+ --wf-color-border: #334155;
227
+ --wf-color-border-light: #1e293b;
228
+ --wf-color-border-dark: #475569;
229
+
230
+ --wf-surface-shadow: 0 1px 3px rgba(0,0,0,0.3);
231
+ --wf-shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
232
+ --wf-shadow: 0 1px 3px rgba(0,0,0,0.3);
233
+ --wf-shadow-md: 0 4px 12px rgba(0,0,0,0.4);
234
+ --wf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4);
235
+
236
+ --wf-focus-ring: 0 0 0 2px var(--wf-color-primary-bg);
237
+
238
+ --wf-surface-bg: var(--wf-color-bg);
239
+ }
240
+ }
241
+
169
242
  /* weifuwu/theme — 基础元素默认样式 */
170
243
  *,
171
244
  *::before,
@@ -173,6 +246,11 @@
173
246
  box-sizing: border-box;
174
247
  }
175
248
 
249
+ :root {
250
+ /* 原生控件/滚动条跟随主题;暗色激活时在 _dark.css 中切为 dark */
251
+ color-scheme: light;
252
+ }
253
+
176
254
  body {
177
255
  margin: 0;
178
256
  background: var(--wf-color-bg);
@@ -210,7 +288,7 @@ select {
210
288
  color: var(--wf-color-text);
211
289
  border: var(--wf-border-width) solid var(--wf-color-border);
212
290
  border-radius: var(--wf-radius);
213
- padding: var(--wf-space-sm) var(--wf-space);
291
+ padding: var(--wf-control-pad-y) var(--wf-space);
214
292
  }
215
293
 
216
294
  input:focus,
@@ -235,7 +313,8 @@ button {
235
313
  color: var(--wf-color-text);
236
314
  border: var(--wf-border-width) solid var(--wf-color-border);
237
315
  border-radius: var(--wf-radius);
238
- padding: var(--wf-space-sm) var(--wf-space-md);
316
+ padding: var(--wf-control-pad-y) var(--wf-space-md);
317
+ min-height: var(--wf-control-height);
239
318
  cursor: pointer;
240
319
  }
241
320
  button:hover {
@@ -243,6 +322,31 @@ button:hover {
243
322
  color: var(--wf-color-primary);
244
323
  }
245
324
 
325
+ /* 键盘焦点:button 与 [tabindex] 统一 focus ring(仅键盘触发,鼠标点击不闪) */
326
+ button:focus-visible,
327
+ [tabindex]:focus-visible {
328
+ outline: none;
329
+ box-shadow: var(--wf-focus-ring);
330
+ }
331
+
332
+ /* 触屏设备(coarse pointer):交互目标提升到 44px(Apple HIG / WCAG 2.5.8) */
333
+ @media (pointer: coarse) {
334
+ button,
335
+ [role="button"],
336
+ input,
337
+ textarea,
338
+ select,
339
+ .wf-card--clickable,
340
+ .wf-table-th--sortable,
341
+ .wf-accordion-summary,
342
+ .wf-tag-close,
343
+ .wf-checkbox,
344
+ .wf-switch,
345
+ .wf-radio {
346
+ min-height: 44px;
347
+ }
348
+ }
349
+
246
350
  h1, h2, h3, h4, h5, h6 {
247
351
  font-weight: var(--wf-font-weight-semibold);
248
352
  line-height: var(--wf-line-height-tight);
@@ -314,6 +418,18 @@ code {
314
418
  border-radius: var(--wf-radius-sm);
315
419
  }
316
420
 
421
+ /* ── 无障碍:尊重系统减弱动效偏好 ── */
422
+ @media (prefers-reduced-motion: reduce) {
423
+ *,
424
+ *::before,
425
+ *::after {
426
+ animation-duration: 0.01ms !important;
427
+ animation-iteration-count: 1 !important;
428
+ transition-duration: 0.01ms !important;
429
+ scroll-behavior: auto !important;
430
+ }
431
+ }
432
+
317
433
  /* stack — 纵向堆叠 */
318
434
  .wf-stack {
319
435
  display: flex;
@@ -558,7 +674,9 @@ code {
558
674
  min-height: 0;
559
675
  }
560
676
 
561
- /* text-align 对齐原语 */
677
+ /* 排版工具类 字号 / 色阶 / 字重 / 截断(零 CSS 用户自排版) */
678
+
679
+ /* ── text-align ── */
562
680
 
563
681
  .wf-text-left {
564
682
  text-align: left;
@@ -572,6 +690,53 @@ code {
572
690
  text-align: right;
573
691
  }
574
692
 
693
+ /* ── 字号 ── */
694
+
695
+ .wf-text-xs { font-size: var(--wf-font-size-xs); }
696
+ .wf-text-sm { font-size: var(--wf-font-size-sm); }
697
+ .wf-text-base { font-size: var(--wf-font-size-base); }
698
+ .wf-text-lg { font-size: var(--wf-font-size-lg); }
699
+ .wf-text-xl { font-size: var(--wf-font-size-xl); }
700
+ .wf-text-2xl { font-size: var(--wf-font-size-2xl); }
701
+ .wf-text-3xl { font-size: var(--wf-font-size-3xl); }
702
+ .wf-text-4xl { font-size: var(--wf-font-size-4xl); }
703
+ .wf-text-5xl { font-size: var(--wf-font-size-5xl); }
704
+
705
+ /* ── 色阶 ── */
706
+
707
+ .wf-text-secondary { color: var(--wf-color-text-secondary); }
708
+ .wf-text-tertiary { color: var(--wf-color-text-tertiary); }
709
+ .wf-text-disabled { color: var(--wf-color-text-disabled); }
710
+ .wf-text-brand { color: var(--wf-color-primary); }
711
+
712
+ /* ── 字重 ── */
713
+
714
+ .wf-text-medium { font-weight: var(--wf-font-weight-medium); }
715
+ .wf-text-semibold { font-weight: var(--wf-font-weight-semibold); }
716
+ .wf-text-bold { font-weight: var(--wf-font-weight-bold); }
717
+
718
+ /* ── 截断 ── */
719
+
720
+ .wf-truncate {
721
+ overflow: hidden;
722
+ text-overflow: ellipsis;
723
+ white-space: nowrap;
724
+ }
725
+
726
+ .wf-line-clamp-2 {
727
+ display: -webkit-box;
728
+ -webkit-line-clamp: 2;
729
+ -webkit-box-orient: vertical;
730
+ overflow: hidden;
731
+ }
732
+
733
+ .wf-line-clamp-3 {
734
+ display: -webkit-box;
735
+ -webkit-line-clamp: 3;
736
+ -webkit-box-orient: vertical;
737
+ overflow: hidden;
738
+ }
739
+
575
740
  /* weifuwu/components — Button */
576
741
  /* 所有颜色/间距/字号引用 layout 的 --wf-* 变量,不硬编码 */
577
742
 
@@ -583,7 +748,8 @@ code {
583
748
  font-family: var(--wf-font-sans);
584
749
  font-size: var(--wf-font-size-sm);
585
750
  font-weight: var(--wf-font-weight-medium);
586
- padding: var(--wf-space-sm) var(--wf-space-md);
751
+ padding: var(--wf-control-pad-y) var(--wf-space-md);
752
+ min-height: var(--wf-control-height);
587
753
  border-radius: var(--wf-radius);
588
754
  border: var(--wf-border-width) solid transparent;
589
755
  cursor: pointer;
@@ -606,8 +772,8 @@ code {
606
772
  .wf-btn--danger { background: var(--wf-color-error); color: #fff; border-color: var(--wf-color-error); }
607
773
  .wf-btn--danger:hover { opacity: 0.9; }
608
774
 
609
- .wf-btn--sm { padding: var(--wf-space-xs) var(--wf-space-sm); font-size: var(--wf-font-size-xs); }
610
- .wf-btn--lg { padding: var(--wf-space) var(--wf-space-lg); font-size: var(--wf-font-size-base); }
775
+ .wf-btn--sm { padding: var(--wf-space-xs) var(--wf-space-sm); font-size: var(--wf-font-size-xs); min-height: 28px; }
776
+ .wf-btn--lg { padding: var(--wf-space) var(--wf-space-lg); font-size: var(--wf-font-size-base); min-height: 40px; }
611
777
 
612
778
  .wf-btn--block { display: flex; width: 100%; }
613
779
 
@@ -637,7 +803,7 @@ code {
637
803
  .wf-input {
638
804
  font-family: var(--wf-font-sans);
639
805
  font-size: var(--wf-font-size-sm);
640
- padding: var(--wf-space-sm) var(--wf-space);
806
+ padding: var(--wf-control-pad-y) var(--wf-space);
641
807
  border: var(--wf-border-width) solid var(--wf-color-border);
642
808
  border-radius: var(--wf-radius);
643
809
  background: var(--wf-color-bg);
@@ -768,7 +934,7 @@ code {
768
934
  .wf-textarea {
769
935
  font-family: var(--wf-font-sans);
770
936
  font-size: var(--wf-font-size-sm);
771
- padding: var(--wf-space-sm) var(--wf-space);
937
+ padding: var(--wf-control-pad-y) var(--wf-space);
772
938
  border: var(--wf-border-width) solid var(--wf-color-border);
773
939
  border-radius: var(--wf-radius);
774
940
  background: var(--wf-color-bg);
@@ -833,7 +999,7 @@ code {
833
999
  .wf-select {
834
1000
  font-family: var(--wf-font-sans);
835
1001
  font-size: var(--wf-font-size-sm);
836
- padding: var(--wf-space-sm) var(--wf-space);
1002
+ padding: var(--wf-control-pad-y) var(--wf-space);
837
1003
  border: var(--wf-border-width) solid var(--wf-color-border);
838
1004
  border-radius: var(--wf-radius);
839
1005
  background: var(--wf-color-bg);
@@ -897,7 +1063,7 @@ code {
897
1063
  font-family: var(--wf-font-sans);
898
1064
  font-size: var(--wf-font-size-sm);
899
1065
  color: var(--wf-color-text);
900
- padding: var(--wf-space-sm) var(--wf-space);
1066
+ padding: var(--wf-control-pad-y) var(--wf-space);
901
1067
  cursor: pointer;
902
1068
  box-sizing: border-box;
903
1069
  }
@@ -917,11 +1083,11 @@ code {
917
1083
  box-shadow: var(--wf-shadow-md);
918
1084
  max-height: 240px;
919
1085
  overflow-y: auto;
920
- z-index: var(--wf-pop-z, 100);
1086
+ z-index: var(--wf-z-dropdown);
921
1087
  }
922
1088
 
923
1089
  .wf-select-search-opt {
924
- padding: var(--wf-space-sm) var(--wf-space);
1090
+ padding: var(--wf-control-pad-y) var(--wf-space);
925
1091
  font-size: var(--wf-font-size-sm);
926
1092
  color: var(--wf-color-text);
927
1093
  cursor: pointer;
@@ -1129,6 +1295,11 @@ code {
1129
1295
 
1130
1296
  /* weifuwu/components — Table */
1131
1297
 
1298
+ .wf-table-wrap {
1299
+ overflow-x: auto;
1300
+ -webkit-overflow-scrolling: touch;
1301
+ }
1302
+
1132
1303
  .wf-table {
1133
1304
  width: 100%;
1134
1305
  border-collapse: collapse;
@@ -1166,6 +1337,11 @@ code {
1166
1337
  .wf-table-th--sortable:hover {
1167
1338
  color: var(--wf-color-primary);
1168
1339
  }
1340
+ .wf-table-th--sortable:focus-visible {
1341
+ outline: none;
1342
+ box-shadow: var(--wf-focus-ring);
1343
+ color: var(--wf-color-primary);
1344
+ }
1169
1345
  .wf-table-th--sorted {
1170
1346
  color: var(--wf-color-primary);
1171
1347
  }
@@ -1191,7 +1367,7 @@ code {
1191
1367
  display: flex;
1192
1368
  align-items: center;
1193
1369
  justify-content: center;
1194
- z-index: var(--wf-cover-z);
1370
+ z-index: var(--wf-z-modal);
1195
1371
  }
1196
1372
 
1197
1373
  .wf-modal--enter {
@@ -1275,7 +1451,7 @@ code {
1275
1451
  display: flex;
1276
1452
  flex-direction: column;
1277
1453
  gap: var(--wf-gap-sm);
1278
- z-index: var(--wf-cover-z, 100);
1454
+ z-index: var(--wf-z-toast);
1279
1455
  pointer-events: none;
1280
1456
  }
1281
1457
 
@@ -1504,12 +1680,12 @@ code {
1504
1680
  border: var(--wf-border-width) solid var(--wf-color-border);
1505
1681
  border-radius: var(--wf-radius);
1506
1682
  box-shadow: var(--wf-shadow-md);
1507
- z-index: var(--wf-pop-z);
1683
+ z-index: var(--wf-z-dropdown);
1508
1684
  padding: var(--wf-space-xs) 0;
1509
1685
  }
1510
1686
 
1511
1687
  .wf-dropdown--open {
1512
- z-index: var(--wf-cover-z);
1688
+ z-index: var(--wf-z-dropdown);
1513
1689
  }
1514
1690
 
1515
1691
  .wf-dropdown-item {
@@ -1626,6 +1802,11 @@ code {
1626
1802
  box-shadow: var(--wf-shadow-md);
1627
1803
  }
1628
1804
 
1805
+ .wf-card--clickable:focus-visible {
1806
+ outline: none;
1807
+ box-shadow: var(--wf-shadow-md), var(--wf-focus-ring);
1808
+ }
1809
+
1629
1810
  /* weifuwu/components — Badge */
1630
1811
 
1631
1812
  .wf-badge {
@@ -1932,7 +2113,7 @@ code {
1932
2113
  display: flex;
1933
2114
  align-items: center;
1934
2115
  gap: var(--wf-gap-sm);
1935
- padding: var(--wf-space-sm) var(--wf-space);
2116
+ padding: var(--wf-control-pad-y) var(--wf-space);
1936
2117
  border: var(--wf-border-width) solid var(--wf-color-border);
1937
2118
  border-radius: var(--wf-radius);
1938
2119
  background: var(--wf-color-bg);
@@ -2050,6 +2231,12 @@ code {
2050
2231
  background: var(--wf-color-bg-tertiary);
2051
2232
  }
2052
2233
 
2234
+ .wf-accordion-summary:focus-visible {
2235
+ outline: none;
2236
+ box-shadow: var(--wf-focus-ring);
2237
+ border-radius: var(--wf-radius-sm);
2238
+ }
2239
+
2053
2240
  .wf-accordion-content {
2054
2241
  padding: var(--wf-space) var(--wf-space-md) var(--wf-space-md);
2055
2242
  font-size: var(--wf-font-size-sm);
@@ -2247,7 +2434,7 @@ code {
2247
2434
  align-items: center;
2248
2435
  justify-content: space-between;
2249
2436
  gap: var(--wf-gap-sm);
2250
- padding: var(--wf-space-sm) var(--wf-space);
2437
+ padding: var(--wf-control-pad-y) var(--wf-space);
2251
2438
  border: var(--wf-border-width) solid var(--wf-color-border);
2252
2439
  border-radius: var(--wf-radius);
2253
2440
  background: var(--wf-color-bg);
@@ -2307,7 +2494,7 @@ code {
2307
2494
 
2308
2495
  .wf-tooltip {
2309
2496
  position: fixed;
2310
- z-index: var(--wf-cover-z, 1000);
2497
+ z-index: var(--wf-z-tooltip);
2311
2498
  background: var(--wf-color-text);
2312
2499
  color: var(--wf-color-bg);
2313
2500
  font-family: var(--wf-font-sans);
@@ -2396,7 +2583,7 @@ code {
2396
2583
  .wf-drawer {
2397
2584
  position: fixed;
2398
2585
  inset: 0;
2399
- z-index: var(--wf-cover-z, 1000);
2586
+ z-index: var(--wf-z-modal);
2400
2587
  display: flex;
2401
2588
  }
2402
2589
 
@@ -2541,12 +2728,12 @@ code {
2541
2728
  .wf-popover-overlay {
2542
2729
  position: fixed;
2543
2730
  inset: 0;
2544
- z-index: 998;
2731
+ z-index: calc(var(--wf-z-popover) - 1);
2545
2732
  }
2546
2733
 
2547
2734
  .wf-popover {
2548
2735
  position: fixed;
2549
- z-index: 999;
2736
+ z-index: var(--wf-z-popover);
2550
2737
  background: var(--wf-color-bg);
2551
2738
  border: var(--wf-border-width) solid var(--wf-color-border);
2552
2739
  border-radius: var(--wf-radius-md);
@@ -2728,7 +2915,7 @@ code {
2728
2915
  .wf-datepicker-input {
2729
2916
  font-family: var(--wf-font-sans);
2730
2917
  font-size: var(--wf-font-size-sm);
2731
- padding: var(--wf-space-sm) var(--wf-space);
2918
+ padding: var(--wf-control-pad-y) var(--wf-space);
2732
2919
  border: var(--wf-border-width) solid var(--wf-color-border);
2733
2920
  border-radius: var(--wf-radius);
2734
2921
  background: var(--wf-color-bg);
@@ -2754,14 +2941,14 @@ code {
2754
2941
  .wf-datepicker-overlay {
2755
2942
  position: fixed;
2756
2943
  inset: 0;
2757
- z-index: calc(var(--wf-pop-z) - 1);
2944
+ z-index: calc(var(--wf-z-popover) - 1);
2758
2945
  }
2759
2946
 
2760
2947
  /* ── 日历面板 ── */
2761
2948
 
2762
2949
  .wf-datepicker-dropdown {
2763
2950
  position: fixed;
2764
- z-index: var(--wf-pop-z);
2951
+ z-index: var(--wf-z-popover);
2765
2952
  background: var(--wf-color-bg);
2766
2953
  border: var(--wf-border-width) solid var(--wf-color-border);
2767
2954
  border-radius: var(--wf-radius-md);
@@ -2868,7 +3055,7 @@ code {
2868
3055
 
2869
3056
  .wf-datepicker-range-wrap {
2870
3057
  position: fixed;
2871
- z-index: var(--wf-pop-z);
3058
+ z-index: var(--wf-z-popover);
2872
3059
  display: flex;
2873
3060
  gap: var(--wf-space);
2874
3061
  background: var(--wf-color-bg);
@@ -2887,7 +3074,7 @@ code {
2887
3074
 
2888
3075
  .wf-time-picker {
2889
3076
  position: fixed;
2890
- z-index: var(--wf-pop-z);
3077
+ z-index: var(--wf-z-popover);
2891
3078
  background: var(--wf-color-bg);
2892
3079
  border: var(--wf-border-width) solid var(--wf-color-border);
2893
3080
  border-radius: var(--wf-radius-md);
@@ -3045,7 +3232,7 @@ code {
3045
3232
 
3046
3233
  .wf-chart-tooltip {
3047
3234
  position: fixed;
3048
- z-index: var(--wf-cover-z, 1000);
3235
+ z-index: var(--wf-z-tooltip);
3049
3236
  background: var(--wf-color-text);
3050
3237
  color: var(--wf-color-bg);
3051
3238
  padding: var(--wf-space-xs) var(--wf-space-sm);
@@ -3145,7 +3332,7 @@ code {
3145
3332
  /* ── 编辑区 ────────────────────────────────────────── */
3146
3333
 
3147
3334
  .wf-editor-content {
3148
- padding: var(--wf-space-sm) var(--wf-space);
3335
+ padding: var(--wf-control-pad-y) var(--wf-space);
3149
3336
  min-height: 200px;
3150
3337
  outline: none;
3151
3338
  font-family: var(--wf-font-sans);
@@ -3231,7 +3418,7 @@ code {
3231
3418
  display: block;
3232
3419
  width: 100%;
3233
3420
  min-height: 200px;
3234
- padding: var(--wf-space-sm) var(--wf-space);
3421
+ padding: var(--wf-control-pad-y) var(--wf-space);
3235
3422
  border: none;
3236
3423
  outline: none;
3237
3424
  font-family: var(--wf-font-mono, 'SF Mono', 'Fira Code', 'Consolas', monospace);
@@ -3255,7 +3442,7 @@ code {
3255
3442
  box-sizing: border-box;
3256
3443
  font-family: var(--wf-font-sans);
3257
3444
  font-size: var(--wf-font-size-sm);
3258
- padding: var(--wf-space-sm) var(--wf-space);
3445
+ padding: var(--wf-control-pad-y) var(--wf-space);
3259
3446
  border: var(--wf-border-width) solid var(--wf-color-border);
3260
3447
  border-radius: var(--wf-radius);
3261
3448
  outline: none;
@@ -3331,7 +3518,7 @@ code {
3331
3518
 
3332
3519
  .wf-editor-table-picker {
3333
3520
 
3334
- z-index: 100;
3521
+ z-index: var(--wf-z-popover);
3335
3522
  background: var(--wf-color-bg);
3336
3523
  border: var(--wf-border-width) solid var(--wf-color-border);
3337
3524
  border-radius: var(--wf-radius);
@@ -3376,3 +3563,57 @@ code {
3376
3563
  border-top: 1px solid var(--wf-color-border);
3377
3564
  }
3378
3565
 
3566
+ /* weifuwu/components — ThemeSwitch 主题切换器 */
3567
+
3568
+ .wf-theme-switch {
3569
+ display: inline-flex;
3570
+ align-items: center;
3571
+ gap: 2px;
3572
+ padding: 2px;
3573
+ border: var(--wf-border-width) solid var(--wf-color-border);
3574
+ border-radius: var(--wf-radius-md);
3575
+ background: var(--wf-color-bg-secondary);
3576
+ font-family: var(--wf-font-sans);
3577
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.04);
3578
+ }
3579
+
3580
+ .wf-theme-seg {
3581
+ display: inline-flex;
3582
+ align-items: center;
3583
+ justify-content: center;
3584
+ padding: var(--wf-space-xs) var(--wf-space-sm);
3585
+ min-height: 26px;
3586
+ border: none;
3587
+ background: none;
3588
+ border-radius: var(--wf-radius-sm);
3589
+ font-size: var(--wf-font-size-sm);
3590
+ line-height: 1;
3591
+ color: var(--wf-color-text-secondary);
3592
+ cursor: pointer;
3593
+ white-space: nowrap;
3594
+ user-select: none;
3595
+ transition: all var(--wf-transition);
3596
+ }
3597
+
3598
+ .wf-theme-seg:hover {
3599
+ color: var(--wf-color-text);
3600
+ background: var(--wf-color-bg-tertiary);
3601
+ }
3602
+
3603
+ .wf-theme-seg--active {
3604
+ background: var(--wf-color-bg);
3605
+ color: var(--wf-color-text);
3606
+ font-weight: var(--wf-font-weight-medium);
3607
+ box-shadow: var(--wf-shadow-sm);
3608
+ }
3609
+
3610
+ .wf-theme-seg:focus-visible {
3611
+ outline: none;
3612
+ box-shadow: var(--wf-focus-ring);
3613
+ }
3614
+
3615
+ .wf-theme-seg:disabled {
3616
+ opacity: var(--wf-opacity-disabled);
3617
+ cursor: not-allowed;
3618
+ }
3619
+
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
2
- export { HttpError } from './types.ts';
2
+ export { HttpError, createMiddleware } from './types.ts';
3
3
  export type { User } from './types.ts';
4
4
  export { serve, DEFAULT_MAX_BODY } from './core/serve.ts';
5
5
  export type { ServeOptions, Server } from './core/serve.ts';
package/dist/index.js CHANGED
@@ -7,6 +7,18 @@ var HttpError = class extends Error {
7
7
  this.status = status;
8
8
  }
9
9
  };
10
+ function createMiddleware(config) {
11
+ const mw = async (req, ctx, next) => {
12
+ const injected = await config.setup(ctx);
13
+ Object.assign(ctx, injected);
14
+ return next(req, ctx);
15
+ };
16
+ mw.__meta = {
17
+ injects: config.injects,
18
+ depends: config.depends ?? []
19
+ };
20
+ return mw;
21
+ }
10
22
 
11
23
  // src/core/serve.ts
12
24
  import http from "node:http";
@@ -714,7 +726,9 @@ var Router = class _Router {
714
726
  if (!next) return this._wildcardMatch(method, segments);
715
727
  node = next;
716
728
  }
717
- return this._resolveMatch(node, method, params, segments.length);
729
+ const resolved = this._resolveMatch(node, method, params, segments.length);
730
+ if (resolved) return resolved;
731
+ return this._wildcardMatch(method, segments);
718
732
  }
719
733
  _wildcardMatch(method, segments) {
720
734
  if (!this._hasWildcard) return null;
@@ -1144,7 +1158,7 @@ function ui() {
1144
1158
  const cached = jsCache.get(absPath);
1145
1159
  if (cached && await jsCacheFresh(cached.inputs)) {
1146
1160
  return new Response(cached.code, {
1147
- headers: { "Content-Type": "application/javascript" }
1161
+ headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
1148
1162
  });
1149
1163
  }
1150
1164
  const result = await build({
@@ -1168,7 +1182,7 @@ function ui() {
1168
1182
  }
1169
1183
  jsCache.set(absPath, { code, inputs });
1170
1184
  return new Response(code, {
1171
- headers: { "Content-Type": "application/javascript" }
1185
+ headers: { "Content-Type": "application/javascript", "Cache-Control": "no-cache" }
1172
1186
  });
1173
1187
  },
1174
1188
  async css(entryPath) {
@@ -1177,7 +1191,7 @@ function ui() {
1177
1191
  const cached = cssCache.get(absPath);
1178
1192
  if (cached && cached.mtime === st.mtimeMs) {
1179
1193
  return new Response(cached.code, {
1180
- headers: { "Content-Type": "text/css; charset=utf-8" }
1194
+ headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
1181
1195
  });
1182
1196
  }
1183
1197
  let code = await readFile(absPath, "utf-8");
@@ -1195,7 +1209,7 @@ function ui() {
1195
1209
  }
1196
1210
  cssCache.set(absPath, { code, mtime: st.mtimeMs });
1197
1211
  return new Response(code, {
1198
- headers: { "Content-Type": "text/css; charset=utf-8" }
1212
+ headers: { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-cache" }
1199
1213
  });
1200
1214
  }
1201
1215
  };
@@ -2933,6 +2947,7 @@ export {
2933
2947
  badRequest,
2934
2948
  conflict,
2935
2949
  cors,
2950
+ createMiddleware,
2936
2951
  created,
2937
2952
  forbidden,
2938
2953
  noContent,
@@ -35,8 +35,20 @@
35
35
  --wf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
36
36
 
37
37
  /* ── 层级 ── */
38
- --wf-pop-z: 50;
39
- --wf-cover-z: 100;
38
+ /* 浮层全部 createPortal 到 body,全局参与堆叠。
39
+ 顺序:sticky < overlay < modal/drawer < 弹出面板族 < toast < tooltip。
40
+ modal 与 drawer 互斥出现;面板族同级(dropdown/select/datepicker/popover)用 DOM 顺序裁决。 */
41
+ --wf-z-sticky: 40;
42
+ --wf-z-overlay: 80;
43
+ --wf-z-modal: 100;
44
+ --wf-z-popover: 120; /* dropdown/select/datepicker 面板与 popover 同层 */
45
+ --wf-z-toast: 140;
46
+ --wf-z-tooltip: 160;
47
+ --wf-z-dropdown: var(--wf-z-popover); /* 语义别名:下拉菜单属弹出面板族 */
48
+
49
+ /* ── 兼容别名(旧 token 名,语义近似映射) ── */
50
+ --wf-pop-z: var(--wf-z-popover);
51
+ --wf-cover-z: var(--wf-z-modal);
40
52
 
41
53
  /* ── 色彩 — 品牌 ── */
42
54
  --wf-color-primary: #4f6ef7;
@@ -71,15 +83,15 @@
71
83
  --wf-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
72
84
  --wf-font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
73
85
 
74
- --wf-font-size-xs: 11px;
75
- --wf-font-size-sm: 12px;
76
- --wf-font-size-base: 13px;
77
- --wf-font-size-lg: 14px;
78
- --wf-font-size-xl: 15px;
79
- --wf-font-size-2xl: 20px;
86
+ --wf-font-size-xs: 12px;
87
+ --wf-font-size-sm: 13px;
88
+ --wf-font-size-base: 14px;
89
+ --wf-font-size-lg: 15px;
90
+ --wf-font-size-xl: 16px;
91
+ --wf-font-size-2xl: 21px;
80
92
  --wf-font-size-3xl: 24px;
81
- --wf-font-size-4xl: 28px;
82
- --wf-font-size-5xl: 32px;
93
+ --wf-font-size-4xl: 30px;
94
+ --wf-font-size-5xl: 36px;
83
95
 
84
96
  --wf-font-weight-normal: 400;
85
97
  --wf-font-weight-medium: 500;
@@ -110,6 +122,10 @@
110
122
  --wf-accent-color: var(--wf-color-primary);
111
123
  --wf-caret-color: var(--wf-color-primary);
112
124
 
125
+ /* 控件尺寸:垂直内边距 + 目标高度(移动端在 _base.css 提升到 44px) */
126
+ --wf-control-pad-y: 10px;
127
+ --wf-control-height: 36px;
128
+
113
129
  /* ── 断点 ── */
114
130
  --wf-bp-sm: 640px;
115
131
  --wf-bp-md: 768px;
@@ -126,8 +142,21 @@
126
142
  --wf-letter-spacing-wider: 1px;
127
143
  }
128
144
 
129
- /* weifuwu/theme — 暗色模式 */
130
- [data-theme="dark"] {
145
+ /* weifuwu/theme — 暗色模式
146
+ *
147
+ * 两种激活方式(优先级:显式 data-theme > 系统偏好):
148
+ * 1. 手动:<html data-theme="dark">(或 data-theme="light" 强制亮色)
149
+ * 2. 自动:系统偏好 prefers-color-scheme: dark,无显式覆盖时生效
150
+ *
151
+ * ⚠️ 两段暗色覆盖值必须保持一致(静态 CSS 无法共享选择器列表,改动需同步)。
152
+ * 每段末尾都设置 color-scheme: dark,让原生控件/滚动条跟随。
153
+ */
154
+
155
+ /* ── 激活方式一:手动 data-theme="dark" ── */
156
+ /* color-scheme 用 :root[data-theme] 提升优先级,压过 _base.css 的 :root 定义(导入顺序在后者) */
157
+ :root[data-theme="dark"] {
158
+ color-scheme: dark;
159
+
131
160
  --wf-color-primary: #6b8aff;
132
161
  --wf-color-primary-hover: #8ba3ff;
133
162
  --wf-color-primary-bg: #1e293b;
@@ -166,6 +195,50 @@
166
195
  --wf-surface-bg: var(--wf-color-bg);
167
196
  }
168
197
 
198
+ /* ── 激活方式二:系统暗色偏好(data-theme="light" 可强制亮色) ── */
199
+ @media (prefers-color-scheme: dark) {
200
+ :root:not([data-theme="light"]) {
201
+ color-scheme: dark;
202
+
203
+ --wf-color-primary: #6b8aff;
204
+ --wf-color-primary-hover: #8ba3ff;
205
+ --wf-color-primary-bg: #1e293b;
206
+
207
+ --wf-color-success: #4ade80;
208
+ --wf-color-success-bg: #14532d;
209
+ --wf-color-warning: #fbbf24;
210
+ --wf-color-warning-bg: #451a03;
211
+ --wf-color-error: #f87171;
212
+ --wf-color-error-bg: #451a1a;
213
+
214
+ --wf-color-info: #60a5fa;
215
+ --wf-color-info-bg: #172554;
216
+
217
+ --wf-color-text: #f1f5f9;
218
+ --wf-color-text-secondary: #94a3b8;
219
+ --wf-color-text-tertiary: #64748b;
220
+ --wf-color-text-disabled: #475569;
221
+
222
+ --wf-color-bg: #0f172a;
223
+ --wf-color-bg-secondary: #1e293b;
224
+ --wf-color-bg-tertiary: #334155;
225
+
226
+ --wf-color-border: #334155;
227
+ --wf-color-border-light: #1e293b;
228
+ --wf-color-border-dark: #475569;
229
+
230
+ --wf-surface-shadow: 0 1px 3px rgba(0,0,0,0.3);
231
+ --wf-shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
232
+ --wf-shadow: 0 1px 3px rgba(0,0,0,0.3);
233
+ --wf-shadow-md: 0 4px 12px rgba(0,0,0,0.4);
234
+ --wf-shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.4);
235
+
236
+ --wf-focus-ring: 0 0 0 2px var(--wf-color-primary-bg);
237
+
238
+ --wf-surface-bg: var(--wf-color-bg);
239
+ }
240
+ }
241
+
169
242
  /* weifuwu/theme — 基础元素默认样式 */
170
243
  *,
171
244
  *::before,
@@ -173,6 +246,11 @@
173
246
  box-sizing: border-box;
174
247
  }
175
248
 
249
+ :root {
250
+ /* 原生控件/滚动条跟随主题;暗色激活时在 _dark.css 中切为 dark */
251
+ color-scheme: light;
252
+ }
253
+
176
254
  body {
177
255
  margin: 0;
178
256
  background: var(--wf-color-bg);
@@ -210,7 +288,7 @@ select {
210
288
  color: var(--wf-color-text);
211
289
  border: var(--wf-border-width) solid var(--wf-color-border);
212
290
  border-radius: var(--wf-radius);
213
- padding: var(--wf-space-sm) var(--wf-space);
291
+ padding: var(--wf-control-pad-y) var(--wf-space);
214
292
  }
215
293
 
216
294
  input:focus,
@@ -235,7 +313,8 @@ button {
235
313
  color: var(--wf-color-text);
236
314
  border: var(--wf-border-width) solid var(--wf-color-border);
237
315
  border-radius: var(--wf-radius);
238
- padding: var(--wf-space-sm) var(--wf-space-md);
316
+ padding: var(--wf-control-pad-y) var(--wf-space-md);
317
+ min-height: var(--wf-control-height);
239
318
  cursor: pointer;
240
319
  }
241
320
  button:hover {
@@ -243,6 +322,31 @@ button:hover {
243
322
  color: var(--wf-color-primary);
244
323
  }
245
324
 
325
+ /* 键盘焦点:button 与 [tabindex] 统一 focus ring(仅键盘触发,鼠标点击不闪) */
326
+ button:focus-visible,
327
+ [tabindex]:focus-visible {
328
+ outline: none;
329
+ box-shadow: var(--wf-focus-ring);
330
+ }
331
+
332
+ /* 触屏设备(coarse pointer):交互目标提升到 44px(Apple HIG / WCAG 2.5.8) */
333
+ @media (pointer: coarse) {
334
+ button,
335
+ [role="button"],
336
+ input,
337
+ textarea,
338
+ select,
339
+ .wf-card--clickable,
340
+ .wf-table-th--sortable,
341
+ .wf-accordion-summary,
342
+ .wf-tag-close,
343
+ .wf-checkbox,
344
+ .wf-switch,
345
+ .wf-radio {
346
+ min-height: 44px;
347
+ }
348
+ }
349
+
246
350
  h1, h2, h3, h4, h5, h6 {
247
351
  font-weight: var(--wf-font-weight-semibold);
248
352
  line-height: var(--wf-line-height-tight);
@@ -314,6 +418,18 @@ code {
314
418
  border-radius: var(--wf-radius-sm);
315
419
  }
316
420
 
421
+ /* ── 无障碍:尊重系统减弱动效偏好 ── */
422
+ @media (prefers-reduced-motion: reduce) {
423
+ *,
424
+ *::before,
425
+ *::after {
426
+ animation-duration: 0.01ms !important;
427
+ animation-iteration-count: 1 !important;
428
+ transition-duration: 0.01ms !important;
429
+ scroll-behavior: auto !important;
430
+ }
431
+ }
432
+
317
433
  /* stack — 纵向堆叠 */
318
434
  .wf-stack {
319
435
  display: flex;
@@ -558,7 +674,9 @@ code {
558
674
  min-height: 0;
559
675
  }
560
676
 
561
- /* text-align 对齐原语 */
677
+ /* 排版工具类 字号 / 色阶 / 字重 / 截断(零 CSS 用户自排版) */
678
+
679
+ /* ── text-align ── */
562
680
 
563
681
  .wf-text-left {
564
682
  text-align: left;
@@ -571,3 +689,50 @@ code {
571
689
  .wf-text-right {
572
690
  text-align: right;
573
691
  }
692
+
693
+ /* ── 字号 ── */
694
+
695
+ .wf-text-xs { font-size: var(--wf-font-size-xs); }
696
+ .wf-text-sm { font-size: var(--wf-font-size-sm); }
697
+ .wf-text-base { font-size: var(--wf-font-size-base); }
698
+ .wf-text-lg { font-size: var(--wf-font-size-lg); }
699
+ .wf-text-xl { font-size: var(--wf-font-size-xl); }
700
+ .wf-text-2xl { font-size: var(--wf-font-size-2xl); }
701
+ .wf-text-3xl { font-size: var(--wf-font-size-3xl); }
702
+ .wf-text-4xl { font-size: var(--wf-font-size-4xl); }
703
+ .wf-text-5xl { font-size: var(--wf-font-size-5xl); }
704
+
705
+ /* ── 色阶 ── */
706
+
707
+ .wf-text-secondary { color: var(--wf-color-text-secondary); }
708
+ .wf-text-tertiary { color: var(--wf-color-text-tertiary); }
709
+ .wf-text-disabled { color: var(--wf-color-text-disabled); }
710
+ .wf-text-brand { color: var(--wf-color-primary); }
711
+
712
+ /* ── 字重 ── */
713
+
714
+ .wf-text-medium { font-weight: var(--wf-font-weight-medium); }
715
+ .wf-text-semibold { font-weight: var(--wf-font-weight-semibold); }
716
+ .wf-text-bold { font-weight: var(--wf-font-weight-bold); }
717
+
718
+ /* ── 截断 ── */
719
+
720
+ .wf-truncate {
721
+ overflow: hidden;
722
+ text-overflow: ellipsis;
723
+ white-space: nowrap;
724
+ }
725
+
726
+ .wf-line-clamp-2 {
727
+ display: -webkit-box;
728
+ -webkit-line-clamp: 2;
729
+ -webkit-box-orient: vertical;
730
+ overflow: hidden;
731
+ }
732
+
733
+ .wf-line-clamp-3 {
734
+ display: -webkit-box;
735
+ -webkit-line-clamp: 3;
736
+ -webkit-box-orient: vertical;
737
+ overflow: hidden;
738
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.55.2",
4
+ "version": "0.55.3",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {
@@ -38,6 +38,7 @@
38
38
  "release:dry": "node scripts/release.mjs --dry-run"
39
39
  },
40
40
  "dependencies": {
41
+ "esbuild": "^0.28.1",
41
42
  "graphql": "^16",
42
43
  "ws": "^8"
43
44
  },
@@ -62,7 +63,6 @@
62
63
  "@tailwindcss/postcss": "^4.3.3",
63
64
  "@types/node": "^25",
64
65
  "@types/ws": "^8",
65
- "esbuild": "^0.28.1",
66
66
  "ioredis": "^5.11.1",
67
67
  "jsdom": "^29.1.1",
68
68
  "postcss": "^8.5.19",