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,234 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ buildTreeNodeIndex,
4
+ collectHalfCheckedKeys,
5
+ filterTreeNodes,
6
+ flattenVisibleTreeNodes,
7
+ isTreeAncestorKey,
8
+ resolveAccordionExpandedKeys,
9
+ resolveTreeIndent,
10
+ resolveTreeRowHeight,
11
+ resolveTreeCheckState,
12
+ resolveTreeFieldNames,
13
+ resolveTreeFocusKey,
14
+ resolveTreeVirtualEnabled,
15
+ shouldShowTreeCheckbox,
16
+ sliceVirtualTreeNodes,
17
+ splitTreeLabelHighlight,
18
+ toggleTreeCheck,
19
+ type RsTreeNode,
20
+ } from '../components/tree-utils'
21
+ import { RS_COMPONENT_SIZES } from '../theme/types'
22
+
23
+ const nodes = [
24
+ {
25
+ key: 'root',
26
+ label: '根',
27
+ children: [
28
+ { key: 'child-a', label: '子节点 A' },
29
+ { key: 'child-b', label: '子节点 B' },
30
+ ],
31
+ },
32
+ ]
33
+
34
+ describe('tree-utils', () => {
35
+ it('filters nodes by keyword and keeps ancestors', () => {
36
+ const filtered = filterTreeNodes(nodes, 'b')
37
+ expect(filtered).toHaveLength(1)
38
+ expect(filtered[0]?.children).toHaveLength(1)
39
+ expect(filtered[0]?.children?.[0]?.key).toBe('child-b')
40
+ })
41
+
42
+ it('filters with custom filterNode', () => {
43
+ const apiNodes = [{ id: '1', name: 'Alpha' }, { id: '2', name: 'Beta' }]
44
+ const filtered = filterTreeNodes(apiNodes, 'bet', {
45
+ fieldNames: { key: 'id', label: 'name' },
46
+ filterNode: (node, keyword) => String(node.name).toLowerCase().includes(keyword.toLowerCase()),
47
+ })
48
+ expect(filtered).toHaveLength(1)
49
+ expect(filtered[0]?.id).toBe('2')
50
+ })
51
+
52
+ it('flattens visible nodes based on expanded keys', () => {
53
+ const expanded = new Set(['root'])
54
+ const flat = flattenVisibleTreeNodes(nodes, expanded)
55
+ expect(flat.map((item) => item.key)).toEqual(['root', 'child-a', 'child-b'])
56
+ })
57
+
58
+ it('tracks ancestor levelLines for continuous showLine guides', () => {
59
+ const tree: RsTreeNode[] = [
60
+ {
61
+ key: 'a',
62
+ label: 'A',
63
+ children: [
64
+ { key: 'a1', label: 'A1' },
65
+ {
66
+ key: 'a2',
67
+ label: 'A2',
68
+ children: [{ key: 'a2x', label: 'A2X' }],
69
+ },
70
+ ],
71
+ },
72
+ { key: 'b', label: 'B' },
73
+ ]
74
+ const flat = flattenVisibleTreeNodes(tree, new Set(['a', 'a2']))
75
+ const a2x = flat.find((item) => item.key === 'a2x')
76
+ // a 不是末项 → 子树行需保留 depth0 贯穿线;a2 是 a 下末项 → depth1 应断开
77
+ expect(a2x?.levelLines).toEqual([true, false])
78
+ expect(a2x?.isLast).toBe(true)
79
+ const a1 = flat.find((item) => item.key === 'a1')
80
+ expect(a1?.levelLines).toEqual([true])
81
+ expect(a1?.isLast).toBe(false)
82
+ })
83
+
84
+ it('toggles parent-child checks', () => {
85
+ const index = buildTreeNodeIndex(nodes)
86
+ const next = toggleTreeCheck('root', new Set(), index, false)
87
+ expect(next).toEqual(expect.arrayContaining(['root', 'child-a', 'child-b']))
88
+ expect(resolveTreeCheckState('root', new Set(next), index, false)).toBe('checked')
89
+ })
90
+
91
+ it('collects half checked keys', () => {
92
+ const index = buildTreeNodeIndex(nodes)
93
+ const half = collectHalfCheckedKeys(index, new Set(['child-a']), false)
94
+ expect(half).toEqual(['root'])
95
+ })
96
+
97
+ it('resolves accordion expanded keys', () => {
98
+ const index = buildTreeNodeIndex(nodes)
99
+ const next = resolveAccordionExpandedKeys('child-b', ['root', 'child-a'], index, ['root'])
100
+ expect(next).toEqual(expect.arrayContaining(['root', 'child-b']))
101
+ expect(next).not.toContain('child-a')
102
+ })
103
+
104
+ it('supports strict checking', () => {
105
+ const index = buildTreeNodeIndex(nodes)
106
+ const next = toggleTreeCheck('child-a', new Set(), index, true)
107
+ expect(next).toEqual(['child-a'])
108
+ expect(resolveTreeCheckState('root', new Set(next), index, true)).toBe('unchecked')
109
+ })
110
+
111
+ it('enables virtual scroll by threshold', () => {
112
+ expect(resolveTreeVirtualEnabled({ flatCount: 50 })).toBe(false)
113
+ expect(resolveTreeVirtualEnabled({ flatCount: 150 })).toBe(true)
114
+ expect(resolveTreeVirtualEnabled({ virtual: true, flatCount: 10 })).toBe(true)
115
+ })
116
+
117
+ it('slices virtual tree nodes', () => {
118
+ const flat = Array.from({ length: 200 }, (_, index) => ({
119
+ key: String(index),
120
+ node: { key: String(index), label: `N${index}` },
121
+ depth: 0,
122
+ hasChildren: false,
123
+ isLast: index === 199,
124
+ parentKey: null,
125
+ levelLines: [] as boolean[],
126
+ }))
127
+ const slice = sliceVirtualTreeNodes(flat, 320, 200, 32, 2)
128
+ expect(slice.nodes.length).toBeLessThan(flat.length)
129
+ expect(slice.paddingTop).toBeGreaterThan(0)
130
+ })
131
+
132
+ it('clamps virtual slice when scrollTop exceeds filtered content', () => {
133
+ const flat = Array.from({ length: 5 }, (_, index) => ({
134
+ key: String(index),
135
+ node: { key: String(index), label: `N${index}` },
136
+ depth: 0,
137
+ hasChildren: false,
138
+ isLast: index === 4,
139
+ parentKey: null,
140
+ levelLines: [] as boolean[],
141
+ }))
142
+ const slice = sliceVirtualTreeNodes(flat, 50_000, 200, 32, 2)
143
+ expect(slice.nodes.length).toBeGreaterThan(0)
144
+ expect(slice.nodes.length).toBeLessThanOrEqual(flat.length)
145
+ expect(slice.paddingTop).toBeLessThan(flat.length * 32)
146
+ })
147
+
148
+ it('reports the slice start index so rows can be positioned in O(1)', () => {
149
+ const flat = Array.from({ length: 500 }, (_, index) => ({
150
+ key: String(index),
151
+ node: { key: String(index), label: `N${index}` },
152
+ depth: 0,
153
+ hasChildren: false,
154
+ isLast: index === 499,
155
+ parentKey: null,
156
+ levelLines: [] as boolean[],
157
+ }))
158
+ const slice = sliceVirtualTreeNodes(flat, 3200, 200, 32, 2)
159
+ expect(slice.startIndex).toBe(98)
160
+ expect(slice.nodes[0]?.key).toBe(String(slice.startIndex))
161
+ expect(slice.paddingTop).toBe(slice.startIndex * 32)
162
+ })
163
+
164
+ it('flattens a huge sibling group without blowing the call stack', () => {
165
+ // 旧实现用 push(...子结果),单层可见节点过多时实参超限会抛 RangeError
166
+ const wide: RsTreeNode = {
167
+ key: 'wide',
168
+ label: '宽节点',
169
+ children: Array.from({ length: 200_000 }, (_, index) => ({
170
+ key: `w-${index}`,
171
+ label: `W${index}`,
172
+ })),
173
+ }
174
+ const flat = flattenVisibleTreeNodes([wide], new Set(['wide']))
175
+ expect(flat).toHaveLength(200_001)
176
+ expect(flat[200_000]?.key).toBe('w-199999')
177
+ })
178
+
179
+ it('detects ancestry by walking up parents', () => {
180
+ const index = buildTreeNodeIndex(nodes)
181
+ expect(isTreeAncestorKey('root', 'child-a', index)).toBe(true)
182
+ expect(isTreeAncestorKey('child-a', 'root', index)).toBe(false)
183
+ expect(isTreeAncestorKey('child-a', 'child-b', index)).toBe(false)
184
+ expect(isTreeAncestorKey('root', 'root', index)).toBe(false)
185
+ })
186
+
187
+ it('short-circuits check state when nothing is checked', () => {
188
+ const index = buildTreeNodeIndex(nodes)
189
+ expect(resolveTreeCheckState('root', new Set(), index, false)).toBe('unchecked')
190
+ expect(collectHalfCheckedKeys(index, new Set(), false)).toEqual([])
191
+ })
192
+
193
+ it('highlights label segments', () => {
194
+ const parts = splitTreeLabelHighlight('RsTree 组件', 'tree')
195
+ expect(parts.find((part) => part.highlight)?.text).toBe('Tree')
196
+ })
197
+
198
+ it('resolves a finite row height and indent for every size', () => {
199
+ // 漏掉任一档会返回 undefined,进而让虚拟滚动的 padding 变成 NaN
200
+ for (const size of RS_COMPONENT_SIZES) {
201
+ expect(Number.isFinite(resolveTreeRowHeight(size))).toBe(true)
202
+ expect(Number.isFinite(resolveTreeIndent(size))).toBe(true)
203
+ }
204
+ expect(resolveTreeRowHeight('ssm')).toBeLessThan(resolveTreeRowHeight('sm'))
205
+ expect(resolveTreeIndent('ssm')).toBeLessThan(resolveTreeIndent('sm'))
206
+ expect(resolveTreeRowHeight('md', 21)).toBe(21)
207
+ })
208
+
209
+ it('maps field names', () => {
210
+ const fields = resolveTreeFieldNames({ key: 'id', label: 'title', children: 'items' })
211
+ expect(fields.key).toBe('id')
212
+ expect(fields.children).toBe('items')
213
+ })
214
+
215
+ it('moves focus within flat nodes', () => {
216
+ const flat = flattenVisibleTreeNodes(nodes, new Set(['root']))
217
+ const index = buildTreeNodeIndex(nodes)
218
+ expect(resolveTreeFocusKey(flat, 'child-a', 'next', index)).toBe('child-b')
219
+ expect(resolveTreeFocusKey(flat, 'child-b', 'parent', index)).toBe('root')
220
+ })
221
+
222
+ it('supports onlyCheckLeaf mode', () => {
223
+ const index = buildTreeNodeIndex(nodes)
224
+ const root = nodes[0]!
225
+ expect(shouldShowTreeCheckbox(root)).toBe(true)
226
+ expect(shouldShowTreeCheckbox(root, undefined, { onlyCheckLeaf: true })).toBe(false)
227
+ const next = toggleTreeCheck('child-a', new Set(), index, false, true)
228
+ expect(next).toEqual(['child-a'])
229
+ expect(resolveTreeCheckState('root', new Set(next), index, false, true)).toBe('indeterminate')
230
+ const both = toggleTreeCheck('child-b', new Set(next), index, false, true)
231
+ expect(resolveTreeCheckState('root', new Set(both), index, false, true)).toBe('checked')
232
+ expect(collectHalfCheckedKeys(index, new Set(next), false, true)).toEqual(['root'])
233
+ })
234
+ })
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it, vi, afterEach, beforeEach } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import { defineComponent, ref } from 'vue'
4
+ import { useTableCellTooltip } from '../composables/useTableCellTooltip'
5
+
6
+ const Host = defineComponent({
7
+ props: {
8
+ delay: { type: Number, default: 0 },
9
+ },
10
+ setup(props) {
11
+ const tipRef = ref<HTMLElement | null>(null)
12
+ const { state, onPointerOver, onPointerOut, hide } = useTableCellTooltip({
13
+ enabled: () => true,
14
+ delay: () => props.delay,
15
+ tipRef,
16
+ })
17
+ return { state, onPointerOver, onPointerOut, hide, tipRef }
18
+ },
19
+ template: `
20
+ <div @pointerover="onPointerOver" @pointerout="onPointerOut">
21
+ <span
22
+ class="rs-table__cell-tip rs-table__ellipsis-text"
23
+ data-rs-table-tip-mode="overflow"
24
+ data-rs-table-tip-text="完整文本"
25
+ >截断</span>
26
+ <div
27
+ v-if="state.visible"
28
+ ref="tipRef"
29
+ class="rs-table__shared-tip"
30
+ :style="state.style"
31
+ >{{ state.text }}</div>
32
+ </div>
33
+ `,
34
+ })
35
+
36
+ describe('useTableCellTooltip', () => {
37
+ beforeEach(() => {
38
+ vi.useFakeTimers()
39
+ })
40
+
41
+ afterEach(() => {
42
+ vi.useRealTimers()
43
+ })
44
+
45
+ it('shows tooltip only when ellipsis content overflows', async () => {
46
+ const wrapper = mount(Host, { attachTo: document.body })
47
+ const cell = wrapper.find('.rs-table__cell-tip').element as HTMLElement
48
+ Object.defineProperty(cell, 'scrollWidth', { value: 200, configurable: true })
49
+ Object.defineProperty(cell, 'clientWidth', { value: 80, configurable: true })
50
+
51
+ await wrapper.find('.rs-table__cell-tip').trigger('pointerover', { pointerType: 'mouse' })
52
+ await vi.runAllTimersAsync()
53
+ await flushPromises()
54
+
55
+ expect(wrapper.find('.rs-table__shared-tip').text()).toBe('完整文本')
56
+ wrapper.unmount()
57
+ })
58
+
59
+ it('hides tooltip when content does not overflow', async () => {
60
+ const wrapper = mount(Host, { attachTo: document.body })
61
+ await wrapper.find('.rs-table__cell-tip').trigger('pointerover', { pointerType: 'mouse' })
62
+ await vi.runAllTimersAsync()
63
+ await flushPromises()
64
+ expect(wrapper.find('.rs-table__shared-tip').exists()).toBe(false)
65
+ wrapper.unmount()
66
+ })
67
+
68
+ it('shows header tooltip via shared layer', async () => {
69
+ const HeaderHost = defineComponent({
70
+ setup() {
71
+ const tipRef = ref<HTMLElement | null>(null)
72
+ const { state, onPointerOver, onPointerOut } = useTableCellTooltip({
73
+ enabled: () => false,
74
+ headerEnabled: () => true,
75
+ delay: () => 0,
76
+ tipRef,
77
+ })
78
+ return { state, onPointerOver, onPointerOut, tipRef }
79
+ },
80
+ template: `
81
+ <div @pointerover="onPointerOver" @pointerout="onPointerOut">
82
+ <span class="rs-table__th-label" data-rs-table-header-tip="字段: id&#10;类型: int">id</span>
83
+ <div
84
+ v-if="state.visible"
85
+ ref="tipRef"
86
+ class="rs-table__shared-tip"
87
+ :class="{ 'rs-table__shared-tip--header': state.kind === 'header' }"
88
+ :style="state.style"
89
+ >{{ state.text }}</div>
90
+ </div>
91
+ `,
92
+ })
93
+ const wrapper = mount(HeaderHost, { attachTo: document.body })
94
+ await wrapper.find('.rs-table__th-label').trigger('pointerover', { pointerType: 'mouse' })
95
+ await vi.runAllTimersAsync()
96
+ await flushPromises()
97
+ expect(wrapper.find('.rs-table__shared-tip--header').exists()).toBe(true)
98
+ expect(wrapper.find('.rs-table__shared-tip').text()).toContain('字段: id')
99
+ wrapper.unmount()
100
+ })
101
+ })
@@ -0,0 +1,115 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { AvatarFallback, AvatarImage, AvatarRoot } from './reka'
4
+ import { getAvatarInitials, type RsAvatarShape, type RsAvatarSize, type RsAvatarTone } from './avatar-utils'
5
+ import RsIcon from './RsIcon.vue'
6
+
7
+ const props = withDefaults(
8
+ defineProps<{
9
+ src?: string
10
+ alt?: string
11
+ name?: string
12
+ fallback?: string
13
+ icon?: string
14
+ size?: RsAvatarSize
15
+ shape?: RsAvatarShape
16
+ tone?: RsAvatarTone
17
+ label?: string
18
+ }>(),
19
+ {
20
+ size: 'md',
21
+ shape: 'circle',
22
+ tone: 'primary',
23
+ icon: 'user',
24
+ },
25
+ )
26
+
27
+ const fallbackText = computed(() => {
28
+ if (props.fallback?.trim()) return props.fallback.trim()
29
+ if (props.name?.trim()) return getAvatarInitials(props.name)
30
+ return ''
31
+ })
32
+
33
+ const showIconFallback = computed(() => !fallbackText.value)
34
+ const imageAlt = computed(() => props.alt ?? props.name ?? '')
35
+ const ariaLabel = computed(() => props.label ?? props.alt ?? props.name ?? undefined)
36
+ </script>
37
+
38
+ <template>
39
+ <AvatarRoot
40
+ class="rs-avatar"
41
+ :class="[`rs-avatar--${size}`, `rs-avatar--${shape}`]"
42
+ :aria-label="ariaLabel"
43
+ :role="ariaLabel ? 'img' : undefined"
44
+ >
45
+ <AvatarImage v-if="src" :src="src" :alt="imageAlt" class="rs-avatar__image" />
46
+ <AvatarFallback class="rs-avatar__fallback" :class="`rs-avatar__fallback--${tone}`">
47
+ <slot name="fallback">
48
+ <RsIcon v-if="showIconFallback" :name="icon" :size="size === 'sm' ? 14 : size === 'lg' ? 20 : 16" />
49
+ <span v-else>{{ fallbackText }}</span>
50
+ </slot>
51
+ </AvatarFallback>
52
+ </AvatarRoot>
53
+ </template>
54
+
55
+ <style>
56
+ .rs-avatar {
57
+ display: inline-flex;
58
+ overflow: hidden;
59
+ flex-shrink: 0;
60
+ border: 1px solid var(--rs-border);
61
+ background: var(--rs-surface);
62
+ }
63
+ .rs-avatar--circle {
64
+ border-radius: var(--rs-radius-full);
65
+ }
66
+ .rs-avatar--square {
67
+ border-radius: var(--rs-radius-sm);
68
+ }
69
+ .rs-avatar--sm {
70
+ width: 1.75rem;
71
+ height: 1.75rem;
72
+ }
73
+ .rs-avatar--md {
74
+ width: 2.25rem;
75
+ height: 2.25rem;
76
+ }
77
+ .rs-avatar--lg {
78
+ width: 3rem;
79
+ height: 3rem;
80
+ }
81
+ .rs-avatar__image {
82
+ width: 100%;
83
+ height: 100%;
84
+ object-fit: cover;
85
+ }
86
+ .rs-avatar__fallback {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ width: 100%;
91
+ height: 100%;
92
+ font-size: var(--rs-font-size-xs);
93
+ font-weight: 600;
94
+ }
95
+ .rs-avatar__fallback--default {
96
+ background: var(--rs-surface-hover);
97
+ color: var(--rs-muted);
98
+ }
99
+ .rs-avatar__fallback--primary {
100
+ background: var(--rs-primary-container);
101
+ color: var(--rs-on-primary-container);
102
+ }
103
+ .rs-avatar__fallback--success {
104
+ background: var(--rs-success-container);
105
+ color: var(--rs-on-success-container);
106
+ }
107
+ .rs-avatar__fallback--warning {
108
+ background: var(--rs-warning-container);
109
+ color: var(--rs-on-warning-container);
110
+ }
111
+ .rs-avatar__fallback--danger {
112
+ background: var(--rs-danger-container);
113
+ color: var(--rs-on-danger-container);
114
+ }
115
+ </style>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
5
+ }>(),
6
+ {
7
+ variant: 'default',
8
+ },
9
+ )
10
+ </script>
11
+
12
+ <template>
13
+ <span class="rs-badge" :class="`rs-badge--${variant}`">
14
+ <slot />
15
+ </span>
16
+ </template>
17
+
18
+ <style scoped>
19
+ .rs-badge {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ padding: 0.125rem 0.625rem;
23
+ border-radius: var(--rs-radius-full);
24
+ font-size: var(--rs-font-size-xs);
25
+ font-weight: 500;
26
+ line-height: var(--rs-line-height-tight);
27
+ letter-spacing: 0.02em;
28
+ }
29
+ .rs-badge--default {
30
+ background: var(--rs-surface-hover);
31
+ color: var(--rs-text);
32
+ border: 1px solid var(--rs-border);
33
+ }
34
+ .rs-badge--primary {
35
+ background: var(--rs-primary-container);
36
+ color: var(--rs-on-primary-container);
37
+ border: 1px solid color-mix(in srgb, var(--rs-primary) 40%, transparent);
38
+ }
39
+ .rs-badge--success {
40
+ background: var(--rs-success-container);
41
+ color: var(--rs-on-success-container);
42
+ border: 1px solid color-mix(in srgb, var(--rs-success) 40%, transparent);
43
+ }
44
+ .rs-badge--warning {
45
+ background: var(--rs-warning-container);
46
+ color: var(--rs-on-warning-container);
47
+ border: 1px solid color-mix(in srgb, var(--rs-warning) 40%, transparent);
48
+ }
49
+ .rs-badge--danger {
50
+ background: var(--rs-danger-container);
51
+ color: var(--rs-on-danger-container);
52
+ border: 1px solid color-mix(in srgb, var(--rs-danger) 40%, transparent);
53
+ }
54
+ .rs-badge--info {
55
+ background: var(--rs-info-container);
56
+ color: var(--rs-on-info-container);
57
+ border: 1px solid color-mix(in srgb, var(--rs-info) 40%, transparent);
58
+ }
59
+ </style>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { useRsI18n } from '../composables/useRsI18n'
4
+ import { buildBreadcrumbRenderItems } from './breadcrumb-utils'
5
+ import RsBreadcrumbItems from './RsBreadcrumbItems.vue'
6
+
7
+ const { t } = useRsI18n()
8
+
9
+ export interface RsBreadcrumbItem {
10
+ label: string
11
+ to?: string
12
+ href?: string
13
+ }
14
+
15
+ const props = defineProps<{
16
+ items: RsBreadcrumbItem[]
17
+ }>()
18
+
19
+ const renderItems = computed(() => buildBreadcrumbRenderItems(props.items))
20
+ </script>
21
+
22
+ <template>
23
+ <nav class="rs-breadcrumb" :aria-label="t('breadcrumb.label')">
24
+ <RsBreadcrumbItems :items="renderItems" :separator-label="t('breadcrumb.separator')" />
25
+ </nav>
26
+ </template>
27
+
28
+ <style>
29
+ .rs-breadcrumb__list {
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ align-items: center;
33
+ gap: 0.25rem;
34
+ margin: 0;
35
+ padding: 0;
36
+ list-style: none;
37
+ }
38
+ .rs-breadcrumb__item {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ gap: 0.25rem;
42
+ }
43
+ .rs-breadcrumb__link {
44
+ font-size: var(--rs-font-size-sm);
45
+ color: var(--rs-muted);
46
+ text-decoration: none;
47
+ padding: 0.125rem 0.25rem;
48
+ border-radius: var(--rs-radius-xs);
49
+ transition:
50
+ color var(--rs-transition-fast),
51
+ background var(--rs-transition-fast);
52
+ }
53
+ .rs-breadcrumb__link:hover:not(.rs-breadcrumb__link--current) {
54
+ color: var(--rs-primary);
55
+ background: var(--rs-surface-hover);
56
+ }
57
+ .rs-breadcrumb__link--current {
58
+ color: var(--rs-text);
59
+ font-weight: 500;
60
+ pointer-events: none;
61
+ }
62
+ .rs-breadcrumb__sep {
63
+ color: var(--rs-muted);
64
+ }
65
+ </style>
@@ -0,0 +1,36 @@
1
+ <script setup lang="ts">
2
+ import type { RsBreadcrumbRenderItem } from './breadcrumb-utils'
3
+ import RsIcon from './RsIcon.vue'
4
+
5
+ defineProps<{
6
+ items: RsBreadcrumbRenderItem[]
7
+ separatorLabel: string
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <ol class="rs-breadcrumb__list">
13
+ <li
14
+ v-for="(item, index) in items"
15
+ :key="item.key"
16
+ class="rs-breadcrumb__item"
17
+ >
18
+ <component
19
+ :is="item.isLink ? 'a' : 'span'"
20
+ :href="item.href"
21
+ class="rs-breadcrumb__link"
22
+ :class="{ 'rs-breadcrumb__link--current': item.isCurrent }"
23
+ :aria-current="item.isCurrent ? 'page' : undefined"
24
+ >
25
+ {{ item.label }}
26
+ </component>
27
+ <RsIcon
28
+ v-if="index < items.length - 1"
29
+ name="chevron-right"
30
+ :size="14"
31
+ class="rs-breadcrumb__sep"
32
+ :label="separatorLabel"
33
+ />
34
+ </li>
35
+ </ol>
36
+ </template>