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,237 @@
1
+ <script setup lang="ts">
2
+ import { computed, useSlots } from 'vue'
3
+ import type { RsRadius } from '../theme/types'
4
+ import { Primitive } from './reka'
5
+ import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
6
+
7
+ export type RsCardVariant = 'grouped' | 'plain' | 'outlined' | 'filled' | 'glass'
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ as?: string
12
+ title?: string
13
+ description?: string
14
+ padding?: boolean
15
+ elevated?: boolean
16
+ /** grouped=mac 分组面板;plain=mac 小组件;outlined/filled=Material 3;glass=mac 毛玻璃 */
17
+ variant?: RsCardVariant
18
+ /** 圆角档位;默认 lg。直角面板传 `none`。 */
19
+ radius?: RsRadius
20
+ }>(),
21
+ {
22
+ as: 'section',
23
+ padding: true,
24
+ elevated: false,
25
+ variant: 'grouped',
26
+ },
27
+ )
28
+
29
+ const slots = useSlots()
30
+ const hasHeader = computed(() => Boolean(props.title || props.description || slots.header))
31
+ const resolvedRadius = useResolvedRsRadius(() => props.radius, 'lg')
32
+ const rootStyle = computed(() => ({
33
+ '--rs-card-radius': rsRadiusCss(resolvedRadius.value),
34
+ }))
35
+ </script>
36
+
37
+ <template>
38
+ <Primitive
39
+ :as="as"
40
+ class="rs-card"
41
+ :class="[
42
+ `rs-card--${variant}`,
43
+ { 'rs-card--elevated': elevated },
44
+ ]"
45
+ :style="rootStyle"
46
+ >
47
+ <header v-if="hasHeader" class="rs-card__header">
48
+ <div class="rs-card__heading">
49
+ <slot name="header">
50
+ <h3 v-if="title" class="rs-card__title">{{ title }}</h3>
51
+ <p v-if="description" class="rs-card__description">{{ description }}</p>
52
+ </slot>
53
+ </div>
54
+ <div v-if="$slots.actions" class="rs-card__actions">
55
+ <slot name="actions" />
56
+ </div>
57
+ </header>
58
+ <div class="rs-card__body" :class="{ 'rs-card__body--padded': padding }">
59
+ <slot />
60
+ </div>
61
+ </Primitive>
62
+ </template>
63
+
64
+ <style>
65
+ .rs-card {
66
+ position: relative;
67
+ overflow: hidden;
68
+ border-radius: var(--rs-card-radius, var(--rs-radius-lg));
69
+ border: 1px solid var(--rs-card-border);
70
+ background: var(--rs-card-body-bg);
71
+ box-shadow: var(--rs-card-shadow);
72
+ isolation: isolate;
73
+ }
74
+
75
+ /* ── grouped:macOS 系统设置分组面板(header 微分层 + inset 高光)── */
76
+ [data-rs-theme='light'] .rs-card--grouped {
77
+ background: color-mix(in srgb, var(--rs-card-body-bg) 94%, transparent);
78
+ backdrop-filter: blur(20px) saturate(150%);
79
+ -webkit-backdrop-filter: blur(20px) saturate(150%);
80
+ }
81
+ .rs-card--grouped .rs-card__header {
82
+ background: var(--rs-card-header-bg);
83
+ border-bottom: 1px solid var(--rs-card-separator);
84
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.05);
85
+ }
86
+ [data-rs-theme='light'] .rs-card--grouped .rs-card__header {
87
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.75);
88
+ }
89
+ .rs-card--grouped .rs-card__body {
90
+ background: var(--rs-card-body-bg);
91
+ }
92
+ [data-rs-theme='light'] .rs-card--grouped .rs-card__body {
93
+ background: transparent;
94
+ }
95
+ .rs-card--grouped.rs-card--elevated {
96
+ box-shadow: var(--rs-card-shadow-elevated);
97
+ }
98
+
99
+ /* ── plain:macOS 小组件 / 仪表盘(单表面、弱分隔)── */
100
+ .rs-card--plain {
101
+ border-color: var(--rs-card-separator);
102
+ box-shadow: none;
103
+ }
104
+ .rs-card--plain .rs-card__header {
105
+ padding-block: 0.625rem;
106
+ background: transparent;
107
+ border-bottom: 1px solid var(--rs-card-separator);
108
+ box-shadow: none;
109
+ }
110
+ .rs-card--plain .rs-card__title {
111
+ font-size: var(--rs-font-size-xs);
112
+ font-weight: 500;
113
+ color: var(--rs-card-description-fg);
114
+ letter-spacing: 0;
115
+ }
116
+ .rs-card--plain .rs-card__body {
117
+ background: transparent;
118
+ }
119
+ .rs-card--plain.rs-card--elevated {
120
+ box-shadow: var(--rs-card-shadow-elevated);
121
+ }
122
+
123
+ /* ── outlined:Material 3 Outlined Card(描边、无 header 底色)── */
124
+ .rs-card--outlined {
125
+ border: 1px solid var(--rs-card-border);
126
+ box-shadow: none;
127
+ background: var(--rs-card-body-bg);
128
+ }
129
+ .rs-card--outlined .rs-card__header {
130
+ padding-block: 1rem;
131
+ background: transparent;
132
+ border-bottom: 1px solid var(--rs-card-separator);
133
+ box-shadow: none;
134
+ }
135
+ .rs-card--outlined .rs-card__title {
136
+ font-size: var(--rs-font-size-base);
137
+ font-weight: 500;
138
+ letter-spacing: 0;
139
+ }
140
+ .rs-card--outlined .rs-card__body {
141
+ background: transparent;
142
+ }
143
+ .rs-card--outlined.rs-card--elevated {
144
+ box-shadow: var(--rs-card-filled-elevated-shadow);
145
+ }
146
+
147
+ /* ── filled:Material 3 Filled Card(色调填充、无边框)── */
148
+ .rs-card--filled {
149
+ border-color: transparent;
150
+ background: var(--rs-card-filled-bg);
151
+ box-shadow: none;
152
+ }
153
+ .rs-card--filled .rs-card__header {
154
+ background: transparent;
155
+ border-bottom: 1px solid var(--rs-card-separator);
156
+ box-shadow: none;
157
+ }
158
+ .rs-card--filled .rs-card__title {
159
+ font-size: var(--rs-font-size-base);
160
+ font-weight: 500;
161
+ letter-spacing: 0;
162
+ }
163
+ .rs-card--filled .rs-card__body {
164
+ background: transparent;
165
+ }
166
+ .rs-card--filled.rs-card--elevated {
167
+ box-shadow: var(--rs-card-filled-elevated-shadow);
168
+ }
169
+
170
+ /* ── glass:macOS Vibrancy 毛玻璃(强模糊 + 半透明)── */
171
+ .rs-card--glass {
172
+ border-color: var(--rs-card-glass-border);
173
+ background: var(--rs-card-glass-bg);
174
+ backdrop-filter: blur(28px) saturate(180%);
175
+ -webkit-backdrop-filter: blur(28px) saturate(180%);
176
+ box-shadow:
177
+ 0 0 0 0.5px rgb(255 255 255 / 0.12),
178
+ 0 8px 24px rgb(0 0 0 / 0.12);
179
+ }
180
+ [data-rs-theme='light'] .rs-card--glass {
181
+ box-shadow:
182
+ 0 0 0 0.5px rgb(255 255 255 / 0.6),
183
+ 0 8px 24px rgb(0 0 0 / 0.08);
184
+ }
185
+ .rs-card--glass .rs-card__header {
186
+ background: rgb(255 255 255 / 0.04);
187
+ border-bottom: 1px solid var(--rs-card-glass-border);
188
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.08);
189
+ }
190
+ [data-rs-theme='light'] .rs-card--glass .rs-card__header {
191
+ background: rgb(255 255 255 / 0.35);
192
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.8);
193
+ }
194
+ .rs-card--glass .rs-card__body {
195
+ background: transparent;
196
+ }
197
+ .rs-card--glass.rs-card--elevated {
198
+ box-shadow:
199
+ 0 0 0 0.5px rgb(255 255 255 / 0.14),
200
+ 0 12px 32px rgb(0 0 0 / 0.18);
201
+ }
202
+
203
+ /* ── 公共结构 ── */
204
+ .rs-card__header {
205
+ display: flex;
206
+ align-items: flex-start;
207
+ justify-content: space-between;
208
+ gap: var(--rs-space-md);
209
+ flex-shrink: 0;
210
+ padding: 0.75rem 1rem 0.75rem 1.25rem;
211
+ }
212
+ .rs-card__heading {
213
+ min-width: 0;
214
+ }
215
+ .rs-card__title {
216
+ margin: 0;
217
+ font-size: var(--rs-font-size-sm);
218
+ font-weight: 600;
219
+ letter-spacing: -0.01em;
220
+ color: var(--rs-card-title-fg);
221
+ }
222
+ .rs-card__description {
223
+ margin: 0.2rem 0 0;
224
+ font-size: var(--rs-font-size-xs);
225
+ color: var(--rs-card-description-fg);
226
+ line-height: var(--rs-line-height-normal);
227
+ }
228
+ .rs-card__actions {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: var(--rs-space-xs);
232
+ flex-shrink: 0;
233
+ }
234
+ .rs-card__body--padded {
235
+ padding: 1.25rem;
236
+ }
237
+ </style>
@@ -0,0 +1,189 @@
1
+ <script setup lang="ts">
2
+ import { computed, useId } from 'vue'
3
+ import type { RsComponentSize } from '../theme/types'
4
+ import { useResolvedRsComponentSize } from './resolve-size'
5
+
6
+ const model = defineModel<boolean>({ default: false })
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ /** 半选(仅视觉;点击仍按 model 取反) */
11
+ indeterminate?: boolean
12
+ disabled?: boolean
13
+ size?: RsComponentSize
14
+ /** 无障碍名称;有默认插槽文案时可省略 */
15
+ ariaLabel?: string
16
+ id?: string
17
+ }>(),
18
+ {
19
+ indeterminate: false,
20
+ disabled: false,
21
+ },
22
+ )
23
+
24
+ const emit = defineEmits<{
25
+ change: [value: boolean]
26
+ }>()
27
+
28
+ const autoId = useId()
29
+ const inputId = computed(() => props.id || autoId)
30
+ const resolvedSize = useResolvedRsComponentSize(() => props.size)
31
+
32
+ const rootClass = computed(() => [
33
+ 'rs-checkbox',
34
+ `rs-checkbox--${resolvedSize.value}`,
35
+ {
36
+ 'rs-checkbox--checked': model.value && !props.indeterminate,
37
+ 'rs-checkbox--indeterminate': props.indeterminate,
38
+ 'rs-checkbox--disabled': props.disabled,
39
+ },
40
+ ])
41
+
42
+ function onChange(event: Event): void {
43
+ if (props.disabled) return
44
+ const checked = (event.target as HTMLInputElement).checked
45
+ model.value = checked
46
+ emit('change', checked)
47
+ }
48
+ </script>
49
+
50
+ <template>
51
+ <label :class="rootClass" :for="inputId">
52
+ <input
53
+ :id="inputId"
54
+ class="rs-checkbox__input"
55
+ type="checkbox"
56
+ :checked="model"
57
+ :disabled="disabled"
58
+ :aria-checked="indeterminate ? 'mixed' : model"
59
+ :aria-label="ariaLabel"
60
+ @change="onChange"
61
+ @click.stop
62
+ >
63
+ <span class="rs-checkbox__box" aria-hidden="true" />
64
+ <span v-if="$slots.default" class="rs-checkbox__label">
65
+ <slot />
66
+ </span>
67
+ </label>
68
+ </template>
69
+
70
+ <style>
71
+ .rs-checkbox {
72
+ position: relative;
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--rs-space-xs);
76
+ vertical-align: middle;
77
+ line-height: 1;
78
+ cursor: pointer;
79
+ user-select: none;
80
+ color: var(--rs-text);
81
+ }
82
+
83
+ .rs-checkbox--disabled {
84
+ cursor: not-allowed;
85
+ opacity: 0.55;
86
+ }
87
+
88
+ .rs-checkbox__input {
89
+ position: absolute;
90
+ width: 1px;
91
+ height: 1px;
92
+ margin: -1px;
93
+ padding: 0;
94
+ overflow: hidden;
95
+ clip: rect(0, 0, 0, 0);
96
+ white-space: nowrap;
97
+ border: 0;
98
+ }
99
+
100
+ .rs-checkbox__box {
101
+ position: relative;
102
+ display: inline-flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ flex-shrink: 0;
106
+ box-sizing: border-box;
107
+ border: 1px solid var(--rs-border);
108
+ border-radius: var(--rs-radius-xs, 4px);
109
+ background: var(--rs-surface);
110
+ transition:
111
+ border-color var(--rs-transition-fast, 0.12s ease),
112
+ background-color var(--rs-transition-fast, 0.12s ease);
113
+ }
114
+
115
+ .rs-checkbox--ssm .rs-checkbox__box {
116
+ width: 0.75rem;
117
+ height: 0.75rem;
118
+ }
119
+
120
+ .rs-checkbox--sm .rs-checkbox__box {
121
+ width: 0.875rem;
122
+ height: 0.875rem;
123
+ }
124
+
125
+ .rs-checkbox--md .rs-checkbox__box {
126
+ width: 1rem;
127
+ height: 1rem;
128
+ }
129
+
130
+ .rs-checkbox--lg .rs-checkbox__box {
131
+ width: 1.125rem;
132
+ height: 1.125rem;
133
+ }
134
+
135
+ .rs-checkbox:hover:not(.rs-checkbox--disabled) .rs-checkbox__box {
136
+ border-color: var(--rs-primary);
137
+ }
138
+
139
+ .rs-checkbox--checked .rs-checkbox__box,
140
+ .rs-checkbox--indeterminate .rs-checkbox__box {
141
+ border-color: var(--rs-primary);
142
+ background: var(--rs-primary);
143
+ }
144
+
145
+ .rs-checkbox--checked .rs-checkbox__box::after {
146
+ content: '';
147
+ position: absolute;
148
+ top: 42%;
149
+ left: 50%;
150
+ width: 0.28rem;
151
+ height: 0.5rem;
152
+ border: solid var(--rs-primary-foreground, #fff);
153
+ border-width: 0 2px 2px 0;
154
+ transform: translate(-50%, -50%) rotate(45deg);
155
+ }
156
+
157
+ .rs-checkbox--ssm.rs-checkbox--checked .rs-checkbox__box::after {
158
+ width: 0.2rem;
159
+ height: 0.36rem;
160
+ }
161
+
162
+ .rs-checkbox--sm.rs-checkbox--checked .rs-checkbox__box::after {
163
+ width: 0.24rem;
164
+ height: 0.42rem;
165
+ }
166
+
167
+ .rs-checkbox--indeterminate .rs-checkbox__box::after {
168
+ content: '';
169
+ position: absolute;
170
+ top: 50%;
171
+ left: 50%;
172
+ width: 0.5rem;
173
+ height: 2px;
174
+ border: 0;
175
+ border-radius: 1px;
176
+ background: var(--rs-primary-foreground, #fff);
177
+ transform: translate(-50%, -50%);
178
+ }
179
+
180
+ .rs-checkbox__label {
181
+ font-size: var(--rs-font-size-sm);
182
+ line-height: var(--rs-line-height-normal);
183
+ }
184
+
185
+ .rs-checkbox--ssm .rs-checkbox__label,
186
+ .rs-checkbox--sm .rs-checkbox__label {
187
+ font-size: var(--rs-font-size-xs);
188
+ }
189
+ </style>
@@ -0,0 +1,278 @@
1
+ <script setup lang="ts">
2
+ import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue'
3
+ import { basicSetup } from 'codemirror'
4
+ import { EditorState } from '@codemirror/state'
5
+ import { EditorView } from '@codemirror/view'
6
+ import { oneDark } from '@codemirror/theme-one-dark'
7
+ import { useRsI18n } from '../composables/useRsI18n'
8
+ import { isCodeMirrorLightTheme, resolveCodeMirrorLanguage } from './code-mirror-lang'
9
+
10
+ const props = withDefaults(
11
+ defineProps<{
12
+ code: string
13
+ lang?: string
14
+ /** 覆盖内置"复制"文案 */
15
+ copyLabel?: string
16
+ /** 覆盖内置"已复制"文案 */
17
+ copiedLabel?: string
18
+ /** 提供时显示下载按钮,值为建议文件名 */
19
+ downloadFilename?: string
20
+ /** 覆盖内置"下载"文案 */
21
+ downloadLabel?: string
22
+ }>(),
23
+ { lang: 'text' },
24
+ )
25
+
26
+ const { t } = useRsI18n()
27
+
28
+ const editorEl = ref<HTMLElement | null>(null)
29
+ const view = shallowRef<EditorView | null>(null)
30
+ const isCopied = ref(false)
31
+ let copyTimer: ReturnType<typeof setTimeout> | null = null
32
+ let themeObserver: MutationObserver | null = null
33
+ /** 标记当前是否已卸载,防止异步初始化的竞态条件。 */
34
+ let unmounted = false
35
+
36
+ // ── 主题检测 ────────────────────────────────────────────────────────────────
37
+
38
+ function isLight(): boolean {
39
+ return isCodeMirrorLightTheme()
40
+ }
41
+
42
+ async function resolveLanguageExtension(lang: string) {
43
+ return resolveCodeMirrorLanguage(lang)
44
+ }
45
+
46
+ // ── 编辑器初始化 ─────────────────────────────────────────────────────────────
47
+
48
+ async function initEditor() {
49
+ if (!editorEl.value || unmounted) return
50
+ // 保留对当前容器的引用,以防 await 期间 editorEl 变化
51
+ const targetEl = editorEl.value
52
+
53
+ view.value?.destroy()
54
+ view.value = null
55
+
56
+ const langExts = await resolveLanguageExtension(props.lang ?? '')
57
+
58
+ // 异步返回后再次校验:组件可能已卸载或容器已换
59
+ if (unmounted || !targetEl.isConnected) return
60
+
61
+ const state = EditorState.create({
62
+ doc: props.code,
63
+ extensions: [
64
+ basicSetup,
65
+ EditorView.editable.of(false),
66
+ EditorView.lineWrapping,
67
+ ...langExts,
68
+ ...(isLight() ? [] : [oneDark]),
69
+ ],
70
+ })
71
+
72
+ view.value = new EditorView({ state, parent: targetEl })
73
+ }
74
+
75
+ onMounted(() => {
76
+ unmounted = false
77
+ void initEditor()
78
+
79
+ // 监听文档主题切换(data-rs-theme 属性变化),重新初始化以切换配色方案
80
+ themeObserver = new MutationObserver(() => {
81
+ void initEditor()
82
+ })
83
+ themeObserver.observe(document.documentElement, {
84
+ attributes: true,
85
+ attributeFilter: ['data-rs-theme'],
86
+ })
87
+ })
88
+
89
+ onUnmounted(() => {
90
+ unmounted = true
91
+ view.value?.destroy()
92
+ view.value = null
93
+ themeObserver?.disconnect()
94
+ themeObserver = null
95
+ if (copyTimer !== null) {
96
+ clearTimeout(copyTimer)
97
+ copyTimer = null
98
+ }
99
+ })
100
+
101
+ // lang 变化时重新初始化(切换语言高亮)
102
+ watch(() => props.lang, () => { void initEditor() })
103
+
104
+ // 代码内容更新时同步文档(不需要重建整个编辑器)
105
+ watch(
106
+ () => props.code,
107
+ (newCode) => {
108
+ if (!view.value) return
109
+ const current = view.value.state.doc.toString()
110
+ if (current === newCode) return
111
+ view.value.dispatch({
112
+ changes: { from: 0, to: view.value.state.doc.length, insert: newCode },
113
+ })
114
+ },
115
+ )
116
+
117
+ // ── 复制 ────────────────────────────────────────────────────────────────────
118
+
119
+ async function copy() {
120
+ try {
121
+ await navigator.clipboard.writeText(props.code)
122
+ isCopied.value = true
123
+ if (copyTimer !== null) clearTimeout(copyTimer)
124
+ copyTimer = setTimeout(() => {
125
+ isCopied.value = false
126
+ copyTimer = null
127
+ }, 1500)
128
+ } catch {
129
+ // clipboard 不可用时静默失败
130
+ }
131
+ }
132
+
133
+ function download() {
134
+ if (!props.downloadFilename) return
135
+ const name = props.downloadFilename.replace(/^.*[/\\]/, '') || 'download.html'
136
+ const lower = name.toLowerCase()
137
+ const mime = lower.endsWith('.html') || lower.endsWith('.htm')
138
+ ? 'text/html;charset=utf-8'
139
+ : 'text/plain;charset=utf-8'
140
+ const blob = new Blob([props.code], { type: mime })
141
+ const url = URL.createObjectURL(blob)
142
+ const anchor = document.createElement('a')
143
+ anchor.href = url
144
+ anchor.download = name
145
+ anchor.rel = 'noopener'
146
+ document.body.appendChild(anchor)
147
+ anchor.click()
148
+ anchor.remove()
149
+ URL.revokeObjectURL(url)
150
+ }
151
+ </script>
152
+
153
+ <template>
154
+ <figure class="rs-code-block">
155
+ <figcaption class="rs-code-block__bar">
156
+ <span class="rs-code-block__lang">{{ lang }}</span>
157
+ <div class="rs-code-block__actions">
158
+ <button
159
+ v-if="downloadFilename"
160
+ type="button"
161
+ class="rs-code-block__copy"
162
+ @click="download"
163
+ >
164
+ {{ downloadLabel ?? t('codeBlock.download') }}
165
+ </button>
166
+ <button type="button" class="rs-code-block__copy" @click="copy">
167
+ {{ isCopied ? (copiedLabel ?? t('codeBlock.copied')) : (copyLabel ?? t('codeBlock.copy')) }}
168
+ </button>
169
+ </div>
170
+ </figcaption>
171
+ <div ref="editorEl" class="rs-code-block__editor" />
172
+ </figure>
173
+ </template>
174
+
175
+ <style scoped>
176
+ .rs-code-block {
177
+ margin: 0;
178
+ border: 1px solid var(--rs-border-subtle);
179
+ border-radius: var(--rs-radius);
180
+ overflow: hidden;
181
+ background: var(--rs-surface-elevated);
182
+ font-size: var(--rs-font-size-sm);
183
+ }
184
+
185
+ .rs-code-block__bar {
186
+ display: flex;
187
+ align-items: center;
188
+ justify-content: space-between;
189
+ padding: var(--rs-space-xs) var(--rs-space-md);
190
+ border-bottom: 1px solid var(--rs-border-subtle);
191
+ background: color-mix(in srgb, var(--rs-surface-elevated) 80%, var(--rs-border) 20%);
192
+ }
193
+
194
+ .rs-code-block__lang {
195
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
196
+ font-size: var(--rs-font-size-xs);
197
+ color: var(--rs-muted);
198
+ text-transform: lowercase;
199
+ }
200
+
201
+ .rs-code-block__actions {
202
+ display: flex;
203
+ align-items: center;
204
+ gap: var(--rs-space-xs);
205
+ }
206
+
207
+ .rs-code-block__copy {
208
+ padding: 2px var(--rs-space-sm);
209
+ border: 1px solid var(--rs-border-subtle);
210
+ border-radius: var(--rs-radius-sm);
211
+ background: transparent;
212
+ color: var(--rs-muted);
213
+ font-size: var(--rs-font-size-xs);
214
+ cursor: pointer;
215
+ transition: color 120ms, border-color 120ms, background 120ms;
216
+ }
217
+
218
+ .rs-code-block__copy:hover {
219
+ color: var(--rs-text);
220
+ border-color: var(--rs-border);
221
+ background: var(--rs-item-hover);
222
+ }
223
+
224
+ /* CodeMirror 编辑器区域:覆盖默认样式以融入设计系统 */
225
+ .rs-code-block__editor :deep(.cm-editor) {
226
+ background: var(--rs-surface-elevated) !important;
227
+ /* 最大高度限制,超出时内部滚动 */
228
+ max-height: 32rem;
229
+ overflow-y: auto;
230
+ }
231
+
232
+ /* 浅色模式:代码区用深色文字 */
233
+ .rs-code-block__editor :deep(.cm-content) {
234
+ color: var(--rs-foreground);
235
+ }
236
+
237
+ /* 行号区域背景与主体一致 */
238
+ .rs-code-block__editor :deep(.cm-gutters) {
239
+ background: color-mix(in srgb, var(--rs-surface-elevated) 85%, var(--rs-border) 15%) !important;
240
+ border-right-color: var(--rs-border-subtle) !important;
241
+ color: var(--rs-muted);
242
+ }
243
+
244
+ /* 激活行高亮跟随设计系统 */
245
+ .rs-code-block__editor :deep(.cm-activeLine) {
246
+ background: color-mix(in srgb, var(--rs-primary) 4%, transparent) !important;
247
+ }
248
+
249
+ .rs-code-block__editor :deep(.cm-activeLineGutter) {
250
+ background: color-mix(in srgb, var(--rs-primary) 6%, transparent) !important;
251
+ }
252
+
253
+ .rs-code-block__editor :deep(.cm-scroller) {
254
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
255
+ font-size: var(--rs-font-size-sm);
256
+ line-height: 1.6;
257
+ }
258
+
259
+ /* 只读时隐藏光标,保持纯展示外观 */
260
+ .rs-code-block__editor :deep(.cm-cursor) {
261
+ display: none;
262
+ }
263
+
264
+ /* 去掉 CodeMirror 默认焦点描边,统一为 macOS 风格聚焦环 */
265
+ .rs-code-block__editor :deep(.cm-editor.cm-focused) {
266
+ outline: none;
267
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
268
+ }
269
+
270
+ /* 选中区跟随设计系统主色 */
271
+ .rs-code-block__editor :deep(.cm-selectionBackground) {
272
+ background: color-mix(in srgb, var(--rs-primary) 22%, transparent) !important;
273
+ }
274
+
275
+ .rs-code-block__editor :deep(.cm-focused .cm-selectionBackground) {
276
+ background: color-mix(in srgb, var(--rs-primary) 28%, transparent) !important;
277
+ }
278
+ </style>