fix: sync wait overlay with runtime
This commit is contained in:
parent
d5e6c9c077
commit
9750b0b8f1
@ -158,9 +158,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="window wait-window" ref="waitWindow">
|
||||
<div class="window-header">
|
||||
<span class="traffic-dot red"></span>
|
||||
<span class="traffic-dot yellow"></span>
|
||||
<span class="traffic-dot green"></span>
|
||||
<span class="window-title">等待</span>
|
||||
</div>
|
||||
<div class="wait-body">
|
||||
<div class="flip-clock" ref="waitDisplay">
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-separator">:</div>
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
<div class="flip-digit" data-value="0"><span class="top">0</span><span class="bottom">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="context-menu" ref="desktopMenu">
|
||||
<button data-action="file">新建文件</button>
|
||||
<button data-action="folder">新建文件夹</button>
|
||||
<button data-action="wait">等待</button>
|
||||
</div>
|
||||
<div class="context-menu" ref="folderMenu">
|
||||
<button data-action="file">新建文件</button>
|
||||
@ -169,6 +188,7 @@
|
||||
<div class="context-menu" ref="fileMenu">
|
||||
<button data-action="read">阅读文件</button>
|
||||
<button data-action="edit">编辑文件</button>
|
||||
<button data-action="delete">删除文件</button>
|
||||
</div>
|
||||
<div class="context-menu" ref="focusMenu">
|
||||
<button data-action="focus">聚焦文件</button>
|
||||
@ -191,6 +211,14 @@
|
||||
<div class="mouse-pointer" ref="mousePointer">
|
||||
<img :src="mouseIcon" alt="pointer" />
|
||||
</div>
|
||||
|
||||
<div class="wait-overlay" ref="waitOverlay">
|
||||
<div class="z z-1">Z</div>
|
||||
<div class="z z-2">Z</div>
|
||||
<div class="z z-3">Z</div>
|
||||
<div class="z z-4">Z</div>
|
||||
<div class="wait-countdown" ref="waitCountdown">5s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="monitor-stand"></div>
|
||||
@ -253,6 +281,10 @@ const memoryTime = ref<HTMLElement | null>(null);
|
||||
const todoWindow = ref<HTMLElement | null>(null);
|
||||
const todoSummary = ref<HTMLElement | null>(null);
|
||||
const todoList = ref<HTMLElement | null>(null);
|
||||
const waitWindow = ref<HTMLElement | null>(null);
|
||||
const waitDisplay = ref<HTMLElement | null>(null);
|
||||
const waitOverlay = ref<HTMLElement | null>(null);
|
||||
const waitCountdown = ref<HTMLElement | null>(null);
|
||||
const desktopMenu = ref<HTMLElement | null>(null);
|
||||
const folderMenu = ref<HTMLElement | null>(null);
|
||||
const fileMenu = ref<HTMLElement | null>(null);
|
||||
@ -334,6 +366,10 @@ const mountDirector = async () => {
|
||||
todoWindow: todoWindow.value!,
|
||||
todoSummary: todoSummary.value!,
|
||||
todoList: todoList.value!,
|
||||
waitWindow: waitWindow.value!,
|
||||
waitDisplay: waitDisplay.value!,
|
||||
waitOverlay: waitOverlay.value!,
|
||||
waitCountdown: waitCountdown.value!,
|
||||
desktopMenu: desktopMenu.value!,
|
||||
folderMenu: folderMenu.value!,
|
||||
fileMenu: fileMenu.value!,
|
||||
|
||||
@ -65,6 +65,10 @@ export interface MonitorElements {
|
||||
todoWindow: HTMLElement;
|
||||
todoSummary: HTMLElement;
|
||||
todoList: HTMLElement;
|
||||
waitWindow: HTMLElement;
|
||||
waitDisplay: HTMLElement;
|
||||
waitOverlay: HTMLElement;
|
||||
waitCountdown: HTMLElement;
|
||||
desktopMenu: HTMLElement;
|
||||
folderMenu: HTMLElement;
|
||||
fileMenu: HTMLElement;
|
||||
@ -237,6 +241,9 @@ export class MonitorDirector implements MonitorDriver {
|
||||
private waitingBubbleTimer: number | null = null;
|
||||
private waitingBubbleBase: string | null = null;
|
||||
private progressBubbleActive = false;
|
||||
private secondaryMenu: HTMLElement | null = null;
|
||||
private waitDigits: HTMLElement[] = [];
|
||||
private waitOverlayTimer: number | null = null;
|
||||
// 当实际执行进度快于动画播放时,用于压制“正在 xxx”提示
|
||||
private playbackLagging = false;
|
||||
// 记录最近一次 Python 执行的ID,用于丢弃过期动画/结果
|
||||
@ -2087,7 +2094,8 @@ export class MonitorDirector implements MonitorDriver {
|
||||
this.elements.terminalWindow,
|
||||
this.elements.readerWindow,
|
||||
this.elements.memoryWindow,
|
||||
this.elements.todoWindow
|
||||
this.elements.todoWindow,
|
||||
this.elements.waitWindow
|
||||
].forEach(win => this.closeWindow(win, { animate: false }));
|
||||
this.clearExtractionWindows();
|
||||
this.clearTerminalSessions();
|
||||
@ -2106,10 +2114,12 @@ export class MonitorDirector implements MonitorDriver {
|
||||
this.windowAnchors.set(this.elements.readerWindow, { x: 0.42, y: 0.05 });
|
||||
this.windowAnchors.set(this.elements.memoryWindow, { x: 0.28, y: 0.32 });
|
||||
this.windowAnchors.set(this.elements.todoWindow, { x: 0.5, y: 0.32 });
|
||||
this.windowAnchors.set(this.elements.waitWindow, { x: 0.74, y: 0.24 });
|
||||
}
|
||||
|
||||
private layoutFloatingWindows() {
|
||||
this.windowAnchors.forEach((anchor, el) => this.positionWindow(el, anchor));
|
||||
this.hideWaitOverlay();
|
||||
}
|
||||
|
||||
private positionWindow(el: HTMLElement, anchor: { x: number; y: number }) {
|
||||
@ -2380,6 +2390,25 @@ export class MonitorDirector implements MonitorDriver {
|
||||
await sleep(300);
|
||||
return;
|
||||
}
|
||||
const openDeleteMenu = async (targetEl: HTMLElement | null, menuType: ContextMenuType = 'file') => {
|
||||
if (!targetEl) {
|
||||
return false;
|
||||
}
|
||||
await this.movePointerToElement(targetEl, { duration: 600 });
|
||||
await this.click({ right: true });
|
||||
this.showContextMenu('file');
|
||||
await sleep(160);
|
||||
const highlighted = await this.highlightMenu('file', 'delete');
|
||||
if (!highlighted) {
|
||||
const btn = this.elements.fileMenu.querySelector<HTMLButtonElement>('button[data-action="delete"]');
|
||||
if (btn) {
|
||||
await this.movePointerToElement(btn, { duration: 320 });
|
||||
}
|
||||
}
|
||||
await this.click();
|
||||
this.hideContextMenus();
|
||||
return true;
|
||||
};
|
||||
if (segments.length) {
|
||||
const parentKey = await this.openFolderChain(segments);
|
||||
if (parentKey) {
|
||||
@ -2390,20 +2419,18 @@ export class MonitorDirector implements MonitorDriver {
|
||||
await sleep(40);
|
||||
const entryPath = this.composePath([parentKey, name]);
|
||||
const entryEl = this.findFolderEntryElement(entryPath);
|
||||
if (entryEl) {
|
||||
await this.movePointerToElement(entryEl, { duration: 600 });
|
||||
entryEl.classList.add('removing');
|
||||
setTimeout(() => {
|
||||
this.removeFolderEntry(parentKey, name);
|
||||
}, 260);
|
||||
} else {
|
||||
const menuShown = await openDeleteMenu(entryEl, 'file');
|
||||
if (!menuShown) {
|
||||
this.removeFolderEntry(parentKey, name);
|
||||
return;
|
||||
}
|
||||
entryEl?.classList.add('removing');
|
||||
setTimeout(() => this.removeFolderEntry(parentKey, name), 260);
|
||||
}
|
||||
} else {
|
||||
const icon = this.fileIcons.get(name) || this.folderIcons.get(name);
|
||||
if (icon) {
|
||||
await this.movePointerToElement(icon, { duration: 500 });
|
||||
await openDeleteMenu(icon, 'file');
|
||||
icon.classList.add('removing');
|
||||
setTimeout(() => icon.remove(), 320);
|
||||
this.fileIcons.delete(name);
|
||||
@ -2420,6 +2447,28 @@ export class MonitorDirector implements MonitorDriver {
|
||||
await sleep(400);
|
||||
};
|
||||
|
||||
this.sceneHandlers.wait = async (payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'wait', '正在等待');
|
||||
const duration =
|
||||
Number(payload?.arguments?.duration || payload?.arguments?.seconds || payload?.seconds || payload?.duration) || 5;
|
||||
await this.movePointerToDesktop();
|
||||
await this.click({ right: true });
|
||||
this.showContextMenu('desktop');
|
||||
await sleep(200);
|
||||
const highlighted = await this.highlightMenu('desktop', 'wait');
|
||||
if (!highlighted) {
|
||||
const btn = this.elements.desktopMenu.querySelector<HTMLButtonElement>('button[data-action="wait"]');
|
||||
if (btn) {
|
||||
await this.movePointerToElement(btn, { duration: 320 });
|
||||
}
|
||||
}
|
||||
await this.click();
|
||||
this.hideContextMenus();
|
||||
await sleep(140); // 等菜单完全收起后再出现等待提示
|
||||
const waitPromise = runtime.waitForResult(payload.executionId || payload.id).catch(() => null);
|
||||
await this.playWaitCountdown(duration, waitPromise);
|
||||
};
|
||||
|
||||
this.sceneHandlers.appendFile = async (payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'appendFile', '正在编辑');
|
||||
const rawPath = payload?.arguments?.path || payload?.argumentSnapshot?.path || 'file.txt';
|
||||
@ -2956,20 +3005,29 @@ export class MonitorDirector implements MonitorDriver {
|
||||
};
|
||||
|
||||
this.sceneHandlers.terminalSleep = async (payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'terminalSleep', '正在关闭终端');
|
||||
const { sessionId } = await this.ensureTerminalSessionReady(payload, { focusPrompt: false, activate: false });
|
||||
terminalMenuDebug('scene:terminalSleep:start', { sessionId });
|
||||
await this.openTerminalContextMenu(sessionId);
|
||||
terminalMenuDebug('scene:terminalSleep:menu-opened', { sessionId });
|
||||
await this.chooseTerminalMenuAction('close');
|
||||
this.closeTerminalSession(sessionId);
|
||||
// 如果已无会话,收起终端窗口以呈现“窗口被关闭”效果
|
||||
if (!this.terminalSessions.size) {
|
||||
this.closeWindow(this.elements.terminalWindow, { animate: true });
|
||||
this.applySceneStatus(runtime, 'terminalSleep', '正在等待');
|
||||
const duration =
|
||||
Number(payload?.arguments?.duration || payload?.arguments?.seconds || payload?.seconds || payload?.duration) || 5;
|
||||
await this.movePointerToDesktop();
|
||||
await this.click({ right: true });
|
||||
this.showContextMenu('desktop');
|
||||
await sleep(200);
|
||||
const highlighted = await this.highlightMenu('desktop', 'wait');
|
||||
if (!highlighted) {
|
||||
const btn = this.elements.desktopMenu.querySelector<HTMLButtonElement>('button[data-action="wait"]');
|
||||
if (btn) {
|
||||
await this.movePointerToElement(btn, { duration: 320 });
|
||||
}
|
||||
}
|
||||
await sleep(320);
|
||||
await this.click();
|
||||
this.hideContextMenus();
|
||||
await sleep(140); // 等菜单完全收起后再出现等待提示
|
||||
const waitPromise = runtime.waitForResult(payload.executionId || payload.id).catch(() => null);
|
||||
await this.playWaitCountdown(duration, waitPromise);
|
||||
};
|
||||
|
||||
this.sceneHandlers.sleep = this.sceneHandlers.wait;
|
||||
|
||||
this.sceneHandlers.webSave = async (_payload, runtime) => {
|
||||
this.applySceneStatus(runtime, 'webSave', '正在保存网页');
|
||||
const targetUrl =
|
||||
@ -3521,9 +3579,57 @@ export class MonitorDirector implements MonitorDriver {
|
||||
menu.classList.remove('visible');
|
||||
menu.querySelectorAll('button').forEach(btn => btn.classList.remove('active'));
|
||||
});
|
||||
this.hideSecondaryMenu();
|
||||
this.hideWaitOverlay();
|
||||
this.terminalContextSessionId = null;
|
||||
}
|
||||
|
||||
private hideSecondaryMenu() {
|
||||
if (this.secondaryMenu && this.secondaryMenu.parentElement) {
|
||||
this.secondaryMenu.parentElement.removeChild(this.secondaryMenu);
|
||||
}
|
||||
this.secondaryMenu = null;
|
||||
}
|
||||
|
||||
private showSecondaryMenu(anchorMenu: HTMLElement | null, items: Array<{ label: string; action: string }>) {
|
||||
this.hideSecondaryMenu();
|
||||
if (!anchorMenu || !items.length) {
|
||||
return null;
|
||||
}
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'context-menu secondary-menu visible';
|
||||
items.forEach(item => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = item.label;
|
||||
btn.dataset.action = item.action;
|
||||
menu.appendChild(btn);
|
||||
});
|
||||
this.elements.screen.appendChild(menu);
|
||||
const rect = anchorMenu.getBoundingClientRect();
|
||||
const left = rect.right - this.screenRect.left + 10;
|
||||
const top = rect.top - this.screenRect.top + 4;
|
||||
const { x, y } = this.clampToScreen(left, top, menu.offsetWidth || 180, menu.offsetHeight || 60);
|
||||
menu.style.left = `${x}px`;
|
||||
menu.style.top = `${y}px`;
|
||||
this.secondaryMenu = menu;
|
||||
return menu;
|
||||
}
|
||||
|
||||
private async chooseSecondaryMenuAction(action: string) {
|
||||
const btn = this.secondaryMenu?.querySelector<HTMLButtonElement>(`button[data-action="${action}"]`) || null;
|
||||
if (!btn) {
|
||||
return false;
|
||||
}
|
||||
await this.movePointerToElement(btn, { duration: 320 });
|
||||
btn.classList.add('active');
|
||||
await sleep(200);
|
||||
btn.classList.remove('active');
|
||||
await this.click();
|
||||
await sleep(140);
|
||||
this.hideSecondaryMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async highlightMenu(type: ContextMenuType, action: string): Promise<boolean> {
|
||||
const map: Record<ContextMenuType, HTMLElement> = {
|
||||
desktop: this.elements.desktopMenu,
|
||||
@ -3620,6 +3726,59 @@ export class MonitorDirector implements MonitorDriver {
|
||||
return menu.classList.contains('visible');
|
||||
}
|
||||
|
||||
private hideWaitOverlay() {
|
||||
if (!this.elements.waitOverlay) return;
|
||||
this.elements.waitOverlay.classList.remove('active');
|
||||
if (this.waitOverlayTimer) {
|
||||
clearInterval(this.waitOverlayTimer);
|
||||
this.waitOverlayTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private formatWaitText(seconds: number) {
|
||||
if (seconds >= 60) {
|
||||
const m = Math.floor(seconds / 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const s = (seconds % 60).toString().padStart(2, '0');
|
||||
return `${m}:${s}`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
private async playWaitCountdown(durationSeconds = 5, until?: Promise<any>) {
|
||||
const total = Math.max(1, Math.round(durationSeconds));
|
||||
if (!this.elements.waitOverlay || !this.elements.waitCountdown) {
|
||||
return;
|
||||
}
|
||||
const tip = this.getPointerTip();
|
||||
this.elements.waitOverlay.style.left = `${tip.x + 16}px`;
|
||||
this.elements.waitOverlay.style.top = `${tip.y - 28}px`;
|
||||
const start = performance.now();
|
||||
this.elements.waitCountdown.textContent = this.formatWaitText(total);
|
||||
this.elements.waitOverlay.classList.add('active');
|
||||
if (this.waitOverlayTimer) {
|
||||
clearInterval(this.waitOverlayTimer);
|
||||
this.waitOverlayTimer = null;
|
||||
}
|
||||
const update = () => {
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
const remain = Math.max(0, total - elapsed);
|
||||
if (this.elements.waitCountdown) {
|
||||
this.elements.waitCountdown.textContent = this.formatWaitText(Math.ceil(remain));
|
||||
}
|
||||
};
|
||||
this.waitOverlayTimer = window.setInterval(update, 200);
|
||||
const done = until || Promise.resolve();
|
||||
try {
|
||||
await done;
|
||||
} catch (_err) {
|
||||
// 忽略错误,仍然结束动画
|
||||
}
|
||||
update();
|
||||
this.hideWaitOverlay();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指针瞬时对准目标元素的中心,防止动画收尾时产生偏移
|
||||
*/
|
||||
@ -5134,25 +5293,49 @@ export class MonitorDirector implements MonitorDriver {
|
||||
await sleep(180);
|
||||
}
|
||||
|
||||
private async scrollTodoItemIntoView(card: HTMLElement | null) {
|
||||
private async scrollTodoItemIntoView(card: HTMLElement | null, options: { waitMs?: number } = {}) {
|
||||
if (!card || !this.elements.todoList) return;
|
||||
const body = this.elements.todoList;
|
||||
const cardTop = card.offsetTop;
|
||||
const cardBottom = cardTop + card.offsetHeight;
|
||||
const viewTop = body.scrollTop;
|
||||
const viewBottom = viewTop + body.clientHeight;
|
||||
const margin = 8;
|
||||
const wait = typeof options.waitMs === 'number' ? options.waitMs : 120;
|
||||
|
||||
// 如果完整可见,直接返回
|
||||
if (cardTop >= viewTop && cardBottom <= viewBottom) {
|
||||
const measure = () => {
|
||||
const bodyRect = body.getBoundingClientRect();
|
||||
const cardRect = card.getBoundingClientRect();
|
||||
const relTop = cardRect.top - bodyRect.top + body.scrollTop;
|
||||
const relBottom = relTop + cardRect.height;
|
||||
const viewTop = body.scrollTop;
|
||||
const viewBottom = viewTop + body.clientHeight;
|
||||
return { relTop, relBottom, viewTop, viewBottom };
|
||||
};
|
||||
|
||||
let { relTop, relBottom, viewTop, viewBottom } = measure();
|
||||
|
||||
// 已完整可见(含少量缓冲),直接返回
|
||||
if (relTop >= viewTop + margin && relBottom <= viewBottom - margin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 让整个卡片进入视口:若底部被遮挡,滚到卡片底部露出;否则滚到卡片顶部
|
||||
const targetTop =
|
||||
cardBottom > viewBottom ? cardBottom - body.clientHeight + 4 : Math.max(0, cardTop - 4);
|
||||
// 计算需要滚动的位置(基于相对坐标,避免 offsetParent 不一致的问题)
|
||||
const needsScrollDown = relBottom > viewBottom - margin;
|
||||
const targetTop = needsScrollDown ? relBottom - body.clientHeight + margin : Math.max(0, relTop - margin);
|
||||
const clampedTop = Math.max(0, targetTop);
|
||||
|
||||
body.scrollTop = Math.max(0, targetTop);
|
||||
await sleep(30);
|
||||
body.scrollTo({ top: clampedTop, behavior: 'smooth' });
|
||||
await this.waitForScrollSettled(body, clampedTop);
|
||||
if (wait > 0) {
|
||||
await sleep(wait);
|
||||
}
|
||||
|
||||
// 二次校验,确保整块已进入可视区域
|
||||
({ relTop, relBottom, viewTop, viewBottom } = measure());
|
||||
if (relTop < viewTop + margin) {
|
||||
body.scrollTop = Math.max(0, relTop - margin);
|
||||
await sleep(30);
|
||||
} else if (relBottom > viewBottom - margin) {
|
||||
body.scrollTop = relBottom - body.clientHeight + margin;
|
||||
await sleep(30);
|
||||
}
|
||||
}
|
||||
|
||||
private async toggleTodoItem(text?: string | null, done?: boolean, index?: number | null) {
|
||||
|
||||
@ -21,10 +21,12 @@ export const SCENE_PROGRESS_LABELS: Record<string, string> = {
|
||||
todoFinish: '正在完成任务',
|
||||
todoFinishConfirm: '正在确认任务',
|
||||
todoDelete: '正在移除任务',
|
||||
wait: '正在等待',
|
||||
sleep: '正在等待',
|
||||
createFolder: '正在创建文件夹',
|
||||
renameFile: '正在重命名',
|
||||
terminalReset: '正在重置终端',
|
||||
terminalSleep: '正在暂停终端',
|
||||
terminalSleep: '准备等待',
|
||||
terminalRun: '正在执行',
|
||||
ocr: '正在提取',
|
||||
memory: '正在同步记忆',
|
||||
|
||||
@ -1253,6 +1253,87 @@
|
||||
border: 1px solid rgba(39, 201, 119, 0.4);
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-window {
|
||||
width: 260px;
|
||||
height: 180px;
|
||||
top: 120px;
|
||||
left: 820px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 20% 20%, rgba(118, 148, 255, 0.18), transparent 45%), radial-gradient(circle at 80% 30%, rgba(92, 209, 183, 0.16), transparent 40%), rgba(248, 251, 255, 0.9);
|
||||
border-radius: 0 0 14px 14px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
perspective: 900px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-separator {
|
||||
font-size: 26px;
|
||||
color: #2d3748;
|
||||
margin: 0 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-digit {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 56px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #101827, #0b1222);
|
||||
color: #f5fbff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 24px rgba(14, 32, 68, 0.35);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-digit .top,
|
||||
.virtual-monitor-surface .flip-digit .bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-digit .top {
|
||||
top: 0;
|
||||
transform-origin: bottom;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-digit .bottom {
|
||||
bottom: 0;
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .flip-digit.flipping {
|
||||
animation: flipCard 0.38s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes flipCard {
|
||||
0% { transform: rotateX(0deg); }
|
||||
50% { transform: rotateX(-82deg); }
|
||||
100% { transform: rotateX(0deg); }
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .context-menu {
|
||||
position: absolute;
|
||||
background: rgba(9, 16, 32, 0.96);
|
||||
@ -1278,6 +1359,11 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .context-menu.secondary-menu {
|
||||
z-index: 45;
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .context-menu button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@ -1317,6 +1403,55 @@
|
||||
--arrow-size: 18px;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-overlay {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease;
|
||||
z-index: 48;
|
||||
filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-overlay.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-overlay .z {
|
||||
position: absolute;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #111;
|
||||
opacity: 0;
|
||||
animation: swayUpToRight 2s ease-out infinite;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-overlay.active .z {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .wait-overlay .z-2 { animation-delay: 0.25s; }
|
||||
.virtual-monitor-surface .wait-overlay .z-3 { animation-delay: 0.5s; }
|
||||
.virtual-monitor-surface .wait-overlay .z-4 { animation-delay: 0.75s; }
|
||||
|
||||
.virtual-monitor-surface .wait-overlay .wait-countdown {
|
||||
position: absolute;
|
||||
left: 34px;
|
||||
top: 8px;
|
||||
padding: 0;
|
||||
color: #111;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@keyframes swayUpToRight {
|
||||
0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
|
||||
100% { transform: translate(80px, -100px) rotate(30deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.virtual-monitor-surface .speech-bubble.visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user