niuma-ui 1.1.3 → 1.1.6
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 +41 -1
- package/package.json +5 -1
- package/src/__tests__/RsDialog.spec.ts +13 -0
- package/src/__tests__/RsIcon.spec.ts +14 -0
- package/src/__tests__/RsSelect.spec.ts +96 -0
- package/src/__tests__/RsTable.spec.ts +17 -0
- package/src/__tests__/createRsDialog.spec.ts +17 -4
- package/src/components/RsDialog.vue +14 -9
- package/src/components/RsMonacoEditor.vue +14 -4
- package/src/components/RsSelect.vue +10 -0
- package/src/components/RsTerminal.vue +182 -78
- package/src/components/dialog-utils.ts +3 -2
- package/src/components/table/rs-table.css +3 -3
- package/src/components/table-utils.ts +2 -2
- package/src/components/terminal-utils.ts +22 -0
- package/src/components/terminal-wheel.ts +32 -7
- package/src/components/use-rs-select.ts +17 -2
- package/src/composables/createRsDialog.ts +58 -7
- package/src/composables/useRsTableEditLayer.ts +1 -1
- package/src/composables/useTableCellTooltip.ts +4 -3
- package/src/dev/vite-xterm-deps.ts +8 -0
- package/src/icons/custom/postgres.ts +52 -0
- package/src/icons/lucide.ts +2 -0
- package/src/index.ts +3 -0
- package/src/monaco/index.ts +1 -0
- package/src/monaco/languages/index.ts +1 -0
- package/src/monaco/languages/types.ts +4 -0
- package/src/styles.css +15 -0
|
@@ -26,15 +26,26 @@ import {
|
|
|
26
26
|
resolveTerminalTheme,
|
|
27
27
|
terminalShortcutLabel,
|
|
28
28
|
type RsTerminalAction,
|
|
29
|
+
type RsTerminalExpose,
|
|
30
|
+
type RsTerminalGeometry,
|
|
29
31
|
type RsTerminalThemeMode,
|
|
30
32
|
} from './terminal-utils'
|
|
31
33
|
import {
|
|
32
34
|
attachWheelScrollGuard,
|
|
35
|
+
isAlternateTerminalBuffer,
|
|
33
36
|
type RsTerminalWheelScrollModifier,
|
|
34
37
|
} from './terminal-wheel'
|
|
35
38
|
|
|
39
|
+
/** 单元格高度倍率(xterm options.lineHeight)。中文比 Canvas 测的西文高,1 会裁到下一行。 */
|
|
36
40
|
const TERMINAL_LINE_HEIGHT = 1.2
|
|
37
41
|
|
|
42
|
+
/** 令牌缺省字号;仅在挂载前(含 SSR)占位,挂载后由 syncResolvedFontSize 换成实测值。 */
|
|
43
|
+
const FALLBACK_FONT_SIZE_PX = 14
|
|
44
|
+
|
|
45
|
+
// class / style 需要落在 .rs-terminal 上;根节点是 RsContextMenu 的 as-child 触发器,
|
|
46
|
+
// 交给自动透传会落到触发器上,消费方只能在外面再套一层控制尺寸。
|
|
47
|
+
defineOptions({ inheritAttrs: false })
|
|
48
|
+
|
|
38
49
|
type RsTerminalFontWeight =
|
|
39
50
|
| 'normal'
|
|
40
51
|
| 'bold'
|
|
@@ -81,7 +92,10 @@ const props = withDefaults(
|
|
|
81
92
|
convertEol?: boolean
|
|
82
93
|
/** 奇数行斑马纹底色,提升长日志可读性 */
|
|
83
94
|
zebraStripes?: boolean
|
|
84
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* none:滚轮始终翻 scrollback。
|
|
97
|
+
* shift:普通 shell 翻 scrollback;仅 vim/top 等备用屏把滚轮转成方向键。
|
|
98
|
+
*/
|
|
85
99
|
wheelScrollModifier?: RsTerminalWheelScrollModifier
|
|
86
100
|
/** TUI 全屏刷新时若视口不在底部,自动滚回底部(修复 top 表头丢失) */
|
|
87
101
|
snapViewportOnTuiWrite?: boolean
|
|
@@ -113,6 +127,8 @@ const emit = defineEmits<{
|
|
|
113
127
|
action: [action: RsTerminalAction]
|
|
114
128
|
/** 询问 AI:携带右键菜单打开时快照的选区(避免菜单点击后选区被清空) */
|
|
115
129
|
askAi: [text: string]
|
|
130
|
+
/** 选区变化,携带当前选中文本(无选区时为空串) */
|
|
131
|
+
selectionChange: [text: string]
|
|
116
132
|
}>()
|
|
117
133
|
|
|
118
134
|
const { t } = useRsI18n()
|
|
@@ -124,6 +140,8 @@ const menuSelectionSnapshot = ref('')
|
|
|
124
140
|
const resolvedThemeMode = ref(resolveTerminalTheme(props.themeMode))
|
|
125
141
|
/** fit 后量一次真实行高,避免亚像素漂移;非每帧更新 */
|
|
126
142
|
const zebraRowStepPx = ref<number | null>(null)
|
|
143
|
+
/** 令牌字号的实测缓存:读 CSS 变量是 DOM 操作,不能放进 computed */
|
|
144
|
+
const resolvedFontSizePx = ref(props.fontSize ?? FALLBACK_FONT_SIZE_PX)
|
|
127
145
|
|
|
128
146
|
const showLoading = computed(() => !terminalReady.value || props.loading)
|
|
129
147
|
|
|
@@ -147,11 +165,15 @@ function resolveTerminalFontWeightBold(): RsTerminalFontWeight {
|
|
|
147
165
|
return props.fontWeightBold ?? asTerminalFontWeight(readTerminalFontWeightBold(hostEl.value))
|
|
148
166
|
}
|
|
149
167
|
|
|
168
|
+
function syncResolvedFontSize(): void {
|
|
169
|
+
resolvedFontSizePx.value = resolveTerminalFontSize()
|
|
170
|
+
}
|
|
171
|
+
|
|
150
172
|
const zebraStyle = computed((): Record<string, string> | undefined => {
|
|
151
173
|
if (!props.zebraStripes) {
|
|
152
174
|
return undefined
|
|
153
175
|
}
|
|
154
|
-
const fontSize =
|
|
176
|
+
const fontSize = resolvedFontSizePx.value
|
|
155
177
|
const step = zebraRowStepPx.value ?? fontSize * TERMINAL_LINE_HEIGHT
|
|
156
178
|
return {
|
|
157
179
|
'--rs-terminal-font-size': `${fontSize}px`,
|
|
@@ -212,6 +234,8 @@ let resizeObserver: ResizeObserver | null = null
|
|
|
212
234
|
let themeObserver: MutationObserver | null = null
|
|
213
235
|
let detachWheelGuard: (() => void) | null = null
|
|
214
236
|
let lastGeometry = { cols: 0, rows: 0 }
|
|
237
|
+
let fitRaf = 0
|
|
238
|
+
let didInitialFit = false
|
|
215
239
|
|
|
216
240
|
function resolveAllowTransparency(): boolean {
|
|
217
241
|
return props.allowTransparency || props.zebraStripes
|
|
@@ -251,6 +275,7 @@ function attachWheelGuard(): void {
|
|
|
251
275
|
detachWheelGuard = attachWheelScrollGuard(hostEl.value, {
|
|
252
276
|
modifier: () => props.wheelScrollModifier,
|
|
253
277
|
inputEnabled: () => props.inputEnabled,
|
|
278
|
+
isAlternateBuffer: () => isAlternateTerminalBuffer(terminal),
|
|
254
279
|
onArrowKeys: (data) => {
|
|
255
280
|
if (props.inputEnabled) {
|
|
256
281
|
emit('data', data)
|
|
@@ -264,7 +289,7 @@ function refreshResolvedTheme(): void {
|
|
|
264
289
|
applyThemeToTerminal()
|
|
265
290
|
}
|
|
266
291
|
|
|
267
|
-
function currentGeometry():
|
|
292
|
+
function currentGeometry(): RsTerminalGeometry | null {
|
|
268
293
|
if (!terminal) {
|
|
269
294
|
return null
|
|
270
295
|
}
|
|
@@ -300,37 +325,35 @@ async function fit(): Promise<void> {
|
|
|
300
325
|
if (clientWidth <= 0 || clientHeight <= 0) {
|
|
301
326
|
return
|
|
302
327
|
}
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!fitAddon || !terminal || !hostEl.value) {
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
await nextTick()
|
|
310
|
-
if (!fitAddon || !terminal) {
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
await rafTwice()
|
|
314
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
315
|
-
if (!fitAddon || !terminal) {
|
|
316
|
-
return
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
fitAddon.fit()
|
|
320
|
-
} catch {
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
|
-
const dims = fitAddon.proposeDimensions()
|
|
324
|
-
if (dims && dims.cols > 0 && dims.rows > 0) {
|
|
325
|
-
break
|
|
328
|
+
if (!didInitialFit) {
|
|
329
|
+
if (typeof document !== 'undefined' && document.fonts?.ready) {
|
|
330
|
+
await document.fonts.ready
|
|
326
331
|
}
|
|
332
|
+
await nextTick()
|
|
327
333
|
await rafTwice()
|
|
328
334
|
}
|
|
329
335
|
if (!fitAddon || !terminal) {
|
|
330
336
|
return
|
|
331
337
|
}
|
|
338
|
+
try {
|
|
339
|
+
fitAddon.fit()
|
|
340
|
+
} catch {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
didInitialFit = true
|
|
332
344
|
emitResizeIfChanged()
|
|
333
345
|
syncZebraRowStepFromDom()
|
|
346
|
+
syncResolvedFontSize()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function scheduleFit(): void {
|
|
350
|
+
if (fitRaf) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
fitRaf = requestAnimationFrame(() => {
|
|
354
|
+
fitRaf = 0
|
|
355
|
+
void fit()
|
|
356
|
+
})
|
|
334
357
|
}
|
|
335
358
|
|
|
336
359
|
function write(data: string): void {
|
|
@@ -347,20 +370,25 @@ function write(data: string): void {
|
|
|
347
370
|
function clear(): void {
|
|
348
371
|
terminal?.clear()
|
|
349
372
|
terminal?.clearSelection()
|
|
350
|
-
|
|
373
|
+
syncSelectionState()
|
|
351
374
|
}
|
|
352
375
|
|
|
353
376
|
function focus(): void {
|
|
354
377
|
terminal?.focus()
|
|
355
378
|
}
|
|
356
379
|
|
|
380
|
+
function getSelectionText(): string {
|
|
381
|
+
return String(terminal?.getSelection?.() ?? '')
|
|
382
|
+
}
|
|
383
|
+
|
|
357
384
|
function syncSelectionState(): void {
|
|
358
385
|
hasSelection.value = Boolean(terminal?.hasSelection())
|
|
386
|
+
emit('selectionChange', getSelectionText())
|
|
359
387
|
}
|
|
360
388
|
|
|
361
389
|
/** 优先活选区;若被右键 word-select / 失焦冲掉,则回退菜单快照。 */
|
|
362
390
|
function resolveMenuSelectionText(): string {
|
|
363
|
-
const live =
|
|
391
|
+
const live = getSelectionText().trim()
|
|
364
392
|
const snap = menuSelectionSnapshot.value.trim()
|
|
365
393
|
if (live && (!snap || live.length >= snap.length)) {
|
|
366
394
|
return live
|
|
@@ -398,7 +426,7 @@ async function pasteFromClipboard(): Promise<void> {
|
|
|
398
426
|
function onTerminalContextMenu(): void {
|
|
399
427
|
void beginClipboardPrefetch()
|
|
400
428
|
// capture 早于 xterm 的 rightClickSelectsWord,先保住用户拖选(vim/less 大段文本)
|
|
401
|
-
const existing =
|
|
429
|
+
const existing = getSelectionText().trim()
|
|
402
430
|
if (existing) {
|
|
403
431
|
menuSelectionSnapshot.value = existing
|
|
404
432
|
hasSelection.value = true
|
|
@@ -408,7 +436,7 @@ function onTerminalContextMenu(): void {
|
|
|
408
436
|
// xterm 在 target 阶段才 rightClickSelect,延后同步;无先验选区时采用 word-select 结果
|
|
409
437
|
void nextTick(() => {
|
|
410
438
|
syncSelectionState()
|
|
411
|
-
const after =
|
|
439
|
+
const after = getSelectionText().trim()
|
|
412
440
|
// 仅在无快照,或 word-select 反而选出更长文本时更新;避免冲掉用户拖选
|
|
413
441
|
if (after && (!menuSelectionSnapshot.value || after.length > menuSelectionSnapshot.value.length)) {
|
|
414
442
|
menuSelectionSnapshot.value = after
|
|
@@ -458,8 +486,10 @@ function onContextMenuSelect(key: string): void {
|
|
|
458
486
|
void runTerminalAction(key as RsTerminalAction)
|
|
459
487
|
}
|
|
460
488
|
|
|
489
|
+
// xterm 只有一个 handler 槽位且无法卸载,所以恒定挂载、由 handler 内部实时读 props.shortcuts,
|
|
490
|
+
// 这样该 prop 关得掉也开得回来。
|
|
461
491
|
function attachShortcuts(): void {
|
|
462
|
-
if (!terminal
|
|
492
|
+
if (!terminal) {
|
|
463
493
|
return
|
|
464
494
|
}
|
|
465
495
|
terminal.attachCustomKeyEventHandler((event) => {
|
|
@@ -499,26 +529,73 @@ function attachShortcuts(): void {
|
|
|
499
529
|
})
|
|
500
530
|
}
|
|
501
531
|
|
|
532
|
+
// 逐项 watch:改字体会让 xterm 清空 WidthCache 并全量重测,不能被其他 prop 的变化连带触发。
|
|
502
533
|
watch(
|
|
503
|
-
() => [props.
|
|
534
|
+
() => [props.fontFamily, props.fontSize, props.fontWeight, props.fontWeightBold] as const,
|
|
504
535
|
() => {
|
|
505
536
|
if (!terminal) {
|
|
506
537
|
return
|
|
507
538
|
}
|
|
508
|
-
refreshResolvedTheme()
|
|
509
|
-
terminal.options.cursorBlink = props.cursorBlink
|
|
510
539
|
terminal.options.fontFamily = resolveTerminalFontFamily()
|
|
511
540
|
terminal.options.fontSize = resolveTerminalFontSize()
|
|
512
541
|
terminal.options.fontWeight = resolveTerminalFontWeight()
|
|
513
542
|
terminal.options.fontWeightBold = resolveTerminalFontWeightBold()
|
|
543
|
+
void fit()
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
watch(
|
|
548
|
+
() => props.cursorBlink,
|
|
549
|
+
(value) => {
|
|
550
|
+
if (terminal) {
|
|
551
|
+
terminal.options.cursorBlink = value
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
watch(
|
|
557
|
+
() => props.convertEol,
|
|
558
|
+
(value) => {
|
|
559
|
+
if (terminal) {
|
|
560
|
+
terminal.options.convertEol = value
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
watch(
|
|
566
|
+
() => props.scrollback,
|
|
567
|
+
(value) => {
|
|
568
|
+
if (terminal) {
|
|
569
|
+
terminal.options.scrollback = value
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
watch(
|
|
575
|
+
() => props.rightClickSelectsWord,
|
|
576
|
+
(value) => {
|
|
577
|
+
if (terminal) {
|
|
578
|
+
terminal.options.rightClickSelectsWord = value
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
watch(
|
|
584
|
+
() => [props.allowTransparency, props.zebraStripes] as const,
|
|
585
|
+
() => {
|
|
586
|
+
if (!terminal) {
|
|
587
|
+
return
|
|
588
|
+
}
|
|
514
589
|
terminal.options.allowTransparency = resolveAllowTransparency()
|
|
515
|
-
terminal.options.convertEol = props.convertEol
|
|
516
|
-
attachWheelGuard()
|
|
517
590
|
void fit()
|
|
518
591
|
},
|
|
519
|
-
{ deep: true },
|
|
520
592
|
)
|
|
521
593
|
|
|
594
|
+
// deep 只用于 theme 这一个对象 prop,避免把内联字面量的重建放大成全量重设。
|
|
595
|
+
watch(() => [props.themeMode, props.theme] as const, refreshResolvedTheme, { deep: true })
|
|
596
|
+
|
|
597
|
+
watch(() => props.wheelScrollModifier, attachWheelGuard)
|
|
598
|
+
|
|
522
599
|
onMounted(async () => {
|
|
523
600
|
if (!hostEl.value) {
|
|
524
601
|
return
|
|
@@ -531,6 +608,7 @@ onMounted(async () => {
|
|
|
531
608
|
fontWeight: resolveTerminalFontWeight(),
|
|
532
609
|
fontWeightBold: resolveTerminalFontWeightBold(),
|
|
533
610
|
lineHeight: TERMINAL_LINE_HEIGHT,
|
|
611
|
+
letterSpacing: 0,
|
|
534
612
|
allowTransparency: resolveAllowTransparency(),
|
|
535
613
|
drawBoldTextInBrightColors: true,
|
|
536
614
|
scrollback: props.scrollback,
|
|
@@ -557,7 +635,7 @@ onMounted(async () => {
|
|
|
557
635
|
emit('ready')
|
|
558
636
|
|
|
559
637
|
resizeObserver = new ResizeObserver(() => {
|
|
560
|
-
|
|
638
|
+
scheduleFit()
|
|
561
639
|
})
|
|
562
640
|
resizeObserver.observe(hostEl.value)
|
|
563
641
|
|
|
@@ -579,12 +657,17 @@ onBeforeUnmount(() => {
|
|
|
579
657
|
resizeObserver = null
|
|
580
658
|
detachWheelGuard?.()
|
|
581
659
|
detachWheelGuard = null
|
|
660
|
+
if (fitRaf) {
|
|
661
|
+
cancelAnimationFrame(fitRaf)
|
|
662
|
+
fitRaf = 0
|
|
663
|
+
}
|
|
582
664
|
terminal?.dispose()
|
|
583
665
|
terminal = null
|
|
584
666
|
fitAddon = null
|
|
667
|
+
didInitialFit = false
|
|
585
668
|
})
|
|
586
669
|
|
|
587
|
-
|
|
670
|
+
const exposed: RsTerminalExpose = {
|
|
588
671
|
write,
|
|
589
672
|
clear,
|
|
590
673
|
focus,
|
|
@@ -592,8 +675,13 @@ defineExpose({
|
|
|
592
675
|
copySelection,
|
|
593
676
|
pasteFromClipboard,
|
|
594
677
|
selectAll,
|
|
678
|
+
getSelection: getSelectionText,
|
|
679
|
+
hasSelection: () => hasSelection.value,
|
|
680
|
+
getGeometry: currentGeometry,
|
|
595
681
|
getTerminal: () => terminal,
|
|
596
|
-
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
defineExpose(exposed)
|
|
597
685
|
</script>
|
|
598
686
|
|
|
599
687
|
<template>
|
|
@@ -602,35 +690,31 @@ defineExpose({
|
|
|
602
690
|
:items="contextMenuItems"
|
|
603
691
|
@select="onContextMenuSelect"
|
|
604
692
|
>
|
|
605
|
-
<
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
</div>
|
|
693
|
+
<section
|
|
694
|
+
v-bind="$attrs"
|
|
695
|
+
class="rs-terminal"
|
|
696
|
+
:class="{ 'rs-terminal--zebra': zebraStripes }"
|
|
697
|
+
:style="zebraStyle"
|
|
698
|
+
@click="focus"
|
|
699
|
+
@contextmenu.capture="onTerminalContextMenu"
|
|
700
|
+
>
|
|
701
|
+
<div ref="hostEl" class="rs-terminal__host" />
|
|
702
|
+
<RsLoading v-if="showLoading" class="rs-terminal__loading" />
|
|
703
|
+
<output v-if="overlay" class="rs-terminal__overlay">
|
|
704
|
+
{{ overlay }}
|
|
705
|
+
</output>
|
|
706
|
+
</section>
|
|
620
707
|
</RsContextMenu>
|
|
621
708
|
</template>
|
|
622
709
|
|
|
623
710
|
<style scoped>
|
|
624
|
-
.rs-terminal-shell {
|
|
625
|
-
display: block;
|
|
626
|
-
height: 100%;
|
|
627
|
-
min-height: 0;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
711
|
.rs-terminal {
|
|
631
712
|
position: relative;
|
|
632
713
|
height: 100%;
|
|
633
714
|
min-height: 0;
|
|
715
|
+
/* 作为 flex/grid 项时,xterm 行盒的固有宽度不得撑开轨道,
|
|
716
|
+
否则容器收窄后列数降不下来、行尾字符会被 overflow 裁掉。 */
|
|
717
|
+
min-width: 0;
|
|
634
718
|
border: 1px solid var(--rs-terminal-border);
|
|
635
719
|
border-radius: var(--rs-radius-md);
|
|
636
720
|
background: var(--rs-terminal-shell-bg, var(--rs-terminal-bg));
|
|
@@ -679,38 +763,58 @@ defineExpose({
|
|
|
679
763
|
line-height: var(--rs-line-height-normal);
|
|
680
764
|
}
|
|
681
765
|
|
|
766
|
+
/*
|
|
767
|
+
* letter-spacing 必须在终端内归零:宿主应用常在祖先上设负字距(NiuMa 的 .nm-app 是 -0.011em)。
|
|
768
|
+
* 它会继承进 xterm 隐藏的字宽测量元素(.xterm-helpers 下的 WidthCache),使 offsetWidth 量到的
|
|
769
|
+
* 字形比实际渲染窄;而 CharSizeService 走 canvas measureText 不受 CSS 影响、单元格宽是准的。
|
|
770
|
+
* DOM 渲染器于是按 `cellWidth - 测量值` 给每个字符补正字距,行盒却仍是 cols × cellWidth,
|
|
771
|
+
* 逐列累积后整行溢出十几到几十像素,被行盒的 overflow:hidden 切掉行尾几个字符。
|
|
772
|
+
*/
|
|
682
773
|
.rs-terminal :deep(.xterm) {
|
|
683
774
|
width: 100%;
|
|
684
775
|
height: 100%;
|
|
776
|
+
line-height: normal;
|
|
777
|
+
letter-spacing: normal;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/*
|
|
781
|
+
* xterm 6 官方仍给空的 .xterm-viewport 写 overflow-y:scroll + 背景 #000。
|
|
782
|
+
* 真正滚动的是 .xterm-scrollable-element;空层滚动条会挡住最后一列。
|
|
783
|
+
*/
|
|
784
|
+
.rs-terminal :deep(.xterm .xterm-viewport) {
|
|
785
|
+
overflow: hidden;
|
|
786
|
+
background-color: transparent;
|
|
685
787
|
}
|
|
686
788
|
|
|
687
789
|
/*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
790
|
+
* xterm 兜底的滑块是占满 14px 轨道的直角块,配色取终端前景色 20% 不透明度,与 RsScrollbar
|
|
791
|
+
* 的圆角胶囊不一致。轨道宽度由 overviewRuler.width 决定,改它会给每个终端实例多建一块
|
|
792
|
+
* overview ruler 画布,因此只重绘滑块:热区仍是 14px,靠透明边框内缩出 10px 可见胶囊。
|
|
691
793
|
*/
|
|
692
|
-
.rs-terminal :deep(.xterm-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
794
|
+
.rs-terminal :deep(.xterm-scrollable-element > .scrollbar > .slider) {
|
|
795
|
+
box-sizing: border-box;
|
|
796
|
+
border: var(--rs-scrollbar-padding, 2px) solid transparent;
|
|
797
|
+
border-radius: var(--rs-radius-full);
|
|
798
|
+
background-color: color-mix(in srgb, var(--rs-muted) 60%, transparent);
|
|
799
|
+
background-clip: padding-box;
|
|
800
|
+
transition: background-color var(--rs-transition-fast);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.rs-terminal :deep(.xterm-scrollable-element > .scrollbar > .slider:hover) {
|
|
804
|
+
background-color: color-mix(in srgb, var(--rs-primary) 40%, var(--rs-muted));
|
|
697
805
|
}
|
|
698
806
|
|
|
699
|
-
.rs-terminal
|
|
700
|
-
|
|
701
|
-
line-height: var(--rs-terminal-line-height, 1.2);
|
|
807
|
+
.rs-terminal :deep(.xterm-scrollable-element > .scrollbar > .slider.active) {
|
|
808
|
+
background-color: color-mix(in srgb, var(--rs-primary) 55%, var(--rs-muted));
|
|
702
809
|
}
|
|
703
810
|
|
|
704
811
|
.rs-terminal--zebra :deep(.xterm-rows),
|
|
705
|
-
.rs-terminal--zebra :deep(.xterm-rows > div)
|
|
812
|
+
.rs-terminal--zebra :deep(.xterm-rows > div),
|
|
813
|
+
.rs-terminal--zebra :deep(.xterm-row) {
|
|
706
814
|
background-color: transparent !important;
|
|
707
815
|
}
|
|
708
816
|
|
|
709
|
-
|
|
710
|
-
* 斑马纹画在 viewport + background-attachment:local:
|
|
711
|
-
* 覆盖字符格取整后的右/下余量,并随 scrollback 一起滚。
|
|
712
|
-
*/
|
|
713
|
-
.rs-terminal--zebra :deep(.xterm-viewport) {
|
|
817
|
+
.rs-terminal--zebra :deep(.xterm-scrollable-element) {
|
|
714
818
|
background-color: transparent !important;
|
|
715
819
|
background-image: repeating-linear-gradient(
|
|
716
820
|
to bottom,
|
|
@@ -10,11 +10,12 @@ export type RsDialogWidthPreset = 'sm' | 'md' | 'lg'
|
|
|
10
10
|
/**
|
|
11
11
|
* 对话框宽度:预设或自定义(number 按 px;string 为 CSS 长度,如 `40rem` / `60%`)。
|
|
12
12
|
* window 布局会在打开时把 px / rem / % 折成像素再居中(% 相对视口扣除 inset);
|
|
13
|
-
* confirm 布局仍把自定义宽度当作 CSS 写入。
|
|
13
|
+
* form / confirm 布局仍把自定义宽度当作 CSS 写入。
|
|
14
14
|
*/
|
|
15
15
|
export type RsDialogWidth = RsDialogWidthPreset | number | string
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** window:可缩放工作窗;form:居中轻量表单/说明;confirm:历史别名,等同 form */
|
|
18
|
+
export type RsDialogLayout = 'window' | 'form' | 'confirm'
|
|
18
19
|
|
|
19
20
|
/** 关闭来源,供 beforeClose / afterClose 区分 */
|
|
20
21
|
export type RsDialogCloseReason =
|
|
@@ -672,14 +672,14 @@
|
|
|
672
672
|
white-space: nowrap;
|
|
673
673
|
min-width: 0;
|
|
674
674
|
}
|
|
675
|
-
/*
|
|
675
|
+
/* 只约束收缩。max-width:100% 会把 slot 卡在容器宽上,
|
|
676
|
+
父级 scrollWidth===clientWidth,公共溢出计算失效。 */
|
|
676
677
|
.rs-table__ellipsis-text > * {
|
|
677
|
-
max-width: 100%;
|
|
678
678
|
min-width: 0;
|
|
679
679
|
}
|
|
680
680
|
.rs-table__shared-tip {
|
|
681
681
|
position: fixed;
|
|
682
|
-
z-index: var(--rs-z-
|
|
682
|
+
z-index: var(--rs-z-dropdown);
|
|
683
683
|
width: max-content;
|
|
684
684
|
max-width: min(16rem, 90vw);
|
|
685
685
|
padding: 0.375rem 0.625rem;
|
|
@@ -153,8 +153,8 @@ export interface RsTableColumn<T extends RsTableRowData = Record<string, unknown
|
|
|
153
153
|
fixed?: RsTableColumnFixed
|
|
154
154
|
ellipsis?: boolean
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
156
|
+
* 格子里看不到的额外提示(always)。不要重复可见文本。
|
|
157
|
+
* 可见文本被截断时的提示用 `ellipsis`:只量表格自身省略容器。
|
|
158
158
|
*/
|
|
159
159
|
tooltip?: (row: T, index: number) => string | null | undefined
|
|
160
160
|
/** 表头原生 title(宽表勿逐列挂 RsTooltip,避免成百实例) */
|
|
@@ -6,6 +6,28 @@ export type RsResolvedTerminalTheme = 'light' | 'dark'
|
|
|
6
6
|
|
|
7
7
|
export type RsTerminalAction = 'copy' | 'paste' | 'selectAll' | 'clear' | 'askAi'
|
|
8
8
|
|
|
9
|
+
export type RsTerminalGeometry = { cols: number; rows: number }
|
|
10
|
+
|
|
11
|
+
/** `RsTerminal` 通过 `defineExpose` 公开的命令式接口。 */
|
|
12
|
+
export interface RsTerminalExpose {
|
|
13
|
+
write: (data: string) => void
|
|
14
|
+
clear: () => void
|
|
15
|
+
focus: () => void
|
|
16
|
+
fit: () => Promise<void>
|
|
17
|
+
copySelection: () => Promise<void>
|
|
18
|
+
pasteFromClipboard: () => Promise<void>
|
|
19
|
+
selectAll: () => void
|
|
20
|
+
getSelection: () => string
|
|
21
|
+
hasSelection: () => boolean
|
|
22
|
+
getGeometry: () => RsTerminalGeometry | null
|
|
23
|
+
/**
|
|
24
|
+
* 逃生舱口:直接取 xterm 实例。
|
|
25
|
+
* 终端选项由组件按 props 托管,外部改写 `terminal.options` 会与之打架(尤其整体替换该对象
|
|
26
|
+
* 会逐项重设、触发全量重测)。需要什么能力优先提为 prop 或加进本接口。
|
|
27
|
+
*/
|
|
28
|
+
getTerminal: () => Terminal | null
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
const TERMINAL_CSS_KEYS: Array<[keyof ITheme, string]> = [
|
|
10
32
|
['background', '--rs-terminal-bg'],
|
|
11
33
|
['foreground', '--rs-terminal-fg'],
|
|
@@ -8,9 +8,28 @@ export function isTerminalViewportScrolledUp(terminal: Terminal): boolean {
|
|
|
8
8
|
return buffer.viewportY < buffer.baseY
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function isAlternateTerminalBuffer(terminal: Terminal | null | undefined): boolean {
|
|
12
|
+
return terminal?.buffer.active.type === 'alternate'
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* shift:仅全屏 TUI(备用屏,vim/top/less)把滚轮转成方向键;
|
|
17
|
+
* 普通 shell 必须滚 scrollback,否则 bash/zsh 会当成历史命令。
|
|
18
|
+
*/
|
|
19
|
+
export function shouldConvertWheelToArrowKeys(options: {
|
|
20
|
+
modifier: RsTerminalWheelScrollModifier
|
|
21
|
+
shiftKey: boolean
|
|
22
|
+
alternateBuffer: boolean
|
|
23
|
+
}): boolean {
|
|
24
|
+
if (options.shiftKey || options.modifier === 'none') {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
return options.alternateBuffer
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 滚轮策略:shell 里滚历史缓冲;仅备用屏才把滚轮发给 PTY。
|
|
32
|
+
* 按住 Shift 时始终原生滚动。
|
|
14
33
|
*/
|
|
15
34
|
export function attachWheelScrollGuard(
|
|
16
35
|
hostEl: HTMLElement,
|
|
@@ -18,18 +37,24 @@ export function attachWheelScrollGuard(
|
|
|
18
37
|
modifier: () => RsTerminalWheelScrollModifier
|
|
19
38
|
inputEnabled: () => boolean
|
|
20
39
|
onArrowKeys: (data: string) => void
|
|
40
|
+
isAlternateBuffer?: () => boolean
|
|
21
41
|
},
|
|
22
42
|
): () => void {
|
|
23
|
-
const viewport =
|
|
43
|
+
const viewport =
|
|
44
|
+
hostEl.querySelector<HTMLElement>('.xterm-scrollable-element') ??
|
|
45
|
+
hostEl.querySelector<HTMLElement>('.xterm-viewport')
|
|
24
46
|
if (!viewport) {
|
|
25
47
|
return () => undefined
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
const handler = (ev: WheelEvent): void => {
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
if (
|
|
52
|
+
!shouldConvertWheelToArrowKeys({
|
|
53
|
+
modifier: options.modifier(),
|
|
54
|
+
shiftKey: ev.shiftKey,
|
|
55
|
+
alternateBuffer: options.isAlternateBuffer?.() ?? false,
|
|
56
|
+
})
|
|
57
|
+
) {
|
|
33
58
|
return
|
|
34
59
|
}
|
|
35
60
|
ev.preventDefault()
|
|
@@ -53,6 +53,8 @@ export interface RsSelectEngineProps {
|
|
|
53
53
|
labelInValue?: boolean
|
|
54
54
|
tokenSeparators?: string[]
|
|
55
55
|
autoClearSearchValue?: boolean
|
|
56
|
+
/** 打开下拉时把当前选中值写入搜索框;默认不回填 */
|
|
57
|
+
fillSearchWithValue?: boolean
|
|
56
58
|
searchPlaceholder?: string
|
|
57
59
|
emptyText?: string
|
|
58
60
|
loadingText?: string
|
|
@@ -100,10 +102,16 @@ export function useRsSelect(
|
|
|
100
102
|
() => Boolean(props.virtual) || flatCount.value > (props.virtualThreshold ?? 50),
|
|
101
103
|
)
|
|
102
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 可搜索时一律走本地过滤,并关掉 Reka 内置 filter。
|
|
107
|
+
* Reka 的 filterSearch 与搜索框 searchQuery 不同步(打开时会被清空),
|
|
108
|
+
* 默认依赖内置过滤时会出现:手输「NVA」已出「使用 NVA」,但 NVARCHAR 仍被整表挡住。
|
|
109
|
+
*/
|
|
103
110
|
const useManualFilter = computed(
|
|
104
111
|
() =>
|
|
105
112
|
Boolean(props.remote) ||
|
|
106
113
|
useVirtual.value ||
|
|
114
|
+
isSearchable.value ||
|
|
107
115
|
typeof props.filterOption === 'function' ||
|
|
108
116
|
props.filterOption === false ||
|
|
109
117
|
Boolean(props.optionFilterProp && props.optionFilterProp !== 'label') ||
|
|
@@ -340,8 +348,15 @@ export function useRsSelect(
|
|
|
340
348
|
const pending = searchQuery.value
|
|
341
349
|
await nextTick()
|
|
342
350
|
await nextTick()
|
|
343
|
-
// ComboboxInput 挂载时会 resetSearchTerm
|
|
344
|
-
|
|
351
|
+
// ComboboxInput 挂载时会 resetSearchTerm,把选中值写进搜索框。
|
|
352
|
+
// 默认清空;受控 / 未清理的 searchValue 写回;fillSearchWithValue 才回填当前选中。
|
|
353
|
+
if (pending) {
|
|
354
|
+
searchQuery.value = pending
|
|
355
|
+
} else if (props.fillSearchWithValue && !props.multiple && hasValue.value) {
|
|
356
|
+
searchQuery.value = singleDisplayLabel.value
|
|
357
|
+
} else {
|
|
358
|
+
searchQuery.value = ''
|
|
359
|
+
}
|
|
345
360
|
return
|
|
346
361
|
}
|
|
347
362
|
if (props.autoClearSearchValue) searchQuery.value = ''
|