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,107 @@
1
+ /** 选项卡项定义 */
2
+ export interface RsTabItem {
3
+ value: string
4
+ label: string
5
+ icon?: string
6
+ disabled?: boolean
7
+ badge?: string | number
8
+ /** 是否可关闭;未设置时继承 RsTabs 的 closable */
9
+ closable?: boolean
10
+ /** 是否可重命名;未设置时继承 RsTabs 的 renamable */
11
+ renamable?: boolean
12
+ }
13
+
14
+ /** 选项卡尺寸 */
15
+ export type RsTabsSize = 'sm' | 'md'
16
+
17
+ /** line 下划线 · segmented 胶囊 · card 卡片(Ant editable-card) */
18
+ export type RsTabsVariant = 'line' | 'segmented' | 'card'
19
+
20
+ /** 标签过多时的处理方式 */
21
+ export type RsTabsOverflow = 'scroll' | 'dropdown'
22
+
23
+ /** 判断单项是否展示关闭按钮 */
24
+ export function isTabClosable(item: RsTabItem, globalClosable = false): boolean {
25
+ if (item.closable !== undefined) return item.closable
26
+ return globalClosable
27
+ }
28
+
29
+ /** 判断单项是否可双击重命名 */
30
+ export function isTabRenamable(item: RsTabItem, globalRenamable = false): boolean {
31
+ if (item.renamable !== undefined) return item.renamable
32
+ return globalRenamable
33
+ }
34
+
35
+ /** 关闭当前激活项时,解析应切换到的下一项(优先右侧,否则左侧) */
36
+ export function getNextTabAfterClose(
37
+ items: RsTabItem[],
38
+ closedValue: string,
39
+ activeValue: string,
40
+ ): string | undefined {
41
+ if (activeValue !== closedValue) return activeValue
42
+ const index = items.findIndex((item) => item.value === closedValue)
43
+ if (index < 0) return undefined
44
+ const remaining = items.filter((item) => item.value !== closedValue)
45
+ if (!remaining.length) return undefined
46
+ return remaining[index]?.value ?? remaining[index - 1]?.value ?? remaining[0]?.value
47
+ }
48
+
49
+ /** 将 dragValue 对应的项移动到 dropValue 之前 */
50
+ export function reorderTabItems(
51
+ items: RsTabItem[],
52
+ dragValue: string,
53
+ dropValue: string,
54
+ ): RsTabItem[] {
55
+ const from = items.findIndex((item) => item.value === dragValue)
56
+ const to = items.findIndex((item) => item.value === dropValue)
57
+ if (from < 0 || to < 0 || from === to) return items
58
+ const next = [...items]
59
+ const [moved] = next.splice(from, 1)
60
+ if (!moved) return items
61
+ next.splice(to, 0, moved)
62
+ return next
63
+ }
64
+
65
+ export interface ResolveVisibleTabOptions {
66
+ /** dropdown 模式下为 false:激活项仅在「更多」展示,不占标签栏 */
67
+ keepActiveVisible?: boolean
68
+ }
69
+
70
+ /**
71
+ * 根据各标签宽度与可用宽度,计算应展示的标签 value 集合。
72
+ */
73
+ export function resolveVisibleTabValues(
74
+ items: RsTabItem[],
75
+ widths: Map<string, number>,
76
+ availableWidth: number,
77
+ activeValue: string,
78
+ reservedWidth = 0,
79
+ options: ResolveVisibleTabOptions = {},
80
+ ): Set<string> {
81
+ const keepActiveVisible = options.keepActiveVisible ?? true
82
+
83
+ if (availableWidth <= 0 || !items.length) {
84
+ return new Set(items.map((item) => item.value))
85
+ }
86
+
87
+ const budget = Math.max(0, availableWidth - reservedWidth)
88
+ const visible: string[] = []
89
+ let used = 0
90
+
91
+ for (const item of items) {
92
+ const width = widths.get(item.value) ?? 0
93
+ if (used + width > budget) break
94
+ visible.push(item.value)
95
+ used += width
96
+ }
97
+
98
+ if (keepActiveVisible && !visible.includes(activeValue)) {
99
+ const activeIndex = items.findIndex((item) => item.value === activeValue)
100
+ if (activeIndex >= 0) {
101
+ if (visible.length > 1) visible.pop()
102
+ if (!visible.includes(activeValue)) visible.push(activeValue)
103
+ }
104
+ }
105
+
106
+ return new Set(visible)
107
+ }
@@ -0,0 +1,301 @@
1
+ import type { ITheme, Terminal } from 'xterm'
2
+ import { readDocumentTheme } from './code-editor-utils'
3
+
4
+ export type RsTerminalThemeMode = 'auto' | 'light' | 'dark'
5
+ export type RsResolvedTerminalTheme = 'light' | 'dark'
6
+
7
+ export type RsTerminalAction = 'copy' | 'paste' | 'selectAll' | 'clear' | 'askAi'
8
+
9
+ const TERMINAL_CSS_KEYS: Array<[keyof ITheme, string]> = [
10
+ ['background', '--rs-terminal-bg'],
11
+ ['foreground', '--rs-terminal-fg'],
12
+ ['cursor', '--rs-terminal-cursor'],
13
+ ['cursorAccent', '--rs-terminal-cursor-accent'],
14
+ ['selectionBackground', '--rs-terminal-selection-bg'],
15
+ ['selectionForeground', '--rs-terminal-selection-fg'],
16
+ ['black', '--rs-terminal-ansi-black'],
17
+ ['red', '--rs-terminal-ansi-red'],
18
+ ['green', '--rs-terminal-ansi-green'],
19
+ ['yellow', '--rs-terminal-ansi-yellow'],
20
+ ['blue', '--rs-terminal-ansi-blue'],
21
+ ['magenta', '--rs-terminal-ansi-magenta'],
22
+ ['cyan', '--rs-terminal-ansi-cyan'],
23
+ ['white', '--rs-terminal-ansi-white'],
24
+ ['brightBlack', '--rs-terminal-ansi-bright-black'],
25
+ ['brightRed', '--rs-terminal-ansi-bright-red'],
26
+ ['brightGreen', '--rs-terminal-ansi-bright-green'],
27
+ ['brightYellow', '--rs-terminal-ansi-bright-yellow'],
28
+ ['brightBlue', '--rs-terminal-ansi-bright-blue'],
29
+ ['brightMagenta', '--rs-terminal-ansi-bright-magenta'],
30
+ ['brightCyan', '--rs-terminal-ansi-bright-cyan'],
31
+ ['brightWhite', '--rs-terminal-ansi-bright-white'],
32
+ ]
33
+
34
+ const themeProbeCache = new Map<RsResolvedTerminalTheme, HTMLElement>()
35
+
36
+ const TERMINAL_ROW_STRIPE_VAR = '--rs-terminal-row-stripe'
37
+
38
+ function readCssVar(name: string, element: HTMLElement): string {
39
+ return getComputedStyle(element).getPropertyValue(name).trim()
40
+ }
41
+
42
+ function channelToHex(raw: string): string | null {
43
+ const value = raw.endsWith('%')
44
+ ? Math.round((Number(raw.slice(0, -1)) / 100) * 255)
45
+ : Math.round(Number(raw))
46
+ if (!Number.isFinite(value)) {
47
+ return null
48
+ }
49
+ return Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0')
50
+ }
51
+
52
+ function parseRgbChannels(value: string): [string, string, string] | null {
53
+ const lower = value.toLowerCase()
54
+ if (!lower.startsWith('rgb')) {
55
+ return null
56
+ }
57
+ const open = value.indexOf('(')
58
+ const close = value.indexOf(')', open + 1)
59
+ if (open === -1 || close === -1) {
60
+ return null
61
+ }
62
+ const parts = value
63
+ .slice(open + 1, close)
64
+ .replaceAll('/', ' ')
65
+ .replaceAll(',', ' ')
66
+ .trim()
67
+ .split(/\s+/)
68
+ .filter(Boolean)
69
+ if (parts.length < 3) {
70
+ return null
71
+ }
72
+ const r = channelToHex(parts[0])
73
+ const g = channelToHex(parts[1])
74
+ const b = channelToHex(parts[2])
75
+ if (!r || !g || !b) {
76
+ return null
77
+ }
78
+ return [r, g, b]
79
+ }
80
+
81
+ /** 将 CSS 颜色规范为 #RRGGBB(支持 hex 与 computed rgb) */
82
+ export function normalizeTerminalHexColor(value: string): string | null {
83
+ const trimmed = value.trim()
84
+ if (!trimmed) {
85
+ return null
86
+ }
87
+ if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) {
88
+ return trimmed.toLowerCase()
89
+ }
90
+ if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
91
+ const [, r, g, b] = trimmed
92
+ return `#${r}${r}${g}${g}${b}${b}`.toLowerCase()
93
+ }
94
+ const rgb = parseRgbChannels(trimmed)
95
+ if (!rgb) {
96
+ return null
97
+ }
98
+ return `#${rgb[0]}${rgb[1]}${rgb[2]}`
99
+ }
100
+
101
+ /** 将 lab()/color() 等现代 CSS 色值转为 xterm 可解析的 #RRGGBB */
102
+ export function resolveCssColorForXterm(value: string): string | null {
103
+ const direct = normalizeTerminalHexColor(value)
104
+ if (direct) {
105
+ return direct
106
+ }
107
+ if (typeof document === 'undefined') {
108
+ return null
109
+ }
110
+ const canvas = document.createElement('canvas')
111
+ canvas.width = 1
112
+ canvas.height = 1
113
+ const ctx = canvas.getContext('2d', { willReadFrequently: true })
114
+ if (!ctx) {
115
+ return null
116
+ }
117
+ try {
118
+ ctx.fillStyle = '#000000'
119
+ ctx.fillStyle = value
120
+ if (typeof ctx.fillStyle !== 'string') {
121
+ return null
122
+ }
123
+ ctx.fillRect(0, 0, 1, 1)
124
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data
125
+ if (a === 0) {
126
+ return null
127
+ }
128
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`
129
+ } catch {
130
+ return null
131
+ }
132
+ }
133
+
134
+ const BACKGROUND_THEME_KEYS = new Set<keyof ITheme>(['background', 'selectionBackground'])
135
+
136
+ function readResolvedCssColor(cssVar: string, source: HTMLElement, asBackground: boolean): string {
137
+ const probe = document.createElement('span')
138
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none'
139
+ if (asBackground) {
140
+ probe.style.backgroundColor = `var(${cssVar})`
141
+ } else {
142
+ probe.style.color = `var(${cssVar})`
143
+ }
144
+ source.appendChild(probe)
145
+ const computed = getComputedStyle(probe)
146
+ const raw = asBackground ? computed.backgroundColor : computed.color
147
+ probe.remove()
148
+ return raw
149
+ }
150
+
151
+ function themeProbeElement(mode: RsResolvedTerminalTheme): HTMLElement {
152
+ const cached = themeProbeCache.get(mode)
153
+ if (cached) {
154
+ return cached
155
+ }
156
+ const probe = document.createElement('div')
157
+ probe.dataset.rsTheme = mode
158
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none'
159
+ document.body.appendChild(probe)
160
+ themeProbeCache.set(mode, probe)
161
+ return probe
162
+ }
163
+
164
+ function themeSourceElement(mode: RsResolvedTerminalTheme): HTMLElement {
165
+ if (document.documentElement.dataset.rsTheme === mode) {
166
+ return document.documentElement
167
+ }
168
+ return themeProbeElement(mode)
169
+ }
170
+
171
+ /** 从全局 `--rs-terminal-*` token 构建 xterm 调色板(须已加载 styles.css) */
172
+ export function readTerminalThemeFromCss(mode: RsResolvedTerminalTheme): ITheme {
173
+ if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') {
174
+ return {}
175
+ }
176
+ const source = themeSourceElement(mode)
177
+ const theme: ITheme = {}
178
+ for (const [key, cssVar] of TERMINAL_CSS_KEYS) {
179
+ const raw = readResolvedCssColor(cssVar, source, BACKGROUND_THEME_KEYS.has(key))
180
+ const value = resolveCssColorForXterm(raw)
181
+ if (value) {
182
+ ;(theme as Record<string, string>)[key] = value
183
+ }
184
+ }
185
+ return theme
186
+ }
187
+
188
+ export function resolveTerminalTheme(mode: RsTerminalThemeMode = 'auto'): RsResolvedTerminalTheme {
189
+ return mode === 'auto' ? readDocumentTheme() : mode
190
+ }
191
+
192
+ export function getTerminalThemePalette(mode: RsResolvedTerminalTheme): ITheme {
193
+ return readTerminalThemeFromCss(mode)
194
+ }
195
+
196
+ export function mergeTerminalTheme(
197
+ mode: RsResolvedTerminalTheme,
198
+ overrides?: Partial<ITheme>,
199
+ ): ITheme {
200
+ return {
201
+ ...readTerminalThemeFromCss(mode),
202
+ ...overrides,
203
+ }
204
+ }
205
+
206
+ /** 读取斑马纹行底色(`--rs-terminal-row-stripe`,解析后供 xterm decoration 使用) */
207
+ export function readTerminalRowStripeFromCss(mode: RsResolvedTerminalTheme): string {
208
+ if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') {
209
+ return ''
210
+ }
211
+ return readCssVar(TERMINAL_ROW_STRIPE_VAR, themeSourceElement(mode))
212
+ }
213
+
214
+ /** PTY 输出是否含终端控制序列(ESC / CSI),此类数据不应批量延迟。 */
215
+ export function containsEscapeSequence(data: string): boolean {
216
+ return data.includes('\x1b') || data.includes('\x9b')
217
+ }
218
+
219
+ const TUI_REFRESH_PATTERN =
220
+ /\x1b\[[0-9;]*[Hf]|\x1b\[2J|\x1b\[\?1049[hl]|\x1b\[\?47[hl]|\x1b\[1049[hl]/
221
+
222
+ /** 检测 ncurses/vim/top 等全屏刷新常用的光标定位与清屏序列 */
223
+ export function containsTuiRefreshSequence(data: string): boolean {
224
+ return TUI_REFRESH_PATTERN.test(data)
225
+ }
226
+
227
+ const FULL_SCREEN_CLEAR_PATTERN = /\x1b\[[0-9;]*2J|\x1b\[[0-9;]*3J/
228
+ const CURSOR_HOME_TOP_PATTERN = /^\x1b\[H|^\x1b\[1;1H|^\x1b\[1H/
229
+
230
+ /** 全屏清屏(top/vim 新一轮绘制常见) */
231
+ export function containsFullScreenClear(data: string): boolean {
232
+ return FULL_SCREEN_CLEAR_PATTERN.test(data)
233
+ }
234
+
235
+ /** 数据块以光标归位首行开头(top 每轮刷新常见) */
236
+ export function startsWithCursorHome(data: string): boolean {
237
+ return CURSOR_HOME_TOP_PATTERN.test(data.trimStart())
238
+ }
239
+
240
+ /**
241
+ * 在本地解析层重置滚动边距等模式(不发给 PTY)。
242
+ * 仅在全屏清屏等明确「新一轮绘制」时调用,避免打断进行中的 TUI 光标位置。
243
+ */
244
+ export function resetLocalTerminalModes(terminal: Terminal): void {
245
+ terminal.write('\x1b[r')
246
+ }
247
+
248
+ export function snapTerminalViewportToBottom(terminal: Terminal): void {
249
+ terminal.scrollToBottom()
250
+ }
251
+
252
+ /** 处理 PTY 写入前的视口/模式准备(不移动写入光标,避免输入错位) */
253
+ export function prepareTerminalForPtyWrite(terminal: Terminal, data: string): void {
254
+ if (containsFullScreenClear(data)) {
255
+ resetLocalTerminalModes(terminal)
256
+ return
257
+ }
258
+ const buffer = terminal.buffer.active
259
+ if (buffer.viewportY < buffer.baseY) {
260
+ snapTerminalViewportToBottom(terminal)
261
+ }
262
+ }
263
+
264
+ export function isMacPlatform(): boolean {
265
+ if (typeof navigator === 'undefined') {
266
+ return false
267
+ }
268
+ return /Mac|iPhone|iPad|iPod/.test(navigator.platform)
269
+ }
270
+
271
+ /** 菜单/文档展示的修饰键标签 */
272
+ export function terminalShortcutLabel(key: string): string {
273
+ return isMacPlatform() ? `⌘${key}` : `Ctrl+${key}`
274
+ }
275
+
276
+ /** ANSI 16 色演示文本 */
277
+ export function buildAnsiColorDemo(): string {
278
+ const names = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
279
+ const rows = [0, 1].map((row) => {
280
+ const base = row === 0 ? 30 : 90
281
+ let line = ''
282
+ for (let i = 0; i < 8; i += 1) {
283
+ line += `\x1b[${base + i}m ${names[i].padEnd(8)} \x1b[0m`
284
+ }
285
+ return line
286
+ })
287
+ let gradient = ''
288
+ for (let i = 16; i < 52; i += 1) {
289
+ gradient += `\x1b[48;5;${i}m \x1b[0m`
290
+ }
291
+ const lines = [
292
+ '\x1b[1mANSI 16 colors\x1b[0m',
293
+ ...rows,
294
+ '',
295
+ '\x1b[1mStyles\x1b[0m \x1b[1mbold\x1b[0m \x1b[3mitalic\x1b[0m \x1b[4munderline\x1b[0m \x1b[9mstrikethrough\x1b[0m',
296
+ '',
297
+ '\x1b[38;5;196m256-color\x1b[0m sample:',
298
+ gradient,
299
+ ]
300
+ return `${lines.join('\r\n')}\r\n`
301
+ }
@@ -0,0 +1,49 @@
1
+ import type { Terminal } from 'xterm'
2
+
3
+ export type RsTerminalWheelScrollModifier = 'none' | 'shift'
4
+
5
+ /** 视口是否已滚入 scrollback(非底部跟随) */
6
+ export function isTerminalViewportScrolledUp(terminal: Terminal): boolean {
7
+ const buffer = terminal.buffer.active
8
+ return buffer.viewportY < buffer.baseY
9
+ }
10
+
11
+ /**
12
+ * 滚轮策略:默认滚轮不翻 scrollback,转为方向键发给 PTY(避免 top/vim 类全屏 TUI 错位);
13
+ * 按住 Shift 时滚轮才滚动历史。
14
+ */
15
+ export function attachWheelScrollGuard(
16
+ hostEl: HTMLElement,
17
+ options: {
18
+ modifier: () => RsTerminalWheelScrollModifier
19
+ inputEnabled: () => boolean
20
+ onArrowKeys: (data: string) => void
21
+ },
22
+ ): () => void {
23
+ const viewport = hostEl.querySelector<HTMLElement>('.xterm-viewport')
24
+ if (!viewport) {
25
+ return () => undefined
26
+ }
27
+
28
+ const handler = (ev: WheelEvent): void => {
29
+ if (options.modifier() === 'none') {
30
+ return
31
+ }
32
+ if (ev.shiftKey) {
33
+ return
34
+ }
35
+ ev.preventDefault()
36
+ ev.stopImmediatePropagation()
37
+ if (!options.inputEnabled() || ev.deltaY === 0) {
38
+ return
39
+ }
40
+ const steps = Math.min(5, Math.max(1, Math.round(Math.abs(ev.deltaY) / 40)))
41
+ const seq = ev.deltaY > 0 ? '\x1b[B' : '\x1b[A'
42
+ options.onArrowKeys(seq.repeat(steps))
43
+ }
44
+
45
+ viewport.addEventListener('wheel', handler, { capture: true, passive: false })
46
+ return () => {
47
+ viewport.removeEventListener('wheel', handler, { capture: true })
48
+ }
49
+ }
@@ -0,0 +1,167 @@
1
+ export interface RsParsedTime {
2
+ hour: number
3
+ minute: number
4
+ second: number
5
+ }
6
+
7
+ export interface RsTimeRangeValue {
8
+ start?: string
9
+ end?: string
10
+ }
11
+
12
+ export const EMPTY_TIME_RANGE: RsTimeRangeValue = {}
13
+
14
+ const TIME_PATTERN = /^([01]\d|2[0-3]):([0-5]\d)$/
15
+ const TIME_WITH_SECONDS_PATTERN = /^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/
16
+
17
+ export function scheduleAfterPaint(callback: () => void): () => void {
18
+ let cancelled = false
19
+ requestAnimationFrame(() => {
20
+ requestAnimationFrame(() => {
21
+ if (!cancelled) callback()
22
+ })
23
+ })
24
+ return () => {
25
+ cancelled = true
26
+ }
27
+ }
28
+
29
+ export function rangeInclusive(max: number): number[] {
30
+ return Array.from({ length: max + 1 }, (_, index) => index)
31
+ }
32
+
33
+ export function parseTimeValue(value?: string, withSeconds = false): RsParsedTime | null {
34
+ if (!value?.trim()) return null
35
+ const trimmed = value.trim()
36
+ const pattern = withSeconds ? TIME_WITH_SECONDS_PATTERN : TIME_PATTERN
37
+ const match = pattern.exec(trimmed)
38
+ if (!match) return null
39
+ return {
40
+ hour: Number.parseInt(match[1], 10),
41
+ minute: Number.parseInt(match[2], 10),
42
+ second: withSeconds ? Number.parseInt(match[3], 10) : 0,
43
+ }
44
+ }
45
+
46
+ export function formatTimeParts(hour: number, minute: number, second?: number): string {
47
+ const h = String(hour).padStart(2, '0')
48
+ const m = String(minute).padStart(2, '0')
49
+ if (second !== undefined) return `${h}:${m}:${String(second).padStart(2, '0')}`
50
+ return `${h}:${m}`
51
+ }
52
+
53
+ export function formatTimeFromParts(
54
+ hour: number,
55
+ minute: number,
56
+ second: number,
57
+ withSeconds = false,
58
+ ): string {
59
+ return formatTimeParts(hour, minute, withSeconds ? second : undefined)
60
+ }
61
+
62
+ export function getCurrentTime(): RsParsedTime {
63
+ const now = new Date()
64
+ return {
65
+ hour: now.getHours(),
66
+ minute: now.getMinutes(),
67
+ second: now.getSeconds(),
68
+ }
69
+ }
70
+
71
+ export function formatTimeValue(value?: string): string {
72
+ return value ?? ''
73
+ }
74
+
75
+ export function timeToSeconds(hour: number, minute: number, second = 0): number {
76
+ return hour * 3600 + minute * 60 + second
77
+ }
78
+
79
+ export function parseTimeToSeconds(value: string, withSeconds = false): number | null {
80
+ const parsed = parseTimeValue(value, withSeconds)
81
+ if (!parsed) return null
82
+ return timeToSeconds(parsed.hour, parsed.minute, parsed.second)
83
+ }
84
+
85
+ export function isTimeWithinBounds(
86
+ hour: number,
87
+ minute: number,
88
+ second: number,
89
+ options: { minTime?: string; maxTime?: string; withSeconds?: boolean } = {},
90
+ ): boolean {
91
+ const { minTime, maxTime, withSeconds = false } = options
92
+ const candidate = timeToSeconds(hour, minute, second)
93
+
94
+ if (minTime) {
95
+ const minSeconds = parseTimeToSeconds(minTime, withSeconds)
96
+ if (minSeconds !== null && candidate < minSeconds) return false
97
+ }
98
+
99
+ if (maxTime) {
100
+ const maxSeconds = parseTimeToSeconds(maxTime, withSeconds)
101
+ if (maxSeconds !== null && candidate > maxSeconds) return false
102
+ }
103
+
104
+ return true
105
+ }
106
+
107
+ export function isTimeRangeEmpty(value: RsTimeRangeValue): boolean {
108
+ return !value.start?.trim() && !value.end?.trim()
109
+ }
110
+
111
+ export function isTimeRangeOrdered(
112
+ range: RsTimeRangeValue,
113
+ withSeconds = false,
114
+ ): boolean {
115
+ if (!range.start || !range.end) return true
116
+ return isTimeRangeOrderedValues(range.start, range.end, withSeconds)
117
+ }
118
+
119
+ export function isTimeRangeOrderedValues(
120
+ start: string,
121
+ end: string,
122
+ withSeconds = false,
123
+ ): boolean {
124
+ const startSeconds = parseTimeToSeconds(start, withSeconds)
125
+ const endSeconds = parseTimeToSeconds(end, withSeconds)
126
+ if (startSeconds === null || endSeconds === null) return true
127
+ return startSeconds <= endSeconds
128
+ }
129
+
130
+ export function pickEarlierTime(
131
+ a?: string,
132
+ b?: string,
133
+ withSeconds = false,
134
+ ): string | undefined {
135
+ if (!a) return b
136
+ if (!b) return a
137
+ const ta = parseTimeToSeconds(a, withSeconds)
138
+ const tb = parseTimeToSeconds(b, withSeconds)
139
+ if (ta === null) return b
140
+ if (tb === null) return a
141
+ return ta <= tb ? a : b
142
+ }
143
+
144
+ export function pickLaterTime(
145
+ a?: string,
146
+ b?: string,
147
+ withSeconds = false,
148
+ ): string | undefined {
149
+ if (!a) return b
150
+ if (!b) return a
151
+ const ta = parseTimeToSeconds(a, withSeconds)
152
+ const tb = parseTimeToSeconds(b, withSeconds)
153
+ if (ta === null) return b
154
+ if (tb === null) return a
155
+ return ta >= tb ? a : b
156
+ }
157
+
158
+ export function formatTimeRangeDisplay(
159
+ range: RsTimeRangeValue,
160
+ options?: { separator?: string },
161
+ ): string {
162
+ const separator = options?.separator ?? ' ~ '
163
+ const start = range.start ? formatTimeValue(range.start) : ''
164
+ const end = range.end ? formatTimeValue(range.end) : ''
165
+ if (start && end) return `${start}${separator}${end}`
166
+ return start || end
167
+ }
@@ -0,0 +1,23 @@
1
+ import type { RsTimeRangeValue } from './time-picker-utils'
2
+ import { isTimeRangeOrdered } from './time-picker-utils'
3
+
4
+ export interface RsTimeRangeValidationRules {
5
+ required?: boolean
6
+ ordered?: boolean
7
+ }
8
+
9
+ export type RsTimeRangeValidationError = 'required' | 'order'
10
+
11
+ export interface RsTimeRangeValidationResult {
12
+ valid: boolean
13
+ error?: RsTimeRangeValidationError
14
+ }
15
+
16
+ export function validateTimeRangeValue(
17
+ value: RsTimeRangeValue,
18
+ rules: RsTimeRangeValidationRules = {},
19
+ ): RsTimeRangeValidationResult {
20
+ if (rules.required && (!value.start || !value.end)) return { valid: false, error: 'required' }
21
+ if (rules.ordered !== false && !isTimeRangeOrdered(value)) return { valid: false, error: 'order' }
22
+ return { valid: true }
23
+ }
@@ -0,0 +1,20 @@
1
+ export interface RsTimeValidationRules {
2
+ required?: boolean
3
+ min?: string
4
+ max?: string
5
+ }
6
+
7
+ export type RsTimeValidationError = 'required' | 'min' | 'max' | 'format'
8
+
9
+ export interface RsTimeValidationResult {
10
+ valid: boolean
11
+ error?: RsTimeValidationError
12
+ }
13
+
14
+ export function validateTimeValue(value: string, rules: RsTimeValidationRules = {}): RsTimeValidationResult {
15
+ if (!value) return rules.required ? { valid: false, error: 'required' } : { valid: true }
16
+ if (!/^\d{2}:\d{2}(?::\d{2})?$/.test(value)) return { valid: false, error: 'format' }
17
+ if (rules.min && value < rules.min) return { valid: false, error: 'min' }
18
+ if (rules.max && value > rules.max) return { valid: false, error: 'max' }
19
+ return { valid: true }
20
+ }