weifuwu 0.76.0 → 0.77.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 +36 -41
- package/dist/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/components/Tour/Tour.d.ts +1 -1
- package/dist/components/index.js +12 -12
- package/dist/components/style.css +17 -0
- package/dist/index.js +1215 -1224
- package/dist/scheduler/index.d.ts +7 -2
- package/dist/ui-dom/hooks/index.d.ts +0 -1
- package/dist/ui-dom/hooks/popup.d.ts +1 -10
- package/dist/ui-dom/hooks/stable.d.ts +1 -1
- package/dist/ui-dom/hooks/types.d.ts +0 -1
- package/dist/ui-dom/index.d.ts +1 -1
- package/dist/ui-dom/index.js +10 -10
- package/dist/ui-dom/jsx-runtime.js +1 -1
- package/dist/ui-dom/testing.js +1 -1
- package/dist/ui-dom/types.d.ts +29 -41
- package/dist/ui-dom/vdom/build.d.ts +10 -3
- package/dist/ui-dom/vdom/diff.d.ts +6 -7
- package/dist/ui-dom/vdom/index.d.ts +1 -1
- package/dist/ui-dom/vdom/mount.d.ts +14 -5
- package/dist/ui-dom/vdom/render.d.ts +2 -0
- package/dist/ui-dom/vdom/serve.d.ts +1 -1
- package/dist/ui-dom/vnode.d.ts +15 -10
- package/docs/components.md +4 -4
- package/docs/custom-components.md +78 -54
- package/docs/examples.md +35 -41
- package/docs/frontend-middleware.md +3 -4
- package/docs/frontend-ui-dom.md +21 -18
- package/docs/frontend.md +243 -168
- package/docs/mobile.md +2 -2
- package/docs/realtime.md +8 -3
- package/package.json +1 -1
- package/dist/ui-dom/focus-trap.d.ts +0 -4
- package/dist/ui-dom/scroll-lock.d.ts +0 -5
- package/dist/ui-dom/vdom/scheduler.d.ts +0 -13
package/dist/ui-dom/types.d.ts
CHANGED
|
@@ -38,17 +38,20 @@ export interface PopupPosition {
|
|
|
38
38
|
refresh: () => void;
|
|
39
39
|
}
|
|
40
40
|
/** 弹层触发方式 — usePopup 的 trigger */
|
|
41
|
-
export type PopupTrigger = 'hover' | 'click' | 'longpress';
|
|
41
|
+
export type PopupTrigger = 'hover' | 'click' | 'longpress' | 'focus' | 'manual';
|
|
42
42
|
/** 弹层组合器配置 — 供 ctx.ui.usePopup 使用 */
|
|
43
43
|
export interface UsePopupOptions {
|
|
44
|
-
/** 触发方式(支持 getter——动态读最新 props;hover 在触屏环境自动降级为 tap
|
|
45
|
-
|
|
44
|
+
/** 触发方式(支持 getter——动态读最新 props;hover 在触屏环境自动降级为 tap)。
|
|
45
|
+
* 可选——缺省 'manual'(无触发 handler——Modal/Drawer 会话级模态场景) */
|
|
46
|
+
trigger?: PopupTrigger | (() => PopupTrigger);
|
|
46
47
|
/** 弹出方向(支持 getter——动态读最新 props),默认 'bottom' */
|
|
47
48
|
placement?: Placement | (() => Placement);
|
|
48
|
-
/** 自由定位(支持 getter):提供则忽略 placement
|
|
49
|
+
/** 自由定位(支持 getter):提供则忽略 placement,直接用坐标(如右键菜单光标处)。
|
|
50
|
+
* 可返回 width(可选)——portal 内联 style 精确宽度(DatePicker 跟随 trigger 宽) */
|
|
49
51
|
position?: () => {
|
|
50
52
|
x: number;
|
|
51
53
|
y: number;
|
|
54
|
+
width?: number;
|
|
52
55
|
};
|
|
53
56
|
/** 水平对齐:center=居中于触发元素(默认),start=左对齐(Menubar 面板用) */
|
|
54
57
|
center?: boolean;
|
|
@@ -56,8 +59,8 @@ export interface UsePopupOptions {
|
|
|
56
59
|
gap?: number;
|
|
57
60
|
/** 视口安全边距(px,默认 8) */
|
|
58
61
|
margin?: number;
|
|
59
|
-
/** 锚定元素 getter(ref
|
|
60
|
-
el
|
|
62
|
+
/** 锚定元素 getter(ref 保存的触发元素);positioning 'none' 场景可省略 */
|
|
63
|
+
el?: () => HTMLElement | null;
|
|
61
64
|
/** 是否打开(getter) */
|
|
62
65
|
isOpen: () => boolean;
|
|
63
66
|
/** 非受控:设置打开状态(调用方负责 render/dirty) */
|
|
@@ -66,8 +69,9 @@ export interface UsePopupOptions {
|
|
|
66
69
|
open?: boolean | (() => boolean);
|
|
67
70
|
/** 受控回调(可选) */
|
|
68
71
|
onOpenChange?: (open: boolean) => void;
|
|
69
|
-
/** 面板宽度(px,可选):自动 clamp 到视口(≤ 100vw - 32px
|
|
70
|
-
|
|
72
|
+
/** 面板宽度(px 或 getter,可选):自动 clamp 到视口(≤ 100vw - 32px);getter 动态跟随
|
|
73
|
+
* (DatePicker date 模式跟随 trigger 宽,range 模式返回 undefined 自适应双面板) */
|
|
74
|
+
width?: number | (() => number | undefined);
|
|
71
75
|
/** 点外部关闭(默认 true) */
|
|
72
76
|
closeOnOutside?: boolean;
|
|
73
77
|
/** Escape 关闭(默认 true) */
|
|
@@ -75,8 +79,9 @@ export interface UsePopupOptions {
|
|
|
75
79
|
/** 遮罩(默认 false):渲染全屏 overlay(--wf-overlay,点击遮罩关闭,
|
|
76
80
|
* 模态语义阻断页面交互)。false = 无遮罩 document 外部点击(§5.4 默认)。
|
|
77
81
|
* 遮罩层 z-index = --wf-z-overlay(80) < 面板 --wf-z-popover(120)。
|
|
78
|
-
* 配合 maskClosable 控制遮罩点击是否关闭。
|
|
79
|
-
|
|
82
|
+
* 配合 maskClosable 控制遮罩点击是否关闭。
|
|
83
|
+
* 传 VNode = 自定义遮罩内容(Tour 挖洞高亮遮罩——交互组件自控,不自动 onClick) */
|
|
84
|
+
mask?: boolean | VNode;
|
|
80
85
|
/** 遮罩点击关闭(默认 true;mask:true 时生效——危险确认 maskClosable=false 防误触) */
|
|
81
86
|
maskClosable?: boolean;
|
|
82
87
|
/** 遮罩面板居中(默认 false;mask:true 时生效):面板覆盖全屏 flex 居中
|
|
@@ -95,12 +100,26 @@ export interface UsePopupOptions {
|
|
|
95
100
|
closeDelay?: number | (() => number);
|
|
96
101
|
/** 禁用(getter):禁用时所有触发不生效且 portal 不渲染 */
|
|
97
102
|
disabled?: () => boolean;
|
|
103
|
+
/** 定位模式:'anchor'(默认——锚定 el 计算坐标)/ 'none'(不加坐标——组件自定义定位,
|
|
104
|
+
* 如 Modal 的 .wf-modal inset:0 居中) */
|
|
105
|
+
positioning?: 'anchor' | 'none';
|
|
106
|
+
/** 会话级模态能力(Modal/Drawer 用——锚定弹层默认全关,零成本) */
|
|
107
|
+
/** 退场状态机(open → exit → closed + animationend 卸载):组件 render 阶段调 sync(open) 驱动 */
|
|
108
|
+
presence?: boolean;
|
|
109
|
+
/** 焦点 trap(面板挂载时锁定焦点,卸载归还——会话级模态专用) */
|
|
110
|
+
trapFocus?: boolean;
|
|
111
|
+
/** 滚动锁(sync(true) 锁 body 滚动 / 面板卸载释放——会话级模态专用) */
|
|
112
|
+
lockScroll?: boolean;
|
|
98
113
|
}
|
|
99
114
|
/** 弹层组合器返回值 — usePopup */
|
|
100
115
|
export interface UsePopupHandle {
|
|
101
116
|
/** 当前打开状态(渲染期读取) */
|
|
102
117
|
open: boolean;
|
|
103
118
|
setOpen: (open: boolean) => void;
|
|
119
|
+
/** 当前阶段(presence 模式:open → exit → closed;非 presence:open/closed 二态) */
|
|
120
|
+
phase?: 'closed' | 'open' | 'exit';
|
|
121
|
+
/** 同步打开状态(render 阶段调用——presence 模式驱动退场状态机,返回当前 phase;非 presence 模式返回二态) */
|
|
122
|
+
sync?: (open: boolean) => 'closed' | 'open' | 'exit';
|
|
104
123
|
/** spread 到触发/包装元素:触发(hover 门控/tap 降级/longpress)+ Escape + focus */
|
|
105
124
|
wrapProps: Record<string, any>;
|
|
106
125
|
/** 包装弹层内容:定位 + 视口/宽度 clamp + portal;关闭时返回 null */
|
|
@@ -467,26 +486,6 @@ export interface WfuiContext {
|
|
|
467
486
|
onFocus: () => void;
|
|
468
487
|
};
|
|
469
488
|
};
|
|
470
|
-
/**
|
|
471
|
-
* 全屏对话框组合器(收敛 Modal/Drawer 的退场状态机 + 滚动锁 + 焦点 trap):
|
|
472
|
-
* mount 创建,render 阶段 sync(open) 驱动状态机;组件只管布局。
|
|
473
|
-
*
|
|
474
|
-
* ```tsx
|
|
475
|
-
* const dialog = ctx.ui.useDialog({ name: 'Modal' })
|
|
476
|
-
* return (props) => {
|
|
477
|
-
* const phase = dialog.sync(props.open)
|
|
478
|
-
* if (phase === 'closed') return null
|
|
479
|
-
* return createPortal(h('div', {
|
|
480
|
-
* ref: dialog.rootRef,
|
|
481
|
-
* class: `wf-modal ${phase === 'exit' ? 'wf-modal--exit' : 'wf-modal--enter'}`,
|
|
482
|
-
* onKeyDown: (e) => { if (e.key === 'Escape') props.onClose?.() },
|
|
483
|
-
* }, [overlay, h('div', { class: 'wf-modal-content', ref: dialog.panelRef }, children)]), 'modal')
|
|
484
|
-
* }
|
|
485
|
-
* ```
|
|
486
|
-
* Escape 语义(危险操作差异)留在组件层——诚实裁剪。
|
|
487
|
-
*/
|
|
488
|
-
/** 通用显隐状态机(非 dialog 浮层/面板):open → exit → closed(animationend 延迟卸载)。
|
|
489
|
-
* useDialog 是其对话框特例(+ lockScroll/trapFocus)。mount 创建,render 阶段 sync(open)。 */
|
|
490
489
|
usePresence: (options?: {
|
|
491
490
|
name?: string;
|
|
492
491
|
}) => {
|
|
@@ -496,17 +495,6 @@ export interface WfuiContext {
|
|
|
496
495
|
/** render 阶段同步 open → 返回当前 phase */
|
|
497
496
|
sync: (open: boolean) => 'closed' | 'open' | 'exit';
|
|
498
497
|
};
|
|
499
|
-
useDialog: (options?: {
|
|
500
|
-
name?: string;
|
|
501
|
-
}) => {
|
|
502
|
-
phase: 'closed' | 'open' | 'exit';
|
|
503
|
-
/** 挂到 portal 根(lockScroll + animationend 退场监听) */
|
|
504
|
-
rootRef: (el: HTMLElement | null) => void;
|
|
505
|
-
/** 挂到焦点 trap 目标(对话框面板) */
|
|
506
|
-
panelRef: (el: HTMLElement | null) => void;
|
|
507
|
-
/** render 阶段同步 open → 返回当前 phase */
|
|
508
|
-
sync: (open: boolean) => 'closed' | 'open' | 'exit';
|
|
509
|
-
};
|
|
510
498
|
/** 响应式系统偏好(prefers-reduced-motion):JS 动画(rAF/tween)侧跳过用。
|
|
511
499
|
* CSS 动画已有 _base.css 全局降级(0.01ms)——此原语覆盖 JS 动画路径。 */
|
|
512
500
|
useReducedMotion: () => boolean;
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* - 剪枝:已构建 + props 同 + 旧 _child 有值 → 复用旧 _child(renderFn 不重跑)
|
|
8
8
|
* - 兄弟组件并行(工厂同步执行到第一个 await 后并发等待)
|
|
9
9
|
* - **纯函数无 DOM**——构建产物只含 vnode 树
|
|
10
|
+
*
|
|
11
|
+
* V3-2(同步快路径):buildVNode 非 async——返回 `VNodeChild | Promise<VNodeChild>`。
|
|
12
|
+
* **红线(用户确认):组件两阶段异步定义不可改动**——组件 vnode 分支仍 await 工厂 +
|
|
13
|
+
* renderFn(renderFn 强制异步契约不变);仅「无需 await 的路径」(剪枝复用/文本/null/
|
|
14
|
+
* 已构建 native)同步返回——零微任务。调用方统一 await 吸收(同步值 await 仅 1 微任务)。
|
|
10
15
|
*/
|
|
11
16
|
import type { VNode, VNodeChild } from '../vnode.ts';
|
|
12
17
|
import type { WfuiContext } from '../types.ts';
|
|
@@ -17,18 +22,20 @@ export declare function componentPropsEqual(a: Record<string, any>, b: Record<st
|
|
|
17
22
|
export declare function mountAsyncComponent(vnode: VNode, ctx: WfuiContext, reg: Registry, opts?: {
|
|
18
23
|
reuse?: VNode;
|
|
19
24
|
}): Promise<{
|
|
20
|
-
renderFn: (props: VNode['props']) => VNode | null
|
|
25
|
+
renderFn: (props: VNode['props']) => Promise<VNode | null>;
|
|
21
26
|
childCtx: WfuiContext;
|
|
22
27
|
}>;
|
|
23
28
|
/**
|
|
24
|
-
*
|
|
29
|
+
* 递归展开组件树:await 工厂 → renderFn → 递归子树。**零 DOM**。
|
|
25
30
|
*
|
|
26
31
|
* - 组件节点保留在树上(挂 `_render` + `_child`)——$ dirty 精准刷新锚点不丢
|
|
27
32
|
* - 兄弟组件 Promise.all 并行
|
|
28
33
|
* - 旧树对照(oldInput):同位置同类型组件复用旧 `_render`;同 props + 旧 _child 有值
|
|
29
34
|
* 复用旧 `_child`(renderFn 不重跑——三态 skip 语义前置)
|
|
30
35
|
* - 原地 mutate vnode(_render/_child)——引用保持
|
|
36
|
+
* - V3-2:非 async——剪枝/文本/null/已构建 native 同步返回(零微任务);
|
|
37
|
+
* 组件路径(工厂 + renderFn await)返回 Promise(异步契约不变)
|
|
31
38
|
*/
|
|
32
39
|
export declare function buildVNode(input: VNodeChild, ctx: WfuiContext, oldInput?: VNodeChild, reg?: Registry, opts?: {
|
|
33
40
|
force?: boolean;
|
|
34
|
-
}): Promise<VNodeChild>;
|
|
41
|
+
}): VNodeChild | Promise<VNodeChild>;
|
|
@@ -8,17 +8,16 @@
|
|
|
8
8
|
* 三态 skip:props 同 + 无 dirty + ctx 版本同 → 复用旧 _child(renderFn 不重跑)。
|
|
9
9
|
*/
|
|
10
10
|
import type { VNodeChild } from '../vnode.ts';
|
|
11
|
-
|
|
12
|
-
export
|
|
11
|
+
import { normalizeChildren } from '../vnode.ts';
|
|
12
|
+
export { normalizeChildren };
|
|
13
13
|
export interface PatchCtx {
|
|
14
14
|
browser: any;
|
|
15
15
|
registry: import('./registry.ts').Registry;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
rendered?: Set<string>;
|
|
19
|
-
/** 当前 ctx 版本号 */
|
|
16
|
+
/** 当前 ctx 版本号(三态 skip 版本比较:组件 _ctxVersion !== 当前版本 → 不 skip,
|
|
17
|
+
* 强制重渲染——bumpCtxVersion 递增后所有组件重跑 renderFn,如 i18n 切换语言) */
|
|
20
18
|
ctxVersion?: number;
|
|
21
|
-
|
|
19
|
+
/** force:跳过三态 skip(mountRoot.rerender 全量重跑用) */
|
|
20
|
+
force?: boolean;
|
|
22
21
|
}
|
|
23
22
|
/**
|
|
24
23
|
* patchValue — 同步 diff 单一节点。
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
export { buildVNode } from './build.ts';
|
|
15
15
|
export { renderValue } from './render.ts';
|
|
16
16
|
export { patchValue } from './diff.ts';
|
|
17
|
-
export {
|
|
17
|
+
export { createRenderer, type Renderer } from './mount.ts';
|
|
18
18
|
export { createRegistry, type Registry } from './registry.ts';
|
|
19
19
|
export { createStore, type ExternalStore } from '../store.ts';
|
|
20
20
|
export { mountRoot, createVdomContext, mountCommand, unmountCommand, createCommandContainer } from './mount.ts';
|
|
@@ -7,19 +7,18 @@
|
|
|
7
7
|
import type { VNode, VNodeChild, Component } from '../vnode.ts';
|
|
8
8
|
import type { WfuiContext } from '../types.ts';
|
|
9
9
|
import type { BrowserEnv } from '../types.ts';
|
|
10
|
-
import { type Scheduler } from './scheduler.ts';
|
|
11
10
|
import { type Registry } from './registry.ts';
|
|
12
11
|
export interface MountOptions {
|
|
13
12
|
browser: BrowserEnv;
|
|
14
13
|
root: HTMLElement;
|
|
15
14
|
registry?: Registry;
|
|
16
|
-
|
|
15
|
+
renderer?: Renderer;
|
|
17
16
|
onError?: (e: unknown) => void;
|
|
18
17
|
}
|
|
19
18
|
export interface MountHandle {
|
|
20
19
|
ctx: WfuiContext;
|
|
21
20
|
registry: Registry;
|
|
22
|
-
|
|
21
|
+
renderer: Renderer;
|
|
23
22
|
/** 挂载根组件 */
|
|
24
23
|
mount(comp: Component | VNodeChild): Promise<void>;
|
|
25
24
|
/** 整树强制重渲染(force——测试辅助/手动刷新:renderFn 重跑 + patch) */
|
|
@@ -30,11 +29,21 @@ export interface MountHandle {
|
|
|
30
29
|
export interface VdomContext {
|
|
31
30
|
ctx: WfuiContext;
|
|
32
31
|
registry: Registry;
|
|
33
|
-
|
|
32
|
+
renderer: Renderer;
|
|
34
33
|
rootUi: any;
|
|
35
34
|
destroyPopupListeners: () => void;
|
|
36
35
|
}
|
|
37
|
-
|
|
36
|
+
export interface Renderer {
|
|
37
|
+
render(ids?: string[]): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export interface RendererOptions {
|
|
40
|
+
registry: Registry;
|
|
41
|
+
ctx: WfuiContext;
|
|
42
|
+
rootEl?: HTMLElement;
|
|
43
|
+
onError?: (e: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
export declare function createRenderer(opts: RendererOptions): Renderer;
|
|
46
|
+
/** 组装 vdom 渲染上下文(ctx/registry/renderer/rootUi——含完整 hooks 转发) */
|
|
38
47
|
export declare function createVdomContext(opts: MountOptions): VdomContext;
|
|
39
48
|
export declare function mountRoot(opts: MountOptions): MountHandle;
|
|
40
49
|
/** vdom 命令式挂载:buildVNode(await 工厂)→ renderValue → append + _parentNode */
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import type { VNodeChild } from '../vnode.ts';
|
|
9
9
|
import type { BrowserEnv } from '../types.ts';
|
|
10
10
|
export declare const SVG_TAGS: Set<string>;
|
|
11
|
+
/** 事件 prop 判定:on + 大写字母(React 约定)——排除 once/only 等 on 开头非事件属性 */
|
|
12
|
+
export declare const EVENT_RE: RegExp;
|
|
11
13
|
export declare function setProp(el: Element, key: string, value: any): void;
|
|
12
14
|
/** 递归渲染(同步——组件必须已构建) */
|
|
13
15
|
export declare function renderValue(v: VNodeChild, ctx: any, browser?: BrowserEnv): Node | null;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 与第 1 代 serve.ts 的区别(AGENTS.md §4.0 无自动渲染原则):
|
|
5
5
|
* - 渲染管线:buildVNode(async 预构建 await 全部)→ renderValue/patchValue(同步落地)
|
|
6
|
-
* - 调度:vdom
|
|
6
|
+
* - 调度:vdom renderer(render() 直接执行——await = DOM 已同步)
|
|
7
7
|
* - 动态挂载:buildVNode 阶段 await(无占位/注释/补全回调)
|
|
8
8
|
*
|
|
9
9
|
* 保留公开 API(uiServe/UIServeOptions/UIServeHandle)——迁移无缝。
|
package/dist/ui-dom/vnode.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export interface VNode {
|
|
|
24
24
|
_remoteEl?: HTMLElement | undefined;
|
|
25
25
|
/** VNode 的 DOM 归属:'local' 在父 DOM 树下,'remote' 在别处 */
|
|
26
26
|
_placement?: 'local' | 'remote';
|
|
27
|
-
/** 两阶段组件的 render 函数(mount
|
|
28
|
-
_render?: (props: Record<string, unknown>) => VNode | null
|
|
27
|
+
/** 两阶段组件的 render 函数(mount 返回的函数——强制异步:props 变化时可 await 数据) */
|
|
28
|
+
_render?: (props: Record<string, unknown>) => Promise<VNode | null>;
|
|
29
29
|
/** 组件实例 ID(如 '_wf_0') */
|
|
30
30
|
_id?: string;
|
|
31
31
|
/** 自定义组件 ID(ctx.ui.selfId() 注册,跨组件精准刷新) */
|
|
@@ -36,23 +36,25 @@ export interface VNode {
|
|
|
36
36
|
_parentVNode?: VNode;
|
|
37
37
|
/** 组件输出的第一个 DOM 节点 */
|
|
38
38
|
_refNode?: Node | null;
|
|
39
|
-
/** 原生 async 组件缓存(vnode 级按实例):in-flight Promise → resolved renderFn(diff 传递继承) */
|
|
40
|
-
_asyncDef?: ((props: Record<string, unknown>) => VNode | null) | Promise<((props: Record<string, unknown>) => VNode | null)> | null;
|
|
41
39
|
/** Fragment 展开后的多个直属 DOM 节点范围(diff 对齐用,见 diff.ts) */
|
|
42
40
|
_childNodes?: Node[];
|
|
43
|
-
/** 组件
|
|
41
|
+
/** 组件 renderFn 上次执行时的 ctx 版本号(buildVNode 剪枝 + diff 三态 skip 的版本比较——
|
|
42
|
+
* bumpCtxVersion 递增后版本不同 → 强制重跑 renderFn,如 i18n 切换语言) */
|
|
44
43
|
_ctxVersion?: number;
|
|
45
44
|
}
|
|
46
45
|
/**
|
|
47
|
-
*
|
|
46
|
+
* 两阶段异步组件(weifuwu 唯一组件形态):
|
|
48
47
|
* async (initProps, ctx) => Promise<renderFn>
|
|
49
|
-
* 外层 = mount(一次,可 await 数据),内层 =
|
|
48
|
+
* 外层 = mount(一次,可 await 数据),内层 = renderFn(每次 dirty/props 变化——**强制异步**,
|
|
49
|
+
* 可 await 数据;统一异步心智:两阶段都可 await,无「同步组件 vs 异步组件」二元形态)。
|
|
50
50
|
* P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* renderFn 签名:async (props) => Promise<VNode | null>——同步 renderFn 是类型错误
|
|
53
|
+
* (diff 永不执行 renderFn——渲染器在 buildVNode 阶段 await,同步上下文拿不到 vnode)。
|
|
54
|
+
* 渲染器按「返回值是 Promise」判别(主路径 buildVNode await 全部工厂 + renderFn)。
|
|
54
55
|
*/
|
|
55
|
-
export type
|
|
56
|
+
export type RenderFn<P> = (props: P) => Promise<VNode | null>;
|
|
57
|
+
export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => Promise<RenderFn<P> | null>;
|
|
56
58
|
export declare const Fragment: unique symbol;
|
|
57
59
|
/** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
|
|
58
60
|
export declare const Portal: unique symbol;
|
|
@@ -63,6 +65,9 @@ export declare function jsxDEV(type: VNodeType, props: Record<string, any> | nul
|
|
|
63
65
|
/** `h`(hyperscript)支持 variadic children: `h('div', {class:'x'}, child1, child2)` */
|
|
64
66
|
export declare function h(type: VNodeType, props: Record<string, any> | null, ...children: VNodeChild[]): VNode;
|
|
65
67
|
export declare function isNative(vnode: VNode): boolean;
|
|
68
|
+
/** 递归文本/数组归一化(children 数组展开——嵌套数组扁平化,DOM 范围对齐)。
|
|
69
|
+
* 栈展开(索引遍历替代 shift/unshift 头部操作——长数组 O(n) 而非 O(n²));逆序入栈 + pop 保持原顺序 */
|
|
70
|
+
export declare function normalizeChildren(c: VNodeChild | undefined | null): VNodeChild[];
|
|
66
71
|
export declare function isComponent(vnode: VNode): boolean;
|
|
67
72
|
export declare function isFragment(vnode: VNode): boolean;
|
|
68
73
|
export declare function isPortal(vnode: VNode): boolean;
|
package/docs/components.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
|
|
4
4
|
|
|
5
|
-
113 个 HTML 原语组件。每个是 `(
|
|
5
|
+
113 个 HTML 原语组件。每个是 `async (initProps, ctx) => (props) => Promise<VNode>`(两阶段组件,与前端框架同一模型——外层工厂 + 内层 renderFn 强制异步),引用 `--wf-*` CSS 变量做主题。另含 `confirm()` / `toast()` 命令式中间件。
|
|
6
6
|
|
|
7
7
|
> **组件速查(weifuwu 组件 ↔ antd / Element Plus / shadcn-ui 对应 + 迁移示例)**:见 [`docs/components-map.md`](components-map.md)——从其他组件库迁来的开发者按功能直接找对应组件。
|
|
8
8
|
> **自定义组件开发**:见 [docs/custom-components.md](custom-components.md)——usePopup/useControlled/对话框/AI 组件/类型纪律逐步指南。
|
|
@@ -133,9 +133,9 @@ import 'weifuwu/components/style.css' // 包含 Token + 58 布局原语 + 136
|
|
|
133
133
|
|
|
134
134
|
```
|
|
135
135
|
mount ──────────────────────────────────────────
|
|
136
|
-
const Counter = (_init, ctx) => {
|
|
136
|
+
const Counter = async (_init, ctx) => { ← mount(只一次,可 await)
|
|
137
137
|
let count = 0 ← 初始化状态
|
|
138
|
-
return (props) => {
|
|
138
|
+
return async (props) => { ← renderFn(强制异步)
|
|
139
139
|
// ... ← 每次 dirty/props 变化执行
|
|
140
140
|
}
|
|
141
141
|
}
|
|
@@ -149,7 +149,7 @@ ref ─────────────────────────
|
|
|
149
149
|
})
|
|
150
150
|
|
|
151
151
|
props 变化 ─────────────────────────────────────
|
|
152
|
-
return (props) => {
|
|
152
|
+
return async (props) => {
|
|
153
153
|
// 每次 render 都收到最新 props ← 相当于 onupdate
|
|
154
154
|
if (props.value !== prevValue) { ... }
|
|
155
155
|
}
|
|
@@ -13,31 +13,51 @@
|
|
|
13
13
|
import { h, type Component } from 'weifuwu/ui-dom'
|
|
14
14
|
|
|
15
15
|
// Component<P, C>:P = props(JSX 自动推断),C = ctx 注入依赖(默认 {})
|
|
16
|
-
const Badge: Component<{ text: string; color?: string }> = () =>
|
|
17
|
-
(props) => h('span', { class: 'my-badge', style: { color: props.color } }, props.text)
|
|
16
|
+
const Badge: Component<{ text: string; color?: string }> = async () =>
|
|
17
|
+
async (props) => h('span', { class: 'my-badge', style: { color: props.color } }, props.text)
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
- 两阶段:外层 `(initProps, ctx) => …` 只执行一次(mount),内层 `(props) => VNode
|
|
21
|
-
-
|
|
20
|
+
- 两阶段:外层 `(initProps, ctx) => …` 只执行一次(mount),内层 `(props) => Promise<VNode>` 每次渲染执行(renderFn 强制异步——可 await 数据)
|
|
21
|
+
- 有状态组件用闭包 `let` + 事件里 `ctx.ui.render()`(render-only);不需要渲染的状态不调 `render()`
|
|
22
|
+
|
|
23
|
+
### mount 与 render 的职责(事件函数写在哪层)
|
|
24
|
+
|
|
25
|
+
| | mount(外层工厂,一次) | render(内层 renderFn,每次) |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| 职责 | 初始化状态 / 订阅 / 定时器 / **定义依赖稳定引用的回调** | 读最新 props / 派生数据 / **定义依赖它们的回调** / 输出视图 |
|
|
28
|
+
| 可访问 | `initProps`(首次)、`ctx`、mount `let`、稳定 handle | 最新 `props`、mount 闭包、`ctx` |
|
|
29
|
+
| 事件函数 | **只依赖稳定引用**(ctx / mount let / 稳定 handle 如 useChat 的 `chat`)→ mount 定义,天然引用恒等 | 依赖最新 props / 派生状态(如 Table 的 `rowSelection`、Menu 的 `openSet`)→ render 内定义(闭包捕获最新值) |
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const AiChat = async (initProps, ctx) => {
|
|
33
|
+
const chat = initProps.chat // 稳定 handle(useChat 返回,引用不变)
|
|
34
|
+
const onSend = () => chat.send() // ✅ mount 定义:只依赖稳定引用——引用恒等,零重绑
|
|
35
|
+
return async (props) => {
|
|
36
|
+
const onSelect = (k: string) => props.onSelect?.(k) // ✅ render 定义:依赖最新 props——闭包捕获当前值
|
|
37
|
+
return h('button', { onClick: onSelect }, '选')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**规则**:回调只依赖 ctx / mount `let` / 稳定 handle → **mount 定义**(天然稳定,不重绑);依赖最新 props / 派生数据 → **render 内定义**(闭包捕获最新值;引用变化导致事件重绑是**正确性要求**——必须读最新状态,框架不做稳定引用魔法)。
|
|
22
43
|
|
|
23
44
|
## 1. 有状态组件
|
|
24
45
|
|
|
25
46
|
```tsx
|
|
26
|
-
const Toggle: Component = (_init, ctx) => {
|
|
27
|
-
|
|
28
|
-
$.on = false
|
|
47
|
+
const Toggle: Component = async (_init, ctx) => {
|
|
48
|
+
let on = false // 普通对象状态(render-only:无 $ Proxy)
|
|
29
49
|
|
|
30
|
-
return (props) => h('button', {
|
|
50
|
+
return async (props) => h('button', {
|
|
31
51
|
class: 'my-toggle',
|
|
32
|
-
onClick: () =>
|
|
33
|
-
},
|
|
52
|
+
onClick: () => { on = !on; ctx.ui.render() }, // 改状态后显式 render()
|
|
53
|
+
}, on ? '开' : '关')
|
|
34
54
|
}
|
|
35
55
|
```
|
|
36
56
|
|
|
37
57
|
| 状态类型 | 存放位置 | 触发渲染 |
|
|
38
58
|
|---------|---------|---------|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
59
|
+
| 组件内部状态 | 闭包 `let` | 改后调 `ctx.ui.render()` |
|
|
60
|
+
| 共享状态 | `createStore` + `ctx.ui.useExternal()` | store 变更自动 |
|
|
41
61
|
| 内部缓存 | 闭包 `let` | 不触发 |
|
|
42
62
|
|
|
43
63
|
## 2. 带弹层的组件(最高频的自定义场景)
|
|
@@ -45,23 +65,22 @@ const Toggle: Component = (_init, ctx) => {
|
|
|
45
65
|
用 `ctx.ui.usePopup`——一个组合器收敛 open 状态 + 触发(hover→tap 降级/longpress)+ Escape + 外部点击 + 定位/视口 clamp + portal:
|
|
46
66
|
|
|
47
67
|
```tsx
|
|
48
|
-
const MyPopover: Component<{ content: string }> = (_init, ctx) => {
|
|
49
|
-
|
|
50
|
-
$.open = false
|
|
68
|
+
const MyPopover: Component<{ content: string }> = async (_init, ctx) => {
|
|
69
|
+
let open = false
|
|
51
70
|
let wrapEl: HTMLElement | null = null
|
|
52
71
|
const wrapRef = (el: HTMLElement | null) => { wrapEl = el }
|
|
53
72
|
|
|
54
73
|
const popup = ctx.ui.usePopup({
|
|
55
74
|
trigger: 'hover', // 触屏自动降级 tap(useHoverCapable 内部判定)
|
|
56
75
|
el: () => wrapEl, // 锚点
|
|
57
|
-
isOpen: () =>
|
|
58
|
-
setOpen: (v) => {
|
|
76
|
+
isOpen: () => open,
|
|
77
|
+
setOpen: (v) => { open = v; ctx.ui.render() }, // 改状态 + render
|
|
59
78
|
width: 320, // 自动 clamp 视口
|
|
60
79
|
closeOnOutside: true, // 外部点击关闭(默认)
|
|
61
80
|
closeOnEscape: true, // Escape 关闭(默认,document 级——portal 焦点也生效)
|
|
62
81
|
})
|
|
63
82
|
|
|
64
|
-
return (props) =>
|
|
83
|
+
return async (props) =>
|
|
65
84
|
h('span', { class: 'anchor', ref: wrapRef, ...popup.wrapProps },
|
|
66
85
|
props.children,
|
|
67
86
|
popup.portal(h('div', { class: 'wf-panel' }, props.content)),
|
|
@@ -73,54 +92,62 @@ const MyPopover: Component<{ content: string }> = (_init, ctx) => {
|
|
|
73
92
|
|
|
74
93
|
## 3. 对话框类组件(Modal 系)
|
|
75
94
|
|
|
76
|
-
全屏对话框(焦点 trap + 滚动锁 +
|
|
95
|
+
全屏对话框(焦点 trap + 滚动锁 + 退场动画)是 usePopup 的**会话级模态模式**(`presence/trapFocus/lockScroll/positioning: 'none'`——Modal/Drawer 同款):
|
|
77
96
|
|
|
78
97
|
```tsx
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
const MyDialog: Component<{ open: boolean; onClose: () => void }> = async (_init, ctx) => {
|
|
99
|
+
let latestOpen = false
|
|
100
|
+
const popup = ctx.ui.usePopup({
|
|
101
|
+
presence: true, // 退场状态机(open → exit → closed + animationend)
|
|
102
|
+
trapFocus: true, // 焦点 trap(面板挂载锁定/卸载归还)
|
|
103
|
+
lockScroll: true, // 滚动锁(打开锁 / 面板卸载释放)
|
|
104
|
+
positioning: 'none', // 组件自定义定位(.wf-modal inset:0 居中)
|
|
105
|
+
closeOnOutside: false, closeOnEscape: false, // 关闭语义组件自控
|
|
106
|
+
isOpen: () => latestOpen,
|
|
107
|
+
setOpen: () => {},
|
|
108
|
+
}) // mount 创建
|
|
109
|
+
|
|
110
|
+
return async (props) => {
|
|
111
|
+
latestOpen = !!props.open
|
|
112
|
+
const phase = popup.sync!(latestOpen) // render 同步 open
|
|
86
113
|
if (phase === 'closed') return null
|
|
87
114
|
|
|
88
|
-
return
|
|
115
|
+
return popup.portal(h('div', {
|
|
89
116
|
class: 'wf-overlay',
|
|
90
117
|
onClick: (e: any) => { if (e.target === e.currentTarget) props.onClose() },
|
|
91
118
|
}, h('div', {
|
|
92
119
|
class: `wf-modal ${phase === 'exit' ? 'wf-modal--exit' : 'wf-modal--enter'}`,
|
|
93
|
-
ref: dialog.panelRef, // 焦点 trap 目标
|
|
94
120
|
onKeyDown: (e: any) => { if (e.key === 'Escape') props.onClose() }, // Escape 语义组件层
|
|
95
|
-
}, props.children)),
|
|
121
|
+
}, props.children)), 'modal') // portalKey 语义化(#__wf_portal 容器标记)
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
```
|
|
99
125
|
|
|
100
|
-
>
|
|
101
|
-
>
|
|
126
|
+
> **ref 接线**:`trapFocus`/`lockScroll`/presence 退场监听全部由 usePopup 内部接线到 portal 面板
|
|
127
|
+
> (`portalPanelRef`——content 的 `ref` prop 会被转发调用)——组件层无需手挂 `rootRef`/`panelRef`。
|
|
128
|
+
> 低层原语 `trapFocus`/`lockScroll` 已收编为 usePopup 内部实现(**不对外导出**——AGENTS.md §5.4);
|
|
129
|
+
> `animateOut` 仍从 `weifuwu/ui-dom` 导出(非弹窗动画场景用)。
|
|
102
130
|
|
|
103
131
|
## 4. AI 组件
|
|
104
132
|
|
|
105
133
|
会话语义由 `ctx.ui.useChat` 提供(消息/流式/工具/审批/stop/retry 全封装),返回的 handle 与 `$` 同一容器:
|
|
106
134
|
|
|
107
135
|
```tsx
|
|
108
|
-
const ChatPanel: Component = (_init, ctx) => {
|
|
109
|
-
const
|
|
136
|
+
const ChatPanel: Component = async (_init, ctx) => {
|
|
137
|
+
const chat = ctx.ui.useChat({
|
|
110
138
|
url: '/api/chat',
|
|
111
139
|
approveUrl: '/api/approve', // HITL 审批上行(缺省 approve() 只清卡片)
|
|
112
140
|
body: (messages) => ({ messages, mode: 'agent' }),
|
|
113
141
|
})
|
|
114
142
|
|
|
115
143
|
return () => h('div', { class: 'chat' },
|
|
116
|
-
|
|
117
|
-
h('
|
|
118
|
-
h('button', { onClick: () => $.send() }, $.streaming ? '…' : '发送'),
|
|
144
|
+
h(AiChat, { chat }), // 标准界面:输入/消息/流式/工具卡全内置
|
|
145
|
+
h('button', { onClick: () => chat.send() }, chat.streaming ? '…' : '发送'),
|
|
119
146
|
)
|
|
120
147
|
}
|
|
121
148
|
```
|
|
122
149
|
|
|
123
|
-
**共享
|
|
150
|
+
**共享 handle 给子组件**(如 `<AiChat chat={chat} />`):会话 handle 带 `subscribe(cb)`——子组件 mount 期 `ctx.ui.useExternal(initProps.chat)` 自订阅(AiChat 已内置),会话状态变化自动重渲染订阅组件。
|
|
124
151
|
|
|
125
152
|
## 5. 异步组件(数据声明在工厂层)
|
|
126
153
|
|
|
@@ -129,29 +156,27 @@ const ChatPanel: Component = (_init, ctx) => {
|
|
|
129
156
|
```tsx
|
|
130
157
|
const UserCard = async (initProps, ctx) => {
|
|
131
158
|
const user = await ctx.data.get(`/api/user/${initProps.userId}`) // 三场景:SSR→__DATA__ / hydration 种子 / SPA fetch
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return (props) => h('div', {}, user.name, h('button', { onClick: () => $.liked = !$.liked }))
|
|
159
|
+
let liked = false
|
|
160
|
+
return async (props) => h('div', {}, user.name, h('button', { onClick: () => { liked = !liked; ctx.ui.render() } }))
|
|
135
161
|
}
|
|
136
162
|
```
|
|
137
163
|
|
|
138
|
-
- 渲染器按「返回值是 Promise」判别:主路径 `buildVNode` await
|
|
164
|
+
- 渲染器按「返回值是 Promise」判别:主路径 `buildVNode` await 全部(无占位);运行时首次挂载的 async 组件在 buildVNode 阶段 await(无占位/补全回调);SSR 直接 await
|
|
139
165
|
- 工厂按实例执行;**数据必须走 ctx.data**(缓存+并发合并,重复执行零成本);禁止副作用裸写工厂
|
|
140
|
-
-
|
|
141
|
-
- **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `$` + fetch
|
|
166
|
+
- **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `let` + fetch + `render()`
|
|
142
167
|
|
|
143
168
|
## 6. 类型纪律(编译期防线)
|
|
144
169
|
|
|
145
170
|
```tsx
|
|
146
|
-
const Badge: Component<{ variant: 'primary' | 'muted' }> = () =>
|
|
147
|
-
(props) => h('span', { class: `badge-${props.variant}` }, props.children)
|
|
171
|
+
const Badge: Component<{ variant: 'primary' | 'muted' }> = async () =>
|
|
172
|
+
async (props) => h('span', { class: `badge-${props.variant}` }, props.children)
|
|
148
173
|
|
|
149
174
|
// 负例:variant 传错 → tsc 报错(@ts-expect-error 是类型流测试的写法)
|
|
150
175
|
// @ts-expect-error variant 不允许 'bogus'
|
|
151
176
|
const bad: { variant: 'primary' | 'muted' } = { variant: 'bogus' }
|
|
152
177
|
|
|
153
178
|
// ctx 注入声明(C 泛型):声明了才能用,未声明编译期报错
|
|
154
|
-
const Page: Component<{}, { api: ApiInjected['api'] }> = (_init, ctx) => {
|
|
179
|
+
const Page: Component<{}, { api: ApiInjected['api'] }> = async (_init, ctx) => {
|
|
155
180
|
ctx.api.get('/x')
|
|
156
181
|
return () => null
|
|
157
182
|
}
|
|
@@ -192,8 +217,8 @@ render() // 重渲染,状态保留
|
|
|
192
217
|
新受控组件**必须**用 `ctx.ui.useControlled`(受控判定 + 缺回调 warn 一次 + 非受控内部状态跨渲染保持):
|
|
193
218
|
|
|
194
219
|
```tsx
|
|
195
|
-
const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => void }> = (_init, ctx) => {
|
|
196
|
-
return (props) => {
|
|
220
|
+
const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => void }> = async (_init, ctx) => {
|
|
221
|
+
return async (props) => {
|
|
197
222
|
const ctrl = ctx.ui.useControlled<boolean>({ value: props.active, onChange: props.onChange, name: 'CollapseItem' })
|
|
198
223
|
return h('button', {
|
|
199
224
|
onClick: () => ctrl.setValue(!(ctrl.value ?? false)), // 受控走 onChange;非受控内部状态
|
|
@@ -221,10 +246,10 @@ const CollapseItem: Component<{ active?: boolean; onChange?: (v: boolean) => voi
|
|
|
221
246
|
> SSR 安全(shim 安全默认)+ 测试 mock 单点 + 环境差异隔离。
|
|
222
247
|
|
|
223
248
|
```tsx
|
|
224
|
-
const MyComp: Component = (_init, ctx) => {
|
|
249
|
+
const MyComp: Component = async (_init, ctx) => {
|
|
225
250
|
// mount 层取 browser(ctx.browser 优先,测试/无注入环境 fallback jsdom)
|
|
226
251
|
const browser = ctx.browser ?? createClientBrowser()
|
|
227
|
-
return (props) =>
|
|
252
|
+
return async (props) =>
|
|
228
253
|
h('button', {
|
|
229
254
|
onClick: () => {
|
|
230
255
|
// 复制/查询/存储/滚动——全部经 browser
|
|
@@ -266,9 +291,8 @@ const MyComp: Component = (_init, ctx) => {
|
|
|
266
291
|
|
|
267
292
|
## 已知边界(诚实裁剪)
|
|
268
293
|
|
|
269
|
-
- `usePopup`
|
|
294
|
+
- `usePopup` 是**统一弹窗能力层**:锚定浮层(Tooltip/Popover/Dropdown/Select/AutoComplete/Mentions/Cascader/ContextMenu/NavMenu/Popconfirm/TreeSelect)+ 会话级模态(Modal/Drawer/Confirm——presence/trapFocus/lockScroll/positioning 'none',Escape 语义留组件层)+ mask 模式(Command/Img preview/Tour——mask/maskCentered/自定义 mask VNode)+ focus 触发(DatePicker)+ positioning 'none' 常驻容器(Toast/Notification)——**全部弹窗单一入口**
|
|
270
295
|
- **事件监听纪律**:组件库内部浏览器事件监听**统一走 `ctx.ui.useXXX`**——滚动/观察/弹层/对话框/快捷键/拖拽/DnD 全覆盖:
|
|
271
|
-
`useInView`(InfiniteScroll)、`useScrollPosition`(AiChat/Affix/BackTop/VirtualList)、`usePopupPosition`(Affix 阈值重算)、`usePopup
|
|
272
|
-
-
|
|
273
|
-
- **Select/DatePicker** 是 inline/absolute 菜单(自适宽),不迁移 usePopup——菜单直接挂在锚点下
|
|
296
|
+
`useInView`(InfiniteScroll)、`useScrollPosition`(AiChat/Affix/BackTop/VirtualList)、`usePopupPosition`(Affix 阈值重算)、`usePopup`(弹窗统一——ContextMenu 自由定位 + Modal/Drawer 模态模式 + mask 遮罩)、`useGlobalKey`(Command 快捷键/Img preview Escape)、`useDrag`(Resizable)、`useDragDrop`(FileUpload)、`useControlled`/`useStableRef`(状态/ref)
|
|
297
|
+
- **唯一保留 usePopupPosition 独立使用**:Affix / Chart(坐标工具——非弹窗组合器,滚动跟随自动)
|
|
274
298
|
- `createReactiveState` 已导出:组件外建全局 store(`createReactiveState(() => {})` + `$.__watch(cb)` 订阅)
|