niuma-ui 1.0.0 → 1.0.2

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 (159) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/NOTICE +2 -2
  3. package/package.json +13 -5
  4. package/src/__tests__/RsButton.spec.ts +47 -0
  5. package/src/__tests__/RsCard.spec.ts +160 -1
  6. package/src/__tests__/RsConfirmDialog.spec.ts +127 -0
  7. package/src/__tests__/RsContextMenu.spec.ts +12 -0
  8. package/src/__tests__/RsDateTimePicker.spec.ts +11 -0
  9. package/src/__tests__/RsDescriptions.spec.ts +50 -0
  10. package/src/__tests__/RsDialog.spec.ts +117 -0
  11. package/src/__tests__/RsDrawer.spec.ts +96 -3
  12. package/src/__tests__/RsDynamicTags.spec.ts +52 -0
  13. package/src/__tests__/RsForm.spec.ts +245 -1
  14. package/src/__tests__/RsInput.spec.ts +15 -1
  15. package/src/__tests__/RsMenu.spec.ts +406 -14
  16. package/src/__tests__/RsPagination.spec.ts +67 -2
  17. package/src/__tests__/RsSelect.spec.ts +42 -1
  18. package/src/__tests__/RsSwitch.spec.ts +44 -0
  19. package/src/__tests__/RsTable.a11y.spec.ts +83 -0
  20. package/src/__tests__/RsTable.dom-baseline.spec.ts +87 -0
  21. package/src/__tests__/RsTable.perf.spec.ts +23 -4
  22. package/src/__tests__/RsTable.spec.ts +63 -8
  23. package/src/__tests__/RsTable.ssr.spec.ts +71 -0
  24. package/src/__tests__/RsTable.summary.spec.ts +45 -0
  25. package/src/__tests__/RsTabs.spec.ts +102 -4
  26. package/src/__tests__/RsTerminal.spec.ts +1 -1
  27. package/src/__tests__/RsTimePicker.spec.ts +11 -0
  28. package/src/__tests__/RsUpload.spec.ts +62 -6
  29. package/src/__tests__/createRsDialog.spec.ts +145 -0
  30. package/src/__tests__/form-rules.spec.ts +76 -0
  31. package/src/__tests__/perf-budgets.ts +32 -0
  32. package/src/__tests__/rs-table-api.spec.ts +206 -0
  33. package/src/__tests__/rs-table-compat-matrix.spec.ts +108 -0
  34. package/src/__tests__/rs-table-echarts-adapter.spec.ts +43 -0
  35. package/src/__tests__/rs-table-grid-nav.spec.ts +63 -0
  36. package/src/__tests__/rs-table-isolation.spec.ts +158 -0
  37. package/src/__tests__/split-pane-utils.spec.ts +21 -0
  38. package/src/__tests__/table-chart-utils.spec.ts +88 -0
  39. package/src/__tests__/table-summary-utils.spec.ts +62 -0
  40. package/src/__tests__/table-utils.spec.ts +124 -0
  41. package/src/__tests__/theme.spec.ts +25 -0
  42. package/src/__tests__/useRsTableColumnResize.spec.ts +25 -0
  43. package/src/__tests__/useRsTableCore.perf.spec.ts +170 -0
  44. package/src/components/RsAlert.vue +212 -0
  45. package/src/components/RsButton.vue +145 -21
  46. package/src/components/RsCard.vue +200 -16
  47. package/src/components/RsConfirmDialog.vue +215 -28
  48. package/src/components/RsContextMenu.vue +51 -13
  49. package/src/components/RsDatePicker.vue +267 -34
  50. package/src/components/RsDateTimePicker.vue +19 -2
  51. package/src/components/RsDescriptions.vue +186 -0
  52. package/src/components/RsDescriptionsItem.vue +32 -0
  53. package/src/components/RsDialog.vue +363 -51
  54. package/src/components/RsDivider.vue +89 -0
  55. package/src/components/RsDrawer.vue +558 -33
  56. package/src/components/RsDropdown.vue +33 -2
  57. package/src/components/RsDynamicTags.vue +517 -0
  58. package/src/components/RsForm.vue +97 -8
  59. package/src/components/RsInput.vue +80 -82
  60. package/src/components/RsInputNumber.vue +64 -23
  61. package/src/components/RsLabel.vue +20 -5
  62. package/src/components/RsLoadingBar.vue +151 -0
  63. package/src/components/RsMenu.vue +73 -15
  64. package/src/components/RsMenuItems.vue +147 -9
  65. package/src/components/RsPagination.vue +93 -14
  66. package/src/components/RsRadio.vue +81 -0
  67. package/src/components/RsRadioItem.vue +156 -0
  68. package/src/components/RsSelect.vue +67 -58
  69. package/src/components/RsSplitPane.vue +69 -1
  70. package/src/components/RsSwitch.vue +179 -0
  71. package/src/components/RsTable.vue +653 -2555
  72. package/src/components/RsTabs.vue +415 -83
  73. package/src/components/RsTag.vue +191 -0
  74. package/src/components/RsTerminal.vue +3 -3
  75. package/src/components/RsTimePicker.vue +44 -5
  76. package/src/components/RsTimePickerColumns.vue +136 -77
  77. package/src/components/RsToaster.vue +58 -18
  78. package/src/components/RsTree.vue +39 -21
  79. package/src/components/RsUpload.vue +145 -12
  80. package/src/components/RsVNodeHost.vue +20 -0
  81. package/src/components/button-utils.ts +39 -0
  82. package/src/components/context-menu-utils.ts +6 -0
  83. package/src/components/date-picker-utils.ts +84 -4
  84. package/src/components/descriptions-utils.ts +24 -0
  85. package/src/components/dialog-utils.ts +147 -0
  86. package/src/components/dialog-window.ts +74 -9
  87. package/src/components/drawer-utils.ts +73 -0
  88. package/src/components/form-rules.ts +257 -0
  89. package/src/components/form-utils.ts +52 -2
  90. package/src/components/menu-utils.ts +11 -0
  91. package/src/components/overlay-utils.ts +3 -0
  92. package/src/components/radio-utils.ts +12 -0
  93. package/src/components/reka.ts +5 -0
  94. package/src/components/select-utils.ts +21 -2
  95. package/src/components/split-pane-utils.ts +65 -12
  96. package/src/components/table/RsTableBody.vue +217 -0
  97. package/src/components/table/RsTableBodyRow.vue +90 -1
  98. package/src/components/table/RsTableColGroup.vue +41 -0
  99. package/src/components/table/RsTableHeader.vue +158 -0
  100. package/src/components/table/RsTableSummaryRow.vue +52 -0
  101. package/src/components/table/rs-table-api.ts +99 -0
  102. package/src/components/table/rs-table-compat-matrix.ts +71 -0
  103. package/src/components/table/rs-table-echarts-adapter.ts +81 -0
  104. package/src/components/table/rs-table-feature-host.ts +178 -0
  105. package/src/components/table/rs-table-grid-nav.ts +122 -0
  106. package/src/components/table/rs-table-header-types.ts +47 -0
  107. package/src/components/table/rs-table-module-registry.ts +72 -0
  108. package/src/components/table/rs-table-props.ts +274 -0
  109. package/src/components/table/rs-table-view-context.ts +258 -0
  110. package/src/components/table/rs-table.css +84 -6
  111. package/src/components/table/table-chart-utils.ts +165 -0
  112. package/src/components/table/table-features.ts +284 -0
  113. package/src/components/table/table-summary-utils.ts +194 -0
  114. package/src/components/table-utils.ts +452 -2
  115. package/src/components/tabs-utils.ts +142 -2
  116. package/src/components/terminal-utils.ts +1 -1
  117. package/src/components/terminal-wheel.ts +1 -1
  118. package/src/components/time-picker-utils.ts +76 -3
  119. package/src/components/upload-utils.ts +122 -0
  120. package/src/components/virtual-list-utils.ts +15 -2
  121. package/src/composables/assembleRsTableApi.ts +79 -0
  122. package/src/composables/bindRsTableViewContext.ts +269 -0
  123. package/src/composables/createRsDialog.ts +357 -0
  124. package/src/composables/createRsTableEditEmitBridge.ts +38 -0
  125. package/src/composables/useRsLoadingBar.ts +36 -0
  126. package/src/composables/useRsTable.ts +134 -0
  127. package/src/composables/useRsTableA11y.ts +52 -0
  128. package/src/composables/useRsTableChartBridge.ts +79 -0
  129. package/src/composables/useRsTableColumnLayout.ts +320 -0
  130. package/src/composables/useRsTableColumnResize.ts +244 -0
  131. package/src/composables/useRsTableColumnVirtual.ts +180 -0
  132. package/src/composables/useRsTableColumns.ts +200 -0
  133. package/src/composables/useRsTableContextMenu.ts +257 -0
  134. package/src/composables/useRsTableCore.ts +256 -0
  135. package/src/composables/useRsTableEditActions.ts +606 -0
  136. package/src/composables/useRsTableEditLayer.ts +251 -0
  137. package/src/composables/useRsTableEngine.ts +357 -0
  138. package/src/composables/useRsTableGridKeyboard.ts +123 -0
  139. package/src/composables/useRsTableHeadless.ts +137 -0
  140. package/src/composables/useRsTableInteraction.ts +433 -0
  141. package/src/composables/useRsTableScrollHost.ts +75 -0
  142. package/src/composables/useRsTableScrollLayout.ts +255 -0
  143. package/src/composables/useRsTableSelectionSource.ts +89 -0
  144. package/src/composables/useRsTableShell.ts +416 -0
  145. package/src/composables/useRsTableShellChrome.ts +170 -0
  146. package/src/composables/useRsTableSummary.ts +113 -0
  147. package/src/composables/useRsTableViewProvide.ts +263 -0
  148. package/src/composables/useRsTableVirtual.ts +154 -0
  149. package/src/composables/useRsTabsNav.ts +4 -0
  150. package/src/composables/useTableEdit.ts +1 -1
  151. package/src/icons/lucide.ts +2 -2
  152. package/src/index.ts +309 -9
  153. package/src/locale/messages.ts +30 -0
  154. package/src/styles.css +305 -52
  155. package/src/theme/brand.example.css +21 -2
  156. package/src/theme/presets.ts +4 -0
  157. package/src/theme/types.ts +67 -0
  158. package/src/components/RsTableColgroup.vue +0 -17
  159. package/src/components/RsTableHeaderRow.vue +0 -30
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
2
3
  import {
3
- DialogClose,
4
4
  DialogContent,
5
5
  DialogDescription,
6
6
  DialogOverlay,
@@ -10,42 +10,342 @@ import {
10
10
  } from './reka'
11
11
  import RsButton from './RsButton.vue'
12
12
  import { useRsI18n } from '../composables/useRsI18n'
13
+ import {
14
+ resolveDrawerOverlayStyle,
15
+ resolveRsDrawerDimensionCss,
16
+ resolveRsDrawerSizeCss,
17
+ runRsDrawerBeforeClose,
18
+ type RsDrawerBeforeClose,
19
+ type RsDrawerCloseReason,
20
+ type RsDrawerDimension,
21
+ type RsDrawerSide,
22
+ type RsDrawerSize,
23
+ } from './drawer-utils'
13
24
 
14
- export type RsDrawerSide = 'left' | 'right' | 'top' | 'bottom'
15
- export type RsDrawerSize = 'sm' | 'md' | 'lg' | 'full'
25
+ defineOptions({ inheritAttrs: false })
16
26
 
17
27
  const open = defineModel<boolean>('open', { default: false })
18
28
 
19
- withDefaults(
29
+ const props = withDefaults(
20
30
  defineProps<{
31
+ /** 标题文案;也可用 `#header` 插槽 */
21
32
  title?: string
22
33
  description?: string
23
34
  side?: RsDrawerSide
35
+ /** 预设尺寸;与 width/height 同时存在时自定义优先 */
24
36
  size?: RsDrawerSize
37
+ /** 左右抽屉自定义宽度(覆盖 size) */
38
+ width?: RsDrawerDimension
39
+ /** 上下抽屉自定义高度(覆盖 size) */
40
+ height?: RsDrawerDimension
41
+ /**
42
+ * 是否模态:锁焦点并拦截背后交互。
43
+ * 未显式传入时跟随 showOverlay(有遮罩=模态,无遮罩=非模态)。
44
+ */
45
+ modal?: boolean
46
+ /** 是否渲染遮罩(与 modal / closeOnOverlayClick 解耦) */
25
47
  showOverlay?: boolean
48
+ /**
49
+ * 遮罩不透明度 0–1;覆盖主题默认。
50
+ * 例:0.08 最浅、0.35 适中、0.55 较深。
51
+ */
52
+ overlayOpacity?: number
53
+ /** 遮罩模糊;number 为 px */
54
+ overlayBlur?: number | string
26
55
  showClose?: boolean
56
+ /**
57
+ * 点击抽屉外是否关闭(含无遮罩时的 interact-outside)。
58
+ * 与 modal 独立:非模态也可保持打开。
59
+ */
27
60
  closeOnOverlayClick?: boolean
61
+ /** 按 Esc 是否关闭;默认 true */
62
+ closeOnEsc?: boolean
63
+ /**
64
+ * DialogPortal 挂载目标
65
+ * - string / HTMLElement:Teleport 到指定节点(容器需 position:relative)
66
+ * - false:禁用 Teleport,就地渲染
67
+ * - undefined:走 Reka 默认(通常为 body)
68
+ */
69
+ teleportTo?: string | HTMLElement | false
70
+ /** 关闭前钩子;返回 false 可阻止关闭(支持 async) */
71
+ beforeClose?: RsDrawerBeforeClose
28
72
  }>(),
29
73
  {
74
+ title: '',
30
75
  side: 'right',
31
76
  size: 'md',
32
77
  showOverlay: true,
33
78
  showClose: true,
34
79
  closeOnOverlayClick: true,
80
+ closeOnEsc: true,
35
81
  },
36
82
  )
37
83
 
84
+ const emit = defineEmits<{
85
+ openChange: [open: boolean]
86
+ afterOpen: []
87
+ afterClose: [reason: RsDrawerCloseReason]
88
+ }>()
89
+
38
90
  const { t } = useRsI18n()
91
+
92
+ /** 挂到自定义容器或就地渲染时按 contained 模式定位,避免盖住布局顶栏 */
93
+ const contained = computed(() => props.teleportTo !== undefined)
94
+ /** 未显式传 modal 时:有遮罩=模态,无遮罩=可点背后 */
95
+ const isModal = computed(() => props.modal ?? props.showOverlay)
96
+
97
+ const overlayStyle = computed(() =>
98
+ resolveDrawerOverlayStyle({
99
+ overlayOpacity: props.overlayOpacity,
100
+ overlayBlur: props.overlayBlur,
101
+ }),
102
+ )
103
+
104
+ const isHorizontal = computed(() => props.side === 'left' || props.side === 'right')
105
+
106
+ const panelSizeCss = computed(() => {
107
+ if (isHorizontal.value) {
108
+ return (
109
+ resolveRsDrawerDimensionCss(props.width) ??
110
+ resolveRsDrawerSizeCss(props.size) ??
111
+ undefined
112
+ )
113
+ }
114
+ return (
115
+ resolveRsDrawerDimensionCss(props.height) ??
116
+ resolveRsDrawerSizeCss(props.size) ??
117
+ undefined
118
+ )
119
+ })
120
+
121
+ const contentStyle = computed(() => {
122
+ const size = panelSizeCss.value
123
+ if (!size || props.size === 'full') return undefined
124
+ return { '--rs-drawer-panel-size': size } as Record<string, string>
125
+ })
126
+
127
+ const contentClass = computed(() => [
128
+ `rs-drawer__content--${props.side}`,
129
+ `rs-drawer__content--${props.size}`,
130
+ {
131
+ 'rs-drawer__content--contained': contained.value,
132
+ 'rs-drawer__content--custom-size': Boolean(panelSizeCss.value) && props.size !== 'full',
133
+ },
134
+ ])
135
+
136
+ const closing = ref(false)
137
+ let pendingCloseReason: RsDrawerCloseReason = 'programmatic'
138
+ let afterOpenTimer = 0
139
+
140
+ /** 跟踪当前按下指针数,用于判断打开是否发生在手势中途 */
141
+ let pointersDown = 0
142
+ let pointerTrackingBound = false
143
+
144
+ function ensurePointerTracking(): void {
145
+ if (pointerTrackingBound || typeof window === 'undefined') return
146
+ pointerTrackingBound = true
147
+ window.addEventListener(
148
+ 'pointerdown',
149
+ () => {
150
+ pointersDown += 1
151
+ },
152
+ true,
153
+ )
154
+ window.addEventListener(
155
+ 'pointerup',
156
+ () => {
157
+ pointersDown = Math.max(0, pointersDown - 1)
158
+ },
159
+ true,
160
+ )
161
+ window.addEventListener(
162
+ 'pointercancel',
163
+ () => {
164
+ pointersDown = Math.max(0, pointersDown - 1)
165
+ },
166
+ true,
167
+ )
168
+ }
169
+
170
+ /**
171
+ * 打开若发生在 pointer 按下期间(或同轮事件冒泡中),抑制 outside dismiss,
172
+ * 直到指针抬起,并再刷一次 macrotask(吞掉打开点击残留的 outside)。
173
+ * 不使用固定毫秒锁,避免拖慢正常点遮罩关闭。
174
+ */
175
+ const suppressOutsideUntilGestureEnd = ref(false)
176
+ let gestureReleaseBound = false
177
+ let gestureMacrotaskTimer = 0
178
+
179
+ function releaseOutsideSuppress(): void {
180
+ suppressOutsideUntilGestureEnd.value = false
181
+ if (gestureMacrotaskTimer) {
182
+ window.clearTimeout(gestureMacrotaskTimer)
183
+ gestureMacrotaskTimer = 0
184
+ }
185
+ if (!gestureReleaseBound) return
186
+ gestureReleaseBound = false
187
+ window.removeEventListener('pointerup', onGesturePointerRelease, true)
188
+ window.removeEventListener('pointercancel', onGesturePointerRelease, true)
189
+ }
190
+
191
+ function onGesturePointerRelease(): void {
192
+ if (pointersDown > 0) return
193
+ // 指针已抬起:再等一个 macrotask,避开同次点击残留的 outside
194
+ if (gestureMacrotaskTimer) window.clearTimeout(gestureMacrotaskTimer)
195
+ gestureMacrotaskTimer = window.setTimeout(() => {
196
+ gestureMacrotaskTimer = 0
197
+ releaseOutsideSuppress()
198
+ }, 0)
199
+ }
200
+
201
+ function armOutsideSuppressForOpenGesture(): void {
202
+ ensurePointerTracking()
203
+ releaseOutsideSuppress()
204
+ suppressOutsideUntilGestureEnd.value = true
205
+ if (pointersDown > 0) {
206
+ gestureReleaseBound = true
207
+ window.addEventListener('pointerup', onGesturePointerRelease, true)
208
+ window.addEventListener('pointercancel', onGesturePointerRelease, true)
209
+ return
210
+ }
211
+ // 指针已抬起(常见于 click 打开):仅吞掉本轮之后的同步/微任务 outside
212
+ gestureMacrotaskTimer = window.setTimeout(() => {
213
+ gestureMacrotaskTimer = 0
214
+ releaseOutsideSuppress()
215
+ }, 0)
216
+ }
217
+
218
+ function clearAfterOpenTimer(): void {
219
+ if (!afterOpenTimer) return
220
+ window.clearTimeout(afterOpenTimer)
221
+ afterOpenTimer = 0
222
+ }
223
+
224
+ function queueAfterOpen(): void {
225
+ clearAfterOpenTimer()
226
+ // 略晚于滑入动画,便于业务在可见后聚焦
227
+ afterOpenTimer = window.setTimeout(() => {
228
+ afterOpenTimer = 0
229
+ if (open.value) emit('afterOpen')
230
+ }, 280)
231
+ }
232
+
233
+ watch(open, (isOpen, wasOpen) => {
234
+ if (isOpen && !wasOpen) {
235
+ armOutsideSuppressForOpenGesture()
236
+ emit('openChange', true)
237
+ queueAfterOpen()
238
+ return
239
+ }
240
+ if (!isOpen && wasOpen) {
241
+ releaseOutsideSuppress()
242
+ clearAfterOpenTimer()
243
+ emit('openChange', false)
244
+ if (!closing.value) emit('afterClose', 'programmatic')
245
+ }
246
+ })
247
+
248
+ onBeforeUnmount(() => {
249
+ releaseOutsideSuppress()
250
+ clearAfterOpenTimer()
251
+ })
252
+
253
+ async function requestClose(reason: RsDrawerCloseReason): Promise<boolean> {
254
+ if (!open.value || closing.value) return false
255
+ closing.value = true
256
+ pendingCloseReason = reason
257
+ try {
258
+ const allowed = await runRsDrawerBeforeClose(props.beforeClose, reason)
259
+ if (!allowed) return false
260
+ open.value = false
261
+ await nextTick()
262
+ emit('afterClose', reason)
263
+ return true
264
+ } finally {
265
+ closing.value = false
266
+ }
267
+ }
268
+
269
+ /** DialogRoot 受控更新:打开直通;关闭统一走 beforeClose */
270
+ async function onUpdateOpen(next: boolean): Promise<void> {
271
+ if (next) {
272
+ open.value = true
273
+ return
274
+ }
275
+ await requestClose(pendingCloseReason)
276
+ pendingCloseReason = 'programmatic'
277
+ }
278
+
279
+ function onEscapeKeyDown(event: Event): void {
280
+ if (!props.closeOnEsc) {
281
+ event.preventDefault()
282
+ return
283
+ }
284
+ pendingCloseReason = 'escape'
285
+ }
286
+
287
+ function shouldBlockOutsideDismiss(): boolean {
288
+ return (
289
+ !props.closeOnOverlayClick ||
290
+ suppressOutsideUntilGestureEnd.value
291
+ )
292
+ }
293
+
294
+ function onPointerDownOutside(event: Event): void {
295
+ if (shouldBlockOutsideDismiss()) {
296
+ event.preventDefault()
297
+ return
298
+ }
299
+ pendingCloseReason = 'overlay'
300
+ }
301
+
302
+ function onInteractOutside(event: Event): void {
303
+ if (shouldBlockOutsideDismiss()) {
304
+ event.preventDefault()
305
+ return
306
+ }
307
+ pendingCloseReason = 'overlay'
308
+ }
309
+
310
+ function onFocusOutside(event: Event): void {
311
+ // 抽屉仅响应 pointer / Esc 关闭,聚焦到背后(尤其非模态)不得 dismiss
312
+ event.preventDefault()
313
+ }
314
+
315
+ async function onHeaderCloseClick(): Promise<void> {
316
+ await requestClose('close')
317
+ }
318
+
319
+ defineExpose({
320
+ /** 请求关闭(走 beforeClose) */
321
+ close: (reason: RsDrawerCloseReason = 'programmatic') => requestClose(reason),
322
+ /** 打开抽屉 */
323
+ openDrawer: () => {
324
+ open.value = true
325
+ },
326
+ })
39
327
  </script>
40
328
 
41
329
  <template>
42
- <DialogRoot v-model:open="open">
43
- <DialogPortal>
44
- <DialogOverlay v-if="showOverlay" class="rs-drawer__overlay" />
330
+ <DialogRoot :open="open" :modal="isModal" @update:open="onUpdateOpen">
331
+ <DialogPortal
332
+ :disabled="teleportTo === false"
333
+ :to="teleportTo === false ? undefined : teleportTo"
334
+ >
335
+ <DialogOverlay
336
+ v-if="showOverlay"
337
+ class="rs-drawer__overlay rs-motion-reduce"
338
+ :class="{ 'rs-drawer__overlay--contained': contained }"
339
+ :style="overlayStyle"
340
+ />
45
341
  <DialogContent
46
- class="rs-drawer__content"
47
- :class="[`rs-drawer__content--${side}`, `rs-drawer__content--${size}`]"
48
- @pointer-down-outside="closeOnOverlayClick ? undefined : $event.preventDefault()"
342
+ class="rs-drawer__content rs-motion-reduce"
343
+ :class="contentClass"
344
+ :style="contentStyle"
345
+ @pointer-down-outside="onPointerDownOutside"
346
+ @interact-outside="onInteractOutside"
347
+ @focus-outside="onFocusOutside"
348
+ @escape-key-down="onEscapeKeyDown"
49
349
  >
50
350
  <header v-if="title || description || showClose || $slots.header" class="rs-drawer__header">
51
351
  <slot name="header">
@@ -56,10 +356,32 @@ const { t } = useRsI18n()
56
356
  </DialogDescription>
57
357
  </div>
58
358
  </slot>
59
- <DialogClose v-if="showClose" as-child>
60
- <RsButton variant="ghost" size="sm" icon="x" :tooltip="t('dialog.close')" />
61
- </DialogClose>
359
+ <RsButton
360
+ v-if="showClose"
361
+ variant="ghost"
362
+ size="sm"
363
+ icon="x"
364
+ :tooltip="t('dialog.close')"
365
+ @click="onHeaderCloseClick"
366
+ />
62
367
  </header>
368
+ <!--
369
+ 自定义 #header 或不传 title 时,默认插槽内的 DialogTitle 不会挂载。
370
+ 补一层仅供读屏的 Title,满足 Reka DialogContent 无障碍要求。
371
+ -->
372
+ <DialogTitle
373
+ v-if="$slots.header || !title"
374
+ class="rs-drawer__title rs-drawer__title--sr-only"
375
+ >
376
+ {{ title || ' ' }}
377
+ </DialogTitle>
378
+ <!-- 无可见 description 时仍提供 DialogDescription,避免 Reka a11y 警告 -->
379
+ <DialogDescription
380
+ v-if="!description || $slots.header"
381
+ class="rs-drawer__description rs-drawer__description--sr-only"
382
+ >
383
+ {{ description || title || ' ' }}
384
+ </DialogDescription>
63
385
  <div class="rs-drawer__body">
64
386
  <slot />
65
387
  </div>
@@ -76,8 +398,19 @@ const { t } = useRsI18n()
76
398
  position: fixed;
77
399
  inset: 0;
78
400
  z-index: var(--rs-z-modal);
79
- background: rgb(0 0 0 / 0.45);
80
- backdrop-filter: blur(4px);
401
+ background: var(--rs-drawer-overlay-bg, var(--rs-dialog-overlay-bg, rgb(0 0 0 / 0.24)));
402
+ backdrop-filter: blur(var(--rs-drawer-overlay-blur, var(--rs-dialog-overlay-blur, 0px))) saturate(120%);
403
+ -webkit-backdrop-filter: blur(var(--rs-drawer-overlay-blur, var(--rs-dialog-overlay-blur, 0px)))
404
+ saturate(120%);
405
+ }
406
+ .rs-drawer__overlay[data-state='open'] {
407
+ animation: rs-drawer-overlay-in var(--rs-drawer-motion-duration-in, 240ms) ease;
408
+ }
409
+ .rs-drawer__overlay[data-state='closed'] {
410
+ animation: rs-drawer-overlay-out var(--rs-drawer-motion-duration-out, 180ms) ease;
411
+ }
412
+ .rs-drawer__overlay--contained {
413
+ position: absolute;
81
414
  }
82
415
  .rs-drawer__content {
83
416
  position: fixed;
@@ -86,17 +419,24 @@ const { t } = useRsI18n()
86
419
  flex-direction: column;
87
420
  overflow: hidden;
88
421
  box-sizing: border-box;
89
- border: 1px solid var(--rs-border);
90
- background: var(--rs-surface-elevated);
422
+ border: 1px solid var(--rs-drawer-border, var(--rs-border));
423
+ background: var(--rs-drawer-body-bg, var(--rs-surface-elevated));
91
424
  color: var(--rs-text);
92
425
  box-shadow: var(--rs-shadow-lg);
93
426
  outline: none;
427
+ will-change: transform;
428
+ }
429
+ .rs-drawer__content--contained {
430
+ position: absolute;
94
431
  }
95
432
  .rs-drawer__content--right,
96
433
  .rs-drawer__content--left {
97
434
  top: 0;
98
435
  bottom: 0;
99
- width: min(100vw, 28rem);
436
+ width: min(100vw, var(--rs-drawer-panel-size, 28rem));
437
+ }
438
+ .rs-drawer__content--contained:is(.rs-drawer__content--right, .rs-drawer__content--left) {
439
+ width: min(100%, var(--rs-drawer-panel-size, 28rem));
100
440
  }
101
441
  .rs-drawer__content--right {
102
442
  right: 0;
@@ -112,7 +452,10 @@ const { t } = useRsI18n()
112
452
  .rs-drawer__content--bottom {
113
453
  left: 0;
114
454
  right: 0;
115
- height: min(100vh, 22rem);
455
+ height: min(100vh, var(--rs-drawer-panel-size, 22rem));
456
+ }
457
+ .rs-drawer__content--contained:is(.rs-drawer__content--top, .rs-drawer__content--bottom) {
458
+ height: min(100%, var(--rs-drawer-panel-size, 22rem));
116
459
  }
117
460
  .rs-drawer__content--top {
118
461
  top: 0;
@@ -125,51 +468,233 @@ const { t } = useRsI18n()
125
468
  border-radius: var(--rs-radius) var(--rs-radius) 0 0;
126
469
  }
127
470
  .rs-drawer__content--sm:is(.rs-drawer__content--left, .rs-drawer__content--right) {
128
- width: min(100vw, 20rem);
471
+ --rs-drawer-panel-size: 20rem;
129
472
  }
130
473
  .rs-drawer__content--lg:is(.rs-drawer__content--left, .rs-drawer__content--right) {
131
- width: min(100vw, 36rem);
474
+ --rs-drawer-panel-size: 36rem;
132
475
  }
133
476
  .rs-drawer__content--full:is(.rs-drawer__content--left, .rs-drawer__content--right) {
134
477
  width: 100vw;
135
478
  }
479
+ .rs-drawer__content--contained.rs-drawer__content--full:is(
480
+ .rs-drawer__content--left,
481
+ .rs-drawer__content--right
482
+ ) {
483
+ width: 100%;
484
+ }
136
485
  .rs-drawer__content--sm:is(.rs-drawer__content--top, .rs-drawer__content--bottom) {
137
- height: min(100vh, 16rem);
486
+ --rs-drawer-panel-size: 16rem;
487
+ }
488
+ .rs-drawer__content--md:is(.rs-drawer__content--top, .rs-drawer__content--bottom) {
489
+ --rs-drawer-panel-size: 22rem;
138
490
  }
139
491
  .rs-drawer__content--lg:is(.rs-drawer__content--top, .rs-drawer__content--bottom) {
140
- height: min(100vh, 32rem);
492
+ --rs-drawer-panel-size: 32rem;
141
493
  }
142
494
  .rs-drawer__content--full:is(.rs-drawer__content--top, .rs-drawer__content--bottom) {
143
495
  height: 100vh;
144
496
  }
145
- .rs-drawer__header,
146
- .rs-drawer__footer {
497
+ .rs-drawer__content--contained.rs-drawer__content--full:is(
498
+ .rs-drawer__content--top,
499
+ .rs-drawer__content--bottom
500
+ ) {
501
+ height: 100%;
502
+ }
503
+
504
+ /* 滑入 / 滑出(与 Reka data-state 对齐;缓动接近主流 sheet) */
505
+ .rs-drawer__content--right[data-state='open'] {
506
+ animation: rs-drawer-slide-right-in var(--rs-drawer-motion-duration-in, 240ms)
507
+ cubic-bezier(0.32, 0.72, 0, 1);
508
+ }
509
+ .rs-drawer__content--right[data-state='closed'] {
510
+ animation: rs-drawer-slide-right-out var(--rs-drawer-motion-duration-out, 180ms) ease;
511
+ }
512
+ .rs-drawer__content--left[data-state='open'] {
513
+ animation: rs-drawer-slide-left-in var(--rs-drawer-motion-duration-in, 240ms)
514
+ cubic-bezier(0.32, 0.72, 0, 1);
515
+ }
516
+ .rs-drawer__content--left[data-state='closed'] {
517
+ animation: rs-drawer-slide-left-out var(--rs-drawer-motion-duration-out, 180ms) ease;
518
+ }
519
+ .rs-drawer__content--top[data-state='open'] {
520
+ animation: rs-drawer-slide-top-in var(--rs-drawer-motion-duration-in, 240ms)
521
+ cubic-bezier(0.32, 0.72, 0, 1);
522
+ }
523
+ .rs-drawer__content--top[data-state='closed'] {
524
+ animation: rs-drawer-slide-top-out var(--rs-drawer-motion-duration-out, 180ms) ease;
525
+ }
526
+ .rs-drawer__content--bottom[data-state='open'] {
527
+ animation: rs-drawer-slide-bottom-in var(--rs-drawer-motion-duration-in, 240ms)
528
+ cubic-bezier(0.32, 0.72, 0, 1);
529
+ }
530
+ .rs-drawer__content--bottom[data-state='closed'] {
531
+ animation: rs-drawer-slide-bottom-out var(--rs-drawer-motion-duration-out, 180ms) ease;
532
+ }
533
+
534
+ @keyframes rs-drawer-overlay-in {
535
+ from {
536
+ opacity: 0;
537
+ }
538
+ to {
539
+ opacity: 1;
540
+ }
541
+ }
542
+ @keyframes rs-drawer-overlay-out {
543
+ from {
544
+ opacity: 1;
545
+ }
546
+ to {
547
+ opacity: 0;
548
+ }
549
+ }
550
+ @keyframes rs-drawer-slide-right-in {
551
+ from {
552
+ transform: translateX(100%);
553
+ }
554
+ to {
555
+ transform: translateX(0);
556
+ }
557
+ }
558
+ @keyframes rs-drawer-slide-right-out {
559
+ from {
560
+ transform: translateX(0);
561
+ }
562
+ to {
563
+ transform: translateX(100%);
564
+ }
565
+ }
566
+ @keyframes rs-drawer-slide-left-in {
567
+ from {
568
+ transform: translateX(-100%);
569
+ }
570
+ to {
571
+ transform: translateX(0);
572
+ }
573
+ }
574
+ @keyframes rs-drawer-slide-left-out {
575
+ from {
576
+ transform: translateX(0);
577
+ }
578
+ to {
579
+ transform: translateX(-100%);
580
+ }
581
+ }
582
+ @keyframes rs-drawer-slide-top-in {
583
+ from {
584
+ transform: translateY(-100%);
585
+ }
586
+ to {
587
+ transform: translateY(0);
588
+ }
589
+ }
590
+ @keyframes rs-drawer-slide-top-out {
591
+ from {
592
+ transform: translateY(0);
593
+ }
594
+ to {
595
+ transform: translateY(-100%);
596
+ }
597
+ }
598
+ @keyframes rs-drawer-slide-bottom-in {
599
+ from {
600
+ transform: translateY(100%);
601
+ }
602
+ to {
603
+ transform: translateY(0);
604
+ }
605
+ }
606
+ @keyframes rs-drawer-slide-bottom-out {
607
+ from {
608
+ transform: translateY(0);
609
+ }
610
+ to {
611
+ transform: translateY(100%);
612
+ }
613
+ }
614
+
615
+ .rs-drawer__header {
147
616
  display: flex;
148
- align-items: flex-start;
617
+ align-items: center;
149
618
  justify-content: space-between;
150
619
  gap: var(--rs-space-md);
151
- padding: var(--rs-space-lg) var(--rs-space-xl);
152
- border-bottom: 1px solid var(--rs-border-subtle);
620
+ flex-shrink: 0;
621
+ box-sizing: border-box;
622
+ height: var(--rs-drawer-header-height);
623
+ min-height: var(--rs-drawer-header-min-height);
624
+ padding: var(--rs-drawer-header-padding-y) var(--rs-drawer-header-padding-x);
625
+ background: var(--rs-drawer-header-bg);
626
+ border-bottom: 1px solid var(--rs-drawer-separator, var(--rs-border-subtle));
627
+ }
628
+ .rs-drawer__heading {
629
+ min-width: 0;
630
+ flex: 1;
631
+ overflow: hidden;
632
+ display: flex;
633
+ flex-direction: column;
634
+ justify-content: center;
153
635
  }
154
636
  .rs-drawer__footer {
637
+ display: flex;
638
+ align-items: center;
155
639
  justify-content: flex-end;
156
- border-top: 1px solid var(--rs-border-subtle);
157
- border-bottom: 0;
640
+ gap: var(--rs-space-md);
641
+ flex-shrink: 0;
642
+ box-sizing: border-box;
643
+ height: var(--rs-drawer-footer-height);
644
+ min-height: var(--rs-drawer-footer-min-height);
645
+ padding: var(--rs-drawer-footer-padding-y) var(--rs-drawer-footer-padding-x);
646
+ background: var(--rs-drawer-footer-bg);
647
+ border-top: 1px solid
648
+ var(--rs-drawer-footer-border, var(--rs-drawer-separator, var(--rs-border-subtle)));
158
649
  }
159
650
  .rs-drawer__body {
160
651
  flex: 1;
161
652
  min-height: 0;
162
- overflow: auto;
163
- padding: var(--rs-space-xl);
653
+ overflow: hidden;
654
+ display: flex;
655
+ flex-direction: column;
656
+ box-sizing: border-box;
657
+ padding: var(--rs-drawer-body-padding-y) var(--rs-drawer-body-padding-x);
658
+ background: var(--rs-drawer-body-bg);
164
659
  }
165
660
  .rs-drawer__title {
166
661
  margin: 0;
662
+ display: inline-flex;
663
+ align-items: center;
664
+ gap: var(--rs-space-sm);
167
665
  font-size: var(--rs-font-size-base);
168
666
  font-weight: 600;
667
+ line-height: var(--rs-line-height-tight, 1.3);
668
+ color: var(--rs-drawer-title-fg, var(--rs-text));
669
+ overflow: hidden;
670
+ text-overflow: ellipsis;
671
+ white-space: nowrap;
169
672
  }
170
673
  .rs-drawer__description {
171
674
  margin: var(--rs-space-xs) 0 0;
172
- color: var(--rs-muted);
675
+ color: var(--rs-drawer-description-fg, var(--rs-muted));
173
676
  font-size: var(--rs-font-size-sm);
174
677
  }
678
+ .rs-drawer__description--sr-only {
679
+ position: absolute;
680
+ width: 1px;
681
+ height: 1px;
682
+ padding: 0;
683
+ margin: -1px;
684
+ overflow: hidden;
685
+ clip: rect(0, 0, 0, 0);
686
+ white-space: nowrap;
687
+ border: 0;
688
+ }
689
+ .rs-drawer__title--sr-only {
690
+ position: absolute;
691
+ width: 1px;
692
+ height: 1px;
693
+ padding: 0;
694
+ margin: -1px;
695
+ overflow: hidden;
696
+ clip: rect(0, 0, 0, 0);
697
+ white-space: nowrap;
698
+ border: 0;
699
+ }
175
700
  </style>