weifuwu 0.38.2 → 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.
@@ -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';
@@ -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 wrapDeep(val, dirty) {
35
- if (val === null || typeof val !== "object") return val;
36
- if (val instanceof Node) return val;
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.set(target, key, wrapDeep(v, dirty));
53
- dirty();
54
- return true;
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
- const proxy = new Proxy(val, handler);
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 el = document.createElement(vnode.type);
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
- ctx.ui.$ = createComponentProxy(_target, () => ctx.ui?.dirty?.());
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.className = String(value ?? "");
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
- Object.assign(el.style, value);
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
- ctx.ui.$ = createComponentProxy(_tgt, () => ctx.ui?.dirty?.());
228
- const childNew = comp(newV.props, ctx);
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
  }
@@ -246,6 +314,9 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
246
314
  }
247
315
  return oldNode;
248
316
  }
317
+ if (newV.type === Portal) {
318
+ return patchPortal(parent, oldNode, oldV, newV, ctx);
319
+ }
249
320
  if (Array.isArray(newInput)) {
250
321
  const oldArr = Array.isArray(oldInput) ? oldInput : [];
251
322
  patchSimpleChildren(parent, oldArr, newInput, ctx);
@@ -260,6 +331,7 @@ function typeOf(input) {
260
331
  const v = input;
261
332
  if (typeof v.type === "function") return `fn:${v.type.name || "anon"}`;
262
333
  if (v.type === Fragment) return "fragment";
334
+ if (v.type === Portal) return "portal";
263
335
  if (typeof v.type === "string") return "tag:" + v.type;
264
336
  return "unknown";
265
337
  }
@@ -270,7 +342,7 @@ function patchProps(el, oldProps, newProps) {
270
342
  if (!newKeys.includes(key)) {
271
343
  if (key.startsWith("on") && typeof oldProps[key] === "function") {
272
344
  el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]);
273
- } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
345
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
274
346
  ;
275
347
  el.value = "";
276
348
  } else {
@@ -283,14 +355,19 @@ function patchProps(el, oldProps, newProps) {
283
355
  const newVal = newProps?.[key];
284
356
  if (newVal !== oldVal) {
285
357
  if (key === "class" || key === "className") {
286
- el.className = String(newVal ?? "");
358
+ if (el instanceof SVGElement) el.setAttribute("class", String(newVal ?? ""));
359
+ else el.className = String(newVal ?? "");
287
360
  } else if (key === "style" && typeof newVal === "object") {
288
- Object.assign(el.style, newVal);
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
+ }
289
366
  } else if (key.startsWith("on") && typeof newVal === "function") {
290
367
  const eventName = key.slice(2).toLowerCase();
291
368
  if (typeof oldVal === "function") el.removeEventListener(eventName, oldVal);
292
369
  el.addEventListener(eventName, newVal);
293
- } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
370
+ } else if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
294
371
  ;
295
372
  el.value = String(newVal ?? "");
296
373
  } else if (newVal === true) {
@@ -298,7 +375,7 @@ function patchProps(el, oldProps, newProps) {
298
375
  } else if (newVal != null && newVal !== false) {
299
376
  el.setAttribute(key, String(newVal));
300
377
  } else {
301
- if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) {
378
+ if (key === "value" && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
302
379
  ;
303
380
  el.value = "";
304
381
  } else {
@@ -411,9 +488,25 @@ function runRefCleanup(vnode) {
411
488
  }
412
489
  });
413
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
+ }
414
502
  function callRefCleanup(input) {
415
503
  if (input == null || typeof input !== "object") return;
416
504
  const vnode = input;
505
+ if (vnode._portalEl) {
506
+ cleanupPortalChildren(vnode);
507
+ vnode._portalEl.remove();
508
+ vnode._portalEl = void 0;
509
+ }
417
510
  if (vnode._cleanup) runRefCleanup(vnode);
418
511
  }
419
512
 
@@ -914,7 +1007,22 @@ var zhCN = {
914
1007
  Switch: { ariaLabel: "\u5207\u6362" },
915
1008
  Breadcrumb: { ariaLabel: "\u9762\u5305\u5C51" },
916
1009
  Modal: { ariaLabel: "\u5F39\u7A97" },
917
- 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
+ }
918
1026
  }
919
1027
  };
920
1028
 
@@ -933,7 +1041,22 @@ var enUS = {
933
1041
  Switch: { ariaLabel: "Toggle" },
934
1042
  Breadcrumb: { ariaLabel: "Breadcrumb" },
935
1043
  Modal: { ariaLabel: "Dialog" },
936
- 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
+ }
937
1060
  }
938
1061
  };
939
1062
 
@@ -1060,6 +1183,33 @@ function trapFocus(container) {
1060
1183
  };
1061
1184
  }
1062
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
+
1063
1213
  // src/client/confirm.ts
1064
1214
  function createConfirmModal(message, options) {
1065
1215
  return new Promise((resolve) => {
@@ -1135,11 +1285,14 @@ export {
1135
1285
  ApiError,
1136
1286
  ErrorBoundary,
1137
1287
  Fragment,
1288
+ Portal,
1138
1289
  RouteView,
1139
1290
  api,
1140
1291
  auth,
1292
+ computeFixedPos,
1141
1293
  confirm,
1142
1294
  createApp,
1295
+ createPortal,
1143
1296
  enUS,
1144
1297
  extendCtx,
1145
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;
@@ -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;
@@ -1,19 +1,9 @@
1
1
  /**
2
2
  * weifuwu/components — Popover
3
- *
4
- * 通用弹出层组件。触发点击后在目标元素附近弹出浮动面板。
5
- * 点击面板外部关闭。
6
- *
7
- * 触发方式:
8
- * 'click' — 点击触发元素切换显示
9
- * 'hover' — 悬停触发元素显示,移出关闭
10
- *
11
- * 受控模式:
12
- * 传入 open + onOpenChange 可外部控制显示状态
13
- * (Dropdown 使用此模式)
14
3
  */
15
4
  import type { Component } from '../../client/vnode.ts';
16
- export type PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
5
+ import type { Placement } from '../../client/popup.ts';
6
+ export type PopoverPosition = Placement;
17
7
  export interface PopoverProps {
18
8
  content?: any;
19
9
  trigger?: 'click' | 'hover';
@@ -1,5 +1,6 @@
1
1
  import type { Component } from '../../client/vnode.ts';
2
- export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
2
+ import type { Placement } from '../../client/popup.ts';
3
+ export type TooltipPosition = Placement;
3
4
  export interface TooltipProps {
4
5
  content: string;
5
6
  position?: TooltipPosition;