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,77 @@
1
+ import {
2
+ computed,
3
+ inject,
4
+ onMounted,
5
+ onUnmounted,
6
+ provide,
7
+ type ComputedRef,
8
+ type InjectionKey,
9
+ type Ref,
10
+ } from 'vue'
11
+
12
+ export type RsFormLabelPosition = 'top' | 'left'
13
+ export type RsFormLabelAlign = 'start' | 'end'
14
+ export type RsFormSize = 'ssm' | 'sm' | 'md' | 'lg'
15
+ export type RsFormGap = 'sm' | 'md' | 'lg'
16
+ export type RsFormMaxWidth = 'sm' | 'md' | 'lg' | 'full' | 'none'
17
+
18
+ export interface RsFormValidationResult {
19
+ valid: boolean
20
+ }
21
+
22
+ export interface RsFormFieldExpose {
23
+ getValue: () => unknown
24
+ setValue: (value: unknown) => void
25
+ validate?: () => RsFormValidationResult | Promise<RsFormValidationResult>
26
+ clearValidation?: () => void
27
+ }
28
+
29
+ export interface RsFormContext {
30
+ disabled: Readonly<Ref<boolean>>
31
+ labelPosition: ComputedRef<RsFormLabelPosition>
32
+ labelWidth: Readonly<Ref<string>>
33
+ labelAlign: Readonly<Ref<RsFormLabelAlign>>
34
+ size: Readonly<Ref<RsFormSize>>
35
+ registerField: (id: symbol, field: RsFormFieldExpose) => void
36
+ unregisterField: (id: symbol) => void
37
+ }
38
+
39
+ export const RS_FORM_INJECTION_KEY: InjectionKey<RsFormContext> = Symbol('RsForm')
40
+
41
+ export function cloneFormFieldValue(value: unknown): unknown {
42
+ if (value === undefined || value === null) return value
43
+ if (typeof value !== 'object') return value
44
+ try {
45
+ return structuredClone(value)
46
+ } catch {
47
+ return value
48
+ }
49
+ }
50
+
51
+ export function provideRsFormContext(context: RsFormContext): void {
52
+ provide(RS_FORM_INJECTION_KEY, context)
53
+ }
54
+
55
+ export function useRsFormContext(): RsFormContext | null {
56
+ return inject(RS_FORM_INJECTION_KEY, null)
57
+ }
58
+
59
+ export function useRsFormField(getExpose: () => RsFormFieldExpose | undefined): void {
60
+ const form = useRsFormContext()
61
+ if (!form) return
62
+
63
+ const fieldId = Symbol('rs-form-field')
64
+
65
+ onMounted(() => {
66
+ const expose = getExpose()
67
+ if (expose) form.registerField(fieldId, expose)
68
+ })
69
+
70
+ onUnmounted(() => {
71
+ form.unregisterField(fieldId)
72
+ })
73
+ }
74
+
75
+ export function resolveRsFormSize(size?: RsFormSize, form?: RsFormContext | null): ComputedRef<RsFormSize> {
76
+ return computed(() => size ?? form?.size.value ?? 'md')
77
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * RsInputNumber 纯函数工具(对齐 Ant InputNumber 核心语义)。
3
+ * 展示/编辑过程用文本草稿;提交边界再解析为 number / string。
4
+ */
5
+
6
+ export type RsInputNumberValue = number | string | null
7
+
8
+ export function countDecimalPlaces(value: number | string): number {
9
+ const s = String(value)
10
+ const exp = s.toLowerCase().indexOf('e')
11
+ if (exp >= 0) {
12
+ const base = s.slice(0, exp)
13
+ const power = Number(s.slice(exp + 1))
14
+ const baseDecimals = Math.max(0, (base.split('.')[1] ?? '').length)
15
+ return Math.max(0, baseDecimals - power)
16
+ }
17
+ const dot = s.indexOf('.')
18
+ return dot >= 0 ? s.length - dot - 1 : 0
19
+ }
20
+
21
+ export function resolveNumberPrecision(step: number | string, precision?: number): number {
22
+ if (precision != null && Number.isFinite(precision) && precision >= 0) {
23
+ return Math.floor(precision)
24
+ }
25
+ return countDecimalPlaces(step)
26
+ }
27
+
28
+ export function roundToPrecision(value: number, precision: number): number {
29
+ if (!Number.isFinite(value)) return value
30
+ if (precision <= 0) return Math.round(value)
31
+ const factor = 10 ** precision
32
+ return Math.round(value * factor) / factor
33
+ }
34
+
35
+ /** 是否为尚可继续输入的中间态(如 `-`、`1.`、`1e`)。 */
36
+ export function isNumberInputInterim(text: string): boolean {
37
+ const t = text.trim()
38
+ if (!t) return true
39
+ // 末尾符号:继续输入小数/指数
40
+ if (/[+-]$/.test(t) || /\.$/.test(t) || /[eE][+-]?$/.test(t)) return true
41
+ if (!/^[+-]?(\d+\.?\d*|\.\d*)([eE][+-]?\d+)?$/.test(t)) return false
42
+ return Number.isNaN(Number(t))
43
+ }
44
+
45
+ export function parseNumberInput(text: string): number | null {
46
+ const t = text.trim()
47
+ if (!t || t === '+' || t === '-' || t === '.' || t === '+.' || t === '-.') return null
48
+ const n = Number(t)
49
+ return Number.isFinite(n) ? n : null
50
+ }
51
+
52
+ export function clampNumber(value: number, min?: number, max?: number): number {
53
+ let next = value
54
+ if (min != null && Number.isFinite(min) && next < min) next = min
55
+ if (max != null && Number.isFinite(max) && next > max) next = max
56
+ return next
57
+ }
58
+
59
+ export function formatNumberValue(
60
+ value: number | null,
61
+ opts?: {
62
+ precision?: number
63
+ formatter?: (value: number, info: { userTyping: boolean; input: string }) => string
64
+ userTyping?: boolean
65
+ input?: string
66
+ },
67
+ ): string {
68
+ if (value == null || !Number.isFinite(value)) return ''
69
+ if (opts?.formatter) {
70
+ return opts.formatter(value, {
71
+ userTyping: Boolean(opts.userTyping),
72
+ input: opts.input ?? String(value),
73
+ })
74
+ }
75
+ if (opts?.precision != null && opts.precision >= 0) {
76
+ return value.toFixed(opts.precision)
77
+ }
78
+ return String(value)
79
+ }
80
+
81
+ export function stepNumberValue(
82
+ current: number | null,
83
+ direction: 1 | -1,
84
+ opts: {
85
+ step?: number | string
86
+ min?: number
87
+ max?: number
88
+ precision?: number
89
+ } = {},
90
+ ): number {
91
+ const step = Number(opts.step ?? 1)
92
+ const safeStep = Number.isFinite(step) && step !== 0 ? Math.abs(step) : 1
93
+ const precision = resolveNumberPrecision(safeStep, opts.precision)
94
+ const base = current == null || !Number.isFinite(current) ? 0 : current
95
+ const next = roundToPrecision(base + direction * safeStep, precision)
96
+ return clampNumber(next, opts.min, opts.max)
97
+ }
98
+
99
+ export function normalizeCommittedNumber(
100
+ raw: string,
101
+ opts: {
102
+ min?: number
103
+ max?: number
104
+ precision?: number
105
+ step?: number | string
106
+ parser?: (display: string) => string
107
+ } = {},
108
+ ): number | null {
109
+ const text = opts.parser ? opts.parser(raw) : raw
110
+ const parsed = parseNumberInput(text)
111
+ if (parsed == null) return null
112
+ const precision = resolveNumberPrecision(opts.step ?? 1, opts.precision)
113
+ return clampNumber(roundToPrecision(parsed, precision), opts.min, opts.max)
114
+ }
115
+
116
+ export function toModelValue(
117
+ value: number | null,
118
+ stringMode: boolean,
119
+ ): RsInputNumberValue {
120
+ if (value == null) return null
121
+ return stringMode ? String(value) : value
122
+ }
123
+
124
+ export function fromModelValue(value: RsInputNumberValue): number | null {
125
+ if (value == null || value === '') return null
126
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null
127
+ return parseNumberInput(String(value))
128
+ }
@@ -0,0 +1,84 @@
1
+ import type { RsTranslateFn } from '../composables/useRsI18n'
2
+ import { createTranslator } from '../composables/useRsI18n'
3
+ import { defaultLocale } from '../locale/types'
4
+
5
+ export type RsInputRule = 'email' | 'number' | 'integer' | 'phone' | 'url' | 'minComplexity'
6
+
7
+ export type RsInputValidateTrigger = 'blur' | 'input' | 'both'
8
+
9
+ export const inputRuleMessageKeys: Record<RsInputRule, string> = {
10
+ email: 'input.rule.email',
11
+ number: 'input.rule.number',
12
+ integer: 'input.rule.integer',
13
+ phone: 'input.rule.phone',
14
+ url: 'input.rule.url',
15
+ minComplexity: 'input.rule.minComplexity',
16
+ }
17
+
18
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
19
+ const numberPattern = /^-?\d+(\.\d+)?$/
20
+ const integerPattern = /^-?\d+$/
21
+ const phonePattern = /^1[3-9]\d{9}$/
22
+ const urlPattern = /^https?:\/\/.+/i
23
+ const minComplexityPattern = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/
24
+
25
+ export function validateInputRule(value: string, rule: RsInputRule): boolean {
26
+ if (value === '') return true
27
+
28
+ switch (rule) {
29
+ case 'email':
30
+ return emailPattern.test(value)
31
+ case 'number':
32
+ return numberPattern.test(value)
33
+ case 'integer':
34
+ return integerPattern.test(value)
35
+ case 'phone':
36
+ return phonePattern.test(value)
37
+ case 'url':
38
+ return urlPattern.test(value)
39
+ case 'minComplexity':
40
+ return minComplexityPattern.test(value)
41
+ default:
42
+ return true
43
+ }
44
+ }
45
+
46
+ export function getInputRuleMessage(rule: RsInputRule, t?: RsTranslateFn): string {
47
+ const key = inputRuleMessageKeys[rule]
48
+ const tr = t ?? createTranslator(defaultLocale)
49
+ return tr(key)
50
+ }
51
+
52
+ export interface RsInputValidateResult {
53
+ valid: boolean
54
+ message?: string
55
+ }
56
+
57
+ export function runInputValidation(
58
+ value: string,
59
+ options: {
60
+ rule?: RsInputRule
61
+ required?: boolean
62
+ validator?: (value: string) => boolean | string
63
+ },
64
+ t?: RsTranslateFn,
65
+ ): RsInputValidateResult {
66
+ const tr = t ?? createTranslator(defaultLocale)
67
+
68
+ if (options.required && value.trim() === '') {
69
+ return { valid: false, message: tr('input.required') }
70
+ }
71
+
72
+ if (options.rule && !validateInputRule(value, options.rule)) {
73
+ return { valid: false, message: getInputRuleMessage(options.rule, tr) }
74
+ }
75
+
76
+ if (options.validator) {
77
+ const result = options.validator(value)
78
+ if (result === true) return { valid: true }
79
+ if (result === false) return { valid: false, message: tr('input.formatInvalid') }
80
+ if (typeof result === 'string') return { valid: false, message: result }
81
+ }
82
+
83
+ return { valid: true }
84
+ }
@@ -0,0 +1,8 @@
1
+ /** 加载指示器尺寸 */
2
+ export type RsLoadingSize = 'sm' | 'md' | 'lg'
3
+
4
+ /** 加载指示器色调 */
5
+ export type RsLoadingTone = 'default' | 'primary'
6
+
7
+ /** 加载指示器变体 */
8
+ export type RsLoadingVariant = 'spinner' | 'dots' | 'skeleton'
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Markdown 渲染工具:GFM + 安全链接/图片 + DOMPurify 消毒。
3
+ * 面向表单/说明类场景,不含 AI 流式、公式、图表等业务能力。
4
+ */
5
+ import { Marked } from 'marked'
6
+ import type { Tokens } from 'marked'
7
+ import DOMPurify from 'dompurify'
8
+
9
+ export type RsMarkdownMode = 'edit' | 'preview' | 'split'
10
+
11
+ export type RsMarkdownRenderOptions = {
12
+ /** GFM 换行(单个换行转 <br>),默认 true */
13
+ breaks?: boolean
14
+ }
15
+
16
+ export function escapeHtml(text: string): string {
17
+ return text
18
+ .replaceAll('&', '&amp;')
19
+ .replaceAll('<', '&lt;')
20
+ .replaceAll('>', '&gt;')
21
+ .replaceAll('"', '&quot;')
22
+ .replaceAll("'", '&#39;')
23
+ }
24
+
25
+ export function isSafeHref(href: string): boolean {
26
+ const value = href.trim()
27
+ if (!value) return false
28
+ if (value.startsWith('#') || value.startsWith('/')) return true
29
+ return /^(https?:|mailto:)/i.test(value)
30
+ }
31
+
32
+ export function isSafeImageSrc(src: string): boolean {
33
+ const value = src.trim()
34
+ if (!value) return false
35
+ if (value.startsWith('data:image/')) return true
36
+ return /^(https?:)/i.test(value)
37
+ }
38
+
39
+ function sanitizeLang(lang: string | undefined): string {
40
+ const raw = (lang ?? 'text').trim().split(/\s+/)[0] ?? 'text'
41
+ return raw.replace(/[^\w.+#-]/g, '') || 'text'
42
+ }
43
+
44
+ function renderCodeBlock(text: string, lang: string | undefined): string {
45
+ const language = sanitizeLang(lang)
46
+ const code = text.replace(/\n$/, '')
47
+ return [
48
+ `<div class="rs-markdown__codeblock" data-rs-md-lang="${escapeHtml(language)}">`,
49
+ `<div class="rs-markdown__code-head">`,
50
+ `<span class="rs-markdown__code-lang">${escapeHtml(language)}</span>`,
51
+ `</div>`,
52
+ `<pre class="rs-markdown__pre"><code class="language-${escapeHtml(language)}">${escapeHtml(code)}</code></pre>`,
53
+ `</div>`,
54
+ ].join('')
55
+ }
56
+
57
+ function wrapTables(html: string): string {
58
+ return html.replace(/<table\b[\s\S]*?<\/table>/gi, (table) => {
59
+ return `<div class="rs-markdown__table-wrap">${table}</div>`
60
+ })
61
+ }
62
+
63
+ function createMarked(breaks: boolean): Marked {
64
+ return new Marked({
65
+ gfm: true,
66
+ breaks,
67
+ renderer: {
68
+ code({ text, lang }) {
69
+ return renderCodeBlock(text, lang)
70
+ },
71
+ link(
72
+ this: { parser: { parseInline: (tokens: Tokens.Link['tokens']) => string } },
73
+ token: Tokens.Link,
74
+ ) {
75
+ const text = this.parser.parseInline(token.tokens)
76
+ const href = token.href?.trim() ?? ''
77
+ if (!isSafeHref(href)) return text
78
+ const title = token.title ? ` title="${escapeHtml(token.title)}"` : ''
79
+ return `<a href="${escapeHtml(href)}"${title} target="_blank" rel="noopener noreferrer">${text}</a>`
80
+ },
81
+ image({ href, title, text }) {
82
+ const src = href?.trim() ?? ''
83
+ if (!isSafeImageSrc(src)) return escapeHtml(text || '')
84
+ const alt = escapeHtml(text || '')
85
+ const titleAttr = title ? ` title="${escapeHtml(title)}"` : ''
86
+ return `<img class="rs-markdown__img" src="${escapeHtml(src)}" alt="${alt}"${titleAttr} loading="lazy" decoding="async" />`
87
+ },
88
+ },
89
+ })
90
+ }
91
+
92
+ const markedCache = new Map<boolean, Marked>()
93
+
94
+ function getMarked(breaks: boolean): Marked {
95
+ const cached = markedCache.get(breaks)
96
+ if (cached) return cached
97
+ const instance = createMarked(breaks)
98
+ markedCache.set(breaks, instance)
99
+ return instance
100
+ }
101
+
102
+ /**
103
+ * 将 Markdown 转为可安全注入的 HTML(必须配合 v-html 使用消毒结果)。
104
+ */
105
+ export function renderMarkdown(source = '', options?: RsMarkdownRenderOptions): string {
106
+ if (!source.trim()) return ''
107
+ const breaks = options?.breaks !== false
108
+ const dirty = wrapTables(getMarked(breaks).parse(source, { async: false }) as string)
109
+ return DOMPurify.sanitize(dirty, {
110
+ USE_PROFILES: { html: true },
111
+ ADD_ATTR: ['class', 'target', 'rel', 'loading', 'decoding', 'alt', 'src', 'data-rs-md-lang'],
112
+ })
113
+ }
114
+
115
+ export function resolveMarkdownHeight(height?: number | string): string {
116
+ if (height === undefined) return '16rem'
117
+ return typeof height === 'number' ? `${height}px` : height
118
+ }
119
+
120
+ export function resolveMarkdownMode(
121
+ mode: RsMarkdownMode | undefined,
122
+ readonly: boolean,
123
+ ): RsMarkdownMode {
124
+ if (readonly) return 'preview'
125
+ return mode ?? 'edit'
126
+ }
@@ -0,0 +1,48 @@
1
+ export interface RsMenuItem {
2
+ key: string
3
+ label: string
4
+ icon?: string
5
+ disabled?: boolean
6
+ children?: RsMenuItem[]
7
+ }
8
+
9
+ export interface RsMenuItemGroup {
10
+ label: string
11
+ children: RsMenuItem[]
12
+ }
13
+
14
+ export type RsMenuItems = RsMenuItem[] | RsMenuItemGroup[]
15
+
16
+ export function isMenuItemGroup(
17
+ item: RsMenuItem | RsMenuItemGroup,
18
+ ): item is RsMenuItemGroup {
19
+ return !('key' in item) && 'children' in item
20
+ }
21
+
22
+ export function findMenuParentKeys(items: RsMenuItems, key: string): string[] {
23
+ function walk(list: RsMenuItem[], ancestors: string[]): string[] | null {
24
+ for (const item of list) {
25
+ if (item.key === key) {
26
+ return ancestors
27
+ }
28
+ if (item.children?.length) {
29
+ const found = walk(item.children, [...ancestors, item.key])
30
+ if (found) {
31
+ return found
32
+ }
33
+ }
34
+ }
35
+ return null
36
+ }
37
+
38
+ for (const entry of items) {
39
+ const found = isMenuItemGroup(entry)
40
+ ? walk(entry.children, [])
41
+ : walk([entry], [])
42
+ if (found) {
43
+ return found
44
+ }
45
+ }
46
+
47
+ return []
48
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 浮层 / 模态 z-index 常量,与 `styles.css` 中 `--rs-z-*` token 对齐。
3
+ * 组件样式应使用 `z-index: var(--rs-z-*)`,禁止硬编码层级。
4
+ */
5
+
6
+ /** 下拉 / Popover / Tooltip 浮层 */
7
+ export const RS_Z_DROPDOWN = 'var(--rs-z-dropdown)'
8
+
9
+ /** Shell 常驻面板(底部 Dock 等),须低于 modal */
10
+ export const RS_Z_PANEL = 'var(--rs-z-panel)'
11
+
12
+ /** 模态对话框 */
13
+ export const RS_Z_MODAL = 'var(--rs-z-modal)'
14
+
15
+ /** 全局 Toast */
16
+ export const RS_Z_TOAST = 'var(--rs-z-toast)'
17
+
18
+ /** 反馈语义色调(Dialog 图标等) */
19
+ export type RsFeedbackTone = 'default' | 'info' | 'success' | 'warning' | 'danger'
20
+
21
+ export type RsToastType = 'success' | 'error' | 'info' | 'warning'
22
+
23
+ export type RsToastPosition =
24
+ | 'top-left'
25
+ | 'top-right'
26
+ | 'bottom-left'
27
+ | 'bottom-right'
28
+ | 'top-center'
29
+ | 'bottom-center'
30
+
31
+ export const RS_TOAST_DEFAULT_POSITION: RsToastPosition = 'top-center'
32
+
33
+ /** 多条 Toast 之间的垂直间距(px),传给 vue-sonner `gap` */
34
+ export const RS_TOAST_DEFAULT_GAP = 4
35
+
36
+ export const rsToastPositions = [
37
+ 'top-center',
38
+ 'top-left',
39
+ 'top-right',
40
+ 'bottom-center',
41
+ 'bottom-left',
42
+ 'bottom-right',
43
+ ] as const satisfies readonly RsToastPosition[]
44
+
45
+ export function rsFeedbackIconClass(tone: RsFeedbackTone): string {
46
+ return `rs-feedback-icon rs-feedback-icon--${tone}`
47
+ }
@@ -0,0 +1,38 @@
1
+ export const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100] as const
2
+
3
+ export function getPageCount(total: number, pageSize: number): number {
4
+ if (pageSize <= 0) return 1
5
+ return Math.max(1, Math.ceil(Math.max(0, total) / pageSize))
6
+ }
7
+
8
+ export function clampPage(page: number, pageCount: number): number {
9
+ return Math.min(Math.max(1, page), Math.max(1, pageCount))
10
+ }
11
+
12
+ export function getPaginationRange(page: number, pageCount: number, siblingCount = 1): Array<number | 'ellipsis'> {
13
+ const totalNumbers = siblingCount * 2 + 5
14
+ if (pageCount <= totalNumbers) return Array.from({ length: pageCount }, (_, index) => index + 1)
15
+
16
+ const leftSibling = Math.max(page - siblingCount, 1)
17
+ const rightSibling = Math.min(page + siblingCount, pageCount)
18
+ const showLeftEllipsis = leftSibling > 2
19
+ const showRightEllipsis = rightSibling < pageCount - 1
20
+
21
+ if (!showLeftEllipsis && showRightEllipsis) {
22
+ return [...Array.from({ length: 3 + siblingCount * 2 }, (_, index) => index + 1), 'ellipsis', pageCount]
23
+ }
24
+ if (showLeftEllipsis && !showRightEllipsis) {
25
+ const start = pageCount - (2 + siblingCount * 2)
26
+ return [1, 'ellipsis', ...Array.from({ length: 3 + siblingCount * 2 }, (_, index) => start + index)]
27
+ }
28
+ return [1, 'ellipsis', ...Array.from({ length: rightSibling - leftSibling + 1 }, (_, index) => leftSibling + index), 'ellipsis', pageCount]
29
+ }
30
+
31
+ export function slicePageData<T>(data: readonly T[], page: number, pageSize: number): T[] {
32
+ const start = (clampPage(page, getPageCount(data.length, pageSize)) - 1) * pageSize
33
+ return data.slice(start, start + pageSize)
34
+ }
35
+
36
+ export function createPageSizeSelectOptions(options: readonly number[] = DEFAULT_PAGE_SIZE_OPTIONS) {
37
+ return options.map((value) => ({ label: String(value), value: String(value) }))
38
+ }
@@ -0,0 +1,8 @@
1
+ /** Popover 弹出方位 */
2
+ export type RsPopoverSide = 'top' | 'right' | 'bottom' | 'left'
3
+
4
+ /** Popover 对齐方式 */
5
+ export type RsPopoverAlign = 'start' | 'center' | 'end'
6
+
7
+ /** Popover 内容宽度档位 */
8
+ export type RsPopoverWidth = 'sm' | 'md' | 'lg' | 'auto'
@@ -0,0 +1,106 @@
1
+ export {
2
+ AlertDialogAction,
3
+ AlertDialogCancel,
4
+ AlertDialogContent,
5
+ AlertDialogDescription,
6
+ AlertDialogOverlay,
7
+ AlertDialogPortal,
8
+ AlertDialogRoot,
9
+ AlertDialogTitle,
10
+ AlertDialogTrigger,
11
+ Primitive,
12
+ AvatarFallback,
13
+ AvatarImage,
14
+ AvatarRoot,
15
+ CollapsibleContent,
16
+ CollapsibleRoot,
17
+ CollapsibleTrigger,
18
+ ComboboxAnchor,
19
+ ComboboxContent,
20
+ ComboboxEmpty,
21
+ ComboboxGroup,
22
+ ComboboxInput,
23
+ ComboboxItem,
24
+ ComboboxItemIndicator,
25
+ ComboboxLabel,
26
+ ComboboxPortal,
27
+ ComboboxRoot,
28
+ ComboboxTrigger,
29
+ ComboboxViewport,
30
+ ComboboxVirtualizer,
31
+ ContextMenuContent,
32
+ ContextMenuItem,
33
+ ContextMenuPortal,
34
+ ContextMenuRoot,
35
+ ContextMenuSeparator,
36
+ ContextMenuSub,
37
+ ContextMenuSubContent,
38
+ ContextMenuSubTrigger,
39
+ ContextMenuTrigger,
40
+ DialogClose,
41
+ DialogContent,
42
+ DialogDescription,
43
+ DialogOverlay,
44
+ DialogPortal,
45
+ DialogRoot,
46
+ DialogTitle,
47
+ DialogTrigger,
48
+ DropdownMenuContent,
49
+ DropdownMenuGroup,
50
+ DropdownMenuItem,
51
+ DropdownMenuLabel,
52
+ DropdownMenuPortal,
53
+ DropdownMenuRadioGroup,
54
+ DropdownMenuRadioItem,
55
+ DropdownMenuRoot,
56
+ DropdownMenuTrigger,
57
+ PopoverContent,
58
+ PopoverPortal,
59
+ PopoverRoot,
60
+ PopoverTrigger,
61
+ PaginationEllipsis,
62
+ PaginationFirst,
63
+ PaginationLast,
64
+ PaginationList,
65
+ PaginationListItem,
66
+ PaginationNext,
67
+ PaginationPrev,
68
+ PaginationRoot,
69
+ ScrollAreaCorner,
70
+ ScrollAreaRoot,
71
+ ScrollAreaScrollbar,
72
+ ScrollAreaThumb,
73
+ ScrollAreaViewport,
74
+ SelectContent,
75
+ SelectItem,
76
+ SelectItemIndicator,
77
+ SelectItemText,
78
+ SelectPortal,
79
+ SelectRoot,
80
+ SelectTrigger,
81
+ SelectValue,
82
+ SelectViewport,
83
+ StepperDescription,
84
+ StepperIndicator,
85
+ StepperItem,
86
+ StepperRoot,
87
+ StepperSeparator,
88
+ StepperTitle,
89
+ StepperTrigger,
90
+ TabsContent,
91
+ TabsList,
92
+ TabsRoot,
93
+ TabsTrigger,
94
+ TooltipContent,
95
+ TooltipPortal,
96
+ TooltipProvider,
97
+ TooltipRoot,
98
+ TooltipTrigger,
99
+ TreeItem,
100
+ TreeRoot,
101
+ TreeVirtualizer,
102
+ useForwardExpose,
103
+ useFilter,
104
+ } from 'reka-ui'
105
+
106
+ export type { AcceptableValue, PrimitiveProps, TooltipRootEmits, TooltipRootProps } from 'reka-ui'
@@ -0,0 +1,22 @@
1
+ import { computed, type ComputedRef } from 'vue'
2
+ import { RS_RADIUS_CSS, type RsRadius } from '../theme/types'
3
+ import { useRsConfigOptional } from '../composables/useRsConfig'
4
+
5
+ /**
6
+ * 解析圆角:props → ConfigProvider.controlRadius → 组件默认档。
7
+ * 业务应传 `radius` / `iconRadius` 或依赖全局配置,避免 :deep 改 border-radius。
8
+ */
9
+ export function useResolvedRsRadius(
10
+ radius: () => RsRadius | undefined,
11
+ fallback: RsRadius,
12
+ ): ComputedRef<RsRadius> {
13
+ const config = useRsConfigOptional()
14
+ return computed(
15
+ () => radius() ?? config?.controlRadius.value ?? fallback,
16
+ )
17
+ }
18
+
19
+ /** 将档位转为可写入 style / CSS 变量的值。 */
20
+ export function rsRadiusCss(radius: RsRadius): string {
21
+ return RS_RADIUS_CSS[radius]
22
+ }