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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +202 -0
- package/NOTICE +25 -0
- package/README.en.md +99 -0
- package/README.md +161 -0
- package/package.json +106 -0
- package/src/__tests__/RsBadge.spec.ts +40 -0
- package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
- package/src/__tests__/RsButton.spec.ts +108 -0
- package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
- package/src/__tests__/RsCard.spec.ts +97 -0
- package/src/__tests__/RsCheckbox.spec.ts +36 -0
- package/src/__tests__/RsCodeEditor.spec.ts +111 -0
- package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
- package/src/__tests__/RsContainer.spec.ts +89 -0
- package/src/__tests__/RsContextMenu.spec.ts +118 -0
- package/src/__tests__/RsDatePicker.spec.ts +88 -0
- package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
- package/src/__tests__/RsDialog.spec.ts +404 -0
- package/src/__tests__/RsDrawer.spec.ts +145 -0
- package/src/__tests__/RsDropdown.spec.ts +135 -0
- package/src/__tests__/RsEmpty.spec.ts +99 -0
- package/src/__tests__/RsForm.spec.ts +173 -0
- package/src/__tests__/RsIcon.spec.ts +244 -0
- package/src/__tests__/RsInput.spec.ts +269 -0
- package/src/__tests__/RsInputNumber.spec.ts +118 -0
- package/src/__tests__/RsLabel.spec.ts +106 -0
- package/src/__tests__/RsLink.spec.ts +49 -0
- package/src/__tests__/RsLoading.spec.ts +84 -0
- package/src/__tests__/RsMarkdown.spec.ts +143 -0
- package/src/__tests__/RsMenu.spec.ts +119 -0
- package/src/__tests__/RsNewComponents.spec.ts +17 -0
- package/src/__tests__/RsPagination.spec.ts +158 -0
- package/src/__tests__/RsPopover.spec.ts +126 -0
- package/src/__tests__/RsScrollbar.spec.ts +53 -0
- package/src/__tests__/RsSelect.spec.ts +291 -0
- package/src/__tests__/RsSidebar.spec.ts +119 -0
- package/src/__tests__/RsSplitPane.spec.ts +177 -0
- package/src/__tests__/RsStatCard.spec.ts +79 -0
- package/src/__tests__/RsSteps.spec.ts +125 -0
- package/src/__tests__/RsTable.perf.spec.ts +103 -0
- package/src/__tests__/RsTable.spec.ts +1135 -0
- package/src/__tests__/RsTabs.spec.ts +328 -0
- package/src/__tests__/RsTerminal.spec.ts +218 -0
- package/src/__tests__/RsTimePicker.spec.ts +77 -0
- package/src/__tests__/RsToaster.spec.ts +95 -0
- package/src/__tests__/RsToolbar.spec.ts +75 -0
- package/src/__tests__/RsTooltip.spec.ts +111 -0
- package/src/__tests__/RsTree.spec.ts +572 -0
- package/src/__tests__/RsUpload.spec.ts +160 -0
- package/src/__tests__/RsVirtualList.spec.ts +77 -0
- package/src/__tests__/date-picker-utils.spec.ts +70 -0
- package/src/__tests__/dialog-viewport.spec.ts +38 -0
- package/src/__tests__/iso-local-datetime.spec.ts +58 -0
- package/src/__tests__/rs-clipboard.spec.ts +142 -0
- package/src/__tests__/setup.ts +12 -0
- package/src/__tests__/split-pane-utils.spec.ts +136 -0
- package/src/__tests__/table-drag.spec.ts +35 -0
- package/src/__tests__/table-edit-utils.spec.ts +194 -0
- package/src/__tests__/table-utils.spec.ts +286 -0
- package/src/__tests__/theme.spec.ts +22 -0
- package/src/__tests__/tree-utils.spec.ts +234 -0
- package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
- package/src/components/RsAvatar.vue +115 -0
- package/src/components/RsBadge.vue +59 -0
- package/src/components/RsBreadcrumb.vue +65 -0
- package/src/components/RsBreadcrumbItems.vue +36 -0
- package/src/components/RsButton.vue +393 -0
- package/src/components/RsCalendarGrid.vue +284 -0
- package/src/components/RsCard.vue +237 -0
- package/src/components/RsCheckbox.vue +189 -0
- package/src/components/RsCodeBlock.vue +278 -0
- package/src/components/RsCodeEditor.vue +597 -0
- package/src/components/RsConfigProvider.vue +92 -0
- package/src/components/RsConfirmDialog.vue +178 -0
- package/src/components/RsContainer.vue +235 -0
- package/src/components/RsContextMenu.vue +238 -0
- package/src/components/RsContextMenuItems.vue +77 -0
- package/src/components/RsDatePicker.vue +756 -0
- package/src/components/RsDateTimePicker.vue +42 -0
- package/src/components/RsDialog.vue +486 -0
- package/src/components/RsDrawer.vue +175 -0
- package/src/components/RsDropdown.vue +252 -0
- package/src/components/RsDropdownItems.vue +90 -0
- package/src/components/RsEmpty.vue +107 -0
- package/src/components/RsForm.vue +131 -0
- package/src/components/RsIcon.vue +104 -0
- package/src/components/RsInput.vue +1101 -0
- package/src/components/RsInputNumber.vue +532 -0
- package/src/components/RsLabel.vue +54 -0
- package/src/components/RsLink.vue +106 -0
- package/src/components/RsLoading.vue +171 -0
- package/src/components/RsMarkdown.vue +329 -0
- package/src/components/RsMenu.vue +256 -0
- package/src/components/RsMenuItems.vue +125 -0
- package/src/components/RsMonacoEditor.vue +541 -0
- package/src/components/RsPagination.vue +184 -0
- package/src/components/RsPopover.vue +77 -0
- package/src/components/RsProseEditor.vue +160 -0
- package/src/components/RsScrollbar.vue +171 -0
- package/src/components/RsSelect.vue +1045 -0
- package/src/components/RsSidebar.vue +121 -0
- package/src/components/RsSidebarGroup.vue +32 -0
- package/src/components/RsSidebarItem.vue +72 -0
- package/src/components/RsSplitPane.vue +619 -0
- package/src/components/RsStatCard.vue +85 -0
- package/src/components/RsSteps.vue +138 -0
- package/src/components/RsTable.vue +2664 -0
- package/src/components/RsTableColgroup.vue +17 -0
- package/src/components/RsTableHeaderRow.vue +30 -0
- package/src/components/RsTabs.vue +951 -0
- package/src/components/RsTerminal.vue +697 -0
- package/src/components/RsTimePicker.vue +554 -0
- package/src/components/RsTimePickerColumns.vue +226 -0
- package/src/components/RsToaster.vue +396 -0
- package/src/components/RsToolbar.vue +135 -0
- package/src/components/RsTooltip.vue +142 -0
- package/src/components/RsTooltipProvider.vue +20 -0
- package/src/components/RsTree.vue +1248 -0
- package/src/components/RsUpload.vue +279 -0
- package/src/components/RsVirtualList.vue +217 -0
- package/src/components/avatar-utils.ts +19 -0
- package/src/components/breadcrumb-utils.ts +19 -0
- package/src/components/class-utils.ts +29 -0
- package/src/components/code-editor-utils.ts +106 -0
- package/src/components/code-mirror-completion.ts +40 -0
- package/src/components/code-mirror-diagnostics.ts +52 -0
- package/src/components/code-mirror-ghost.ts +158 -0
- package/src/components/code-mirror-inline-edit.ts +55 -0
- package/src/components/code-mirror-intel.ts +81 -0
- package/src/components/code-mirror-lang.ts +128 -0
- package/src/components/context-menu-utils.ts +20 -0
- package/src/components/date-picker-utils.ts +284 -0
- package/src/components/date-range-validation.ts +29 -0
- package/src/components/date-validation.ts +38 -0
- package/src/components/dialog-viewport.ts +48 -0
- package/src/components/dialog-window.ts +253 -0
- package/src/components/dropdown-utils.ts +31 -0
- package/src/components/form-utils.ts +77 -0
- package/src/components/input-number-utils.ts +128 -0
- package/src/components/input-rules.ts +84 -0
- package/src/components/loading-utils.ts +8 -0
- package/src/components/markdown-utils.ts +126 -0
- package/src/components/menu-utils.ts +48 -0
- package/src/components/overlay-utils.ts +47 -0
- package/src/components/pagination-utils.ts +38 -0
- package/src/components/popover-utils.ts +8 -0
- package/src/components/reka.ts +106 -0
- package/src/components/resolve-radius.ts +22 -0
- package/src/components/resolve-size.ts +29 -0
- package/src/components/scrollbar-utils.ts +10 -0
- package/src/components/select-utils.ts +72 -0
- package/src/components/split-pane-utils.ts +187 -0
- package/src/components/steps-utils.ts +22 -0
- package/src/components/table/RsTableBodyRow.vue +287 -0
- package/src/components/table/RsTableCell.vue +239 -0
- package/src/components/table/RsTableCellContent.ts +18 -0
- package/src/components/table/RsTableCellEditor.vue +351 -0
- package/src/components/table/RsTableEditGutter.vue +54 -0
- package/src/components/table/RsTableHeaderFilter.vue +107 -0
- package/src/components/table/RsTableRowStatus.vue +69 -0
- package/src/components/table/rs-table.css +1141 -0
- package/src/components/table/table-context-menu.ts +53 -0
- package/src/components/table/table-edit-utils.ts +496 -0
- package/src/components/table/table-keys.ts +12 -0
- package/src/components/table-drag.ts +215 -0
- package/src/components/table-utils.ts +950 -0
- package/src/components/tabs-utils.ts +107 -0
- package/src/components/terminal-utils.ts +301 -0
- package/src/components/terminal-wheel.ts +49 -0
- package/src/components/time-picker-utils.ts +167 -0
- package/src/components/time-range-validation.ts +23 -0
- package/src/components/time-validation.ts +20 -0
- package/src/components/tree-utils.ts +518 -0
- package/src/components/upload-utils.ts +55 -0
- package/src/components/virtual-list-utils.ts +17 -0
- package/src/composables/useClickOutside.ts +22 -0
- package/src/composables/useRsConfig.ts +91 -0
- package/src/composables/useRsI18n.ts +22 -0
- package/src/composables/useRsTabsNav.ts +187 -0
- package/src/composables/useRsToast.ts +48 -0
- package/src/composables/useTableCellTooltip.ts +239 -0
- package/src/composables/useTableEdit.ts +271 -0
- package/src/composables/useTableRowHighlight.ts +50 -0
- package/src/dev/vite-codemirror-deps.ts +20 -0
- package/src/icons/custom/api.ts +54 -0
- package/src/icons/custom/clickhouse.ts +52 -0
- package/src/icons/custom/dameng.ts +83 -0
- package/src/icons/custom/ftp.ts +61 -0
- package/src/icons/custom/kingbase.ts +52 -0
- package/src/icons/custom/mongodb.ts +50 -0
- package/src/icons/custom/mysql.ts +51 -0
- package/src/icons/custom/oracle.ts +52 -0
- package/src/icons/custom/redis.ts +51 -0
- package/src/icons/custom/sqlite.ts +50 -0
- package/src/icons/custom/sqlserver.ts +52 -0
- package/src/icons/custom/vastbase.ts +57 -0
- package/src/icons/lucide.ts +78 -0
- package/src/icons/registry.ts +35 -0
- package/src/index.ts +425 -0
- package/src/lib/iso-local-datetime.ts +152 -0
- package/src/lib/rs-dayjs.ts +56 -0
- package/src/locale/messages.ts +362 -0
- package/src/locale/types.ts +5 -0
- package/src/monaco/debug-decorations.css +20 -0
- package/src/monaco/debug-decorations.ts +70 -0
- package/src/monaco/index.ts +22 -0
- package/src/monaco/languages/index.ts +21 -0
- package/src/monaco/languages/mongodb-shell.ts +125 -0
- package/src/monaco/languages/types.ts +55 -0
- package/src/monaco/setup.ts +101 -0
- package/src/styles.css +1040 -0
- package/src/theme/apply.ts +12 -0
- package/src/theme/brand.example.css +37 -0
- package/src/theme/presets.ts +69 -0
- package/src/theme/types.ts +81 -0
- package/src/utils/rs-clipboard.ts +272 -0
- package/src/vite-env.d.ts +7 -0
- package/vite-plugins/monaco-zh-nls.ts +101 -0
- 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
|
+
}
|