polish(frontend): 侧栏与徽章可读性,智能悬停提示
降权全站字重;优化右栏公告/数据块;类型状态徽章用固定浅色区分;TipHost 仅在文案不一致、非文字或省略时显示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,67 @@ type TipState = {
|
||||
place: "top" | "bottom";
|
||||
} | null;
|
||||
|
||||
function norm(s: string): string {
|
||||
return s.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
/** 可见文字:跳过 aria-hidden / svg / img,避免把图标标题算进来 */
|
||||
function getVisibleText(root: Element): string {
|
||||
const parts: string[] = [];
|
||||
const walk = (node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const t = node.textContent?.replace(/\s+/g, " ").trim();
|
||||
if (t) parts.push(t);
|
||||
return;
|
||||
}
|
||||
if (!(node instanceof Element)) return;
|
||||
if (node.getAttribute("aria-hidden") === "true") return;
|
||||
const tag = node.tagName.toUpperCase();
|
||||
if (tag === "SVG" || tag === "IMG" || tag === "CANVAS" || tag === "PATH") return;
|
||||
for (const child of node.childNodes) walk(child);
|
||||
};
|
||||
walk(root);
|
||||
return norm(parts.join(" "));
|
||||
}
|
||||
|
||||
/** 是否存在省略截断(含 line-clamp) */
|
||||
function isVisuallyTruncated(root: Element): boolean {
|
||||
const nodes: Element[] = [root, ...Array.from(root.querySelectorAll("*"))];
|
||||
for (const el of nodes) {
|
||||
if (!(el instanceof HTMLElement)) continue;
|
||||
if (el.clientWidth <= 0 && el.clientHeight <= 0) continue;
|
||||
const st = getComputedStyle(el);
|
||||
const ellipsis = st.textOverflow === "ellipsis" || el.classList.contains("truncate");
|
||||
const nowrapHidden =
|
||||
(st.overflowX === "hidden" || st.overflow === "hidden") &&
|
||||
(st.whiteSpace === "nowrap" || st.whiteSpace === "pre");
|
||||
const clampRaw = st.getPropertyValue("-webkit-line-clamp").trim();
|
||||
const clamped = Boolean(clampRaw) && clampRaw !== "none";
|
||||
if (ellipsis || nowrapHidden) {
|
||||
if (el.scrollWidth > el.clientWidth + 1) return true;
|
||||
}
|
||||
if (clamped && el.scrollHeight > el.clientHeight + 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅在以下场景显示气泡:
|
||||
* 1. 提示文案与可见内容不一致(且提示未完整出现在可见文字中)
|
||||
* 2. 停留目标无可见文字(图标等)
|
||||
* 3. 可见内容被省略截断,需要补全
|
||||
*/
|
||||
function shouldShowTip(host: Element, tip: string): boolean {
|
||||
if (isVisuallyTruncated(host)) return true;
|
||||
const visible = getVisibleText(host);
|
||||
if (!visible) return true;
|
||||
const nTip = norm(tip);
|
||||
const nVis = visible;
|
||||
if (nTip === nVis) return false;
|
||||
if (nVis.includes(nTip)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全站悬停即显提示:监听 [data-tip],替代浏览器原生 title 的延迟气泡。
|
||||
* 挂在 root layout 即可,业务侧写 data-tip="文案" 并保留 aria-label。
|
||||
@@ -38,6 +99,16 @@ export default function TipHost() {
|
||||
});
|
||||
};
|
||||
|
||||
const tryShow = (host: Element) => {
|
||||
const text = host.getAttribute("data-tip")?.trim();
|
||||
if (!text || !shouldShowTip(host, text)) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
activeEl.current = host;
|
||||
placeTip(host, text);
|
||||
};
|
||||
|
||||
const onOver = (e: Event) => {
|
||||
const t = e.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
@@ -46,18 +117,12 @@ export default function TipHost() {
|
||||
if (activeEl.current) hide();
|
||||
return;
|
||||
}
|
||||
const text = host.getAttribute("data-tip")?.trim();
|
||||
if (!text) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
activeEl.current = host;
|
||||
placeTip(host, text);
|
||||
tryShow(host);
|
||||
};
|
||||
|
||||
const onOut = (e: Event) => {
|
||||
if (!(e instanceof MouseEvent)) return;
|
||||
const related = e.relatedTarget;
|
||||
if (!(e instanceof MouseEvent) && !(e instanceof FocusEvent)) return;
|
||||
const related = "relatedTarget" in e ? e.relatedTarget : null;
|
||||
if (
|
||||
related instanceof Node &&
|
||||
activeEl.current &&
|
||||
@@ -70,12 +135,7 @@ export default function TipHost() {
|
||||
|
||||
const onScrollOrResize = () => {
|
||||
if (!activeEl.current) return;
|
||||
const text = activeEl.current.getAttribute("data-tip")?.trim();
|
||||
if (!text) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
placeTip(activeEl.current, text);
|
||||
tryShow(activeEl.current);
|
||||
};
|
||||
|
||||
// mouseover 冒泡,便于 closest;立即显示,无延时
|
||||
|
||||
Reference in New Issue
Block a user