Files
ST-Bionic-Memory-Ecology/ui/notice.js
Youzini-afk 702b6ac493 fix(notice): fix compact layout broken by duplicate CSS blocks and missing overflow rules
Root causes:
1. Duplicate .st-bme-notice__title and .st-bme-notice__message blocks (from
   original code) were overriding the new transition-enabled versions,
   stripping transitions and causing missing overflow:hidden on __message.
2. .st-bme-notice::after pseudo-element CSS was accidentally merged into
   .st-bme-notice__message--marquee block during previous edit.
3. Compact layout .st-bme-notice__content had overflow:hidden with no
   min-width constraint, causing the title to collapse to 'ST...'.

Fixes:
- Removed all duplicate CSS blocks; single canonical definitions with transitions
- Restored ::after pseudo-element as separate rule
- Added max-width:320px with overflow:hidden explicitly for compact content
- Added normal content max-width:360px with overflow:hidden
- Restored missing .st-bme-notice__actions base rule with transition
- Added .st-bme-notice__message overflow:hidden for max-height animation
2026-04-29 17:25:19 +08:00

596 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const STYLE_ID = "st-bme-notice-style";
const HOST_ID = "st-bme-notice-host";
function resolveNoticeDocument() {
const runtime = globalThis;
const chatDocument = runtime?.SillyTavern?.Chat?.document;
if (chatDocument && typeof chatDocument.createElement === "function") {
return chatDocument;
}
try {
return (window.parent && window.parent !== window ? window.parent : window).document;
} catch {
return document;
}
}
function ensureStyle(doc) {
if (doc.getElementById(STYLE_ID)) return;
const style = doc.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${HOST_ID} {
position: fixed;
top: 14px;
right: 14px;
z-index: 12020;
width: min(400px, calc(100vw - 28px));
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.st-bme-notice {
--st-bme-accent: #73b8ff;
pointer-events: auto;
position: relative;
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
align-items: start;
padding: 12px 12px 12px 10px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.16);
background:
radial-gradient(circle at 10% -10%, rgba(115, 184, 255, 0.2), transparent 52%),
linear-gradient(145deg, rgba(27, 37, 54, 0.95), rgba(12, 18, 29, 0.93));
box-shadow:
0 14px 34px rgba(4, 10, 17, 0.46),
inset 0 0 0 1px rgba(255, 255, 255, 0.04);
color: #edf3fb;
overflow: hidden;
transform: translateY(-8px) scale(0.985);
opacity: 0;
animation: stBmeNoticeIn 190ms ease forwards;
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei UI", sans-serif;
backdrop-filter: blur(10px) saturate(125%);
-webkit-backdrop-filter: blur(10px) saturate(125%);
transition:
grid-template-columns 260ms cubic-bezier(0.22, 1, 0.36, 1),
padding 260ms cubic-bezier(0.22, 1, 0.36, 1),
border-radius 260ms cubic-bezier(0.22, 1, 0.36, 1),
width 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.st-bme-notice[data-layout="compact"] {
display: flex;
align-items: center;
gap: 10px;
width: fit-content;
max-width: 100%;
align-self: flex-end;
padding: 10px;
}
.st-bme-notice__content {
min-width: 0;
transition: max-width 280ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 220ms ease;
}
.st-bme-notice[data-layout="normal"] .st-bme-notice__content {
max-width: 360px;
overflow: hidden;
}
.st-bme-notice[data-layout="compact"] .st-bme-notice__content {
display: flex;
align-items: center;
min-width: 0;
max-width: 320px;
overflow: hidden;
}
.st-bme-notice__title {
margin: 0;
font-size: 17px;
line-height: 1.18;
font-weight: 800;
letter-spacing: 0.01em;
color: #f0f6ff;
transition: font-size 260ms ease;
}
.st-bme-notice[data-layout="compact"] .st-bme-notice__title {
font-size: 16px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.st-bme-notice__message {
margin: 4px 0 0;
font-size: 14px;
line-height: 1.38;
color: rgba(240, 246, 255, 0.86);
white-space: pre-wrap;
word-break: break-word;
overflow: hidden;
transition: max-height 280ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 220ms ease,
margin 260ms ease;
}
.st-bme-notice[data-layout="normal"] .st-bme-notice__message {
max-height: 200px;
opacity: 1;
}
.st-bme-notice[data-layout="compact"] .st-bme-notice__message {
max-height: 0;
opacity: 0;
margin-top: 0;
}
.st-bme-notice::after {
content: "";
position: absolute;
inset: 0;
border-left: 3px solid var(--st-bme-accent);
border-radius: 14px;
pointer-events: none;
opacity: 0.9;
}
.st-bme-notice--out {
animation: stBmeNoticeOut 160ms ease forwards;
}
.st-bme-notice__icon {
width: 30px;
height: 30px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 15px;
font-weight: 800;
color: #f4f8ff;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.16);
flex-shrink: 0;
}
.st-bme-notice[data-busy="true"] .st-bme-notice__icon {
animation: stBmeNoticeBusy 900ms linear infinite;
}
.st-bme-notice__message--marquee {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: "Cascadia Code", "Fira Code", "JetBrains Mono", monospace;
font-size: 12.5px;
color: rgba(240, 246, 255, 0.72);
mask-image: linear-gradient(90deg, transparent 0%, black 6%, black 88%, transparent 100%);
-webkit-mask-image: linear-gradient(90deg, transparent 0%, black 6%, black 88%, transparent 100%);
}
.st-bme-notice__action {
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.08);
color: #eef4ff;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: background 140ms ease, border-color 140ms ease, transform 140ms ease;
}
.st-bme-notice__action:hover,
.st-bme-notice__action:focus-visible {
background: rgba(255, 255, 255, 0.16);
border-color: rgba(255, 255, 255, 0.24);
transform: translateY(-1px);
outline: none;
}
.st-bme-notice__action[data-kind="danger"] {
background: rgba(245, 123, 143, 0.16);
border-color: rgba(245, 123, 143, 0.42);
color: #ffd9df;
}
.st-bme-notice__close {
width: 22px;
height: 22px;
border: none;
border-radius: 7px;
background: rgba(255, 255, 255, 0.08);
color: #d7e0ec;
font-size: 15px;
line-height: 1;
cursor: pointer;
transition: background 140ms ease;
flex-shrink: 0;
}
.st-bme-notice__close:hover,
.st-bme-notice__close:focus-visible {
background: rgba(255, 255, 255, 0.2);
outline: none;
}
.st-bme-notice__toggle-hint {
position: absolute;
bottom: 6px;
right: 32px;
font-size: 10px;
color: rgba(240, 246, 255, 0.35);
pointer-events: none;
transition: opacity 180ms ease;
opacity: 0;
}
.st-bme-notice:hover .st-bme-notice__toggle-hint {
opacity: 1;
}
.st-bme-notice[data-layout="normal"] .st-bme-notice__toggle-hint::after {
content: "▸ 简洁";
}
.st-bme-notice[data-layout="compact"] .st-bme-notice__toggle-hint::after {
content: "▸ 详细";
}
.st-bme-notice--switching {
pointer-events: none;
}
.st-bme-notice--switching .st-bme-notice__content {
opacity: 0;
}
.st-bme-notice__actions {
display: flex;
gap: 8px;
margin-top: 10px;
overflow: hidden;
transition: max-height 260ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 220ms ease,
margin 260ms ease;
}
.st-bme-notice[data-layout="compact"] .st-bme-notice__actions {
max-height: 0;
opacity: 0;
margin-top: 0;
overflow: hidden;
}
.st-bme-notice[data-layout="normal"] .st-bme-notice__actions {
max-height: 60px;
opacity: 1;
}
.st-bme-notice[data-layout="compact"] .st-bme-notice__progress {
display: none;
}
.st-bme-notice__progress {
position: absolute;
left: 0;
bottom: 0;
height: 2px;
width: 100%;
background: linear-gradient(90deg, var(--st-bme-accent), rgba(255, 255, 255, 0.24));
transform-origin: left center;
animation: stBmeNoticeProgress linear forwards;
}
.st-bme-notice[data-level="success"] {
--st-bme-accent: #65d39c;
}
.st-bme-notice[data-level="error"] {
--st-bme-accent: #f57b8f;
}
.st-bme-notice[data-level="warning"] {
--st-bme-accent: #eab96f;
}
@keyframes stBmeNoticeIn {
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
@keyframes stBmeNoticeOut {
to {
transform: translateY(-6px) scale(0.98);
opacity: 0;
}
}
@keyframes stBmeNoticeProgress {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
@keyframes stBmeNoticeBusy {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (max-width: 900px) {
#${HOST_ID} {
top: 8px;
right: 8px;
width: calc(100vw - 16px);
}
}
@media (prefers-reduced-motion: reduce) {
.st-bme-notice,
.st-bme-notice--out,
.st-bme-notice__progress {
animation-duration: 1ms !important;
}
}
`;
(doc.head || doc.documentElement).appendChild(style);
}
function ensureHost(doc) {
let host = doc.getElementById(HOST_ID);
if (host) return host;
host = doc.createElement("div");
host.id = HOST_ID;
host.setAttribute("aria-live", "polite");
host.setAttribute("aria-atomic", "false");
(doc.body || doc.documentElement).appendChild(host);
return host;
}
function getIcon(level) {
switch (level) {
case "success":
return "✓";
case "error":
return "!";
case "warning":
return "△";
default:
return "i";
}
}
function applyNoticeState(item, input, progress) {
const level = input.level || "info";
const displayMode = input.displayMode === "compact" ? "compact" : "normal";
const isCompact = displayMode === "compact";
item.dataset.level = level;
item.dataset.busy = input.busy ? "true" : "false";
item.dataset.layout = displayMode;
const icon = item.querySelector(".st-bme-notice__icon");
if (icon) {
icon.textContent = input.busy ? "◌" : getIcon(level);
}
const title = item.querySelector(".st-bme-notice__title");
if (title) {
title.textContent = input.title || "ST-BME";
}
const message = item.querySelector(".st-bme-notice__message");
if (message) {
message.textContent = input.message || "";
message.hidden = !String(input.message || "").trim();
if (input.marquee) {
message.classList.add("st-bme-notice__message--marquee");
} else {
message.classList.remove("st-bme-notice__message--marquee");
}
}
const actionWrap = item.querySelector(".st-bme-notice__actions");
const actionButton = item.querySelector(".st-bme-notice__action");
if (actionWrap && actionButton) {
if (input.action?.label) {
actionButton.style.display = "";
actionButton.textContent = input.action.label;
actionButton.dataset.kind = input.action.kind || "neutral";
} else {
actionButton.style.display = "none";
actionButton.textContent = "";
actionButton.dataset.kind = "neutral";
}
}
if (input.persist || isCompact) {
progress.style.display = "none";
progress.style.animationDuration = "";
} else {
const duration = Math.max(1400, input.duration_ms || 3200);
progress.style.display = "";
progress.style.animationDuration = `${duration}ms`;
}
}
export function showManagedBmeNotice(input) {
const doc = resolveNoticeDocument();
ensureStyle(doc);
const host = ensureHost(doc);
const item = doc.createElement("article");
item.className = "st-bme-notice";
const icon = doc.createElement("span");
icon.className = "st-bme-notice__icon";
const content = doc.createElement("div");
content.className = "st-bme-notice__content";
const title = doc.createElement("h4");
title.className = "st-bme-notice__title";
const message = doc.createElement("p");
message.className = "st-bme-notice__message";
const actions = doc.createElement("div");
actions.className = "st-bme-notice__actions";
const actionButton = doc.createElement("button");
actionButton.className = "st-bme-notice__action";
actionButton.type = "button";
actionButton.style.display = "none";
const closeButton = doc.createElement("button");
closeButton.className = "st-bme-notice__close";
closeButton.type = "button";
closeButton.setAttribute("aria-label", "关闭提示");
closeButton.textContent = "×";
const progress = doc.createElement("div");
progress.className = "st-bme-notice__progress";
const toggleHint = doc.createElement("span");
toggleHint.className = "st-bme-notice__toggle-hint";
content.appendChild(title);
content.appendChild(message);
actions.appendChild(actionButton);
content.appendChild(actions);
item.appendChild(icon);
item.appendChild(content);
item.appendChild(closeButton);
item.appendChild(toggleHint);
item.appendChild(progress);
let currentInput = input || {};
let closed = false;
let closeTimer = null;
let switching = false;
const clearCloseTimer = () => {
if (!closeTimer) return;
clearTimeout(closeTimer);
closeTimer = null;
};
const close = () => {
if (closed) return;
clearCloseTimer();
closed = true;
item.classList.add("st-bme-notice--out");
setTimeout(() => {
item.remove();
if (!host.childElementCount) {
host.remove();
}
}, 170);
};
const scheduleAutoClose = (nextInput) => {
clearCloseTimer();
if (nextInput.persist) return;
const duration = Math.max(1400, nextInput.duration_ms || 3200);
closeTimer = setTimeout(close, duration);
};
const update = (nextInput) => {
if (closed) return;
currentInput = nextInput || {};
applyNoticeState(item, currentInput, progress);
scheduleAutoClose(currentInput);
};
applyNoticeState(item, currentInput, progress);
scheduleAutoClose(currentInput);
actionButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
currentInput.action?.onClick?.();
});
closeButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
close();
});
content.addEventListener("click", (event) => {
if (closed || switching) return;
if (event.target === actionButton || event.target === closeButton) return;
if (actionButton.contains(event.target) || closeButton.contains(event.target)) return;
switching = true;
item.classList.add("st-bme-notice--switching");
const currentLayout = item.dataset.layout || "normal";
const nextLayout = currentLayout === "compact" ? "normal" : "compact";
setTimeout(() => {
item.dataset.layout = nextLayout;
currentInput.displayMode = nextLayout;
if (nextLayout === "normal") {
const msgEl = item.querySelector(".st-bme-notice__message");
if (msgEl) msgEl.hidden = !String(currentInput.message || "").trim();
}
if (nextLayout === "normal" && !currentInput.persist) {
clearCloseTimer();
const duration = Math.max(1400, currentInput.duration_ms || 3200);
closeTimer = setTimeout(close, duration);
const progressEl = item.querySelector(".st-bme-notice__progress");
if (progressEl) {
progressEl.style.display = "";
progressEl.style.animationDuration = `${duration}ms`;
progressEl.style.animation = "none";
void progressEl.offsetHeight;
progressEl.style.animation = "";
}
}
setTimeout(() => {
item.classList.remove("st-bme-notice--switching");
switching = false;
}, 60);
}, 180);
});
host.appendChild(item);
return {
update,
dismiss: close,
isClosed: () => closed,
};
}
export function showBmeNotice(input) {
return showManagedBmeNotice(input);
}