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,532 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * RsInputNumber — 对齐 Ant Design InputNumber / Element Plus InputNumber。
4
+ *
5
+ * 契约:
6
+ * - 默认 modelValue 为 `number | null`
7
+ * - `stringMode` 时为 `string | null`(高精度 / 表格草稿场景)
8
+ * - 输入过程用文本草稿,允许中间态;blur / step / Enter 再规范化
9
+ */
10
+ import { computed, ref, useId, watch } from 'vue'
11
+ import type { RsComponentSize, RsRadius } from '../theme/types'
12
+ import { useRsI18n } from '../composables/useRsI18n'
13
+ import { useResolvedRsComponentSize } from './resolve-size'
14
+ import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
15
+ import {
16
+ useRsFormContext,
17
+ useRsFormField,
18
+ type RsFormLabelPosition,
19
+ } from './form-utils'
20
+ import RsIcon from './RsIcon.vue'
21
+ import {
22
+ formatNumberValue,
23
+ fromModelValue,
24
+ isNumberInputInterim,
25
+ normalizeCommittedNumber,
26
+ stepNumberValue,
27
+ toModelValue,
28
+ type RsInputNumberValue,
29
+ } from './input-number-utils'
30
+
31
+ defineOptions({ inheritAttrs: false })
32
+
33
+ const { t } = useRsI18n()
34
+
35
+ const model = defineModel<RsInputNumberValue>({ default: null })
36
+
37
+ const props = withDefaults(
38
+ defineProps<{
39
+ id?: string
40
+ min?: number
41
+ max?: number
42
+ step?: number | string
43
+ /** 小数位;未设时按 step 推断 */
44
+ precision?: number
45
+ /**
46
+ * true:modelValue 为 string | null(对齐 Ant stringMode)
47
+ * false:modelValue 为 number | null
48
+ */
49
+ stringMode?: boolean
50
+ /** 是否展示步进按钮(对齐 Ant controls) */
51
+ controls?: boolean
52
+ /** 是否响应键盘上下键(对齐 Ant keyboard) */
53
+ keyboard?: boolean
54
+ /** 是否响应滚轮改值(默认关,避免误触) */
55
+ changeOnWheel?: boolean
56
+ disabled?: boolean
57
+ readonly?: boolean
58
+ placeholder?: string
59
+ size?: RsComponentSize
60
+ radius?: RsRadius
61
+ invalid?: boolean
62
+ errorMessage?: string
63
+ label?: string
64
+ hint?: string
65
+ required?: boolean
66
+ labelPosition?: RsFormLabelPosition
67
+ /** 展示格式化(对齐 Ant formatter) */
68
+ formatter?: (value: number, info: { userTyping: boolean; input: string }) => string
69
+ /** 解析展示值(对齐 Ant parser) */
70
+ parser?: (display: string) => string
71
+ }>(),
72
+ {
73
+ step: 1,
74
+ stringMode: false,
75
+ controls: true,
76
+ keyboard: true,
77
+ changeOnWheel: false,
78
+ disabled: false,
79
+ readonly: false,
80
+ required: false,
81
+ },
82
+ )
83
+
84
+ const emit = defineEmits<{
85
+ pressEnter: [event: KeyboardEvent]
86
+ focus: [event: FocusEvent]
87
+ blur: [event: FocusEvent]
88
+ step: [value: RsInputNumberValue, info: { offset: number }]
89
+ }>()
90
+
91
+ const formContext = useRsFormContext()
92
+ const autoId = useId()
93
+ const resolvedId = computed(() => props.id || `rs-input-number-${autoId}`)
94
+ const resolvedSize = useResolvedRsComponentSize(() => props.size)
95
+ const resolvedRadius = useResolvedRsRadius(() => props.radius, 'sm')
96
+ const radiusStyle = computed(() => ({
97
+ '--rs-input-radius': rsRadiusCss(resolvedRadius.value),
98
+ }))
99
+
100
+ const resolvedLabelPosition = computed(
101
+ () => props.labelPosition ?? formContext?.labelPosition.value ?? 'top',
102
+ )
103
+ const resolvedLabelAlign = computed(() => formContext?.labelAlign.value ?? 'start')
104
+ const resolvedDisabled = computed(
105
+ () => props.disabled || Boolean(formContext?.disabled.value),
106
+ )
107
+ const resolvedReadonly = computed(() => props.readonly)
108
+
109
+ const fieldStyle = computed(() => {
110
+ if (resolvedLabelPosition.value !== 'left') return undefined
111
+ const labelWidth = formContext?.labelWidth.value
112
+ if (!labelWidth) return undefined
113
+ return { '--rs-field-label-width': labelWidth }
114
+ })
115
+
116
+ const errorId = computed(() => `${resolvedId.value}-error`)
117
+ const isInvalid = computed(() => Boolean(props.invalid || props.errorMessage))
118
+ const displayMessage = computed(() => props.errorMessage || '')
119
+
120
+ /** 输入框文本草稿(允许中间态) */
121
+ const draft = ref('')
122
+ const focused = ref(false)
123
+ const userTyping = ref(false)
124
+
125
+ function syncDraftFromModel(value: RsInputNumberValue = model.value): void {
126
+ const num = fromModelValue(value)
127
+ draft.value = formatNumberValue(num, {
128
+ precision: props.precision,
129
+ formatter: props.formatter,
130
+ userTyping: false,
131
+ input: num == null ? '' : String(num),
132
+ })
133
+ }
134
+
135
+ watch(
136
+ () => model.value,
137
+ (value) => {
138
+ if (focused.value && userTyping.value) return
139
+ syncDraftFromModel(value)
140
+ },
141
+ { immediate: true },
142
+ )
143
+
144
+ watch(
145
+ () => [props.precision, props.formatter] as const,
146
+ () => {
147
+ if (!focused.value) syncDraftFromModel()
148
+ },
149
+ )
150
+
151
+ const numericValue = computed(() => fromModelValue(model.value))
152
+
153
+ const canIncrease = computed(() => {
154
+ if (resolvedDisabled.value || resolvedReadonly.value) return false
155
+ if (props.max == null || !Number.isFinite(props.max)) return true
156
+ const current = numericValue.value
157
+ if (current == null) return true
158
+ return current < props.max
159
+ })
160
+
161
+ const canDecrease = computed(() => {
162
+ if (resolvedDisabled.value || resolvedReadonly.value) return false
163
+ if (props.min == null || !Number.isFinite(props.min)) return true
164
+ const current = numericValue.value
165
+ if (current == null) return true
166
+ return current > props.min
167
+ })
168
+
169
+ function commitDraft(raw = draft.value): void {
170
+ const next = normalizeCommittedNumber(raw, {
171
+ min: props.min,
172
+ max: props.max,
173
+ precision: props.precision,
174
+ step: props.step,
175
+ parser: props.parser,
176
+ })
177
+ model.value = toModelValue(next, props.stringMode)
178
+ syncDraftFromModel(model.value)
179
+ userTyping.value = false
180
+ }
181
+
182
+ function applyStep(direction: 1 | -1): void {
183
+ if (resolvedDisabled.value || resolvedReadonly.value) return
184
+ // 先提交中间态,再步进(对齐 Ant:基于合法值步进)
185
+ if (userTyping.value) commitDraft()
186
+ const next = stepNumberValue(numericValue.value, direction, {
187
+ step: props.step,
188
+ min: props.min,
189
+ max: props.max,
190
+ precision: props.precision,
191
+ })
192
+ model.value = toModelValue(next, props.stringMode)
193
+ syncDraftFromModel(model.value)
194
+ emit('step', model.value, { offset: direction * Number(props.step || 1) })
195
+ }
196
+
197
+ function onNativeInput(event: Event): void {
198
+ const el = event.target as HTMLInputElement
199
+ const text = el.value
200
+ userTyping.value = true
201
+ draft.value = text
202
+
203
+ // 合法完整数字可即时同步(仍保留中间态文本)
204
+ if (!isNumberInputInterim(text) && text.trim() !== '') {
205
+ const next = normalizeCommittedNumber(text, {
206
+ min: props.min,
207
+ max: props.max,
208
+ precision: props.precision,
209
+ step: props.step,
210
+ parser: props.parser,
211
+ })
212
+ if (next != null) {
213
+ model.value = toModelValue(next, props.stringMode)
214
+ }
215
+ } else if (text.trim() === '') {
216
+ model.value = null
217
+ }
218
+ }
219
+
220
+ function onFocus(event: FocusEvent): void {
221
+ focused.value = true
222
+ emit('focus', event)
223
+ }
224
+
225
+ function onBlur(event: FocusEvent): void {
226
+ focused.value = false
227
+ commitDraft()
228
+ emit('blur', event)
229
+ }
230
+
231
+ function onKeydown(event: KeyboardEvent): void {
232
+ if (event.key === 'Enter') {
233
+ commitDraft()
234
+ emit('pressEnter', event)
235
+ return
236
+ }
237
+ if (!props.keyboard || resolvedDisabled.value || resolvedReadonly.value) return
238
+ if (event.key === 'ArrowUp') {
239
+ event.preventDefault()
240
+ applyStep(1)
241
+ } else if (event.key === 'ArrowDown') {
242
+ event.preventDefault()
243
+ applyStep(-1)
244
+ }
245
+ }
246
+
247
+ function onWheel(event: WheelEvent): void {
248
+ if (!props.changeOnWheel || !focused.value) return
249
+ if (resolvedDisabled.value || resolvedReadonly.value) return
250
+ event.preventDefault()
251
+ applyStep(event.deltaY < 0 ? 1 : -1)
252
+ }
253
+
254
+ function setValue(value: unknown): void {
255
+ if (value == null || value === '') {
256
+ model.value = null
257
+ syncDraftFromModel(null)
258
+ return
259
+ }
260
+ const num = fromModelValue(
261
+ typeof value === 'number' || typeof value === 'string' ? value : String(value),
262
+ )
263
+ model.value = toModelValue(num, props.stringMode)
264
+ syncDraftFromModel(model.value)
265
+ }
266
+
267
+ useRsFormField(() => ({
268
+ getValue: () => model.value,
269
+ setValue,
270
+ validate: () => {
271
+ if (props.required && model.value == null) {
272
+ return { valid: false }
273
+ }
274
+ return { valid: true }
275
+ },
276
+ clearValidation: () => undefined,
277
+ }))
278
+
279
+ defineExpose({
280
+ focus: () => {
281
+ const el = document.getElementById(resolvedId.value) as HTMLInputElement | null
282
+ el?.focus()
283
+ },
284
+ blur: () => {
285
+ const el = document.getElementById(resolvedId.value) as HTMLInputElement | null
286
+ el?.blur()
287
+ },
288
+ commit: commitDraft,
289
+ setValue,
290
+ step: applyStep,
291
+ })
292
+ </script>
293
+
294
+ <template>
295
+ <div
296
+ class="rs-field"
297
+ :class="[
298
+ `rs-field--label-${resolvedLabelPosition}`,
299
+ resolvedLabelPosition === 'left' && resolvedLabelAlign === 'end'
300
+ ? 'rs-field--label-align-end'
301
+ : undefined,
302
+ `rs-input-number-field--${resolvedSize}`,
303
+ ]"
304
+ :style="[fieldStyle, radiusStyle]"
305
+ >
306
+ <span v-if="label" class="rs-field__label">
307
+ <label :for="resolvedId">{{ label }}</label>
308
+ <span v-if="required" class="rs-field__required" aria-hidden="true">*</span>
309
+ </span>
310
+
311
+ <div class="rs-input-number-field">
312
+ <div
313
+ class="rs-input-number"
314
+ :class="{
315
+ 'rs-input-number--invalid': isInvalid,
316
+ 'rs-input-number--disabled': resolvedDisabled,
317
+ 'rs-input-number--readonly': resolvedReadonly,
318
+ 'rs-input-number--controls': controls,
319
+ [`rs-input-number--${resolvedSize}`]: true,
320
+ }"
321
+ >
322
+ <input
323
+ :id="resolvedId"
324
+ class="rs-input-number__control"
325
+ :class="`rs-input-number__control--${resolvedSize}`"
326
+ type="text"
327
+ inputmode="decimal"
328
+ role="spinbutton"
329
+ :value="draft"
330
+ :placeholder="placeholder"
331
+ :disabled="resolvedDisabled"
332
+ :readonly="resolvedReadonly"
333
+ :aria-invalid="isInvalid || undefined"
334
+ :aria-describedby="displayMessage ? errorId : undefined"
335
+ :aria-valuemin="min"
336
+ :aria-valuemax="max"
337
+ :aria-valuenow="numericValue ?? undefined"
338
+ :aria-valuetext="draft || undefined"
339
+ @input="onNativeInput"
340
+ @focus="onFocus"
341
+ @blur="onBlur"
342
+ @keydown="onKeydown"
343
+ @wheel="onWheel"
344
+ />
345
+
346
+ <div v-if="controls" class="rs-input-number__handlers" aria-hidden="true">
347
+ <button
348
+ type="button"
349
+ class="rs-input-number__handler rs-input-number__handler--up"
350
+ tabindex="-1"
351
+ :disabled="!canIncrease"
352
+ :aria-label="t('inputNumber.increase')"
353
+ @pointerdown.prevent
354
+ @click="applyStep(1)"
355
+ >
356
+ <RsIcon name="chevron-up" :size="12" />
357
+ </button>
358
+ <button
359
+ type="button"
360
+ class="rs-input-number__handler rs-input-number__handler--down"
361
+ tabindex="-1"
362
+ :disabled="!canDecrease"
363
+ :aria-label="t('inputNumber.decrease')"
364
+ @pointerdown.prevent
365
+ @click="applyStep(-1)"
366
+ >
367
+ <RsIcon name="chevron-down" :size="12" />
368
+ </button>
369
+ </div>
370
+ </div>
371
+ </div>
372
+
373
+ <p v-if="displayMessage" :id="errorId" class="rs-input-number-field__error" role="alert">
374
+ {{ displayMessage }}
375
+ </p>
376
+ <span v-if="hint" class="rs-field__hint">{{ hint }}</span>
377
+ </div>
378
+ </template>
379
+
380
+ <style scoped>
381
+ .rs-input-number-field {
382
+ width: 100%;
383
+ }
384
+ .rs-field--label-left .rs-input-number-field {
385
+ grid-column: 2;
386
+ min-width: 0;
387
+ }
388
+ .rs-field--label-left .rs-input-number-field__error {
389
+ grid-column: 2;
390
+ }
391
+ .rs-field--label-left .rs-field__label {
392
+ display: flex;
393
+ align-items: center;
394
+ min-height: var(--rs-control-height-md);
395
+ }
396
+ .rs-input-number-field--ssm.rs-field--label-left .rs-field__label {
397
+ min-height: var(--rs-control-height-ssm);
398
+ }
399
+ .rs-input-number-field--sm.rs-field--label-left .rs-field__label {
400
+ min-height: var(--rs-control-height-sm);
401
+ }
402
+ .rs-input-number-field--lg.rs-field--label-left .rs-field__label {
403
+ min-height: var(--rs-control-height-lg);
404
+ }
405
+
406
+ .rs-input-number {
407
+ display: flex;
408
+ align-items: stretch;
409
+ width: 100%;
410
+ min-height: var(--rs-control-height-md);
411
+ border-radius: var(--rs-input-radius, var(--rs-radius-sm));
412
+ border: 1px solid var(--rs-input-border, var(--rs-border));
413
+ background: var(--rs-input-bg);
414
+ box-shadow: var(--rs-input-shadow, none);
415
+ transition:
416
+ border-color var(--rs-transition-fast),
417
+ box-shadow var(--rs-transition-fast),
418
+ background var(--rs-transition-fast);
419
+ overflow: hidden;
420
+ }
421
+ .rs-input-number--ssm {
422
+ min-height: var(--rs-control-height-ssm);
423
+ }
424
+ .rs-input-number--sm {
425
+ min-height: var(--rs-control-height-sm);
426
+ }
427
+ .rs-input-number--lg {
428
+ min-height: var(--rs-control-height-lg);
429
+ }
430
+ .rs-input-number:hover:not(.rs-input-number--disabled):not(.rs-input-number--readonly):not(
431
+ :focus-within
432
+ ) {
433
+ border-color: var(--rs-input-border-hover, var(--rs-border));
434
+ }
435
+ .rs-input-number:focus-within {
436
+ border-color: var(--rs-focus-border, var(--rs-primary));
437
+ box-shadow:
438
+ var(--rs-input-shadow, none),
439
+ 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
440
+ }
441
+ .rs-input-number--invalid {
442
+ border-color: var(--rs-danger);
443
+ }
444
+ .rs-input-number--invalid:focus-within {
445
+ border-color: var(--rs-danger);
446
+ box-shadow:
447
+ var(--rs-input-shadow, none),
448
+ 0 0 0 var(--rs-focus-ring-width, 2px) color-mix(in srgb, var(--rs-danger) 14%, transparent);
449
+ }
450
+ .rs-input-number--disabled {
451
+ opacity: 0.55;
452
+ cursor: not-allowed;
453
+ background: var(--rs-surface-muted, var(--rs-input-bg));
454
+ }
455
+
456
+ .rs-input-number__control {
457
+ flex: 1;
458
+ min-width: 0;
459
+ border: 0;
460
+ outline: none;
461
+ background: transparent;
462
+ color: var(--rs-fg);
463
+ font: inherit;
464
+ padding: 0 10px;
465
+ height: 100%;
466
+ appearance: textfield;
467
+ -moz-appearance: textfield;
468
+ }
469
+ .rs-input-number__control--ssm {
470
+ padding: 0 8px;
471
+ font-size: 12px;
472
+ }
473
+ .rs-input-number__control--sm {
474
+ padding: 0 8px;
475
+ font-size: 12px;
476
+ }
477
+ .rs-input-number__control--lg {
478
+ padding: 0 12px;
479
+ font-size: 15px;
480
+ }
481
+ .rs-input-number__control:disabled {
482
+ cursor: not-allowed;
483
+ }
484
+ .rs-input-number__control::-webkit-outer-spin-button,
485
+ .rs-input-number__control::-webkit-inner-spin-button {
486
+ -webkit-appearance: none;
487
+ margin: 0;
488
+ }
489
+
490
+ .rs-input-number__handlers {
491
+ display: flex;
492
+ flex-direction: column;
493
+ flex-shrink: 0;
494
+ width: 22px;
495
+ border-left: 1px solid var(--rs-input-border, var(--rs-border));
496
+ }
497
+ .rs-input-number--ssm .rs-input-number__handlers,
498
+ .rs-input-number--sm .rs-input-number__handlers {
499
+ width: 18px;
500
+ }
501
+ .rs-input-number__handler {
502
+ flex: 1;
503
+ display: inline-flex;
504
+ align-items: center;
505
+ justify-content: center;
506
+ border: 0;
507
+ margin: 0;
508
+ padding: 0;
509
+ background: transparent;
510
+ color: var(--rs-fg-muted);
511
+ cursor: pointer;
512
+ line-height: 1;
513
+ }
514
+ .rs-input-number__handler:hover:not(:disabled) {
515
+ color: var(--rs-fg);
516
+ background: var(--rs-surface-muted, transparent);
517
+ }
518
+ .rs-input-number__handler:disabled {
519
+ opacity: 0.35;
520
+ cursor: not-allowed;
521
+ }
522
+ .rs-input-number__handler--up {
523
+ border-bottom: 1px solid var(--rs-input-border, var(--rs-border));
524
+ }
525
+
526
+ .rs-input-number-field__error {
527
+ margin: 4px 0 0;
528
+ font-size: 12px;
529
+ color: var(--rs-danger);
530
+ line-height: 1.4;
531
+ }
532
+ </style>
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ forId?: string
5
+ required?: boolean
6
+ hint?: string
7
+ disabled?: boolean
8
+ }>(),
9
+ {
10
+ required: false,
11
+ disabled: false,
12
+ },
13
+ )
14
+ </script>
15
+
16
+ <template>
17
+ <label
18
+ class="rs-label"
19
+ :class="{ 'rs-label--disabled': disabled }"
20
+ :for="forId"
21
+ >
22
+ <span class="rs-label__text">
23
+ <slot />
24
+ <span v-if="required" class="rs-label__required" aria-hidden="true">*</span>
25
+ </span>
26
+ <span v-if="hint" class="rs-label__hint">{{ hint }}</span>
27
+ </label>
28
+ </template>
29
+
30
+ <style scoped>
31
+ .rs-label {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 0.25rem;
35
+ font-size: var(--rs-font-size-sm);
36
+ color: var(--rs-text);
37
+ }
38
+ .rs-label--disabled {
39
+ opacity: 0.5;
40
+ cursor: not-allowed;
41
+ }
42
+ .rs-label__text {
43
+ font-weight: 500;
44
+ }
45
+ .rs-label__required {
46
+ margin-left: 0.125rem;
47
+ color: var(--rs-danger);
48
+ }
49
+ .rs-label__hint {
50
+ font-size: var(--rs-font-size-xs);
51
+ font-weight: 400;
52
+ color: var(--rs-muted);
53
+ }
54
+ </style>
@@ -0,0 +1,106 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { Primitive } from './reka'
4
+
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ href: string
8
+ target?: '_self' | '_blank' | '_parent' | '_top' | (string & {})
9
+ rel?: string
10
+ disabled?: boolean
11
+ underline?: 'always' | 'hover' | 'none'
12
+ }>(),
13
+ {
14
+ target: '_self',
15
+ rel: undefined,
16
+ disabled: false,
17
+ underline: 'hover',
18
+ },
19
+ )
20
+
21
+ const mergedRel = computed(() => {
22
+ const relParts = (props.rel ?? '')
23
+ .split(/\s+/)
24
+ .map((item) => item.trim())
25
+ .filter(Boolean)
26
+
27
+ if (props.target === '_blank') {
28
+ if (!relParts.includes('noopener')) relParts.push('noopener')
29
+ if (!relParts.includes('noreferrer')) relParts.push('noreferrer')
30
+ }
31
+
32
+ return relParts.length > 0 ? relParts.join(' ') : undefined
33
+ })
34
+
35
+ function onClick(event: MouseEvent) {
36
+ if (!props.disabled) return
37
+ event.preventDefault()
38
+ event.stopPropagation()
39
+ }
40
+ </script>
41
+
42
+ <template>
43
+ <Primitive
44
+ as="a"
45
+ :href="disabled ? undefined : href"
46
+ :target="disabled ? undefined : target"
47
+ :rel="disabled ? undefined : mergedRel"
48
+ class="rs-link"
49
+ :class="{
50
+ 'rs-link--disabled': disabled,
51
+ [`rs-link--underline-${underline}`]: true,
52
+ }"
53
+ :aria-disabled="disabled ? 'true' : undefined"
54
+ :tabindex="disabled ? -1 : undefined"
55
+ @click="onClick"
56
+ >
57
+ <slot />
58
+ </Primitive>
59
+ </template>
60
+
61
+ <style>
62
+ .rs-link {
63
+ color: var(--rs-primary);
64
+ font-size: var(--rs-font-size-sm);
65
+ font-weight: 500;
66
+ line-height: var(--rs-line-height-tight);
67
+ text-underline-offset: 0.18em;
68
+ text-decoration-thickness: 1px;
69
+ transition:
70
+ color var(--rs-transition-fast),
71
+ text-decoration-color var(--rs-transition-fast);
72
+ }
73
+ .rs-link:visited {
74
+ color: var(--rs-primary);
75
+ }
76
+ .rs-link:hover {
77
+ color: var(--rs-primary-hover);
78
+ }
79
+ .rs-link:active {
80
+ color: var(--rs-primary-hover);
81
+ }
82
+ .rs-link:focus-visible {
83
+ outline: none;
84
+ border-radius: var(--rs-radius-xs);
85
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
86
+ }
87
+ .rs-link--underline-always {
88
+ text-decoration: underline;
89
+ }
90
+ .rs-link--underline-hover {
91
+ text-decoration: none;
92
+ }
93
+ .rs-link--underline-hover:hover,
94
+ .rs-link--underline-hover:focus-visible {
95
+ text-decoration: underline;
96
+ }
97
+ .rs-link--underline-none {
98
+ text-decoration: none;
99
+ }
100
+ .rs-link--disabled {
101
+ color: var(--rs-muted);
102
+ cursor: not-allowed;
103
+ text-decoration: none;
104
+ pointer-events: auto;
105
+ }
106
+ </style>