feat(chat): show_html 卡片工具栏改为 ⋯ 菜单并新增全屏预览
This commit is contained in:
parent
79b916f44c
commit
2db77fe663
@ -505,42 +505,309 @@ function escapeHtml(input: string) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function bindShowHtmlRefreshControl(wrapper: HTMLElement, onRefresh: () => void) {
|
||||
/**
|
||||
* show_html 卡片全屏预览载荷。
|
||||
* - js="on" 卡片:直接复用已注入 CSP/守卫的 srcdoc,沙箱允许脚本
|
||||
* - js="off" 卡片:用 sanitize 后的 HTML 现场构建 srcdoc,沙箱禁脚本(与卡片内语义一致)
|
||||
*/
|
||||
interface ShowHtmlFullscreenPayload {
|
||||
srcdoc: string;
|
||||
allowScripts: boolean;
|
||||
}
|
||||
|
||||
interface ShowHtmlCardControls {
|
||||
onRefresh: () => void;
|
||||
getFullscreenPayload: () => ShowHtmlFullscreenPayload | null;
|
||||
}
|
||||
|
||||
const SHOW_HTML_CONTROLS_KEY = '__showHtmlCardControls';
|
||||
|
||||
function getShowHtmlCardControls(wrapper: HTMLElement): ShowHtmlCardControls | null {
|
||||
return ((wrapper as any)[SHOW_HTML_CONTROLS_KEY] as ShowHtmlCardControls | undefined) || null;
|
||||
}
|
||||
|
||||
function triggerShowHtmlCardRefresh(wrapper: HTMLElement, btn: HTMLElement | null) {
|
||||
const controls = getShowHtmlCardControls(wrapper);
|
||||
if (!controls) return;
|
||||
markShowTagDrawingActive(1200);
|
||||
if (btn) {
|
||||
btn.classList.remove('is-refreshing');
|
||||
// 触发重放动画
|
||||
void btn.offsetWidth;
|
||||
btn.classList.add('is-refreshing');
|
||||
window.setTimeout(() => btn.classList.remove('is-refreshing'), 720);
|
||||
}
|
||||
try {
|
||||
controls.onRefresh();
|
||||
} catch (error) {
|
||||
debugShowHtmlLog('refresh:error', {
|
||||
message: error instanceof Error ? error.message : String(error || '')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 卡片 ⋯ 菜单(全局 fixed 单例,同一时间只存在一个) =====
|
||||
let showHtmlCardMenuEl: HTMLElement | null = null;
|
||||
let showHtmlCardMenuAnchor: HTMLElement | null = null;
|
||||
let showHtmlCardMenuEventsBound = false;
|
||||
|
||||
function closeShowHtmlCardMenu() {
|
||||
if (showHtmlCardMenuEl) {
|
||||
showHtmlCardMenuEl.remove();
|
||||
showHtmlCardMenuEl = null;
|
||||
showHtmlCardMenuAnchor = null;
|
||||
}
|
||||
}
|
||||
|
||||
function bindShowHtmlCardMenuGlobalEvents() {
|
||||
if (showHtmlCardMenuEventsBound || typeof document === 'undefined') return;
|
||||
showHtmlCardMenuEventsBound = true;
|
||||
document.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (!showHtmlCardMenuEl) return;
|
||||
const target = event.target as Node | null;
|
||||
if (target && showHtmlCardMenuEl.contains(target)) return;
|
||||
// 锚点按钮自身走 toggle 逻辑,不在全局监听里关闭
|
||||
if (target && showHtmlCardMenuAnchor?.contains(target)) return;
|
||||
closeShowHtmlCardMenu();
|
||||
},
|
||||
true
|
||||
);
|
||||
document.addEventListener(
|
||||
'keydown',
|
||||
(event) => {
|
||||
if (event.key === 'Escape' && showHtmlCardMenuEl) {
|
||||
event.stopPropagation();
|
||||
closeShowHtmlCardMenu();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
window.addEventListener('resize', closeShowHtmlCardMenu);
|
||||
// 聊天区滚动时菜单位置即失效,直接关闭(capture 以捕获任意滚动容器)
|
||||
document.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
if (showHtmlCardMenuEl) closeShowHtmlCardMenu();
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function buildShowHtmlCardMenuItem(label: string): HTMLButtonElement {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'show-html-card-menu__item';
|
||||
item.textContent = label;
|
||||
return item;
|
||||
}
|
||||
|
||||
function openShowHtmlCardMenu(anchor: HTMLElement, wrapper: HTMLElement) {
|
||||
bindShowHtmlCardMenuGlobalEvents();
|
||||
if (showHtmlCardMenuEl && showHtmlCardMenuAnchor === anchor) {
|
||||
closeShowHtmlCardMenu();
|
||||
return;
|
||||
}
|
||||
closeShowHtmlCardMenu();
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'show-html-card-menu';
|
||||
menu.setAttribute('role', 'menu');
|
||||
|
||||
const refreshItem = buildShowHtmlCardMenuItem('刷新');
|
||||
refreshItem.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeShowHtmlCardMenu();
|
||||
triggerShowHtmlCardRefresh(wrapper, anchor);
|
||||
};
|
||||
|
||||
const fullscreenItem = buildShowHtmlCardMenuItem('全屏');
|
||||
fullscreenItem.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const controls = getShowHtmlCardControls(wrapper);
|
||||
closeShowHtmlCardMenu();
|
||||
const payload = controls?.getFullscreenPayload?.();
|
||||
if (!payload || !payload.srcdoc) return;
|
||||
openShowHtmlFullscreen(payload);
|
||||
};
|
||||
|
||||
menu.appendChild(refreshItem);
|
||||
menu.appendChild(fullscreenItem);
|
||||
document.body.appendChild(menu);
|
||||
|
||||
// 定位:按钮下方、右对齐,视口边缘留 8px 安全距离
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const menuWidth = menu.offsetWidth;
|
||||
let left = Math.round(rect.right - menuWidth);
|
||||
left = Math.max(8, Math.min(left, window.innerWidth - menuWidth - 8));
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${Math.round(rect.bottom + 6)}px`;
|
||||
|
||||
showHtmlCardMenuEl = menu;
|
||||
showHtmlCardMenuAnchor = anchor;
|
||||
}
|
||||
|
||||
// ===== show_html 全屏预览(应用内 overlay,复用沙箱 srcdoc,不走路由) =====
|
||||
interface ShowHtmlFullscreenState {
|
||||
root: HTMLElement;
|
||||
iframe: HTMLIFrameElement;
|
||||
payload: ShowHtmlFullscreenPayload | null;
|
||||
}
|
||||
|
||||
const SHOW_HTML_FULLSCREEN_EMPTY_SRCDOC =
|
||||
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
||||
|
||||
let showHtmlFullscreenState: ShowHtmlFullscreenState | null = null;
|
||||
let showHtmlFullscreenEventsBound = false;
|
||||
|
||||
function isShowHtmlFullscreenOpen(): boolean {
|
||||
return !!showHtmlFullscreenState?.root.classList.contains('is-open');
|
||||
}
|
||||
|
||||
function bindShowHtmlFullscreenGlobalEvents() {
|
||||
if (showHtmlFullscreenEventsBound || typeof document === 'undefined') return;
|
||||
showHtmlFullscreenEventsBound = true;
|
||||
// 桌面端 Esc 退出;移动端没有 Esc,依赖顶栏 ✕ 按钮(触屏主路径)
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Escape' || !isShowHtmlFullscreenOpen()) return;
|
||||
event.stopPropagation();
|
||||
closeShowHtmlFullscreen();
|
||||
});
|
||||
}
|
||||
|
||||
function ensureShowHtmlFullscreenDom(): ShowHtmlFullscreenState {
|
||||
if (showHtmlFullscreenState) return showHtmlFullscreenState;
|
||||
bindShowHtmlFullscreenGlobalEvents();
|
||||
|
||||
const root = document.createElement('div');
|
||||
root.className = 'show-html-fullscreen';
|
||||
root.setAttribute('role', 'dialog');
|
||||
root.setAttribute('aria-modal', 'true');
|
||||
root.setAttribute('aria-label', '卡片全屏预览');
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'show-html-fullscreen__bar';
|
||||
const title = document.createElement('span');
|
||||
title.className = 'show-html-fullscreen__title';
|
||||
title.textContent = '全屏预览';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'show-html-fullscreen__actions';
|
||||
|
||||
const refreshBtn = document.createElement('button');
|
||||
refreshBtn.type = 'button';
|
||||
refreshBtn.className = 'show-html-fullscreen__btn';
|
||||
refreshBtn.title = '刷新';
|
||||
refreshBtn.setAttribute('aria-label', '刷新');
|
||||
refreshBtn.textContent = '↻';
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
closeBtn.className = 'show-html-fullscreen__btn show-html-fullscreen__btn--close';
|
||||
closeBtn.title = '关闭(Esc)';
|
||||
closeBtn.setAttribute('aria-label', '关闭全屏预览');
|
||||
closeBtn.textContent = '✕';
|
||||
|
||||
actions.appendChild(refreshBtn);
|
||||
actions.appendChild(closeBtn);
|
||||
bar.appendChild(title);
|
||||
bar.appendChild(actions);
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'show-html-fullscreen__body';
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'show-html-fullscreen__iframe';
|
||||
iframe.setAttribute('referrerpolicy', 'no-referrer');
|
||||
// 与内联卡片一致:overlay 用 CSS fixed 占满视口,不需要浏览器 Fullscreen API
|
||||
iframe.setAttribute('allow', "fullscreen 'none'");
|
||||
body.appendChild(iframe);
|
||||
|
||||
root.appendChild(bar);
|
||||
root.appendChild(body);
|
||||
|
||||
refreshBtn.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const state = showHtmlFullscreenState;
|
||||
if (!state?.payload) return;
|
||||
// 与内联卡片刷新同策略:先挂空文档再重挂,强制 iframe 内页面重载
|
||||
const keep = state.payload.srcdoc;
|
||||
state.iframe.srcdoc = SHOW_HTML_FULLSCREEN_EMPTY_SRCDOC;
|
||||
requestAnimationFrame(() => {
|
||||
if (showHtmlFullscreenState?.payload) {
|
||||
showHtmlFullscreenState.iframe.srcdoc = keep;
|
||||
}
|
||||
});
|
||||
debugShowHtmlLog('fullscreen:refresh', { srcdocLength: keep.length });
|
||||
};
|
||||
closeBtn.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeShowHtmlFullscreen();
|
||||
};
|
||||
|
||||
document.body.appendChild(root);
|
||||
showHtmlFullscreenState = { root, iframe, payload: null };
|
||||
return showHtmlFullscreenState;
|
||||
}
|
||||
|
||||
function openShowHtmlFullscreen(payload: ShowHtmlFullscreenPayload) {
|
||||
if (typeof document === 'undefined') return;
|
||||
const state = ensureShowHtmlFullscreenDom();
|
||||
state.payload = payload;
|
||||
// js=off 卡片内容已 sanitize,全屏时连脚本执行权也不给,语义与卡片内一致
|
||||
state.iframe.setAttribute('sandbox', payload.allowScripts ? 'allow-scripts' : '');
|
||||
state.iframe.srcdoc = payload.srcdoc;
|
||||
state.root.classList.add('is-open');
|
||||
document.documentElement.classList.add('show-html-fullscreen-open');
|
||||
debugShowHtmlLog('fullscreen:open', {
|
||||
allowScripts: payload.allowScripts,
|
||||
srcdocLength: payload.srcdoc.length
|
||||
});
|
||||
}
|
||||
|
||||
function closeShowHtmlFullscreen() {
|
||||
const state = showHtmlFullscreenState;
|
||||
if (!state || !isShowHtmlFullscreenOpen()) return;
|
||||
state.root.classList.remove('is-open');
|
||||
document.documentElement.classList.remove('show-html-fullscreen-open');
|
||||
state.payload = null;
|
||||
// 清空 srcdoc:停掉 iframe 内脚本/定时器并释放内存
|
||||
state.iframe.srcdoc = SHOW_HTML_FULLSCREEN_EMPTY_SRCDOC;
|
||||
debugShowHtmlLog('fullscreen:close', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 show_html 卡片右上角工具栏(⋯ 菜单:刷新 / 全屏)。
|
||||
* 渲染器每次重渲染都会调用:最新回调挂在 wrapper 上,按钮与菜单只创建一次。
|
||||
*/
|
||||
function bindShowHtmlCardControls(wrapper: HTMLElement, controls: ShowHtmlCardControls) {
|
||||
(wrapper as any)[SHOW_HTML_CONTROLS_KEY] = controls;
|
||||
let toolbar = wrapper.querySelector(':scope > .chat-inline-card__toolbar') as HTMLElement | null;
|
||||
if (!toolbar) {
|
||||
toolbar = document.createElement('div');
|
||||
toolbar.className = 'chat-inline-card__toolbar';
|
||||
wrapper.appendChild(toolbar);
|
||||
}
|
||||
let refreshBtn = toolbar.querySelector(
|
||||
':scope > .chat-inline-card__refresh-btn'
|
||||
let menuBtn = toolbar.querySelector(
|
||||
':scope > .chat-inline-card__menu-btn'
|
||||
) as HTMLButtonElement | null;
|
||||
if (!refreshBtn) {
|
||||
refreshBtn = document.createElement('button');
|
||||
refreshBtn.type = 'button';
|
||||
refreshBtn.className = 'chat-inline-card__refresh-btn';
|
||||
refreshBtn.title = '刷新卡片';
|
||||
refreshBtn.setAttribute('aria-label', '刷新卡片');
|
||||
refreshBtn.textContent = '↻';
|
||||
toolbar.appendChild(refreshBtn);
|
||||
if (!menuBtn) {
|
||||
menuBtn = document.createElement('button');
|
||||
menuBtn.type = 'button';
|
||||
menuBtn.className = 'chat-inline-card__menu-btn';
|
||||
menuBtn.title = '卡片操作';
|
||||
menuBtn.setAttribute('aria-label', '卡片操作');
|
||||
menuBtn.setAttribute('aria-haspopup', 'menu');
|
||||
menuBtn.textContent = '⋯';
|
||||
toolbar.appendChild(menuBtn);
|
||||
}
|
||||
refreshBtn.onclick = (event) => {
|
||||
menuBtn.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
markShowTagDrawingActive(1200);
|
||||
refreshBtn!.classList.remove('is-refreshing');
|
||||
// 触发重放动画
|
||||
void refreshBtn!.offsetWidth;
|
||||
refreshBtn!.classList.add('is-refreshing');
|
||||
try {
|
||||
onRefresh();
|
||||
} catch (error) {
|
||||
debugShowHtmlLog('refresh:error', {
|
||||
message: error instanceof Error ? error.message : String(error || '')
|
||||
});
|
||||
} finally {
|
||||
window.setTimeout(() => refreshBtn?.classList.remove('is-refreshing'), 720);
|
||||
}
|
||||
openShowHtmlCardMenu(menuBtn!, wrapper);
|
||||
};
|
||||
}
|
||||
|
||||
@ -787,21 +1054,29 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
};
|
||||
showHtmlJsIframeRenderByPath.set(pathKey, persistent);
|
||||
}
|
||||
bindShowHtmlRefreshControl(persistent.wrapper, () => {
|
||||
// 强制重新挂载同一份 srcdoc,触发 iframe 内页面重载
|
||||
const keep = persistent?.srcdoc || srcdoc;
|
||||
persistent.iframe.srcdoc =
|
||||
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
||||
requestAnimationFrame(() => {
|
||||
if (persistent?.iframe) {
|
||||
persistent.iframe.srcdoc = keep;
|
||||
}
|
||||
});
|
||||
debugShowHtmlLog('refresh:iframe', {
|
||||
pathKey,
|
||||
ratioKey: persistent?.ratioKey || ratioKey,
|
||||
srcdocLength: keep.length
|
||||
});
|
||||
bindShowHtmlCardControls(persistent.wrapper, {
|
||||
onRefresh: () => {
|
||||
// 强制重新挂载同一份 srcdoc,触发 iframe 内页面重载
|
||||
const keep = persistent?.srcdoc || srcdoc;
|
||||
persistent.iframe.srcdoc =
|
||||
'<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>';
|
||||
requestAnimationFrame(() => {
|
||||
if (persistent?.iframe) {
|
||||
persistent.iframe.srcdoc = keep;
|
||||
}
|
||||
});
|
||||
debugShowHtmlLog('refresh:iframe', {
|
||||
pathKey,
|
||||
ratioKey: persistent?.ratioKey || ratioKey,
|
||||
srcdocLength: keep.length
|
||||
});
|
||||
},
|
||||
// 全屏直接复用当前 srcdoc(已含 CSP meta 与守卫脚本),沙箱保持 allow-scripts
|
||||
getFullscreenPayload: () => {
|
||||
const current = persistent?.srcdoc || '';
|
||||
if (!current) return null;
|
||||
return { srcdoc: current, allowScripts: true };
|
||||
}
|
||||
});
|
||||
const needsUpdate =
|
||||
persistent.encoded !== effectiveEncoded ||
|
||||
@ -933,19 +1208,27 @@ function renderShowImages(root: ParentNode | null = document) {
|
||||
};
|
||||
showHtmlPersistentRenderByPath.set(pathKey, persistent);
|
||||
}
|
||||
bindShowHtmlRefreshControl(persistent.wrapper, () => {
|
||||
const keep = persistent?.safeHtml || '';
|
||||
persistent.root.innerHTML = '';
|
||||
requestAnimationFrame(() => {
|
||||
if (persistent?.root) {
|
||||
persistent.root.innerHTML = keep;
|
||||
}
|
||||
});
|
||||
debugShowHtmlLog('refresh:shadow-root', {
|
||||
pathKey,
|
||||
ratioKey: persistent?.ratioKey || ratioKey,
|
||||
safeLength: keep.length
|
||||
});
|
||||
bindShowHtmlCardControls(persistent.wrapper, {
|
||||
onRefresh: () => {
|
||||
const keep = persistent?.safeHtml || '';
|
||||
persistent.root.innerHTML = '';
|
||||
requestAnimationFrame(() => {
|
||||
if (persistent?.root) {
|
||||
persistent.root.innerHTML = keep;
|
||||
}
|
||||
});
|
||||
debugShowHtmlLog('refresh:shadow-root', {
|
||||
pathKey,
|
||||
ratioKey: persistent?.ratioKey || ratioKey,
|
||||
safeLength: keep.length
|
||||
});
|
||||
},
|
||||
// js=off 内容已 sanitize,全屏走禁脚本沙箱 iframe,渲染效果与 Shadow DOM 一致
|
||||
getFullscreenPayload: () => {
|
||||
const current = persistent?.safeHtml || '';
|
||||
if (!current.trim()) return null;
|
||||
return { srcdoc: buildShowHtmlIframeSrcdoc(current), allowScripts: false };
|
||||
}
|
||||
});
|
||||
|
||||
const needsUpdate =
|
||||
@ -1369,6 +1652,9 @@ export function teardownShowImageObserver() {
|
||||
showHtmlJsPendingRenderByPath.clear();
|
||||
showHtmlJsIframeRenderByPath.clear();
|
||||
showTagObservedContainer = null;
|
||||
// 菜单/全屏层是全局单例,卡片 Map 清空后其引用的 wrapper 已卸载,必须一并关闭
|
||||
closeShowHtmlCardMenu();
|
||||
closeShowHtmlFullscreen();
|
||||
}
|
||||
|
||||
function updateViewportHeightVar() {
|
||||
|
||||
@ -1758,42 +1758,42 @@ body[data-theme='dark'] .more-icon {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chat-inline-card__refresh-btn {
|
||||
/* ⋯ 菜单按钮:纯文字风格,无边框无背景,hover 仅变色;保留 26px 点击热区 */
|
||||
.chat-inline-card__menu-btn {
|
||||
pointer-events: auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-default);
|
||||
background: color-mix(in srgb, var(--surface-card) 78%, transparent);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
/* ⋯ 字形视觉重心偏下,上抬补偵使其看起来居中 */
|
||||
padding-bottom: 5px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.chat-inline-card__refresh-btn:hover {
|
||||
.chat-inline-card__menu-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--highlight) 72%, transparent);
|
||||
}
|
||||
|
||||
.chat-inline-card__refresh-btn.is-refreshing {
|
||||
animation: chat-inline-refresh-spin 0.6s linear 1;
|
||||
.chat-inline-card__menu-btn.is-refreshing {
|
||||
animation: chat-inline-menu-pulse 0.6s ease 1;
|
||||
}
|
||||
|
||||
@keyframes chat-inline-refresh-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@keyframes chat-inline-menu-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
40% {
|
||||
opacity: 0.35;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1818,6 +1818,148 @@ body[data-theme='dark'] .more-icon {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ---------------- show_html 卡片 ⋯ 菜单(全局 fixed 单例) ---------------- */
|
||||
|
||||
.show-html-card-menu {
|
||||
position: fixed;
|
||||
z-index: 3050;
|
||||
min-width: 112px;
|
||||
padding: 4px;
|
||||
background: var(--surface-raised);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-card);
|
||||
animation: show-html-card-menu-in 0.16s ease-out;
|
||||
}
|
||||
|
||||
@keyframes show-html-card-menu-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-3px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.show-html-card-menu__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.show-html-card-menu__item:hover {
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
|
||||
/* ---------------- show_html 全屏预览 overlay ---------------- */
|
||||
|
||||
/* 打开期间锁定底层页面滚动(聊天区滚动容器在 body 下) */
|
||||
html.show-html-fullscreen-open,
|
||||
html.show-html-fullscreen-open body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.show-html-fullscreen {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* 与聊天区同一套视口高度变量,规避移动端 100vh 地址栏问题 */
|
||||
height: var(--app-viewport, 100vh);
|
||||
z-index: 3000;
|
||||
background: var(--surface-base);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.show-html-fullscreen.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.show-html-fullscreen__bar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
/* 手机刘海/状态栏安全区;移动端无法 Esc,关闭按钮必须永远可见可达 */
|
||||
padding: calc(8px + env(safe-area-inset-top, 0px))
|
||||
calc(10px + env(safe-area-inset-right, 0px)) 8px
|
||||
calc(16px + env(safe-area-inset-left, 0px));
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.show-html-fullscreen__title {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.show-html-fullscreen__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 与卡片 ⋯ 按钮一致的纯文字风格:无边框无背景,hover 仅变色 */
|
||||
.show-html-fullscreen__btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.show-html-fullscreen__btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.show-html-fullscreen__body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.show-html-fullscreen__iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 移动端加大热区,保证触屏可可靠点中关闭按钮 */
|
||||
@media (max-width: 768px) {
|
||||
.show-html-fullscreen__btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 兼容旧类名(可逐步移除) */
|
||||
.chat-inline-image__error {
|
||||
@extend .chat-inline-card__error;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user