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,5 +1,15 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, ref, toRef, watch } from 'vue'
2
+ import {
3
+ computed,
4
+ nextTick,
5
+ onBeforeUnmount,
6
+ onMounted,
7
+ ref,
8
+ toRef,
9
+ useAttrs,
10
+ useSlots,
11
+ watch,
12
+ } from 'vue'
3
13
  import {
4
14
  DialogContent,
5
15
  DialogDescription,
@@ -11,28 +21,60 @@ import {
11
21
  import { useRsI18n } from '../composables/useRsI18n'
12
22
  import RsButton from './RsButton.vue'
13
23
  import type { RsFeedbackTone } from './overlay-utils'
24
+ import {
25
+ isRsDialogWidthPreset,
26
+ resolveDialogOverlayStyle,
27
+ resolveRsDialogCssWidth,
28
+ runRsDialogBeforeClose,
29
+ type RsDialogBeforeClose,
30
+ type RsDialogCloseReason,
31
+ type RsDialogLayout,
32
+ type RsDialogWidth,
33
+ type RsDialogWidthPreset,
34
+ } from './dialog-utils'
14
35
  import { useRsDialogWindow } from './dialog-window'
15
36
 
37
+ defineOptions({ inheritAttrs: false })
38
+
16
39
  const open = defineModel<boolean>('open', { default: false })
17
40
 
18
41
  const props = withDefaults(
19
42
  defineProps<{
20
- title: string
43
+ /** 标题文案;也可用 `#title` / `#header` 插槽。缺省时用空格满足无障碍 Title */
44
+ title?: string
21
45
  description?: string
22
- width?: 'sm' | 'md' | 'lg'
46
+ width?: RsDialogWidth
23
47
  tone?: RsFeedbackTone
24
- layout?: 'window' | 'confirm'
48
+ /**
49
+ * window:可拖拽/缩放工作窗(推荐)。
50
+ * confirm:历史兼容的居中轻量窗;新代码的确认/提示请用 RsConfirmDialog。
51
+ */
52
+ layout?: RsDialogLayout
25
53
  draggable?: boolean
26
54
  resizable?: boolean
27
55
  fullscreenable?: boolean
28
56
  /** 是否模态(false 时不锁焦点/不挡背后交互,适合页签内浮层) */
29
57
  modal?: boolean
30
58
  showOverlay?: boolean
59
+ /**
60
+ * 遮罩不透明度 0–1;覆盖主题默认(最浅)。
61
+ * 例:0.08 最浅、0.35 适中、0.55 较深。
62
+ */
63
+ overlayOpacity?: number
64
+ /** 遮罩模糊;number 为 px。默认主题为 0(无模糊,背后内容清晰) */
65
+ overlayBlur?: number | string
31
66
  showClose?: boolean
32
67
  /** 点击外部是否关闭;默认 false。与 modal 独立:非模态也可保持打开。 */
33
68
  closeOnOverlayClick?: boolean
34
- /** DialogPortal 挂载目标(id 选择器或 Element) */
35
- teleportTo?: string | HTMLElement
69
+ /** 按 Esc 是否关闭;默认 true(与 Reka 默认一致) */
70
+ closeOnEsc?: boolean
71
+ /**
72
+ * DialogPortal 挂载目标
73
+ * - string / HTMLElement:Teleport 到指定节点
74
+ * - false:禁用 Teleport,就地渲染(不挂到 body / 全局挂载点)
75
+ * - undefined:走 Reka 默认(通常为 body,或 ConfigProvider.teleportTo)
76
+ */
77
+ teleportTo?: string | HTMLElement | false
36
78
  /**
37
79
  * 延后挂载 #body 插槽,避免与打开动画/重组件 init 争抢主线程。
38
80
  * 默认:window 布局为 true,confirm 为 false。
@@ -40,8 +82,25 @@ const props = withDefaults(
40
82
  deferBodyMount?: boolean
41
83
  /** 全屏/还原时是否播放 bounds 过渡(含编辑器时建议保持 false) */
42
84
  boundsTransition?: boolean
85
+ /** 关闭前钩子;返回 false 可阻止关闭(支持 async) */
86
+ beforeClose?: RsDialogBeforeClose
87
+ /**
88
+ * 显示内置底部按钮区。默认 false,避免影响仅用 `#footer` 或不需要按钮的旧用法。
89
+ * 无 `#footer` 且为 true 时渲染取消/确定。
90
+ * 危险确认/提示请用 RsConfirmDialog,不要用本 footer 冒充确认 UI。
91
+ */
92
+ showFooter?: boolean
93
+ showCancel?: boolean
94
+ showConfirm?: boolean
95
+ cancelText?: string
96
+ confirmText?: string
97
+ confirmLoading?: boolean
98
+ confirmVariant?: 'primary' | 'danger'
99
+ /** 点击确定后是否自动关闭(仍会走 beforeClose) */
100
+ autoCloseOnConfirm?: boolean
43
101
  }>(),
44
102
  {
103
+ title: '',
45
104
  width: 'md',
46
105
  tone: 'default',
47
106
  layout: 'window',
@@ -52,20 +111,64 @@ const props = withDefaults(
52
111
  showOverlay: false,
53
112
  showClose: true,
54
113
  closeOnOverlayClick: false,
114
+ closeOnEsc: true,
55
115
  boundsTransition: false,
116
+ showFooter: false,
117
+ showCancel: true,
118
+ showConfirm: true,
119
+ confirmLoading: false,
120
+ confirmVariant: 'primary',
121
+ autoCloseOnConfirm: false,
56
122
  },
57
123
  )
58
124
 
125
+ const emit = defineEmits<{
126
+ openChange: [open: boolean]
127
+ afterOpen: []
128
+ afterClose: [reason: RsDialogCloseReason]
129
+ confirm: []
130
+ cancel: []
131
+ }>()
132
+
133
+ const attrs = useAttrs()
134
+ const slots = useSlots()
59
135
  const { t } = useRsI18n()
136
+
60
137
  const isWindowLayout = computed(() => props.layout === 'window')
61
138
  const isConfirmLayout = computed(() => props.layout === 'confirm')
62
139
  const enableDraggable = computed(() => props.draggable && isWindowLayout.value)
63
140
  const enableResizable = computed(() => props.resizable && isWindowLayout.value)
64
141
  const deferBodyMount = computed(() => props.deferBodyMount ?? isWindowLayout.value)
142
+ const overlayStyle = computed(() =>
143
+ resolveDialogOverlayStyle({
144
+ overlayOpacity: props.overlayOpacity,
145
+ overlayBlur: props.overlayBlur,
146
+ }),
147
+ )
148
+
149
+ const widthPreset = computed<RsDialogWidthPreset>(() =>
150
+ isRsDialogWidthPreset(props.width) ? props.width : 'md',
151
+ )
152
+
153
+ const initialWidthPx = computed(() => {
154
+ if (typeof props.width === 'number' && Number.isFinite(props.width) && props.width > 0) {
155
+ return props.width
156
+ }
157
+ if (typeof props.width === 'string') {
158
+ const trimmed = props.width.trim()
159
+ if (/^\d+(\.\d+)?px$/i.test(trimmed)) return Number.parseFloat(trimmed)
160
+ }
161
+ return undefined
162
+ })
163
+
164
+ const customCssWidth = computed(() => resolveRsDialogCssWidth(props.width))
65
165
 
66
166
  const bodyReady = ref(false)
167
+ const closing = ref(false)
168
+ let pendingCloseReason: RsDialogCloseReason = 'programmatic'
67
169
  let bodyMountFrameOuter = 0
68
170
  let bodyMountFrameInner = 0
171
+ let afterOpenTimer = 0
69
172
 
70
173
  function resetBodyMount(): void {
71
174
  if (bodyMountFrameOuter) {
@@ -103,17 +206,152 @@ watch(
103
206
  { immediate: true },
104
207
  )
105
208
 
106
- onBeforeUnmount(resetBodyMount)
209
+ function clearAfterOpenTimer(): void {
210
+ if (!afterOpenTimer) return
211
+ window.clearTimeout(afterOpenTimer)
212
+ afterOpenTimer = 0
213
+ }
214
+
215
+ function queueAfterOpen(): void {
216
+ clearAfterOpenTimer()
217
+ // 略晚于打开动画,便于业务在可见后聚焦
218
+ afterOpenTimer = window.setTimeout(() => {
219
+ afterOpenTimer = 0
220
+ if (open.value) emit('afterOpen')
221
+ }, 230)
222
+ }
223
+
224
+ watch(open, (isOpen, wasOpen) => {
225
+ if (isOpen && !wasOpen) {
226
+ emit('openChange', true)
227
+ queueAfterOpen()
228
+ return
229
+ }
230
+ if (!isOpen && wasOpen) {
231
+ emit('openChange', false)
232
+ // 父级直接改 v-model 关闭时补发 afterClose(requestClose 路径已发过)
233
+ if (!closing.value) emit('afterClose', 'programmatic')
234
+ }
235
+ })
236
+
237
+ onMounted(() => {
238
+ if (import.meta.env.DEV && props.layout === 'confirm') {
239
+ console.warn(
240
+ '[RsDialog] layout:"confirm" 已弃用。确认/提示请使用 RsConfirmDialog / rsConfirm;RsDialog 仅用于工作窗与表单。',
241
+ )
242
+ }
243
+ })
244
+
245
+ onBeforeUnmount(() => {
246
+ resetBodyMount()
247
+ clearAfterOpenTimer()
248
+ })
107
249
 
108
250
  const showBodyContent = computed(() => !deferBodyMount.value || bodyReady.value)
109
251
 
252
+ const showBuiltinFooter = computed(
253
+ () => props.showFooter && !slots.footer && (props.showCancel || props.showConfirm),
254
+ )
255
+
256
+ const resolvedTitle = computed(() => {
257
+ const text = props.title?.trim()
258
+ return text || ' '
259
+ })
260
+
261
+ const contentClass = computed(() => {
262
+ const presetClass = isRsDialogWidthPreset(props.width)
263
+ ? `rs-dialog__content--${props.width}`
264
+ : 'rs-dialog__content--custom-width'
265
+ return [
266
+ `rs-dialog__content--${props.layout}`,
267
+ presetClass,
268
+ `rs-dialog__content--tone-${props.tone}`,
269
+ {
270
+ 'rs-dialog__content--fullscreen': isFullscreen.value,
271
+ 'rs-dialog__content--draggable': enableDraggable.value,
272
+ 'rs-dialog__content--bounds-transition': boundsTransitionEnabled.value,
273
+ },
274
+ ]
275
+ })
276
+
277
+ const contentStyle = computed(() => {
278
+ const base = isWindowLayout.value ? dialogStyle.value : undefined
279
+ const cssW = customCssWidth.value
280
+ // 数值/px 已通过 initialWidth 进入 window bounds;其它自定义宽度用 CSS 覆盖
281
+ if (!cssW) return base
282
+ if (isWindowLayout.value && initialWidthPx.value != null) return base
283
+ return {
284
+ ...(base ?? {}),
285
+ width: isConfirmLayout.value ? undefined : cssW,
286
+ maxWidth: cssW,
287
+ }
288
+ })
289
+
290
+ async function requestClose(reason: RsDialogCloseReason): Promise<boolean> {
291
+ if (!open.value || closing.value) return false
292
+ closing.value = true
293
+ pendingCloseReason = reason
294
+ try {
295
+ const allowed = await runRsDialogBeforeClose(props.beforeClose, reason)
296
+ if (!allowed) return false
297
+ open.value = false
298
+ await nextTick()
299
+ emit('afterClose', reason)
300
+ return true
301
+ } finally {
302
+ closing.value = false
303
+ }
304
+ }
305
+
306
+ /** DialogRoot 受控更新:打开直通;关闭统一走 beforeClose */
307
+ async function onUpdateOpen(next: boolean): Promise<void> {
308
+ if (next) {
309
+ open.value = true
310
+ return
311
+ }
312
+ await requestClose(pendingCloseReason)
313
+ pendingCloseReason = 'programmatic'
314
+ }
315
+
316
+ function onEscapeKeyDown(event: Event): void {
317
+ if (!props.closeOnEsc || props.confirmLoading) {
318
+ event.preventDefault()
319
+ return
320
+ }
321
+ pendingCloseReason = 'escape'
322
+ }
323
+
110
324
  /** 是否因外部交互关闭:与 modal 无关;非模态仍允许背后操作,仅控制是否 dismiss。 */
111
325
  function onPointerDownOutside(event: Event): void {
112
- if (!props.closeOnOverlayClick) event.preventDefault()
326
+ if (!props.closeOnOverlayClick || props.confirmLoading) {
327
+ event.preventDefault()
328
+ return
329
+ }
330
+ pendingCloseReason = 'overlay'
113
331
  }
114
332
 
115
333
  function onInteractOutside(event: Event): void {
116
- if (!props.closeOnOverlayClick) event.preventDefault()
334
+ if (!props.closeOnOverlayClick || props.confirmLoading) {
335
+ event.preventDefault()
336
+ return
337
+ }
338
+ pendingCloseReason = 'overlay'
339
+ }
340
+
341
+ async function onHeaderCloseClick(): Promise<void> {
342
+ await requestClose('close')
343
+ }
344
+
345
+ async function onBuiltinCancel(): Promise<void> {
346
+ emit('cancel')
347
+ await requestClose('cancel')
348
+ }
349
+
350
+ async function onBuiltinConfirm(): Promise<void> {
351
+ emit('confirm')
352
+ if (props.autoCloseOnConfirm) {
353
+ await requestClose('confirm')
354
+ }
117
355
  }
118
356
 
119
357
  const {
@@ -121,38 +359,61 @@ const {
121
359
  boundsTransitionEnabled,
122
360
  dialogStyle,
123
361
  resizeHandles,
362
+ setPanelEl,
124
363
  toggleFullscreen,
125
364
  onHeaderPointerDown,
126
365
  onResizePointerDown,
127
366
  } = useRsDialogWindow({
128
367
  open,
129
- widthPreset: toRef(props, 'width'),
368
+ widthPreset,
369
+ initialWidth: initialWidthPx,
130
370
  draggable: enableDraggable,
131
371
  resizable: enableResizable,
132
372
  compact: isConfirmLayout,
133
373
  boundsTransition: toRef(props, 'boundsTransition'),
134
374
  })
375
+
376
+ const contentRef = ref<{ $el?: HTMLElement } | HTMLElement | null>(null)
377
+
378
+ watch(
379
+ contentRef,
380
+ (inst) => {
381
+ if (!inst) {
382
+ setPanelEl(null)
383
+ return
384
+ }
385
+ const el = inst instanceof HTMLElement ? inst : (inst.$el ?? null)
386
+ setPanelEl(el instanceof HTMLElement ? el : null)
387
+ },
388
+ { flush: 'post' },
389
+ )
390
+
391
+ defineExpose({
392
+ /** 请求关闭(走 beforeClose) */
393
+ close: (reason: RsDialogCloseReason = 'programmatic') => requestClose(reason),
394
+ /** 打开对话框 */
395
+ openDialog: () => {
396
+ open.value = true
397
+ },
398
+ })
135
399
  </script>
136
400
 
137
401
  <template>
138
- <DialogRoot v-model:open="open" :modal="modal">
139
- <DialogPortal :to="teleportTo">
140
- <DialogOverlay v-if="showOverlay" class="rs-dialog__overlay rs-motion-reduce" />
402
+ <DialogRoot :open="open" :modal="modal" @update:open="onUpdateOpen">
403
+ <DialogPortal
404
+ :disabled="teleportTo === false"
405
+ :to="teleportTo === false ? undefined : teleportTo"
406
+ >
407
+ <DialogOverlay v-if="showOverlay" class="rs-dialog__overlay rs-motion-reduce" :style="overlayStyle" />
141
408
  <DialogContent
409
+ ref="contentRef"
410
+ v-bind="attrs"
142
411
  class="rs-dialog__content rs-motion-reduce"
143
- :class="[
144
- `rs-dialog__content--${width}`,
145
- `rs-dialog__content--${layout}`,
146
- `rs-dialog__content--tone-${tone}`,
147
- {
148
- 'rs-dialog__content--fullscreen': isFullscreen,
149
- 'rs-dialog__content--draggable': enableDraggable,
150
- 'rs-dialog__content--bounds-transition': boundsTransitionEnabled,
151
- },
152
- ]"
153
- :style="isWindowLayout ? dialogStyle : undefined"
412
+ :class="contentClass"
413
+ :style="contentStyle"
154
414
  @pointer-down-outside="onPointerDownOutside"
155
415
  @interact-outside="onInteractOutside"
416
+ @escape-key-down="onEscapeKeyDown"
156
417
  >
157
418
  <template v-if="enableResizable && !isFullscreen">
158
419
  <div
@@ -164,15 +425,19 @@ const {
164
425
  />
165
426
  </template>
166
427
  <header class="rs-dialog__header" @pointerdown="onHeaderPointerDown">
167
- <div class="rs-dialog__heading">
168
- <DialogTitle class="rs-dialog__title">{{ title }}</DialogTitle>
169
- <DialogDescription
170
- class="rs-dialog__description"
171
- :class="{ 'rs-dialog__description--sr-only': !description }"
172
- >
173
- {{ description || title }}
174
- </DialogDescription>
175
- </div>
428
+ <slot name="header">
429
+ <div class="rs-dialog__heading">
430
+ <DialogTitle class="rs-dialog__title">
431
+ <slot name="title">{{ resolvedTitle }}</slot>
432
+ </DialogTitle>
433
+ <DialogDescription
434
+ class="rs-dialog__description"
435
+ :class="{ 'rs-dialog__description--sr-only': !description && !$slots.description }"
436
+ >
437
+ <slot name="description">{{ description || resolvedTitle }}</slot>
438
+ </DialogDescription>
439
+ </div>
440
+ </slot>
176
441
  <div class="rs-dialog__actions">
177
442
  <RsButton
178
443
  v-if="fullscreenable && isWindowLayout"
@@ -189,8 +454,9 @@ const {
189
454
  size="sm"
190
455
  icon-only
191
456
  icon="x"
457
+ :disabled="confirmLoading"
192
458
  :tooltip="t('dialog.close')"
193
- @click="open = false"
459
+ @click="onHeaderCloseClick"
194
460
  />
195
461
  </div>
196
462
  </header>
@@ -200,8 +466,34 @@ const {
200
466
  <slot name="body-placeholder" />
201
467
  </div>
202
468
  </div>
203
- <footer v-if="$slots.footer" class="rs-dialog__footer">
204
- <slot name="footer" />
469
+ <footer v-if="$slots.footer || showBuiltinFooter" class="rs-dialog__footer">
470
+ <slot
471
+ name="footer"
472
+ :confirm-loading="confirmLoading"
473
+ :on-confirm="onBuiltinConfirm"
474
+ :on-cancel="onBuiltinCancel"
475
+ >
476
+ <template v-if="showBuiltinFooter">
477
+ <RsButton
478
+ v-if="showCancel"
479
+ variant="default"
480
+ size="sm"
481
+ :disabled="confirmLoading"
482
+ @click="onBuiltinCancel"
483
+ >
484
+ {{ cancelText ?? t('common.cancel') }}
485
+ </RsButton>
486
+ <RsButton
487
+ v-if="showConfirm"
488
+ :variant="confirmVariant"
489
+ size="sm"
490
+ :loading="confirmLoading"
491
+ @click="onBuiltinConfirm"
492
+ >
493
+ {{ confirmText ?? t('common.confirm') }}
494
+ </RsButton>
495
+ </template>
496
+ </slot>
205
497
  </footer>
206
498
  </DialogContent>
207
499
  </DialogPortal>
@@ -243,9 +535,10 @@ const {
243
535
  overflow: hidden;
244
536
  border-radius: var(--rs-radius-lg);
245
537
  border: 1px solid var(--rs-dialog-border);
246
- background: var(--rs-dialog-header-bg);
538
+ background: var(--rs-dialog-bg);
247
539
  box-shadow: var(--rs-dialog-shadow);
248
540
  outline: none;
541
+ color: var(--rs-dialog-title-fg);
249
542
  }
250
543
 
251
544
  /* 亮色 confirm:轻微 vibrancy;window 用实底避免 CEF 下 blur 合成开销 */
@@ -349,7 +642,8 @@ const {
349
642
  }
350
643
  .rs-dialog__content--window.rs-dialog__content--sm,
351
644
  .rs-dialog__content--window.rs-dialog__content--md,
352
- .rs-dialog__content--window.rs-dialog__content--lg {
645
+ .rs-dialog__content--window.rs-dialog__content--lg,
646
+ .rs-dialog__content--window.rs-dialog__content--custom-width {
353
647
  max-width: none;
354
648
  }
355
649
  .rs-dialog__content--fullscreen {
@@ -359,26 +653,40 @@ const {
359
653
  display: flex;
360
654
  align-items: center;
361
655
  justify-content: space-between;
362
- gap: 0.75rem;
656
+ gap: var(--rs-space-md);
363
657
  flex-shrink: 0;
364
- padding: 0.625rem 0.75rem 0.625rem 1.25rem;
658
+ box-sizing: border-box;
659
+ height: var(--rs-dialog-header-height);
660
+ min-height: var(--rs-dialog-header-min-height);
661
+ padding: var(--rs-dialog-header-padding-y) var(--rs-dialog-header-padding-x);
365
662
  background: var(--rs-dialog-header-bg);
366
663
  border-bottom: 1px solid var(--rs-dialog-separator);
367
- box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.05);
664
+ }
665
+ .rs-dialog__heading {
666
+ min-width: 0;
667
+ flex: 1;
668
+ display: flex;
669
+ flex-direction: column;
670
+ justify-content: center;
671
+ gap: 2px;
368
672
  }
369
673
  .rs-dialog__content--window.rs-dialog__content--draggable .rs-dialog__header {
370
- cursor: grab;
674
+ cursor: move;
371
675
  user-select: none;
372
676
  }
373
677
  .rs-dialog__title {
374
678
  margin: 0;
679
+ display: inline-flex;
680
+ align-items: center;
681
+ gap: var(--rs-space-sm);
375
682
  font-size: var(--rs-font-size-base);
376
683
  font-weight: 600;
377
- letter-spacing: -0.01em;
684
+ letter-spacing: -0.015em;
685
+ line-height: var(--rs-line-height-tight, 1.3);
378
686
  color: var(--rs-dialog-title-fg);
379
687
  }
380
688
  .rs-dialog__description {
381
- margin: 0.25rem 0 0;
689
+ margin: var(--rs-space-xs) 0 0;
382
690
  font-size: var(--rs-font-size-sm);
383
691
  color: var(--rs-dialog-description-fg);
384
692
  line-height: var(--rs-line-height-normal);
@@ -398,7 +706,8 @@ const {
398
706
  flex: 1;
399
707
  min-height: 0;
400
708
  overflow: auto;
401
- padding: 1.25rem;
709
+ box-sizing: border-box;
710
+ padding: var(--rs-dialog-body-padding-y) var(--rs-dialog-body-padding-x);
402
711
  background: var(--rs-dialog-body-bg);
403
712
  }
404
713
  .rs-dialog__content--window .rs-dialog__body {
@@ -411,20 +720,23 @@ const {
411
720
  }
412
721
  .rs-dialog__footer {
413
722
  display: flex;
723
+ align-items: center;
414
724
  justify-content: flex-end;
415
- gap: 0.5rem;
725
+ flex-wrap: wrap;
726
+ gap: var(--rs-space-sm);
416
727
  flex-shrink: 0;
417
- padding: 0.75rem 1.25rem;
728
+ box-sizing: border-box;
729
+ height: var(--rs-dialog-footer-height);
730
+ min-height: var(--rs-dialog-footer-min-height);
731
+ padding: var(--rs-dialog-footer-padding-y) var(--rs-dialog-footer-padding-x);
418
732
  background: var(--rs-dialog-footer-bg);
419
- border-top: 1px solid var(--rs-dialog-separator);
420
- }
421
- [data-rs-theme='light'] .rs-dialog__header {
422
- box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.75);
733
+ border-top: 1px solid var(--rs-dialog-footer-border);
423
734
  }
424
735
  .rs-dialog__actions {
425
736
  display: inline-flex;
426
737
  align-items: center;
427
738
  gap: var(--rs-space-xs);
739
+ flex-shrink: 0;
428
740
  }
429
741
  .rs-dialog__resize-handle {
430
742
  position: absolute;
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ withDefaults(
3
+ defineProps<{
4
+ orientation?: 'horizontal' | 'vertical'
5
+ dashed?: boolean
6
+ }>(),
7
+ {
8
+ orientation: 'horizontal',
9
+ dashed: false,
10
+ },
11
+ )
12
+ </script>
13
+
14
+ <template>
15
+ <div
16
+ class="rs-divider"
17
+ :class="[
18
+ `rs-divider--${orientation}`,
19
+ { 'rs-divider--dashed': dashed, 'rs-divider--with-label': $slots.default },
20
+ ]"
21
+ role="separator"
22
+ :aria-orientation="orientation"
23
+ >
24
+ <span v-if="$slots.default && orientation === 'horizontal'" class="rs-divider__label">
25
+ <slot />
26
+ </span>
27
+ </div>
28
+ </template>
29
+
30
+ <style scoped>
31
+ .rs-divider {
32
+ flex-shrink: 0;
33
+ box-sizing: border-box;
34
+ border: 0;
35
+ background: transparent;
36
+ }
37
+
38
+ .rs-divider--horizontal {
39
+ display: flex;
40
+ align-items: center;
41
+ width: 100%;
42
+ margin: var(--rs-space-md) 0;
43
+ }
44
+
45
+ .rs-divider--horizontal:not(.rs-divider--with-label)::before {
46
+ content: '';
47
+ display: block;
48
+ width: 100%;
49
+ border-top: 1px solid var(--rs-border);
50
+ }
51
+
52
+ .rs-divider--horizontal.rs-divider--dashed:not(.rs-divider--with-label)::before {
53
+ border-top-style: dashed;
54
+ }
55
+
56
+ .rs-divider--horizontal.rs-divider--with-label::before,
57
+ .rs-divider--horizontal.rs-divider--with-label::after {
58
+ content: '';
59
+ flex: 1 1 auto;
60
+ border-top: 1px solid var(--rs-border);
61
+ }
62
+
63
+ .rs-divider--horizontal.rs-divider--dashed.rs-divider--with-label::before,
64
+ .rs-divider--horizontal.rs-divider--dashed.rs-divider--with-label::after {
65
+ border-top-style: dashed;
66
+ }
67
+
68
+ .rs-divider__label {
69
+ flex-shrink: 0;
70
+ padding: 0 var(--rs-space-md);
71
+ font-size: var(--rs-font-size-xs);
72
+ color: var(--rs-muted);
73
+ white-space: nowrap;
74
+ }
75
+
76
+ .rs-divider--vertical {
77
+ display: inline-block;
78
+ align-self: stretch;
79
+ width: 0;
80
+ min-height: 1em;
81
+ margin: 0 var(--rs-space-md);
82
+ border-left: 1px solid var(--rs-border);
83
+ vertical-align: middle;
84
+ }
85
+
86
+ .rs-divider--vertical.rs-divider--dashed {
87
+ border-left-style: dashed;
88
+ }
89
+ </style>