weifuwu 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ npm install weifuwu
23
23
  | **ui** | `weifuwu` | SSR 渲染 + 动态 JS/CSS 编译 → `ctx.ui` | `Router` |
24
24
  | **graphql** | `weifuwu` | GraphQL 端点 | `Router` |
25
25
  | **client** | `weifuwu/client` | 前端 VDOM + Proxy 框架 + i18n + ErrorBoundary | — |
26
- | **components** | `weifuwu/components` | 34 个 HTML 原语组件(Button/Table/Modal/...) | `client` |
26
+ | **components** | `weifuwu/components` | 36 个 HTML 原语组件(Button/Table/Modal/...) | `client` |
27
27
  | **layout** | `weifuwu/layout` | 纯 CSS 布局原语 + 主题 Token | — |
28
28
 
29
29
  ---
@@ -526,6 +526,51 @@ ctx.i18n?.setLocale('en-US') // → 自动触发重渲染
526
526
  import { i18n, zhCN, enUS } from 'weifuwu/client'
527
527
  ```
528
528
 
529
+ ### confirm —— 确认对话框
530
+
531
+ ```ts
532
+ import { createApp, confirm } from 'weifuwu/client'
533
+
534
+ createApp()
535
+ .use(confirm())
536
+ .mount('#root', App)
537
+
538
+ // 在组件中使用
539
+ async function handleDelete(ctx: WfuiContext) {
540
+ const ok = await ctx.confirm?.('确定删除?', {
541
+ title: '确认',
542
+ confirmText: '删除',
543
+ cancelText: '取消',
544
+ variant: 'danger', // 'default' | 'danger'
545
+ })
546
+ if (ok) { /* 执行 */ }
547
+ }
548
+ ```
549
+
550
+ - 直接 DOM 渲染(不依赖 VDOM)
551
+ - 返回 `Promise<boolean>` — `await ctx.confirm(msg, opts?)`
552
+ - 自动锁定背景滚动(ScrollLock)
553
+ - ESC / 点击遮罩关闭
554
+
555
+ ### ScrollLock / FocusTrap 工具
556
+
557
+ ```ts
558
+ import { lockScroll, unlockScroll } from 'weifuwu/client'
559
+ import { trapFocus } from 'weifuwu/client'
560
+
561
+ // 锁定/解锁滚动(支持嵌套计数)
562
+ lockScroll()
563
+ unlockScroll()
564
+
565
+ // 焦点陷阱,返回 cleanup 函数
566
+ const cleanup = trapFocus(containerEl)
567
+ // 组件卸载时
568
+ cleanup() // 恢复焦点
569
+ ```
570
+
571
+ - `lockScroll()`: 多层级可嵌套锁定,iOS Safari `position:fixed` 兼容
572
+ - `trapFocus(el)`: Tab/Shift+Tab 在容器内循环,restore 之前焦点
573
+
529
574
  ### 前端类型
530
575
 
531
576
  `VNode`, `VNodeType`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `ApiOptions`, `ApiRequestOptions`, `ApiError`, `AuthClient`, `AuthOptions`, `ErrorBoundaryProps`, `I18nOptions`, `I18nState`, `LocalePackage`
@@ -645,7 +690,7 @@ document.documentElement.setAttribute('data-theme', 'dark')
645
690
 
646
691
  ## 组件库 — `weifuwu/components`
647
692
 
648
- 29 个 **HTML 原语**,覆盖 90% 的 SaaS 页面 HTML 需求。每个组件是 `(props, ctx) => VNode` 纯函数,引用 `weifuwu/layout` 的 CSS 变量做主题。
693
+ 36 个 **HTML 原语**,覆盖 90% 的 SaaS 页面 HTML 需求。每个组件是 `(props, ctx) => VNode` 纯函数,引用 `weifuwu/layout` 的 CSS 变量做主题。
649
694
 
650
695
  ```ts
651
696
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
@@ -657,11 +702,10 @@ import 'weifuwu/components/style.css'
657
702
  | 类别 | 组件 | 用途 |
658
703
  |------|------|------|
659
704
  | **表单核心** | `Button` `Input` `Textarea` `Select` | 4 个最常用的表单元素 |
660
- | **表单核心** | `InputNumber` | 数字输入,带自定义步进按钮 (`showStepper`) |
661
705
  | **表单选择** | `Checkbox` `Switch` `RadioGroup` `Slider` | 选择类输入 |
662
706
  | **表单增强** | `Form` `Field` `FileUpload` `SearchInput` `ProgressBar` | 文件上传、搜索、进度 |
663
- | **数据展示** | `Table` `Card` `Badge` `Tag` `Avatar` `StatCard` `PageHeader` | 数据展示与页面标题 |
664
- | **数据反馈** | `Modal` `Drawer` `Tooltip` `Toast` `Alert` `Loading` `EmptyState` | 弹窗、抽屉、提示 |
707
+ | **数据展示** | `Table` `Card` `Badge` `Tag` `Avatar` `StatCard` `PageHeader` `Img` | 数据展示与页面标题 |
708
+ | **数据反馈** | `Modal` `Drawer` `Tooltip` `Popover` `Toast` `Alert` `Loading` `EmptyState` `Skeleton` | 弹窗、抽屉、弹出层、骨架屏 |
665
709
  | **导航组件** | `Breadcrumb` `Tabs` `Dropdown` `Pagination` `Steps` `Accordion` | 面包屑、标签页、分页 |
666
710
  | **布局** | `Divider` | 分割线 (水平/垂直/带文字) |
667
711
 
@@ -0,0 +1,26 @@
1
+ /**
2
+ * weifuwu/client — Confirm 对话框
3
+ *
4
+ * 使用:
5
+ * import { createApp, confirm } from 'weifuwu/client'
6
+ *
7
+ * createApp()
8
+ * .use(confirm())
9
+ * .mount('#root', () => <App />)
10
+ *
11
+ * // 页面中使用
12
+ * if (await ctx.confirm?.('确定删除?')) {
13
+ * // 执行删除
14
+ * }
15
+ */
16
+ import type { AppMiddleware } from './types.ts';
17
+ export interface ConfirmOptions {
18
+ title?: string;
19
+ confirmText?: string;
20
+ cancelText?: string;
21
+ variant?: 'primary' | 'danger';
22
+ }
23
+ export interface ConfirmState {
24
+ confirm: (message: string, options?: ConfirmOptions) => Promise<boolean>;
25
+ }
26
+ export declare function confirm(): AppMiddleware;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * weifuwu/client — FocusTrap
3
+ */
4
+ export declare function trapFocus(container: HTMLElement): () => void;
@@ -31,5 +31,9 @@ export { ErrorBoundary } from './error-boundary.ts';
31
31
  export type { ErrorBoundaryProps } from './error-boundary.ts';
32
32
  export { i18n } from './i18n.ts';
33
33
  export type { I18nOptions, I18nState } from './i18n.ts';
34
+ export { lockScroll, unlockScroll } from './scroll-lock.ts';
35
+ export { trapFocus } from './focus-trap.ts';
36
+ export { confirm } from './confirm.ts';
37
+ export type { ConfirmOptions, ConfirmState } from './confirm.ts';
34
38
  export { zhCN } from './locale/zh_CN.ts';
35
39
  export { enUS } from './locale/en_US.ts';
@@ -71,6 +71,7 @@ function wrapDeep(val, dirty) {
71
71
  function createComponentProxy(target, dirty) {
72
72
  return new Proxy(target, {
73
73
  set(t, k, v) {
74
+ if (t[k] === v) return true;
74
75
  t[k] = wrapDeep(v, dirty);
75
76
  dirty();
76
77
  return true;
@@ -334,6 +335,14 @@ function normalize(children) {
334
335
  return result;
335
336
  }
336
337
  function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
338
+ for (let i = oldChildren.length - 1; i >= newChildren.length; i--) {
339
+ const oldChild = oldChildren[i];
340
+ const node = parent.childNodes[i];
341
+ if (node) {
342
+ callRefCleanup(oldChild);
343
+ node.remove();
344
+ }
345
+ }
337
346
  const max = Math.max(oldChildren.length, newChildren.length);
338
347
  for (let i = 0; i < max; i++) {
339
348
  const oldChild = oldChildren[i];
@@ -342,12 +351,7 @@ function patchSimpleChildren(parent, oldChildren, newChildren, ctx) {
342
351
  if (oldChild === void 0 && newChild !== void 0) {
343
352
  const node = renderValue(newChild, ctx);
344
353
  parent.appendChild(node);
345
- } else if (newChild === void 0) {
346
- if (existingNode) {
347
- callRefCleanup(oldChild);
348
- existingNode.remove();
349
- }
350
- } else {
354
+ } else if (oldChild !== void 0 && newChild !== void 0) {
351
355
  patchValue(parent, existingNode, oldChild, newChild, ctx);
352
356
  }
353
357
  }
@@ -909,8 +913,7 @@ var zhCN = {
909
913
  Switch: { ariaLabel: "\u5207\u6362" },
910
914
  Breadcrumb: { ariaLabel: "\u9762\u5305\u5C51" },
911
915
  Modal: { ariaLabel: "\u5F39\u7A97" },
912
- Drawer: { ariaLabel: "\u4FA7\u8FB9\u9762\u677F" },
913
- InputNumber: { increase: "\u589E\u52A0", decrease: "\u51CF\u5C11" }
916
+ Drawer: { ariaLabel: "\u4FA7\u8FB9\u9762\u677F" }
914
917
  }
915
918
  };
916
919
 
@@ -929,8 +932,7 @@ var enUS = {
929
932
  Switch: { ariaLabel: "Toggle" },
930
933
  Breadcrumb: { ariaLabel: "Breadcrumb" },
931
934
  Modal: { ariaLabel: "Dialog" },
932
- Drawer: { ariaLabel: "Panel" },
933
- InputNumber: { increase: "Increase", decrease: "Decrease" }
935
+ Drawer: { ariaLabel: "Panel" }
934
936
  }
935
937
  };
936
938
 
@@ -989,6 +991,145 @@ function deepMerge(a, b) {
989
991
  }
990
992
  return result;
991
993
  }
994
+
995
+ // src/client/scroll-lock.ts
996
+ var lockedCount = 0;
997
+ var originalOverflow = "";
998
+ var originalPosition = "";
999
+ var originalTop = "";
1000
+ var originalWidth = "";
1001
+ var scrollY = 0;
1002
+ function canLock() {
1003
+ return typeof window !== "undefined" && typeof document !== "undefined";
1004
+ }
1005
+ function lockScroll() {
1006
+ lockedCount++;
1007
+ if (lockedCount > 1) return;
1008
+ if (!canLock()) return;
1009
+ scrollY = window.scrollY;
1010
+ const body = document.body;
1011
+ originalOverflow = body.style.overflow;
1012
+ originalPosition = body.style.position;
1013
+ originalTop = body.style.top;
1014
+ originalWidth = body.style.width;
1015
+ body.style.overflow = "hidden";
1016
+ const isIOS = /iPhone|iPad|iPod/.test(navigator.platform) || /Mac/.test(navigator.platform) && "ontouchend" in document;
1017
+ if (isIOS) {
1018
+ body.style.position = "fixed";
1019
+ body.style.top = `-${scrollY}px`;
1020
+ body.style.width = "100%";
1021
+ }
1022
+ }
1023
+ function unlockScroll() {
1024
+ lockedCount--;
1025
+ if (lockedCount > 0) return;
1026
+ if (!canLock()) return;
1027
+ const body = document.body;
1028
+ body.style.overflow = originalOverflow;
1029
+ body.style.position = originalPosition;
1030
+ body.style.top = originalTop;
1031
+ body.style.width = originalWidth;
1032
+ if (scrollY > 0) window.scrollTo(0, scrollY);
1033
+ }
1034
+
1035
+ // src/client/focus-trap.ts
1036
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
1037
+ function trapFocus(container) {
1038
+ const focusable = container.querySelectorAll(FOCUSABLE);
1039
+ if (focusable.length === 0) return () => {
1040
+ };
1041
+ const first = focusable[0];
1042
+ const last = focusable[focusable.length - 1];
1043
+ const handler = (e) => {
1044
+ if (e.key !== "Tab") return;
1045
+ if (e.shiftKey && document.activeElement === first) {
1046
+ e.preventDefault();
1047
+ last.focus();
1048
+ } else if (!e.shiftKey && document.activeElement === last) {
1049
+ e.preventDefault();
1050
+ first.focus();
1051
+ }
1052
+ };
1053
+ const prevFocused = document.activeElement;
1054
+ first.focus();
1055
+ container.addEventListener("keydown", handler);
1056
+ return () => {
1057
+ container.removeEventListener("keydown", handler);
1058
+ prevFocused?.focus();
1059
+ };
1060
+ }
1061
+
1062
+ // src/client/confirm.ts
1063
+ function createConfirmModal(message, options) {
1064
+ return new Promise((resolve) => {
1065
+ const {
1066
+ title = "\u786E\u8BA4\u64CD\u4F5C",
1067
+ confirmText = "\u786E\u5B9A",
1068
+ cancelText = "\u53D6\u6D88",
1069
+ variant = "primary"
1070
+ } = options;
1071
+ const overlay = document.createElement("div");
1072
+ overlay.className = "wf-modal";
1073
+ overlay.style.cssText = "position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center";
1074
+ const bg = document.createElement("div");
1075
+ bg.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:-1";
1076
+ const box = document.createElement("div");
1077
+ box.style.cssText = `background:var(--wf-color-bg,#fff);border-radius:var(--wf-radius-md,8px);box-shadow:var(--wf-shadow-lg,0 4px 24px rgba(0,0,0,0.12));min-width:360px;max-width:90vw;z-index:1`;
1078
+ if (title) {
1079
+ const header = document.createElement("div");
1080
+ header.style.cssText = "display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--wf-color-border,#e5e7eb);font-family:var(--wf-font-sans);font-size:var(--wf-font-size-lg);font-weight:var(--wf-font-weight-semibold);color:var(--wf-color-text);line-height:1.4";
1081
+ header.textContent = title;
1082
+ box.appendChild(header);
1083
+ }
1084
+ const body = document.createElement("div");
1085
+ body.style.cssText = "padding:20px;font-family:var(--wf-font-sans);font-size:var(--wf-font-size-sm);color:var(--wf-color-text);line-height:var(--wf-line-height-normal)";
1086
+ body.textContent = message;
1087
+ box.appendChild(body);
1088
+ const footer = document.createElement("div");
1089
+ footer.style.cssText = "display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--wf-color-border,#e5e7eb)";
1090
+ const cancelBtn = document.createElement("button");
1091
+ cancelBtn.className = "wf-btn wf-btn--secondary wf-btn--md";
1092
+ cancelBtn.textContent = cancelText;
1093
+ const confirmBtn = document.createElement("button");
1094
+ confirmBtn.className = `wf-btn wf-btn--${variant} wf-btn--md`;
1095
+ confirmBtn.textContent = confirmText;
1096
+ footer.appendChild(cancelBtn);
1097
+ footer.appendChild(confirmBtn);
1098
+ box.appendChild(footer);
1099
+ overlay.appendChild(bg);
1100
+ overlay.appendChild(box);
1101
+ document.body.appendChild(overlay);
1102
+ lockScroll();
1103
+ const close = (result) => {
1104
+ unlockScroll();
1105
+ overlay.remove();
1106
+ resolve(result);
1107
+ };
1108
+ const onKeyDown = (e) => {
1109
+ if (e.key === "Escape") close(false);
1110
+ };
1111
+ document.addEventListener("keydown", onKeyDown);
1112
+ cancelBtn.onclick = () => {
1113
+ document.removeEventListener("keydown", onKeyDown);
1114
+ close(false);
1115
+ };
1116
+ confirmBtn.onclick = () => {
1117
+ document.removeEventListener("keydown", onKeyDown);
1118
+ close(true);
1119
+ };
1120
+ bg.onclick = () => {
1121
+ document.removeEventListener("keydown", onKeyDown);
1122
+ close(false);
1123
+ };
1124
+ });
1125
+ }
1126
+ function confirm() {
1127
+ return (ctx) => {
1128
+ ;
1129
+ ctx.confirm = (message, options) => createConfirmModal(message, options ?? {});
1130
+ return ctx;
1131
+ };
1132
+ }
992
1133
  export {
993
1134
  ApiError,
994
1135
  ErrorBoundary,
@@ -996,6 +1137,7 @@ export {
996
1137
  RouteView,
997
1138
  api,
998
1139
  auth,
1140
+ confirm,
999
1141
  createApp,
1000
1142
  enUS,
1001
1143
  extendCtx,
@@ -1004,7 +1146,10 @@ export {
1004
1146
  jsx,
1005
1147
  jsxDEV,
1006
1148
  jsxs,
1149
+ lockScroll,
1007
1150
  router,
1151
+ trapFocus,
1152
+ unlockScroll,
1008
1153
  ws,
1009
1154
  zhCN
1010
1155
  };
@@ -30,9 +30,5 @@ export declare const enUS: {
30
30
  Drawer: {
31
31
  ariaLabel: string;
32
32
  };
33
- InputNumber: {
34
- increase: string;
35
- decrease: string;
36
- };
37
33
  };
38
34
  };
@@ -30,9 +30,5 @@ export declare const zhCN: {
30
30
  Drawer: {
31
31
  ariaLabel: string;
32
32
  };
33
- InputNumber: {
34
- increase: string;
35
- decrease: string;
36
- };
37
33
  };
38
34
  };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * weifuwu/client — ScrollLock
3
+ */
4
+ export declare function lockScroll(): void;
5
+ export declare function unlockScroll(): void;
@@ -1,3 +1,6 @@
1
+ /**
2
+ * weifuwu/components — Drawer
3
+ */
1
4
  import type { Component } from '../../client/vnode.ts';
2
5
  export type DrawerPosition = 'left' | 'right';
3
6
  export interface DrawerProps {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * weifuwu/components — Img
3
+ *
4
+ * `<img>` 增强组件。支持 fallback、loading="lazy"。
5
+ */
6
+ import type { Component } from '../../client/vnode.ts';
7
+ export interface ImgProps {
8
+ src?: string;
9
+ alt?: string;
10
+ fallback?: string;
11
+ loading?: 'lazy' | 'eager';
12
+ width?: number | string;
13
+ height?: number | string;
14
+ className?: string;
15
+ style?: Record<string, string>;
16
+ }
17
+ export declare const Img: Component<ImgProps>;
@@ -1,14 +1,13 @@
1
1
  import type { Component } from '../../client/vnode.ts';
2
2
  export interface InputProps {
3
3
  label?: string;
4
- type?: 'text' | 'email' | 'password' | 'number' | 'url';
4
+ type?: 'text' | 'email' | 'password' | 'number' | 'url' | 'date' | 'tel' | 'time' | 'color';
5
5
  value?: string;
6
6
  placeholder?: string;
7
7
  required?: boolean;
8
8
  disabled?: boolean;
9
9
  error?: string;
10
10
  hint?: string;
11
- showStepper?: boolean;
12
11
  onInput?: (e: Event) => void;
13
12
  onChange?: (e: Event) => void;
14
13
  }
@@ -1,3 +1,6 @@
1
+ /**
2
+ * weifuwu/components — Modal
3
+ */
1
4
  import type { Component } from '../../client/vnode.ts';
2
5
  export interface ModalProps {
3
6
  open?: boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * weifuwu/components — Popover
3
+ *
4
+ * 通用弹出层组件。触发点击后在目标元素附近弹出浮动面板。
5
+ * 点击面板外部关闭。
6
+ *
7
+ * 触发方式:
8
+ * 'click' — 点击触发元素切换显示
9
+ * 'hover' — 悬停触发元素显示,移出关闭
10
+ *
11
+ * 受控模式:
12
+ * 传入 open + onOpenChange 可外部控制显示状态
13
+ * (Dropdown 使用此模式)
14
+ */
15
+ import type { Component } from '../../client/vnode.ts';
16
+ export type PopoverPosition = 'top' | 'bottom' | 'left' | 'right';
17
+ export interface PopoverProps {
18
+ content?: any;
19
+ trigger?: 'click' | 'hover';
20
+ position?: PopoverPosition;
21
+ open?: boolean;
22
+ onOpenChange?: (open: boolean) => void;
23
+ disabled?: boolean;
24
+ children?: any;
25
+ }
26
+ export declare const Popover: Component<PopoverProps>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * weifuwu/components — Skeleton
3
+ *
4
+ * 骨架屏占位组件。纯 CSS,无交互,无状态。
5
+ *
6
+ * 用法:
7
+ * <Skeleton /> ← 单行文本
8
+ * <Skeleton lines={3} /> ← 多行段落
9
+ * <Skeleton variant="circle" /> ← 圆形头像
10
+ * <Skeleton variant="rect" w={200} h={120} /> ← 矩形区域
11
+ */
12
+ import type { Component } from '../../client/vnode.ts';
13
+ export type SkeletonVariant = 'text' | 'circle' | 'rect';
14
+ export interface SkeletonProps {
15
+ variant?: SkeletonVariant;
16
+ lines?: number;
17
+ width?: number | string;
18
+ height?: number | string;
19
+ className?: string;
20
+ }
21
+ export declare const Skeleton: Component<SkeletonProps>;
@@ -73,3 +73,9 @@ export { Tooltip } from './Tooltip/Tooltip.ts';
73
73
  export type { TooltipProps, TooltipPosition } from './Tooltip/Tooltip.ts';
74
74
  export { Drawer } from './Drawer/Drawer.ts';
75
75
  export type { DrawerProps, DrawerPosition } from './Drawer/Drawer.ts';
76
+ export { Popover } from './Popover/Popover.ts';
77
+ export type { PopoverProps, PopoverPosition } from './Popover/Popover.ts';
78
+ export { Skeleton } from './Skeleton/Skeleton.ts';
79
+ export type { SkeletonProps, SkeletonVariant } from './Skeleton/Skeleton.ts';
80
+ export { Img } from './Img/Img.ts';
81
+ export type { ImgProps } from './Img/Img.ts';
@@ -1,4 +1,5 @@
1
1
  // src/client/vnode.ts
2
+ var Fragment = /* @__PURE__ */ Symbol("Fragment");
2
3
  function h(type, props, ...children) {
3
4
  const p = normalizeProps(props ?? {});
4
5
  if (children.length > 0) {
@@ -37,55 +38,9 @@ var Button = (props, ctx) => {
37
38
  };
38
39
 
39
40
  // src/components/Input/Input.ts
40
- var Input = (props, ctx) => {
41
- const { label, type = "text", value, placeholder, required, disabled, error, hint, showStepper, onInput, onChange } = props;
42
- const NL = type === "number" && showStepper ? ctx?.i18n?.components?.InputNumber ?? {} : {};
43
- const inputEl = type === "number" && showStepper ? h("div", { class: "wf-input-number-wrap" }, [
44
- h("input", {
45
- class: "wf-input",
46
- type,
47
- value: value ?? "",
48
- placeholder,
49
- required: required || void 0,
50
- disabled: disabled || void 0,
51
- onInput,
52
- onChange
53
- }),
54
- h("div", { class: "wf-input-number-stepper" }, [
55
- h("button", {
56
- class: "wf-input-number-step",
57
- "aria-label": NL.increase ?? "\u589E\u52A0",
58
- disabled: disabled || void 0,
59
- onClick: (e) => {
60
- const input = e.currentTarget.parentElement.previousElementSibling;
61
- if (input) {
62
- const step = parseFloat(input.getAttribute("step") || "1");
63
- const cur = parseFloat(input.value) || 0;
64
- input.value = String(cur + step);
65
- input.dispatchEvent(new Event("input", { bubbles: true }));
66
- }
67
- }
68
- }, "\u25B2"),
69
- h("button", {
70
- class: "wf-input-number-step",
71
- "aria-label": NL.decrease ?? "\u51CF\u5C11",
72
- disabled: disabled || void 0,
73
- onClick: (e) => {
74
- const input = e.currentTarget.parentElement.previousElementSibling;
75
- if (input) {
76
- const step = parseFloat(input.getAttribute("step") || "1");
77
- const cur = parseFloat(input.value) || 0;
78
- const min = parseFloat(input.getAttribute("min") || String(-Infinity));
79
- const val = cur - step;
80
- if (val >= min || isNaN(min)) {
81
- input.value = String(val);
82
- input.dispatchEvent(new Event("input", { bubbles: true }));
83
- }
84
- }
85
- }
86
- }, "\u25BC")
87
- ])
88
- ]) : h("input", {
41
+ var Input = (props) => {
42
+ const { label, type = "text", value, placeholder, required, disabled, error, hint, onInput, onChange } = props;
43
+ const inputEl = h("input", {
89
44
  class: "wf-input",
90
45
  type,
91
46
  value: value ?? "",
@@ -258,20 +213,113 @@ var Table = (props, _ctx) => {
258
213
  return h("table", { class: "wf-table" }, [thead, tbody]);
259
214
  };
260
215
 
216
+ // src/client/scroll-lock.ts
217
+ var lockedCount = 0;
218
+ var originalOverflow = "";
219
+ var originalPosition = "";
220
+ var originalTop = "";
221
+ var originalWidth = "";
222
+ var scrollY = 0;
223
+ function canLock() {
224
+ return typeof window !== "undefined" && typeof document !== "undefined";
225
+ }
226
+ function lockScroll() {
227
+ lockedCount++;
228
+ if (lockedCount > 1) return;
229
+ if (!canLock()) return;
230
+ scrollY = window.scrollY;
231
+ const body = document.body;
232
+ originalOverflow = body.style.overflow;
233
+ originalPosition = body.style.position;
234
+ originalTop = body.style.top;
235
+ originalWidth = body.style.width;
236
+ body.style.overflow = "hidden";
237
+ const isIOS = /iPhone|iPad|iPod/.test(navigator.platform) || /Mac/.test(navigator.platform) && "ontouchend" in document;
238
+ if (isIOS) {
239
+ body.style.position = "fixed";
240
+ body.style.top = `-${scrollY}px`;
241
+ body.style.width = "100%";
242
+ }
243
+ }
244
+ function unlockScroll() {
245
+ lockedCount--;
246
+ if (lockedCount > 0) return;
247
+ if (!canLock()) return;
248
+ const body = document.body;
249
+ body.style.overflow = originalOverflow;
250
+ body.style.position = originalPosition;
251
+ body.style.top = originalTop;
252
+ body.style.width = originalWidth;
253
+ if (scrollY > 0) window.scrollTo(0, scrollY);
254
+ }
255
+
256
+ // src/client/focus-trap.ts
257
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
258
+ function trapFocus(container) {
259
+ const focusable = container.querySelectorAll(FOCUSABLE);
260
+ if (focusable.length === 0) return () => {
261
+ };
262
+ const first = focusable[0];
263
+ const last = focusable[focusable.length - 1];
264
+ const handler = (e) => {
265
+ if (e.key !== "Tab") return;
266
+ if (e.shiftKey && document.activeElement === first) {
267
+ e.preventDefault();
268
+ last.focus();
269
+ } else if (!e.shiftKey && document.activeElement === last) {
270
+ e.preventDefault();
271
+ first.focus();
272
+ }
273
+ };
274
+ const prevFocused = document.activeElement;
275
+ first.focus();
276
+ container.addEventListener("keydown", handler);
277
+ return () => {
278
+ container.removeEventListener("keydown", handler);
279
+ prevFocused?.focus();
280
+ };
281
+ }
282
+
261
283
  // src/components/Modal/Modal.ts
262
284
  var Modal = (props, ctx) => {
263
285
  const { open, title, onClose, children, footer } = props;
264
- if (!open) return null;
286
+ const $ = ctx.ui.$;
287
+ if (!ctx.ui.ready) {
288
+ $.exiting = false;
289
+ $.prevOpen = open;
290
+ }
291
+ if ($.prevOpen !== open) {
292
+ $.prevOpen = open;
293
+ if (open && $.exiting) $.exiting = false;
294
+ }
295
+ const startClose = () => {
296
+ $.exiting = true;
297
+ };
298
+ const onExitEnd = () => {
299
+ $.exiting = false;
300
+ onClose?.();
301
+ };
302
+ if (!open && !$.exiting) return null;
303
+ const isClosing = $.exiting;
265
304
  const handleKeyDown = (e) => {
266
- if (e.key === "Escape" && onClose) onClose();
305
+ if (e.key === "Escape" && !isClosing) startClose();
306
+ };
307
+ const modalRef = (el) => {
308
+ if (!el) return;
309
+ lockScroll();
310
+ const cleanupFocus = trapFocus(el);
311
+ return () => {
312
+ unlockScroll();
313
+ cleanupFocus();
314
+ };
267
315
  };
268
316
  const overlay = h("div", {
269
317
  class: "wf-modal-overlay",
270
- onClick: onClose
318
+ onClick: isClosing ? void 0 : startClose
271
319
  });
272
320
  const closeBtn = h("button", {
273
321
  class: "wf-modal-close",
274
- onClick: onClose,
322
+ onClick: isClosing ? void 0 : startClose,
275
323
  type: "button"
276
324
  }, "\u2715");
277
325
  const titleEl = title ? h("div", { class: "wf-modal-header" }, [title, closeBtn]) : null;
@@ -282,12 +330,15 @@ var Modal = (props, ctx) => {
282
330
  onClick: (e) => e.stopPropagation()
283
331
  }, [titleEl, bodyEl, footerEl].filter(Boolean));
284
332
  const ML = ctx?.i18n?.components?.Modal ?? {};
333
+ const cls = isClosing ? "wf-modal wf-modal--exit" : "wf-modal wf-modal--enter";
285
334
  return h("div", {
286
- class: "wf-modal",
335
+ class: cls,
287
336
  role: "dialog",
288
337
  "aria-modal": "true",
289
338
  "aria-label": title ?? (ML.ariaLabel ?? "\u5F39\u7A97"),
290
- onKeyDown: handleKeyDown
339
+ onKeyDown: handleKeyDown,
340
+ onAnimationEnd: isClosing ? onExitEnd : void 0,
341
+ ref: modalRef
291
342
  }, [overlay, content]);
292
343
  };
293
344
 
@@ -819,17 +870,43 @@ var Tooltip = (props, ctx) => {
819
870
  // src/components/Drawer/Drawer.ts
820
871
  var Drawer = (props, ctx) => {
821
872
  const { open, title, position = "right", onClose, children, footer } = props;
822
- if (!open) return null;
873
+ const $ = ctx.ui.$;
874
+ if (!ctx.ui.ready) {
875
+ $.exiting = false;
876
+ $.prevOpen = open;
877
+ }
878
+ if ($.prevOpen !== open) {
879
+ $.prevOpen = open;
880
+ if (open && $.exiting) $.exiting = false;
881
+ }
882
+ const startClose = () => {
883
+ $.exiting = true;
884
+ };
885
+ const onExitEnd = () => {
886
+ $.exiting = false;
887
+ onClose?.();
888
+ };
889
+ if (!open && !$.exiting) return null;
890
+ const isClosing = $.exiting;
823
891
  const handleKeyDown = (e) => {
824
- if (e.key === "Escape" && onClose) onClose();
892
+ if (e.key === "Escape" && !isClosing) startClose();
893
+ };
894
+ const drawerRef = (el) => {
895
+ if (!el) return;
896
+ lockScroll();
897
+ const cleanupFocus = trapFocus(el);
898
+ return () => {
899
+ unlockScroll();
900
+ cleanupFocus();
901
+ };
825
902
  };
826
903
  const overlay = h("div", {
827
904
  class: "wf-drawer-overlay",
828
- onClick: onClose
905
+ onClick: isClosing ? void 0 : startClose
829
906
  });
830
907
  const closeBtn = h("button", {
831
908
  class: "wf-drawer-close",
832
- onClick: onClose,
909
+ onClick: isClosing ? void 0 : startClose,
833
910
  type: "button",
834
911
  "aria-label": "\u5173\u95ED"
835
912
  }, "\u2715");
@@ -838,17 +915,114 @@ var Drawer = (props, ctx) => {
838
915
  const footerEl = footer ? h("div", { class: "wf-drawer-footer" }, footer) : null;
839
916
  const panel = h("div", {
840
917
  class: `wf-drawer-panel wf-drawer-panel--${position}`,
841
- onClick: (e) => e.stopPropagation()
918
+ onClick: (e) => e.stopPropagation(),
919
+ onAnimationEnd: isClosing ? onExitEnd : void 0
842
920
  }, [titleEl, bodyEl, footerEl].filter(Boolean));
843
921
  const DL = ctx?.i18n?.components?.Drawer ?? {};
922
+ const cls = isClosing ? `wf-drawer wf-drawer--${position} wf-drawer--exit` : `wf-drawer wf-drawer--${position} wf-drawer--enter`;
844
923
  return h("div", {
845
- class: `wf-drawer wf-drawer--${position}`,
924
+ class: cls,
846
925
  role: "dialog",
847
926
  "aria-modal": "true",
848
927
  "aria-label": title ?? (DL.ariaLabel ?? "\u4FA7\u8FB9\u9762\u677F"),
849
- onKeyDown: handleKeyDown
928
+ onKeyDown: handleKeyDown,
929
+ ref: drawerRef
850
930
  }, [overlay, panel]);
851
931
  };
932
+
933
+ // src/components/Popover/Popover.ts
934
+ var Popover = (props, ctx) => {
935
+ const {
936
+ content,
937
+ trigger = "click",
938
+ position = "bottom",
939
+ open,
940
+ onOpenChange,
941
+ disabled,
942
+ children
943
+ } = props;
944
+ const $ = ctx.ui.$;
945
+ if (!ctx.ui.ready) {
946
+ $.show = false;
947
+ }
948
+ const isOpen = open !== void 0 ? open : $.show;
949
+ const setOpen = (v) => {
950
+ if (open === void 0) $.show = v;
951
+ onOpenChange?.(v);
952
+ };
953
+ const onClick = trigger === "click" && !disabled ? () => setOpen(!isOpen) : void 0;
954
+ const hoverProps = {};
955
+ if (trigger === "hover" && !disabled) {
956
+ hoverProps.onMouseEnter = () => setOpen(true);
957
+ hoverProps.onMouseLeave = () => setOpen(false);
958
+ hoverProps.onFocus = () => setOpen(true);
959
+ hoverProps.onBlur = () => setOpen(false);
960
+ }
961
+ const overlay = isOpen ? h("div", {
962
+ class: "wf-popover-overlay",
963
+ onMouseDown: () => setOpen(false)
964
+ }) : null;
965
+ const panel = isOpen ? h("div", {
966
+ class: `wf-popover wf-popover--${position}`,
967
+ role: "dialog",
968
+ "aria-modal": "true",
969
+ "aria-label": "\u5F39\u51FA\u9762\u677F",
970
+ onMouseDown: (e) => e.stopPropagation()
971
+ }, [
972
+ h("div", { class: "wf-popover-arrow" }),
973
+ h("div", { class: "wf-popover-content" }, content)
974
+ ]) : null;
975
+ return h("div", {
976
+ class: `wf-popover-wrap${isOpen ? " wf-popover-wrap--open" : ""}`,
977
+ ...hoverProps
978
+ }, [
979
+ h("div", { class: "wf-popover-trigger", onClick }, children),
980
+ overlay,
981
+ panel
982
+ ].filter(Boolean));
983
+ };
984
+
985
+ // src/components/Skeleton/Skeleton.ts
986
+ var Skeleton = (props) => {
987
+ const { variant = "text", lines = 1, width, height, className } = props;
988
+ const style = {};
989
+ if (width !== void 0) style.width = typeof width === "number" ? `${width}px` : width;
990
+ if (height !== void 0) style.height = typeof height === "number" ? `${height}px` : height;
991
+ const cls = [
992
+ "wf-skeleton",
993
+ `wf-skeleton--${variant}`,
994
+ className
995
+ ].filter(Boolean).join(" ");
996
+ if (lines <= 1) {
997
+ return h("div", { class: cls, style: Object.keys(style).length ? style : void 0 });
998
+ }
999
+ const items = Array.from({ length: lines }, (_, i) => {
1000
+ const itemCls = i === lines - 1 && lines > 1 ? `${cls} wf-skeleton--short` : cls;
1001
+ return h("div", { class: itemCls, style: Object.keys(style).length ? style : void 0 });
1002
+ });
1003
+ return h(Fragment, null, items);
1004
+ };
1005
+
1006
+ // src/components/Img/Img.ts
1007
+ var Img = (props) => {
1008
+ const { src, alt = "", fallback, loading, width, height, className, style } = props;
1009
+ const imgProps = {
1010
+ class: ["wf-image", className].filter(Boolean).join(" "),
1011
+ src: src ?? fallback ?? "",
1012
+ alt,
1013
+ loading: loading ?? "lazy"
1014
+ };
1015
+ if (width !== void 0) imgProps.width = width;
1016
+ if (height !== void 0) imgProps.height = height;
1017
+ if (style) imgProps.style = style;
1018
+ if (fallback) {
1019
+ imgProps.onError = (e) => {
1020
+ const el = e.currentTarget;
1021
+ if (el.src !== fallback) el.src = fallback;
1022
+ };
1023
+ }
1024
+ return h("img", imgProps);
1025
+ };
852
1026
  export {
853
1027
  Accordion,
854
1028
  Alert,
@@ -865,15 +1039,18 @@ export {
865
1039
  Field,
866
1040
  FileUpload,
867
1041
  Form,
1042
+ Img,
868
1043
  Input,
869
1044
  Loading,
870
1045
  Modal,
871
1046
  PageHeader,
872
1047
  Pagination,
1048
+ Popover,
873
1049
  ProgressBar,
874
1050
  RadioGroup,
875
1051
  SearchInput,
876
1052
  Select,
1053
+ Skeleton,
877
1054
  Slider,
878
1055
  StatCard,
879
1056
  Steps,
@@ -519,6 +519,24 @@
519
519
  z-index: var(--wf-cover-z);
520
520
  }
521
521
 
522
+ .wf-modal--enter {
523
+ animation: wf-modal-fadein 0.2s ease-out;
524
+ }
525
+
526
+ .wf-modal--exit {
527
+ animation: wf-modal-fadeout 0.2s ease-out forwards;
528
+ }
529
+
530
+ @keyframes wf-modal-fadein {
531
+ from { opacity: 0; }
532
+ to { opacity: 1; }
533
+ }
534
+
535
+ @keyframes wf-modal-fadeout {
536
+ from { opacity: 1; }
537
+ to { opacity: 0; }
538
+ }
539
+
522
540
  .wf-modal-overlay {
523
541
  position: fixed;
524
542
  inset: 0;
@@ -1595,8 +1613,8 @@
1595
1613
  }
1596
1614
 
1597
1615
  @keyframes wf-tooltip-fadein {
1598
- from { opacity: 0; transform: translateY(2px); }
1599
- to { opacity: 1; transform: translateY(0); }
1616
+ from { opacity: 0; }
1617
+ to { opacity: 1; }
1600
1618
  }
1601
1619
 
1602
1620
  .wf-tooltip-content {
@@ -1653,6 +1671,7 @@
1653
1671
  top: 50%;
1654
1672
  transform: translateY(-50%);
1655
1673
  padding-right: 4px;
1674
+ flex-direction: row;
1656
1675
  }
1657
1676
 
1658
1677
  .wf-tooltip--left .wf-tooltip-arrow {
@@ -1666,6 +1685,7 @@
1666
1685
  top: 50%;
1667
1686
  transform: translateY(-50%);
1668
1687
  padding-left: 4px;
1688
+ flex-direction: row;
1669
1689
  }
1670
1690
 
1671
1691
  .wf-tooltip--right .wf-tooltip-arrow {
@@ -1694,14 +1714,26 @@
1694
1714
  position: absolute;
1695
1715
  inset: 0;
1696
1716
  background: rgba(0, 0, 0, 0.4);
1717
+ }
1718
+
1719
+ .wf-drawer--enter .wf-drawer-overlay {
1697
1720
  animation: wf-drawer-overlay-fadein 0.2s ease-out;
1698
1721
  }
1699
1722
 
1723
+ .wf-drawer--exit .wf-drawer-overlay {
1724
+ animation: wf-drawer-overlay-fadeout 0.2s ease-out forwards;
1725
+ }
1726
+
1700
1727
  @keyframes wf-drawer-overlay-fadein {
1701
1728
  from { opacity: 0; }
1702
1729
  to { opacity: 1; }
1703
1730
  }
1704
1731
 
1732
+ @keyframes wf-drawer-overlay-fadeout {
1733
+ from { opacity: 1; }
1734
+ to { opacity: 0; }
1735
+ }
1736
+
1705
1737
  .wf-drawer-panel {
1706
1738
  position: relative;
1707
1739
  display: flex;
@@ -1711,17 +1743,32 @@
1711
1743
  height: 100%;
1712
1744
  background: var(--wf-color-bg);
1713
1745
  box-shadow: var(--wf-shadow-lg, 0 4px 24px rgba(0, 0, 0, 0.12));
1746
+ }
1747
+
1748
+ .wf-drawer--enter .wf-drawer-panel {
1714
1749
  animation: wf-drawer-slidein 0.25s ease-out;
1715
1750
  }
1716
1751
 
1717
- .wf-drawer-panel--right {
1752
+ .wf-drawer--enter .wf-drawer-panel--right {
1718
1753
  animation-name: wf-drawer-slidein-right;
1719
1754
  }
1720
1755
 
1721
- .wf-drawer-panel--left {
1756
+ .wf-drawer--enter .wf-drawer-panel--left {
1722
1757
  animation-name: wf-drawer-slidein-left;
1723
1758
  }
1724
1759
 
1760
+ .wf-drawer--exit .wf-drawer-panel {
1761
+ animation: wf-drawer-slideout 0.2s ease-out forwards;
1762
+ }
1763
+
1764
+ .wf-drawer--exit .wf-drawer-panel--right {
1765
+ animation-name: wf-drawer-slideout-right;
1766
+ }
1767
+
1768
+ .wf-drawer--exit .wf-drawer-panel--left {
1769
+ animation-name: wf-drawer-slideout-left;
1770
+ }
1771
+
1725
1772
  @keyframes wf-drawer-slidein-right {
1726
1773
  from { transform: translateX(100%); }
1727
1774
  to { transform: translateX(0); }
@@ -1732,6 +1779,16 @@
1732
1779
  to { transform: translateX(0); }
1733
1780
  }
1734
1781
 
1782
+ @keyframes wf-drawer-slideout-right {
1783
+ from { transform: translateX(0); }
1784
+ to { transform: translateX(100%); }
1785
+ }
1786
+
1787
+ @keyframes wf-drawer-slideout-left {
1788
+ from { transform: translateX(0); }
1789
+ to { transform: translateX(-100%); }
1790
+ }
1791
+
1735
1792
  .wf-drawer-header {
1736
1793
  display: flex;
1737
1794
  align-items: center;
@@ -1777,3 +1834,172 @@
1777
1834
  border-top: var(--wf-border-width, 1px) solid var(--wf-color-border);
1778
1835
  }
1779
1836
 
1837
+ /* weifuwu/components — Popover */
1838
+
1839
+ .wf-popover-wrap {
1840
+ position: relative;
1841
+ display: inline-flex;
1842
+ }
1843
+
1844
+ .wf-popover-overlay {
1845
+ position: fixed;
1846
+ inset: 0;
1847
+ z-index: 998;
1848
+ }
1849
+
1850
+ .wf-popover {
1851
+ position: absolute;
1852
+ z-index: 999;
1853
+ display: flex;
1854
+ flex-direction: column;
1855
+ align-items: center;
1856
+ animation: wf-popover-fadein 0.15s ease-out;
1857
+ }
1858
+
1859
+ @keyframes wf-popover-fadein {
1860
+ from { opacity: 0; }
1861
+ to { opacity: 1; }
1862
+ }
1863
+
1864
+ .wf-popover-content {
1865
+ font-family: var(--wf-font-sans);
1866
+ font-size: var(--wf-font-size-sm);
1867
+ color: var(--wf-color-text);
1868
+ background: var(--wf-color-bg);
1869
+ border: 1px solid var(--wf-color-border);
1870
+ border-radius: var(--wf-radius);
1871
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
1872
+ padding: var(--wf-space-sm);
1873
+ min-width: 120px;
1874
+ max-width: 320px;
1875
+ line-height: var(--wf-line-height-normal);
1876
+ }
1877
+
1878
+ .wf-popover-arrow {
1879
+ width: 8px;
1880
+ height: 8px;
1881
+ background: var(--wf-color-bg);
1882
+ border: 1px solid var(--wf-color-border);
1883
+ transform: rotate(45deg);
1884
+ border-radius: 1px;
1885
+ }
1886
+
1887
+ .wf-popover-arrow {
1888
+ border-bottom: none;
1889
+ border-right: none;
1890
+ }
1891
+
1892
+ /* 位置: top */
1893
+ .wf-popover--top {
1894
+ bottom: 100%;
1895
+ left: 50%;
1896
+ transform: translateX(-50%);
1897
+ padding-bottom: 6px;
1898
+ }
1899
+
1900
+ .wf-popover--top .wf-popover-arrow {
1901
+ order: 1;
1902
+ margin-top: -4px;
1903
+ border: none;
1904
+ border-top: 1px solid var(--wf-color-border);
1905
+ border-left: 1px solid var(--wf-color-border);
1906
+ }
1907
+
1908
+ /* 位置: bottom */
1909
+ .wf-popover--bottom {
1910
+ top: 100%;
1911
+ left: 50%;
1912
+ transform: translateX(-50%);
1913
+ padding-top: 6px;
1914
+ }
1915
+
1916
+ .wf-popover--bottom .wf-popover-arrow {
1917
+ order: -1;
1918
+ margin-bottom: -4px;
1919
+ border: none;
1920
+ border-bottom: 1px solid var(--wf-color-border);
1921
+ border-right: 1px solid var(--wf-color-border);
1922
+ }
1923
+
1924
+ /* 位置: left */
1925
+ .wf-popover--left {
1926
+ right: 100%;
1927
+ top: 50%;
1928
+ transform: translateY(-50%);
1929
+ padding-right: 6px;
1930
+ flex-direction: row;
1931
+ }
1932
+
1933
+ .wf-popover--left .wf-popover-arrow {
1934
+ order: 1;
1935
+ margin-left: -4px;
1936
+ border: none;
1937
+ border-left: 1px solid var(--wf-color-border);
1938
+ border-bottom: 1px solid var(--wf-color-border);
1939
+ }
1940
+
1941
+ /* 位置: right */
1942
+ .wf-popover--right {
1943
+ left: 100%;
1944
+ top: 50%;
1945
+ transform: translateY(-50%);
1946
+ padding-left: 6px;
1947
+ flex-direction: row;
1948
+ }
1949
+
1950
+ .wf-popover--right .wf-popover-arrow {
1951
+ order: -1;
1952
+ margin-right: -4px;
1953
+ border: none;
1954
+ border-right: 1px solid var(--wf-color-border);
1955
+ border-top: 1px solid var(--wf-color-border);
1956
+ }
1957
+
1958
+ /* weifuwu/components — Skeleton */
1959
+
1960
+ .wf-skeleton {
1961
+ display: inline-block;
1962
+ height: 1em;
1963
+ border-radius: var(--wf-radius-sm, 4px);
1964
+ background: linear-gradient(
1965
+ 90deg,
1966
+ var(--wf-color-border, #e5e7eb) 25%,
1967
+ var(--wf-color-bg-hover, #f3f4f6) 50%,
1968
+ var(--wf-color-border, #e5e7eb) 75%
1969
+ );
1970
+ background-size: 200% 100%;
1971
+ animation: wf-skeleton-shimmer 1.5s ease-in-out infinite;
1972
+ vertical-align: middle;
1973
+ }
1974
+
1975
+ @keyframes wf-skeleton-shimmer {
1976
+ 0% { background-position: 200% 0; }
1977
+ 100% { background-position: -200% 0; }
1978
+ }
1979
+
1980
+ /* 变体 */
1981
+
1982
+ .wf-skeleton--text {
1983
+ width: 100%;
1984
+ min-width: 60px;
1985
+ }
1986
+
1987
+ .wf-skeleton--circle {
1988
+ border-radius: 50%;
1989
+ width: 40px;
1990
+ height: 40px;
1991
+ min-width: unset;
1992
+ }
1993
+
1994
+ .wf-skeleton--rect {
1995
+ border-radius: var(--wf-radius, 8px);
1996
+ width: 100%;
1997
+ height: 120px;
1998
+ }
1999
+
2000
+ /* 多行:最后一行缩短 */
2001
+
2002
+ .wf-skeleton--short {
2003
+ width: 60%;
2004
+ }
2005
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.37.0",
4
+ "version": "0.38.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {