From 5ef9af74ee372d173ff20d8b6acd300841de8bf6 Mon Sep 17 00:00:00 2001 From: ZToolsWordCount Date: Wed, 30 Sep 2026 09:35:50 +0800 Subject: [PATCH] =?UTF-8?q?Add=20plugin=20=E5=AD=97=E6=95=B0=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=20v1.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - feat: ZTools 字数统计插件 v1.0.0 - chore: fix CHANGELOG version heading for publish - chore: ignore local publish logs - fix: remove AI生成 watermark from plugin UI - fix: compact one-screen stats layout and remove AI watermark - fix: strip AI watermark again before publish --- plugins/word-count/.gitignore | 6 + plugins/word-count/CHANGELOG.md | 13 + plugins/word-count/count.js | 140 +++++++++++ plugins/word-count/index.css | 305 ++++++++++++++++++++++++ plugins/word-count/index.html | 84 +++++++ plugins/word-count/index.js | 221 +++++++++++++++++ plugins/word-count/logo.png | Bin 0 -> 3287 bytes plugins/word-count/plugin.json | 28 +++ plugins/word-count/preload.js | 82 +++++++ plugins/word-count/test-count.js | 43 ++++ plugins/word-count/test-ui-structure.js | 46 ++++ 11 files changed, 968 insertions(+) create mode 100644 plugins/word-count/.gitignore create mode 100644 plugins/word-count/CHANGELOG.md create mode 100644 plugins/word-count/count.js create mode 100644 plugins/word-count/index.css create mode 100644 plugins/word-count/index.html create mode 100644 plugins/word-count/index.js create mode 100644 plugins/word-count/logo.png create mode 100644 plugins/word-count/plugin.json create mode 100644 plugins/word-count/preload.js create mode 100644 plugins/word-count/test-count.js create mode 100644 plugins/word-count/test-ui-structure.js diff --git a/plugins/word-count/.gitignore b/plugins/word-count/.gitignore new file mode 100644 index 000000000..f30a31d75 --- /dev/null +++ b/plugins/word-count/.gitignore @@ -0,0 +1,6 @@ +publish.log +publish.err +publish.pid +dist/ +run-publish.ps1 + diff --git a/plugins/word-count/CHANGELOG.md b/plugins/word-count/CHANGELOG.md new file mode 100644 index 000000000..e884d95b0 --- /dev/null +++ b/plugins/word-count/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 1.0.1 - 2026-09-28 + +- 移除界面左上角「AI生成」水印 + +## 1.0.0 - 2026-09-27 + +- 主界面粘贴/输入实时统计 +- 写作向指标:汉字、英文单词、预估阅读时长 +- 辅助指标:总字符、不含空白、数字、标点、行、段落 +- 搜索框 over 指令与主搜索即时结果 +- 复制统计报告、深浅色主题适配 diff --git a/plugins/word-count/count.js b/plugins/word-count/count.js new file mode 100644 index 000000000..042bd6242 --- /dev/null +++ b/plugins/word-count/count.js @@ -0,0 +1,140 @@ +/** + * 写作向字数统计核心(纯函数,无 DOM / 无 ztools 依赖) + * + * 口径说明: + * - 字符数:Unicode 码点数(emoji 不拆成代理对) + * - 汉字:Unicode Script=Han + * - 英文单词:拉丁字母连续段(含常见撇号缩写),数字不计入单词 + * - 数字:连续数字段按 1 个「数」计 + * - 标点:Unicode P* 标点类别(中英标点均计入) + * - 阅读时长:汉字 400 字/分 + 英文单词 200 词/分,向上取整 + */ +(function (global) { + "use strict"; + + var RE_HAN = /\p{Script=Han}/gu; + var RE_LATIN_WORD = /\p{Script=Latin}+(?:['’]\p{Script=Latin}+)*/gu; + var RE_NUMBER = /\d+/g; + var RE_PUNCT = /\p{P}/gu; + var RE_SPACE = /\s/u; + + function toCodePoints(text) { + return Array.from(text); + } + + function countMatches(text, re) { + var m = text.match(re); + return m ? m.length : 0; + } + + function formatReadingTime(minutes) { + if (!isFinite(minutes) || minutes <= 0) return "0 秒"; + if (minutes < 1) { + var secs = Math.max(1, Math.round(minutes * 60)); + return secs + " 秒"; + } + if (minutes < 60) { + var mins = Math.round(minutes); + return mins + " 分钟"; + } + var h = Math.floor(minutes / 60); + var rest = Math.round(minutes % 60); + return rest > 0 ? h + " 小时 " + rest + " 分钟" : h + " 小时"; + } + + /** + * @param {string} text + * @returns {{ + * charCount: number, + * charNoSpace: number, + * whitespace: number, + * han: number, + * latinWords: number, + * numbers: number, + * punctuation: number, + * lines: number, + * paragraphs: number, + * readingMinutes: number, + * readingLabel: string, + * summary: string + * }} + */ + function countText(text) { + if (typeof text !== "string") text = ""; + var codePoints = toCodePoints(text); + var charCount = codePoints.length; + var whitespace = 0; + for (var i = 0; i < codePoints.length; i++) { + if (RE_SPACE.test(codePoints[i])) whitespace++; + } + var charNoSpace = charCount - whitespace; + + var han = countMatches(text, RE_HAN); + var latinWords = countMatches(text, RE_LATIN_WORD); + var numbers = countMatches(text, RE_NUMBER); + var punctuation = countMatches(text, RE_PUNCT); + + var lines = text.length === 0 ? 0 : text.split(/\r\n|\r|\n/).length; + var paragraphs = + text.trim() === "" + ? 0 + : text + .split(/\r\n[\s]*\r\n|\r[\s]*\r|\n[\s]*\n/) + .filter(function (p) { + return p.trim() !== ""; + }).length; + + var readingMinutes = han / 400 + latinWords / 200; + if (charCount > 0 && readingMinutes < 1 / 60) { + readingMinutes = 1 / 60; + } + + var summary = + "汉字 " + + han + + " · 英文 " + + latinWords + + " · 阅读约 " + + formatReadingTime(readingMinutes); + + return { + charCount: charCount, + charNoSpace: charNoSpace, + whitespace: whitespace, + han: han, + latinWords: latinWords, + numbers: numbers, + punctuation: punctuation, + lines: lines, + paragraphs: paragraphs, + readingMinutes: readingMinutes, + readingLabel: formatReadingTime(readingMinutes), + summary: summary, + }; + } + + function formatFullReport(stats) { + return [ + "总字符 " + stats.charCount, + "不含空白 " + stats.charNoSpace, + "汉字 " + stats.han, + "英文单词 " + stats.latinWords, + "数字 " + stats.numbers, + "标点 " + stats.punctuation, + "行 " + stats.lines, + "段落 " + stats.paragraphs, + "预估阅读 " + stats.readingLabel, + ].join(" · "); + } + + var api = { + countText: countText, + formatReadingTime: formatReadingTime, + formatFullReport: formatFullReport, + }; + + if (typeof module !== "undefined" && module.exports) { + module.exports = api; + } + global.WordCount = api; +})(typeof globalThis !== "undefined" ? globalThis : this); diff --git a/plugins/word-count/index.css b/plugins/word-count/index.css new file mode 100644 index 000000000..d831e1c25 --- /dev/null +++ b/plugins/word-count/index.css @@ -0,0 +1,305 @@ +:root { + --bg: #f6f4f0; + --bg-elevated: #ffffff; + --ink: #171614; + --ink-muted: #8a847c; + --line: #ebe6de; + --accent: #2b5cff; + --hero: #f0f3ff; + --hero-ink: #1a3fa0; + --shadow: 0 1px 2px rgba(23, 22, 20, 0.04), 0 8px 20px rgba(23, 22, 20, 0.05); + --radius: 12px; + --font: "SF Pro Text", "PingFang SC", "Microsoft YaHei", "Segoe UI", system-ui, + sans-serif; +} + +html[data-theme="dark"] { + --bg: #111110; + --bg-elevated: #1b1a18; + --ink: #f2efe9; + --ink-muted: #8f8880; + --line: #2a2825; + --accent: #6b8cff; + --hero: #1c2233; + --hero-ink: #c5d2ff; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 20px rgba(0, 0, 0, 0.28); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; + background: var(--bg); + color: var(--ink); + font-family: var(--font); + -webkit-font-smoothing: antialiased; +} + +body { + line-height: 1.4; + overflow: hidden; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.app { + max-width: 920px; + margin: 0 auto; + height: 100vh; + height: 100dvh; + padding: 12px 14px 12px; + display: flex; + flex-direction: column; + gap: 10px; + position: relative; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-shrink: 0; +} + +.brand { + display: flex; + align-items: center; + gap: 8px; +} + +.brand-mark { + width: 28px; + height: 28px; + border-radius: 8px; + background: var(--ink); + color: var(--bg); + display: grid; + place-items: center; + font-weight: 700; + font-size: 13px; +} + +.brand h1 { + margin: 0; + font-size: 15px; + font-weight: 650; + letter-spacing: -0.01em; +} + +.header-actions { + display: flex; + gap: 6px; +} + +.btn { + appearance: none; + border: 1px solid var(--line); + background: var(--bg-elevated); + color: var(--ink); + border-radius: 999px; + padding: 5px 12px; + font-size: 12px; + font-weight: 500; + font-family: inherit; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease; +} + +.btn:hover { + border-color: color-mix(in srgb, var(--ink-muted) 55%, var(--line)); +} + +.btn:active { + transform: scale(0.97); +} + +.btn.primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.btn.primary:hover { + filter: brightness(1.06); +} + +.btn.ghost { + background: transparent; +} + +.main { + display: flex; + flex-direction: column; + gap: 10px; + flex: 1; + min-height: 0; +} + +.editor-wrap { + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow); + overflow: hidden; + flex: 1 1 auto; + min-height: 120px; + display: flex; +} + +.editor { + width: 100%; + height: 100%; + min-height: 120px; + border: 0; + resize: none; + padding: 12px 14px; + font: 14px/1.6 var(--font); + color: var(--ink); + background: transparent; + outline: none; +} + +.editor::placeholder { + color: var(--ink-muted); + opacity: 0.75; +} + +.stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + flex-shrink: 0; +} + +.stats.secondary { + grid-template-columns: repeat(6, 1fr); +} + +.stat-card { + background: var(--bg-elevated); + border: 1px solid var(--line); + border-radius: 10px; + padding: 10px 12px 9px; + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.stat-card.hero { + background: var(--hero); + border-color: transparent; + padding: 12px 14px 11px; +} + +.stat-label { + font-size: 11px; + font-weight: 500; + color: var(--ink-muted); + letter-spacing: 0.02em; + white-space: nowrap; +} + +.stat-card.hero .stat-label { + color: color-mix(in srgb, var(--hero-ink) 72%, var(--ink-muted)); +} + +.stat-value { + font-size: 22px; + font-weight: 650; + line-height: 1.1; + font-variant-numeric: tabular-nums; + letter-spacing: -0.03em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.stat-card.hero .stat-value { + font-size: 28px; + color: var(--hero-ink); +} + +.stat-value.reading { + font-size: 22px; +} + +.stats.secondary .stat-value { + font-size: 16px; + font-weight: 600; +} + +.toast { + position: fixed; + left: 50%; + bottom: 24px; + transform: translateX(-50%) translateY(8px); + background: var(--ink); + color: var(--bg); + padding: 8px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 500; + box-shadow: var(--shadow); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 20; +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +@media (max-width: 720px) { + body { + overflow: auto; + } + + .app { + height: auto; + min-height: 100vh; + } + + .stats.secondary { + grid-template-columns: repeat(3, 1fr); + } + + .stat-card.hero .stat-value { + font-size: 24px; + } + + .stat-value.reading { + font-size: 18px; + } +} + +@media (max-width: 420px) { + .app { + padding: 10px; + } + + .stats { + grid-template-columns: 1fr 1fr 1fr; + } + + .stats.secondary { + grid-template-columns: 1fr 1fr; + } +} diff --git a/plugins/word-count/index.html b/plugins/word-count/index.html new file mode 100644 index 000000000..8489ba585 --- /dev/null +++ b/plugins/word-count/index.html @@ -0,0 +1,84 @@ + + + + + + 字数统计 + + + + +
+
+
+ +

字数统计

+
+
+ + + +
+
+ +
+
+ + +
+ +
+
+
汉字
+
0
+
+
+
英文
+
0
+
+
+
阅读
+
0 秒
+
+
+ +
+
+
总字符
+
0
+
+
+
不含空白
+
0
+
+
+
数字
+
0
+
+
+
标点
+
0
+
+
+
行
+
0
+
+
+
段落
+
0
+
+
+
+ + +
+ + + + + diff --git a/plugins/word-count/index.js b/plugins/word-count/index.js new file mode 100644 index 000000000..f7862182f --- /dev/null +++ b/plugins/word-count/index.js @@ -0,0 +1,221 @@ +(function () { + "use strict"; + + var input = document.getElementById("input-text"); + var toastEl = document.getElementById("toast"); + var btnCopy = document.getElementById("btn-copy"); + var btnClear = document.getElementById("btn-clear"); + var btnPaste = document.getElementById("btn-paste"); + + var lastStats = null; + var debounceTimer = null; + var toastTimer = null; + + function z() { + return typeof window !== "undefined" && window.ztools ? window.ztools : null; + } + + function setTheme(isDark) { + document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light"); + } + + function applyThemeFromHost() { + var api = z(); + if (!api) { + var prefersDark = + window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; + setTheme(prefersDark); + return; + } + try { + if (typeof api.isDarkColors === "function") { + setTheme(!!api.isDarkColors()); + } else if (typeof api.getThemeInfo === "function") { + var info = api.getThemeInfo(); + setTheme(!!(info && info.isDark)); + } + if (typeof api.onThemeChange === "function") { + api.onThemeChange(function (themeInfo) { + setTheme(!!(themeInfo && themeInfo.isDark)); + }); + } + } catch (e) { + console.warn("theme init failed", e); + } + } + + function renderStats(stats) { + lastStats = stats; + var keys = [ + "han", + "latinWords", + "readingLabel", + "charCount", + "charNoSpace", + "numbers", + "punctuation", + "lines", + "paragraphs", + ]; + keys.forEach(function (key) { + var el = document.querySelector('[data-key="' + key + '"]'); + if (!el) return; + el.textContent = String(stats[key]); + }); + } + + function refresh(text) { + var stats = WordCount.countText(text || ""); + renderStats(stats); + return stats; + } + + function scheduleRefresh() { + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(function () { + refresh(input.value); + }, 80); + } + + function showToast(message) { + var api = z(); + if (api && typeof api.showToast === "function") { + try { + api.showToast(message); + return; + } catch (e) { + /* fall through */ + } + } + if (!toastEl) return; + toastEl.textContent = message; + toastEl.hidden = false; + toastEl.classList.add("show"); + if (toastTimer) clearTimeout(toastTimer); + toastTimer = setTimeout(function () { + toastEl.classList.remove("show"); + setTimeout(function () { + toastEl.hidden = true; + }, 200); + }, 1600); + } + + async function copyReport() { + var stats = lastStats || refresh(input.value); + var text = WordCount.formatFullReport(stats); + var api = z(); + if (api && typeof api.copyText === "function") { + try { + var ok = api.copyText(text); + showToast(ok ? "已复制" : "复制失败"); + return; + } catch (e) { + /* fall through */ + } + } + try { + await navigator.clipboard.writeText(text); + showToast("已复制"); + } catch (e) { + showToast("复制失败"); + } + } + + async function pasteFromClipboard() { + var api = z(); + if (api && api.clipboard && typeof api.clipboard.getHistory === "function") { + try { + var history = await api.clipboard.getHistory(1, 1, "text"); + var items = (history && (history.items || history.list || history)) || []; + if (Array.isArray(items) && items[0]) { + var content = items[0].content || items[0].text || items[0].data || ""; + if (typeof content === "string" && content) { + input.value = content; + refresh(input.value); + showToast("已粘贴"); + return; + } + } + } catch (e) { + /* fall through */ + } + } + try { + var text = await navigator.clipboard.readText(); + input.value = text || ""; + refresh(input.value); + showToast(text ? "已粘贴" : "剪贴板为空"); + } catch (e) { + showToast("无法读取剪贴板"); + input.focus(); + } + } + + function clearAll() { + input.value = ""; + refresh(""); + input.focus(); + } + + function setText(text) { + input.value = typeof text === "string" ? text : ""; + refresh(input.value); + } + + function onPluginEnter(param) { + if (!param) return; + var payload = param.payload; + if (typeof payload === "string" && payload) { + setText(payload); + } else if (payload && typeof payload === "object") { + var maybe = + payload.text || + payload.content || + payload.payload || + (typeof payload.data === "string" ? payload.data : ""); + if (typeof maybe === "string" && maybe) setText(maybe); + } + } + + function bindHostEvents() { + var api = z(); + if (!api) return; + try { + if (typeof api.onPluginEnter === "function") { + api.onPluginEnter(onPluginEnter); + } else if (typeof api.onPluginReady === "function") { + api.onPluginReady(onPluginEnter); + } + } catch (e) { + console.warn("onPluginEnter bind failed", e); + } + } + + input.addEventListener("input", scheduleRefresh); + btnCopy.addEventListener("click", function () { + copyReport(); + }); + btnClear.addEventListener("click", clearAll); + btnPaste.addEventListener("click", function () { + pasteFromClipboard(); + }); + + document.addEventListener("keydown", function (e) { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "enter") { + e.preventDefault(); + copyReport(); + } + }); + + applyThemeFromHost(); + bindHostEvents(); + refresh(input.value); + + try { + var params = new URLSearchParams(window.location.search); + var q = params.get("text"); + if (q) setText(q); + } catch (e) { + /* ignore */ + } +})(); diff --git a/plugins/word-count/logo.png b/plugins/word-count/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..27e23a5aac7a4260ad17e9df0d90650c01b330aa GIT binary patch literal 3287 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 0 && readingMinutes < 1 / 60) readingMinutes = 1 / 60; + var readingLabel; + if (readingMinutes <= 0) readingLabel = "0 秒"; + else if (readingMinutes < 1) readingLabel = Math.max(1, Math.round(readingMinutes * 60)) + " 秒"; + else if (readingMinutes < 60) readingLabel = Math.round(readingMinutes) + " 分钟"; + else { + var h = Math.floor(readingMinutes / 60); + var rest = Math.round(readingMinutes % 60); + readingLabel = rest > 0 ? h + " 小时 " + rest + " 分钟" : h + " 小时"; + } + return { + charCount: charCount, + han: han, + latinWords: latinWords, + readingLabel: readingLabel, + }; + } + + function pickPayload(queryData) { + if (!queryData) return ""; + if (typeof queryData === "string") return queryData; + return ( + queryData.payload || + queryData.text || + queryData.query || + queryData.content || + (typeof queryData.data === "string" ? queryData.data : "") || + "" + ); + } + + function registerMainPush() { + if (!window.ztools || typeof window.ztools.onMainPush !== "function") return; + + window.ztools.onMainPush( + function (queryData) { + var text = pickPayload(queryData); + if (!text || !text.trim()) return []; + // 避免对极短指令词误报 + if (text.trim().length < 2) return []; + var s = countTextLite(text); + return [ + { + title: "字数统计", + description: + "汉字 " + + s.han + + " · 英文 " + + s.latinWords + + " · 阅读约 " + + s.readingLabel, + icon: "logo.png", + featureCode: "word-count", + }, + ]; + }, + function (selectData) { + return true; + } + ); + } + + try { + registerMainPush(); + } catch (e) { + console.warn("onMainPush register failed", e); + } +})(); diff --git a/plugins/word-count/test-count.js b/plugins/word-count/test-count.js new file mode 100644 index 000000000..5debffcc3 --- /dev/null +++ b/plugins/word-count/test-count.js @@ -0,0 +1,43 @@ +const { countText, formatFullReport, formatReadingTime } = require("./count.js"); + +function assert(cond, msg) { + if (!cond) { + console.error("FAIL:", msg); + process.exitCode = 1; + } else { + console.log("OK:", msg); + } +} + +let s = countText(""); +assert( + s.charCount === 0 && s.han === 0 && s.latinWords === 0 && s.lines === 0, + "empty" +); + +s = countText("你好世界"); +assert(s.han === 4 && s.latinWords === 0 && s.charCount === 4, "pure han"); + +s = countText("Hello world's test"); +assert(s.latinWords === 3 && s.han === 0, "latin words with apostrophe: " + s.latinWords); + +s = countText("Hello 世界 123!"); +assert( + s.han === 2 && s.latinWords === 1 && s.numbers === 1 && s.punctuation >= 1, + "mixed " + JSON.stringify({ han: s.han, words: s.latinWords, num: s.numbers, p: s.punctuation }) +); + +s = countText("😀字"); +assert(s.charCount === 2 && s.han === 1, "emoji code points: " + s.charCount); + +s = countText("a\nb\n\nc"); +assert(s.lines === 4 && s.paragraphs === 2, "lines/para " + s.lines + "/" + s.paragraphs); + +s = countText("一".repeat(400)); +assert(s.readingMinutes === 1, "400 han = 1 min, got " + s.readingMinutes); + +assert(formatReadingTime(0.01).includes("秒"), "seconds label"); +assert(formatReadingTime(1.2).includes("分钟"), "minutes label"); + +console.log("report:", formatFullReport(countText("Hello 世界"))); +console.log("done"); diff --git a/plugins/word-count/test-ui-structure.js b/plugins/word-count/test-ui-structure.js new file mode 100644 index 000000000..7c7ff4b5d --- /dev/null +++ b/plugins/word-count/test-ui-structure.js @@ -0,0 +1,46 @@ +const fs = require("fs"); +const html = fs.readFileSync("index.html", "utf8"); +const js = fs.readFileSync("index.js", "utf8"); +const css = fs.readFileSync("index.css", "utf8"); + +const keys = [...html.matchAll(/data-key="([^"]+)"/g)].map((m) => m[1]); +const needed = [ + "han", + "latinWords", + "readingLabel", + "charCount", + "charNoSpace", + "numbers", + "punctuation", + "lines", + "paragraphs", +]; +const missing = needed.filter((k) => !keys.includes(k)); +console.log("data-keys missing:", missing.length ? missing.join(",") : "none"); + +const ids = ["input-text", "btn-copy", "btn-clear", "btn-paste", "toast", "stats"]; +for (const id of ids) { + console.log("id", id, html.includes('id="' + id + '"') ? "OK" : "MISSING"); +} + +const banned = [ + "AI生成", + "写作向", + "中文字符", + "拉丁词", + "400 汉字", + "实时统计", + "等待输入", + "口径:", + "stat-hint", + "footer-note", + "tagline", + "editor-meta", +]; +for (const s of banned) { + const hit = html.includes(s) || css.includes(s) || js.includes(s); + console.log("ban", s, hit ? "FOUND" : "clean"); +} + +console.log("dark theme:", /html\[data-theme/.test(css)); +console.log("toast styles:", css.includes(".toast"));