fix: improve mobile terminal interaction
This commit is contained in:
@@ -82,9 +82,63 @@
|
||||
>
|
||||
<div class="terminal-header">
|
||||
<span>{{ session.title }}</span>
|
||||
<span class="status-text">{{ statusLabel(session.status) }}</span>
|
||||
<div class="terminal-header-actions">
|
||||
<span class="status-text">{{ statusLabel(session.status) }}</span>
|
||||
<button
|
||||
class="secondary tiny"
|
||||
type="button"
|
||||
@click="toggleNativeCopyMode(session)"
|
||||
>
|
||||
{{ session.nativeTextMode ? "退出原生复制" : "原生复制" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-body" :ref="(el) => setTerminalRef(session.id, el)">
|
||||
<div
|
||||
v-if="!session.nativeTextMode"
|
||||
class="terminal-touch-overlay"
|
||||
@touchstart.stop.prevent="handleTerminalTouchStart(session, $event)"
|
||||
@touchmove.stop.prevent="handleTerminalTouchMove(session, $event)"
|
||||
@touchend.stop.prevent="handleTerminalTouchEnd(session, $event)"
|
||||
@touchcancel.stop.prevent="handleTerminalTouchCancel(session)"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!session.nativeTextMode && session.selectionActive && session.id === activeId"
|
||||
class="terminal-selection-bar"
|
||||
>
|
||||
<span>选区</span>
|
||||
<span class="selection-size">
|
||||
{{ selectionSizeText(session) }}
|
||||
</span>
|
||||
<button class="secondary tiny" type="button" @click="shrinkTerminalSelection(session)">
|
||||
缩小
|
||||
</button>
|
||||
<button class="secondary tiny" type="button" @click="extendTerminalSelection(session, 'up')">
|
||||
上扩
|
||||
</button>
|
||||
<button class="secondary tiny" type="button" @click="extendTerminalSelection(session, 'down')">
|
||||
下扩
|
||||
</button>
|
||||
<button class="secondary tiny" type="button" @click="copyTerminalSelection(session)">
|
||||
复制
|
||||
</button>
|
||||
<button class="secondary tiny" type="button" @click="clearTerminalSelection(session)">
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="session.nativeTextMode && session.id === activeId"
|
||||
class="terminal-selection-bar native-selection-bar"
|
||||
>
|
||||
<span>原生复制</span>
|
||||
<button class="secondary tiny" type="button" @click="copyTerminalSelection(session)">
|
||||
复制
|
||||
</button>
|
||||
<button class="secondary tiny" type="button" @click="toggleNativeCopyMode(session, false)">
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
<div class="terminal-body" :ref="(el) => setTerminalRef(session.id, el)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -976,6 +1030,11 @@ function createSession() {
|
||||
ws: null,
|
||||
term: null,
|
||||
fit: null,
|
||||
viewport: null,
|
||||
touchState: null,
|
||||
selectionActive: false,
|
||||
fontSize: 14,
|
||||
nativeTextMode: false,
|
||||
};
|
||||
|
||||
sessions.value.push(session);
|
||||
@@ -998,15 +1057,18 @@ function createSession() {
|
||||
function initSession(session, payload) {
|
||||
const container = terminalRefs.get(session.id);
|
||||
if (!container) return;
|
||||
session.fontSize = getTerminalFontSize();
|
||||
session.nativeTextMode = false;
|
||||
|
||||
const term = markRaw(
|
||||
new Terminal({
|
||||
fontFamily: `"JetBrains Mono", "Fira Code", "Cascadia Code", monospace`,
|
||||
fontSize: 14,
|
||||
fontSize: session.fontSize,
|
||||
lineHeight: 1.4,
|
||||
cursorBlink: true,
|
||||
scrollback: 8000,
|
||||
smoothScrollDuration: 140,
|
||||
// 移动端拖动历史输出时,平滑滚动会显得黏滞,直接关闭更跟手。
|
||||
smoothScrollDuration: 0,
|
||||
scrollSensitivity: 1.15,
|
||||
fastScrollSensitivity: 4,
|
||||
theme: {
|
||||
@@ -1040,6 +1102,8 @@ function initSession(session, payload) {
|
||||
term.focus();
|
||||
term.writeln("\x1b[90m[正在建立连接...]\x1b[0m");
|
||||
|
||||
session.viewport = term.element?.querySelector(".xterm-viewport") || null;
|
||||
|
||||
const ws = markRaw(new WebSocket(wsUrl.value));
|
||||
session.ws = ws;
|
||||
session.term = term;
|
||||
@@ -1108,14 +1172,70 @@ function initSession(session, payload) {
|
||||
}
|
||||
});
|
||||
|
||||
term.onSelectionChange(() => {
|
||||
session.selectionActive = term.hasSelection();
|
||||
});
|
||||
|
||||
term.onResize(() => sendResize());
|
||||
|
||||
focusSession(session);
|
||||
}
|
||||
|
||||
function getTerminalFontSize() {
|
||||
if (typeof window === "undefined") return 14;
|
||||
return window.matchMedia("(max-width: 768px)").matches ? 11 : 14;
|
||||
}
|
||||
|
||||
function syncTerminalFontSize(session) {
|
||||
if (!session?.term) return;
|
||||
const nextSize = getTerminalFontSize();
|
||||
if (session.fontSize === nextSize) return;
|
||||
session.fontSize = nextSize;
|
||||
session.term.options.fontSize = nextSize;
|
||||
session.fit?.fit();
|
||||
if (session.ws?.readyState === WebSocket.OPEN) {
|
||||
session.ws.send(
|
||||
JSON.stringify({
|
||||
type: "resize",
|
||||
cols: session.term.cols,
|
||||
rows: session.term.rows,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setNativeCopyMode(session, enabled) {
|
||||
const term = session?.term;
|
||||
if (!term) return;
|
||||
session.nativeTextMode = enabled;
|
||||
session.selectionActive = false;
|
||||
term.clearSelection();
|
||||
term.options.screenReaderMode = enabled;
|
||||
if (term.element) {
|
||||
term.element.classList.toggle("native-terminal-copy-mode", enabled);
|
||||
}
|
||||
if (!enabled) {
|
||||
const selection = document.getSelection?.();
|
||||
selection?.removeAllRanges?.();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleNativeCopyMode(session, force) {
|
||||
const next = typeof force === "boolean" ? force : !session.nativeTextMode;
|
||||
if (!session?.term || next === session.nativeTextMode) return;
|
||||
setNativeCopyMode(session, next);
|
||||
if (next) {
|
||||
blurTerminal(session);
|
||||
showNotice("已进入原生复制模式,长按文字后点复制", "info", 2200);
|
||||
} else {
|
||||
focusSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
function focusSession(session) {
|
||||
if (!session || !session.term || !session.fit) return;
|
||||
nextTick(() => {
|
||||
syncTerminalFontSize(session);
|
||||
session.fit.fit();
|
||||
if (session.ws?.readyState === WebSocket.OPEN) {
|
||||
session.ws.send(
|
||||
@@ -1126,11 +1246,290 @@ function focusSession(session) {
|
||||
})
|
||||
);
|
||||
}
|
||||
session.term.scrollToBottom();
|
||||
session.term.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function getTerminalTouchState(session) {
|
||||
if (!session.touchState) {
|
||||
session.touchState = {
|
||||
timer: null,
|
||||
mode: "idle",
|
||||
touched: false,
|
||||
moved: false,
|
||||
longPress: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
lastY: 0,
|
||||
rowHeight: 0,
|
||||
bodyRect: null,
|
||||
bodyPaddingTop: 0,
|
||||
bodyPaddingBottom: 0,
|
||||
selectionStartRow: 0,
|
||||
selectionEndRow: 0,
|
||||
};
|
||||
}
|
||||
return session.touchState;
|
||||
}
|
||||
|
||||
function clearTerminalTouchTimer(state) {
|
||||
if (state?.timer) {
|
||||
clearTimeout(state.timer);
|
||||
state.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getVisibleBufferRow(session, clientY) {
|
||||
if (!session.term || !session.touchState?.bodyRect) return null;
|
||||
const state = session.touchState;
|
||||
const rowHeight = state.rowHeight || 1;
|
||||
const y = clientY - state.bodyRect.top - state.bodyPaddingTop;
|
||||
const row = Math.floor(y / rowHeight);
|
||||
const clamped = Math.max(0, Math.min(session.term.rows - 1, row));
|
||||
return session.term.buffer.active.viewportY + clamped;
|
||||
}
|
||||
|
||||
function getWrappedSelectionRange(term, bufferRow) {
|
||||
const buffer = term?.buffer?.active;
|
||||
if (!buffer) return null;
|
||||
let start = bufferRow;
|
||||
let end = bufferRow;
|
||||
while (start > 0) {
|
||||
const line = buffer.getLine(start);
|
||||
const prev = buffer.getLine(start - 1);
|
||||
if (!line?.isWrapped || !prev) break;
|
||||
start -= 1;
|
||||
}
|
||||
while (end < buffer.length - 1) {
|
||||
const next = buffer.getLine(end + 1);
|
||||
if (!next?.isWrapped) break;
|
||||
end += 1;
|
||||
}
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function applyTerminalSelection(session, startRow, endRow) {
|
||||
const term = session.term;
|
||||
if (!term) return;
|
||||
const startRange = getWrappedSelectionRange(term, startRow);
|
||||
const endRange = getWrappedSelectionRange(term, endRow);
|
||||
if (!startRange || !endRange) return;
|
||||
const start = Math.min(startRange.start, endRange.start);
|
||||
const end = Math.max(startRange.end, endRange.end);
|
||||
term.clearSelection();
|
||||
term.selectLines(start, end);
|
||||
session.selectionActive = true;
|
||||
}
|
||||
|
||||
function blurTerminal(session) {
|
||||
session.term?.blur();
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && typeof activeElement.blur === "function") {
|
||||
activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
function getNativeSelectionText(session) {
|
||||
const term = session?.term;
|
||||
if (!term?.element) return "";
|
||||
const selection = document.getSelection?.();
|
||||
if (!selection) return "";
|
||||
const text = selection.toString();
|
||||
if (!text) return "";
|
||||
const anchorNode = selection.anchorNode;
|
||||
const focusNode = selection.focusNode;
|
||||
if (!term.element.contains(anchorNode) && !term.element.contains(focusNode)) {
|
||||
return "";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function selectionSizeText(session) {
|
||||
const state = session.touchState;
|
||||
if (!state) return "";
|
||||
const top = Math.min(state.selectionStartRow, state.selectionEndRow);
|
||||
const bottom = Math.max(state.selectionStartRow, state.selectionEndRow);
|
||||
return `${bottom - top + 1} 行`;
|
||||
}
|
||||
|
||||
function extendTerminalSelection(session, direction) {
|
||||
const term = session.term;
|
||||
const state = session.touchState;
|
||||
if (!term || !state) return;
|
||||
blurTerminal(session);
|
||||
const buffer = term.buffer.active;
|
||||
let top = Math.min(state.selectionStartRow, state.selectionEndRow);
|
||||
let bottom = Math.max(state.selectionStartRow, state.selectionEndRow);
|
||||
|
||||
if (direction === "up") {
|
||||
top = Math.max(0, top - 1);
|
||||
} else if (direction === "down") {
|
||||
bottom = Math.min(buffer.length - 1, bottom + 1);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectionStartRow = top;
|
||||
state.selectionEndRow = bottom;
|
||||
applyTerminalSelection(session, top, bottom);
|
||||
}
|
||||
|
||||
function shrinkTerminalSelection(session) {
|
||||
const term = session.term;
|
||||
const state = session.touchState;
|
||||
if (!term || !state) return;
|
||||
blurTerminal(session);
|
||||
let top = Math.min(state.selectionStartRow, state.selectionEndRow);
|
||||
let bottom = Math.max(state.selectionStartRow, state.selectionEndRow);
|
||||
if (bottom - top <= 0) return;
|
||||
const shrinkFromTop = state.startY < (state.bodyRect?.top || 0) + (state.bodyRect?.height || 0) / 2;
|
||||
if (shrinkFromTop) {
|
||||
top += 1;
|
||||
} else {
|
||||
bottom -= 1;
|
||||
}
|
||||
state.selectionStartRow = top;
|
||||
state.selectionEndRow = bottom;
|
||||
applyTerminalSelection(session, top, bottom);
|
||||
}
|
||||
|
||||
async function copyTerminalSelection(session) {
|
||||
const term = session.term;
|
||||
if (!term) return false;
|
||||
blurTerminal(session);
|
||||
const text = session.nativeTextMode ? getNativeSelectionText(session) : term.getSelection();
|
||||
if (!text) return false;
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.setAttribute("readonly", "readonly");
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-9999px";
|
||||
textarea.style.top = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const ok = document.execCommand("copy");
|
||||
textarea.remove();
|
||||
if (!ok) throw new Error("copy command failed");
|
||||
}
|
||||
showNotice("已复制终端输出", "success");
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showNotice("复制失败,请重试", "error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearTerminalSelection(session) {
|
||||
blurTerminal(session);
|
||||
if (session.nativeTextMode) {
|
||||
setNativeCopyMode(session, false);
|
||||
}
|
||||
session.term?.clearSelection();
|
||||
session.selectionActive = false;
|
||||
if (session.touchState) {
|
||||
session.touchState.selectionStartRow = 0;
|
||||
session.touchState.selectionEndRow = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTerminalTouchStart(session, event) {
|
||||
const touch = event.touches?.[0];
|
||||
if (!touch || !session.term) return;
|
||||
const state = getTerminalTouchState(session);
|
||||
const body = terminalRefs.get(session.id);
|
||||
if (!body) return;
|
||||
clearTerminalTouchTimer(state);
|
||||
const rect = body.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(body);
|
||||
const paddingTop = parseFloat(style.paddingTop) || 0;
|
||||
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
||||
const usableHeight = Math.max(1, rect.height - paddingTop - paddingBottom);
|
||||
state.touched = true;
|
||||
state.moved = false;
|
||||
state.longPress = false;
|
||||
state.mode = "pending";
|
||||
state.startX = touch.clientX;
|
||||
state.startY = touch.clientY;
|
||||
state.lastY = touch.clientY;
|
||||
state.rowHeight = usableHeight / Math.max(1, session.term.rows);
|
||||
state.bodyRect = rect;
|
||||
state.bodyPaddingTop = paddingTop;
|
||||
state.bodyPaddingBottom = paddingBottom;
|
||||
|
||||
state.timer = window.setTimeout(async () => {
|
||||
if (!state.touched || state.moved || !session.term) return;
|
||||
clearTerminalTouchTimer(state);
|
||||
state.mode = "select";
|
||||
state.longPress = true;
|
||||
const bufferRow = getVisibleBufferRow(session, state.startY);
|
||||
if (bufferRow == null) return;
|
||||
state.selectionStartRow = bufferRow;
|
||||
state.selectionEndRow = bufferRow;
|
||||
applyTerminalSelection(session, bufferRow, bufferRow);
|
||||
showNotice("已选中终端内容,点“复制”即可复制", "info", 2200);
|
||||
}, 520);
|
||||
}
|
||||
|
||||
function handleTerminalTouchMove(session, event) {
|
||||
const touch = event.touches?.[0];
|
||||
if (!touch || !session.term) return;
|
||||
const state = session.touchState;
|
||||
if (!state || !state.touched) return;
|
||||
|
||||
const dx = touch.clientX - state.startX;
|
||||
const dy = touch.clientY - state.startY;
|
||||
if (state.mode === "select") {
|
||||
const bufferRow = getVisibleBufferRow(session, touch.clientY);
|
||||
if (bufferRow == null) return;
|
||||
state.selectionEndRow = bufferRow;
|
||||
applyTerminalSelection(session, state.selectionStartRow, state.selectionEndRow);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.moved && Math.hypot(dx, dy) > 8) {
|
||||
state.moved = true;
|
||||
state.mode = "scroll";
|
||||
clearTerminalTouchTimer(state);
|
||||
}
|
||||
|
||||
if (state.mode !== "scroll") return;
|
||||
const viewport = session.viewport;
|
||||
if (!viewport) return;
|
||||
event.preventDefault();
|
||||
viewport.scrollTop += state.lastY - touch.clientY;
|
||||
state.lastY = touch.clientY;
|
||||
}
|
||||
|
||||
function handleTerminalTouchEnd(session) {
|
||||
const state = session.touchState;
|
||||
if (!state) return;
|
||||
clearTerminalTouchTimer(state);
|
||||
const shouldFocus = state.touched && state.mode === "pending" && !state.moved;
|
||||
state.touched = false;
|
||||
state.moved = false;
|
||||
state.longPress = false;
|
||||
state.mode = "idle";
|
||||
if (shouldFocus) {
|
||||
focusSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTerminalTouchCancel(session) {
|
||||
const state = session.touchState;
|
||||
if (!state) return;
|
||||
clearTerminalTouchTimer(state);
|
||||
state.touched = false;
|
||||
state.moved = false;
|
||||
state.longPress = false;
|
||||
state.mode = "idle";
|
||||
}
|
||||
|
||||
function setActive(id) {
|
||||
activeId.value = id;
|
||||
}
|
||||
@@ -1153,6 +1552,7 @@ function closeSession(id) {
|
||||
function handleWindowResize() {
|
||||
const session = sessions.value.find((item) => item.id === activeId.value);
|
||||
if (!session) return;
|
||||
syncTerminalFontSize(session);
|
||||
focusSession(session);
|
||||
}
|
||||
|
||||
@@ -1287,6 +1687,20 @@ function handlePaste(event) {
|
||||
sendImageToTerminal(file);
|
||||
}
|
||||
|
||||
function isMobileTouchInput() {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.matchMedia("(pointer: coarse)").matches;
|
||||
}
|
||||
|
||||
function handleAppPointerDown(event) {
|
||||
if (!isMobileTouchInput()) return;
|
||||
if ("pointerType" in event && event.pointerType !== "touch") return;
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (!target.closest("button")) return;
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// 根元素引用,用于动态调整高度以适配移动端虚拟键盘
|
||||
const appRef = ref(null);
|
||||
let viewportFitTimer = null;
|
||||
@@ -1294,14 +1708,11 @@ let viewportFitTimer = null;
|
||||
function updateAppHeight() {
|
||||
const vv = window.visualViewport;
|
||||
const h = vv ? vv.height : window.innerHeight;
|
||||
// 部分移动端浏览器键盘弹出时高度不缩,但会覆盖页面;用 padding-bottom 兜底
|
||||
const keyboard =
|
||||
vv && typeof vv.height === "number"
|
||||
? Math.max(0, window.innerHeight - vv.height - (vv.offsetTop || 0))
|
||||
: 0;
|
||||
if (appRef.value) {
|
||||
appRef.value.style.height = h + "px";
|
||||
appRef.value.style.setProperty("--keyboard-inset", keyboard + "px");
|
||||
// 这里已经直接跟随 visualViewport.height,不再额外预留整块键盘高度,
|
||||
// 否则手机端会在快捷键栏和系统键盘之间留下很大的空白。
|
||||
appRef.value.style.setProperty("--keyboard-inset", "0px");
|
||||
}
|
||||
// 触发布局变化后,让终端重新适配可视区域
|
||||
requestAnimationFrame(() => {
|
||||
@@ -1328,6 +1739,7 @@ function hideSplashScreen() {
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
document.addEventListener("paste", handlePaste);
|
||||
document.addEventListener("pointerdown", handleAppPointerDown, true);
|
||||
// 移动端虚拟键盘弹出时,visualViewport 会缩减,用它动态撑高度
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener("resize", updateAppHeight);
|
||||
@@ -1345,6 +1757,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
document.removeEventListener("pointerdown", handleAppPointerDown, true);
|
||||
if (viewportFitTimer) {
|
||||
clearTimeout(viewportFitTimer);
|
||||
viewportFitTimer = null;
|
||||
@@ -1673,8 +2086,6 @@ onBeforeUnmount(() => {
|
||||
0 30px 80px rgba(15, 23, 42, 0.95),
|
||||
0 0 40px rgba(56, 189, 248, 0.12);
|
||||
overflow: hidden;
|
||||
/* 虚拟键盘覆盖时,给底部留白避免遮挡终端最后几行 */
|
||||
padding-bottom: var(--keyboard-inset, 0px);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -1815,6 +2226,13 @@ onBeforeUnmount(() => {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.terminal-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.terminal-header .status-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
@@ -1825,6 +2243,86 @@ onBeforeUnmount(() => {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: contain;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal-touch-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
background: transparent;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode {
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-screen canvas {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-accessibility:not(.debug) {
|
||||
pointer-events: auto !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-accessibility-tree {
|
||||
user-select: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
color: #e5e7eb !important;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-accessibility-tree * {
|
||||
user-select: text !important;
|
||||
-webkit-user-select: text !important;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-accessibility-tree *::selection {
|
||||
background: rgba(59, 130, 246, 0.35);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.terminal-selection-bar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 10px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 12;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: calc(100% - 16px);
|
||||
padding: 6px 8px;
|
||||
border-radius: 16px;
|
||||
background: rgba(2, 6, 23, 0.9);
|
||||
border: 1px solid rgba(59, 130, 246, 0.35);
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.55);
|
||||
backdrop-filter: blur(12px);
|
||||
font-size: 11px;
|
||||
color: #dbeafe;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.native-selection-bar {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-size {
|
||||
color: #93c5fd;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 10px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.quick-keys {
|
||||
@@ -2192,6 +2690,42 @@ textarea {
|
||||
.overlay {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.terminal-header-actions {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.terminal-header-actions .secondary.tiny {
|
||||
padding: 4px 7px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.terminal-touch-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-screen canvas {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.native-terminal-copy-mode .xterm-accessibility:not(.debug) {
|
||||
inset: 0 !important;
|
||||
}
|
||||
|
||||
.terminal-selection-bar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.terminal-selection-bar .secondary.tiny {
|
||||
padding: 4px 7px;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user