/** * Stylesheet that merges the live TPS slot into the official StatsLine row — * always on ONE line, no wrapping at any width. * * The composer dock (`conversation.composer.dock`) is a list slot: every * registered entry renders, and the renderer emits them inside a wrapper — * `
`. * While the TPS slot is mounted the merge turns that wrapper into a * horizontal flex row (the inline `display: contents` is overridden with * `!important`, which only affects layout — the wrapper still carries no * visual box), so the official StatsLine and the TPS sit side by side as one * compact, centered unit: * * - the official row shrinks to its content width (capped at 620px so the * merged line stays compact even on very wide docks; its own * `white-space: nowrap` + ellipsis handle the rest — the row can never * wrap); * - the TPS slot is a fixed-width item right after it, separated by a `·` * in the official separator style (hidden while the slot has no content). * * The TPS slot stays mounted even when no rate sample exists yet (it renders * empty instead of unmounting), so the merged layout — and the official * row's width — never flips between content width and full width when a * stream starts or ends. * * Selector notes (all verified against the real rendered DOM): * - the slot renderer wraps entries in `div[data-slot="conversation.composer.dock"]`, * so the entries are its direct children — selectors must anchor on the * wrapper; * - nested `:has()` (a `:has()` whose argument contains another `:has()` * with a combinator) fails to parse and the whole rule is silently dropped * by the engine, so the merge uses flat selectors only: `:has(> ...)` on * the wrapper (scoping the merge to the moment the TPS slot is mounted) * and the plain sibling combinator `* + [data-dsh-live-tps]` for the slot. * * When the plugin is inactive the slot does not exist and no merge rule * matches: the dock keeps its original full-width look. While the slot is * mounted but the official row is absent, the TPS alone stays visible and * centered. */ export const MERGE_CSS = ` /* 官方行缺席时的兜底:TPS 独立成行、居中 */ [data-dsh-live-tps] { align-self: center; } /* ── 合并:官方统计行 + 实时 TPS 恒为一行 ── 仅当 TPS 槽位存在(插件激活)时,把渲染器的槽位包装层(内联 display: contents)覆盖为横向 flex 行:两个条目并排、整体居中、 永不换行。插件未激活时此规则不匹配,官方行保持原样。 */ div[data-slot="conversation.composer.dock"]:has(> [data-dsh-live-tps]) { display: flex !important; flex-direction: row; flex-wrap: nowrap; align-items: center; justify-content: center; width: 100%; box-sizing: border-box; } /* 官方行:收缩为内容宽度(上限 620px);清掉官方 margin/padding 的横向 占位,保留 4px 上内边距与文字行高对齐。极窄容器下由 flex 收缩 (0 1 auto + min-width: 0 + 省略号)让位给 TPS 槽位,无需负值 max-width 兜底。 hover/focus 时官方 Tooltip 会把气泡 span 插到统计行与 TPS 之间 (DOM: [统计行, span[role=tooltip], TPS]),所以同时匹配两种相邻形态: 「下一兄弟是 TPS」或「下一兄弟是气泡、再下一兄弟是 TPS」, 否则气泡一出现统计行就回退官方样式、变宽把 TPS 挤走。 */ div[data-slot="conversation.composer.dock"] > *:not([role="tooltip"]):has(+ [data-dsh-live-tps], + [role="tooltip"] + [data-dsh-live-tps]) { width: auto; max-width: 620px; min-width: 0; margin: 0; padding: 4px 0 0; flex: 0 1 auto; } /* TPS 槽位:固定宽度条目,紧跟官方行 */ div[data-slot="conversation.composer.dock"] > * + [data-dsh-live-tps] { flex: 0 0 auto; } /* 与官方行同风格的分隔符(仅合并态、且槽位有内容时显示) */ div[data-slot="conversation.composer.dock"] > * + [data-dsh-live-tps]::before { content: '\\B7'; color: var(--dsw-alias-separator-primary); margin: 0 10px; } /* 无速率样本时槽位为空:隐藏分隔符,保持布局稳定 */ div[data-slot="conversation.composer.dock"] > * + [data-dsh-live-tps]:empty::before { content: none; } /* 官方行 hover 的 Tooltip 气泡:按内容宽度单行显示(官方默认最大 半视口宽,长统计文本会折行);窄屏时受视口限制自动回退换行 */ div[data-slot="conversation.composer.dock"] > [role="tooltip"] { width: max-content; max-width: calc(100vw - 32px); } `.trim() /** Injected-once guard for the merge stylesheet (one tag per page load). */ let mergeCssInjected = false /** Inject the merge stylesheet once; no-op outside the browser or when already present. */ export function ensureMergeCss(): void { if (mergeCssInjected || typeof document === 'undefined') return mergeCssInjected = true if (document.querySelector('style[data-dsh-live-stats-merge]') !== null) return const style = document.createElement('style') style.dataset.dshLiveStatsMerge = '' style.textContent = MERGE_CSS document.head.appendChild(style) }