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,184 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import RsButton from './RsButton.vue'
4
+ import RsInput from './RsInput.vue'
5
+ import RsSelect from './RsSelect.vue'
6
+ import { clampPage, DEFAULT_PAGE_SIZE_OPTIONS, getPageCount, getPaginationRange } from './pagination-utils'
7
+ import { useRsI18n } from '../composables/useRsI18n'
8
+
9
+ export type RsPaginationSize = 'sm' | 'md'
10
+
11
+ const page = defineModel<number>('page', { default: 1 })
12
+ const pageSize = defineModel<number>('pageSize', { default: 20 })
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ total: number
17
+ siblingCount?: number
18
+ showSummary?: boolean
19
+ showPageSize?: boolean
20
+ showQuickJumper?: boolean
21
+ pageSizeOptions?: readonly number[]
22
+ disabled?: boolean
23
+ size?: RsPaginationSize
24
+ }>(),
25
+ {
26
+ siblingCount: 1,
27
+ showSummary: true,
28
+ showPageSize: false,
29
+ showQuickJumper: false,
30
+ pageSizeOptions: () => DEFAULT_PAGE_SIZE_OPTIONS,
31
+ disabled: false,
32
+ size: 'md',
33
+ },
34
+ )
35
+
36
+ const { t } = useRsI18n()
37
+ const jumpPageInput = ref(String(page.value))
38
+ const pageCount = computed(() => getPageCount(props.total, pageSize.value))
39
+ const range = computed(() => getPaginationRange(page.value, pageCount.value, props.siblingCount))
40
+ const summaryText = computed(() => t('pagination.summary', 'Total {total} items').replace('{total}', String(props.total)))
41
+ const pageSizeSelectValue = computed({
42
+ get: () => String(pageSize.value),
43
+ set: (value: string) => {
44
+ pageSize.value = Number(value)
45
+ },
46
+ })
47
+ const pageSizeSelectOptions = computed(() =>
48
+ props.pageSizeOptions.map((option) => ({
49
+ label: `${option} / ${t('pagination.page')}`,
50
+ value: String(option),
51
+ })),
52
+ )
53
+ const showJumper = computed(() => props.showQuickJumper && pageCount.value > 1)
54
+
55
+ watch(pageCount, (count) => {
56
+ page.value = clampPage(page.value, count)
57
+ })
58
+
59
+ watch(page, (value) => {
60
+ jumpPageInput.value = String(value)
61
+ })
62
+
63
+ function go(next: number): void {
64
+ if (props.disabled) return
65
+ page.value = clampPage(next, pageCount.value)
66
+ }
67
+
68
+ function confirmJump(): void {
69
+ if (props.disabled) return
70
+ const parsed = Number.parseInt(jumpPageInput.value.trim(), 10)
71
+ if (Number.isNaN(parsed)) {
72
+ jumpPageInput.value = String(page.value)
73
+ return
74
+ }
75
+ go(parsed)
76
+ jumpPageInput.value = String(page.value)
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <nav class="rs-pagination" :class="`rs-pagination--${size}`" :aria-label="t('pagination.label')">
82
+ <span v-if="showSummary" class="rs-pagination__summary">
83
+ {{ summaryText }}
84
+ </span>
85
+ <div class="rs-pagination__buttons">
86
+ <RsButton variant="ghost" size="sm" :disabled="disabled || page <= 1" @click="go(page - 1)">
87
+ {{ t('pagination.prev') }}
88
+ </RsButton>
89
+ <template v-for="(item, index) in range" :key="`${item}-${index}`">
90
+ <span v-if="item === 'ellipsis'" class="rs-pagination__ellipsis">...</span>
91
+ <RsButton
92
+ v-else
93
+ :variant="item === page ? 'primary' : 'ghost'"
94
+ size="sm"
95
+ :disabled="disabled"
96
+ @click="go(item)"
97
+ >
98
+ {{ item }}
99
+ </RsButton>
100
+ </template>
101
+ <RsButton variant="ghost" size="sm" :disabled="disabled || page >= pageCount" @click="go(page + 1)">
102
+ {{ t('pagination.next') }}
103
+ </RsButton>
104
+ </div>
105
+ <div v-if="showJumper" class="rs-pagination__jumper">
106
+ <span class="rs-pagination__jumper-label">{{ t('pagination.jumpTo') }}</span>
107
+ <div class="rs-pagination__jumper-input">
108
+ <RsInput
109
+ v-model="jumpPageInput"
110
+ size="sm"
111
+ :disabled="disabled"
112
+ @keydown.enter="confirmJump"
113
+ />
114
+ </div>
115
+ <span v-if="t('pagination.jumpPageSuffix')" class="rs-pagination__jumper-suffix">
116
+ {{ t('pagination.jumpPageSuffix') }}
117
+ </span>
118
+ <RsButton
119
+ variant="ghost"
120
+ size="sm"
121
+ class="rs-pagination__jumper-btn"
122
+ :disabled="disabled"
123
+ @click="confirmJump"
124
+ >
125
+ {{ t('pagination.jumpConfirm') }}
126
+ </RsButton>
127
+ </div>
128
+ <div v-if="showPageSize" class="rs-pagination__size">
129
+ <RsSelect
130
+ v-model="pageSizeSelectValue"
131
+ :options="pageSizeSelectOptions"
132
+ size="sm"
133
+ :disabled="disabled"
134
+ :placeholder="t('select.placeholder')"
135
+ />
136
+ </div>
137
+ </nav>
138
+ </template>
139
+
140
+ <style scoped>
141
+ .rs-pagination {
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ align-items: center;
145
+ gap: var(--rs-space-sm);
146
+ color: var(--rs-muted);
147
+ font-size: var(--rs-font-size-sm);
148
+ }
149
+ .rs-pagination__buttons {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ gap: var(--rs-space-xs);
153
+ }
154
+ .rs-pagination__ellipsis {
155
+ padding: 0 var(--rs-space-xs);
156
+ font-size: var(--rs-font-size-xs);
157
+ line-height: var(--rs-control-height-sm);
158
+ }
159
+ .rs-pagination__jumper {
160
+ display: inline-flex;
161
+ align-items: center;
162
+ gap: var(--rs-space-xs);
163
+ }
164
+ .rs-pagination__jumper-label,
165
+ .rs-pagination__jumper-suffix {
166
+ flex-shrink: 0;
167
+ color: var(--rs-muted);
168
+ font-size: var(--rs-font-size-xs);
169
+ line-height: var(--rs-line-height-tight);
170
+ }
171
+ .rs-pagination__jumper-input {
172
+ width: 2.75rem;
173
+ }
174
+ .rs-pagination__jumper-input :deep(.rs-input-field) {
175
+ width: 100%;
176
+ }
177
+ .rs-pagination__jumper-btn {
178
+ flex-shrink: 0;
179
+ }
180
+ .rs-pagination__size {
181
+ width: 7.25rem;
182
+ min-width: 7.25rem;
183
+ }
184
+ </style>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from './reka'
4
+ import type { RsPopoverAlign, RsPopoverSide, RsPopoverWidth } from './popover-utils'
5
+
6
+ const open = defineModel<boolean>('open', { default: false })
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ side?: RsPopoverSide
11
+ align?: RsPopoverAlign
12
+ sideOffset?: number
13
+ modal?: boolean
14
+ width?: RsPopoverWidth
15
+ lazyMount?: boolean
16
+ forceMount?: boolean
17
+ }>(),
18
+ {
19
+ side: 'bottom',
20
+ align: 'start',
21
+ sideOffset: 6,
22
+ modal: false,
23
+ width: 'md',
24
+ lazyMount: true,
25
+ forceMount: false,
26
+ },
27
+ )
28
+
29
+ const contentForceMount = computed(() => props.forceMount || !props.lazyMount)
30
+ const portalMounted = computed(() => props.forceMount || !props.lazyMount || open.value)
31
+ </script>
32
+
33
+ <template>
34
+ <PopoverRoot v-model:open="open" :modal="modal">
35
+ <PopoverTrigger as-child>
36
+ <slot />
37
+ </PopoverTrigger>
38
+ <PopoverPortal v-if="portalMounted">
39
+ <PopoverContent
40
+ class="rs-popover__content"
41
+ :class="`rs-popover__content--${width}`"
42
+ :side="side"
43
+ :align="align"
44
+ :side-offset="sideOffset"
45
+ :force-mount="contentForceMount"
46
+ >
47
+ <slot name="content" />
48
+ </PopoverContent>
49
+ </PopoverPortal>
50
+ </PopoverRoot>
51
+ </template>
52
+
53
+ <style>
54
+ .rs-popover__content {
55
+ z-index: var(--rs-z-dropdown);
56
+ padding: 0.75rem;
57
+ border-radius: var(--rs-radius-sm);
58
+ border: 1px solid var(--rs-border);
59
+ background: var(--rs-surface-elevated);
60
+ box-shadow: var(--rs-shadow);
61
+ outline: none;
62
+ overflow: visible;
63
+ }
64
+ .rs-popover__content--sm {
65
+ width: 12rem;
66
+ }
67
+ .rs-popover__content--md {
68
+ width: 16rem;
69
+ }
70
+ .rs-popover__content--lg {
71
+ width: 22rem;
72
+ }
73
+ .rs-popover__content--auto {
74
+ width: auto;
75
+ min-width: 10rem;
76
+ }
77
+ </style>
@@ -0,0 +1,160 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
3
+ import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
4
+ import { EditorState } from '@codemirror/state'
5
+ import { EditorView, drawSelection, dropCursor, keymap, placeholder } from '@codemirror/view'
6
+
7
+ const model = defineModel<string>({ default: '' })
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ readonly?: boolean
12
+ placeholder?: string
13
+ minHeight?: string
14
+ }>(),
15
+ {
16
+ readonly: false,
17
+ placeholder: '',
18
+ minHeight: '12rem',
19
+ },
20
+ )
21
+
22
+ const emit = defineEmits<{
23
+ selectionChange: [{ start: number; end: number }]
24
+ }>()
25
+
26
+ const rootEl = ref<HTMLElement | null>(null)
27
+ const view = shallowRef<EditorView | null>(null)
28
+ let unmounted = false
29
+
30
+ function buildExtensions() {
31
+ const exts = [
32
+ history(),
33
+ drawSelection(),
34
+ dropCursor(),
35
+ keymap.of([...defaultKeymap, ...historyKeymap]),
36
+ EditorView.lineWrapping,
37
+ EditorView.updateListener.of((update) => {
38
+ if (update.docChanged && !props.readonly) {
39
+ const next = update.state.doc.toString()
40
+ if (next !== model.value) {
41
+ model.value = next
42
+ }
43
+ }
44
+ if (update.selectionSet) {
45
+ const { from, to } = update.state.selection.main
46
+ emit('selectionChange', { start: from, end: to })
47
+ }
48
+ }),
49
+ EditorView.editable.of(!props.readonly),
50
+ EditorView.theme({
51
+ '&': { height: '100%', backgroundColor: 'transparent' },
52
+ '.cm-scroller': {
53
+ fontFamily: "'Noto Serif SC', 'Source Han Serif SC', 'Songti SC', Georgia, serif",
54
+ fontSize: '1.05rem',
55
+ lineHeight: '1.9',
56
+ letterSpacing: '0.02em',
57
+ },
58
+ '.cm-content': {
59
+ padding: '0.75rem 1rem',
60
+ caretColor: 'var(--rs-primary)',
61
+ },
62
+ '&.cm-focused': { outline: 'none' },
63
+ '.cm-cursor': { borderLeftColor: 'var(--rs-primary)' },
64
+ '.cm-selectionBackground, ::selection': {
65
+ backgroundColor: 'color-mix(in srgb, var(--rs-primary) 18%, transparent) !important',
66
+ },
67
+ }),
68
+ ]
69
+ if (props.placeholder) {
70
+ exts.push(placeholder(props.placeholder))
71
+ }
72
+ return exts
73
+ }
74
+
75
+ function initEditor() {
76
+ if (!rootEl.value || unmounted) return
77
+ view.value?.destroy()
78
+ const state = EditorState.create({
79
+ doc: model.value,
80
+ extensions: buildExtensions(),
81
+ })
82
+ view.value = new EditorView({ state, parent: rootEl.value })
83
+ }
84
+
85
+ function syncDocFromOutside(val: string) {
86
+ const v = view.value
87
+ if (!v) return
88
+ const cur = v.state.doc.toString()
89
+ if (cur === val) return
90
+ v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: val } })
91
+ }
92
+
93
+ function getSelection() {
94
+ const v = view.value
95
+ if (!v) return { start: 0, end: 0 }
96
+ const { from, to } = v.state.selection.main
97
+ return { start: from, end: to }
98
+ }
99
+
100
+ function focus() {
101
+ view.value?.focus()
102
+ }
103
+
104
+ defineExpose({ getSelection, focus, view })
105
+
106
+ onMounted(() => {
107
+ unmounted = false
108
+ initEditor()
109
+ })
110
+
111
+ onUnmounted(() => {
112
+ unmounted = true
113
+ view.value?.destroy()
114
+ view.value = null
115
+ })
116
+
117
+ watch(
118
+ () => model.value,
119
+ (val) => syncDocFromOutside(val ?? ''),
120
+ )
121
+
122
+ watch(
123
+ () => props.readonly,
124
+ () => initEditor(),
125
+ )
126
+ </script>
127
+
128
+ <template>
129
+ <div
130
+ class="rs-prose-editor"
131
+ :class="{ 'rs-prose-editor--readonly': readonly }"
132
+ :style="{ minHeight }"
133
+ >
134
+ <div ref="rootEl" class="rs-prose-editor__cm" />
135
+ </div>
136
+ </template>
137
+
138
+ <style scoped>
139
+ .rs-prose-editor {
140
+ display: flex;
141
+ flex: 1;
142
+ flex-direction: column;
143
+ min-height: 0;
144
+ overflow: hidden;
145
+ }
146
+
147
+ .rs-prose-editor__cm {
148
+ flex: 1;
149
+ min-height: 0;
150
+ overflow: auto;
151
+ }
152
+
153
+ .rs-prose-editor__cm :deep(.cm-editor) {
154
+ height: 100%;
155
+ }
156
+
157
+ .rs-prose-editor--readonly {
158
+ opacity: 0.72;
159
+ }
160
+ </style>
@@ -0,0 +1,171 @@
1
+ <script setup lang="ts">
2
+ import { computed, useTemplateRef } from 'vue'
3
+ import {
4
+ ScrollAreaCorner,
5
+ ScrollAreaRoot,
6
+ ScrollAreaScrollbar,
7
+ ScrollAreaThumb,
8
+ ScrollAreaViewport,
9
+ } from './reka'
10
+ import {
11
+ resolveScrollbarSize,
12
+ type RsScrollbarOrientation,
13
+ type RsScrollbarType,
14
+ } from './scrollbar-utils'
15
+
16
+ const props = withDefaults(
17
+ defineProps<{
18
+ type?: RsScrollbarType
19
+ scrollHideDelay?: number
20
+ orientation?: RsScrollbarOrientation
21
+ height?: string | number
22
+ maxHeight?: string | number
23
+ minHeight?: string | number
24
+ }>(),
25
+ {
26
+ type: 'hover',
27
+ scrollHideDelay: 600,
28
+ orientation: 'both',
29
+ height: '14rem',
30
+ },
31
+ )
32
+
33
+ const rootRef = useTemplateRef<InstanceType<typeof ScrollAreaRoot>>('rootRef')
34
+
35
+ const rootStyle = computed(() => {
36
+ const style: Record<string, string> = {}
37
+ const height = resolveScrollbarSize(props.height)
38
+ const maxHeight = resolveScrollbarSize(props.maxHeight)
39
+ const minHeight = resolveScrollbarSize(props.minHeight)
40
+ if (height) style.height = height
41
+ if (maxHeight) style.maxHeight = maxHeight
42
+ if (minHeight) style.minHeight = minHeight
43
+ return Object.keys(style).length > 0 ? style : undefined
44
+ })
45
+
46
+ const showVertical = computed(
47
+ () => props.orientation === 'vertical' || props.orientation === 'both',
48
+ )
49
+ const showHorizontal = computed(
50
+ () => props.orientation === 'horizontal' || props.orientation === 'both',
51
+ )
52
+
53
+ defineExpose({
54
+ scrollTop: () => rootRef.value?.scrollTop(),
55
+ scrollTopLeft: () => rootRef.value?.scrollTopLeft(),
56
+ getViewport: () => rootRef.value?.viewport,
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <ScrollAreaRoot
62
+ ref="rootRef"
63
+ class="rs-scrollbar"
64
+ :type="type"
65
+ :scroll-hide-delay="scrollHideDelay"
66
+ :style="rootStyle"
67
+ >
68
+ <ScrollAreaViewport class="rs-scrollbar__viewport">
69
+ <div class="rs-scrollbar__content">
70
+ <slot />
71
+ </div>
72
+ </ScrollAreaViewport>
73
+
74
+ <ScrollAreaScrollbar
75
+ v-if="showVertical"
76
+ class="rs-scrollbar__bar rs-scrollbar__bar--vertical"
77
+ orientation="vertical"
78
+ >
79
+ <ScrollAreaThumb class="rs-scrollbar__thumb" />
80
+ </ScrollAreaScrollbar>
81
+
82
+ <ScrollAreaScrollbar
83
+ v-if="showHorizontal"
84
+ class="rs-scrollbar__bar rs-scrollbar__bar--horizontal"
85
+ orientation="horizontal"
86
+ >
87
+ <ScrollAreaThumb class="rs-scrollbar__thumb" />
88
+ </ScrollAreaScrollbar>
89
+
90
+ <ScrollAreaCorner v-if="showVertical && showHorizontal" class="rs-scrollbar__corner" />
91
+ </ScrollAreaRoot>
92
+ </template>
93
+
94
+ <style>
95
+ .rs-scrollbar {
96
+ position: relative;
97
+ width: 100%;
98
+ overflow: hidden;
99
+ border: 1px solid var(--rs-border);
100
+ border-radius: var(--rs-radius-sm);
101
+ background: var(--rs-surface);
102
+ }
103
+
104
+ .rs-scrollbar__viewport {
105
+ width: 100%;
106
+ height: 100%;
107
+ border-radius: inherit;
108
+ }
109
+
110
+ /* Reka 注入的 Primitive div。
111
+ 必须用 height:100% 而非 min-height:100%:
112
+ 只有父元素拥有"明确高度"时,子元素的 min-height:100% 才能解析(CSS 规范)。
113
+ height:100% 让 Primitive 与 viewport 等高(= 明确高度),
114
+ 超出内容通过 overflow:visible 溢出 → viewport(overflow:scroll) 正常滚动。 */
115
+ .rs-scrollbar__viewport > div {
116
+ height: 100%;
117
+ }
118
+
119
+ /* rs-scrollbar__content 同理:height:100% = Primitive 的明确高度。
120
+ slot 内容若超出则溢出,滚动仍然工作;短内容时填满整个滚动区(空白可捕获右键)。 */
121
+ .rs-scrollbar__content {
122
+ min-width: 0;
123
+ height: 100%;
124
+ display: flex;
125
+ flex-direction: column;
126
+ }
127
+
128
+ .rs-scrollbar__bar {
129
+ user-select: none;
130
+ touch-action: none;
131
+ display: flex;
132
+ padding: var(--rs-scrollbar-padding);
133
+ background: transparent;
134
+ transition: background var(--rs-transition-fast);
135
+ }
136
+
137
+ .rs-scrollbar__bar[data-orientation='vertical'] {
138
+ width: var(--rs-scrollbar-size);
139
+ }
140
+
141
+ .rs-scrollbar__bar[data-orientation='horizontal'] {
142
+ flex-direction: column;
143
+ height: var(--rs-scrollbar-size);
144
+ }
145
+
146
+ .rs-scrollbar__bar:hover {
147
+ background: color-mix(in srgb, var(--rs-border-subtle) 80%, transparent);
148
+ }
149
+
150
+ .rs-scrollbar__thumb {
151
+ flex: 1;
152
+ border-radius: var(--rs-radius-full);
153
+ background: color-mix(in srgb, var(--rs-muted) 60%, transparent);
154
+ position: relative;
155
+ transition: background var(--rs-transition-fast);
156
+ }
157
+
158
+ .rs-scrollbar__thumb::before {
159
+ content: '';
160
+ position: absolute;
161
+ inset: -0.375rem;
162
+ }
163
+
164
+ .rs-scrollbar__thumb:hover {
165
+ background: color-mix(in srgb, var(--rs-primary) 40%, var(--rs-muted));
166
+ }
167
+
168
+ .rs-scrollbar__corner {
169
+ background: var(--rs-surface);
170
+ }
171
+ </style>