niuma-ui 1.0.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.
Files changed (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,518 @@
1
+ import type { RsComponentSize } from '../theme/types'
2
+
3
+ export interface RsTreeNode {
4
+ key?: string
5
+ id?: string
6
+ label?: string
7
+ title?: string
8
+ children?: RsTreeNode[]
9
+ disabled?: boolean
10
+ isLeaf?: boolean
11
+ icon?: string
12
+ disableCheckbox?: boolean
13
+ loading?: boolean
14
+ [key: string]: unknown
15
+ }
16
+
17
+ export interface RsTreeFieldNames {
18
+ key?: string
19
+ label?: string
20
+ children?: string
21
+ disabled?: string
22
+ isLeaf?: string
23
+ icon?: string
24
+ disableCheckbox?: string
25
+ loading?: string
26
+ }
27
+
28
+ export interface RsTreeFlatNode {
29
+ key: string
30
+ node: RsTreeNode
31
+ depth: number
32
+ hasChildren: boolean
33
+ isLast: boolean
34
+ parentKey: string | null
35
+ /**
36
+ * showLine 用:下标对应祖先深度 `0..depth-1`。
37
+ * `true` 表示该祖先不是同级最后一项,竖线需贯穿当前行;`false` 则留空缺口。
38
+ */
39
+ levelLines: boolean[]
40
+ }
41
+
42
+ export interface RsTreeNodeIndex {
43
+ node: RsTreeNode
44
+ parentKey: string | null
45
+ childrenKeys: string[]
46
+ }
47
+
48
+ export type RsTreeCheckState = 'checked' | 'indeterminate' | 'unchecked'
49
+ export type RsTreeFocusMove = 'next' | 'prev' | 'parent' | 'first' | 'last'
50
+ export type RsTreeDropPosition = 'before' | 'inside' | 'after'
51
+ /** handle=显示拖拽手柄;row=整行拖拽且不显示手柄 */
52
+ export type RsTreeDragTrigger = 'handle' | 'row'
53
+ export type RsTreeSize = RsComponentSize
54
+
55
+ export const DEFAULT_TREE_FIELD_NAMES: Required<RsTreeFieldNames> = {
56
+ key: 'key',
57
+ label: 'label',
58
+ children: 'children',
59
+ disabled: 'disabled',
60
+ isLeaf: 'isLeaf',
61
+ icon: 'icon',
62
+ disableCheckbox: 'disableCheckbox',
63
+ loading: 'loading',
64
+ }
65
+
66
+ /**
67
+ * 各尺寸档行高 / 缩进。
68
+ * 显式标注 Record<RsTreeSize, number>:尺寸类型再加档位时这里会直接编译报错,
69
+ * 而不是在运行时取到 undefined —— 行高 undefined 会让虚拟滚动的 padding 全变 NaN。
70
+ */
71
+ export const TREE_ROW_HEIGHT: Record<RsTreeSize, number> = { ssm: 24, sm: 28, md: 32, lg: 40 }
72
+ export const TREE_INDENT_BY_SIZE: Record<RsTreeSize, number> = { ssm: 12, sm: 16, md: 20, lg: 24 }
73
+
74
+ export function resolveTreeFieldNames(fieldNames?: RsTreeFieldNames): Required<RsTreeFieldNames> {
75
+ return { ...DEFAULT_TREE_FIELD_NAMES, ...fieldNames }
76
+ }
77
+
78
+ export function resolveTreeRowHeight(size: RsTreeSize = 'md', custom?: number): number {
79
+ if (custom !== undefined) return custom
80
+ return TREE_ROW_HEIGHT[size]
81
+ }
82
+
83
+ export function resolveTreeIndent(size: RsTreeSize = 'md'): number {
84
+ return TREE_INDENT_BY_SIZE[size]
85
+ }
86
+
87
+ function readNodeField(node: RsTreeNode, field: string): unknown {
88
+ return node[field]
89
+ }
90
+
91
+ export function getTreeKey(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): string {
92
+ const mapped = readNodeField(node, fields.key)
93
+ if (mapped !== undefined && mapped !== null && mapped !== '') return String(mapped)
94
+ if (node.key !== undefined && node.key !== '') return String(node.key)
95
+ if (node.id !== undefined && node.id !== '') return String(node.id)
96
+ const label = readNodeField(node, fields.label) ?? node.label ?? node.title
97
+ return String(label ?? '')
98
+ }
99
+
100
+ export function getTreeLabel(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): string {
101
+ const mapped = readNodeField(node, fields.label)
102
+ if (mapped !== undefined && mapped !== null && mapped !== '') return String(mapped)
103
+ if (node.label !== undefined && node.label !== '') return String(node.label)
104
+ if (node.title !== undefined && node.title !== '') return String(node.title)
105
+ return getTreeKey(node, fields)
106
+ }
107
+
108
+ export function getTreeChildren(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): RsTreeNode[] {
109
+ const mapped = readNodeField(node, fields.children)
110
+ if (Array.isArray(mapped)) return mapped as RsTreeNode[]
111
+ return node.children ?? []
112
+ }
113
+
114
+ export function getTreeIsLeaf(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): boolean {
115
+ return Boolean(readNodeField(node, fields.isLeaf) ?? node.isLeaf)
116
+ }
117
+
118
+ export function isTreeNodeDisabled(node: RsTreeNode, fields = DEFAULT_TREE_FIELD_NAMES): boolean {
119
+ return Boolean(readNodeField(node, fields.disabled) ?? node.disabled)
120
+ }
121
+
122
+ export function isTreeCheckboxDisabled(
123
+ node: RsTreeNode,
124
+ fields = DEFAULT_TREE_FIELD_NAMES,
125
+ options: { onlyCheckLeaf?: boolean; lazy?: boolean } = {},
126
+ ): boolean {
127
+ if (isTreeNodeDisabled(node, fields)) return true
128
+ if (options.onlyCheckLeaf && hasTreeChildren(node, fields, options.lazy ?? false)) return true
129
+ return Boolean(readNodeField(node, fields.disableCheckbox) ?? node.disableCheckbox)
130
+ }
131
+
132
+ export function shouldShowTreeCheckbox(
133
+ node: RsTreeNode,
134
+ fields = DEFAULT_TREE_FIELD_NAMES,
135
+ options: { onlyCheckLeaf?: boolean; lazy?: boolean } = {},
136
+ ): boolean {
137
+ if (!options.onlyCheckLeaf) return true
138
+ return !hasTreeChildren(node, fields, options.lazy ?? false)
139
+ }
140
+
141
+ export function isTreeNodeLoading(
142
+ node: RsTreeNode,
143
+ key: string,
144
+ loadingKeys: ReadonlySet<string>,
145
+ fields = DEFAULT_TREE_FIELD_NAMES,
146
+ ): boolean {
147
+ return loadingKeys.has(key) || Boolean(readNodeField(node, fields.loading) ?? node.loading)
148
+ }
149
+
150
+ export function hasTreeChildren(
151
+ node: RsTreeNode,
152
+ fields = DEFAULT_TREE_FIELD_NAMES,
153
+ lazy = false,
154
+ ): boolean {
155
+ if (getTreeIsLeaf(node, fields)) return false
156
+ if (getTreeChildren(node, fields).length > 0) return true
157
+ return lazy
158
+ }
159
+
160
+ export function flattenTreeNodeIds(
161
+ nodes: readonly RsTreeNode[],
162
+ fields = DEFAULT_TREE_FIELD_NAMES,
163
+ ): string[] {
164
+ return nodes.flatMap((node) => [
165
+ getTreeKey(node, fields),
166
+ ...flattenTreeNodeIds(getTreeChildren(node, fields), fields),
167
+ ])
168
+ }
169
+
170
+ export function collectExpandableKeys(
171
+ nodes: readonly RsTreeNode[],
172
+ fields = DEFAULT_TREE_FIELD_NAMES,
173
+ lazy = false,
174
+ ): string[] {
175
+ const keys: string[] = []
176
+ function walk(items: readonly RsTreeNode[]): void {
177
+ for (const node of items) {
178
+ const key = getTreeKey(node, fields)
179
+ if (hasTreeChildren(node, fields, lazy)) {
180
+ keys.push(key)
181
+ walk(getTreeChildren(node, fields))
182
+ }
183
+ }
184
+ }
185
+ walk(nodes)
186
+ return keys
187
+ }
188
+
189
+ export function buildTreeNodeIndex(
190
+ nodes: readonly RsTreeNode[],
191
+ fields = DEFAULT_TREE_FIELD_NAMES,
192
+ lazy = false,
193
+ ): Map<string, RsTreeNodeIndex> {
194
+ const map = new Map<string, RsTreeNodeIndex>()
195
+ function walk(items: readonly RsTreeNode[], parentKey: string | null): void {
196
+ for (const node of items) {
197
+ const key = getTreeKey(node, fields)
198
+ const childrenKeys = hasTreeChildren(node, fields, lazy)
199
+ ? getTreeChildren(node, fields).map((child) => getTreeKey(child, fields))
200
+ : []
201
+ map.set(key, { node, parentKey, childrenKeys })
202
+ if (getTreeChildren(node, fields).length > 0) walk(getTreeChildren(node, fields), key)
203
+ }
204
+ }
205
+ walk(nodes, null)
206
+ return map
207
+ }
208
+
209
+ export function collectDescendantKeys(
210
+ key: string,
211
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
212
+ ): string[] {
213
+ const entry = index.get(key)
214
+ if (!entry) return []
215
+ const result: string[] = []
216
+ for (const childKey of entry.childrenKeys) {
217
+ result.push(childKey, ...collectDescendantKeys(childKey, index))
218
+ }
219
+ return result
220
+ }
221
+
222
+ /**
223
+ * `ancestorKey` 是否为 `descendantKey` 的祖先。
224
+ * 沿 parentKey 上溯 O(depth),比收集整棵子树再 includes 便宜得多(拖拽 dragover 每帧都要判一次)。
225
+ */
226
+ export function isTreeAncestorKey(
227
+ ancestorKey: string,
228
+ descendantKey: string,
229
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
230
+ ): boolean {
231
+ let parentKey = index.get(descendantKey)?.parentKey ?? null
232
+ while (parentKey !== null) {
233
+ if (parentKey === ancestorKey) return true
234
+ parentKey = index.get(parentKey)?.parentKey ?? null
235
+ }
236
+ return false
237
+ }
238
+
239
+ export function collectHalfCheckedKeys(
240
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
241
+ checkedKeys: ReadonlySet<string>,
242
+ checkStrictly: boolean,
243
+ onlyCheckLeaf = false,
244
+ ): string[] {
245
+ if (checkStrictly && !onlyCheckLeaf) return []
246
+ // 无勾选项 → 不可能有半选,省去整棵树的状态遍历
247
+ if (checkedKeys.size === 0) return []
248
+ const half: string[] = []
249
+ for (const key of index.keys()) {
250
+ if (resolveTreeCheckState(key, checkedKeys, index, checkStrictly, onlyCheckLeaf) === 'indeterminate') {
251
+ half.push(key)
252
+ }
253
+ }
254
+ return half
255
+ }
256
+
257
+ /** 根层节点无祖先竖线;共享同一个空数组,省去每个节点一次数组分配 */
258
+ const NO_LEVEL_LINES: readonly boolean[] = Object.freeze([])
259
+
260
+ export function flattenVisibleTreeNodes(
261
+ nodes: readonly RsTreeNode[],
262
+ expandedKeys: ReadonlySet<string>,
263
+ fields = DEFAULT_TREE_FIELD_NAMES,
264
+ lazy = false,
265
+ depth = 0,
266
+ parentKey: string | null = null,
267
+ /** 路径上各祖先是否为同级最后一项(长度 = depth) */
268
+ ancestorIsLast: readonly boolean[] = [],
269
+ ): RsTreeFlatNode[] {
270
+ const result: RsTreeFlatNode[] = []
271
+
272
+ // 递归写入同一个 result,而非 push(...子结果):后者在单层可见节点达数万时会因实参过多抛 RangeError
273
+ function walk(
274
+ items: readonly RsTreeNode[],
275
+ currentDepth: number,
276
+ currentParentKey: string | null,
277
+ ancestors: readonly boolean[],
278
+ ): void {
279
+ const lastIndex = items.length - 1
280
+ // 祖先非末项 → 画贯穿竖线;末项 → 该列断开,避免「幽灵」延伸。同级共用一份,行内只读
281
+ const levelLines = ancestors.length === 0 ? NO_LEVEL_LINES : ancestors.map((last) => !last)
282
+ for (let index = 0; index <= lastIndex; index += 1) {
283
+ const node = items[index] as RsTreeNode
284
+ const key = getTreeKey(node, fields)
285
+ const hasChildren = hasTreeChildren(node, fields, lazy)
286
+ const isLast = index === lastIndex
287
+ result.push({
288
+ key,
289
+ node,
290
+ depth: currentDepth,
291
+ hasChildren,
292
+ isLast,
293
+ parentKey: currentParentKey,
294
+ levelLines: levelLines as boolean[],
295
+ })
296
+ if (hasChildren && expandedKeys.has(key)) {
297
+ walk(getTreeChildren(node, fields), currentDepth + 1, key, [...ancestors, isLast])
298
+ }
299
+ }
300
+ }
301
+
302
+ walk(nodes, depth, parentKey, ancestorIsLast)
303
+ return result
304
+ }
305
+
306
+ export function defaultTreeFilterNode(
307
+ node: RsTreeNode,
308
+ keyword: string,
309
+ fields = DEFAULT_TREE_FIELD_NAMES,
310
+ ): boolean {
311
+ return getTreeLabel(node, fields).toLowerCase().includes(keyword.trim().toLowerCase())
312
+ }
313
+
314
+ export function filterTreeNodes(
315
+ nodes: readonly RsTreeNode[],
316
+ keyword: string,
317
+ options: {
318
+ fieldNames?: RsTreeFieldNames
319
+ filterNode?: (node: RsTreeNode, keyword: string) => boolean
320
+ } = {},
321
+ ): RsTreeNode[] {
322
+ const fields = resolveTreeFieldNames(options.fieldNames)
323
+ const query = keyword.trim()
324
+ if (!query) return [...nodes]
325
+
326
+ const match = options.filterNode ?? ((node, value) => defaultTreeFilterNode(node, value, fields))
327
+
328
+ function filterNode(node: RsTreeNode): RsTreeNode | null {
329
+ const children = getTreeChildren(node, fields)
330
+ const filteredChildren = children
331
+ .map(filterNode)
332
+ .filter((item): item is RsTreeNode => item !== null)
333
+
334
+ if (match(node, query)) return { ...node, children }
335
+ if (filteredChildren.length > 0) return { ...node, children: filteredChildren }
336
+ return null
337
+ }
338
+
339
+ return nodes
340
+ .map(filterNode)
341
+ .filter((item): item is RsTreeNode => item !== null)
342
+ }
343
+
344
+ export function splitTreeLabelHighlight(
345
+ label: string,
346
+ keyword: string,
347
+ ): Array<{ text: string; highlight: boolean }> {
348
+ const query = keyword.trim()
349
+ if (!query) return [{ text: label, highlight: false }]
350
+
351
+ const lowerLabel = label.toLowerCase()
352
+ const lowerQuery = query.toLowerCase()
353
+ const index = lowerLabel.indexOf(lowerQuery)
354
+ if (index < 0) return [{ text: label, highlight: false }]
355
+
356
+ const parts: Array<{ text: string; highlight: boolean }> = []
357
+ if (index > 0) parts.push({ text: label.slice(0, index), highlight: false })
358
+ parts.push({ text: label.slice(index, index + query.length), highlight: true })
359
+ if (index + query.length < label.length) {
360
+ parts.push({ text: label.slice(index + query.length), highlight: false })
361
+ }
362
+ return parts
363
+ }
364
+
365
+ export function resolveTreeCheckState(
366
+ key: string,
367
+ checkedKeys: ReadonlySet<string>,
368
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
369
+ checkStrictly: boolean,
370
+ onlyCheckLeaf = false,
371
+ ): RsTreeCheckState {
372
+ if (checkStrictly && !onlyCheckLeaf) return checkedKeys.has(key) ? 'checked' : 'unchecked'
373
+ // 无勾选项时任何子树都是 unchecked,提前返回避免递归整棵子树
374
+ if (checkedKeys.size === 0) return 'unchecked'
375
+
376
+ const entry = index.get(key)
377
+ if (!entry || entry.childrenKeys.length === 0) {
378
+ return checkedKeys.has(key) ? 'checked' : 'unchecked'
379
+ }
380
+
381
+ const childStates = entry.childrenKeys.map((childKey) =>
382
+ resolveTreeCheckState(childKey, checkedKeys, index, checkStrictly, onlyCheckLeaf),
383
+ )
384
+
385
+ if (childStates.every((state) => state === 'checked')) return 'checked'
386
+ if (childStates.some((state) => state === 'checked' || state === 'indeterminate')) return 'indeterminate'
387
+ return 'unchecked'
388
+ }
389
+
390
+ export function toggleTreeCheck(
391
+ key: string,
392
+ checkedKeys: ReadonlySet<string>,
393
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
394
+ checkStrictly: boolean,
395
+ onlyCheckLeaf = false,
396
+ ): string[] {
397
+ const next = new Set(checkedKeys)
398
+ const current = resolveTreeCheckState(key, next, index, checkStrictly, onlyCheckLeaf)
399
+ const shouldCheck = current !== 'checked'
400
+
401
+ if (checkStrictly && !onlyCheckLeaf) {
402
+ if (shouldCheck) next.add(key)
403
+ else next.delete(key)
404
+ return [...next]
405
+ }
406
+
407
+ if (onlyCheckLeaf) {
408
+ const entry = index.get(key)
409
+ if (!entry || entry.childrenKeys.length > 0) return [...checkedKeys]
410
+ if (shouldCheck) next.add(key)
411
+ else next.delete(key)
412
+ return [...next]
413
+ }
414
+
415
+ for (const id of [key, ...collectDescendantKeys(key, index)]) {
416
+ if (shouldCheck) next.add(id)
417
+ else next.delete(id)
418
+ }
419
+
420
+ let parentKey = index.get(key)?.parentKey ?? null
421
+ while (parentKey) {
422
+ const parent = index.get(parentKey)
423
+ if (!parent) break
424
+ const allChecked = parent.childrenKeys.every(
425
+ (childKey) => resolveTreeCheckState(childKey, next, index, false, onlyCheckLeaf) === 'checked',
426
+ )
427
+ if (allChecked) next.add(parentKey)
428
+ else next.delete(parentKey)
429
+ parentKey = parent.parentKey
430
+ }
431
+
432
+ return [...next]
433
+ }
434
+
435
+ export function resolveAccordionExpandedKeys(
436
+ key: string,
437
+ expandedKeys: readonly string[],
438
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
439
+ rootKeys: readonly string[],
440
+ ): string[] {
441
+ const parentKey = index.get(key)?.parentKey ?? null
442
+ const siblings = parentKey ? (index.get(parentKey)?.childrenKeys ?? []) : [...rootKeys]
443
+ const next = new Set(expandedKeys)
444
+ for (const sibling of siblings) {
445
+ if (sibling !== key) next.delete(sibling)
446
+ }
447
+ next.add(key)
448
+ return [...next]
449
+ }
450
+
451
+ export function resolveTreeFocusKey(
452
+ flatNodes: readonly RsTreeFlatNode[],
453
+ currentKey: string | null,
454
+ move: RsTreeFocusMove,
455
+ index: ReadonlyMap<string, RsTreeNodeIndex>,
456
+ ): string | null {
457
+ const keys = flatNodes.map((item) => item.key)
458
+ if (keys.length === 0) return null
459
+ if (!currentKey) return keys[0] ?? null
460
+
461
+ const currentIndex = keys.indexOf(currentKey)
462
+ if (currentIndex < 0) return keys[0] ?? null
463
+
464
+ switch (move) {
465
+ case 'next':
466
+ return keys[Math.min(currentIndex + 1, keys.length - 1)] ?? null
467
+ case 'prev':
468
+ return keys[Math.max(currentIndex - 1, 0)] ?? null
469
+ case 'first':
470
+ return keys[0] ?? null
471
+ case 'last':
472
+ return keys[keys.length - 1] ?? null
473
+ case 'parent':
474
+ return index.get(currentKey)?.parentKey ?? currentKey
475
+ default:
476
+ return currentKey
477
+ }
478
+ }
479
+
480
+ export function resolveTreeVirtualEnabled(options: {
481
+ virtual?: boolean
482
+ flatCount: number
483
+ virtualThreshold?: number
484
+ }): boolean {
485
+ if (options.virtual) return true
486
+ return options.flatCount > (options.virtualThreshold ?? 100)
487
+ }
488
+
489
+ export function sliceVirtualTreeNodes(
490
+ nodes: readonly RsTreeFlatNode[],
491
+ scrollTop: number,
492
+ viewportHeight: number,
493
+ itemHeight: number,
494
+ overscan = 4,
495
+ ): {
496
+ nodes: RsTreeFlatNode[]
497
+ /** 切片首项在完整扁平列表中的下标,供 aria-posinset 等按 O(1) 定位 */
498
+ startIndex: number
499
+ paddingTop: number
500
+ paddingBottom: number
501
+ } {
502
+ if (nodes.length === 0) return { nodes: [], startIndex: 0, paddingTop: 0, paddingBottom: 0 }
503
+
504
+ const totalHeight = nodes.length * itemHeight
505
+ // 过滤/折叠后内容变短时,浏览器不一定派发 scroll;钳位避免 start 越界导致空白切片
506
+ const maxScrollTop = Math.max(0, totalHeight - Math.max(0, viewportHeight))
507
+ const clampedScrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop)
508
+ const start = Math.max(0, Math.floor(clampedScrollTop / itemHeight) - overscan)
509
+ const visibleCount = Math.ceil(Math.max(0, viewportHeight) / itemHeight) + overscan * 2
510
+ const end = Math.min(nodes.length, start + visibleCount)
511
+
512
+ return {
513
+ nodes: nodes.slice(start, end),
514
+ startIndex: start,
515
+ paddingTop: start * itemHeight,
516
+ paddingBottom: Math.max(0, totalHeight - end * itemHeight),
517
+ }
518
+ }
@@ -0,0 +1,55 @@
1
+ export interface RsUploadValidationRules {
2
+ accept?: string
3
+ maxSize?: number
4
+ maxCount?: number
5
+ }
6
+
7
+ export interface RsUploadValidationError {
8
+ file: File
9
+ reason: 'accept' | 'maxSize' | 'maxCount'
10
+ }
11
+
12
+ export function formatFileSize(size: number): string {
13
+ if (size < 1024) return `${size} B`
14
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
15
+ return `${(size / 1024 / 1024).toFixed(1)} MB`
16
+ }
17
+
18
+ export function isFileAccepted(file: File, accept?: string): boolean {
19
+ if (!accept) return true
20
+ const rules = accept.split(',').map((item) => item.trim()).filter(Boolean)
21
+ return rules.some((rule) => {
22
+ if (rule.startsWith('.')) return file.name.toLowerCase().endsWith(rule.toLowerCase())
23
+ if (rule.endsWith('/*')) return file.type.startsWith(rule.slice(0, -1))
24
+ return file.type === rule
25
+ })
26
+ }
27
+
28
+ export function validateUploadFiles(
29
+ files: readonly File[],
30
+ rules: RsUploadValidationRules = {},
31
+ ): { accepted: File[]; rejected: RsUploadValidationError[] } {
32
+ const accepted: File[] = []
33
+ const rejected: RsUploadValidationError[] = []
34
+ for (const file of files) {
35
+ if (rules.maxCount && accepted.length >= rules.maxCount) {
36
+ rejected.push({ file, reason: 'maxCount' })
37
+ } else if (!isFileAccepted(file, rules.accept)) {
38
+ rejected.push({ file, reason: 'accept' })
39
+ } else if (rules.maxSize && file.size > rules.maxSize) {
40
+ rejected.push({ file, reason: 'maxSize' })
41
+ } else {
42
+ accepted.push(file)
43
+ }
44
+ }
45
+ return { accepted, rejected }
46
+ }
47
+
48
+ export function mergeUploadFiles(current: readonly File[], next: readonly File[], maxCount?: number): File[] {
49
+ const merged = [...current, ...next]
50
+ return maxCount ? merged.slice(0, maxCount) : merged
51
+ }
52
+
53
+ export function removeUploadFileAt(files: readonly File[], index: number): File[] {
54
+ return files.filter((_, currentIndex) => currentIndex !== index)
55
+ }
@@ -0,0 +1,17 @@
1
+ export type RsVirtualListItemSize = number | ((index: number) => number)
2
+
3
+ export function resolveItemSize(itemSize: RsVirtualListItemSize, index = 0): number {
4
+ const size = typeof itemSize === 'function' ? itemSize(index) : itemSize
5
+ return Number.isFinite(size) && size > 0 ? size : 32
6
+ }
7
+
8
+ export function resolveVirtualListHeight(height?: number | string, fallback = 240): string | undefined {
9
+ if (height === 0) return undefined
10
+ if (height === undefined) return `${fallback}px`
11
+ return typeof height === 'number' ? `${height}px` : height
12
+ }
13
+
14
+ /** height 为 0 时由父级 flex 撑满,组件内用 ResizeObserver 测量可视高度 */
15
+ export function isVirtualListFillHeight(height?: number | string): boolean {
16
+ return height === 0
17
+ }
@@ -0,0 +1,22 @@
1
+ import { onBeforeUnmount, onMounted, type Ref } from 'vue'
2
+
3
+ export function useClickOutside(
4
+ target: Ref<HTMLElement | null>,
5
+ handler: () => void,
6
+ ) {
7
+ function onPointerDown(event: MouseEvent) {
8
+ const el = target.value
9
+ if (!el || el.contains(event.target as Node)) {
10
+ return
11
+ }
12
+ handler()
13
+ }
14
+
15
+ onMounted(() => {
16
+ document.addEventListener('mousedown', onPointerDown)
17
+ })
18
+
19
+ onBeforeUnmount(() => {
20
+ document.removeEventListener('mousedown', onPointerDown)
21
+ })
22
+ }
@@ -0,0 +1,91 @@
1
+ import { inject, provide, ref, type InjectionKey, type Ref } from 'vue'
2
+ import { localeMap } from '../locale/messages'
3
+ import { defaultLocale, type RsLocale } from '../locale/types'
4
+ import type { RsComponentSize, RsRadius, RsThemeMode } from '../theme/types'
5
+
6
+ export interface RsConfigContext {
7
+ theme: Ref<RsThemeMode>
8
+ locale: Ref<RsLocale>
9
+ /** 全局默认控件尺寸(ssm / sm / md / lg)。 */
10
+ controlSize: Ref<RsComponentSize>
11
+ /**
12
+ * 全局默认圆角。未设置时各组件保留自身默认档(如按钮 full、输入 sm)。
13
+ * 设为 `none` 可整站直角风格。
14
+ */
15
+ controlRadius: Ref<RsRadius | undefined>
16
+ setTheme: (mode: RsThemeMode) => void
17
+ setLocale: (locale: RsLocale) => void
18
+ setControlSize: (size: RsComponentSize) => void
19
+ setControlRadius: (radius: RsRadius | undefined) => void
20
+ t: (key: string, fallback?: string) => string
21
+ }
22
+
23
+ export const rsConfigKey: InjectionKey<RsConfigContext> = Symbol('rs-config')
24
+
25
+ export function useRsConfig(): RsConfigContext {
26
+ const ctx = inject(rsConfigKey)
27
+ if (!ctx) {
28
+ throw new Error('useRsConfig() must be used within RsConfigProvider')
29
+ }
30
+ return ctx
31
+ }
32
+
33
+ /** 可选读取;组件在 Provider 外也可独立使用。 */
34
+ export function useRsConfigOptional(): RsConfigContext | null {
35
+ return inject(rsConfigKey, null)
36
+ }
37
+
38
+ export function createRsConfigState(
39
+ initialTheme: RsThemeMode = 'light',
40
+ initialLocale: RsLocale = defaultLocale,
41
+ initialControlSize: RsComponentSize = 'md',
42
+ initialControlRadius?: RsRadius,
43
+ ): RsConfigContext {
44
+ const theme = ref<RsThemeMode>(initialTheme)
45
+ const locale = ref<RsLocale>(initialLocale)
46
+ const controlSize = ref<RsComponentSize>(initialControlSize)
47
+ const controlRadius = ref<RsRadius | undefined>(initialControlRadius)
48
+
49
+ function setTheme(mode: RsThemeMode) {
50
+ theme.value = mode
51
+ }
52
+
53
+ function setLocale(next: RsLocale) {
54
+ locale.value = next
55
+ if (typeof document !== 'undefined') {
56
+ document.documentElement.setAttribute('data-rs-locale', next)
57
+ }
58
+ }
59
+
60
+ function setControlSize(size: RsComponentSize) {
61
+ controlSize.value = size
62
+ }
63
+
64
+ function setControlRadius(radius: RsRadius | undefined) {
65
+ controlRadius.value = radius
66
+ }
67
+
68
+ function t(key: string, fallback?: string) {
69
+ return localeMap[locale.value][key] ?? fallback ?? key
70
+ }
71
+
72
+ if (typeof document !== 'undefined') {
73
+ document.documentElement.setAttribute('data-rs-locale', locale.value)
74
+ }
75
+
76
+ return {
77
+ theme,
78
+ locale,
79
+ controlSize,
80
+ controlRadius,
81
+ setTheme,
82
+ setLocale,
83
+ setControlSize,
84
+ setControlRadius,
85
+ t,
86
+ }
87
+ }
88
+
89
+ export function provideRsConfig(ctx: RsConfigContext) {
90
+ provide(rsConfigKey, ctx)
91
+ }