weifuwu 0.38.1 → 0.39.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 +3 -2
- package/dist/client/index.d.ts +3 -1
- package/dist/client/index.js +199 -45
- package/dist/client/locale/en_US.d.ts +15 -0
- package/dist/client/locale/zh_CN.d.ts +15 -0
- package/dist/client/popup.d.ts +19 -0
- package/dist/client/vnode.d.ts +8 -1
- package/dist/components/Chart/Chart.d.ts +18 -0
- package/dist/components/Chart/chart-utils.d.ts +53 -0
- package/dist/components/DatePicker/DatePicker.d.ts +16 -0
- package/dist/components/DatePicker/calendar-utils.d.ts +30 -0
- package/dist/components/InView/InView.d.ts +31 -0
- package/dist/components/Popover/Popover.d.ts +2 -12
- package/dist/components/Tooltip/Tooltip.d.ts +2 -1
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +800 -39
- package/dist/components/style.css +448 -153
- package/dist/layout/weifuwu-layout.css +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ npm install weifuwu
|
|
|
32
32
|
| `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
|
|
33
33
|
| `weifuwu/client` | **confirm** | Promise 化确认对话框 | createApp |
|
|
34
34
|
| `weifuwu/client** | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
|
|
35
|
-
| `weifuwu/components` | **
|
|
35
|
+
| `weifuwu/components` | **38 个组件** | Button/Table/Modal/Toast/... | weifuwu/client |
|
|
36
36
|
| `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 72 个主题 Token | — |
|
|
37
37
|
|
|
38
38
|
---
|
|
@@ -1246,7 +1246,7 @@ import type { RouterOptions } from 'weifuwu/client'
|
|
|
1246
1246
|
|
|
1247
1247
|
# 组件库 (`weifuwu/components`)
|
|
1248
1248
|
|
|
1249
|
-
|
|
1249
|
+
38 个 HTML 原语组件。每个是 `(props, ctx) => VNode` 纯函数,引用 `--wf-*` CSS 变量做主题。
|
|
1250
1250
|
|
|
1251
1251
|
```ts
|
|
1252
1252
|
import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
|
|
@@ -1295,6 +1295,7 @@ import 'weifuwu/components/style.css'
|
|
|
1295
1295
|
| StatCard | `StatCard` | `title`, `value`, `trend`, `icon`, `variant` | 统计卡片 |
|
|
1296
1296
|
| PageHeader | `PageHeader` | `title`, `subtitle`, `actions`, `onBack`, `breadcrumb` | 页面标题 |
|
|
1297
1297
|
| Img | `Img` | `src`, `alt`, `fallback`, `lazy`, `fit` | 图片(含 fallback) |
|
|
1298
|
+
| InView | `InView` | `once`, `threshold`, `rootMargin`, `placeholder`, `onEnter` | 进入视窗后懒加载内容 |
|
|
1298
1299
|
|
|
1299
1300
|
### 数据反馈
|
|
1300
1301
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* extendCtx → ctx 扩展
|
|
16
16
|
* WfuiContext / AppMiddleware / RouteDef → 类型
|
|
17
17
|
*/
|
|
18
|
-
export { h, jsx, jsxs, jsxDEV, Fragment } from './vnode.ts';
|
|
18
|
+
export { h, jsx, jsxs, jsxDEV, Fragment, Portal, createPortal } from './vnode.ts';
|
|
19
19
|
export type { VNode, VNodeType, Component } from './vnode.ts';
|
|
20
20
|
export { createApp } from './app.ts';
|
|
21
21
|
export { router, RouteView } from './router.ts';
|
|
@@ -33,6 +33,8 @@ export { i18n } from './i18n.ts';
|
|
|
33
33
|
export type { I18nOptions, I18nState } from './i18n.ts';
|
|
34
34
|
export { lockScroll, unlockScroll } from './scroll-lock.ts';
|
|
35
35
|
export { trapFocus } from './focus-trap.ts';
|
|
36
|
+
export { computeFixedPos } from './popup.ts';
|
|
37
|
+
export type { FixedPos, Placement } from './popup.ts';
|
|
36
38
|
export { confirm } from './confirm.ts';
|
|
37
39
|
export type { ConfirmOptions, ConfirmState } from './confirm.ts';
|
|
38
40
|
export { zhCN } from './locale/zh_CN.ts';
|
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/client/vnode.ts
|
|
2
2
|
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
3
|
+
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
3
4
|
function jsx(type, props, key) {
|
|
4
5
|
return {
|
|
5
6
|
type,
|
|
@@ -27,17 +28,25 @@ function normalizeProps(props) {
|
|
|
27
28
|
}
|
|
28
29
|
return result;
|
|
29
30
|
}
|
|
31
|
+
function createPortal(children, portalKey) {
|
|
32
|
+
return {
|
|
33
|
+
type: Portal,
|
|
34
|
+
props: { children, portalKey },
|
|
35
|
+
key: portalKey ?? void 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
30
38
|
|
|
31
39
|
// src/client/render.ts
|
|
40
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
41
|
+
var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "line", "rect", "text", "g", "polyline", "polygon", "ellipse", "defs", "use", "clipPath", "mask", "linearGradient", "radialGradient", "stop", "tspan"]);
|
|
42
|
+
var _renderCount = 0;
|
|
32
43
|
var mutationMethods = ["push", "pop", "splice", "shift", "unshift", "sort", "reverse"];
|
|
33
44
|
var wrappedCache = /* @__PURE__ */ new WeakMap();
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (typeof Blob !== "undefined" && val instanceof Blob) return val;
|
|
38
|
-
if (wrappedCache.has(val)) return wrappedCache.get(val);
|
|
39
|
-
const handler = Array.isArray(val) ? {
|
|
45
|
+
function makeProxyHandler(dirty) {
|
|
46
|
+
const isSkip = (k) => typeof k === "string" && k.startsWith("_");
|
|
47
|
+
return {
|
|
40
48
|
get(target, key, receiver) {
|
|
49
|
+
if (isSkip(key)) return Reflect.get(target, key, receiver);
|
|
41
50
|
const v = Reflect.get(target, key, receiver);
|
|
42
51
|
if (typeof key === "string" && mutationMethods.includes(key) && typeof v === "function") {
|
|
43
52
|
return function(...args) {
|
|
@@ -49,37 +58,25 @@ function wrapDeep(val, dirty) {
|
|
|
49
58
|
return wrapDeep(v, dirty);
|
|
50
59
|
},
|
|
51
60
|
set(target, key, v) {
|
|
52
|
-
Reflect.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} : {
|
|
57
|
-
get(_target, key, receiver) {
|
|
58
|
-
const v = Reflect.get(_target, key, receiver);
|
|
59
|
-
return wrapDeep(v, dirty);
|
|
60
|
-
},
|
|
61
|
-
set(target, key, v) {
|
|
62
|
-
Reflect.set(target, key, wrapDeep(v, dirty));
|
|
63
|
-
dirty();
|
|
61
|
+
const old = Reflect.get(target, key);
|
|
62
|
+
if (old === v) return true;
|
|
63
|
+
Reflect.set(target, key, isSkip(key) ? v : wrapDeep(v, dirty));
|
|
64
|
+
if (!isSkip(key)) dirty();
|
|
64
65
|
return true;
|
|
65
66
|
}
|
|
66
67
|
};
|
|
67
|
-
|
|
68
|
+
}
|
|
69
|
+
function wrapDeep(val, dirty) {
|
|
70
|
+
if (val === null || typeof val !== "object") return val;
|
|
71
|
+
if (val instanceof Node) return val;
|
|
72
|
+
if (typeof Blob !== "undefined" && val instanceof Blob) return val;
|
|
73
|
+
if (wrappedCache.has(val)) return wrappedCache.get(val);
|
|
74
|
+
const proxy = new Proxy(val, makeProxyHandler(dirty));
|
|
68
75
|
wrappedCache.set(val, proxy);
|
|
69
76
|
return proxy;
|
|
70
77
|
}
|
|
71
78
|
function createComponentProxy(target, dirty) {
|
|
72
|
-
return new Proxy(target,
|
|
73
|
-
set(t, k, v) {
|
|
74
|
-
if (t[k] === v) return true;
|
|
75
|
-
t[k] = wrapDeep(v, dirty);
|
|
76
|
-
dirty();
|
|
77
|
-
return true;
|
|
78
|
-
},
|
|
79
|
-
get(t, k) {
|
|
80
|
-
return wrapDeep(t[k], dirty);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
79
|
+
return new Proxy(target, makeProxyHandler(dirty));
|
|
83
80
|
}
|
|
84
81
|
function render(input, ctx) {
|
|
85
82
|
return renderValue(input, ctx);
|
|
@@ -89,6 +86,9 @@ function renderValue(v, ctx) {
|
|
|
89
86
|
if (typeof v === "string" || typeof v === "number") return document.createTextNode(String(v));
|
|
90
87
|
if (Array.isArray(v)) return renderArray(v, ctx);
|
|
91
88
|
const vnode = v;
|
|
89
|
+
if (vnode.type === Portal) {
|
|
90
|
+
return renderPortal(vnode, ctx);
|
|
91
|
+
}
|
|
92
92
|
if (vnode.type === Fragment) {
|
|
93
93
|
const frag = document.createDocumentFragment();
|
|
94
94
|
forEach(vnode.props?.children, (child) => frag.appendChild(renderValue(child, ctx)));
|
|
@@ -97,16 +97,27 @@ function renderValue(v, ctx) {
|
|
|
97
97
|
if (typeof vnode.type === "function") {
|
|
98
98
|
return renderComponent(vnode.type, vnode.props, vnode, ctx);
|
|
99
99
|
}
|
|
100
|
-
const
|
|
100
|
+
const tag = vnode.type;
|
|
101
|
+
const el = SVG_TAGS.has(tag) ? document.createElementNS(SVG_NS, tag) : document.createElement(tag);
|
|
101
102
|
vnode.el = el;
|
|
103
|
+
let selectValue;
|
|
102
104
|
for (const [key, value] of Object.entries(vnode.props ?? {})) {
|
|
103
|
-
if (key === "children" || key === "key" || key === "ref") continue;
|
|
105
|
+
if (key === "children" || key === "key" || key === "ref" || key === "value") continue;
|
|
104
106
|
setProp(el, key, value);
|
|
105
107
|
}
|
|
108
|
+
if ("value" in (vnode.props ?? {}) && el instanceof HTMLSelectElement) {
|
|
109
|
+
selectValue = vnode.props.value;
|
|
110
|
+
} else if ("value" in (vnode.props ?? {})) {
|
|
111
|
+
setProp(el, "value", vnode.props.value);
|
|
112
|
+
}
|
|
106
113
|
const flatChildren = flattenChildren(vnode.props?.children);
|
|
107
114
|
for (const child of flatChildren) {
|
|
108
115
|
el.appendChild(renderValue(child, ctx));
|
|
109
116
|
}
|
|
117
|
+
if (selectValue !== void 0) {
|
|
118
|
+
;
|
|
119
|
+
el.value = String(selectValue);
|
|
120
|
+
}
|
|
110
121
|
if (vnode.props?.ref) {
|
|
111
122
|
const result = vnode.props.ref(el);
|
|
112
123
|
if (typeof result === "function") vnode._cleanup = result;
|
|
@@ -119,8 +130,13 @@ function renderComponent(Comp, props, vnode, ctx) {
|
|
|
119
130
|
ctx.ui = ctx.ui ?? {};
|
|
120
131
|
ctx.ui.ready = !!prev$;
|
|
121
132
|
const _target = vnode._$;
|
|
122
|
-
|
|
133
|
+
const _dirtyFn = () => {
|
|
134
|
+
if (_renderCount > 0) return;
|
|
135
|
+
ctx.ui?.dirty?.();
|
|
136
|
+
};
|
|
137
|
+
ctx.ui.$ = createComponentProxy(_target, _dirtyFn);
|
|
123
138
|
let childVNode;
|
|
139
|
+
_renderCount++;
|
|
124
140
|
try {
|
|
125
141
|
childVNode = Comp(props, ctx);
|
|
126
142
|
} catch (e) {
|
|
@@ -132,6 +148,8 @@ function renderComponent(Comp, props, vnode, ctx) {
|
|
|
132
148
|
console.error("Component render error:", e);
|
|
133
149
|
childVNode = null;
|
|
134
150
|
}
|
|
151
|
+
} finally {
|
|
152
|
+
_renderCount--;
|
|
135
153
|
}
|
|
136
154
|
if (childVNode == null) {
|
|
137
155
|
vnode._child = null;
|
|
@@ -145,6 +163,41 @@ function renderArray(arr, ctx) {
|
|
|
145
163
|
for (const item of arr) frag.appendChild(renderValue(item, ctx));
|
|
146
164
|
return frag;
|
|
147
165
|
}
|
|
166
|
+
function ensurePortalContainer() {
|
|
167
|
+
let c = document.getElementById("__wf_portal");
|
|
168
|
+
if (!c) {
|
|
169
|
+
c = document.createElement("div");
|
|
170
|
+
c.id = "__wf_portal";
|
|
171
|
+
c.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:9999";
|
|
172
|
+
document.body.appendChild(c);
|
|
173
|
+
}
|
|
174
|
+
return c;
|
|
175
|
+
}
|
|
176
|
+
function renderPortal(vnode, ctx) {
|
|
177
|
+
const container = ensurePortalContainer();
|
|
178
|
+
const sub = document.createElement("div");
|
|
179
|
+
sub.style.pointerEvents = "auto";
|
|
180
|
+
container.appendChild(sub);
|
|
181
|
+
vnode._portalEl = sub;
|
|
182
|
+
const children = normalize(vnode.props?.children);
|
|
183
|
+
vnode._child = children;
|
|
184
|
+
for (const child of children) {
|
|
185
|
+
sub.appendChild(renderValue(child, ctx));
|
|
186
|
+
}
|
|
187
|
+
const placeholder = document.createTextNode("");
|
|
188
|
+
vnode.el = placeholder;
|
|
189
|
+
return placeholder;
|
|
190
|
+
}
|
|
191
|
+
function patchPortal(_parent, oldNode, oldV, newV, ctx) {
|
|
192
|
+
const sub = oldV._portalEl;
|
|
193
|
+
newV._portalEl = sub;
|
|
194
|
+
if (!sub) return renderPortal(newV, ctx);
|
|
195
|
+
const newChildren = normalize(newV.props?.children);
|
|
196
|
+
const oldChildren = oldV._child || [];
|
|
197
|
+
newV._child = newChildren;
|
|
198
|
+
patchSimpleChildren(sub, oldChildren, newChildren, ctx);
|
|
199
|
+
return oldNode ?? document.createTextNode("");
|
|
200
|
+
}
|
|
148
201
|
function forEach(children, fn) {
|
|
149
202
|
if (children == null) return;
|
|
150
203
|
if (Array.isArray(children)) {
|
|
@@ -168,12 +221,17 @@ function flattenChildren(children) {
|
|
|
168
221
|
}
|
|
169
222
|
function setProp(el, key, value) {
|
|
170
223
|
if (key === "class" || key === "className") {
|
|
171
|
-
el.
|
|
224
|
+
if (el instanceof SVGElement) el.setAttribute("class", String(value ?? ""));
|
|
225
|
+
else el.className = String(value ?? "");
|
|
172
226
|
} else if (key === "style" && typeof value === "object" && value !== null) {
|
|
173
|
-
|
|
227
|
+
const st = el.style;
|
|
228
|
+
for (const sk of Object.keys(value)) {
|
|
229
|
+
const sv = value[sk];
|
|
230
|
+
if (sv != null) st[sk] = typeof sv === "number" ? sv + "px" : String(sv);
|
|
231
|
+
}
|
|
174
232
|
} else if (key.startsWith("on") && typeof value === "function") {
|
|
175
233
|
el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
176
|
-
} else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
|
|
234
|
+
} else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
|
|
177
235
|
;
|
|
178
236
|
el.value = String(value ?? "");
|
|
179
237
|
} else if (value === true) {
|
|
@@ -224,8 +282,18 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
224
282
|
ctx.ui = ctx.ui ?? {};
|
|
225
283
|
ctx.ui.ready = !!newV._$;
|
|
226
284
|
const _tgt = newV._$;
|
|
227
|
-
|
|
228
|
-
|
|
285
|
+
const _dirtyFn2 = () => {
|
|
286
|
+
if (_renderCount > 0) return;
|
|
287
|
+
ctx.ui?.dirty?.();
|
|
288
|
+
};
|
|
289
|
+
ctx.ui.$ = createComponentProxy(_tgt, _dirtyFn2);
|
|
290
|
+
_renderCount++;
|
|
291
|
+
let childNew;
|
|
292
|
+
try {
|
|
293
|
+
childNew = comp(newV.props, ctx);
|
|
294
|
+
} finally {
|
|
295
|
+
_renderCount--;
|
|
296
|
+
}
|
|
229
297
|
newV._child = childNew;
|
|
230
298
|
return patchValue(parent, oldNode, oldV._child, childNew, ctx);
|
|
231
299
|
}
|
|
@@ -237,6 +305,7 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
237
305
|
if (oldNode && oldNode.nodeType === 1) {
|
|
238
306
|
patchProps(oldNode, oldV.props, newV.props);
|
|
239
307
|
patchChildren(oldNode, oldV, newV, ctx);
|
|
308
|
+
if (oldV._cleanup) newV._cleanup = oldV._cleanup;
|
|
240
309
|
} else if (oldNode) {
|
|
241
310
|
callRefCleanup(oldInput);
|
|
242
311
|
const node = renderValue(newInput, ctx);
|
|
@@ -245,6 +314,9 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
245
314
|
}
|
|
246
315
|
return oldNode;
|
|
247
316
|
}
|
|
317
|
+
if (newV.type === Portal) {
|
|
318
|
+
return patchPortal(parent, oldNode, oldV, newV, ctx);
|
|
319
|
+
}
|
|
248
320
|
if (Array.isArray(newInput)) {
|
|
249
321
|
const oldArr = Array.isArray(oldInput) ? oldInput : [];
|
|
250
322
|
patchSimpleChildren(parent, oldArr, newInput, ctx);
|
|
@@ -259,6 +331,7 @@ function typeOf(input) {
|
|
|
259
331
|
const v = input;
|
|
260
332
|
if (typeof v.type === "function") return `fn:${v.type.name || "anon"}`;
|
|
261
333
|
if (v.type === Fragment) return "fragment";
|
|
334
|
+
if (v.type === Portal) return "portal";
|
|
262
335
|
if (typeof v.type === "string") return "tag:" + v.type;
|
|
263
336
|
return "unknown";
|
|
264
337
|
}
|
|
@@ -269,7 +342,7 @@ function patchProps(el, oldProps, newProps) {
|
|
|
269
342
|
if (!newKeys.includes(key)) {
|
|
270
343
|
if (key.startsWith("on") && typeof oldProps[key] === "function") {
|
|
271
344
|
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
|
|
272
|
-
} else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
|
|
345
|
+
} else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
|
|
273
346
|
;
|
|
274
347
|
el.value = "";
|
|
275
348
|
} else {
|
|
@@ -282,14 +355,19 @@ function patchProps(el, oldProps, newProps) {
|
|
|
282
355
|
const newVal = newProps?.[key];
|
|
283
356
|
if (newVal !== oldVal) {
|
|
284
357
|
if (key === "class" || key === "className") {
|
|
285
|
-
el.
|
|
358
|
+
if (el instanceof SVGElement) el.setAttribute("class", String(newVal ?? ""));
|
|
359
|
+
else el.className = String(newVal ?? "");
|
|
286
360
|
} else if (key === "style" && typeof newVal === "object") {
|
|
287
|
-
|
|
361
|
+
const st = el.style;
|
|
362
|
+
for (const sk of Object.keys(newVal)) {
|
|
363
|
+
const sv = newVal[sk];
|
|
364
|
+
if (sv != null) st[sk] = typeof sv === "number" ? sv + "px" : String(sv);
|
|
365
|
+
}
|
|
288
366
|
} else if (key.startsWith("on") && typeof newVal === "function") {
|
|
289
367
|
const eventName = key.slice(2).toLowerCase();
|
|
290
368
|
if (typeof oldVal === "function") el.removeEventListener(eventName, oldVal);
|
|
291
369
|
el.addEventListener(eventName, newVal);
|
|
292
|
-
} else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
|
|
370
|
+
} else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
|
|
293
371
|
;
|
|
294
372
|
el.value = String(newVal ?? "");
|
|
295
373
|
} else if (newVal === true) {
|
|
@@ -297,7 +375,7 @@ function patchProps(el, oldProps, newProps) {
|
|
|
297
375
|
} else if (newVal != null && newVal !== false) {
|
|
298
376
|
el.setAttribute(key, String(newVal));
|
|
299
377
|
} else {
|
|
300
|
-
if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
|
|
378
|
+
if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
|
|
301
379
|
;
|
|
302
380
|
el.value = "";
|
|
303
381
|
} else {
|
|
@@ -410,9 +488,25 @@ function runRefCleanup(vnode) {
|
|
|
410
488
|
}
|
|
411
489
|
});
|
|
412
490
|
}
|
|
491
|
+
function cleanupPortalChildren(vnode) {
|
|
492
|
+
const child = vnode._child;
|
|
493
|
+
if (child == null) return;
|
|
494
|
+
if (Array.isArray(child)) {
|
|
495
|
+
for (const c of child) {
|
|
496
|
+
if (c && typeof c === "object") callRefCleanup(c);
|
|
497
|
+
}
|
|
498
|
+
} else if (typeof child === "object") {
|
|
499
|
+
callRefCleanup(child);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
413
502
|
function callRefCleanup(input) {
|
|
414
503
|
if (input == null || typeof input !== "object") return;
|
|
415
504
|
const vnode = input;
|
|
505
|
+
if (vnode._portalEl) {
|
|
506
|
+
cleanupPortalChildren(vnode);
|
|
507
|
+
vnode._portalEl.remove();
|
|
508
|
+
vnode._portalEl = void 0;
|
|
509
|
+
}
|
|
416
510
|
if (vnode._cleanup) runRefCleanup(vnode);
|
|
417
511
|
}
|
|
418
512
|
|
|
@@ -913,7 +1007,22 @@ var zhCN = {
|
|
|
913
1007
|
Switch: { ariaLabel: "\u5207\u6362" },
|
|
914
1008
|
Breadcrumb: { ariaLabel: "\u9762\u5305\u5C51" },
|
|
915
1009
|
Modal: { ariaLabel: "\u5F39\u7A97" },
|
|
916
|
-
Drawer: { ariaLabel: "\u4FA7\u8FB9\u9762\u677F" }
|
|
1010
|
+
Drawer: { ariaLabel: "\u4FA7\u8FB9\u9762\u677F" },
|
|
1011
|
+
DatePicker: {
|
|
1012
|
+
w0: "\u65E5",
|
|
1013
|
+
w1: "\u4E00",
|
|
1014
|
+
w2: "\u4E8C",
|
|
1015
|
+
w3: "\u4E09",
|
|
1016
|
+
w4: "\u56DB",
|
|
1017
|
+
w5: "\u4E94",
|
|
1018
|
+
w6: "\u516D",
|
|
1019
|
+
hour: "\u65F6",
|
|
1020
|
+
minute: "\u5206",
|
|
1021
|
+
time: "\u65F6\u95F4",
|
|
1022
|
+
confirm: "\u786E\u5B9A",
|
|
1023
|
+
cancel: "\u53D6\u6D88",
|
|
1024
|
+
placeholder: "\u9009\u62E9\u65E5\u671F"
|
|
1025
|
+
}
|
|
917
1026
|
}
|
|
918
1027
|
};
|
|
919
1028
|
|
|
@@ -932,7 +1041,22 @@ var enUS = {
|
|
|
932
1041
|
Switch: { ariaLabel: "Toggle" },
|
|
933
1042
|
Breadcrumb: { ariaLabel: "Breadcrumb" },
|
|
934
1043
|
Modal: { ariaLabel: "Dialog" },
|
|
935
|
-
Drawer: { ariaLabel: "Panel" }
|
|
1044
|
+
Drawer: { ariaLabel: "Panel" },
|
|
1045
|
+
DatePicker: {
|
|
1046
|
+
w0: "Su",
|
|
1047
|
+
w1: "Mo",
|
|
1048
|
+
w2: "Tu",
|
|
1049
|
+
w3: "We",
|
|
1050
|
+
w4: "Th",
|
|
1051
|
+
w5: "Fr",
|
|
1052
|
+
w6: "Sa",
|
|
1053
|
+
hour: "Hour",
|
|
1054
|
+
minute: "Min",
|
|
1055
|
+
time: "Time",
|
|
1056
|
+
confirm: "OK",
|
|
1057
|
+
cancel: "Cancel",
|
|
1058
|
+
placeholder: "Pick a date"
|
|
1059
|
+
}
|
|
936
1060
|
}
|
|
937
1061
|
};
|
|
938
1062
|
|
|
@@ -1059,6 +1183,33 @@ function trapFocus(container) {
|
|
|
1059
1183
|
};
|
|
1060
1184
|
}
|
|
1061
1185
|
|
|
1186
|
+
// src/client/popup.ts
|
|
1187
|
+
function computeFixedPos(el, placement = "bottom", gap = 6, center = true) {
|
|
1188
|
+
const rect = el.getBoundingClientRect();
|
|
1189
|
+
switch (placement) {
|
|
1190
|
+
case "bottom":
|
|
1191
|
+
return {
|
|
1192
|
+
top: rect.bottom + gap,
|
|
1193
|
+
left: center ? rect.left + rect.width / 2 : rect.left
|
|
1194
|
+
};
|
|
1195
|
+
case "top":
|
|
1196
|
+
return {
|
|
1197
|
+
top: rect.top - gap,
|
|
1198
|
+
left: center ? rect.left + rect.width / 2 : rect.left
|
|
1199
|
+
};
|
|
1200
|
+
case "left":
|
|
1201
|
+
return {
|
|
1202
|
+
top: center ? rect.top + rect.height / 2 : rect.top,
|
|
1203
|
+
left: rect.left - gap
|
|
1204
|
+
};
|
|
1205
|
+
case "right":
|
|
1206
|
+
return {
|
|
1207
|
+
top: center ? rect.top + rect.height / 2 : rect.top,
|
|
1208
|
+
left: rect.right + gap
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1062
1213
|
// src/client/confirm.ts
|
|
1063
1214
|
function createConfirmModal(message, options) {
|
|
1064
1215
|
return new Promise((resolve) => {
|
|
@@ -1134,11 +1285,14 @@ export {
|
|
|
1134
1285
|
ApiError,
|
|
1135
1286
|
ErrorBoundary,
|
|
1136
1287
|
Fragment,
|
|
1288
|
+
Portal,
|
|
1137
1289
|
RouteView,
|
|
1138
1290
|
api,
|
|
1139
1291
|
auth,
|
|
1292
|
+
computeFixedPos,
|
|
1140
1293
|
confirm,
|
|
1141
1294
|
createApp,
|
|
1295
|
+
createPortal,
|
|
1142
1296
|
enUS,
|
|
1143
1297
|
extendCtx,
|
|
1144
1298
|
h,
|
|
@@ -30,5 +30,20 @@ export declare const enUS: {
|
|
|
30
30
|
Drawer: {
|
|
31
31
|
ariaLabel: string;
|
|
32
32
|
};
|
|
33
|
+
DatePicker: {
|
|
34
|
+
w0: string;
|
|
35
|
+
w1: string;
|
|
36
|
+
w2: string;
|
|
37
|
+
w3: string;
|
|
38
|
+
w4: string;
|
|
39
|
+
w5: string;
|
|
40
|
+
w6: string;
|
|
41
|
+
hour: string;
|
|
42
|
+
minute: string;
|
|
43
|
+
time: string;
|
|
44
|
+
confirm: string;
|
|
45
|
+
cancel: string;
|
|
46
|
+
placeholder: string;
|
|
47
|
+
};
|
|
33
48
|
};
|
|
34
49
|
};
|
|
@@ -30,5 +30,20 @@ export declare const zhCN: {
|
|
|
30
30
|
Drawer: {
|
|
31
31
|
ariaLabel: string;
|
|
32
32
|
};
|
|
33
|
+
DatePicker: {
|
|
34
|
+
w0: string;
|
|
35
|
+
w1: string;
|
|
36
|
+
w2: string;
|
|
37
|
+
w3: string;
|
|
38
|
+
w4: string;
|
|
39
|
+
w5: string;
|
|
40
|
+
w6: string;
|
|
41
|
+
hour: string;
|
|
42
|
+
minute: string;
|
|
43
|
+
time: string;
|
|
44
|
+
confirm: string;
|
|
45
|
+
cancel: string;
|
|
46
|
+
placeholder: string;
|
|
47
|
+
};
|
|
33
48
|
};
|
|
34
49
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/client — 弹出层定位工具
|
|
3
|
+
*
|
|
4
|
+
* 基于 position: fixed 的坐标计算,替代 CSS absolute 定位。
|
|
5
|
+
* 配合 createPortal 使用,让弹出层不受父级 overflow/transform 影响。
|
|
6
|
+
*/
|
|
7
|
+
export interface FixedPos {
|
|
8
|
+
top: number;
|
|
9
|
+
left: number;
|
|
10
|
+
}
|
|
11
|
+
export type Placement = 'top' | 'bottom' | 'left' | 'right';
|
|
12
|
+
/**
|
|
13
|
+
* 根据触发元素和方向计算弹出层的 fixed 坐标
|
|
14
|
+
* @param el 触发元素
|
|
15
|
+
* @param placement 弹出方向
|
|
16
|
+
* @param gap 间距(px),默认 6
|
|
17
|
+
* @param center 是否居中于触发元素,默认 true
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeFixedPos(el: HTMLElement, placement?: Placement, gap?: number, center?: boolean): FixedPos;
|
package/dist/client/vnode.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* --jsxImportSource=weifuwu/client
|
|
8
8
|
*/
|
|
9
9
|
import type { WfuiContext } from './types.ts';
|
|
10
|
-
export type VNodeType = string | Component | typeof Fragment;
|
|
10
|
+
export type VNodeType = string | Component | typeof Fragment | typeof Portal;
|
|
11
11
|
export interface VNode {
|
|
12
12
|
type: VNodeType;
|
|
13
13
|
props: Record<string, any>;
|
|
@@ -18,9 +18,13 @@ export interface VNode {
|
|
|
18
18
|
_child?: any;
|
|
19
19
|
/** ref 回调返回的清理函数,卸载时由框架调用 */
|
|
20
20
|
_cleanup?: (() => void) | undefined;
|
|
21
|
+
/** Portal 子容器 DOM */
|
|
22
|
+
_portalEl?: HTMLDivElement | undefined;
|
|
21
23
|
}
|
|
22
24
|
export type Component<P = {}> = (props: P, ctx: WfuiContext) => VNode | null;
|
|
23
25
|
export declare const Fragment: unique symbol;
|
|
26
|
+
/** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
|
|
27
|
+
export declare const Portal: unique symbol;
|
|
24
28
|
/** JSX 类型声明 — 使 TypeScript 理解自定义 JSX 运行时 */
|
|
25
29
|
declare global {
|
|
26
30
|
namespace JSX {
|
|
@@ -39,3 +43,6 @@ export declare function h(type: VNodeType, props: Record<string, any> | null, ..
|
|
|
39
43
|
export declare function isNative(vnode: VNode): boolean;
|
|
40
44
|
export declare function isComponent(vnode: VNode): boolean;
|
|
41
45
|
export declare function isFragment(vnode: VNode): boolean;
|
|
46
|
+
export declare function isPortal(vnode: VNode): boolean;
|
|
47
|
+
/** Portal VNode — 子节点渲染到 document.body#__wf_portal 中 */
|
|
48
|
+
export declare function createPortal(children: any, portalKey?: string): VNode;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/components — Chart
|
|
3
|
+
*
|
|
4
|
+
* SVG 图表组件,支持 line / bar / pie / donut 四种模式。
|
|
5
|
+
* 纯函数坐标计算,tooltip 通过 DOM 事件。
|
|
6
|
+
*/
|
|
7
|
+
import type { Component } from '../../client/vnode.ts';
|
|
8
|
+
import type { DataPoint, ChartType, ChartOptions } from './chart-utils.ts';
|
|
9
|
+
export type { DataPoint, ChartType, ChartOptions };
|
|
10
|
+
export interface ChartProps {
|
|
11
|
+
type?: ChartType;
|
|
12
|
+
data: DataPoint[];
|
|
13
|
+
options?: ChartOptions;
|
|
14
|
+
title?: string;
|
|
15
|
+
area?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const Chart: Component<ChartProps>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/components — Chart 纯函数坐标计算
|
|
3
|
+
*
|
|
4
|
+
* 无 DOM 依赖,可测试。
|
|
5
|
+
*/
|
|
6
|
+
export type ChartType = 'line' | 'bar' | 'pie';
|
|
7
|
+
export interface DataPoint {
|
|
8
|
+
label: string;
|
|
9
|
+
value: number;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ChartOptions {
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
padding?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ScaleLinear {
|
|
18
|
+
(v: number): number;
|
|
19
|
+
invert?(v: number): number;
|
|
20
|
+
domain: [number, number];
|
|
21
|
+
range: [number, number];
|
|
22
|
+
}
|
|
23
|
+
export declare function scaleLinear(domain: [number, number], range: [number, number]): ScaleLinear;
|
|
24
|
+
export declare function linePath(data: DataPoint[], xScale: ScaleLinear, yScale: ScaleLinear): string;
|
|
25
|
+
export declare function areaPath(data: DataPoint[], xScale: ScaleLinear, yScale: ScaleLinear, yBase: number): string;
|
|
26
|
+
export interface BarRect {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
color: string;
|
|
32
|
+
label: string;
|
|
33
|
+
value: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function barRects(data: DataPoint[], xScale: ScaleLinear, yScale: ScaleLinear, barWidth?: number): BarRect[];
|
|
36
|
+
export interface Arc {
|
|
37
|
+
d: string;
|
|
38
|
+
color: string;
|
|
39
|
+
label: string;
|
|
40
|
+
value: number;
|
|
41
|
+
centroid: {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function pieArcs(data: DataPoint[], cx: number, cy: number, radius: number): Arc[];
|
|
47
|
+
export declare function getDefaultColor(index: number): string;
|
|
48
|
+
export interface Tick {
|
|
49
|
+
value: number;
|
|
50
|
+
y: number;
|
|
51
|
+
label: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function getYTicks(yScale: ScaleLinear, tickCount?: number): Tick[];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/components — DatePicker
|
|
3
|
+
*
|
|
4
|
+
* 四合一日期选择器,支持 mode: date | datetime | time | range
|
|
5
|
+
* 使用 createPortal + position:fixed 定位弹出层。
|
|
6
|
+
*/
|
|
7
|
+
import type { Component } from '../../client/vnode.ts';
|
|
8
|
+
export type DatePickerMode = 'date' | 'datetime' | 'time' | 'range';
|
|
9
|
+
export interface DatePickerProps {
|
|
10
|
+
mode?: DatePickerMode;
|
|
11
|
+
value?: string;
|
|
12
|
+
onChange?: (value: string) => void;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare const DatePicker: Component<DatePickerProps>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/components — DatePicker 日期计算工具
|
|
3
|
+
*
|
|
4
|
+
* 纯函数,无 DOM 依赖,可测试。
|
|
5
|
+
*/
|
|
6
|
+
export interface CalendarDay {
|
|
7
|
+
year: number;
|
|
8
|
+
month: number;
|
|
9
|
+
day: number;
|
|
10
|
+
isOtherMonth: boolean;
|
|
11
|
+
isToday: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function getWeekdays(): string[];
|
|
14
|
+
export declare function isToday(year: number, month: number, day: number): boolean;
|
|
15
|
+
export declare function getDaysInMonth(year: number, month: number): number;
|
|
16
|
+
export declare function getFirstDayOfMonth(year: number, month: number): number;
|
|
17
|
+
/** 生成一个月历网格(6 行 × 7 列 = 42 格) */
|
|
18
|
+
export declare function getCalendarGrid(year: number, month: number): CalendarDay[][];
|
|
19
|
+
/** 格式化日期 */
|
|
20
|
+
export declare function formatDate(year: number, month: number, day: number): string;
|
|
21
|
+
export declare function formatTime(hour: number, minute: number): string;
|
|
22
|
+
export declare function formatDateTime(year: number, month: number, day: number, hour: number, minute: number): string;
|
|
23
|
+
/** 小时选项(00-23) */
|
|
24
|
+
export declare function hourOptions(): number[];
|
|
25
|
+
/** 分钟选项(00-59,步长 5) */
|
|
26
|
+
export declare function minuteOptions(): number[];
|
|
27
|
+
/** 日期比较:a 是否在 b 之前 */
|
|
28
|
+
export declare function isBefore(a: Date, b: Date): boolean;
|
|
29
|
+
/** 日期比较:a 是否在 b 之后 */
|
|
30
|
+
export declare function isAfter(a: Date, b: Date): boolean;
|