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,393 @@
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, ref, useSlots, watch } from 'vue'
3
+ import type { RsComponentSize, RsRadius } from '../theme/types'
4
+ import { RS_COMPONENT_SIZE_ICON_PX } from '../theme/types'
5
+ import { useResolvedRsComponentSize } from './resolve-size'
6
+ import { rsRadiusCss, useResolvedRsRadius } from './resolve-radius'
7
+ import RsIcon from './RsIcon.vue'
8
+
9
+ const props = withDefaults(
10
+ defineProps<{
11
+ variant?: 'primary' | 'default' | 'secondary' | 'ghost' | 'danger' | 'link'
12
+ size?: RsComponentSize
13
+ /** 圆角档位;默认 full(胶囊)。直角 UI 传 `none`。 */
14
+ radius?: RsRadius
15
+ type?: 'button' | 'submit' | 'reset'
16
+ disabled?: boolean
17
+ loading?: boolean
18
+ /** 前缀图标(Lucide kebab-case 名称) */
19
+ icon?: string
20
+ iconSize?: number
21
+ /** 仅显示图标,文字通过 tooltip / slot 悬浮展示 */
22
+ iconOnly?: boolean
23
+ /** 悬浮提示文案 */
24
+ tooltip?: string
25
+ /** icon-only 时无障碍标签(与 tooltip 二选一,避免与外部 RsTooltip 重复) */
26
+ ariaLabel?: string
27
+ /** 默认收起文字,悬浮时展开(需配合 slot 文案) */
28
+ revealLabel?: boolean
29
+ }>(),
30
+ {
31
+ variant: 'primary',
32
+ type: 'button',
33
+ disabled: false,
34
+ loading: false,
35
+ iconOnly: false,
36
+ revealLabel: false,
37
+ },
38
+ )
39
+
40
+ const slots = useSlots()
41
+ const btnRef = ref<HTMLButtonElement | null>(null)
42
+ const resolvedSize = useResolvedRsComponentSize(() => props.size)
43
+ const resolvedRadius = useResolvedRsRadius(() => props.radius, 'full')
44
+ const resolvedIconSize = computed(
45
+ () => props.iconSize ?? RS_COMPONENT_SIZE_ICON_PX[resolvedSize.value],
46
+ )
47
+ const rootStyle = computed(() => ({
48
+ '--rs-btn-radius': rsRadiusCss(resolvedRadius.value),
49
+ }))
50
+
51
+ const hasLabel = computed(() => Boolean(slots.default))
52
+
53
+ const resolvedVariant = computed(() => (props.variant === 'secondary' ? 'default' : props.variant))
54
+
55
+ /** 仅在有 tooltip 文案或 icon-only 默认插槽文案时展示内置悬浮层 */
56
+ const showFloatingText = computed(
57
+ () => Boolean(props.tooltip) || (props.iconOnly && hasLabel.value),
58
+ )
59
+
60
+ const ariaLabel = computed(() => {
61
+ if (!props.iconOnly) return undefined
62
+ return props.ariaLabel || props.tooltip || undefined
63
+ })
64
+
65
+ /** tooltip 水平对齐方式:center / left / right */
66
+ const tooltipAlign = ref<'center' | 'left' | 'right'>('center')
67
+
68
+ function updateTooltipAlign(): void {
69
+ const el = btnRef.value
70
+ if (!el) return
71
+ const rect = el.getBoundingClientRect()
72
+ const mid = rect.left + rect.width / 2
73
+ const vw = window.innerWidth
74
+ if (mid > vw * 0.72) {
75
+ tooltipAlign.value = 'right'
76
+ } else if (mid < vw * 0.28) {
77
+ tooltipAlign.value = 'left'
78
+ } else {
79
+ tooltipAlign.value = 'center'
80
+ }
81
+ }
82
+
83
+ watch(
84
+ () => props.loading,
85
+ async (loading) => {
86
+ const el = btnRef.value
87
+ if (!el) return
88
+ if (loading) {
89
+ await nextTick()
90
+ const w = el.offsetWidth
91
+ if (w > 0) {
92
+ el.style.minWidth = `${w}px`
93
+ }
94
+ } else {
95
+ el.style.minWidth = ''
96
+ }
97
+ },
98
+ )
99
+ </script>
100
+
101
+ <template>
102
+ <button
103
+ ref="btnRef"
104
+ :type="type"
105
+ :disabled="disabled"
106
+ class="rs-btn"
107
+ :class="{
108
+ [`rs-btn--${resolvedVariant}`]: true,
109
+ [`rs-btn--${resolvedSize}`]: true,
110
+ 'rs-btn--icon-only': iconOnly,
111
+ 'rs-btn--reveal-label': revealLabel && icon && hasLabel,
112
+ 'rs-btn--loading': loading,
113
+ }"
114
+ :style="rootStyle"
115
+ :aria-busy="loading || undefined"
116
+ :aria-disabled="disabled || loading || undefined"
117
+ :aria-label="ariaLabel"
118
+ @mouseenter="updateTooltipAlign"
119
+ >
120
+ <span v-if="loading" class="rs-btn__spinner" aria-hidden="true">
121
+ <span class="rs-btn__spinner-ring" />
122
+ </span>
123
+ <RsIcon
124
+ v-if="icon && !loading"
125
+ class="rs-btn__icon"
126
+ :name="icon"
127
+ :size="resolvedIconSize"
128
+ :label="iconOnly && tooltip ? tooltip : undefined"
129
+ />
130
+ <span
131
+ v-if="hasLabel && !iconOnly"
132
+ class="rs-btn__label"
133
+ :class="{ 'rs-btn__label--reveal': revealLabel && icon }"
134
+ >
135
+ <slot />
136
+ </span>
137
+ <span
138
+ v-if="showFloatingText"
139
+ class="rs-btn__tooltip"
140
+ :class="`rs-btn__tooltip--${tooltipAlign}`"
141
+ role="tooltip"
142
+ >
143
+ <template v-if="tooltip">{{ tooltip }}</template>
144
+ <slot v-else-if="iconOnly" />
145
+ </span>
146
+ </button>
147
+ </template>
148
+
149
+ <style scoped>
150
+ .rs-btn {
151
+ position: relative;
152
+ display: inline-flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ gap: var(--rs-space-sm);
156
+ min-height: var(--rs-control-height-md);
157
+ padding: 0 var(--rs-space-lg);
158
+ border-radius: var(--rs-btn-radius, var(--rs-radius-full));
159
+ border: 1px solid transparent;
160
+ font-size: var(--rs-font-size-sm);
161
+ font-weight: 500;
162
+ letter-spacing: 0.01em;
163
+ line-height: var(--rs-line-height-tight);
164
+ white-space: nowrap;
165
+ cursor: pointer;
166
+ transition:
167
+ background var(--rs-transition-fast),
168
+ border-color var(--rs-transition-fast),
169
+ color var(--rs-transition-fast),
170
+ box-shadow var(--rs-transition-fast),
171
+ padding var(--rs-transition-normal),
172
+ gap var(--rs-transition-normal);
173
+ }
174
+ .rs-btn--ssm {
175
+ min-height: var(--rs-control-height-ssm);
176
+ padding: 0 var(--rs-space-xs);
177
+ font-size: var(--rs-font-size-xs);
178
+ gap: var(--rs-space-xs);
179
+ }
180
+ .rs-btn--sm {
181
+ min-height: var(--rs-control-height-sm);
182
+ padding: 0 var(--rs-space-sm);
183
+ font-size: var(--rs-font-size-xs);
184
+ gap: var(--rs-space-xs);
185
+ }
186
+ .rs-btn--lg {
187
+ min-height: var(--rs-control-height-lg);
188
+ padding: 0 var(--rs-space-xl);
189
+ font-size: var(--rs-font-size-base);
190
+ }
191
+ .rs-btn:disabled {
192
+ opacity: 0.38;
193
+ cursor: not-allowed;
194
+ }
195
+ .rs-btn--loading {
196
+ pointer-events: none;
197
+ cursor: wait;
198
+ transition: none;
199
+ }
200
+ /* loading 时不使用 disabled,避免部分浏览器暂停子元素 CSS 动画 */
201
+ .rs-btn--loading:disabled {
202
+ opacity: 1;
203
+ cursor: wait;
204
+ }
205
+ .rs-btn--loading.rs-btn--default {
206
+ color: var(--rs-text);
207
+ border-color: var(--rs-border);
208
+ background: var(--rs-surface);
209
+ }
210
+ .rs-btn--loading.rs-btn--ghost {
211
+ color: var(--rs-text);
212
+ background: var(--rs-surface-hover);
213
+ }
214
+ .rs-btn:focus-visible {
215
+ outline: none;
216
+ box-shadow: 0 0 0 var(--rs-focus-ring-width, 2px) var(--rs-focus-ring);
217
+ }
218
+ .rs-btn--icon-only {
219
+ padding: 0;
220
+ min-width: var(--rs-control-height-md);
221
+ }
222
+ .rs-btn--icon-only.rs-btn--ssm {
223
+ min-width: var(--rs-control-height-ssm);
224
+ }
225
+ .rs-btn--icon-only.rs-btn--sm {
226
+ min-width: var(--rs-control-height-sm);
227
+ }
228
+ .rs-btn--icon-only.rs-btn--lg {
229
+ min-width: var(--rs-control-height-lg);
230
+ }
231
+ .rs-btn--primary {
232
+ background: var(--rs-primary);
233
+ color: var(--rs-primary-foreground);
234
+ box-shadow: var(--rs-shadow-sm);
235
+ }
236
+ .rs-btn--primary:hover:not(:disabled) {
237
+ background: var(--rs-primary-hover);
238
+ box-shadow: var(--rs-shadow);
239
+ }
240
+ .rs-btn--primary:active:not(:disabled) {
241
+ box-shadow: none;
242
+ }
243
+ .rs-btn--default {
244
+ background: var(--rs-surface);
245
+ border-color: var(--rs-border);
246
+ color: var(--rs-text);
247
+ box-shadow: var(--rs-shadow-sm);
248
+ }
249
+ .rs-btn--default:hover:not(:disabled) {
250
+ color: var(--rs-primary);
251
+ border-color: var(--rs-primary);
252
+ }
253
+ .rs-btn--default:active:not(:disabled) {
254
+ color: var(--rs-primary-hover);
255
+ border-color: var(--rs-primary-hover);
256
+ }
257
+ .rs-btn--ghost {
258
+ background: transparent;
259
+ border-color: var(--rs-border);
260
+ color: var(--rs-text);
261
+ }
262
+ .rs-btn--ghost:hover:not(:disabled) {
263
+ background: var(--rs-surface-hover);
264
+ border-color: var(--rs-border);
265
+ }
266
+ .rs-btn--danger {
267
+ background: var(--rs-danger-container);
268
+ border-color: color-mix(in srgb, var(--rs-danger) 30%, transparent);
269
+ color: var(--rs-on-danger-container);
270
+ }
271
+ .rs-btn--danger:hover:not(:disabled) {
272
+ border-color: var(--rs-danger);
273
+ background: color-mix(in srgb, var(--rs-danger) 20%, var(--rs-danger-container));
274
+ }
275
+ .rs-btn--link {
276
+ min-height: auto;
277
+ padding: 0;
278
+ border-color: transparent;
279
+ border-radius: 0;
280
+ background: transparent;
281
+ color: var(--rs-primary);
282
+ box-shadow: none;
283
+ }
284
+ .rs-btn--link:hover:not(:disabled) {
285
+ color: var(--rs-primary-hover);
286
+ text-decoration: underline;
287
+ }
288
+ .rs-btn__spinner {
289
+ display: inline-flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ width: 1rem;
293
+ height: 1rem;
294
+ flex-shrink: 0;
295
+ pointer-events: none;
296
+ }
297
+ .rs-btn__spinner-ring {
298
+ display: block;
299
+ width: 100%;
300
+ height: 100%;
301
+ border: 2px solid color-mix(in srgb, var(--rs-primary) 28%, transparent);
302
+ border-top-color: var(--rs-primary);
303
+ border-radius: var(--rs-radius-full);
304
+ transform-origin: center center;
305
+ animation: rs-spin 0.75s linear infinite;
306
+ }
307
+ .rs-btn--default .rs-btn__spinner-ring,
308
+ .rs-btn--ghost .rs-btn__spinner-ring {
309
+ border-color: color-mix(in srgb, var(--rs-primary) 28%, transparent);
310
+ border-top-color: var(--rs-primary);
311
+ }
312
+ .rs-btn--primary .rs-btn__spinner-ring {
313
+ border-color: color-mix(in srgb, currentcolor 35%, transparent);
314
+ border-top-color: currentcolor;
315
+ }
316
+ .rs-btn--danger .rs-btn__spinner-ring {
317
+ border-color: color-mix(in srgb, var(--rs-danger) 28%, transparent);
318
+ border-top-color: var(--rs-danger);
319
+ }
320
+ .rs-btn__icon {
321
+ flex-shrink: 0;
322
+ }
323
+ .rs-btn__label {
324
+ white-space: nowrap;
325
+ }
326
+ .rs-btn__label--reveal {
327
+ display: inline-block;
328
+ max-width: 0;
329
+ opacity: 0;
330
+ overflow: hidden;
331
+ white-space: nowrap;
332
+ vertical-align: bottom;
333
+ transition:
334
+ max-width 0.2s ease,
335
+ opacity 0.15s ease;
336
+ }
337
+ .rs-btn--reveal-label:hover:not(:disabled) .rs-btn__label--reveal,
338
+ .rs-btn--reveal-label:focus-visible .rs-btn__label--reveal {
339
+ max-width: 12rem;
340
+ opacity: 1;
341
+ }
342
+ .rs-btn__tooltip {
343
+ position: absolute;
344
+ left: 50%;
345
+ top: calc(100% + 0.375rem);
346
+ transform: translateX(-50%) translateY(-2px);
347
+ padding: 0.25rem 0.5rem;
348
+ border-radius: var(--rs-radius-sm);
349
+ border: 1px solid var(--rs-border);
350
+ background: var(--rs-surface-elevated);
351
+ color: var(--rs-text);
352
+ font-size: var(--rs-font-size-xs);
353
+ font-weight: 500;
354
+ line-height: 1.25rem;
355
+ white-space: nowrap;
356
+ box-shadow: var(--rs-shadow-sm);
357
+ opacity: 0;
358
+ pointer-events: none;
359
+ transition:
360
+ opacity 0.15s ease,
361
+ transform 0.15s ease;
362
+ z-index: var(--rs-z-tooltip);
363
+ }
364
+ /* 靠左边的按钮:tooltip 左对齐 */
365
+ .rs-btn__tooltip--left {
366
+ left: 0;
367
+ transform: translateY(-2px);
368
+ }
369
+ /* 靠右边的按钮:tooltip 右对齐 */
370
+ .rs-btn__tooltip--right {
371
+ left: auto;
372
+ right: 0;
373
+ transform: translateY(-2px);
374
+ }
375
+ .rs-btn:hover:not(:disabled) .rs-btn__tooltip,
376
+ .rs-btn:focus-visible .rs-btn__tooltip {
377
+ opacity: 1;
378
+ transform: translateX(-50%) translateY(0);
379
+ }
380
+ .rs-btn:hover:not(:disabled) .rs-btn__tooltip--left,
381
+ .rs-btn:focus-visible .rs-btn__tooltip--left {
382
+ transform: translateY(0);
383
+ }
384
+ .rs-btn:hover:not(:disabled) .rs-btn__tooltip--right,
385
+ .rs-btn:focus-visible .rs-btn__tooltip--right {
386
+ transform: translateY(0);
387
+ }
388
+ @keyframes rs-spin {
389
+ to {
390
+ transform: rotate(360deg);
391
+ }
392
+ }
393
+ </style>
@@ -0,0 +1,284 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import RsIcon from './RsIcon.vue'
4
+ import { useRsI18n } from '../composables/useRsI18n'
5
+ import {
6
+ buildCalendarGrid,
7
+ formatDateParts,
8
+ isDateBetweenParsed,
9
+ isDateWithinBounds,
10
+ isSameDate,
11
+ type RsCalendarCell,
12
+ type RsParsedDate,
13
+ } from './date-picker-utils'
14
+
15
+ const viewYear = defineModel<number>('viewYear', { required: true })
16
+ const viewMonth = defineModel<number>('viewMonth', { required: true })
17
+
18
+ const props = withDefaults(
19
+ defineProps<{
20
+ selected?: RsParsedDate | null
21
+ rangeStart?: RsParsedDate | null
22
+ rangeEnd?: RsParsedDate | null
23
+ minDate?: string
24
+ maxDate?: string
25
+ disabledDates?: string[]
26
+ }>(),
27
+ {
28
+ selected: null,
29
+ rangeStart: null,
30
+ rangeEnd: null,
31
+ disabledDates: () => [],
32
+ },
33
+ )
34
+
35
+ const emit = defineEmits<{
36
+ select: [date: RsParsedDate]
37
+ }>()
38
+
39
+ const { t, locale } = useRsI18n()
40
+
41
+ const weekdayLabels = computed(() => {
42
+ const formatter = new Intl.DateTimeFormat(locale.value, { weekday: 'short' })
43
+ const monday = new Date(2024, 0, 1)
44
+ return Array.from({ length: 7 }, (_, index) => {
45
+ const date = new Date(monday)
46
+ date.setDate(monday.getDate() + index)
47
+ return formatter.format(date)
48
+ })
49
+ })
50
+
51
+ const monthLabel = computed(() =>
52
+ new Intl.DateTimeFormat(locale.value, {
53
+ year: 'numeric',
54
+ month: 'long',
55
+ }).format(new Date(viewYear.value, viewMonth.value - 1, 1)),
56
+ )
57
+
58
+ const cells = computed(() => buildCalendarGrid(viewYear.value, viewMonth.value))
59
+
60
+ const weeks = computed(() => {
61
+ const rows: RsCalendarCell[][] = []
62
+ for (let index = 0; index < cells.value.length; index += 7) {
63
+ rows.push(cells.value.slice(index, index + 7))
64
+ }
65
+ return rows
66
+ })
67
+
68
+ function goPrevMonth(): void {
69
+ if (viewMonth.value === 1) {
70
+ viewYear.value -= 1
71
+ viewMonth.value = 12
72
+ return
73
+ }
74
+ viewMonth.value -= 1
75
+ }
76
+
77
+ function goNextMonth(): void {
78
+ if (viewMonth.value === 12) {
79
+ viewYear.value += 1
80
+ viewMonth.value = 1
81
+ return
82
+ }
83
+ viewMonth.value += 1
84
+ }
85
+
86
+ function isSelected(cell: RsCalendarCell): boolean {
87
+ return props.selected ? isSameDate(props.selected, cell) : false
88
+ }
89
+
90
+ function isRangeStart(cell: RsCalendarCell): boolean {
91
+ return props.rangeStart ? isSameDate(props.rangeStart, cell) : false
92
+ }
93
+
94
+ function isRangeEnd(cell: RsCalendarCell): boolean {
95
+ return props.rangeEnd ? isSameDate(props.rangeEnd, cell) : false
96
+ }
97
+
98
+ function isInRange(cell: RsCalendarCell): boolean {
99
+ if (!props.rangeStart || !props.rangeEnd) return false
100
+ return isDateBetweenParsed(cell, props.rangeStart, props.rangeEnd)
101
+ }
102
+
103
+ function isDisabled(cell: RsCalendarCell): boolean {
104
+ const iso = formatDateParts(cell)
105
+ if (props.disabledDates.includes(iso)) return true
106
+ return !isDateWithinBounds(cell.year, cell.month, cell.day, {
107
+ minDate: props.minDate,
108
+ maxDate: props.maxDate,
109
+ })
110
+ }
111
+
112
+ function cellClass(cell: RsCalendarCell): Record<string, boolean> {
113
+ const endpoint = isRangeStart(cell) || isRangeEnd(cell)
114
+ const inRange = isInRange(cell)
115
+ const selected = isSelected(cell)
116
+
117
+ return {
118
+ 'rs-calendar-grid__cell--outside': !cell.inCurrentMonth,
119
+ 'rs-calendar-grid__cell--selected': selected,
120
+ 'rs-calendar-grid__cell--range-start': isRangeStart(cell),
121
+ 'rs-calendar-grid__cell--range-end': isRangeEnd(cell),
122
+ 'rs-calendar-grid__cell--in-range': inRange && !endpoint,
123
+ 'rs-calendar-grid__cell--endpoint': endpoint || selected,
124
+ }
125
+ }
126
+
127
+ function selectDate(cell: RsCalendarCell): void {
128
+ if (isDisabled(cell)) return
129
+ emit('select', { year: cell.year, month: cell.month, day: cell.day })
130
+ }
131
+ </script>
132
+
133
+ <template>
134
+ <div class="rs-calendar-grid">
135
+ <div class="rs-calendar-grid__header">
136
+ <button
137
+ type="button"
138
+ class="rs-calendar-grid__nav"
139
+ :aria-label="t('datePicker.prevMonth')"
140
+ @click="goPrevMonth"
141
+ >
142
+ <RsIcon name="chevron-left" :size="16" />
143
+ </button>
144
+ <span class="rs-calendar-grid__title">{{ monthLabel }}</span>
145
+ <button
146
+ type="button"
147
+ class="rs-calendar-grid__nav"
148
+ :aria-label="t('datePicker.nextMonth')"
149
+ @click="goNextMonth"
150
+ >
151
+ <RsIcon name="chevron-right" :size="16" />
152
+ </button>
153
+ </div>
154
+
155
+ <table class="rs-calendar-grid__table">
156
+ <thead>
157
+ <tr>
158
+ <th
159
+ v-for="(label, index) in weekdayLabels"
160
+ :key="index"
161
+ scope="col"
162
+ class="rs-calendar-grid__weekday"
163
+ >
164
+ {{ label }}
165
+ </th>
166
+ </tr>
167
+ </thead>
168
+ <tbody>
169
+ <tr v-for="(week, weekIndex) in weeks" :key="weekIndex">
170
+ <td
171
+ v-for="cell in week"
172
+ :key="formatDateParts(cell)"
173
+ class="rs-calendar-grid__day"
174
+ >
175
+ <button
176
+ type="button"
177
+ class="rs-calendar-grid__cell"
178
+ :class="cellClass(cell)"
179
+ :disabled="isDisabled(cell)"
180
+ @click="selectDate(cell)"
181
+ >
182
+ {{ cell.day }}
183
+ </button>
184
+ </td>
185
+ </tr>
186
+ </tbody>
187
+ </table>
188
+ </div>
189
+ </template>
190
+
191
+ <style scoped>
192
+ .rs-calendar-grid {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: var(--rs-space-sm);
196
+ }
197
+ .rs-calendar-grid__header {
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: space-between;
201
+ gap: var(--rs-space-sm);
202
+ }
203
+ .rs-calendar-grid__title {
204
+ flex: 1;
205
+ text-align: center;
206
+ font-size: var(--rs-font-size-sm);
207
+ font-weight: 600;
208
+ color: var(--rs-text);
209
+ }
210
+ .rs-calendar-grid__nav {
211
+ display: inline-flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ width: 1.75rem;
215
+ height: 1.75rem;
216
+ border: 0;
217
+ border-radius: var(--rs-radius-sm);
218
+ background: transparent;
219
+ color: var(--rs-muted);
220
+ cursor: pointer;
221
+ transition: background var(--rs-transition-fast), color var(--rs-transition-fast);
222
+ }
223
+ .rs-calendar-grid__nav:hover {
224
+ background: var(--rs-surface-hover);
225
+ color: var(--rs-text);
226
+ }
227
+ .rs-calendar-grid__table {
228
+ width: 100%;
229
+ border-collapse: separate;
230
+ border-spacing: 2px;
231
+ table-layout: fixed;
232
+ }
233
+ .rs-calendar-grid__weekday {
234
+ padding: 0.125rem 0;
235
+ text-align: center;
236
+ font-size: 0.6875rem;
237
+ font-weight: 500;
238
+ color: var(--rs-muted);
239
+ }
240
+ .rs-calendar-grid__day {
241
+ padding: 0;
242
+ text-align: center;
243
+ vertical-align: middle;
244
+ }
245
+ .rs-calendar-grid__cell {
246
+ width: 100%;
247
+ height: 2rem;
248
+ border: 0;
249
+ border-radius: var(--rs-radius-sm);
250
+ background: transparent;
251
+ color: var(--rs-text);
252
+ font-size: var(--rs-font-size-sm);
253
+ cursor: pointer;
254
+ transition:
255
+ background var(--rs-transition-fast),
256
+ color var(--rs-transition-fast);
257
+ }
258
+ .rs-calendar-grid__cell:hover:not(:disabled) {
259
+ background: var(--rs-item-hover);
260
+ }
261
+ .rs-calendar-grid__cell--outside {
262
+ color: var(--rs-placeholder);
263
+ }
264
+ .rs-calendar-grid__cell--endpoint,
265
+ .rs-calendar-grid__cell--selected {
266
+ background: var(--rs-primary);
267
+ color: var(--rs-primary-foreground);
268
+ }
269
+ .rs-calendar-grid__cell--in-range {
270
+ border-radius: 0;
271
+ background: color-mix(in srgb, var(--rs-primary) 12%, transparent);
272
+ color: var(--rs-primary);
273
+ }
274
+ .rs-calendar-grid__cell--range-start:not(.rs-calendar-grid__cell--range-end) {
275
+ border-radius: var(--rs-radius-sm) 0 0 var(--rs-radius-sm);
276
+ }
277
+ .rs-calendar-grid__cell--range-end:not(.rs-calendar-grid__cell--range-start) {
278
+ border-radius: 0 var(--rs-radius-sm) var(--rs-radius-sm) 0;
279
+ }
280
+ .rs-calendar-grid__cell:disabled {
281
+ opacity: 0.38;
282
+ cursor: not-allowed;
283
+ }
284
+ </style>