weifuwu 0.55.2 → 0.56.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 +263 -29
- package/dist/client/app.d.ts +3 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +354 -88
- package/dist/client/reactive.d.ts +6 -0
- package/dist/client/render.d.ts +11 -0
- package/dist/client/route-match.d.ts +22 -0
- package/dist/client/types.d.ts +17 -0
- package/dist/client/vnode.d.ts +32 -1
- package/dist/components/Table/Table.d.ts +2 -0
- package/dist/components/ThemeSwitch/ThemeSwitch.d.ts +27 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +136 -19
- package/dist/components/style.css +285 -44
- package/dist/dev/index.js +23 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +320 -7
- package/dist/layout/weifuwu-layout.css +180 -15
- package/dist/ui/html-safe.d.ts +11 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/ssr-page.d.ts +41 -0
- package/dist/ui/ssr.d.ts +35 -0
- package/package.json +7 -4
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 | typeof Portal;
|
|
10
|
+
export type VNodeType = string | Component | AsyncComponent | typeof Fragment | typeof Portal;
|
|
11
11
|
export interface VNode {
|
|
12
12
|
type: VNodeType;
|
|
13
13
|
props: Record<string, any>;
|
|
@@ -23,6 +23,8 @@ export interface VNode {
|
|
|
23
23
|
_render?: (props: any) => VNode | null;
|
|
24
24
|
/** 组件实例 ID(如 '_wf_0') */
|
|
25
25
|
_id?: string;
|
|
26
|
+
/** 自定义组件 ID(ctx.ui.selfId() 注册,跨组件精准刷新) */
|
|
27
|
+
_customId?: string;
|
|
26
28
|
/** 组件输出的 DOM 父节点 */
|
|
27
29
|
_parentNode?: Node;
|
|
28
30
|
/** 组件输出的第一个 DOM 节点 */
|
|
@@ -35,6 +37,35 @@ export interface VNode {
|
|
|
35
37
|
* P = props 类型(JSX 自动推断),C = 组件依赖的 ctx 注入(如 ApiInjected & RouteInjected)
|
|
36
38
|
*/
|
|
37
39
|
export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiContext & C) => ((props: P) => VNode | null) | null;
|
|
40
|
+
/**
|
|
41
|
+
* 异步组件工厂(形态 C):async (ctx) => (initProps, ctx) => (props) => VNode
|
|
42
|
+
*
|
|
43
|
+
* 工厂层(async,只执行一次并缓存):
|
|
44
|
+
* - 数据声明:const data = await ctx.data.get(key, fetcher)
|
|
45
|
+
* - 代码分割:const { default: def } = await import('./view.tsx')
|
|
46
|
+
* - 异步初始化:模块加载、共享资源
|
|
47
|
+
*
|
|
48
|
+
* 返回标准的 Component——mount/render 保持同步,异步只发生在工厂边界。
|
|
49
|
+
* 服务端遍历器与客户端渲染器都 await 工厂;更新路径(_render 缓存)不碰工厂。
|
|
50
|
+
*
|
|
51
|
+
* 必须用 asyncComponent() 包装以标记(渲染器据此区分调用约定):
|
|
52
|
+
* Component → Comp(initProps, ctx)
|
|
53
|
+
* AsyncComponent → await Comp(ctx) → Component
|
|
54
|
+
*/
|
|
55
|
+
export type AsyncComponent<C extends object = {}, P = {}> = (ctx: WfuiContext & C) => Promise<Component<P, C>>;
|
|
56
|
+
/**
|
|
57
|
+
* 包装异步组件工厂。
|
|
58
|
+
*
|
|
59
|
+
* ```tsx
|
|
60
|
+
* const UserProfile = asyncComponent(async (ctx) => {
|
|
61
|
+
* const user = await ctx.data.get(`/api/user/${ctx.params.id}`)
|
|
62
|
+
* return (initProps, ctx) => (props) => h('div', {}, user.name)
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function asyncComponent<C extends object = {}, P = {}>(factory: AsyncComponent<C, P>): AsyncComponent<C, P>;
|
|
67
|
+
/** 判定一个组件类型是否为 async 工厂(asyncComponent 包装过) */
|
|
68
|
+
export declare function isAsyncComponent(type: any): type is AsyncComponent;
|
|
38
69
|
export declare const Fragment: unique symbol;
|
|
39
70
|
/** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
|
|
40
71
|
export declare const Portal: unique symbol;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/components — ThemeSwitch 主题切换器
|
|
3
|
+
*
|
|
4
|
+
* 三段式切换:auto(跟随系统偏好)/ light(强制亮色)/ dark(强制暗色)。
|
|
5
|
+
* 对应 layout 的暗色双段激活机制:
|
|
6
|
+
* - auto → 移除 data-theme,由 @media (prefers-color-scheme) 决定
|
|
7
|
+
* - light → <html data-theme="light">(系统暗色也强制亮色)
|
|
8
|
+
* - dark → <html data-theme="dark">
|
|
9
|
+
*
|
|
10
|
+
* mount 时读取 localStorage 并立即应用(避免闪白);
|
|
11
|
+
* 用户选择后持久化。
|
|
12
|
+
*/
|
|
13
|
+
import type { Component } from '../../client/vnode.ts';
|
|
14
|
+
export type ThemeMode = 'auto' | 'light' | 'dark';
|
|
15
|
+
export interface ThemeSwitchProps {
|
|
16
|
+
/** 初始模式(默认从 localStorage 读取,无记录时为 auto) */
|
|
17
|
+
mode?: ThemeMode;
|
|
18
|
+
/** 切换回调 */
|
|
19
|
+
onChange?: (mode: ThemeMode) => void;
|
|
20
|
+
/** localStorage 存储 key */
|
|
21
|
+
storageKey?: string;
|
|
22
|
+
}
|
|
23
|
+
/** 应用主题:auto 移除属性,light/dark 显式设置 */
|
|
24
|
+
export declare function applyTheme(mode: ThemeMode): void;
|
|
25
|
+
/** 读取当前生效主题(localStorage 优先,其次系统偏好) */
|
|
26
|
+
export declare function getTheme(): ThemeMode;
|
|
27
|
+
export declare const ThemeSwitch: Component<ThemeSwitchProps>;
|
|
@@ -89,3 +89,6 @@ export { Chart } from './Chart/Chart.ts';
|
|
|
89
89
|
export type { ChartProps, ChartType, DataPoint, ChartOptions } from './Chart/Chart.ts';
|
|
90
90
|
export { Editor } from './Editor/Editor.ts';
|
|
91
91
|
export type { EditorProps, ToolbarItem } from './Editor/Editor.ts';
|
|
92
|
+
export { ThemeSwitch } from './ThemeSwitch/ThemeSwitch.ts';
|
|
93
|
+
export type { ThemeSwitchProps, ThemeMode } from './ThemeSwitch/ThemeSwitch.ts';
|
|
94
|
+
export { applyTheme, getTheme } from './ThemeSwitch/ThemeSwitch.ts';
|
package/dist/components/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// src/client/vnode.ts
|
|
2
|
+
var ASYNC_MARK = "__wfAsyncComponent";
|
|
3
|
+
function isAsyncComponent(type) {
|
|
4
|
+
return typeof type === "function" && type?.[ASYNC_MARK] === true;
|
|
5
|
+
}
|
|
2
6
|
var Fragment = /* @__PURE__ */ Symbol("Fragment");
|
|
3
7
|
var Portal = /* @__PURE__ */ Symbol("Portal");
|
|
4
8
|
function h(type, props, ...children) {
|
|
@@ -296,6 +300,7 @@ var Table = (_init, _ctx) => (props) => {
|
|
|
296
300
|
return h("th", {
|
|
297
301
|
class: `wf-table-th${col.sortable ? " wf-table-th--sortable" : ""}${isSorted ? " wf-table-th--sorted" : ""}`,
|
|
298
302
|
scope: "col",
|
|
303
|
+
tabindex: col.sortable ? 0 : void 0,
|
|
299
304
|
style: col.width ? { width: col.width } : void 0,
|
|
300
305
|
onClick: col.sortable && onSort ? () => onSort(col.key, isSorted && isAsc ? "desc" : "asc") : void 0
|
|
301
306
|
}, label);
|
|
@@ -327,7 +332,11 @@ var Table = (_init, _ctx) => (props) => {
|
|
|
327
332
|
});
|
|
328
333
|
}
|
|
329
334
|
const tbody = h("tbody", { class: "wf-table-tbody" }, bodyRows);
|
|
330
|
-
|
|
335
|
+
const table = h("table", {
|
|
336
|
+
class: "wf-table",
|
|
337
|
+
style: props.minWidth ? { minWidth: props.minWidth } : void 0
|
|
338
|
+
}, [thead, tbody]);
|
|
339
|
+
return h("div", { class: "wf-table-wrap" }, table);
|
|
331
340
|
};
|
|
332
341
|
|
|
333
342
|
// src/client/scroll-lock.ts
|
|
@@ -462,10 +471,11 @@ function renderValue(v, ctx) {
|
|
|
462
471
|
}
|
|
463
472
|
if (vnode.type === Fragment) {
|
|
464
473
|
const frag = document.createDocumentFragment();
|
|
465
|
-
|
|
474
|
+
const children = vnode.props?.children == null ? [] : Array.isArray(vnode.props.children) ? vnode.props.children : [vnode.props.children];
|
|
475
|
+
for (const child of children) {
|
|
466
476
|
const node = renderValue(child, ctx);
|
|
467
477
|
if (node != null) frag.appendChild(node);
|
|
468
|
-
}
|
|
478
|
+
}
|
|
469
479
|
return frag;
|
|
470
480
|
}
|
|
471
481
|
if (typeof vnode.type === "function") {
|
|
@@ -508,6 +518,54 @@ function renderValue(v, ctx) {
|
|
|
508
518
|
if (typeof vnode.props?.ref === "function") vnode.props.ref(el);
|
|
509
519
|
return el;
|
|
510
520
|
}
|
|
521
|
+
var asyncFactoryCache = /* @__PURE__ */ new WeakMap();
|
|
522
|
+
function startAsyncFactory(Comp, ctx) {
|
|
523
|
+
const existing = asyncFactoryCache.get(Comp);
|
|
524
|
+
if (existing) return existing;
|
|
525
|
+
const entry = { promise: null };
|
|
526
|
+
entry.promise = Promise.resolve().then(() => Comp(ctx)).then((def) => {
|
|
527
|
+
if (typeof def !== "function") {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`asyncComponent factory <${Comp.name || "anonymous"}> must return a Component (initProps, ctx) => (props) => VNode.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
entry.resolved = def;
|
|
533
|
+
return def;
|
|
534
|
+
});
|
|
535
|
+
asyncFactoryCache.set(Comp, entry);
|
|
536
|
+
return entry;
|
|
537
|
+
}
|
|
538
|
+
function resolveAsyncFactorySync(Comp) {
|
|
539
|
+
return asyncFactoryCache.get(Comp)?.resolved;
|
|
540
|
+
}
|
|
541
|
+
function scheduleFullReRender(ctx) {
|
|
542
|
+
const ui = ctx.ui;
|
|
543
|
+
if (ui && typeof ui.render === "function") ui.render(["_wf_root"]);
|
|
544
|
+
}
|
|
545
|
+
function mountComponent(Comp, props, vnode, ctx) {
|
|
546
|
+
let def;
|
|
547
|
+
if (isAsyncComponent(Comp)) {
|
|
548
|
+
def = resolveAsyncFactorySync(Comp);
|
|
549
|
+
if (!def) {
|
|
550
|
+
void startAsyncFactory(Comp, ctx).promise.then(
|
|
551
|
+
() => scheduleFullReRender(ctx),
|
|
552
|
+
() => {
|
|
553
|
+
}
|
|
554
|
+
);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
def = Comp;
|
|
559
|
+
}
|
|
560
|
+
let childVNode = def(props, ctx);
|
|
561
|
+
if (typeof childVNode !== "function") {
|
|
562
|
+
throw new Error(
|
|
563
|
+
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
vnode._render = childVNode;
|
|
567
|
+
return childVNode(props);
|
|
568
|
+
}
|
|
511
569
|
function renderComponent(Comp, props, vnode, ctx) {
|
|
512
570
|
;
|
|
513
571
|
ctx.ui = ctx.ui ?? {};
|
|
@@ -522,14 +580,7 @@ function renderComponent(Comp, props, vnode, ctx) {
|
|
|
522
580
|
vnode._ctxVersion = childCtx.ui._ctxVersion ?? 0;
|
|
523
581
|
let childVNode;
|
|
524
582
|
try {
|
|
525
|
-
childVNode = Comp
|
|
526
|
-
if (typeof childVNode !== "function") {
|
|
527
|
-
throw new Error(
|
|
528
|
-
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
vnode._render = childVNode;
|
|
532
|
-
childVNode = childVNode(props);
|
|
583
|
+
childVNode = mountComponent(Comp, props, vnode, childCtx);
|
|
533
584
|
} catch (e) {
|
|
534
585
|
const errHandler = ctx.ui?._errorHandler;
|
|
535
586
|
if (errHandler) {
|
|
@@ -586,14 +637,6 @@ function renderPortal(vnode, ctx) {
|
|
|
586
637
|
if (node != null) sub.appendChild(node);
|
|
587
638
|
}
|
|
588
639
|
}
|
|
589
|
-
function forEach(children, fn) {
|
|
590
|
-
if (children == null) return;
|
|
591
|
-
if (Array.isArray(children)) {
|
|
592
|
-
children.forEach(fn);
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
fn(children);
|
|
596
|
-
}
|
|
597
640
|
function flattenChildren(children) {
|
|
598
641
|
if (children == null) return [];
|
|
599
642
|
if (!Array.isArray(children)) return [children];
|
|
@@ -655,6 +698,15 @@ function cleanupPortalChildren(vnode) {
|
|
|
655
698
|
function callRefCleanup(input) {
|
|
656
699
|
if (input == null || typeof input !== "object") return;
|
|
657
700
|
const vnode = input;
|
|
701
|
+
if (vnode._id) {
|
|
702
|
+
if (vnode._customId) idRegistry.delete(vnode._customId);
|
|
703
|
+
idRegistry.delete(vnode._id);
|
|
704
|
+
vnode._id = void 0;
|
|
705
|
+
vnode._customId = void 0;
|
|
706
|
+
vnode._render = void 0;
|
|
707
|
+
vnode._parentNode = void 0;
|
|
708
|
+
vnode._refNode = void 0;
|
|
709
|
+
}
|
|
658
710
|
if (vnode._child != null) {
|
|
659
711
|
if (Array.isArray(vnode._child)) {
|
|
660
712
|
for (const child of vnode._child) {
|
|
@@ -2970,6 +3022,68 @@ var Editor = (_props, ctx) => {
|
|
|
2970
3022
|
]);
|
|
2971
3023
|
};
|
|
2972
3024
|
};
|
|
3025
|
+
|
|
3026
|
+
// src/components/ThemeSwitch/ThemeSwitch.ts
|
|
3027
|
+
var DEFAULT_KEY = "wf_theme";
|
|
3028
|
+
function readStored(key) {
|
|
3029
|
+
try {
|
|
3030
|
+
const v = localStorage.getItem(key);
|
|
3031
|
+
return v === "light" || v === "dark" || v === "auto" ? v : null;
|
|
3032
|
+
} catch {
|
|
3033
|
+
return null;
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
function writeStored(key, mode) {
|
|
3037
|
+
try {
|
|
3038
|
+
localStorage.setItem(key, mode);
|
|
3039
|
+
} catch {
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
function applyTheme(mode) {
|
|
3043
|
+
if (typeof document === "undefined") return;
|
|
3044
|
+
if (mode === "auto") document.documentElement.removeAttribute("data-theme");
|
|
3045
|
+
else document.documentElement.setAttribute("data-theme", mode);
|
|
3046
|
+
}
|
|
3047
|
+
function getTheme() {
|
|
3048
|
+
const stored = readStored(DEFAULT_KEY);
|
|
3049
|
+
if (stored) return stored;
|
|
3050
|
+
return "auto";
|
|
3051
|
+
}
|
|
3052
|
+
var ThemeSwitch = (initProps, ctx) => {
|
|
3053
|
+
const storageKey = initProps.storageKey ?? DEFAULT_KEY;
|
|
3054
|
+
let mode = initProps.mode ?? readStored(storageKey) ?? "auto";
|
|
3055
|
+
applyTheme(mode);
|
|
3056
|
+
return (props) => {
|
|
3057
|
+
const SL = ctx?.i18n?.components?.ThemeSwitch ?? {};
|
|
3058
|
+
const modes = [
|
|
3059
|
+
{ value: "auto", label: SL.auto ?? "\u81EA\u52A8" },
|
|
3060
|
+
{ value: "light", label: SL.light ?? "\u4EAE\u8272" },
|
|
3061
|
+
{ value: "dark", label: SL.dark ?? "\u6697\u8272" }
|
|
3062
|
+
];
|
|
3063
|
+
const segments = modes.map(
|
|
3064
|
+
(m) => h("button", {
|
|
3065
|
+
type: "button",
|
|
3066
|
+
class: `wf-theme-seg${mode === m.value ? " wf-theme-seg--active" : ""}`,
|
|
3067
|
+
role: "radio",
|
|
3068
|
+
"aria-checked": String(mode === m.value),
|
|
3069
|
+
"aria-label": m.label,
|
|
3070
|
+
onClick: () => {
|
|
3071
|
+
if (mode === m.value) return;
|
|
3072
|
+
mode = m.value;
|
|
3073
|
+
applyTheme(mode);
|
|
3074
|
+
writeStored(storageKey, mode);
|
|
3075
|
+
props.onChange?.(mode);
|
|
3076
|
+
ctx.ui.render();
|
|
3077
|
+
}
|
|
3078
|
+
}, m.label)
|
|
3079
|
+
);
|
|
3080
|
+
return h("div", {
|
|
3081
|
+
class: "wf-theme-switch",
|
|
3082
|
+
role: "radiogroup",
|
|
3083
|
+
"aria-label": SL.label ?? "\u4E3B\u9898\u5207\u6362"
|
|
3084
|
+
}, segments);
|
|
3085
|
+
};
|
|
3086
|
+
};
|
|
2973
3087
|
export {
|
|
2974
3088
|
Accordion,
|
|
2975
3089
|
Alert,
|
|
@@ -3011,8 +3125,11 @@ export {
|
|
|
3011
3125
|
Tabs,
|
|
3012
3126
|
Tag,
|
|
3013
3127
|
Textarea,
|
|
3128
|
+
ThemeSwitch,
|
|
3014
3129
|
Toast,
|
|
3015
3130
|
Tooltip,
|
|
3131
|
+
applyTheme,
|
|
3016
3132
|
confirm,
|
|
3133
|
+
getTheme,
|
|
3017
3134
|
toast
|
|
3018
3135
|
};
|