niuma-ui 1.1.1 → 1.1.3
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.
- package/CHANGELOG.md +19 -1
- package/package.json +1 -1
- package/src/__tests__/RsDrawer.spec.ts +48 -0
- package/src/components/RsDrawer.vue +282 -3
- package/src/components/drawer-utils.ts +36 -0
- package/src/icons/lucide.ts +34 -20
- package/src/index.ts +4 -0
- package/src/locale/messages.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.1.3] - 2026-08-14
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- **RsDrawer 内边缘拖拽缩放**:默认 `resizable`(`size=full` 除外)。左右改宽、上下改高,可配 `minSize` / `maxSize`,支持键盘方向键。
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- **RsDrawer 贴边定位**:覆盖 Reka DialogContent 默认居中,避免抽屉被压成标题条。
|
|
18
|
+
|
|
19
|
+
## [1.1.2] - 2026-08-14
|
|
20
|
+
|
|
21
|
+
### 修复
|
|
22
|
+
|
|
23
|
+
- **RsIcon 在 pnpm/npm 宿主中为空**:不再用 `import.meta.glob` 扫 `node_modules/@lucide/vue`(该路径在 pnpm 虚拟 store 下不存在)。改为 `import * as LucideVue from '@lucide/vue'`,走正常包解析,宿主安装 `niuma-ui` 即可使用图标。
|
|
24
|
+
|
|
9
25
|
## [1.1.1] - 2026-08-14
|
|
10
26
|
|
|
11
27
|
### 修复
|
|
@@ -106,7 +122,9 @@
|
|
|
106
122
|
|
|
107
123
|
- 1.0 之前的私有 tag(如 `v0.1.0`)仅作历史记录;新接入请依赖 `v1.0.0` 及之后版本。
|
|
108
124
|
|
|
109
|
-
[Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.
|
|
125
|
+
[Unreleased]: https://github.com/Blair-Shang/niuma-ui/compare/v1.1.3...HEAD
|
|
126
|
+
[1.1.3]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.3
|
|
127
|
+
[1.1.2]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.2
|
|
110
128
|
[1.1.1]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.1
|
|
111
129
|
[1.1.0]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.1.0
|
|
112
130
|
[1.0.2]: https://github.com/Blair-Shang/niuma-ui/releases/tag/v1.0.2
|
package/package.json
CHANGED
|
@@ -235,4 +235,52 @@ describe('RsDrawer', () => {
|
|
|
235
235
|
expect(closeBtn.querySelector('.rs-btn__tooltip')?.textContent).toContain('Close')
|
|
236
236
|
wrapper.unmount()
|
|
237
237
|
})
|
|
238
|
+
|
|
239
|
+
it('renders an inner-edge resize handle by default', async () => {
|
|
240
|
+
const wrapper = mount(RsDrawer, {
|
|
241
|
+
props: { open: true, title: '帮助' },
|
|
242
|
+
attachTo: document.body,
|
|
243
|
+
})
|
|
244
|
+
await flushPromises()
|
|
245
|
+
const handle = document.body.querySelector('.rs-drawer__resize') as HTMLElement | null
|
|
246
|
+
expect(handle).toBeTruthy()
|
|
247
|
+
expect(handle?.getAttribute('aria-orientation')).toBe('vertical')
|
|
248
|
+
wrapper.unmount()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('hides the resize handle when size is full or resizable is false', async () => {
|
|
252
|
+
const full = mount(RsDrawer, {
|
|
253
|
+
props: { open: true, title: '全屏', size: 'full' },
|
|
254
|
+
attachTo: document.body,
|
|
255
|
+
})
|
|
256
|
+
await flushPromises()
|
|
257
|
+
expect(document.body.querySelector('.rs-drawer__resize')).toBeNull()
|
|
258
|
+
full.unmount()
|
|
259
|
+
|
|
260
|
+
const locked = mount(RsDrawer, {
|
|
261
|
+
props: { open: true, title: '固定', resizable: false },
|
|
262
|
+
attachTo: document.body,
|
|
263
|
+
})
|
|
264
|
+
await flushPromises()
|
|
265
|
+
expect(document.body.querySelector('.rs-drawer__resize')).toBeNull()
|
|
266
|
+
locked.unmount()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('widens a right drawer with ArrowLeft on the resize handle', async () => {
|
|
270
|
+
const wrapper = mount(RsDrawer, {
|
|
271
|
+
props: { open: true, title: '缩放', width: 360 },
|
|
272
|
+
attachTo: document.body,
|
|
273
|
+
})
|
|
274
|
+
await flushPromises()
|
|
275
|
+
const content = document.body.querySelector('.rs-drawer__content') as HTMLElement
|
|
276
|
+
Object.defineProperty(content, 'getBoundingClientRect', {
|
|
277
|
+
configurable: true,
|
|
278
|
+
value: () => ({ width: 360, height: 800, top: 0, left: 800, right: 1160, bottom: 800, x: 800, y: 0, toJSON: () => ({}) }),
|
|
279
|
+
})
|
|
280
|
+
const handle = document.body.querySelector('.rs-drawer__resize') as HTMLElement
|
|
281
|
+
await handle.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
|
|
282
|
+
await flushPromises()
|
|
283
|
+
expect(content.style.width).toBe('376px')
|
|
284
|
+
wrapper.unmount()
|
|
285
|
+
})
|
|
238
286
|
})
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
resolveDrawerOverlayStyle,
|
|
15
15
|
resolveRsDrawerDimensionCss,
|
|
16
16
|
resolveRsDrawerSizeCss,
|
|
17
|
+
resolveRsDrawerSizePx,
|
|
18
|
+
clampRsDrawerSize,
|
|
19
|
+
RS_DRAWER_MIN_SIZE_PX,
|
|
20
|
+
RS_DRAWER_MAX_VIEWPORT_RATIO,
|
|
17
21
|
runRsDrawerBeforeClose,
|
|
18
22
|
type RsDrawerBeforeClose,
|
|
19
23
|
type RsDrawerCloseReason,
|
|
@@ -38,6 +42,15 @@ const props = withDefaults(
|
|
|
38
42
|
width?: RsDrawerDimension
|
|
39
43
|
/** 上下抽屉自定义高度(覆盖 size) */
|
|
40
44
|
height?: RsDrawerDimension
|
|
45
|
+
/**
|
|
46
|
+
* 内边缘拖拽改尺寸。full 始终不可拖。
|
|
47
|
+
* 控制台 / 帮助栏等侧栏场景建议保持开启。
|
|
48
|
+
*/
|
|
49
|
+
resizable?: boolean
|
|
50
|
+
/** 可拖下限(px / CSS);默认 256px */
|
|
51
|
+
minSize?: RsDrawerDimension
|
|
52
|
+
/** 可拖上限(px / CSS);默认视口 90% */
|
|
53
|
+
maxSize?: RsDrawerDimension
|
|
41
54
|
/**
|
|
42
55
|
* 是否模态:锁焦点并拦截背后交互。
|
|
43
56
|
* 未显式传入时跟随 showOverlay(有遮罩=模态,无遮罩=非模态)。
|
|
@@ -78,6 +91,7 @@ const props = withDefaults(
|
|
|
78
91
|
showClose: true,
|
|
79
92
|
closeOnOverlayClick: true,
|
|
80
93
|
closeOnEsc: true,
|
|
94
|
+
resizable: true,
|
|
81
95
|
},
|
|
82
96
|
)
|
|
83
97
|
|
|
@@ -85,6 +99,10 @@ const emit = defineEmits<{
|
|
|
85
99
|
openChange: [open: boolean]
|
|
86
100
|
afterOpen: []
|
|
87
101
|
afterClose: [reason: RsDrawerCloseReason]
|
|
102
|
+
/** 拖拽过程与结束都会抛出当前像素尺寸 */
|
|
103
|
+
resize: [size: number]
|
|
104
|
+
'update:width': [value: number]
|
|
105
|
+
'update:height': [value: number]
|
|
88
106
|
}>()
|
|
89
107
|
|
|
90
108
|
const { t } = useRsI18n()
|
|
@@ -102,8 +120,15 @@ const overlayStyle = computed(() =>
|
|
|
102
120
|
)
|
|
103
121
|
|
|
104
122
|
const isHorizontal = computed(() => props.side === 'left' || props.side === 'right')
|
|
123
|
+
const enableResizable = computed(() => props.resizable && props.size !== 'full')
|
|
124
|
+
|
|
125
|
+
/** 本次打开后拖拽得到的像素尺寸;关闭时清空,回到 size/width/height */
|
|
126
|
+
const liveSizePx = ref<number | undefined>(undefined)
|
|
127
|
+
const resizing = ref(false)
|
|
128
|
+
const contentRef = ref<{ $el?: HTMLElement } | HTMLElement | null>(null)
|
|
105
129
|
|
|
106
130
|
const panelSizeCss = computed(() => {
|
|
131
|
+
if (liveSizePx.value != null) return `${liveSizePx.value}px`
|
|
107
132
|
if (isHorizontal.value) {
|
|
108
133
|
return (
|
|
109
134
|
resolveRsDrawerDimensionCss(props.width) ??
|
|
@@ -119,9 +144,50 @@ const panelSizeCss = computed(() => {
|
|
|
119
144
|
})
|
|
120
145
|
|
|
121
146
|
const contentStyle = computed(() => {
|
|
147
|
+
const style: Record<string, string> = {}
|
|
122
148
|
const size = panelSizeCss.value
|
|
123
|
-
if (
|
|
124
|
-
|
|
149
|
+
if (size && props.size !== 'full') {
|
|
150
|
+
style['--rs-drawer-panel-size'] = size
|
|
151
|
+
}
|
|
152
|
+
if (liveSizePx.value != null && props.size !== 'full') {
|
|
153
|
+
if (isHorizontal.value) style.width = `${liveSizePx.value}px`
|
|
154
|
+
else style.height = `${liveSizePx.value}px`
|
|
155
|
+
}
|
|
156
|
+
// Reka DialogContent 默认居中;抽屉必须钉在对应边缘,否则只剩一条标题。
|
|
157
|
+
if (props.side === 'right') {
|
|
158
|
+
style.left = 'auto'
|
|
159
|
+
style.right = '0'
|
|
160
|
+
style.top = '0'
|
|
161
|
+
style.bottom = '0'
|
|
162
|
+
style.height = '100%'
|
|
163
|
+
style.maxHeight = 'none'
|
|
164
|
+
style.transform = 'none'
|
|
165
|
+
} else if (props.side === 'left') {
|
|
166
|
+
style.left = '0'
|
|
167
|
+
style.right = 'auto'
|
|
168
|
+
style.top = '0'
|
|
169
|
+
style.bottom = '0'
|
|
170
|
+
style.height = '100%'
|
|
171
|
+
style.maxHeight = 'none'
|
|
172
|
+
style.transform = 'none'
|
|
173
|
+
} else if (props.side === 'top') {
|
|
174
|
+
style.left = '0'
|
|
175
|
+
style.right = '0'
|
|
176
|
+
style.top = '0'
|
|
177
|
+
style.bottom = 'auto'
|
|
178
|
+
style.width = '100%'
|
|
179
|
+
style.maxWidth = 'none'
|
|
180
|
+
style.transform = 'none'
|
|
181
|
+
} else if (props.side === 'bottom') {
|
|
182
|
+
style.left = '0'
|
|
183
|
+
style.right = '0'
|
|
184
|
+
style.top = 'auto'
|
|
185
|
+
style.bottom = '0'
|
|
186
|
+
style.width = '100%'
|
|
187
|
+
style.maxWidth = 'none'
|
|
188
|
+
style.transform = 'none'
|
|
189
|
+
}
|
|
190
|
+
return Object.keys(style).length ? style : undefined
|
|
125
191
|
})
|
|
126
192
|
|
|
127
193
|
const contentClass = computed(() => [
|
|
@@ -130,6 +196,8 @@ const contentClass = computed(() => [
|
|
|
130
196
|
{
|
|
131
197
|
'rs-drawer__content--contained': contained.value,
|
|
132
198
|
'rs-drawer__content--custom-size': Boolean(panelSizeCss.value) && props.size !== 'full',
|
|
199
|
+
'rs-drawer__content--resizable': enableResizable.value,
|
|
200
|
+
'rs-drawer__content--resizing': resizing.value,
|
|
133
201
|
},
|
|
134
202
|
])
|
|
135
203
|
|
|
@@ -238,6 +306,8 @@ watch(open, (isOpen, wasOpen) => {
|
|
|
238
306
|
return
|
|
239
307
|
}
|
|
240
308
|
if (!isOpen && wasOpen) {
|
|
309
|
+
liveSizePx.value = undefined
|
|
310
|
+
stopResize()
|
|
241
311
|
releaseOutsideSuppress()
|
|
242
312
|
clearAfterOpenTimer()
|
|
243
313
|
emit('openChange', false)
|
|
@@ -246,6 +316,7 @@ watch(open, (isOpen, wasOpen) => {
|
|
|
246
316
|
})
|
|
247
317
|
|
|
248
318
|
onBeforeUnmount(() => {
|
|
319
|
+
stopResize()
|
|
249
320
|
releaseOutsideSuppress()
|
|
250
321
|
clearAfterOpenTimer()
|
|
251
322
|
})
|
|
@@ -287,7 +358,8 @@ function onEscapeKeyDown(event: Event): void {
|
|
|
287
358
|
function shouldBlockOutsideDismiss(): boolean {
|
|
288
359
|
return (
|
|
289
360
|
!props.closeOnOverlayClick ||
|
|
290
|
-
suppressOutsideUntilGestureEnd.value
|
|
361
|
+
suppressOutsideUntilGestureEnd.value ||
|
|
362
|
+
resizing.value
|
|
291
363
|
)
|
|
292
364
|
}
|
|
293
365
|
|
|
@@ -316,6 +388,136 @@ async function onHeaderCloseClick(): Promise<void> {
|
|
|
316
388
|
await requestClose('close')
|
|
317
389
|
}
|
|
318
390
|
|
|
391
|
+
function resolveContentEl(): HTMLElement | null {
|
|
392
|
+
const inst = contentRef.value
|
|
393
|
+
if (!inst) return null
|
|
394
|
+
if (inst instanceof HTMLElement) return inst
|
|
395
|
+
const el = inst.$el
|
|
396
|
+
return el instanceof HTMLElement ? el : null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function rootFontPx(): number {
|
|
400
|
+
const raw = getComputedStyle(document.documentElement).fontSize
|
|
401
|
+
const n = Number.parseFloat(raw)
|
|
402
|
+
return Number.isFinite(n) && n > 0 ? n : 16
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function viewportPx(): number {
|
|
406
|
+
return isHorizontal.value ? window.innerWidth : window.innerHeight
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function sizeBounds(): { min: number; max: number } {
|
|
410
|
+
const vp = viewportPx()
|
|
411
|
+
const root = rootFontPx()
|
|
412
|
+
const min = resolveRsDrawerSizePx(props.minSize, RS_DRAWER_MIN_SIZE_PX, root, vp)
|
|
413
|
+
const max = resolveRsDrawerSizePx(
|
|
414
|
+
props.maxSize,
|
|
415
|
+
Math.round(vp * RS_DRAWER_MAX_VIEWPORT_RATIO),
|
|
416
|
+
root,
|
|
417
|
+
vp,
|
|
418
|
+
)
|
|
419
|
+
return { min, max }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function applyLiveSize(px: number): number {
|
|
423
|
+
const { min, max } = sizeBounds()
|
|
424
|
+
const next = clampRsDrawerSize(px, min, max)
|
|
425
|
+
const el = resolveContentEl()
|
|
426
|
+
if (el) {
|
|
427
|
+
el.style.setProperty('--rs-drawer-panel-size', `${next}px`)
|
|
428
|
+
if (isHorizontal.value) el.style.width = `${next}px`
|
|
429
|
+
else el.style.height = `${next}px`
|
|
430
|
+
}
|
|
431
|
+
return next
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
let resizePointerId: number | null = null
|
|
435
|
+
let resizeStartClient = 0
|
|
436
|
+
let resizeStartSize = 0
|
|
437
|
+
|
|
438
|
+
function stopResize(): void {
|
|
439
|
+
if (resizePointerId != null) {
|
|
440
|
+
window.removeEventListener('pointermove', onResizePointerMove)
|
|
441
|
+
window.removeEventListener('pointerup', onResizePointerUp)
|
|
442
|
+
window.removeEventListener('pointercancel', onResizePointerUp)
|
|
443
|
+
resizePointerId = null
|
|
444
|
+
}
|
|
445
|
+
resizing.value = false
|
|
446
|
+
document.body.style.removeProperty('cursor')
|
|
447
|
+
document.body.style.removeProperty('user-select')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function currentPanelPx(): number {
|
|
451
|
+
const el = resolveContentEl()
|
|
452
|
+
if (!el) return RS_DRAWER_MIN_SIZE_PX
|
|
453
|
+
const rect = el.getBoundingClientRect()
|
|
454
|
+
return isHorizontal.value ? rect.width : rect.height
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function onResizePointerDown(event: PointerEvent): void {
|
|
458
|
+
if (!enableResizable.value || event.button !== 0) return
|
|
459
|
+
event.preventDefault()
|
|
460
|
+
event.stopPropagation()
|
|
461
|
+
resizeStartClient = isHorizontal.value ? event.clientX : event.clientY
|
|
462
|
+
resizeStartSize = currentPanelPx()
|
|
463
|
+
resizePointerId = event.pointerId
|
|
464
|
+
resizing.value = true
|
|
465
|
+
document.body.style.cursor = isHorizontal.value ? 'ew-resize' : 'ns-resize'
|
|
466
|
+
document.body.style.userSelect = 'none'
|
|
467
|
+
window.addEventListener('pointermove', onResizePointerMove)
|
|
468
|
+
window.addEventListener('pointerup', onResizePointerUp)
|
|
469
|
+
window.addEventListener('pointercancel', onResizePointerUp)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function deltaForPointer(event: PointerEvent): number {
|
|
473
|
+
const client = isHorizontal.value ? event.clientX : event.clientY
|
|
474
|
+
const raw = client - resizeStartClient
|
|
475
|
+
if (props.side === 'right' || props.side === 'bottom') return -raw
|
|
476
|
+
return raw
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function onResizePointerMove(event: PointerEvent): void {
|
|
480
|
+
if (resizePointerId == null || event.pointerId !== resizePointerId) return
|
|
481
|
+
const next = applyLiveSize(resizeStartSize + deltaForPointer(event))
|
|
482
|
+
emit('resize', next)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function commitResize(next: number): void {
|
|
486
|
+
liveSizePx.value = next
|
|
487
|
+
emit('resize', next)
|
|
488
|
+
if (isHorizontal.value) emit('update:width', next)
|
|
489
|
+
else emit('update:height', next)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function onResizePointerUp(event: PointerEvent): void {
|
|
493
|
+
if (resizePointerId == null || event.pointerId !== resizePointerId) return
|
|
494
|
+
const next = applyLiveSize(resizeStartSize + deltaForPointer(event))
|
|
495
|
+
stopResize()
|
|
496
|
+
commitResize(next)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function onResizeKeydown(event: KeyboardEvent): void {
|
|
500
|
+
if (!enableResizable.value) return
|
|
501
|
+
const step = event.shiftKey ? 48 : 16
|
|
502
|
+
let delta = 0
|
|
503
|
+
if (props.side === 'right') {
|
|
504
|
+
if (event.key === 'ArrowLeft') delta = step
|
|
505
|
+
else if (event.key === 'ArrowRight') delta = -step
|
|
506
|
+
} else if (props.side === 'left') {
|
|
507
|
+
if (event.key === 'ArrowRight') delta = step
|
|
508
|
+
else if (event.key === 'ArrowLeft') delta = -step
|
|
509
|
+
} else if (props.side === 'top') {
|
|
510
|
+
if (event.key === 'ArrowDown') delta = step
|
|
511
|
+
else if (event.key === 'ArrowUp') delta = -step
|
|
512
|
+
} else if (props.side === 'bottom') {
|
|
513
|
+
if (event.key === 'ArrowUp') delta = step
|
|
514
|
+
else if (event.key === 'ArrowDown') delta = -step
|
|
515
|
+
}
|
|
516
|
+
if (!delta) return
|
|
517
|
+
event.preventDefault()
|
|
518
|
+
commitResize(applyLiveSize(currentPanelPx() + delta))
|
|
519
|
+
}
|
|
520
|
+
|
|
319
521
|
defineExpose({
|
|
320
522
|
/** 请求关闭(走 beforeClose) */
|
|
321
523
|
close: (reason: RsDrawerCloseReason = 'programmatic') => requestClose(reason),
|
|
@@ -339,6 +541,7 @@ defineExpose({
|
|
|
339
541
|
:style="overlayStyle"
|
|
340
542
|
/>
|
|
341
543
|
<DialogContent
|
|
544
|
+
ref="contentRef"
|
|
342
545
|
class="rs-drawer__content rs-motion-reduce"
|
|
343
546
|
:class="contentClass"
|
|
344
547
|
:style="contentStyle"
|
|
@@ -347,6 +550,16 @@ defineExpose({
|
|
|
347
550
|
@focus-outside="onFocusOutside"
|
|
348
551
|
@escape-key-down="onEscapeKeyDown"
|
|
349
552
|
>
|
|
553
|
+
<button
|
|
554
|
+
v-if="enableResizable"
|
|
555
|
+
type="button"
|
|
556
|
+
class="rs-drawer__resize"
|
|
557
|
+
:aria-label="t('drawer.resize')"
|
|
558
|
+
:aria-orientation="isHorizontal ? 'vertical' : 'horizontal'"
|
|
559
|
+
:aria-valuenow="liveSizePx != null ? Math.round(liveSizePx) : undefined"
|
|
560
|
+
@pointerdown="onResizePointerDown"
|
|
561
|
+
@keydown="onResizeKeydown"
|
|
562
|
+
/>
|
|
350
563
|
<header v-if="title || description || showClose || $slots.header" class="rs-drawer__header">
|
|
351
564
|
<slot name="header">
|
|
352
565
|
<div class="rs-drawer__heading">
|
|
@@ -426,6 +639,67 @@ defineExpose({
|
|
|
426
639
|
outline: none;
|
|
427
640
|
will-change: transform;
|
|
428
641
|
}
|
|
642
|
+
.rs-drawer__resize {
|
|
643
|
+
position: absolute;
|
|
644
|
+
z-index: 3;
|
|
645
|
+
padding: 0;
|
|
646
|
+
border: 0;
|
|
647
|
+
background: transparent;
|
|
648
|
+
appearance: none;
|
|
649
|
+
}
|
|
650
|
+
.rs-drawer__resize:focus-visible {
|
|
651
|
+
outline: 2px solid var(--rs-primary);
|
|
652
|
+
outline-offset: -2px;
|
|
653
|
+
}
|
|
654
|
+
.rs-drawer__content--right > .rs-drawer__resize,
|
|
655
|
+
.rs-drawer__content--left > .rs-drawer__resize {
|
|
656
|
+
top: 0;
|
|
657
|
+
bottom: 0;
|
|
658
|
+
width: 6px;
|
|
659
|
+
cursor: ew-resize;
|
|
660
|
+
}
|
|
661
|
+
.rs-drawer__content--right > .rs-drawer__resize {
|
|
662
|
+
left: 0;
|
|
663
|
+
}
|
|
664
|
+
.rs-drawer__content--left > .rs-drawer__resize {
|
|
665
|
+
right: 0;
|
|
666
|
+
}
|
|
667
|
+
.rs-drawer__content--top > .rs-drawer__resize,
|
|
668
|
+
.rs-drawer__content--bottom > .rs-drawer__resize {
|
|
669
|
+
left: 0;
|
|
670
|
+
right: 0;
|
|
671
|
+
height: 6px;
|
|
672
|
+
cursor: ns-resize;
|
|
673
|
+
}
|
|
674
|
+
.rs-drawer__content--top > .rs-drawer__resize {
|
|
675
|
+
bottom: 0;
|
|
676
|
+
}
|
|
677
|
+
.rs-drawer__content--bottom > .rs-drawer__resize {
|
|
678
|
+
top: 0;
|
|
679
|
+
}
|
|
680
|
+
.rs-drawer__content--right > .rs-drawer__resize:hover,
|
|
681
|
+
.rs-drawer__content--right > .rs-drawer__resize:focus-visible,
|
|
682
|
+
.rs-drawer__content--resizing.rs-drawer__content--right > .rs-drawer__resize {
|
|
683
|
+
background: linear-gradient(to right, var(--rs-primary) 0, var(--rs-primary) 2px, transparent 2px);
|
|
684
|
+
}
|
|
685
|
+
.rs-drawer__content--left > .rs-drawer__resize:hover,
|
|
686
|
+
.rs-drawer__content--left > .rs-drawer__resize:focus-visible,
|
|
687
|
+
.rs-drawer__content--resizing.rs-drawer__content--left > .rs-drawer__resize {
|
|
688
|
+
background: linear-gradient(to left, var(--rs-primary) 0, var(--rs-primary) 2px, transparent 2px);
|
|
689
|
+
}
|
|
690
|
+
.rs-drawer__content--top > .rs-drawer__resize:hover,
|
|
691
|
+
.rs-drawer__content--top > .rs-drawer__resize:focus-visible,
|
|
692
|
+
.rs-drawer__content--resizing.rs-drawer__content--top > .rs-drawer__resize {
|
|
693
|
+
background: linear-gradient(to top, var(--rs-primary) 0, var(--rs-primary) 2px, transparent 2px);
|
|
694
|
+
}
|
|
695
|
+
.rs-drawer__content--bottom > .rs-drawer__resize:hover,
|
|
696
|
+
.rs-drawer__content--bottom > .rs-drawer__resize:focus-visible,
|
|
697
|
+
.rs-drawer__content--resizing.rs-drawer__content--bottom > .rs-drawer__resize {
|
|
698
|
+
background: linear-gradient(to bottom, var(--rs-primary) 0, var(--rs-primary) 2px, transparent 2px);
|
|
699
|
+
}
|
|
700
|
+
.rs-drawer__content--resizing iframe {
|
|
701
|
+
pointer-events: none;
|
|
702
|
+
}
|
|
429
703
|
.rs-drawer__content--contained {
|
|
430
704
|
position: absolute;
|
|
431
705
|
}
|
|
@@ -433,18 +707,23 @@ defineExpose({
|
|
|
433
707
|
.rs-drawer__content--left {
|
|
434
708
|
top: 0;
|
|
435
709
|
bottom: 0;
|
|
710
|
+
height: 100%;
|
|
711
|
+
max-height: none;
|
|
436
712
|
width: min(100vw, var(--rs-drawer-panel-size, 28rem));
|
|
713
|
+
transform: none;
|
|
437
714
|
}
|
|
438
715
|
.rs-drawer__content--contained:is(.rs-drawer__content--right, .rs-drawer__content--left) {
|
|
439
716
|
width: min(100%, var(--rs-drawer-panel-size, 28rem));
|
|
440
717
|
}
|
|
441
718
|
.rs-drawer__content--right {
|
|
719
|
+
left: auto;
|
|
442
720
|
right: 0;
|
|
443
721
|
border-right: 0;
|
|
444
722
|
border-radius: var(--rs-radius) 0 0 var(--rs-radius);
|
|
445
723
|
}
|
|
446
724
|
.rs-drawer__content--left {
|
|
447
725
|
left: 0;
|
|
726
|
+
right: auto;
|
|
448
727
|
border-left: 0;
|
|
449
728
|
border-radius: 0 var(--rs-radius) var(--rs-radius) 0;
|
|
450
729
|
}
|
|
@@ -29,6 +29,11 @@ const SIZE_INLINE: Record<Exclude<RsDrawerSize, 'full'>, string> = {
|
|
|
29
29
|
lg: '36rem',
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** 左右抽屉默认可拖最小宽度 */
|
|
33
|
+
export const RS_DRAWER_MIN_SIZE_PX = 256
|
|
34
|
+
/** 相对视口的默认可拖上限比例 */
|
|
35
|
+
export const RS_DRAWER_MAX_VIEWPORT_RATIO = 0.9
|
|
36
|
+
|
|
32
37
|
/** 将 size 预设解析为 CSS 长度(full 由样式类处理) */
|
|
33
38
|
export function resolveRsDrawerSizeCss(size: RsDrawerSize): string | undefined {
|
|
34
39
|
if (size === 'full') return undefined
|
|
@@ -46,6 +51,37 @@ export function resolveRsDrawerDimensionCss(value: RsDrawerDimension | undefined
|
|
|
46
51
|
return trimmed || undefined
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
/**
|
|
55
|
+
* 将抽屉尺寸表达式解析为像素,供拖拽缩放夹紧。
|
|
56
|
+
* 无法解析时返回 fallbackPx。
|
|
57
|
+
*/
|
|
58
|
+
export function resolveRsDrawerSizePx(
|
|
59
|
+
value: RsDrawerDimension | undefined,
|
|
60
|
+
fallbackPx: number,
|
|
61
|
+
rootFontPx: number,
|
|
62
|
+
viewportPx: number,
|
|
63
|
+
): number {
|
|
64
|
+
if (value == null) return fallbackPx
|
|
65
|
+
if (typeof value === 'number') {
|
|
66
|
+
return Number.isFinite(value) && value > 0 ? value : fallbackPx
|
|
67
|
+
}
|
|
68
|
+
const trimmed = String(value).trim()
|
|
69
|
+
const n = Number.parseFloat(trimmed)
|
|
70
|
+
if (!Number.isFinite(n)) return fallbackPx
|
|
71
|
+
if (trimmed.endsWith('rem')) return n * rootFontPx
|
|
72
|
+
if (trimmed.endsWith('%') || trimmed.endsWith('vw') || trimmed.endsWith('vh')) {
|
|
73
|
+
return (n / 100) * viewportPx
|
|
74
|
+
}
|
|
75
|
+
return n
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 将拖拽得到的尺寸夹在 min/max 之间并取整。 */
|
|
79
|
+
export function clampRsDrawerSize(px: number, minPx: number, maxPx: number): number {
|
|
80
|
+
const lo = Math.min(minPx, maxPx)
|
|
81
|
+
const hi = Math.max(minPx, maxPx)
|
|
82
|
+
return Math.round(Math.min(hi, Math.max(lo, px)))
|
|
83
|
+
}
|
|
84
|
+
|
|
49
85
|
export function runRsDrawerBeforeClose(
|
|
50
86
|
beforeClose: RsDrawerBeforeClose | undefined,
|
|
51
87
|
reason: RsDrawerCloseReason,
|
package/src/icons/lucide.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Component } from 'vue'
|
|
2
|
+
import * as LucideVue from '@lucide/vue'
|
|
2
3
|
import { ApiIcon } from './custom/api'
|
|
3
4
|
import { FtpIcon } from './custom/ftp'
|
|
4
5
|
import { MongodbIcon } from './custom/mongodb'
|
|
@@ -32,36 +33,45 @@ const customIconMap = new Map<string, Component>([
|
|
|
32
33
|
['sqlserver', SqlserverIcon],
|
|
33
34
|
])
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
'../../node_modules/@lucide/vue/dist/esm/icons/*.mjs',
|
|
38
|
-
{ eager: true },
|
|
39
|
-
)
|
|
36
|
+
const lucideExports = LucideVue as Record<string, unknown>
|
|
37
|
+
const SKIP_LUCIDE_EXPORTS = new Set(['default', 'createLucideIcon'])
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
function isVueComponent(value: unknown): value is Component {
|
|
40
|
+
return typeof value === 'function' || (typeof value === 'object' && value !== null)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** RsIcon 使用 kebab-case(如 trash-2),@lucide/vue 导出名是 PascalCase(Trash2) */
|
|
44
|
+
function kebabToPascal(name: string): string {
|
|
45
|
+
return name
|
|
46
|
+
.split('-')
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
49
|
+
.join('')
|
|
50
|
+
}
|
|
43
51
|
|
|
44
|
-
|
|
52
|
+
function getLucideComponent(name: string): Component | undefined {
|
|
53
|
+
const pascal = kebabToPascal(name)
|
|
54
|
+
if (SKIP_LUCIDE_EXPORTS.has(pascal)) return undefined
|
|
55
|
+
const value = lucideExports[pascal]
|
|
56
|
+
return isVueComponent(value) ? value : undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Lucide 更名兼容:旧 kebab 名 → 当前导出名对应的 kebab */
|
|
45
60
|
const iconAliases = new Map<string, string>([
|
|
46
61
|
['code-2', 'code-xml'],
|
|
47
|
-
// Lucide v0.5xx:circle-help / help-circle → circle-question-mark
|
|
48
62
|
['circle-help', 'circle-question-mark'],
|
|
49
63
|
['help-circle', 'circle-question-mark'],
|
|
50
64
|
])
|
|
51
65
|
|
|
52
|
-
for (const [path, mod] of Object.entries(iconModules)) {
|
|
53
|
-
const match = iconPathPattern.exec(path)
|
|
54
|
-
if (match) iconMap.set(match[1], mod.default)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const iconCache = new Map<string, Component>()
|
|
58
|
-
|
|
59
66
|
function resolveIconName(name: string): string {
|
|
60
67
|
return iconAliases.get(name) ?? name
|
|
61
68
|
}
|
|
62
69
|
|
|
70
|
+
const iconCache = new Map<string, Component>()
|
|
71
|
+
|
|
63
72
|
export function isRsIconName(name: string): boolean {
|
|
64
|
-
|
|
73
|
+
const resolvedName = resolveIconName(name)
|
|
74
|
+
return customIconMap.has(resolvedName) || getLucideComponent(resolvedName) !== undefined
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
export function resolveLucideIcon(name: string): Component | undefined {
|
|
@@ -69,10 +79,14 @@ export function resolveLucideIcon(name: string): Component | undefined {
|
|
|
69
79
|
const cached = iconCache.get(resolvedName)
|
|
70
80
|
if (cached) return cached
|
|
71
81
|
|
|
72
|
-
const icon =
|
|
82
|
+
const icon = customIconMap.get(resolvedName) ?? getLucideComponent(resolvedName)
|
|
73
83
|
if (icon) iconCache.set(resolvedName, icon)
|
|
74
84
|
return icon
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
/** Lucide
|
|
78
|
-
export const lucideIconCount =
|
|
87
|
+
/** 可解析的图标数量(自定义 + Lucide 命名导出) */
|
|
88
|
+
export const lucideIconCount =
|
|
89
|
+
customIconMap.size +
|
|
90
|
+
Object.keys(lucideExports).filter(
|
|
91
|
+
(key) => !SKIP_LUCIDE_EXPORTS.has(key) && isVueComponent(lucideExports[key]),
|
|
92
|
+
).length
|
package/src/index.ts
CHANGED
|
@@ -103,9 +103,13 @@ export type {
|
|
|
103
103
|
RsDrawerSize,
|
|
104
104
|
} from './components/drawer-utils'
|
|
105
105
|
export {
|
|
106
|
+
clampRsDrawerSize,
|
|
106
107
|
resolveDrawerOverlayStyle,
|
|
107
108
|
resolveRsDrawerDimensionCss,
|
|
108
109
|
resolveRsDrawerSizeCss,
|
|
110
|
+
resolveRsDrawerSizePx,
|
|
111
|
+
RS_DRAWER_MAX_VIEWPORT_RATIO,
|
|
112
|
+
RS_DRAWER_MIN_SIZE_PX,
|
|
109
113
|
runRsDrawerBeforeClose,
|
|
110
114
|
} from './components/drawer-utils'
|
|
111
115
|
export { default as RsForm } from './components/RsForm.vue'
|
package/src/locale/messages.ts
CHANGED
|
@@ -138,6 +138,7 @@ export const zhCN: RsLocaleMessages = {
|
|
|
138
138
|
'tree.drag': '拖拽排序',
|
|
139
139
|
'split.label': '面板分割',
|
|
140
140
|
'split.resize': '拖动调整面板尺寸',
|
|
141
|
+
'drawer.resize': '拖动调整抽屉尺寸',
|
|
141
142
|
'terminal.copy': '复制',
|
|
142
143
|
'terminal.paste': '粘贴',
|
|
143
144
|
'terminal.selectAll': '全选',
|
|
@@ -336,6 +337,7 @@ export const enUS: RsLocaleMessages = {
|
|
|
336
337
|
'tree.drag': 'Drag to reorder',
|
|
337
338
|
'split.label': 'Split panes',
|
|
338
339
|
'split.resize': 'Drag to resize panes',
|
|
340
|
+
'drawer.resize': 'Drag to resize the drawer',
|
|
339
341
|
'terminal.copy': 'Copy',
|
|
340
342
|
'terminal.paste': 'Paste',
|
|
341
343
|
'terminal.selectAll': 'Select All',
|