@h-ai/ui 0.1.0-alpha.39 → 0.1.0-alpha.41

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.
@@ -0,0 +1,129 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { DataAttributes } from '../../types.js';
3
+ /** 工具栏筛选字段定义。 */
4
+ type FilterFieldDef = {
5
+ /** 筛选字段 key,同时作为回调中的属性名。 */
6
+ id: string;
7
+ /** 用户可见字段名。 */
8
+ label: string | (() => string);
9
+ /** 控件类型;date-range 会渲染起止日期输入。 */
10
+ type: string;
11
+ /** 控件占位文案。 */
12
+ placeholder?: string | (() => string);
13
+ /** 日期范围起始值 key。 */
14
+ startKey?: string;
15
+ /** 日期范围结束值 key。 */
16
+ endKey?: string;
17
+ /** 数值控件最小值。 */
18
+ min?: number;
19
+ /** 数值控件最大值。 */
20
+ max?: number;
21
+ /** 数值控件步长。 */
22
+ step?: number;
23
+ /** 选择类控件选项。 */
24
+ options?: Array<{
25
+ label: string;
26
+ value: string | number | boolean;
27
+ }> | (() => Array<{
28
+ label: string;
29
+ value: string | number | boolean;
30
+ }>);
31
+ };
32
+ /** TableToolbar 对外入参。 */
33
+ type TableToolbarProps = {
34
+ /** 是否显示搜索框,默认 true。 */
35
+ searchable?: boolean;
36
+ /** 搜索框占位文案。 */
37
+ searchPlaceholder?: string;
38
+ /** 搜索框当前值,可通过 bind:searchValue 受控。 */
39
+ searchValue?: string;
40
+ /** 搜索框无障碍标签。 */
41
+ searchLabel?: string;
42
+ /** 可用筛选字段。 */
43
+ filterFields?: FilterFieldDef[];
44
+ /** 当前筛选值,可通过 bind:filterValues 受控。 */
45
+ filterValues?: Record<string, unknown>;
46
+ /** 可用排序列。 */
47
+ sortableColumns?: Array<{
48
+ key: string;
49
+ label: string;
50
+ }>;
51
+ /** 当前排序字段,可通过 bind:sortBy 受控。 */
52
+ sortBy?: string;
53
+ /** 当前排序方向。 */
54
+ sortDirection?: 'asc' | 'desc';
55
+ /** 当前视图模式,可通过 bind:viewMode 受控。 */
56
+ viewMode?: 'table' | 'card';
57
+ /** 是否显示表格/卡片切换。 */
58
+ showViewSwitch?: boolean;
59
+ /** 主操作按钮文案;为空时不显示。 */
60
+ primaryActionLabel?: string;
61
+ /** 工具栏左侧内容插槽,可放置页面标题、统计信息等非操作内容。 */
62
+ leading?: Snippet;
63
+ /** 工具栏右侧自定义操作插槽。 */
64
+ children?: Snippet;
65
+ /** 搜索提交回调。 */
66
+ onsearch?: (search: string) => void;
67
+ /** 筛选值变化回调。 */
68
+ onfilterchange?: (filters: Record<string, unknown>) => void;
69
+ /** 清空筛选回调。 */
70
+ onclearfilters?: () => void;
71
+ /** 排序变化回调。 */
72
+ onsort?: (key: string, direction: 'asc' | 'desc') => void;
73
+ /** 清空排序回调。 */
74
+ onclearsort?: () => void;
75
+ /** 视图变化回调。 */
76
+ onviewmodechange?: (mode: 'table' | 'card') => void;
77
+ /** 主操作点击回调。 */
78
+ onPrimaryAction?: () => void;
79
+ /** 根节点自定义 class。 */
80
+ class?: string;
81
+ } & DataAttributes;
82
+ /**
83
+ * TableToolbar
84
+ * 通用表格工具栏,支持搜索、筛选弹层、排序弹层、视图切换、主操作按钮和自定义按钮插槽。
85
+ *
86
+ * @prop {boolean} searchable - 是否显示搜索框
87
+ * @prop {string} searchPlaceholder - 搜索框占位文案
88
+ * @prop {string} searchValue - 搜索框当前值(bindable)
89
+ * @prop {string} searchLabel - 搜索框 aria-label
90
+ * @prop {FilterFieldDef[]} filterFields - 筛选字段配置
91
+ * @prop {Record<string, unknown>} filterValues - 当前筛选值(bindable)
92
+ * @prop {Array<{key: string, label: string}>} sortableColumns - 可排序列
93
+ * @prop {string} sortBy - 当前排序字段
94
+ * @prop {'asc' | 'desc'} sortDirection - 当前排序方向
95
+ * @prop {'table' | 'card'} viewMode - 当前视图模式
96
+ * @prop {boolean} showViewSwitch - 是否显示视图切换
97
+ * @prop {string} primaryActionLabel - 主操作按钮文案
98
+ * @prop {Snippet} leading - 工具栏左侧内容插槽,可用于页面标题或说明标签
99
+ * @prop {Snippet} children - 自定义按钮插槽
100
+ * @prop {Function} onsearch - 搜索回调
101
+ * @prop {Function} onfilterchange - 筛选变化回调
102
+ * @prop {Function} onclearfilters - 清空筛选回调
103
+ * @prop {Function} onsort - 排序回调
104
+ * @prop {Function} onclearsort - 清空排序回调
105
+ * @prop {Function} onviewmodechange - 视图切换回调
106
+ * @prop {Function} onPrimaryAction - 主操作回调
107
+ *
108
+ * @example
109
+ * <TableToolbar
110
+ * searchable
111
+ * searchPlaceholder="搜索..."
112
+ * bind:searchValue
113
+ * filterFields={[
114
+ * { id: 'status', label: '状态', type: 'enum', options: [{ label: '启用', value: 'active' }] }
115
+ * ]}
116
+ * bind:filterValues
117
+ * sortableColumns={[{ key: 'name', label: '名称' }]}
118
+ * sortBy="name"
119
+ * sortDirection="asc"
120
+ * onsearch={(v) => handleSearch(v)}
121
+ * onfilterchange={(f) => handleFilter(f)}
122
+ * onsort={(key, dir) => handleSort(key, dir)}
123
+ * >
124
+ * <Button size="sm" onclick={handleExport}>导出</Button>
125
+ * </TableToolbar>
126
+ */
127
+ declare const TableToolbar: import("svelte").Component<TableToolbarProps, {}, "searchValue" | "filterValues" | "sortBy" | "sortDirection" | "viewMode">;
128
+ type TableToolbar = ReturnType<typeof TableToolbar>;
129
+ export default TableToolbar;
@@ -39,6 +39,7 @@ export { default as Skeleton } from './Skeleton.svelte';
39
39
  export { default as Steps } from './Steps.svelte';
40
40
  export type { SwipeCellAction } from './swipe-cell-types.js';
41
41
  export { default as SwipeCell } from './SwipeCell.svelte';
42
+ export { default as TableToolbar } from './TableToolbar.svelte';
42
43
  export { default as Tabs } from './Tabs.svelte';
43
44
  export { default as TagInput } from './TagInput.svelte';
44
45
  export type { TimelineItem } from './timeline-types.js';
@@ -51,6 +51,8 @@ export { default as SafeArea } from './SafeArea.svelte';
51
51
  export { default as Skeleton } from './Skeleton.svelte';
52
52
  export { default as Steps } from './Steps.svelte';
53
53
  export { default as SwipeCell } from './SwipeCell.svelte';
54
+ // 表格工具栏
55
+ export { default as TableToolbar } from './TableToolbar.svelte';
54
56
  export { default as Tabs } from './Tabs.svelte';
55
57
  export { default as TagInput } from './TagInput.svelte';
56
58
  export { default as Timeline } from './Timeline.svelte';
@@ -10,6 +10,7 @@
10
10
  -->
11
11
  <script lang='ts' generics="T = string">
12
12
  import type { DataAttributes, SelectProps } from '../../types.js'
13
+ import { tick } from 'svelte'
13
14
  import { cn, getDataAttributes, portal } from '../../utils.js'
14
15
  import { getFormControlSizeClasses } from '../control-size.js'
15
16
 
@@ -39,6 +40,8 @@
39
40
  let isHovered = $state(false)
40
41
  // 下拉层定位样式(portal 到 body,用 fixed 逃逸 overflow/stacking)
41
42
  let dropdownStyle = $state('')
43
+ /** portal 移动节点后用于二次测量的动画帧句柄,关闭时必须取消避免过期写入。 */
44
+ let dropdownPositionFrame: number | undefined = $state()
42
45
 
43
46
  const sizeClasses = $derived(getFormControlSizeClasses(size))
44
47
 
@@ -131,15 +134,40 @@
131
134
  dropdownStyle = `position:fixed;left:${Math.round(rect.left)}px;width:${Math.round(rect.width)}px;${vertical};z-index:1200;`
132
135
  }
133
136
 
137
+ /**
138
+ * 在 portal 完成节点移动和父级 flex 布局收敛后重算一次浮层坐标。
139
+ *
140
+ * 首次打开时,Svelte 创建浮层、portal 将其移入 body 与分页栏的 flex 尺寸更新可能落在同一渲染批次;
141
+ * 立即测量用于避免首帧无定位,下一帧复测则保证 fixed 坐标只基于最终的触发器视口位置。
142
+ */
143
+ async function scheduleSettledDropdownPosition(): Promise<void> {
144
+ await tick()
145
+ if (!isDropdownOpen) {
146
+ return
147
+ }
148
+
149
+ dropdownPositionFrame = window.requestAnimationFrame(() => {
150
+ dropdownPositionFrame = undefined
151
+ if (isDropdownOpen) {
152
+ updateDropdownPosition()
153
+ }
154
+ })
155
+ }
156
+
134
157
  // 打开时定位并跟随滚动/缩放更新
135
158
  $effect(() => {
136
159
  if (!isDropdownOpen)
137
160
  return
138
161
  updateDropdownPosition()
162
+ void scheduleSettledDropdownPosition()
139
163
  const scrollOpts = { passive: true, capture: true } as const
140
164
  window.addEventListener('scroll', updateDropdownPosition, scrollOpts)
141
165
  window.addEventListener('resize', updateDropdownPosition)
142
166
  return () => {
167
+ if (dropdownPositionFrame !== undefined) {
168
+ window.cancelAnimationFrame(dropdownPositionFrame)
169
+ dropdownPositionFrame = undefined
170
+ }
143
171
  window.removeEventListener('scroll', updateDropdownPosition, scrollOpts)
144
172
  window.removeEventListener('resize', updateDropdownPosition)
145
173
  }