weifuwu 0.51.1 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,7 +82,7 @@ createApp()
82
82
  <!-- 组件样式(可选,如只用 weifuwu/client 则不需要) -->
83
83
  <link
84
84
  rel="stylesheet"
85
- href="https://unpkg.com/weifuwu@0.51.0/dist/components/style.css"
85
+ href="https://unpkg.com/weifuwu@latest/dist/components/style.css"
86
86
  />
87
87
  </head>
88
88
  <body>
@@ -92,8 +92,8 @@ createApp()
92
92
  <script type="importmap">
93
93
  {
94
94
  "imports": {
95
- "weifuwu/client": "https://unpkg.com/weifuwu@0.51.0/dist/client/index.js",
96
- "weifuwu/components": "https://unpkg.com/weifuwu@0.51.0/dist/components/index.js"
95
+ "weifuwu/client": "https://unpkg.com/weifuwu@latest/dist/client/index.js",
96
+ "weifuwu/components": "https://unpkg.com/weifuwu@latest/dist/components/index.js"
97
97
  }
98
98
  }
99
99
  </script>
@@ -137,12 +137,11 @@ createApp()
137
137
 
138
138
  | 资源 | CDN 地址 | 说明 |
139
139
  |------|---------|------|
140
- | `weifuwu/client` | `https://unpkg.com/weifuwu@0.51.0/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
141
- | `weifuwu/components` | `https://unpkg.com/weifuwu@0.51.0/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
142
- | 组件样式 | `https://unpkg.com/weifuwu@0.51.0/dist/components/style.css` | 组件 CSS + 72 个主题 Token + 35 个布局原语 |
143
- | 独立布局系统 | `https://unpkg.com/weifuwu@0.51.0/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
140
+ | `weifuwu/client` | `https://unpkg.com/weifuwu@latest/dist/client/index.js` | 客户端核心(createApp, h, 路由, 状态管理等) |
141
+ | `weifuwu/components` | `https://unpkg.com/weifuwu@latest/dist/components/index.js` | 41 个 UI 组件(Button, Card, Table, Modal 等) |
142
+ | 组件样式 | `https://unpkg.com/weifuwu@latest/dist/components/style.css` | 组件 CSS + 72 个主题 Token + 35 个布局原语 |
143
+ | 独立布局系统 | `https://unpkg.com/weifuwu@latest/dist/layout/weifuwu-layout.css` | 仅 CSS 布局,不依赖 JS |
144
144
 
145
- > 提示:将 `@0.51.0` 替换为 `@latest` 始终使用最新版,或固定版本避免意外变更。
146
145
 
147
146
  ---
148
147
 
@@ -898,6 +897,8 @@ h('div', { class: 'x' }, child1, child2)
898
897
  | `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | 当前/指定 | **绕过 Proxy 后手动标记** |
899
898
  | `ctx.ui.render()` | 主动调用 | 立即同步 | 当前/指定 | **需要立即拿到最新 DOM** — DOM 测量、动画触发 |
900
899
  | `ctx.ui.render(['id'])` | 主动调用 | 立即同步 | 指定组件 | **跨组件精准刷新** — 全局事件、Portal 远程控制 |
900
+ | `ctx.ui.useMedia()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **响应式媒体查询** — 断点变化时自动 dirty |
901
+ | `ctx.ui.useBreakpoint()` | 注册监听 | 浏览器事件驱动 | 当前组件 | **命名断点** — mobile/tablet/desktop 自动 dirty |
901
902
 
902
903
  `render()` 和 `dirty()` 无参 = 当前组件,传参 = 指定组件列表。三套 API 同一 scope 机制。
903
904
 
@@ -945,6 +946,79 @@ const FormPage: Component = (_init, ctx) => {
945
946
  - 不需要触发渲染的内部缓存(用闭包变量 `let`)
946
947
  - 简单组件只有一两个状态变量(闭包变量 + `render()` 更轻量)
947
948
 
949
+ ### 响应式自适应组件
950
+
951
+ #### `ctx.ui.useMedia(query, callback)` — 响应式媒体查询
952
+
953
+ 注册媒体查询监听,值变化时自动调用 callback(callback 内赋值 `$` 触发 dirty):
954
+
955
+ ```tsx
956
+ const Card = (_init, ctx) => {
957
+ const $ = ctx.ui.$()
958
+ $.isMobile = false
959
+ // 立即回调一次(取当前值),之后变化时自动重新回调
960
+ ctx.ui.useMedia('(max-width: 640px)', (v) => { $.isMobile = v })
961
+
962
+ return (props) => (
963
+ <div class={$.isMobile ? 'wf-stack' : 'wf-row'}>
964
+ {!$.isMobile && <Sidebar />}
965
+ <Content />
966
+ </div>
967
+ )
968
+ }
969
+ ```
970
+
971
+ `callback` 在 mount 时立即执行一次,之后断点变化时再次执行。赋值给 `$` 的属性自动触发渲染。
972
+
973
+ #### `ctx.ui.useBreakpoint(callback)` — 命名断点
974
+
975
+ 预设三个断点名称:`mobile`(<640px)、`tablet`(640-1023px)、`desktop`(≥1024px):
976
+
977
+ ```tsx
978
+ const Layout = (_init, ctx) => {
979
+ const $ = ctx.ui.$()
980
+ ctx.ui.useBreakpoint((vp) => { $.vp = vp })
981
+
982
+ return (props) =>
983
+ <div class={`sidebar-${$.vp}`}>
984
+ {$.vp === 'mobile' ? <BottomNav /> : <SideNav />}
985
+ {$.vp === 'mobile' ? <MobileContent /> : <Content />}
986
+ </div>
987
+ }
988
+ ```
989
+
990
+ 也支持自定义断点:
991
+
992
+ ```tsx
993
+ ctx.ui.useBreakpoint(
994
+ { narrow: '(max-width: 480px)', wide: '(min-width: 1200px)' },
995
+ (vp) => { $.size = vp },
996
+ )
997
+ ```
998
+
999
+ #### CSS 层响应式(不碰 JS)
1000
+
1001
+ 配合 `weifuwu/layout` 的断点变体,纯 CSS 实现布局方向切换:
1002
+
1003
+ ```html
1004
+ <!-- 小屏堆叠,桌面并排 -->
1005
+ <div class="wf-stack wf-stack@md"></div>
1006
+
1007
+ <!-- 小屏隐藏侧栏 -->
1008
+ <aside class="wf-hidden wf-block@md"></aside>
1009
+ ```
1010
+
1011
+ 可用断点变体:
1012
+
1013
+ | 原语 | 变体 | 效果 |
1014
+ |------|------|------|
1015
+ | `wf-stack` | `@sm` `@md` `@lg` | 断点以上改为横向排列 |
1016
+ | `wf-row` | `@sm` `@md` `@lg` | 断点以上保持横向 |
1017
+ | `wf-hidden` | `@sm` `@md` `@lg` | 断点以上隐藏 |
1018
+ | `wf-block` | `@sm` `@md` `@lg` | 断点以上显示 |
1019
+
1020
+ 断点尺寸:`--wf-bp-sm: 640px` / `--wf-bp-md: 768px` / `--wf-bp-lg: 1024px` / `--wf-bp-xl: 1280px`
1021
+
948
1022
  ### `ctx.ui.dirty()` — 异步标记脏
949
1023
 
950
1024
  异步版本,无参 = 当前组件,传参 = 指定组件列表。多次调用合并为一次微任务渲染。`$` 内部就是调 `dirty()`。
@@ -1004,8 +1078,14 @@ ctx.ui.dirty(['stats']) // 批处理合并
1004
1078
 
1005
1079
  **性能说明**:
1006
1080
  - `$.x = val` 和 `dirty()` 都是微任务批量合并
1007
- - `render()` 每次调用都触发一次完整 diff/patch
1008
- - 三个入口同一套 scope 机制,不想要的渲染不触发
1081
+ - `render()` dirty 组件**向下**遍历(scope render),兄弟组件不遍历
1082
+ - **三态 skip 自动优化**:组件重新渲染时,框架自动检查三个维度:
1083
+ - **props**(含 children 元素级比较)——值没变则不渲染
1084
+ - **`$` 状态**——没被 dirty 标记则不渲染
1085
+ - **ctx 版本**——ctx 没变化则不渲染
1086
+ 三个条件全部满足时跳过整个子树(零 `_render` 调用、零 `patchValue` 遍历)
1087
+ - **lastIndex keyed diff**:列表 diff 采用正向 lastIndex 算法(React 同款),顺序不变时零 `insertBefore`。对比传统的逆序循环全量移动,DOM 修改从 O(N) 降到 O(0)。
1088
+ - 示例:DemoButton 点击一次,DOM 修改从 34 次降到 **1 次**(仅变更文本节点的 `textContent`)
1009
1089
 
1010
1090
  ### 实践建议
1011
1091
 
@@ -1038,6 +1118,17 @@ const OrderPage = (_init, ctx) => {
1038
1118
 
1039
1119
  同一个组件内可以按变量混用两种模式:需要渲染的用 `$`,不需要的用 `let`。
1040
1120
 
1121
+ ### VDOM diff 优化机制
1122
+
1123
+ weifuwu 的 VDOM 在每次 render 时自动执行**三态 skip 判定**,减少不必要的组件渲染和 DOM 操作:
1124
+
1125
+ ```
1126
+ canSkip = (props 没变) AND ($ 没脏) AND (ctx 版本一致)
1127
+ ↑ 值级浅比较 ↑ VNode dirty 标记 ↑ 全局版本号
1128
+ ```
1129
+
1130
+ 三个维度各自独立判断,AND 合并。任何一个维度说
1131
+
1041
1132
  ---
1042
1133
 
1043
1134
  ## 条件与列表
@@ -1766,10 +1857,10 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
1766
1857
 
1767
1858
  | 类别 | 原语 | 效果 |
1768
1859
  |------|------|------|
1769
- | **排列** | `wf-stack` | 纵向 flex + gap |
1770
- | | `wf-stack-reverse` | 纵向反向 |
1771
- | | `wf-row` | 横向 flex + wrap + gap |
1772
- | | `wf-row-reverse` | 横向反向 |
1860
+ | **排列** | `wf-stack` `wf-stack@sm/md/lg` | 纵向 flex + gap(断点变体→横向) |
1861
+ | | `wf-stack-reverse` `@sm/md/lg` | 纵向反向 |
1862
+ | | `wf-row` `wf-row@sm/md/lg` | 横向 flex + wrap + gap |
1863
+ | | `wf-row-reverse` `@sm/md/lg` | 横向反向 |
1773
1864
  | | `wf-nowrap` | flex-wrap: nowrap |
1774
1865
  | | `wf-cluster` | 换行居中簇 |
1775
1866
  | **分布** | `wf-split` | justify-content: space-between |
@@ -1794,8 +1885,8 @@ app.get('/layout.css', (req, ctx) => ctx.ui.css('weifuwu/layout'))
1794
1885
  | | `wf-container` | max-width + margin: auto |
1795
1886
  | | `wf-scroll` | overflow: auto |
1796
1887
  | | `wf-clip` | overflow: hidden |
1797
- | **显隐** | `wf-hidden` | display: none |
1798
- | | `wf-block` | display: block |
1888
+ | **显隐** | `wf-hidden` `wf-hidden@sm/md/lg` | display: none |
1889
+ | | `wf-block` `wf-block@sm/md/lg` | display: block |
1799
1890
  | | `wf-inline` | display: inline |
1800
1891
  | | `wf-inline-block` | display: inline-block |
1801
1892
  | | `wf-contents` | display: contents |
@@ -32,7 +32,8 @@ function createPortal(children, portalKey) {
32
32
  return {
33
33
  type: Portal,
34
34
  props: { children, portalKey },
35
- key: portalKey ?? void 0
35
+ key: portalKey ?? void 0,
36
+ _placement: "remote"
36
37
  };
37
38
  }
38
39
 
@@ -45,16 +46,20 @@ function render(input, ctx) {
45
46
  return renderValue(input, ctx);
46
47
  }
47
48
  function renderValue(v, ctx) {
48
- if (v == null || typeof v === "boolean") return document.createTextNode("");
49
+ if (v == null || typeof v === "boolean") return null;
49
50
  if (typeof v === "string" || typeof v === "number") return document.createTextNode(String(v));
50
51
  if (Array.isArray(v)) return renderArray(v, ctx);
51
52
  const vnode = v;
52
53
  if (vnode.type === Portal) {
53
- return renderPortal(vnode, ctx);
54
+ renderPortal(vnode, ctx);
55
+ return null;
54
56
  }
55
57
  if (vnode.type === Fragment) {
56
58
  const frag = document.createDocumentFragment();
57
- forEach(vnode.props?.children, (child) => frag.appendChild(renderValue(child, ctx)));
59
+ forEach(vnode.props?.children, (child) => {
60
+ const node = renderValue(child, ctx);
61
+ if (node != null) frag.appendChild(node);
62
+ });
58
63
  return frag;
59
64
  }
60
65
  if (typeof vnode.type === "function") {
@@ -79,6 +84,7 @@ function renderValue(v, ctx) {
79
84
  const flatChildren = flattenChildren(vnode.props?.children);
80
85
  for (const child of flatChildren) {
81
86
  const childNode = renderValue(child, ctx);
87
+ if (childNode == null) continue;
82
88
  el.appendChild(childNode);
83
89
  if (child && typeof child === "object" && typeof child.type === "function") {
84
90
  const childVNode = child;
@@ -107,6 +113,7 @@ function renderComponent(Comp, props, vnode, ctx) {
107
113
  childCtx.ui = Object.create(ctx.ui);
108
114
  childCtx.ui._selfId = vnode._id;
109
115
  childCtx.ui._selfVNode = vnode;
116
+ vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
110
117
  let childVNode;
111
118
  try {
112
119
  childVNode = Comp(props, childCtx);
@@ -129,7 +136,7 @@ function renderComponent(Comp, props, vnode, ctx) {
129
136
  }
130
137
  if (childVNode == null) {
131
138
  vnode._child = null;
132
- return document.createTextNode("");
139
+ return null;
133
140
  }
134
141
  vnode._child = childVNode;
135
142
  const domNode = renderValue(childVNode, childCtx);
@@ -141,7 +148,10 @@ function renderComponent(Comp, props, vnode, ctx) {
141
148
  }
142
149
  function renderArray(arr, ctx) {
143
150
  const frag = document.createDocumentFragment();
144
- for (const item of arr) frag.appendChild(renderValue(item, ctx));
151
+ for (const item of arr) {
152
+ const node = renderValue(item, ctx);
153
+ if (node != null) frag.appendChild(node);
154
+ }
145
155
  return frag;
146
156
  }
147
157
  function ensurePortalContainer() {
@@ -159,25 +169,26 @@ function renderPortal(vnode, ctx) {
159
169
  const sub = document.createElement("div");
160
170
  sub.style.pointerEvents = "auto";
161
171
  container.appendChild(sub);
162
- vnode._portalEl = sub;
172
+ vnode._remoteEl = sub;
163
173
  const children = normalize(vnode.props?.children);
164
174
  vnode._child = children;
165
175
  for (const child of children) {
166
- sub.appendChild(renderValue(child, ctx));
176
+ const node = renderValue(child, ctx);
177
+ if (node != null) sub.appendChild(node);
167
178
  }
168
- const placeholder = document.createTextNode("");
169
- vnode.el = placeholder;
170
- return placeholder;
171
179
  }
172
- function patchPortal(_parent, oldNode, oldV, newV, ctx) {
173
- const sub = oldV._portalEl;
174
- newV._portalEl = sub;
175
- if (!sub) return renderPortal(newV, ctx);
180
+ function patchPortal(oldV, newV, ctx) {
181
+ const sub = oldV?._remoteEl;
182
+ newV._remoteEl = sub;
183
+ if (!sub) {
184
+ renderPortal(newV, ctx);
185
+ return;
186
+ }
176
187
  const newChildren = normalize(newV.props?.children);
177
188
  const oldChildren = oldV._child || [];
178
189
  newV._child = newChildren;
179
- patchSimpleChildren(sub, oldChildren, newChildren, ctx);
180
- return oldNode ?? document.createTextNode("");
190
+ ensureKeys(oldChildren, newChildren);
191
+ patchKeyedChildren(sub, oldChildren, newChildren, ctx);
181
192
  }
182
193
  function forEach(children, fn) {
183
194
  if (children == null) return;
@@ -225,6 +236,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
225
236
  if (oldInput == null) {
226
237
  if (newInput == null) return null;
227
238
  const node = renderValue(newInput, ctx);
239
+ if (node == null) return null;
228
240
  if (oldNode && oldNode.parentNode) {
229
241
  oldNode.parentNode.insertBefore(node, oldNode);
230
242
  } else {
@@ -236,6 +248,8 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
236
248
  if (oldNode) {
237
249
  callRefCleanup(oldInput);
238
250
  oldNode.remove();
251
+ } else {
252
+ callRefCleanup(oldInput);
239
253
  }
240
254
  return null;
241
255
  }
@@ -244,6 +258,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
244
258
  if (oldType !== newType) {
245
259
  callRefCleanup(oldInput);
246
260
  const node = renderValue(newInput, ctx);
261
+ if (node == null) return null;
247
262
  if (oldNode?.parentNode) {
248
263
  oldNode.parentNode.replaceChild(node, oldNode);
249
264
  }
@@ -270,6 +285,13 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
270
285
  childCtx.ui = Object.create(ctx.ui);
271
286
  childCtx.ui._selfId = newV._id;
272
287
  childCtx.ui._selfVNode = newV;
288
+ newV._ctxVersion = oldV._ctxVersion ?? childCtx.ui._ctxVersion ?? 0;
289
+ if (oldV._render && componentPropsEqual(oldV.props, newV.props) && !childCtx.ui._dirtySet?.has(oldV._id) && newV._ctxVersion === childCtx.ui._ctxVersion) {
290
+ newV._child = oldV._child;
291
+ return oldNode;
292
+ }
293
+ ;
294
+ childCtx.ui._dirtySet?.delete(oldV._id);
273
295
  let childNew;
274
296
  try {
275
297
  if (typeof newV._render === "function") {
@@ -293,7 +315,9 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
293
315
  }
294
316
  const _prevChild = oldV._child;
295
317
  newV._child = childNew;
296
- return patchValue(parent, oldNode, _prevChild, childNew, childCtx);
318
+ const returnedNode = patchValue(parent, oldNode, _prevChild, childNew, childCtx);
319
+ if (!returnedNode) newV._refNode = null;
320
+ return returnedNode;
297
321
  }
298
322
  if (newV.type === Fragment) {
299
323
  patchChildren(parent, oldV, newV, ctx);
@@ -312,17 +336,20 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
312
336
  } else if (oldNode) {
313
337
  callRefCleanup(oldInput);
314
338
  const node = renderValue(newInput, ctx);
339
+ if (node == null) return null;
315
340
  oldNode.parentNode?.replaceChild(node, oldNode);
316
341
  return node;
317
342
  }
318
343
  return oldNode;
319
344
  }
320
345
  if (newV.type === Portal) {
321
- return patchPortal(parent, oldNode, oldV, newV, ctx);
346
+ patchPortal(oldV, newV, ctx);
347
+ return null;
322
348
  }
323
349
  if (Array.isArray(newInput)) {
324
350
  const oldArr = Array.isArray(oldInput) ? oldInput : [];
325
- patchSimpleChildren(parent, oldArr, newInput, ctx);
351
+ ensureKeys(oldArr, newInput);
352
+ patchKeyedChildren(parent, oldArr, newInput, ctx);
326
353
  return oldNode;
327
354
  }
328
355
  return oldNode;
@@ -396,15 +423,24 @@ function getKey(input) {
396
423
  if (input == null || typeof input !== "object") return void 0;
397
424
  return input.key;
398
425
  }
426
+ function ensureKeys(oldChildren, newChildren) {
427
+ const hasKey = newChildren.some((c) => c && typeof c === "object" && c.key !== void 0);
428
+ if (!hasKey) {
429
+ for (let i = 0; i < newChildren.length; i++) {
430
+ const c = newChildren[i];
431
+ if (c && typeof c === "object") c.key = i;
432
+ }
433
+ for (let i = 0; i < oldChildren.length; i++) {
434
+ const c = oldChildren[i];
435
+ if (c && typeof c === "object") c.key = i;
436
+ }
437
+ }
438
+ }
399
439
  function patchChildren(parent, oldVNode, newVNode, ctx) {
400
440
  const oldChildren = normalize(oldVNode.props?.children);
401
441
  const newChildren = normalize(newVNode.props?.children);
402
- const hasKey = newChildren.some((c) => getKey(c) !== void 0);
403
- if (hasKey) {
404
- patchKeyedChildren(parent, oldChildren, newChildren, ctx);
405
- } else {
406
- patchSimpleChildren(parent, oldChildren, newChildren, ctx);
407
- }
442
+ ensureKeys(oldChildren, newChildren);
443
+ patchKeyedChildren(parent, oldChildren, newChildren, ctx);
408
444
  }
409
445
  function normalize(children) {
410
446
  if (children == null) return [];
@@ -419,38 +455,56 @@ function normalize(children) {
419
455
  }
420
456
  return result;
421
457
  }
422
- function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
423
- for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
424
- const oldChild = oldChildren[i];
425
- const node = parent.childNodes[i];
426
- if (node) {
427
- callRefCleanup(oldChild);
428
- node.remove();
458
+ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
459
+ const allUnkeyed = !newChildren.some((c) => c && typeof c === "object" && c.key !== void 0);
460
+ if (allUnkeyed) {
461
+ const len = Math.max(oldChildren.length, newChildren.length);
462
+ for (let i = 0; i < len; i++) {
463
+ const oldC = i < oldChildren.length ? oldChildren[i] : null;
464
+ const newC = i < newChildren.length ? newChildren[i] : null;
465
+ if (newC == null) {
466
+ if (oldC != null) {
467
+ callRefCleanup(oldC);
468
+ const node = parent.childNodes[i];
469
+ if (node) node.remove();
470
+ }
471
+ } else if (oldC == null) {
472
+ const node = renderValue(newC, ctx);
473
+ if (node != null) parent.appendChild(node);
474
+ } else {
475
+ const oldNode = parent.childNodes[i] || null;
476
+ patchValue(parent, oldNode, oldC, newC, ctx);
477
+ }
429
478
  }
479
+ return;
430
480
  }
431
- const max = Math.max(oldChildren.length, newChildren.length);
432
- const oldNodes = [];
433
- for (let i = 0; i < max; i++) {
434
- oldNodes.push(parent.childNodes[i] || null);
435
- }
436
- for (let i = 0; i < max; i++) {
437
- const oldChild = oldChildren[i];
438
- const newChild = newChildren[i];
439
- const existingNode = oldNodes[i];
440
- if (oldChild === void 0 && newChild !== void 0) {
441
- const node = renderValue(newChild, ctx);
442
- parent.insertBefore(node, oldNodes[i + 1] ?? null);
443
- } else if (oldChild !== void 0 && newChild !== void 0) {
444
- patchValue(parent, existingNode, oldChild, newChild, ctx);
481
+ let rmIdx = 0;
482
+ for (let i = 0; i < oldChildren.length; i++) {
483
+ const child = oldChildren[i];
484
+ if (child == null || typeof child === "boolean") continue;
485
+ const key = getKey(child);
486
+ if (key === void 0) {
487
+ const node = parent.childNodes[rmIdx];
488
+ if (node) node.remove();
489
+ } else {
490
+ const isRemote = child && typeof child === "object" && child._placement === "remote";
491
+ if (!isRemote) rmIdx++;
445
492
  }
446
493
  }
447
- }
448
- function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
449
494
  const oldKeyMap = /* @__PURE__ */ new Map();
495
+ let domIdx = 0;
450
496
  for (let i = 0; i < oldChildren.length; i++) {
451
497
  const key = getKey(oldChildren[i]);
452
498
  if (key !== void 0) {
453
- oldKeyMap.set(key, { vnode: oldChildren[i], node: parent.childNodes[i] || null });
499
+ const child = oldChildren[i];
500
+ const isRemote = child && typeof child === "object" && child._placement === "remote";
501
+ oldKeyMap.set(key, {
502
+ vnode: child,
503
+ node: isRemote ? null : parent.childNodes[domIdx] || null,
504
+ remote: !!isRemote,
505
+ index: domIdx
506
+ });
507
+ if (!isRemote) domIdx++;
454
508
  }
455
509
  }
456
510
  const newKeys = newChildren.map((c) => getKey(c));
@@ -458,35 +512,69 @@ function patchKeyedChildren(parent, oldChildren, newChildren, ctx) {
458
512
  if (!newKeys.includes(key)) {
459
513
  const entry = oldKeyMap.get(key);
460
514
  callRefCleanup(entry.vnode);
461
- entry.node?.remove();
515
+ if (entry.node) entry.node?.remove();
462
516
  oldKeyMap.delete(key);
463
517
  }
464
518
  }
465
- for (let i = oldChildren.length - 1; i >= 0; i--) {
466
- const key = getKey(oldChildren[i]);
467
- if (key === void 0) {
468
- const node = parent.childNodes[i];
469
- if (node) {
470
- callRefCleanup(oldChildren[i]);
471
- node.remove();
472
- }
473
- }
474
- }
475
- let insertBefore = parent.firstChild;
476
- for (let i = newChildren.length - 1; i >= 0; i--) {
519
+ let lastIndex = -1;
520
+ let nextRef = parent.firstChild;
521
+ for (let i = 0; i < newChildren.length; i++) {
477
522
  const key = newKeys[i];
478
523
  const newChild = newChildren[i];
479
524
  const oldEntry = key !== void 0 ? oldKeyMap.get(key) : void 0;
480
- if (oldEntry && oldEntry.node) {
481
- parent.insertBefore(oldEntry.node, insertBefore);
482
- insertBefore = oldEntry.node;
483
- patchValue(parent, oldEntry.node, oldEntry.vnode, newChild, ctx);
525
+ const isRemote = newChild && typeof newChild === "object" && newChild._placement === "remote";
526
+ if (oldEntry) {
527
+ if (oldEntry.node) {
528
+ if (oldEntry.index < lastIndex) {
529
+ parent.insertBefore(oldEntry.node, nextRef);
530
+ }
531
+ lastIndex = Math.max(lastIndex, oldEntry.index);
532
+ patchValue(parent, oldEntry.node, oldEntry.vnode, newChild, ctx);
533
+ nextRef = (oldEntry.node.parentNode === parent ? oldEntry.node : parent.firstChild)?.nextSibling ?? null;
534
+ } else if (oldEntry.remote) {
535
+ patchPortal(oldEntry.vnode, newChild, ctx);
536
+ } else {
537
+ const newNode = patchValue(parent, null, oldEntry.vnode, newChild, ctx);
538
+ if (newNode != null) {
539
+ parent.insertBefore(newNode, nextRef);
540
+ nextRef = newNode.nextSibling;
541
+ }
542
+ }
543
+ } else if (isRemote) {
544
+ renderPortal(newChild, ctx);
484
545
  } else {
485
546
  const node = renderValue(newChild, ctx);
486
- parent.insertBefore(node, insertBefore);
487
- insertBefore = node;
547
+ if (node != null) {
548
+ parent.insertBefore(node, nextRef);
549
+ nextRef = node.nextSibling;
550
+ }
551
+ }
552
+ }
553
+ }
554
+ function childrenEqual(a, b) {
555
+ if (a === b) return true;
556
+ if (Array.isArray(a) && Array.isArray(b)) {
557
+ if (a.length !== b.length) return false;
558
+ for (let i = 0; i < a.length; i++) {
559
+ if (a[i] !== b[i]) return false;
560
+ }
561
+ return true;
562
+ }
563
+ return a === b;
564
+ }
565
+ function componentPropsEqual(a, b) {
566
+ if (a === b) return true;
567
+ if (!a || !b) return false;
568
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
569
+ for (const key of keys) {
570
+ if (key === "key") continue;
571
+ if (key === "children") {
572
+ if (!childrenEqual(a[key], b[key])) return false;
573
+ } else if (a[key] !== b[key]) {
574
+ return false;
488
575
  }
489
576
  }
577
+ return true;
490
578
  }
491
579
  function cleanupPortalChildren(vnode) {
492
580
  const child = vnode._child;
@@ -519,10 +607,10 @@ function callRefCleanup(input) {
519
607
  }
520
608
  }
521
609
  if (typeof vnode.props?.ref === "function") vnode.props.ref(null);
522
- if (vnode._portalEl) {
610
+ if (vnode._remoteEl) {
523
611
  cleanupPortalChildren(vnode);
524
- vnode._portalEl.remove();
525
- vnode._portalEl = void 0;
612
+ vnode._remoteEl.remove();
613
+ vnode._remoteEl = void 0;
526
614
  }
527
615
  }
528
616
 
@@ -536,15 +624,27 @@ function createApp() {
536
624
  let _rendering = false;
537
625
  let _dirtyBatch = /* @__PURE__ */ new Set();
538
626
  let _dirtyScheduled = false;
627
+ const _mediaRegistry = /* @__PURE__ */ new Map();
539
628
  function renderByIds(ids) {
540
629
  if (_rendering) return;
541
630
  _rendering = true;
542
631
  for (const id of ids) {
543
632
  const vnode = idRegistry.get(id);
544
633
  if (!vnode || !vnode._render) continue;
634
+ ctx.ui._dirtySet?.delete(id);
545
635
  const oldChild = vnode._child;
546
636
  const newChild = vnode._render(vnode.props);
547
637
  vnode._child = newChild;
638
+ if (oldChild && oldChild._placement === "remote" || newChild && newChild._placement === "remote") {
639
+ if (oldChild && newChild) {
640
+ patchPortal(oldChild, newChild, ctx);
641
+ } else if (newChild) {
642
+ renderPortal(newChild, ctx);
643
+ } else if (oldChild) {
644
+ callRefCleanup(oldChild);
645
+ }
646
+ continue;
647
+ }
548
648
  if (!vnode._parentNode && vnode._refNode) {
549
649
  ;
550
650
  vnode._parentNode = vnode._refNode.parentNode;
@@ -559,6 +659,9 @@ function createApp() {
559
659
  );
560
660
  if (newNode && newNode !== vnode._refNode) {
561
661
  vnode._refNode = newNode;
662
+ } else if (!newNode) {
663
+ ;
664
+ vnode._refNode = null;
562
665
  }
563
666
  }
564
667
  }
@@ -598,6 +701,12 @@ function createApp() {
598
701
  container.innerHTML = "";
599
702
  ctx.ui = {
600
703
  _selfId: "_wf_root",
704
+ // ── ctx 版本号(供三态 skip 判定) ──
705
+ _ctxVersion: 0,
706
+ _dirtySet: /* @__PURE__ */ new Set(),
707
+ bumpCtxVersion: function() {
708
+ this._ctxVersion++;
709
+ },
601
710
  /** 同步刷新(无参 = 当前组件,传参 = 指定组件列表) */
602
711
  render: function(ids) {
603
712
  if (!ids || ids.length === 0) {
@@ -616,7 +725,10 @@ function createApp() {
616
725
  else return;
617
726
  }
618
727
  for (const id of ids) {
619
- if (id) _dirtyBatch.add(id);
728
+ if (id) {
729
+ _dirtyBatch.add(id);
730
+ ctx.ui._dirtySet.add(id);
731
+ }
620
732
  }
621
733
  if (!_dirtyScheduled) {
622
734
  _dirtyScheduled = true;
@@ -630,10 +742,68 @@ function createApp() {
630
742
  },
631
743
  /** 创建响应式状态容器:$.x = val 自动触发 dirty() */
632
744
  $: function() {
745
+ if (!this._$cache) {
746
+ const selfId = getSelfId(this);
747
+ this._$cache = createReactiveState(() => ctx.ui.dirty([selfId]));
748
+ }
749
+ return this._$cache;
750
+ },
751
+ /**
752
+ * 响应式媒体查询:注册监听,值变化时自动 dirty
753
+ *
754
+ * 用法:
755
+ * const $ = ctx.ui.$()
756
+ * ctx.ui.useMedia('(max-width: 640px)', (v) => { $.isMobile = v })
757
+ *
758
+ * callback 会立即执行一次(取当前值),之后在变化时再次执行
759
+ */
760
+ useMedia: function(query, callback) {
633
761
  const selfId = getSelfId(this);
634
- return createReactiveState(() => {
635
- if (selfId) ctx.ui.dirty([selfId]);
636
- });
762
+ const key = `media:${selfId}:${query}`;
763
+ if (!_mediaRegistry.has(key)) {
764
+ const mql = window.matchMedia(query);
765
+ callback(mql.matches);
766
+ const handler = (e) => callback(e.matches);
767
+ mql.addEventListener("change", handler);
768
+ _mediaRegistry.set(key, { mql, handler });
769
+ }
770
+ },
771
+ /**
772
+ * 响应式断点:注册命名断点监听,值变化时自动 dirty
773
+ *
774
+ * 用法:
775
+ * const $ = ctx.ui.$()
776
+ * ctx.ui.useBreakpoint((vp) => { $.vp = vp })
777
+ * // vp: 'mobile' | 'tablet' | 'desktop'
778
+ *
779
+ * 自定义断点:
780
+ * ctx.ui.useBreakpoint(
781
+ * { narrow: '(max-width: 480px)', wide: '(min-width: 1200px)' },
782
+ * (vp) => { $.size = vp }
783
+ * )
784
+ */
785
+ useBreakpoint: function(bpsOrCallback, callback) {
786
+ const bps = typeof bpsOrCallback === "function" ? { mobile: "(max-width: 639px)", tablet: "(min-width: 640px) and (max-width: 1023px)", desktop: "(min-width: 1024px)" } : bpsOrCallback;
787
+ const cb = typeof bpsOrCallback === "function" ? bpsOrCallback : callback;
788
+ const selfId = getSelfId(this);
789
+ const key = `bp:${selfId}`;
790
+ function evaluate() {
791
+ for (const [name, query] of Object.entries(bps)) {
792
+ if (window.matchMedia(query).matches) return name;
793
+ }
794
+ return Object.keys(bps)[0] ?? "";
795
+ }
796
+ if (!_mediaRegistry.has(key)) {
797
+ cb(evaluate());
798
+ const handlers = [];
799
+ for (const query of Object.values(bps)) {
800
+ const mql = window.matchMedia(query);
801
+ const handler = () => cb(evaluate());
802
+ mql.addEventListener("change", handler);
803
+ handlers.push(() => mql.removeEventListener("change", handler));
804
+ }
805
+ _mediaRegistry.set(key, { mql: null, handler: null });
806
+ }
637
807
  },
638
808
  /** 注册组件实例的自定义 ID(用于跨组件精准刷新) */
639
809
  selfId: function(name) {
@@ -790,12 +960,14 @@ function router(opts) {
790
960
  ctx.route = resolved2;
791
961
  }
792
962
  if (ctx.route?.title) document.title = ctx.route.title;
963
+ ctx.ui?.bumpCtxVersion?.();
793
964
  ctx.ui?.render();
794
965
  };
795
966
  const onPop = () => {
796
967
  const resolved2 = resolve(getPath());
797
968
  ctx.route = resolved2;
798
969
  if (ctx.route?.title) document.title = ctx.route.title;
970
+ ctx.ui?.bumpCtxVersion?.();
799
971
  ctx.ui?.render();
800
972
  };
801
973
  window.addEventListener("popstate", onPop);
@@ -1212,6 +1384,7 @@ function i18n(opts = {}) {
1212
1384
  merged = mergeLocales(pkg2, messages, components);
1213
1385
  state.locale = lang2;
1214
1386
  state.components = merged.components;
1387
+ ctx?.ui?.bumpCtxVersion?.();
1215
1388
  ctx?.ui?.render();
1216
1389
  };
1217
1390
  return ctx;
@@ -13,6 +13,12 @@
13
13
  import type { VNode } from './vnode.ts';
14
14
  import type { WfuiContext } from './types.ts';
15
15
  export declare const idRegistry: Map<string, VNode>;
16
- export declare function render(input: any, ctx: WfuiContext): Node;
16
+ export declare function render(input: any, ctx: WfuiContext): Node | null;
17
+ /** 首次渲染 Portal:创建远程容器、渲染子节点(不返回占位节点) */
18
+ export declare function renderPortal(vnode: VNode, ctx: WfuiContext): void;
19
+ /** 更新 Portal:复用远程容器,patch 子节点(不操作父 DOM) */
20
+ export declare function patchPortal(oldV: VNode | null, newV: VNode, ctx: WfuiContext): void;
17
21
  export declare function patchValue(parent: Node, oldNode: Node | null, oldInput: any, newInput: any, ctx: WfuiContext): Node | null;
22
+ /** 通知 ref 清理 + Portal 子容器清理 */
23
+ export declare function callRefCleanup(input: any): void;
18
24
  export declare function mountVNode(container: Element, vnode: VNode, ctx: WfuiContext): void;
@@ -15,8 +15,10 @@ export interface VNode {
15
15
  el?: Node;
16
16
  /** 子 VNode 缓存(用于 patchValue diff,避免重复执行组件) */
17
17
  _child?: any;
18
- /** Portal 子容器 DOM */
19
- _portalEl?: HTMLDivElement | undefined;
18
+ /** 远程 DOM 容器(Portal remote VNode 的 DOM 所在处) */
19
+ _remoteEl?: HTMLElement | undefined;
20
+ /** VNode 的 DOM 归属:'local' 在父 DOM 树下,'remote' 在别处 */
21
+ _placement?: 'local' | 'remote';
20
22
  /** 两阶段组件的 render 函数(mount 返回的函数) */
21
23
  _render?: (props: any) => VNode | null;
22
24
  /** 组件实例 ID(如 '_wf_0') */
@@ -25,6 +27,8 @@ export interface VNode {
25
27
  _parentNode?: Node;
26
28
  /** 组件输出的第一个 DOM 节点 */
27
29
  _refNode?: Node | null;
30
+ /** 组件 mount/render 时的 ctx 版本号(供三态 skip 判定) */
31
+ _ctxVersion?: number;
28
32
  }
29
33
  export type Component<P = {}> = (initProps: P, ctx: WfuiContext) => ((props: P) => VNode | null) | null;
30
34
  export declare const Fragment: unique symbol;
@@ -21,7 +21,8 @@ function createPortal(children, portalKey) {
21
21
  return {
22
22
  type: Portal,
23
23
  props: { children, portalKey },
24
- key: portalKey ?? void 0
24
+ key: portalKey ?? void 0,
25
+ _placement: "remote"
25
26
  };
26
27
  }
27
28
 
@@ -1383,7 +1384,6 @@ function minuteOptions() {
1383
1384
 
1384
1385
  // src/components/DatePicker/DatePicker.ts
1385
1386
  var DatePicker = (_props, ctx) => {
1386
- const L = ctx?.i18n?.components?.DatePicker ?? {};
1387
1387
  let show = false;
1388
1388
  let selectedValue = "";
1389
1389
  const now = /* @__PURE__ */ new Date();
@@ -1398,6 +1398,7 @@ var DatePicker = (_props, ctx) => {
1398
1398
  let rangeEnd = null;
1399
1399
  let inputEl = null;
1400
1400
  return (props) => {
1401
+ const L = ctx?.i18n?.components?.DatePicker ?? {};
1401
1402
  const { mode = "date", value, onChange, placeholder = L.placeholder ?? "\u9009\u62E9\u65E5\u671F", disabled } = props;
1402
1403
  const isOpen = show;
1403
1404
  const setOpen = (v) => {
@@ -2158,19 +2159,34 @@ function renderToolbar(items, active, isSource, onItem, customRender) {
2158
2159
  // src/components/Editor/tools/table.ts
2159
2160
  var MAX = 6;
2160
2161
  function insertTable(rows, cols) {
2161
- const tds = Array.from(
2162
- { length: cols },
2163
- (_, ci) => `<td${ci === 0 ? ' style="font-weight:var(--wf-font-weight-semibold,600)"' : ""}>&nbsp;</td>`
2164
- ).join("");
2165
- const rowsHtml = Array.from(
2166
- { length: rows },
2167
- (_, ri) => `<tr>${ri === 0 ? tds : tds.replace(/ style="[^"]*"/, "")}</tr>`
2168
- ).join("");
2169
- const html = `<div><table class="wf-editor-table"><tbody>${rowsHtml}</tbody></table></div>`;
2170
- try {
2171
- document.execCommand("insertHTML", false, html);
2172
- } catch {
2162
+ const sel = window.getSelection();
2163
+ if (!sel || !sel.rangeCount) return;
2164
+ const range = sel.getRangeAt(0);
2165
+ if (!range) return;
2166
+ const table = document.createElement("table");
2167
+ table.className = "wf-editor-table";
2168
+ const tbody = document.createElement("tbody");
2169
+ for (let ri = 0; ri < rows; ri++) {
2170
+ const tr = document.createElement("tr");
2171
+ for (let ci = 0; ci < cols; ci++) {
2172
+ const td = document.createElement("td");
2173
+ if (ri === 0) {
2174
+ td.style.fontWeight = "var(--wf-font-weight-semibold,600)";
2175
+ }
2176
+ td.innerHTML = "&nbsp;";
2177
+ tr.appendChild(td);
2178
+ }
2179
+ tbody.appendChild(tr);
2173
2180
  }
2181
+ table.appendChild(tbody);
2182
+ const wrapper = document.createElement("div");
2183
+ wrapper.appendChild(table);
2184
+ range.deleteContents();
2185
+ range.insertNode(wrapper);
2186
+ range.setStartAfter(wrapper);
2187
+ range.collapse(true);
2188
+ sel.removeAllRanges();
2189
+ sel.addRange(range);
2174
2190
  }
2175
2191
  function renderTableGrid(hoverRow, hoverCol, onSelect, onHover, onLeave) {
2176
2192
  const grid = h("div", {
@@ -2203,8 +2219,18 @@ function renderTableGrid(hoverRow, hoverCol, onSelect, onHover, onLeave) {
2203
2219
  }
2204
2220
 
2205
2221
  // src/components/Editor/Editor.ts
2222
+ function shallowEqual(a, b) {
2223
+ if (a === b) return true;
2224
+ if (!a || !b) return false;
2225
+ const keys = Object.keys(a);
2226
+ if (keys.length !== Object.keys(b).length) return false;
2227
+ for (const k of keys) {
2228
+ if (a[k] !== b[k]) return false;
2229
+ }
2230
+ return true;
2231
+ }
2206
2232
  var Editor = (_props, ctx) => {
2207
- let activeFormats = {};
2233
+ let activeFormats = null;
2208
2234
  let showLinkInput = false;
2209
2235
  let linkUrl = "";
2210
2236
  let mode = "rich";
@@ -2338,9 +2364,11 @@ var Editor = (_props, ctx) => {
2338
2364
  showTableGrid = false;
2339
2365
  tableHoverRow = -1;
2340
2366
  tableHoverCol = -1;
2341
- ctx.ui.render();
2367
+ console.log("[ED] table sel", "editorEl=", !!editorEl, "savedRange=", !!savedRange);
2342
2368
  restoreSelection();
2369
+ console.log("[ED] table after restore", "rc=", window.getSelection()?.rangeCount, "focused=", document.activeElement?.className?.includes("wf-editor-content"));
2343
2370
  insertTable(rows, cols);
2371
+ ctx.ui.render();
2344
2372
  if (editorEl && onChange) emitChange(editorEl.innerHTML);
2345
2373
  };
2346
2374
  const handleTableHover = (row, col) => {
@@ -2373,6 +2401,7 @@ var Editor = (_props, ctx) => {
2373
2401
  open: isRichMode && !!showTableGrid,
2374
2402
  onOpenChange: (v) => {
2375
2403
  showTableGrid = v;
2404
+ if (v) saveSelection();
2376
2405
  ctx.ui.render();
2377
2406
  },
2378
2407
  content: tableGrid
@@ -2398,10 +2427,18 @@ var Editor = (_props, ctx) => {
2398
2427
  const handleMouseUp = () => {
2399
2428
  if (!isRichMode) return;
2400
2429
  saveSelection();
2401
- activeFormats = queryFormats();
2402
- ctx.ui.render();
2430
+ const newFormats = queryFormats();
2431
+ if (activeFormats === null) {
2432
+ activeFormats = newFormats;
2433
+ return;
2434
+ }
2435
+ if (!shallowEqual(activeFormats, newFormats)) {
2436
+ activeFormats = newFormats;
2437
+ ctx.ui.render();
2438
+ }
2403
2439
  };
2404
2440
  const handleMouseDown = () => {
2441
+ saveSelection();
2405
2442
  let needsRender = false;
2406
2443
  if (showLinkInput) {
2407
2444
  showLinkInput = false;
@@ -2527,7 +2564,7 @@ var Editor = (_props, ctx) => {
2527
2564
  return h("div", {
2528
2565
  class: `wf-editor${disabled ? " wf-editor--disabled" : ""}`
2529
2566
  }, [
2530
- !disabled && toolbarItems.length > 0 ? renderToolbar(toolbarItems, activeFormats, !isRichMode, handleToolbarItem, customRender) : null,
2567
+ !disabled && toolbarItems.length > 0 ? renderToolbar(toolbarItems, activeFormats ?? {}, !isRichMode, handleToolbarItem, customRender) : null,
2531
2568
  editorBody,
2532
2569
  linkModal,
2533
2570
  imageModal,
@@ -110,6 +110,12 @@
110
110
  --wf-accent-color: var(--wf-color-primary);
111
111
  --wf-caret-color: var(--wf-color-primary);
112
112
 
113
+ /* ── 断点 ── */
114
+ --wf-bp-sm: 640px;
115
+ --wf-bp-md: 768px;
116
+ --wf-bp-lg: 1024px;
117
+ --wf-bp-xl: 1280px;
118
+
113
119
  /* ── 透明度 ── */
114
120
  --wf-opacity-disabled: 0.5;
115
121
  --wf-opacity-overlay: 0.4;
@@ -314,6 +320,9 @@ code {
314
320
  flex-direction: column;
315
321
  gap: var(--wf-gap, var(--wf-gap-md));
316
322
  }
323
+ @media (min-width: 640px) { .wf-stack\@sm { flex-direction: row; align-items: var(--wf-align, center); } }
324
+ @media (min-width: 768px) { .wf-stack\@md { flex-direction: row; align-items: var(--wf-align, center); } }
325
+ @media (min-width: 1024px) { .wf-stack\@lg { flex-direction: row; align-items: var(--wf-align, center); } }
317
326
 
318
327
  /* stack-reverse — 纵向反向堆叠 */
319
328
  .wf-stack-reverse {
@@ -321,6 +330,9 @@ code {
321
330
  flex-direction: column-reverse;
322
331
  gap: var(--wf-gap, var(--wf-gap-md));
323
332
  }
333
+ @media (min-width: 640px) { .wf-stack-reverse\@sm { flex-direction: row-reverse; align-items: var(--wf-align, center); } }
334
+ @media (min-width: 768px) { .wf-stack-reverse\@md { flex-direction: row-reverse; align-items: var(--wf-align, center); } }
335
+ @media (min-width: 1024px) { .wf-stack-reverse\@lg { flex-direction: row-reverse; align-items: var(--wf-align, center); } }
324
336
 
325
337
  /* row — 横向排列 */
326
338
  .wf-row {
@@ -329,6 +341,9 @@ code {
329
341
  gap: var(--wf-gap, var(--wf-gap-md));
330
342
  align-items: var(--wf-align, center);
331
343
  }
344
+ @media (min-width: 640px) { .wf-row\@sm { flex-direction: row; flex-wrap: wrap; } }
345
+ @media (min-width: 768px) { .wf-row\@md { flex-direction: row; flex-wrap: wrap; } }
346
+ @media (min-width: 1024px) { .wf-row\@lg { flex-direction: row; flex-wrap: wrap; } }
332
347
 
333
348
  /* row-reverse — 横向反向排列 */
334
349
  .wf-row-reverse {
@@ -338,6 +353,9 @@ code {
338
353
  gap: var(--wf-gap, var(--wf-gap-md));
339
354
  align-items: var(--wf-align, center);
340
355
  }
356
+ @media (min-width: 640px) { .wf-row-reverse\@sm { flex-direction: row-reverse; flex-wrap: wrap; } }
357
+ @media (min-width: 768px) { .wf-row-reverse\@md { flex-direction: row-reverse; flex-wrap: wrap; } }
358
+ @media (min-width: 1024px) { .wf-row-reverse\@lg { flex-direction: row-reverse; flex-wrap: wrap; } }
341
359
 
342
360
  /* split — 两端展开 */
343
361
  .wf-split {
@@ -486,11 +504,17 @@ code {
486
504
  .wf-hidden {
487
505
  display: none;
488
506
  }
507
+ @media (min-width: 640px) { .wf-hidden\@sm { display: none; } }
508
+ @media (min-width: 768px) { .wf-hidden\@md { display: none; } }
509
+ @media (min-width: 1024px) { .wf-hidden\@lg { display: none; } }
489
510
 
490
511
  /* block — 块级显示 */
491
512
  .wf-block {
492
513
  display: block;
493
514
  }
515
+ @media (min-width: 640px) { .wf-block\@sm { display: block; } }
516
+ @media (min-width: 768px) { .wf-block\@md { display: block; } }
517
+ @media (min-width: 1024px) { .wf-block\@lg { display: block; } }
494
518
 
495
519
  /* inline — 行内显示 */
496
520
  .wf-inline {
@@ -110,6 +110,12 @@
110
110
  --wf-accent-color: var(--wf-color-primary);
111
111
  --wf-caret-color: var(--wf-color-primary);
112
112
 
113
+ /* ── 断点 ── */
114
+ --wf-bp-sm: 640px;
115
+ --wf-bp-md: 768px;
116
+ --wf-bp-lg: 1024px;
117
+ --wf-bp-xl: 1280px;
118
+
113
119
  /* ── 透明度 ── */
114
120
  --wf-opacity-disabled: 0.5;
115
121
  --wf-opacity-overlay: 0.4;
@@ -314,6 +320,9 @@ code {
314
320
  flex-direction: column;
315
321
  gap: var(--wf-gap, var(--wf-gap-md));
316
322
  }
323
+ @media (min-width: 640px) { .wf-stack\@sm { flex-direction: row; align-items: var(--wf-align, center); } }
324
+ @media (min-width: 768px) { .wf-stack\@md { flex-direction: row; align-items: var(--wf-align, center); } }
325
+ @media (min-width: 1024px) { .wf-stack\@lg { flex-direction: row; align-items: var(--wf-align, center); } }
317
326
 
318
327
  /* stack-reverse — 纵向反向堆叠 */
319
328
  .wf-stack-reverse {
@@ -321,6 +330,9 @@ code {
321
330
  flex-direction: column-reverse;
322
331
  gap: var(--wf-gap, var(--wf-gap-md));
323
332
  }
333
+ @media (min-width: 640px) { .wf-stack-reverse\@sm { flex-direction: row-reverse; align-items: var(--wf-align, center); } }
334
+ @media (min-width: 768px) { .wf-stack-reverse\@md { flex-direction: row-reverse; align-items: var(--wf-align, center); } }
335
+ @media (min-width: 1024px) { .wf-stack-reverse\@lg { flex-direction: row-reverse; align-items: var(--wf-align, center); } }
324
336
 
325
337
  /* row — 横向排列 */
326
338
  .wf-row {
@@ -329,6 +341,9 @@ code {
329
341
  gap: var(--wf-gap, var(--wf-gap-md));
330
342
  align-items: var(--wf-align, center);
331
343
  }
344
+ @media (min-width: 640px) { .wf-row\@sm { flex-direction: row; flex-wrap: wrap; } }
345
+ @media (min-width: 768px) { .wf-row\@md { flex-direction: row; flex-wrap: wrap; } }
346
+ @media (min-width: 1024px) { .wf-row\@lg { flex-direction: row; flex-wrap: wrap; } }
332
347
 
333
348
  /* row-reverse — 横向反向排列 */
334
349
  .wf-row-reverse {
@@ -338,6 +353,9 @@ code {
338
353
  gap: var(--wf-gap, var(--wf-gap-md));
339
354
  align-items: var(--wf-align, center);
340
355
  }
356
+ @media (min-width: 640px) { .wf-row-reverse\@sm { flex-direction: row-reverse; flex-wrap: wrap; } }
357
+ @media (min-width: 768px) { .wf-row-reverse\@md { flex-direction: row-reverse; flex-wrap: wrap; } }
358
+ @media (min-width: 1024px) { .wf-row-reverse\@lg { flex-direction: row-reverse; flex-wrap: wrap; } }
341
359
 
342
360
  /* split — 两端展开 */
343
361
  .wf-split {
@@ -486,11 +504,17 @@ code {
486
504
  .wf-hidden {
487
505
  display: none;
488
506
  }
507
+ @media (min-width: 640px) { .wf-hidden\@sm { display: none; } }
508
+ @media (min-width: 768px) { .wf-hidden\@md { display: none; } }
509
+ @media (min-width: 1024px) { .wf-hidden\@lg { display: none; } }
489
510
 
490
511
  /* block — 块级显示 */
491
512
  .wf-block {
492
513
  display: block;
493
514
  }
515
+ @media (min-width: 640px) { .wf-block\@sm { display: block; } }
516
+ @media (min-width: 768px) { .wf-block\@md { display: block; } }
517
+ @media (min-width: 1024px) { .wf-block\@lg { display: block; } }
494
518
 
495
519
  /* inline — 行内显示 */
496
520
  .wf-inline {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.51.1",
4
+ "version": "0.53.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run build && tsc --emitDeclarationOnly --outdir dist",
34
34
  "typecheck": "tsc --noEmit",
35
35
  "pretest": "docker compose up -d --wait postgres redis 2>/dev/null; sleep 1",
36
- "test": "node --env-file=.env --test 'src/test/**/*.test.ts'",
36
+ "test": "node --env-file=.env --test 'src/test/**/*.test.ts' 'src/components/**/*.test.ts'",
37
37
  "release": "node scripts/release.mjs",
38
38
  "release:dry": "node scripts/release.mjs --dry-run"
39
39
  },