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,279 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import RsIcon from './RsIcon.vue'
4
+ import { formatFileSize, mergeUploadFiles, removeUploadFileAt, validateUploadFiles } from './upload-utils'
5
+ import { useRsI18n } from '../composables/useRsI18n'
6
+
7
+ const model = defineModel<File[]>({ default: () => [] })
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ accept?: string
12
+ multiple?: boolean
13
+ maxSize?: number
14
+ maxCount?: number
15
+ disabled?: boolean
16
+ label?: string
17
+ hint?: string
18
+ }>(),
19
+ {
20
+ multiple: false,
21
+ disabled: false,
22
+ },
23
+ )
24
+
25
+ const emit = defineEmits<{
26
+ reject: [errors: ReturnType<typeof validateUploadFiles>['rejected']]
27
+ }>()
28
+
29
+ const { t } = useRsI18n()
30
+ const canAdd = computed(() => !props.maxCount || model.value.length < props.maxCount)
31
+ const isDropzoneDisabled = computed(() => props.disabled || !canAdd.value)
32
+ const resolvedHint = computed(() => props.hint ?? t('upload.browse'))
33
+
34
+ function onFilesChange(event: Event): void {
35
+ const input = event.target as HTMLInputElement
36
+ const files = Array.from(input.files ?? [])
37
+ const result = validateUploadFiles(files, {
38
+ accept: props.accept,
39
+ maxSize: props.maxSize,
40
+ maxCount: props.maxCount ? props.maxCount - model.value.length : undefined,
41
+ })
42
+ model.value = mergeUploadFiles(model.value, result.accepted, props.maxCount)
43
+ if (result.rejected.length) emit('reject', result.rejected)
44
+ input.value = ''
45
+ }
46
+
47
+ function removeFile(index: number): void {
48
+ model.value = removeUploadFileAt(model.value, index)
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <div class="rs-upload">
54
+ <label
55
+ class="rs-upload__dropzone"
56
+ :class="{ 'rs-upload__dropzone--disabled': isDropzoneDisabled }"
57
+ >
58
+ <input
59
+ class="rs-upload__input"
60
+ type="file"
61
+ :accept="accept"
62
+ :multiple="multiple"
63
+ :disabled="isDropzoneDisabled"
64
+ @change="onFilesChange"
65
+ >
66
+ <span class="rs-upload__icon" aria-hidden="true">
67
+ <RsIcon name="cloud-upload" :size="22" />
68
+ </span>
69
+ <span class="rs-upload__text">
70
+ <span class="rs-upload__label">{{ label ?? t('upload.label') }}</span>
71
+ <span class="rs-upload__hint">{{ resolvedHint }}</span>
72
+ </span>
73
+ </label>
74
+ <ul v-if="model.length" class="rs-upload__list">
75
+ <li
76
+ v-for="(file, index) in model"
77
+ :key="`${file.name}-${file.size}-${index}`"
78
+ class="rs-upload__file"
79
+ >
80
+ <span class="rs-upload__file-icon" aria-hidden="true">
81
+ <RsIcon name="file" :size="16" />
82
+ </span>
83
+ <span class="rs-upload__file-meta">
84
+ <span class="rs-upload__file-name">{{ file.name }}</span>
85
+ <span class="rs-upload__file-size">{{ formatFileSize(file.size) }}</span>
86
+ </span>
87
+ <button
88
+ type="button"
89
+ class="rs-upload__file-remove"
90
+ :aria-label="t('common.remove')"
91
+ @click.stop="removeFile(index)"
92
+ >
93
+ <RsIcon name="x" :size="14" />
94
+ </button>
95
+ </li>
96
+ </ul>
97
+ </div>
98
+ </template>
99
+
100
+ <style scoped>
101
+ .rs-upload {
102
+ display: grid;
103
+ gap: var(--rs-space-md);
104
+ }
105
+
106
+ .rs-upload__dropzone {
107
+ position: relative;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ justify-content: center;
112
+ gap: var(--rs-space-md);
113
+ min-height: 8.5rem;
114
+ padding: var(--rs-space-xl) var(--rs-space-lg);
115
+ border: 1px dashed var(--rs-border);
116
+ border-radius: var(--rs-radius);
117
+ background: color-mix(in srgb, var(--rs-surface) 82%, var(--rs-bg));
118
+ cursor: pointer;
119
+ text-align: center;
120
+ transition:
121
+ border-color var(--rs-transition-fast),
122
+ background var(--rs-transition-fast),
123
+ box-shadow var(--rs-transition-fast);
124
+ }
125
+
126
+ .rs-upload__dropzone:hover:not(.rs-upload__dropzone--disabled) {
127
+ border-color: color-mix(in srgb, var(--rs-primary) 55%, var(--rs-border));
128
+ background: color-mix(in srgb, var(--rs-primary) 6%, var(--rs-surface));
129
+ }
130
+
131
+ .rs-upload__dropzone:focus-within:not(.rs-upload__dropzone--disabled) {
132
+ border-color: var(--rs-focus-border, var(--rs-primary));
133
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
134
+ }
135
+
136
+ .rs-upload__dropzone--disabled {
137
+ opacity: 0.38;
138
+ cursor: not-allowed;
139
+ }
140
+
141
+ .rs-upload__input {
142
+ position: absolute;
143
+ inset: 0;
144
+ width: 100%;
145
+ height: 100%;
146
+ opacity: 0;
147
+ cursor: inherit;
148
+ }
149
+
150
+ .rs-upload__icon {
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ width: 3rem;
155
+ height: 3rem;
156
+ border-radius: var(--rs-radius-full);
157
+ border: 1px solid color-mix(in srgb, var(--rs-primary) 24%, var(--rs-border));
158
+ background: var(--rs-primary-container);
159
+ color: var(--rs-on-primary-container);
160
+ box-shadow: var(--rs-shadow-sm);
161
+ transition:
162
+ transform var(--rs-transition-fast),
163
+ box-shadow var(--rs-transition-fast);
164
+ }
165
+
166
+ .rs-upload__dropzone:hover:not(.rs-upload__dropzone--disabled) .rs-upload__icon {
167
+ transform: translateY(-1px);
168
+ box-shadow: var(--rs-shadow);
169
+ }
170
+
171
+ .rs-upload__text {
172
+ display: grid;
173
+ gap: 0.25rem;
174
+ max-width: 20rem;
175
+ }
176
+
177
+ .rs-upload__label {
178
+ color: var(--rs-text);
179
+ font-size: var(--rs-font-size-sm);
180
+ font-weight: 600;
181
+ line-height: var(--rs-line-height-tight);
182
+ }
183
+
184
+ .rs-upload__hint {
185
+ color: var(--rs-muted);
186
+ font-size: var(--rs-font-size-xs);
187
+ line-height: var(--rs-line-height-normal);
188
+ }
189
+
190
+ .rs-upload__list {
191
+ display: grid;
192
+ gap: var(--rs-space-xs);
193
+ margin: 0;
194
+ padding: 0;
195
+ list-style: none;
196
+ }
197
+
198
+ .rs-upload__file {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: var(--rs-space-sm);
202
+ padding: var(--rs-space-sm) var(--rs-space-md);
203
+ border: 1px solid var(--rs-border-subtle);
204
+ border-radius: var(--rs-radius-sm);
205
+ background: var(--rs-surface);
206
+ transition:
207
+ border-color var(--rs-transition-fast),
208
+ background var(--rs-transition-fast);
209
+ }
210
+
211
+ .rs-upload__file:hover {
212
+ border-color: var(--rs-border);
213
+ background: color-mix(in srgb, var(--rs-surface-hover) 35%, var(--rs-surface));
214
+ }
215
+
216
+ .rs-upload__file-icon {
217
+ display: flex;
218
+ flex-shrink: 0;
219
+ align-items: center;
220
+ justify-content: center;
221
+ width: 2rem;
222
+ height: 2rem;
223
+ border-radius: var(--rs-radius-sm);
224
+ border: 1px solid var(--rs-border-subtle);
225
+ background: color-mix(in srgb, var(--rs-bg) 55%, var(--rs-surface));
226
+ color: var(--rs-muted);
227
+ }
228
+
229
+ .rs-upload__file-meta {
230
+ display: grid;
231
+ gap: 0.125rem;
232
+ min-width: 0;
233
+ flex: 1;
234
+ }
235
+
236
+ .rs-upload__file-name {
237
+ overflow: hidden;
238
+ color: var(--rs-text);
239
+ font-size: var(--rs-font-size-sm);
240
+ font-weight: 500;
241
+ line-height: var(--rs-line-height-tight);
242
+ text-overflow: ellipsis;
243
+ white-space: nowrap;
244
+ }
245
+
246
+ .rs-upload__file-size {
247
+ color: var(--rs-muted);
248
+ font-size: var(--rs-font-size-xs);
249
+ line-height: var(--rs-line-height-tight);
250
+ }
251
+
252
+ .rs-upload__file-remove {
253
+ display: inline-flex;
254
+ flex-shrink: 0;
255
+ align-items: center;
256
+ justify-content: center;
257
+ width: 1.75rem;
258
+ height: 1.75rem;
259
+ padding: 0;
260
+ border: none;
261
+ border-radius: var(--rs-radius-sm);
262
+ background: transparent;
263
+ color: var(--rs-muted);
264
+ cursor: pointer;
265
+ transition:
266
+ background var(--rs-transition-fast),
267
+ color var(--rs-transition-fast);
268
+ }
269
+
270
+ .rs-upload__file-remove:hover {
271
+ background: var(--rs-item-hover);
272
+ color: var(--rs-text);
273
+ }
274
+
275
+ .rs-upload__file-remove:focus-visible {
276
+ outline: none;
277
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
278
+ }
279
+ </style>
@@ -0,0 +1,217 @@
1
+ <script setup lang="ts" generic="T">
2
+ import { computed, onActivated, onMounted, onUnmounted, ref, watch, nextTick } from 'vue'
3
+ import type { RsRadius } from '../theme/types'
4
+ import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
5
+ import type { RsVirtualListItemSize } from './virtual-list-utils'
6
+ import { isVirtualListFillHeight, resolveItemSize, resolveVirtualListHeight } from './virtual-list-utils'
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ items: T[]
11
+ itemSize?: RsVirtualListItemSize
12
+ height?: number | string
13
+ overscan?: number
14
+ activeIndex?: number | null
15
+ /** keep-alive 外层可见性;变为 true 时重同步视口与 scroll 位置 */
16
+ layoutActive?: boolean
17
+ /** 外层圆角。直角列表传 `none`。默认 sm。 */
18
+ radius?: RsRadius
19
+ }>(),
20
+ {
21
+ itemSize: 32,
22
+ height: 240,
23
+ overscan: 4,
24
+ activeIndex: null,
25
+ },
26
+ )
27
+
28
+ const resolvedRadius = useResolvedRsRadius(() => props.radius, 'sm')
29
+
30
+ const rootRef = ref<HTMLElement | null>(null)
31
+ const scrollTop = ref(0)
32
+ const measuredHeight = ref(0)
33
+ const fillHeight = computed(() => isVirtualListFillHeight(props.height))
34
+ const heightStyle = computed(() => {
35
+ const resolved = resolveVirtualListHeight(props.height)
36
+ return resolved ? { height: resolved } : undefined
37
+ })
38
+ const rootStyle = computed(() => ({
39
+ ...heightStyle.value,
40
+ '--rs-virtual-list-radius': rsRadiusCss(resolvedRadius.value),
41
+ }))
42
+ const fixedItemSize = computed(() => resolveItemSize(props.itemSize))
43
+ const viewportHeightPx = computed(() => {
44
+ if (typeof props.height === 'number' && props.height > 0) {
45
+ return props.height
46
+ }
47
+ if (typeof props.height === 'string') {
48
+ const parsed = Number.parseInt(props.height, 10)
49
+ if (Number.isFinite(parsed) && parsed > 0 && !props.height.includes('%')) {
50
+ return parsed
51
+ }
52
+ }
53
+ if (measuredHeight.value > 0) {
54
+ return measuredHeight.value
55
+ }
56
+ const el = rootRef.value
57
+ if (el && el.clientHeight > 0) {
58
+ return el.clientHeight
59
+ }
60
+ return 240
61
+ })
62
+
63
+ let resizeObserver: ResizeObserver | null = null
64
+
65
+ function syncMeasuredHeight(): void {
66
+ const el = rootRef.value
67
+ if (!el) {
68
+ return
69
+ }
70
+ const next = el.clientHeight
71
+ if (next > 0 && next !== measuredHeight.value) {
72
+ measuredHeight.value = next
73
+ }
74
+ }
75
+
76
+ function syncLayoutFromDom(): void {
77
+ syncMeasuredHeight()
78
+ const el = rootRef.value
79
+ if (el) {
80
+ scrollTop.value = el.scrollTop
81
+ }
82
+ }
83
+
84
+ function scheduleLayoutSync(): void {
85
+ syncLayoutFromDom()
86
+ void nextTick(() => {
87
+ syncLayoutFromDom()
88
+ requestAnimationFrame(() => {
89
+ syncLayoutFromDom()
90
+ })
91
+ })
92
+ }
93
+
94
+ onMounted(() => {
95
+ scheduleLayoutSync()
96
+ const el = rootRef.value
97
+ if (!el || typeof ResizeObserver === 'undefined') {
98
+ return
99
+ }
100
+ resizeObserver = new ResizeObserver(() => syncMeasuredHeight())
101
+ resizeObserver.observe(el)
102
+ })
103
+
104
+ onActivated(() => {
105
+ scheduleLayoutSync()
106
+ })
107
+
108
+ watch(
109
+ () => props.layoutActive,
110
+ (active) => {
111
+ if (active) scheduleLayoutSync()
112
+ },
113
+ { flush: 'post' },
114
+ )
115
+
116
+ onUnmounted(() => {
117
+ resizeObserver?.disconnect()
118
+ resizeObserver = null
119
+ })
120
+ const startIndex = computed(() => Math.max(0, Math.floor(scrollTop.value / fixedItemSize.value) - props.overscan))
121
+ const visibleCount = computed(
122
+ () => Math.ceil(viewportHeightPx.value / fixedItemSize.value) + props.overscan * 2,
123
+ )
124
+ const visibleItems = computed(() =>
125
+ props.items.slice(startIndex.value, startIndex.value + visibleCount.value).map((item, index) => ({
126
+ item,
127
+ index: startIndex.value + index,
128
+ })),
129
+ )
130
+ const totalHeight = computed(() => props.items.length * fixedItemSize.value)
131
+ const offsetY = computed(() => startIndex.value * fixedItemSize.value)
132
+
133
+ function scrollToIndex(index: number) {
134
+ const el = rootRef.value
135
+ if (!el || index < 0 || index >= props.items.length) return
136
+ const itemTop = index * fixedItemSize.value
137
+ const itemBottom = itemTop + fixedItemSize.value
138
+ const viewTop = el.scrollTop
139
+ const viewBottom = viewTop + el.clientHeight
140
+ if (itemTop >= viewTop && itemBottom <= viewBottom) {
141
+ return
142
+ }
143
+ const maxScroll = Math.max(0, totalHeight.value - el.clientHeight)
144
+ const centered = itemTop - (el.clientHeight - fixedItemSize.value) / 2
145
+ el.scrollTop = Math.min(maxScroll, Math.max(0, centered))
146
+ scrollTop.value = el.scrollTop
147
+ }
148
+
149
+ watch(
150
+ () => [props.activeIndex, props.items.length] as const,
151
+ async ([index, length]) => {
152
+ if (index == null || index < 0 || length === 0) return
153
+ await nextTick()
154
+ requestAnimationFrame(() => scrollToIndex(index))
155
+ },
156
+ { immediate: true },
157
+ )
158
+
159
+ defineExpose({ scrollToIndex })
160
+ </script>
161
+
162
+ <template>
163
+ <div
164
+ ref="rootRef"
165
+ class="rs-virtual-list"
166
+ :class="{ 'rs-virtual-list--fill': fillHeight }"
167
+ :style="rootStyle"
168
+ @scroll="scrollTop = ($event.target as HTMLElement).scrollTop"
169
+ >
170
+ <div class="rs-virtual-list__spacer" :style="{ height: `${totalHeight}px` }">
171
+ <div class="rs-virtual-list__items" :style="{ transform: `translateY(${offsetY}px)` }">
172
+ <div
173
+ v-for="entry in visibleItems"
174
+ :key="entry.index"
175
+ class="rs-virtual-list__item"
176
+ :class="{ 'rs-virtual-list__item--active': activeIndex === entry.index }"
177
+ :style="{ height: `${fixedItemSize}px` }"
178
+ >
179
+ <slot :item="entry.item" :index="entry.index">
180
+ {{ entry.item }}
181
+ </slot>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </template>
187
+
188
+ <style scoped>
189
+ .rs-virtual-list {
190
+ overflow: auto;
191
+ border: 1px solid var(--rs-border-subtle);
192
+ border-radius: var(--rs-virtual-list-radius, var(--rs-radius-sm));
193
+ }
194
+ .rs-virtual-list--fill {
195
+ flex: 1;
196
+ align-self: stretch;
197
+ width: 100%;
198
+ min-height: 0;
199
+ height: 0;
200
+ }
201
+ .rs-virtual-list__spacer {
202
+ position: relative;
203
+ }
204
+ .rs-virtual-list__items {
205
+ position: absolute;
206
+ left: 0;
207
+ right: 0;
208
+ top: 0;
209
+ }
210
+ .rs-virtual-list__item {
211
+ box-sizing: border-box;
212
+ }
213
+ .rs-virtual-list__item--active {
214
+ background: color-mix(in srgb, var(--rs-primary) 14%, var(--rs-surface));
215
+ box-shadow: inset 2px 0 0 var(--rs-primary);
216
+ }
217
+ </style>
@@ -0,0 +1,19 @@
1
+ /** 头像尺寸 */
2
+ export type RsAvatarSize = 'sm' | 'md' | 'lg'
3
+
4
+ /** 头像形状 */
5
+ export type RsAvatarShape = 'circle' | 'square'
6
+
7
+ /** 头像占位色调 */
8
+ export type RsAvatarTone = 'default' | 'primary' | 'success' | 'warning' | 'danger'
9
+
10
+ /**
11
+ * 从姓名提取缩写(最多 2 个字符)。
12
+ * @param name 显示名称
13
+ */
14
+ export function getAvatarInitials(name: string): string {
15
+ const parts = name.trim().split(/\s+/).filter(Boolean)
16
+ if (!parts.length) return '?'
17
+ if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase()
18
+ return `${parts[0]![0] ?? ''}${parts[1]![0] ?? ''}`.toUpperCase()
19
+ }
@@ -0,0 +1,19 @@
1
+ export interface RsBreadcrumbRenderItem {
2
+ key: string
3
+ label: string
4
+ href?: string
5
+ isLink: boolean
6
+ isCurrent: boolean
7
+ }
8
+
9
+ export function buildBreadcrumbRenderItems(
10
+ items: Array<{ label: string; to?: string; href?: string }>,
11
+ ): RsBreadcrumbRenderItem[] {
12
+ return items.map((item, index) => ({
13
+ key: `${item.label}-${index}`,
14
+ label: item.label,
15
+ href: item.href,
16
+ isLink: Boolean(item.to || item.href),
17
+ isCurrent: index === items.length - 1,
18
+ }))
19
+ }
@@ -0,0 +1,29 @@
1
+ export type RsClassValue =
2
+ | string
3
+ | number
4
+ | false
5
+ | null
6
+ | undefined
7
+ | Record<string, boolean | null | undefined>
8
+ | RsClassValue[]
9
+
10
+ function flattenClassValue(value: RsClassValue, output: string[]): void {
11
+ if (!value) return
12
+ if (typeof value === 'string' || typeof value === 'number') {
13
+ output.push(String(value))
14
+ return
15
+ }
16
+ if (Array.isArray(value)) {
17
+ value.forEach((item) => flattenClassValue(item, output))
18
+ return
19
+ }
20
+ Object.entries(value).forEach(([key, enabled]) => {
21
+ if (enabled) output.push(key)
22
+ })
23
+ }
24
+
25
+ export function rsCn(...values: RsClassValue[]): string {
26
+ const output: string[] = []
27
+ values.forEach((value) => flattenClassValue(value, output))
28
+ return Array.from(new Set(output.join(' ').split(/\s+/).filter(Boolean))).join(' ')
29
+ }
@@ -0,0 +1,106 @@
1
+ export type RsCodeEditorLanguage =
2
+ | 'plaintext'
3
+ | 'json'
4
+ | 'typescript'
5
+ | 'javascript'
6
+ | 'css'
7
+ | 'html'
8
+ | 'xml'
9
+ | 'sql'
10
+ | 'go'
11
+ | 'python'
12
+ | 'rust'
13
+ | 'java'
14
+ | 'cpp'
15
+ | 'c'
16
+ | 'shell'
17
+ | 'yaml'
18
+ | 'toml'
19
+ | 'markdown'
20
+ | 'vue'
21
+
22
+ export type RsCodeEditorTheme = 'auto' | 'light' | 'dark'
23
+ export type RsResolvedCodeEditorTheme = 'light' | 'dark'
24
+
25
+ export interface RsCodeEditorDiagnostic {
26
+ message: string
27
+ line?: number
28
+ column?: number
29
+ severity?: 'error' | 'warning' | 'info'
30
+ }
31
+
32
+ /** CodeMirror SQL 字段/表补全(对应 @codemirror/lang-sql SQLConfig 子集) */
33
+ export type RsCodeEditorSqlDialect = 'postgresql' | 'standard' | 'mysql'
34
+
35
+ export interface RsCodeEditorSqlColumn {
36
+ label: string
37
+ /** 展示用类型,如 integer / text */
38
+ detail?: string
39
+ type?: string
40
+ boost?: number
41
+ }
42
+
43
+ export type RsCodeEditorSqlNamespace =
44
+ | readonly (string | RsCodeEditorSqlColumn)[]
45
+ | { [name: string]: RsCodeEditorSqlNamespace }
46
+
47
+ export interface RsCodeEditorSqlConfig {
48
+ dialect?: RsCodeEditorSqlDialect
49
+ schema?: RsCodeEditorSqlNamespace
50
+ /** 顶层直接补全该表列(适合 WHERE 片段) */
51
+ defaultTable?: string
52
+ defaultSchema?: string
53
+ }
54
+
55
+ const SUPPORTED_LANGUAGES: RsCodeEditorLanguage[] = [
56
+ 'plaintext', 'json', 'typescript', 'javascript', 'css', 'html', 'xml', 'sql',
57
+ 'go', 'python', 'rust', 'java', 'cpp', 'c', 'shell', 'yaml', 'toml', 'markdown', 'vue',
58
+ ]
59
+
60
+ /** 语言 → 工具栏展示名称映射 */
61
+ const LANGUAGE_DISPLAY_NAME: Record<RsCodeEditorLanguage, string> = {
62
+ plaintext: 'Plain Text',
63
+ json: 'JSON',
64
+ typescript: 'TypeScript',
65
+ javascript: 'JavaScript',
66
+ css: 'CSS',
67
+ html: 'HTML',
68
+ xml: 'XML',
69
+ sql: 'SQL',
70
+ go: 'Go',
71
+ python: 'Python',
72
+ rust: 'Rust',
73
+ java: 'Java',
74
+ cpp: 'C++',
75
+ c: 'C',
76
+ shell: 'Shell',
77
+ yaml: 'YAML',
78
+ toml: 'TOML',
79
+ markdown: 'Markdown',
80
+ vue: 'Vue',
81
+ }
82
+
83
+ export function resolveCodeEditorLanguage(language?: string): RsCodeEditorLanguage {
84
+ return (SUPPORTED_LANGUAGES as string[]).includes(language ?? '')
85
+ ? (language as RsCodeEditorLanguage)
86
+ : 'plaintext'
87
+ }
88
+
89
+ export function codeEditorLanguageLabel(language?: string): string {
90
+ const lang = resolveCodeEditorLanguage(language)
91
+ return LANGUAGE_DISPLAY_NAME[lang] ?? lang
92
+ }
93
+
94
+ export function readDocumentTheme(): RsResolvedCodeEditorTheme {
95
+ if (typeof document === 'undefined') return 'dark'
96
+ return document.documentElement.dataset.rsTheme === 'light' ? 'light' : 'dark'
97
+ }
98
+
99
+ export function resolveCodeEditorTheme(theme: RsCodeEditorTheme = 'auto'): RsResolvedCodeEditorTheme {
100
+ return theme === 'auto' ? readDocumentTheme() : theme
101
+ }
102
+
103
+ export function resolveCodeEditorSize(height?: number | string): string {
104
+ if (height === undefined) return '20rem'
105
+ return typeof height === 'number' ? `${height}px` : height
106
+ }
@@ -0,0 +1,40 @@
1
+ import { keymap } from '@codemirror/view'
2
+ import type { Extension } from '@codemirror/state'
3
+ import { clearGhostCompletion, setGhostCompletion } from './code-mirror-ghost'
4
+ import { posToLineColumn } from './code-mirror-intel'
5
+
6
+ export interface CodeMirrorCompletionHandlers {
7
+ completionRequest?: (prefix: string, suffix: string, line: number, column: number) => Promise<string | null>
8
+ }
9
+
10
+ export function codeMirrorCompletionExtensions(handlers: () => CodeMirrorCompletionHandlers): Extension[] {
11
+ return [
12
+ keymap.of([
13
+ {
14
+ key: 'Ctrl-Space',
15
+ mac: 'Cmd-Space',
16
+ run: (view) => {
17
+ const h = handlers()
18
+ if (!h.completionRequest) return false
19
+ clearGhostCompletion(view)
20
+ const pos = view.state.selection.main.head
21
+ const prefix = view.state.doc.sliceString(0, pos)
22
+ const suffix = view.state.doc.sliceString(pos)
23
+ const { line, column } = posToLineColumn(view, pos)
24
+ void h.completionRequest(prefix, suffix, line, column).then((text) => {
25
+ if (!text?.trim()) return
26
+ const latestPos = view.state.selection.main.head
27
+ if (view.state.doc.sliceString(latestPos) !== suffix) return
28
+ view.dispatch({
29
+ changes: { from: latestPos, to: latestPos, insert: text },
30
+ selection: { anchor: latestPos + text.length },
31
+ effects: setGhostCompletion.of(null),
32
+ })
33
+ view.focus()
34
+ })
35
+ return true
36
+ },
37
+ },
38
+ ]),
39
+ ]
40
+ }