// ==UserScript== // @name WHUT教务系统小助手 // @namespace http://tampermonkey.net/ // @version 3.8.4 // @description Shadow DOM 隔离的现代教务助手:支持成绩回填与分析、学业监测、GPA、官方证明、一键评教、主题切换和自动更新。 // @author 毫厘 // @match *://jwxt.whut.edu.cn/jwapp/sys/* // @match *://jwxt.whut.edu.cn/jwmobile/index#/kwApp/cjcx/* // @homepageURL https://scriptcat.org/zh-CN/script-show-page/6588 // @supportURL https://scriptcat.org/zh-CN/script-show-page/6588 // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // @require https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js // @grant GM_xmlhttpRequest // @grant GM_info // @connect jwxt.whut.edu.cn // @connect home.haoli.site // @run-at document-start // @license MIT // ==/UserScript== // ================= 内置成绩单结构化解析器 ================= (function (root, factory) { 'use strict'; const api = factory(); if (typeof module === 'object' && module.exports) module.exports = api; if (root) root.WHUTTranscriptParser = api; })(typeof globalThis !== 'undefined' ? globalThis : this, function () { 'use strict'; const PARSER_VERSION = '2.0.1'; const HEADER_TOP = ['课程', '课程', '学', '成']; const HEADER_BOTTOM = ['名称', '性质', '分', '绩']; const SCORE_PATTERN = /^(?:100(?:\.0+)?|\d{1,2}(?:\.\d+)?|优秀|良好|中等|及格|不及格|合格|不合格|通过|不通过|免修|优|良|中)$/; function textOf(value) { return String(value == null ? '' : value).normalize('NFKC').trim(); } function normalizeText(value) { return textOf(value) .replace(/[\s\[\]【】()(){}<>《》·•,,。::;;'"“”‘’_\-—–−]/g, '') .toLowerCase(); } function normalizeSemester(value) { const match = textOf(value).match(/((?:19|20)\d{2})\s*[-–—−]\s*((?:19|20)\d{2})\s*[-–—−]\s*([12])/); return match ? `${match[1]}-${match[2]}-${match[3]}` : ''; } function cleanScore(value) { const score = textOf(value).replace(/\s+/g, '').replace(/分$/, ''); return SCORE_PATTERN.test(score) ? score : ''; } function numeric(value) { const number = Number.parseFloat(value); return Number.isFinite(number) ? number : NaN; } function itemCenter(item) { const center = numeric(item && item.centerX); if (Number.isFinite(center)) return center; return numeric(item && item.x) + Math.max(0, numeric(item && item.width) || 0) / 2; } function normalizedRows(rows) { return (Array.isArray(rows) ? rows : []).map((row, rowIndex) => ({ ...row, rowIndex: Number.isInteger(row.rowIndex) ? row.rowIndex : rowIndex, pageNumber: Number(row.pageNumber || 1), y: numeric(row.y) || 0, items: (Array.isArray(row.items) ? row.items : []).map((item, sourceIndex) => ({ ...item, sourceIndex: Number.isInteger(item.sourceIndex) ? item.sourceIndex : sourceIndex, text: textOf(item.text != null ? item.text : item.str), x: numeric(item.x != null ? item.x : item.transform && item.transform[4]) || 0, y: numeric(item.y != null ? item.y : item.transform && item.transform[5]) || 0, width: Math.max(0, numeric(item.width) || 0), height: Math.max(1, Math.abs(numeric(item.height) || 10)), centerX: itemCenter(item) })).filter(item => item.text).sort((a, b) => a.x - b.x) })).sort((a, b) => a.pageNumber - b.pageNumber || b.y - a.y); } function labelItems(row, allowed) { return row.items.filter(item => allowed.includes(textOf(item.text).replace(/\s+/g, ''))); } function sequenceMatches(items, pattern) { if (items.length < pattern.length || items.length % pattern.length !== 0) return false; return items.every((item, index) => textOf(item.text).replace(/\s+/g, '') === pattern[index % pattern.length]); } function detectLayout(pageRows) { const rows = normalizedRows(pageRows); let best = null; for (const topRow of rows) { const top = labelItems(topRow, HEADER_TOP); if (!sequenceMatches(top, HEADER_TOP)) continue; for (const bottomRow of rows) { const gap = topRow.y - bottomRow.y; if (gap <= 0 || gap > 24) continue; const bottom = labelItems(bottomRow, HEADER_BOTTOM); if (!sequenceMatches(bottom, HEADER_BOTTOM) || bottom.length !== top.length) continue; const groupCount = top.length / 4; if (!best || groupCount > best.groupCount) best = { topRow, bottomRow, top, bottom, groupCount }; } } if (!best) return null; const columns = []; for (let index = 0; index < best.groupCount; index++) { const offset = index * 4; const centers = ['name', 'nature', 'credit', 'score'].map((field, fieldIndex) => ({ field, center: (itemCenter(best.top[offset + fieldIndex]) + itemCenter(best.bottom[offset + fieldIndex])) / 2 })); columns.push({ index, fields: Object.fromEntries(centers.map(entry => [entry.field, entry.center])) }); } const period = columns.length > 1 ? columns.slice(1).reduce((sum, column, index) => sum + column.fields.name - columns[index].fields.name, 0) / (columns.length - 1) : Math.max(180, (columns[0].fields.score - columns[0].fields.name) * 1.65); const firstFieldSpan = columns[0].fields.nature - columns[0].fields.name; const tableLeft = columns[0].fields.name - firstFieldSpan; columns.forEach((column, index) => { // 多栏成绩单的相邻课程名称单元格紧贴上一栏成绩单元格。 // 以重复表头周期切栏,避免较长的下一栏课程名中心点落入上一栏。 column.left = tableLeft + period * index; column.right = tableLeft + period * (index + 1); column.fieldCuts = [ (column.fields.name + column.fields.nature) / 2, (column.fields.nature + column.fields.credit) / 2, (column.fields.credit + column.fields.score) / 2 ]; }); return { pageNumber: best.topRow.pageNumber, headerTopY: best.topRow.y, headerBottomY: best.bottomRow.y, columns }; } function fieldFor(column, item) { const center = itemCenter(item); if (center < column.fieldCuts[0]) return 'name'; if (center < column.fieldCuts[1]) return 'nature'; if (center < column.fieldCuts[2]) return 'credit'; return 'score'; } function joinItems(items) { return items.slice().sort((a, b) => a.x - b.x).map(item => item.text).join('').trim(); } function isFooterOrHeader(text) { const compact = normalizeText(text); return !compact || /^(课程名称|课程性质|学分|成绩)+$/.test(compact) || /毕业应取得总学分|已获得总学分|平均学分绩点|打印日期|本科生成绩单/.test(textOf(text)); } function parseRows(inputRows) { const rows = normalizedRows(inputRows); const pages = new Map(); rows.forEach(row => { if (!pages.has(row.pageNumber)) pages.set(row.pageNumber, []); pages.get(row.pageNumber).push(row); }); const records = []; const layouts = []; const issues = []; let inheritedSemester = ''; let inheritedGeometry = null; [...pages.entries()].sort((a, b) => a[0] - b[0]).forEach(([pageNumber, pageRows]) => { pageRows.sort((a, b) => b.y - a.y); const detected = detectLayout(pageRows); const layout = detected || (inheritedGeometry ? { ...inheritedGeometry, pageNumber, headerTopY: Number.POSITIVE_INFINITY, headerBottomY: Number.POSITIVE_INFINITY } : null); if (!layout) { issues.push({ type: 'layout-missing', pageNumber }); return; } if (detected) inheritedGeometry = { ...detected, columns: detected.columns.map(column => ({ ...column, fields: { ...column.fields }, fieldCuts: [...column.fieldCuts] })) }; layouts.push(layout); for (const column of layout.columns) { let currentSemester = inheritedSemester; const dataRows = pageRows.filter(row => !detected || row.y < layout.headerBottomY - 0.8); for (const row of dataRows) { const items = row.items.filter(item => { const center = itemCenter(item); return center >= column.left && center < column.right; }); if (!items.length) continue; const semester = normalizeSemester(joinItems(items)); if (semester) { currentSemester = semester; continue; } const fields = { name: [], nature: [], credit: [], score: [] }; items.forEach(item => fields[fieldFor(column, item)].push(item)); const name = joinItems(fields.name); const nature = joinItems(fields.nature); const creditText = joinItems(fields.credit); const scoreText = joinItems(fields.score); const score = cleanScore(scoreText); const credit = numeric(creditText); const combined = joinItems(items); if (isFooterOrHeader(combined) || !name) continue; if (!score && !Number.isFinite(credit) && !nature) continue; const record = { semester: currentSemester, name, nature, credit: Number.isFinite(credit) ? credit : null, score, page: pageNumber, columnIndex: column.index, rowIndex: row.rowIndex, y: row.y, rawText: combined }; records.push(record); if (!score) issues.push({ type: 'score-missing', pageNumber, columnIndex: column.index, rowIndex: row.rowIndex, name }); } inheritedSemester = currentSemester; } }); return { parserVersion: PARSER_VERSION, records, layouts, issues }; } function courseValue(course, keys) { for (const key of keys) { const value = course && course[key]; if (value !== undefined && value !== null && value !== '') return value; } return ''; } function matchCourse(course, records) { const name = normalizeText(courseValue(course, ['KCM', '_name', 'courseName', 'name'])); const semester = normalizeSemester(courseValue(course, ['XNXQDM', '_semester', 'semester'])); const credit = numeric(courseValue(course, ['XF', '_credit', 'credit'])); if (!name) return null; const candidates = (Array.isArray(records) ? records : []) .filter(record => normalizeText(record.name) === name) // 学期在两侧均明确时必须一致,避免同名课程跨学期串分。 .filter(record => !semester || !record.semester || record.semester === semester) .map(record => { const semesterMatched = !!semester && !!record.semester && record.semester === semester; const creditMatched = Number.isFinite(credit) && Number.isFinite(record.credit) && Math.abs(record.credit - credit) < 0.001; let rank = 100; if (semesterMatched) rank += 80; if (creditMatched) rank += 30; else if (Number.isFinite(credit) && Number.isFinite(record.credit)) rank -= 15; return { record, score: cleanScore(record.score), rank, semesterMatched, creditMatched }; }).filter(candidate => candidate.score); if (!candidates.length) return null; candidates.sort((a, b) => b.rank - a.rank || a.record.page - b.record.page || a.record.columnIndex - b.record.columnIndex || b.record.y - a.record.y); const bestRank = candidates[0].rank; const strongest = candidates.filter(candidate => candidate.rank === bestRank); const scores = [...new Set(strongest.map(candidate => candidate.score))]; if (scores.length !== 1) { return { conflict: true, confidence: 'low', reason: 'multiple-conflicting-records', candidates: strongest }; } const best = strongest[0]; const confidence = best.semesterMatched && best.creditMatched ? 'high' : 'medium'; return { conflict: false, score: best.score, confidence, reason: confidence === 'high' ? 'name-semester-credit-exact' : (best.semesterMatched ? 'name-semester-exact' : 'name-only'), record: best.record, candidates }; } return { version: PARSER_VERSION, normalizeText, normalizeSemester, cleanScore, isScoreText: value => !!cleanScore(value), detectLayout, parseRows, matchCourse }; }); (function() { 'use strict'; const TRANSCRIPT_PARSER = globalThis.WHUTTranscriptParser || null; // ================= 🎨 模块化设计系统 ================= const THEME = Object.freeze({ primary: "#1e88e5", primaryHover: "#1565c0", accent: "#87b0f9", deepBlue: "#1565c0", bg: "rgb(239, 241, 245)", panelBg: "rgb(250, 251, 253)", border: "rgb(211, 215, 225)", textMain: "rgb(76, 79, 105)", textSub: "rgb(108, 111, 133)", success: "#10b981", warning: "#f59e0b", danger: "#ef4444", info: "#334155" }); const PANEL_LAYOUT = Object.freeze({ width: 680, heightRatio: 0.82, viewportMargin: 12 }); const STYLE_MODULES = Object.freeze({ tokensAndReset: ` :host { all: initial; --uikit-brand: #1e88e5; --uikit-brand-rgb: 30 136 229; --uikit-brand-2: #87b0f9; --uikit-brand-soft: rgb(30 136 229 / 10%); --uikit-brand-text: #1565c0; --uikit-success: #10b981; --uikit-warning: #f59e0b; --uikit-danger: #ef4444; --uikit-info: rgb(76 79 105); --uikit-bg: rgb(239 241 245); --uikit-bg-muted: rgb(230 233 239); --uikit-bg-elevated: rgb(250 251 253); --uikit-border: rgb(211 215 225); --uikit-border-strong: rgb(188 194 207); --uikit-text: rgb(76 79 105); --uikit-text-muted: rgb(108 111 133); --uikit-text-faint: rgb(140 143 161); --uikit-overlay: rgb(30 30 46 / 42%); --uikit-radius-xs: 4px; --uikit-radius-sm: 6px; --uikit-radius-md: 10px; --uikit-radius-lg: 14px; --uikit-radius-pill: 999px; --uikit-shadow-sm: 0 2px 8px rgb(0 0 0 / 5%); --uikit-shadow-md: 0 10px 25px rgb(30 136 229 / 15%); --uikit-shadow-lg: 0 22px 58px rgb(30 30 46 / 18%); --uikit-duration-instant: 150ms; --uikit-duration-fast: 300ms; --uikit-duration-normal: 500ms; --uikit-duration-theme: 800ms; --uikit-ease-standard: cubic-bezier(.19, 1, .22, 1); --uikit-ease-emphasized: cubic-bezier(.34, 1.56, .64, 1.1); --uikit-z-panel: 10; --uikit-z-fab: 20; --uikit-z-progress: 30; --uikit-z-tooltip: 80; --uikit-z-menu: 90; --uikit-z-dialog: 100; --uikit-z-toast: 110; --whut-primary: var(--uikit-brand); --whut-primary-hover: #1565c0; --whut-primary-soft: var(--uikit-brand-soft); --whut-primary-softer: rgb(30 136 229 / 5%); --whut-accent: var(--uikit-brand-2); --whut-deep-blue: var(--uikit-brand-text); --whut-bg: var(--uikit-bg-muted); --whut-surface: var(--uikit-bg); --whut-surface-raised: var(--uikit-bg-elevated); --whut-surface-muted: var(--uikit-bg-muted); --whut-border: var(--uikit-border); --whut-border-strong: var(--uikit-border-strong); --whut-text: var(--uikit-text); --whut-text-secondary: var(--uikit-text-muted); --whut-text-muted: var(--uikit-text-faint); --whut-success: var(--uikit-success); --whut-success-soft: rgb(16 185 129 / 10%); --whut-warning: var(--uikit-warning); --whut-warning-soft: rgb(245 158 11 / 10%); --whut-danger: var(--uikit-danger); --whut-danger-soft: rgb(239 68 68 / 9%); --whut-info: var(--uikit-info); --whut-shadow-xs: var(--uikit-shadow-sm); --whut-shadow-sm: var(--uikit-shadow-sm); --whut-shadow-md: var(--uikit-shadow-md); --whut-shadow-lg: var(--uikit-shadow-lg); --whut-radius-sm: var(--uikit-radius-sm); --whut-radius-md: var(--uikit-radius-md); --whut-radius-lg: var(--uikit-radius-lg); --whut-radius-xl: var(--uikit-radius-lg); --whut-ease: var(--uikit-ease-standard); --whut-fast: var(--uikit-duration-fast); --whut-normal: var(--uikit-duration-normal); font-family: Inter, "SF Pro Text", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 14px; line-height: 1.5; color: var(--uikit-text); -webkit-text-size-adjust: 100%; } :host([data-theme='dark']) { --uikit-brand: #87b0f9; --uikit-brand-rgb: 135 176 249; --uikit-brand-2: #1e88e5; --uikit-brand-soft: rgb(135 176 249 / 14%); --uikit-brand-text: #87b0f9; --uikit-bg: rgb(30 30 46); --uikit-bg-muted: rgb(24 24 37); --uikit-bg-elevated: rgb(36 36 56); --uikit-border: rgb(49 50 68); --uikit-border-strong: rgb(65 69 89); --uikit-text: rgb(198 208 245); --uikit-text-muted: rgb(161 168 201); --uikit-text-faint: rgb(131 139 167); --uikit-overlay: rgb(0 0 0 / 58%); --uikit-shadow-sm: 0 1px 3px rgb(0 0 0 / 42%); --uikit-shadow-md: 0 10px 25px rgb(137 220 235 / 15%); --uikit-shadow-lg: 0 20px 50px rgb(0 0 0 / 72%); --whut-primary-hover: #74a5ec; --whut-primary-softer: rgb(135 176 249 / 9%); --whut-success-soft: rgb(16 185 129 / 15%); --whut-warning-soft: rgb(245 158 11 / 15%); --whut-danger-soft: rgb(239 68 68 / 14%); } *, *::before, *::after { box-sizing: border-box; } [hidden] { display: none !important; } button, input, select, textarea { font: inherit; } button { -webkit-tap-highlight-color: transparent; } .uikit-root { position: fixed; inset: 0; z-index: 2147483647; pointer-events: none; } .uikit-layer { position: fixed; inset: 0; pointer-events: none; } .uikit-layer > * { pointer-events: auto; } .uikit-layer--panel { z-index: var(--uikit-z-panel); } .uikit-layer--menu { z-index: var(--uikit-z-menu); } .uikit-layer--dialog { z-index: var(--uikit-z-dialog); } .uikit-layer--toast { z-index: var(--uikit-z-toast); } #whut-helper-panel, #whut-helper-panel *, #whut-toast-stack, #whut-toast-stack * { box-sizing: border-box; } #whut-helper-panel:focus { outline: none; } #whut-helper-panel button, #whut-helper-panel input, #whut-helper-panel select, #whut-helper-panel textarea, .uikit-dialog-backdrop button, .uikit-dialog-backdrop input, .uikit-dialog-backdrop select, .uikit-dialog-backdrop textarea { font: inherit; } #whut-helper-panel button, .uikit-dialog-backdrop button { -webkit-tap-highlight-color: transparent; } #whut-helper-panel ::selection { color: white; background: var(--whut-primary); } #whut-helper-panel .whut-sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; } #whut-helper-panel :focus-visible, #whut-fab:focus-visible, .uikit-dialog-backdrop :focus-visible { outline: 3px solid rgb(var(--uikit-brand-rgb) / 30%); outline-offset: 2px; } #whut-helper-panel input:focus-visible, #whut-helper-panel select:focus-visible, #whut-helper-panel textarea:focus-visible, .uikit-dialog-backdrop input:focus-visible, .uikit-dialog-backdrop select:focus-visible, .uikit-dialog-backdrop textarea:focus-visible { outline: none; } *, *::before, *::after { scrollbar-width: thin; scrollbar-color: rgb(var(--uikit-brand-rgb) / 32%) transparent; } *::-webkit-scrollbar { width: 8px; height: 8px; } *::-webkit-scrollbar-track { background: transparent; } *::-webkit-scrollbar-thumb { background: rgb(var(--uikit-brand-rgb) / 28%); border: 2px solid transparent; border-radius: 999px; background-clip: padding-box; transition: background-color var(--uikit-duration-fast) var(--uikit-ease-standard); } *::-webkit-scrollbar-thumb:hover { background-color: rgb(var(--uikit-brand-rgb) / 56%); } *::-webkit-scrollbar-corner { background: transparent; } `, shell: ` #whut-fab { position: fixed; width: 60px; height: 60px; z-index: 100000; display: flex; align-items: center; justify-content: center; color: white; border: 1px solid rgba(255, 255, 255, .72); border-radius: 20px; cursor: grab; user-select: none; background: linear-gradient(145deg, #2196f3 0%, #1976d2 55%, #60a5fa 100%); box-shadow: 0 14px 34px rgba(30, 136, 229, .28), inset 0 1px 0 rgba(255, 255, 255, .34); transition: transform var(--whut-normal) var(--whut-ease), box-shadow var(--whut-normal) var(--whut-ease), border-radius var(--whut-normal) var(--whut-ease); isolation: isolate; touch-action: none; } #whut-fab::before { content: ""; position: absolute; inset: -5px; z-index: -1; border: 1px solid rgba(30, 136, 229, .22); border-radius: 24px; opacity: 0; transform: scale(.88); transition: opacity var(--whut-fast), transform var(--whut-normal) var(--whut-ease); } #whut-fab:hover { transform: translateY(-4px) scale(1.03); border-radius: 22px; box-shadow: 0 20px 42px rgba(30, 136, 229, .34); } #whut-fab:hover::before { opacity: 1; transform: scale(1); } #whut-fab:active { cursor: grabbing; transform: translateY(-1px) scale(.96); } .whut-fab-glyph { font-size: 27px; line-height: 1; filter: drop-shadow(0 2px 3px rgba(17, 55, 98, .18)); } .whut-fab-pulse { position: absolute; right: 7px; top: 7px; width: 8px; height: 8px; border-radius: 50%; background: #86efac; box-shadow: 0 0 0 3px rgba(255, 255, 255, .7); } #whut-helper-panel { display: none; position: fixed; width: ${PANEL_LAYOUT.width}px; min-width: 440px; max-width: calc(100vw - ${PANEL_LAYOUT.viewportMargin * 2}px); height: 88vh; min-height: 460px; max-height: calc(100vh - ${PANEL_LAYOUT.viewportMargin * 2}px); z-index: 99999; flex-direction: column; overflow: hidden; color: var(--whut-text); font-size: 13px; line-height: 1.5; background: radial-gradient(circle at 8% -10%, rgba(135, 176, 249, .22), transparent 34%), linear-gradient(180deg, rgba(255, 255, 255, .98), rgba(244, 247, 251, .97)); border: 1px solid rgba(198, 213, 229, .88); border-radius: var(--whut-radius-xl); box-shadow: var(--whut-shadow-lg), inset 0 1px 0 rgba(255, 255, 255, .9); backdrop-filter: blur(22px) saturate(1.15); -webkit-backdrop-filter: blur(22px) saturate(1.15); transform-origin: top right; } #whut-helper-panel.open { display: flex; animation: whutPanelIn 320ms var(--whut-ease) both; } #whut-helper-panel.is-dragging { opacity: .96; box-shadow: 0 30px 80px rgba(30, 64, 104, .22); } .helper-header { flex: 0 0 auto; padding: 17px 18px 0; cursor: grab; user-select: none; background: linear-gradient(180deg, rgba(255, 255, 255, .88), rgba(248, 250, 252, .76)); border-bottom: 1px solid rgba(217, 227, 239, .88); touch-action: none; } .helper-header:active { cursor: grabbing; } .header-top { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 14px; pointer-events: none; } .header-title { display: flex; align-items: center; min-width: 0; gap: 11px; color: var(--whut-text); } .header-logo { width: 38px; height: 38px; flex: 0 0 38px; display: inline-flex; align-items: center; justify-content: center; border-radius: 13px; font-size: 20px; background: linear-gradient(145deg, #eaf5ff, #dceeff); border: 1px solid #c9e2f8; box-shadow: inset 0 1px 0 white, var(--whut-shadow-xs); } .header-brand-copy { display: flex; flex-direction: column; min-width: 0; gap: 4px; } .header-name { overflow: hidden; color: var(--whut-text); font-size: 17px; font-weight: 850; letter-spacing: -.02em; text-overflow: ellipsis; white-space: nowrap; } .header-meta { display: flex; align-items: center; gap: 6px; } .header-version, .header-author { display: inline-flex; align-items: center; min-height: 21px; padding: 2px 8px; border-radius: 999px; color: var(--whut-primary); font-size: 10px; font-weight: 850; letter-spacing: .02em; background: var(--whut-primary-soft); border: 1px solid #cfe7fb; } .header-author { color: var(--whut-deep-blue); background: #eef3fb; border-color: #d7e1ef; } .header-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; pointer-events: auto; touch-action: auto; } .header-action { min-width: 38px; height: 38px; padding: 0 11px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; color: var(--whut-text-secondary); font-size: 13px; line-height: 1; font-weight: 750; cursor: pointer; background: rgba(255, 255, 255, .86); border: 1px solid var(--whut-border); border-radius: 12px; box-shadow: var(--whut-shadow-xs); transition: color var(--whut-fast), background var(--whut-fast), border-color var(--whut-fast), transform var(--whut-normal) var(--whut-ease), box-shadow var(--whut-normal) var(--whut-ease); } .header-action:hover { color: var(--whut-primary); background: white; border-color: #aad4f5; transform: translateY(-2px); box-shadow: var(--whut-shadow-sm); } .header-action:active { transform: translateY(0) scale(.96); } .header-update { min-width: 92px; } .header-update.has-update { color: #15803d; background: var(--whut-success-soft); border-color: #a7e8bd; } .header-update.is-checking > span:first-child { display: inline-block; animation: whutSpin .8s linear infinite; } .header-close { width: 38px; padding: 0; font-size: 18px; font-weight: 500; } .header-close:hover { color: var(--whut-danger); background: var(--whut-danger-soft); border-color: #fecdd3; } `, navigation: ` .helper-tabs { display: grid; grid-template-columns: repeat(5, minmax(max-content, 1fr)); gap: 6px; padding: 0 0 12px; overflow-x: auto; pointer-events: auto; scrollbar-width: none; scroll-snap-type: x proximity; touch-action: pan-x; } .helper-tabs::-webkit-scrollbar { display: none; } .helper-tab { position: relative; min-height: 41px; padding: 8px 10px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; color: var(--whut-text-secondary); font-size: 12px; font-weight: 750; white-space: nowrap; cursor: pointer; background: transparent; border: 1px solid transparent; border-radius: 12px; transition: color var(--whut-fast), background var(--whut-fast), border-color var(--whut-fast), transform var(--whut-normal) var(--whut-ease), box-shadow var(--whut-normal) var(--whut-ease); scroll-snap-align: center; } .helper-tab::after { content: ""; position: absolute; left: 25%; right: 25%; bottom: 3px; height: 2px; border-radius: 999px; background: var(--whut-primary); opacity: 0; transform: scaleX(.4); transition: opacity var(--whut-fast), transform var(--whut-normal) var(--whut-ease); } .helper-tab:hover { color: var(--whut-primary); background: rgba(30, 136, 229, .07); transform: translateY(-1px); } .helper-tab.active { color: var(--whut-primary); background: var(--whut-primary-soft); border-color: #c8e4fa; box-shadow: 0 5px 14px rgba(30, 136, 229, .10); } .helper-tab.active::after { opacity: 1; transform: scaleX(1); } .helper-tab-icon { font-size: 14px; line-height: 1; } .tab-view { display: none; flex: 1 1 auto; min-height: 0; flex-direction: column; overflow: hidden; } .tab-view.active { display: flex; animation: whutViewIn 240ms var(--whut-ease) both; } `, controls: ` .helper-toolbar { flex: 0 0 auto; padding: 14px 18px; background: rgba(248, 250, 252, .82); border-bottom: 1px solid var(--whut-border); box-shadow: 0 5px 16px rgba(30, 64, 104, .035); } .search-box { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; margin-bottom: 10px; } .filter-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; align-items: center; } .action-row { display: flex; gap: 10px; } .action-row > .btn-modern, .btn-flex { flex: 1 1 0; } .search-input, .filter-select, .semester-filter-button, .eval-field textarea, .eval-field select, .eval-field input, .uikit-dialog .gpa-sem-input { width: 100%; color: var(--whut-text); background: rgba(255, 255, 255, .94); border: 1px solid var(--whut-border); border-radius: var(--whut-radius-md); outline: none; box-shadow: inset 0 1px 0 white, var(--whut-shadow-xs); transition: border-color var(--whut-fast), box-shadow var(--whut-fast), background var(--whut-fast); } .search-input { min-width: 0; height: 42px; padding: 9px 13px 9px 39px; font-size: 13px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m20 20-3.5-3.5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: 13px center; } .search-input::placeholder, .eval-field textarea::placeholder { color: var(--whut-text-muted); } .filter-select { height: 42px; padding: 0 36px 0 12px; font-size: 12px; font-weight: 650; cursor: pointer; appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--whut-text-secondary) 50%), linear-gradient(135deg, var(--whut-text-secondary) 50%, transparent 50%); background-position: calc(100% - 16px) 18px, calc(100% - 11px) 18px; background-size: 5px 5px; background-repeat: no-repeat; } .search-input:hover, .filter-select:hover, .semester-filter-button:hover, .eval-field textarea:hover, .eval-field select:hover, .eval-field input:hover, .uikit-dialog .gpa-sem-input:hover { border-color: var(--whut-border-strong); } .search-input:focus, .filter-select:focus, .semester-filter-button:focus, .eval-field textarea:focus, .eval-field select:focus, .eval-field input:focus, .uikit-dialog .gpa-sem-input:focus { background-color: white; border-color: #72baf0; box-shadow: 0 0 0 4px rgba(30, 136, 229, .12), inset 0 1px 0 white; } .semester-filter { position: relative; width: 100%; min-width: 0; } .semester-filter-button { position: relative; height: 42px; padding: 0 36px 0 12px; display: flex; align-items: center; justify-content: flex-start; gap: 8px; font-size: 12px; font-weight: 650; text-align: left; cursor: pointer; } .semester-filter-chevron { position: absolute; top: 50%; right: 12px; transform: translateY(-50%); } .semester-filter-chevron { color: var(--whut-text-muted); transition: transform var(--whut-normal) var(--whut-ease); } .semester-filter.open .semester-filter-chevron { transform: translateY(-50%) rotate(180deg); } .semester-filter-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .semester-menu { display: none; position: absolute; left: 0; right: 0; top: calc(100% + 8px); z-index: 100001; max-height: 290px; padding: 10px; overflow-y: auto; background: rgba(255, 255, 255, .98); border: 1px solid var(--whut-border); border-radius: 16px; box-shadow: var(--whut-shadow-md); transform-origin: top center; } .semester-filter.open .semester-menu { display: block; animation: whutMenuIn 180ms var(--whut-ease) both; } .semester-actions { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; padding-bottom: 9px; margin-bottom: 5px; border-bottom: 1px solid var(--whut-border); } .semester-action { min-height: 30px; padding: 5px 7px; color: var(--whut-text-secondary); font-size: 11px; font-weight: 700; cursor: pointer; background: var(--whut-surface-muted); border: 1px solid var(--whut-border); border-radius: 9px; transition: all var(--whut-fast); } .semester-action:hover { color: var(--whut-primary); background: var(--whut-primary-soft); border-color: #c8e4fa; } .semester-option { display: flex; align-items: center; gap: 9px; min-height: 34px; padding: 7px 8px; color: var(--whut-text); font-size: 12px; cursor: pointer; border-radius: 9px; transition: background var(--whut-fast); } .semester-option:hover { background: var(--whut-primary-softer); } .semester-option input, .pj-grp-cb, .pj-item-cb { width: 16px; height: 16px; margin: 0; accent-color: var(--whut-primary); cursor: pointer; } .btn-modern, .uikit-dialog .btn-modern { min-height: 40px; padding: 9px 15px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: white; font-size: 12px; font-weight: 800; line-height: 1.2; cursor: pointer; background: linear-gradient(135deg, #2495ed, var(--whut-primary)); border: 1px solid transparent; border-radius: 12px; box-shadow: 0 7px 16px rgba(30, 136, 229, .18); transition: color var(--whut-fast), background var(--whut-fast), border-color var(--whut-fast), transform var(--whut-normal) var(--whut-ease), box-shadow var(--whut-normal) var(--whut-ease), opacity var(--whut-fast); } .btn-modern:hover, .uikit-dialog .btn-modern:hover { background: linear-gradient(135deg, var(--whut-primary), var(--whut-primary-hover)); transform: translateY(-2px); box-shadow: 0 11px 22px rgba(30, 136, 229, .24); } .btn-modern:active, .uikit-dialog .btn-modern:active { transform: translateY(0) scale(.975); } .btn-modern:disabled, .uikit-dialog .btn-modern:disabled { opacity: .55; cursor: wait; transform: none; box-shadow: none; } .btn-spinner { width: 14px; height: 14px; flex: 0 0 14px; border: 2px solid rgba(255, 255, 255, .46); border-top-color: currentColor; border-radius: 50%; animation: whutSpin .72s linear infinite; } .btn-outline .btn-spinner { border-color: rgba(30, 136, 229, .22); border-top-color: var(--whut-primary); } .btn-outline, .uikit-dialog .btn-outline { color: var(--whut-text-secondary); background: rgba(255, 255, 255, .92); border-color: var(--whut-border); box-shadow: var(--whut-shadow-xs); } .btn-outline:hover, .uikit-dialog .btn-outline:hover { color: var(--whut-primary); background: var(--whut-primary-softer); border-color: #acd5f4; } .btn-success { background: linear-gradient(135deg, #22c55e, #16a34a); box-shadow: 0 7px 16px rgba(22, 163, 74, .18); } .btn-success:hover { background: linear-gradient(135deg, #16a34a, #15803d); box-shadow: 0 11px 22px rgba(22, 163, 74, .22); } .btn-compact { min-height: 34px; padding: 7px 11px; font-size: 11px; border-radius: 10px; } .btn-block { width: 100%; } .field-label { display: block; margin: 0 0 6px; color: var(--whut-text-secondary); font-size: 11px; font-weight: 800; } `, statesAndContent: ` .helper-content, .monitor-container, .gpa-container, .export-container { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 18px; background: var(--whut-bg); } .ui-state { min-height: 190px; padding: 28px 18px; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--whut-text-secondary); text-align: center; } .ui-state.compact { min-height: 92px; padding: 18px 12px; } .ui-state-icon { width: 44px; height: 44px; margin-bottom: 12px; display: inline-flex; align-items: center; justify-content: center; color: var(--whut-primary); font-size: 21px; background: var(--whut-primary-soft); border: 1px solid #d3e9fb; border-radius: 15px; } .ui-state-spinner { width: 21px; height: 21px; border: 2.5px solid #bdddf5; border-top-color: var(--whut-primary); border-radius: 50%; animation: whutSpin .75s linear infinite; } .ui-state-title { color: var(--whut-text); font-size: 13px; font-weight: 800; } .ui-state-detail { max-width: 430px; margin-top: 5px; color: var(--whut-text-secondary); font-size: 11px; line-height: 1.65; } .ui-state.error .ui-state-icon { color: var(--whut-danger); background: var(--whut-danger-soft); border-color: #fecdd3; } .ui-state.warning .ui-state-icon { color: var(--whut-warning); background: var(--whut-warning-soft); border-color: #fed7aa; } .ui-state.empty .ui-state-icon { color: var(--whut-text-muted); background: #f1f5f9; border-color: #e2e8f0; } .text-muted { color: var(--whut-text-muted) !important; } .text-success { color: var(--whut-success) !important; } .text-danger { color: var(--whut-danger) !important; } .section-heading { margin: 18px 2px 10px; display: flex; align-items: center; gap: 8px; color: var(--whut-text); font-size: 13px; font-weight: 850; } .section-heading::after { content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, var(--whut-border), transparent); } `, analysis: ` .course-item { position: relative; margin-bottom: 11px; overflow: hidden; background: var(--whut-surface-raised); border: 1px solid var(--whut-border); border-radius: var(--whut-radius-lg); box-shadow: var(--whut-shadow-sm); transition: border-color var(--whut-fast), background var(--whut-fast), transform var(--whut-normal) var(--whut-ease), box-shadow var(--whut-normal) var(--whut-ease); } .course-item::before { content: ""; position: absolute; left: 0; top: 14px; bottom: 14px; width: 3px; border-radius: 0 3px 3px 0; background: transparent; transition: background var(--whut-fast); } .course-item:hover, .course-item:focus-within { background: white; border-color: #c4ddf1; transform: translateY(-3px); box-shadow: 0 13px 28px rgba(30, 64, 104, .11); } .course-item:hover::before, .course-item:focus-within::before { background: linear-gradient(180deg, var(--whut-primary), var(--whut-accent)); } .course-summary { width: 100%; min-height: 86px; padding: 15px 17px; display: flex; align-items: center; justify-content: space-between; gap: 14px; color: inherit; text-align: left; cursor: pointer; background: transparent; border: 0; } .course-main { flex: 1 1 auto; min-width: 0; } .course-title { display: block; margin-bottom: 8px; overflow: hidden; color: var(--whut-text); font-size: 14px; font-weight: 780; letter-spacing: -.01em; text-overflow: ellipsis; white-space: nowrap; } .course-tags { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 10px; } .tag { min-height: 23px; padding: 3px 8px; display: inline-flex; align-items: center; color: var(--whut-text-secondary); font-weight: 650; background: #f1f5f9; border: 1px solid #e7edf4; border-radius: 999px; } .tag-credit { color: #0b74bb; background: #eaf6ff; border-color: #d1eafd; } .tag-compulsory { color: #be185d; font-weight: 750; background: #fdf2f8; border-color: #fbcfe8; } .tag-source-monitor { color: #15803d; background: #ecfdf3; border-color: #bbf7d0; } .tag-source-transcript { color: #1d4ed8; background: #eff6ff; border-color: #bfdbfe; } .course-score { flex: 0 0 auto; min-width: 72px; color: var(--whut-text-muted); font-size: 19px; font-weight: 850; letter-spacing: -.02em; text-align: right; } .score-high { color: var(--whut-success); } .score-med { color: var(--whut-warning); } .score-low { color: var(--whut-danger); } .score-none { color: #b4c1d0; font-size: 13px; font-weight: 700; } .helper-footer { flex: 0 0 auto; padding: 11px 18px 12px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; pointer-events: none; background: rgba(255, 255, 255, .94); border-top: 1px solid var(--whut-border); box-shadow: 0 -8px 24px rgba(30, 64, 104, .06); } .footer-metric { min-width: 0; padding: 7px 9px; display: flex; align-items: center; justify-content: space-between; gap: 7px; background: #f8fafc; border: 1px solid #e8eef5; border-radius: 11px; } .footer-label { overflow: hidden; color: var(--whut-text-secondary); font-size: 10px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } .footer-value { display: inline-flex; align-items: baseline; gap: 3px; color: var(--whut-text); font-size: 14px; font-weight: 900; letter-spacing: -.02em; } .footer-value small { color: var(--whut-text-muted); font-size: 9px; font-weight: 700; } .footer-metric.is-deep .footer-value { color: var(--whut-deep-blue); font-size: 17px; } .footer-metric.is-primary .footer-value { color: var(--whut-primary); font-size: 17px; } .detail-panel { display: none; padding: 15px 16px 16px; background: #f7fafe; border-top: 1px solid var(--whut-border); } .detail-panel.open { display: block; animation: whutDetailIn 220ms var(--whut-ease) both; } .compare-table { width: 100%; margin-bottom: 13px; overflow: hidden; color: var(--whut-text); font-size: 11px; text-align: center; background: white; border: 1px solid var(--whut-border); border-radius: 12px; border-collapse: separate; border-spacing: 0; } .compare-table th { padding: 9px 6px; color: var(--whut-text-secondary); font-weight: 750; background: var(--whut-primary-softer); border-bottom: 1px solid var(--whut-border); } .compare-table td { padding: 10px 6px; font-weight: 650; } .val-rank { color: var(--whut-primary); font-weight: 850; } .detail-section-head { margin: 12px 0 8px; display: flex; align-items: center; gap: 8px; color: var(--whut-text-secondary); font-size: 11px; font-weight: 800; } .detail-section-head::after { content: ""; flex: 1; height: 1px; background: var(--whut-border); } .dist-container { padding: 3px 1px 1px; } .dist-row { display: flex; align-items: center; gap: 9px; min-height: 25px; color: var(--whut-text-secondary); font-size: 11px; } .dist-name { width: 38px; text-align: right; } .dist-track { flex: 1; height: 7px; overflow: hidden; background: #e6edf5; border-radius: 999px; } .dist-fill { height: 100%; background: linear-gradient(90deg, #5eb2ef, var(--whut-primary)); border-radius: 999px; transition: width 420ms var(--whut-ease); } .dist-num { width: 30px; font-weight: 750; } .table-empty { padding: 22px 12px !important; color: var(--whut-text-muted) !important; text-align: center !important; } `, gpaAndPrint: ` .export-card, .gpa-card { margin-bottom: 14px; padding: 18px; background: var(--whut-surface-raised); border: 1px solid var(--whut-border); border-radius: var(--whut-radius-lg); box-shadow: var(--whut-shadow-sm); } .gpa-card.is-overall { position: relative; overflow: hidden; border-color: #b9dcf6; box-shadow: 0 14px 32px rgba(30, 136, 229, .11); } .gpa-card.is-overall::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 4px; background: linear-gradient(90deg, var(--whut-primary), var(--whut-accent)); } .gpa-card.is-semester { padding: 15px; } .gpa-card.is-empty { padding: 13px 15px; } .gpa-card-title { margin-bottom: 14px; padding-bottom: 10px; display: flex; align-items: center; gap: 8px; color: var(--whut-text); font-size: 15px; font-weight: 850; border-bottom: 1px solid var(--whut-border); } .gpa-card-title.is-compact { margin: 0; padding: 0; justify-content: space-between; font-size: 13px; border: 0; } .gpa-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; margin-bottom: 0; } .gpa-item { min-width: 0; padding: 13px; display: flex; flex-direction: column; background: linear-gradient(145deg, #f8fbff, #eef6fd); border: 1px solid #dceaf6; border-radius: 13px; } .gpa-item-label { margin-bottom: 5px; color: var(--whut-text-secondary); font-size: 10px; font-weight: 700; } .gpa-item-value { color: var(--whut-primary); font-size: 20px; font-weight: 900; letter-spacing: -.03em; } .gpa-item-value.accent { color: #db2777; } .gpa-item-value.compulsory-gpa-value { color: var(--whut-deep-blue); } .gpa-value-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; } .gpa-value-row .gpa-item-value { font-size: 16px; } .gpa-inline-rank { color: var(--whut-text-secondary); font-size: 10px; } .gpa-inline-rank b { color: var(--whut-text); } .gpa-ranks { margin-top: 8px; padding: 6px 8px; display: flex; flex-wrap: wrap; gap: 10px; color: var(--whut-text-secondary); font-size: 10px; background: rgba(255, 255, 255, .72); border-radius: 9px; } .gpa-ranks b { color: var(--whut-text); } .gpa-sem-row { display: flex; gap: 8px; margin-bottom: 8px; } .uikit-dialog .gpa-sem-input { flex: 1; min-width: 0; height: 40px; padding: 8px 11px; } .gpa-sem-help { margin-bottom: 11px; color: var(--whut-text-secondary); font-size: 11px; line-height: 1.65; text-align: left; } .export-card h3 { margin: 0 0 11px; display: flex; align-items: center; gap: 8px; color: var(--whut-text); font-size: 14px; font-weight: 850; } .export-card p { margin: 0 0 15px; color: var(--whut-text-secondary); font-size: 12px; line-height: 1.65; } .print-list { display: flex; flex-direction: column; gap: 8px; margin-top: 9px; } .print-row { padding: 11px 12px; display: flex; align-items: center; justify-content: space-between; gap: 12px; background: var(--whut-surface-muted); border: 1px solid var(--whut-border); border-radius: 13px; transition: background var(--whut-fast), border-color var(--whut-fast), transform var(--whut-normal) var(--whut-ease), box-shadow var(--whut-normal) var(--whut-ease); } .print-row:hover { background: white; border-color: #b9d9f1; transform: translateY(-2px); box-shadow: var(--whut-shadow-sm); } .print-name { min-width: 0; overflow: hidden; color: var(--whut-text); font-size: 12px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; } `, evaluation: ` .eval-toolbar { display: grid; gap: 11px; } .eval-status-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .eval-status-copy { min-width: 0; display: flex; align-items: center; gap: 8px; } .eval-status-dot { width: 8px; height: 8px; flex: 0 0 8px; background: var(--uikit-success); border-radius: 50%; box-shadow: 0 0 0 4px rgb(16 185 129 / 10%); } .eval-status-row[data-state='loading'] .eval-status-dot { background: var(--uikit-brand); box-shadow: 0 0 0 4px var(--uikit-brand-soft); animation: evalStatusPulse 1.25s ease-in-out infinite; } .eval-status-row[data-state='error'] .eval-status-dot { background: var(--uikit-danger); box-shadow: 0 0 0 4px rgb(239 68 68 / 10%); } .eval-status-row[data-state='warning'] .eval-status-dot { background: var(--uikit-warning); box-shadow: 0 0 0 4px rgb(245 158 11 / 10%); } .eval-status-row[data-state='idle'] .eval-status-dot { background: var(--uikit-text-faint); box-shadow: 0 0 0 4px rgb(108 111 133 / 9%); } .eval-status-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; } .eval-status-note { color: var(--whut-text-muted); font-size: 10px; white-space: nowrap; } .eval-config-card { padding: 14px; display: grid; gap: 12px; background: var(--uikit-bg-elevated); border: 1px solid var(--whut-border); border-radius: var(--uikit-radius-md); box-shadow: var(--whut-shadow-sm); animation: whutDetailIn var(--uikit-duration-fast) var(--uikit-ease-standard) both; } .eval-config-card.collapsed { display: none; } .eval-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; align-items: end; } .eval-comment { grid-column: 1 / -1; } .eval-field { min-width: 0; } .eval-field textarea { min-height: 68px; padding: 10px 12px; resize: vertical; font-size: 12px; line-height: 1.55; } .eval-field select, .eval-field input { height: 40px; padding: 8px 11px; font-size: 12px; } .eval-config-actions { display: flex; justify-content: flex-end; gap: 9px; } .eval-footer { display: flex; gap: 10px; border-top: 1px solid var(--whut-border); border-bottom: 0; } #pj-list-area { padding: 14px 18px !important; background: var(--whut-bg) !important; } .eval-summary { margin-bottom: 12px; padding: 13px 14px; display: grid; grid-template-columns: minmax(150px, 1fr) auto; align-items: center; gap: 14px; color: var(--uikit-text); background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); box-shadow: var(--uikit-shadow-sm); } .eval-summary-copy { min-width: 0; display: grid; gap: 2px; } .eval-summary-eyebrow { color: var(--uikit-brand); font-size: 9px; font-weight: 900; letter-spacing: .1em; } .eval-summary-term { overflow: hidden; color: var(--uikit-text); font-size: 15px; font-weight: 850; text-overflow: ellipsis; white-space: nowrap; } .eval-summary-detail { color: var(--uikit-text-muted); font-size: 10px; } .eval-summary-stats { display: grid; grid-template-columns: repeat(4, minmax(48px, auto)); gap: 6px; } .eval-summary-stat { min-height: 48px; padding: 6px 9px; display: grid; place-items: center; align-content: center; color: var(--uikit-text-muted); font-size: 9px; line-height: 1.25; background: var(--uikit-bg-muted); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-sm); } .eval-summary-stat b { color: var(--uikit-text); font-size: 14px; font-variant-numeric: tabular-nums; } .eval-summary-stat.is-active b { color: var(--uikit-brand); } .eval-summary-stat.is-done b { color: var(--uikit-success); } .eval-loading-shell { display: grid; gap: 10px; padding: 1px; } .eval-loading-card { height: 74px; overflow: hidden; position: relative; background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); } .eval-loading-card::after { content: ''; position: absolute; inset: 0; background: linear-gradient(100deg, transparent 20%, rgb(var(--uikit-brand-rgb) / 10%) 48%, transparent 76%); transform: translateX(-100%); animation: evalSkeleton 1.35s var(--uikit-ease-standard) infinite; } .eval-loading-line { height: 9px; margin: 16px 16px 0; background: var(--uikit-border); border-radius: var(--uikit-radius-pill); } .eval-loading-line + .eval-loading-line { width: 44%; margin-top: 11px; opacity: .72; } #pj-list-area > .pj-group { margin-bottom: 11px; overflow: hidden; background: var(--uikit-bg-elevated); border: 1px solid var(--whut-border); border-radius: var(--uikit-radius-md); box-shadow: var(--whut-shadow-sm); transition: border-color var(--uikit-duration-fast) var(--uikit-ease-standard), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-standard); } #pj-list-area > .pj-group:hover { border-color: rgb(var(--uikit-brand-rgb) / 44%); box-shadow: 0 8px 20px rgb(var(--uikit-brand-rgb) / 10%); transform: translateY(-1px); } .pj-group-head { min-height: 46px; display: flex; align-items: stretch; background: var(--uikit-bg-elevated); border-bottom: 1px solid var(--whut-border); } .pj-group-select { padding: 0 0 0 13px; display: inline-flex; align-items: center; gap: 6px; color: var(--uikit-text-muted); font-size: 9px; font-weight: 750; cursor: pointer; } .pj-group-header { min-width: 0; min-height: 45px; padding: 10px 13px; flex: 1; display: flex; align-items: center; gap: 9px; color: var(--whut-text); font-size: 12px; font-weight: 820; text-align: left; cursor: pointer; background: transparent; border: 0; transition: background var(--uikit-duration-fast) var(--uikit-ease-standard), color var(--uikit-duration-fast) var(--uikit-ease-standard); } .pj-group-header:hover { color: var(--whut-primary); background: var(--whut-primary-softer); } .pj-group-mark { width: 8px; height: 8px; flex: 0 0 8px; border-radius: 50%; background: var(--uikit-text-faint); box-shadow: 0 0 0 4px rgb(108 111 133 / 8%); } .pj-group-mark.is-active { background: var(--uikit-success); box-shadow: 0 0 0 4px rgb(16 185 129 / 10%); } .pj-group-mark.is-done { background: var(--uikit-brand); box-shadow: 0 0 0 4px rgb(var(--uikit-brand-rgb) / 12%); } .pj-group-mark.is-wait { background: var(--uikit-warning); box-shadow: 0 0 0 4px rgb(245 158 11 / 10%); } .pj-group-mark.is-end { background: var(--uikit-danger); box-shadow: 0 0 0 4px rgb(239 68 68 / 9%); } .pj-group-title { flex: 1; } .pj-arrow { color: var(--whut-text-muted); font-size: 15px; transition: transform var(--uikit-duration-fast) var(--uikit-ease-standard); } .pj-group-header[aria-expanded="false"] .pj-arrow { transform: rotate(-90deg); } .pj-group-content { display: grid; grid-template-rows: 1fr; opacity: 1; background: var(--uikit-bg-elevated); transition: grid-template-rows var(--uikit-duration-fast) var(--uikit-ease-standard), opacity var(--uikit-duration-fast) var(--uikit-ease-standard); } .pj-group-content.is-collapsed { grid-template-rows: 0fr; opacity: 0; } .pj-group-items { min-height: 0; overflow: hidden; } .pj-item { padding: 12px 13px; display: flex; align-items: center; gap: 10px; background: var(--uikit-bg-elevated); border-bottom: 1px solid var(--uikit-border); transition: background var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-standard); } .pj-item:hover { background: var(--uikit-brand-soft); transform: translateX(2px); } .pj-item:last-child { border-bottom: 0; } .pj-item-main { flex: 1 1 auto; min-width: 0; line-height: 1.45; } .pj-item-title { overflow: hidden; color: var(--whut-text); font-size: 12px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; } .pj-item-sub { display: flex; flex-wrap: wrap; gap: 4px 8px; color: var(--whut-text-secondary); font-size: 10px; } .pj-item-sub span + span::before { content: '·'; margin-right: 8px; color: var(--uikit-text-faint); } .pj-item-status { flex: 0 0 auto; display: grid; justify-items: end; gap: 4px; text-align: right; } .pj-badge { min-height: 21px; padding: 3px 8px; display: inline-flex; align-items: center; font-size: 9px; font-weight: 850; border-radius: 999px; } .pj-badge.ing { color: var(--uikit-success); background: rgb(16 185 129 / 13%); } .pj-badge.done { color: var(--uikit-brand); background: var(--uikit-brand-soft); } .pj-badge.wait { color: var(--uikit-warning); background: rgb(245 158 11 / 12%); } .pj-badge.end { color: var(--uikit-danger); background: rgb(239 68 68 / 11%); } .pj-log { margin-top: 3px; color: var(--whut-warning); font-size: 9px; font-weight: 700; text-align: right; } .pj-item-cb:disabled { cursor: not-allowed; opacity: .46; } .pj-item-state-icon { width: 17px; height: 17px; flex: 0 0 17px; display: inline-flex; align-items: center; justify-content: center; color: var(--uikit-text-faint); font-size: 10px; font-weight: 900; background: var(--uikit-bg-muted); border: 1px solid var(--uikit-border); border-radius: 50%; } .pj-item-state-icon.done { color: #fff; background: var(--uikit-success); border-color: var(--uikit-success); } .pj-item-state-icon.wait { color: var(--uikit-warning); background: rgb(245 158 11 / 10%); border-color: rgb(245 158 11 / 24%); } .pj-detail-btn { min-height: 25px; padding: 4px 8px; color: var(--uikit-brand-text); font-size: 9px; font-weight: 800; cursor: pointer; background: var(--uikit-brand-soft); border: 1px solid rgb(var(--uikit-brand-rgb) / 22%); border-radius: var(--uikit-radius-pill); transition: color var(--uikit-duration-fast) var(--uikit-ease-standard), background-color var(--uikit-duration-fast) var(--uikit-ease-standard), border-color var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-emphasized), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard); } .pj-detail-btn:hover { color: #fff; background: var(--uikit-brand); border-color: var(--uikit-brand); transform: translateY(-1px); box-shadow: 0 5px 12px rgb(var(--uikit-brand-rgb) / 18%); } .pj-detail-btn:active { transform: scale(.96); } .pj-detail-btn:disabled { cursor: wait; opacity: .68; transform: none; box-shadow: none; } .pj-run-count { min-width: 19px; height: 19px; padding: 0 6px; display: inline-flex; align-items: center; justify-content: center; color: var(--uikit-brand-text); font-size: 10px; font-weight: 900; background: rgb(255 255 255 / 82%); border-radius: var(--uikit-radius-pill); } .eval-history-loader { margin: 0 0 11px; padding: 13px 14px; display: flex; align-items: center; justify-content: space-between; gap: 14px; background: linear-gradient(135deg, var(--uikit-bg-elevated), var(--uikit-brand-soft)); border: 1px solid rgb(var(--uikit-brand-rgb) / 20%); border-radius: var(--uikit-radius-md); box-shadow: var(--uikit-shadow-sm); transition: border-color var(--uikit-duration-fast) var(--uikit-ease-standard), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-emphasized); } .eval-history-loader:hover { border-color: rgb(var(--uikit-brand-rgb) / 38%); box-shadow: 0 8px 20px rgb(var(--uikit-brand-rgb) / 9%); transform: translateY(-1px); } .eval-history-copy { min-width: 0; display: grid; gap: 3px; } .eval-history-kicker { color: var(--uikit-brand); font-size: 9px; font-weight: 900; letter-spacing: .09em; } .eval-history-title { color: var(--uikit-text); font-size: 12px; font-weight: 850; } .eval-history-meta { overflow: hidden; color: var(--uikit-text-muted); font-size: 10px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; } .eval-history-btn { min-width: 128px; min-height: 38px; flex: 0 0 auto; } .pj-item.is-history { background-image: linear-gradient(90deg, rgb(var(--uikit-brand-rgb) / 4%), transparent 42%); } .pj-item-term { padding: 1px 5px; color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-radius: var(--uikit-radius-pill); } .pj-item-sub .pj-item-term::before { content: none; margin: 0; } .eval-detail-sheet { display: grid; gap: 12px; color: var(--uikit-text); } .eval-detail-overview { padding: 14px; display: flex; align-items: center; justify-content: space-between; gap: 14px; background: linear-gradient(135deg, var(--uikit-brand-soft), var(--uikit-bg-muted)); border: 1px solid rgb(var(--uikit-brand-rgb) / 22%); border-radius: var(--uikit-radius-md); } .eval-detail-kicker { color: var(--uikit-brand); font-size: 9px; font-weight: 900; letter-spacing: .1em; text-transform: uppercase; } .eval-detail-overview h2, .eval-detail-teacher h3 { margin: 3px 0 0; color: var(--uikit-text); } .eval-detail-overview h2 { font-size: 15px; line-height: 1.35; } .eval-detail-overview p { margin: 4px 0 0; color: var(--uikit-text-muted); font-size: 10px; } .eval-detail-total { flex: 0 0 auto; padding: 7px 10px; color: var(--uikit-success); font-size: 12px; font-weight: 900; white-space: nowrap; background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-sm); } .eval-detail-deadline { padding: 7px 10px; color: var(--uikit-text-muted); font-size: 10px; background: var(--uikit-bg-muted); border-radius: var(--uikit-radius-sm); } .eval-detail-teacher { overflow: hidden; background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); } .eval-detail-teacher > header { padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; gap: 12px; background: var(--uikit-bg-muted); border-bottom: 1px solid var(--uikit-border); } .eval-detail-teacher h3 { font-size: 13px; } .eval-detail-score { display: grid; color: var(--uikit-brand); font-size: 17px; font-weight: 900; line-height: 1; text-align: right; font-variant-numeric: tabular-nums; } .eval-detail-score small { margin-top: 3px; color: var(--uikit-text-faint); font-size: 8px; font-weight: 700; } .eval-detail-answers { margin: 0; padding: 0; list-style: none; } .eval-detail-answer { padding: 10px 13px; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 12px; border-bottom: 1px solid var(--uikit-border); } .eval-detail-answer:last-child { border-bottom: 0; } .eval-detail-question { min-width: 0; display: flex; align-items: flex-start; gap: 8px; } .eval-detail-question > span { width: 20px; height: 20px; flex: 0 0 20px; display: inline-flex; align-items: center; justify-content: center; color: var(--uikit-brand-text); font-size: 9px; font-weight: 900; background: var(--uikit-brand-soft); border-radius: 50%; } .eval-detail-question p { margin: 0; color: var(--uikit-text); font-size: 11px; line-height: 1.55; } .eval-detail-result { min-width: 74px; display: grid; justify-items: end; gap: 2px; text-align: right; } .eval-detail-result strong { color: var(--uikit-success); font-size: 11px; } .eval-detail-points { color: var(--uikit-text-faint); font-size: 9px; } .eval-detail-empty { padding: 18px; color: var(--uikit-text-faint); text-align: center; } @media (max-width: 620px) { .eval-history-loader { align-items: stretch; flex-direction: column; gap: 10px; } .eval-history-meta { white-space: normal; } .eval-history-btn { width: 100%; } } @keyframes evalStatusPulse { 50% { opacity: .45; transform: scale(.82); } } @keyframes evalSkeleton { to { transform: translateX(100%); } } `, monitor: ` .monitor-toolbar-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .monitor-status { min-width: 0; overflow: hidden; color: var(--whut-text-secondary); font-size: 11px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; } .monitor-actions { display: flex; gap: 8px; flex: 0 0 auto; } .monitor-overview { position: relative; margin-bottom: 15px; padding: 17px; overflow: hidden; background: linear-gradient(145deg, white, #f5faff); border: 1px solid #cfe5f7; border-radius: 18px; box-shadow: var(--whut-shadow-sm); } .monitor-overview::after { content: ""; position: absolute; width: 150px; height: 150px; right: -58px; top: -74px; border-radius: 50%; background: rgba(135, 176, 249, .18); pointer-events: none; } .monitor-overview-top { position: relative; z-index: 1; margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .monitor-overview-label { margin-bottom: 3px; color: var(--whut-text-secondary); font-size: 11px; font-weight: 700; } .monitor-overview-value { color: var(--whut-text); font-size: 19px; font-weight: 900; letter-spacing: -.025em; } .monitor-status-pill, .monitor-group-badge { min-height: 25px; padding: 4px 9px; display: inline-flex; align-items: center; font-size: 10px; font-weight: 850; white-space: nowrap; border-radius: 999px; } .monitor-status-pill.pass, .monitor-group-badge.pass { color: #15803d; background: #dcfce7; border: 1px solid #bbf7d0; } .monitor-status-pill.warn, .monitor-group-badge.warn { color: #be123c; background: #ffe4e6; border: 1px solid #fecdd3; } .monitor-progress-track { height: 10px; margin-bottom: 8px; overflow: hidden; background: #e1eaf4; border-radius: 999px; box-shadow: inset 0 1px 2px rgba(30, 64, 104, .08); } .monitor-progress-fill { height: 100%; background: linear-gradient(90deg, var(--whut-primary), #4da8ee, var(--whut-accent)); border-radius: inherit; box-shadow: 0 0 12px rgba(30, 136, 229, .24); transition: width 520ms var(--whut-ease); } .monitor-progress-meta { display: flex; justify-content: space-between; gap: 12px; color: var(--whut-text-secondary); font-size: 11px; } .monitor-progress-meta b { color: var(--whut-primary); font-size: 12px; } .monitor-section-title { margin: 17px 2px 9px; display: flex; align-items: center; gap: 8px; color: var(--whut-text); font-size: 12px; font-weight: 850; } .monitor-section-title::after { content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, var(--whut-border), transparent); } .monitor-groups { overflow: hidden; background: white; border: 1px solid var(--whut-border); border-radius: 17px; box-shadow: var(--whut-shadow-sm); } .monitor-group { border-bottom: 1px solid var(--whut-border); } .monitor-group:last-child { border-bottom: 0; } .monitor-group-header { width: 100%; min-height: 55px; padding: 11px 13px; display: grid; grid-template-columns: 18px minmax(100px, auto) minmax(0, 1fr) auto; align-items: center; gap: 8px; color: var(--whut-text); text-align: left; cursor: pointer; background: white; border: 0; transition: color var(--whut-fast), background var(--whut-fast); } .monitor-group-header:hover { color: var(--whut-primary); background: #f7fbff; } .monitor-group-arrow { color: var(--whut-text-muted); font-size: 10px; transition: transform var(--whut-normal) var(--whut-ease); } .monitor-group-title { font-size: 12px; font-weight: 850; } .monitor-group-credit { overflow: hidden; color: var(--whut-success); font-size: 11px; font-weight: 750; text-overflow: ellipsis; white-space: nowrap; } .monitor-group-badge { justify-self: end; } .monitor-group-body { overflow-x: auto; border-top: 1px solid var(--whut-border); animation: whutDetailIn 220ms var(--whut-ease) both; } .monitor-course-table { width: 100%; min-width: 690px; color: var(--whut-text); font-size: 11px; border-collapse: separate; border-spacing: 0; } .monitor-course-table th { padding: 10px 11px; color: var(--whut-text-secondary); font-weight: 800; text-align: left; white-space: nowrap; background: #f3f8fd; border-bottom: 1px solid var(--whut-border); } .monitor-course-table td { padding: 10px 11px; vertical-align: middle; border-top: 1px solid #edf2f7; } .monitor-course-table tbody tr:first-child td { border-top: 0; } .monitor-course-table tbody tr:hover td { background: #f8fbff; } .monitor-course-row.pending td { color: var(--whut-text-muted); background: #fafbfd; } .monitor-course-name { max-width: 260px; line-height: 1.5; } .monitor-pass-dot { width: 21px; height: 21px; display: inline-flex; align-items: center; justify-content: center; color: white; font-size: 11px; font-weight: 900; border-radius: 50%; } .monitor-pass-dot.pass { background: var(--whut-success); box-shadow: 0 3px 8px rgba(22, 163, 74, .18); } .monitor-pass-dot.fail { background: var(--whut-danger); } .monitor-pass-dot.pending { color: var(--whut-text-secondary); background: #dbe4ee; } .monitor-warning { margin-bottom: 9px; padding: 11px 12px; color: #9f1239; font-size: 11px; line-height: 1.6; background: var(--whut-danger-soft); border: 1px solid #fecdd3; border-radius: 13px; } .monitor-warning.is-ok { color: #166534; background: var(--whut-success-soft); border-color: #bbf7d0; } .monitor-empty { padding: 30px 12px; color: var(--whut-text-muted); text-align: center; } `, updateFeedbackAndModal: ` #update-container, #update-container-modal, .update-card, .update-card p, .update-card h3, .update-notes, .update-notes *, .uikit-dialog #update-container-modal * { text-align: left !important; } .update-card { color: var(--whut-text); } .update-card-intro { margin: 0 0 13px; color: var(--whut-text-secondary); font-size: 12px; line-height: 1.7; } .update-version-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; margin-bottom: 12px; } .update-status { margin-bottom: 12px; font-size: 11px; font-weight: 850; } .update-status.is-new { color: var(--whut-success); } .update-status.is-current { color: var(--whut-primary); } .update-status.is-error { color: var(--whut-danger); } .update-notes { margin-bottom: 13px; padding: 12px 14px; background: white; border: 1px solid var(--whut-border); border-radius: 14px; } .update-notes-title { margin-bottom: 7px; color: var(--whut-text); font-weight: 850; } .update-notes ul { margin: 0; padding-left: 20px; color: var(--whut-text-secondary); line-height: 1.75; } .update-notes li { display: list-item; margin: 3px 0; text-align: left !important; } #whut-toast-stack { position: fixed; left: 18px; bottom: 18px; z-index: 1000002; width: min(380px, calc(100vw - 36px)); display: flex; flex-direction: column-reverse; gap: 9px; pointer-events: none; } .whut-helper-toast, .whut-update-toast { position: relative; width: 100%; min-height: 58px; padding: 11px 38px 12px 13px; display: grid; grid-template-columns: 32px minmax(0, 1fr); align-items: start; gap: 10px; color: var(--whut-text); background: rgba(255, 255, 255, .97); border: 1px solid var(--whut-border); border-radius: 15px; box-shadow: 0 15px 38px rgba(30, 64, 104, .18); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); pointer-events: auto; overflow: hidden; animation: whutToastIn 260ms var(--whut-ease) both; } .whut-helper-toast::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: var(--whut-info); } .whut-helper-toast.success::before { background: var(--whut-success); } .whut-helper-toast.warning::before { background: var(--whut-warning); } .whut-helper-toast.error::before { background: var(--whut-danger); } .whut-helper-toast-icon { width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; font-size: 15px; background: #eef7fd; border-radius: 10px; } .whut-helper-toast.success .whut-helper-toast-icon { background: var(--whut-success-soft); } .whut-helper-toast.warning .whut-helper-toast-icon { background: var(--whut-warning-soft); } .whut-helper-toast.error .whut-helper-toast-icon { background: var(--whut-danger-soft); } .whut-helper-toast-body { min-width: 0; padding-top: 1px; display: flex; flex-direction: column; } .whut-helper-toast-title { overflow-wrap: anywhere; color: var(--whut-text); font-size: 12px; font-weight: 850; line-height: 1.45; } .whut-helper-toast-text { margin-top: 3px; overflow-wrap: anywhere; color: var(--whut-text-secondary); font-size: 10px; font-weight: 600; line-height: 1.55; } .whut-helper-toast-close { position: absolute; right: 7px; top: 7px; width: 25px; height: 25px; padding: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--whut-text-muted); font-size: 14px; cursor: pointer; background: transparent; border: 0; border-radius: 8px; } .whut-helper-toast-close:hover { color: var(--whut-text); background: #eef2f7; } .whut-helper-toast-progress { position: absolute; left: 4px; right: 0; bottom: 0; height: 2px; background: rgba(2, 132, 199, .2); transform-origin: left; animation: whutToastProgress var(--whut-toast-duration, 3000ms) linear both; } .whut-helper-toast.success .whut-helper-toast-progress { background: rgba(22, 163, 74, .25); } .whut-helper-toast.warning .whut-helper-toast-progress { background: rgba(217, 119, 6, .25); } .whut-helper-toast.error .whut-helper-toast-progress { background: rgba(220, 38, 38, .25); } .whut-helper-toast.is-leaving { animation: whutToastOut 180ms ease both; } `, uikitConformance: ` /* Tampermonkey UIKit 1.0 基线:统一组件密度、层级、主题与交互状态。 */ #whut-fab { width: 56px; height: 56px; z-index: var(--uikit-z-fab); color: #fff; border: 0; border-radius: 50%; background: linear-gradient(135deg, var(--uikit-brand-2), var(--uikit-brand)); box-shadow: var(--uikit-shadow-md); transition: transform var(--uikit-duration-fast) var(--uikit-ease-emphasized), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard), background-color var(--uikit-duration-theme) var(--uikit-ease-standard); } #whut-fab::before { display: none; } #whut-fab:hover { border-radius: 50%; transform: translateY(-2px) scale(1.06); box-shadow: var(--uikit-shadow-md); } #whut-fab:active { transform: scale(.96); } .whut-fab-glyph { font-size: 24px; filter: none; } .whut-fab-pulse { right: 5px; top: 5px; width: 9px; height: 9px; background: var(--uikit-success); box-shadow: 0 0 0 3px var(--uikit-bg); } #whut-helper-panel { z-index: var(--uikit-z-panel); width: ${PANEL_LAYOUT.width}px; min-width: 440px; height: 82vh; min-height: 480px; color: var(--uikit-text); font-size: 14px; background: var(--uikit-bg); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-lg); box-shadow: var(--uikit-shadow-lg); backdrop-filter: none; -webkit-backdrop-filter: none; transform-origin: right bottom; transition: background-color var(--uikit-duration-theme) var(--uikit-ease-standard), color var(--uikit-duration-theme) var(--uikit-ease-standard), border-color var(--uikit-duration-theme) var(--uikit-ease-standard), box-shadow var(--uikit-duration-theme) var(--uikit-ease-standard); } #whut-helper-panel.open { animation: uikitPanelIn var(--uikit-duration-normal) var(--uikit-ease-emphasized) both; } #whut-helper-panel.is-dragging { opacity: .94; box-shadow: var(--uikit-shadow-lg); } .helper-header { padding: 0; background: var(--uikit-bg); border-bottom: 1px solid var(--uikit-border); cursor: grab; } .header-top { min-height: 58px; margin: 0; padding: 8px 10px 8px 16px; gap: 12px; background: linear-gradient(135deg, var(--uikit-bg-muted), var(--uikit-bg-elevated)); border-bottom: 1px solid var(--uikit-border); } .header-title { gap: 10px; } .header-logo { width: 36px; height: 36px; flex-basis: 36px; border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); background: var(--uikit-brand-soft); box-shadow: var(--uikit-shadow-sm); font-size: 19px; } .header-brand-copy { gap: 2px; } .header-name { color: var(--uikit-text); font-size: 16px; font-weight: 800; letter-spacing: .01em; } .header-meta { gap: 5px; } .header-version, .header-author { min-height: 18px; padding: 1px 6px; color: var(--uikit-brand-text); font-size: 9px; font-weight: 800; background: var(--uikit-brand-soft); border: 0; border-radius: var(--uikit-radius-pill); } .header-author { color: var(--uikit-text-muted); background: var(--uikit-bg-muted); border: 1px solid var(--uikit-border); } .header-actions { gap: 5px; } .header-action { min-width: 34px; height: 34px; padding: 0 9px; color: var(--uikit-text-muted); background: transparent; border: 1px solid transparent; border-radius: var(--uikit-radius-pill); box-shadow: none; font-size: 12px; transition: background var(--uikit-duration-fast) var(--uikit-ease-standard), color var(--uikit-duration-fast) var(--uikit-ease-standard), border-color var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-emphasized), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard); } .header-action:hover { color: var(--uikit-text); background: var(--uikit-bg-muted); border-color: transparent; transform: translateY(-1px); box-shadow: var(--uikit-shadow-sm); } .header-action:active { transform: scale(.94); } .header-theme { width: 34px; padding: 0; font-size: 15px; } .header-update { min-width: 88px; border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-sm); background: var(--uikit-bg); } .header-update:hover { color: var(--uikit-brand); border-color: var(--uikit-brand); background: var(--uikit-brand-soft); } .header-update.has-update { color: var(--uikit-success); background: rgb(16 185 129 / 10%); border-color: rgb(16 185 129 / 32%); } .header-close { width: 34px; padding: 0; font-size: 18px; } .header-close:hover { color: #fff; background: var(--uikit-danger); border-color: var(--uikit-danger); transform: rotate(90deg); } .helper-tabs { display: flex; gap: 0; padding: 0; overflow-x: auto; background: var(--uikit-bg); border: 0; scroll-snap-type: x proximity; scrollbar-width: thin; } .helper-tabs::-webkit-scrollbar { display: block; height: 8px; } .helper-tab { position: relative; min-width: 104px; min-height: 44px; flex: 1 0 auto; padding: 10px 9px; gap: 5px; color: var(--uikit-text-muted); font-size: 12px; font-weight: 700; background: transparent; border: 0; border-radius: 0; box-shadow: none; } .helper-tab::after { left: 10px; right: 10px; bottom: 0; height: 3px; opacity: 0; transform: scaleX(.5); background: linear-gradient(90deg, var(--uikit-brand-2), var(--uikit-brand)); border-radius: 3px 3px 0 0; } .helper-tab:hover { color: var(--uikit-text); background: var(--uikit-brand-soft); transform: none; } .helper-tab.active { color: var(--uikit-brand); background: transparent; border: 0; box-shadow: none; } .helper-tab.active::after { opacity: 1; transform: scaleX(1); } .tab-view.active { animation: uikitViewIn var(--uikit-duration-fast) var(--uikit-ease-standard) both; } .helper-toolbar { padding: 12px 16px; background: var(--uikit-bg); border-bottom: 1px solid var(--uikit-border); box-shadow: none; } .search-box { gap: 8px; margin-bottom: 9px; } .filter-row { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-bottom: 9px; } .action-row { gap: 8px; } .search-input, .filter-select, .semester-filter-button, .eval-field textarea, .eval-field select, .eval-field input, .uikit-dialog .gpa-sem-input { color: var(--uikit-text); background-color: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border-strong); border-radius: var(--uikit-radius-sm); box-shadow: none; transition: color var(--uikit-duration-fast) var(--uikit-ease-standard), border-color var(--uikit-duration-fast) var(--uikit-ease-standard), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard), background-color var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-standard); } .search-input { height: 36px; padding: 7px 34px 7px 34px; background-position: 10px center; } .ui-select { position: relative; width: 100%; min-width: 0; } .ui-select-native { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; clip-path: inset(50%) !important; white-space: nowrap !important; border: 0 !important; opacity: 0 !important; pointer-events: none !important; } .ui-select-trigger, .semester-filter-button { width: 100%; height: 38px; min-height: 38px; padding: 0 36px 0 12px; color: var(--uikit-text); font-size: 12px; font-weight: 650; line-height: 1; text-align: left; background-color: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border-strong); border-radius: var(--uikit-radius-sm); box-shadow: var(--uikit-shadow-sm); transition: color var(--uikit-duration-fast) var(--uikit-ease-standard), border-color var(--uikit-duration-fast) var(--uikit-ease-standard), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard), background-color var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-emphasized); } .semester-filter { width: 100%; } .ui-select-trigger, .semester-filter-button { position: relative; display: flex; align-items: center; justify-content: flex-start; gap: 8px; cursor: pointer; } .ui-select-value, .semester-filter-label { min-width: 0; overflow: hidden; flex: 1; text-overflow: ellipsis; white-space: nowrap; } .ui-select-chevron, .semester-filter-chevron { position: absolute; top: 50%; right: 12px; color: var(--uikit-text-muted); transform: translateY(-50%); transition: transform var(--uikit-duration-fast) var(--uikit-ease-emphasized), color var(--uikit-duration-fast) var(--uikit-ease-standard); } .ui-select.is-open .ui-select-chevron, .semester-filter.open .semester-filter-chevron { transform: translateY(-50%) rotate(180deg); } .search-input:hover, .ui-select-trigger:hover, .semester-filter-button:hover, .eval-field textarea:hover, .eval-field select:hover, .eval-field input:hover, .uikit-dialog .gpa-sem-input:hover { border-color: var(--uikit-brand); background-color: var(--uikit-bg-elevated); } .ui-select-trigger:hover, .semester-filter-button:hover { color: var(--uikit-brand-text); transform: translateY(-1px); box-shadow: 0 5px 14px rgb(var(--uikit-brand-rgb) / 11%); } .search-input:focus, .ui-select-trigger:focus, .semester-filter-button:focus, .eval-field textarea:focus, .eval-field select:focus, .eval-field input:focus, .uikit-dialog .gpa-sem-input:focus { background: var(--uikit-bg-elevated); border-color: var(--uikit-brand); box-shadow: 0 0 0 3px rgb(var(--uikit-brand-rgb) / 20%); } .ui-select.is-open .ui-select-trigger, .semester-filter.open .semester-filter-button { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-color: var(--uikit-brand); box-shadow: 0 0 0 3px rgb(var(--uikit-brand-rgb) / 14%); } .ui-select-trigger:disabled { color: var(--uikit-text-faint); cursor: not-allowed; opacity: .58; transform: none; box-shadow: none; } .ui-select-menu { display: none; position: fixed; z-index: var(--uikit-z-menu); max-height: min(320px, calc(100vh - 24px)); padding: 6px; overflow-y: auto; color: var(--uikit-text); background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); box-shadow: var(--uikit-shadow-lg); transform-origin: top center; } .ui-select-menu.is-open { display: grid; gap: 2px; animation: whutMenuIn var(--uikit-duration-fast) var(--uikit-ease-emphasized) both; } .ui-select-option { width: 100%; min-height: 35px; padding: 7px 10px; display: flex; align-items: center; gap: 9px; color: var(--uikit-text); font-size: 12px; font-weight: 620; line-height: 1.35; text-align: left; cursor: pointer; background: transparent; border: 1px solid transparent; border-radius: var(--uikit-radius-sm); transition: color var(--uikit-duration-fast) var(--uikit-ease-standard), background-color var(--uikit-duration-fast) var(--uikit-ease-standard), border-color var(--uikit-duration-fast) var(--uikit-ease-standard), transform var(--uikit-duration-fast) var(--uikit-ease-emphasized); } .ui-select-option:hover, .ui-select-option.is-active { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-color: rgb(var(--uikit-brand-rgb) / 20%); transform: translateX(2px); } .ui-select-option[aria-selected='true'] { color: var(--uikit-brand-text); font-weight: 780; background: var(--uikit-brand-soft); } .ui-select-option:disabled { color: var(--uikit-text-faint); cursor: not-allowed; opacity: .52; transform: none; } .ui-select-option-check { width: 17px; flex: 0 0 17px; color: transparent; font-size: 12px; font-weight: 900; text-align: center; } .ui-select-option[aria-selected='true'] .ui-select-option-check { color: var(--uikit-brand); } .ui-select-option-label { min-width: 0; overflow: hidden; flex: 1; text-overflow: ellipsis; white-space: nowrap; } .semester-menu { top: calc(100% + 6px); padding: 6px; background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); box-shadow: var(--uikit-shadow-lg); } .semester-menu.is-portaled { display: block; position: fixed; right: auto; z-index: var(--uikit-z-menu); overflow-y: auto; } .semester-action { min-height: 30px; color: var(--uikit-text-muted); background: var(--uikit-bg-muted); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-sm); } .semester-action:hover, .semester-option:hover { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-color: var(--uikit-brand); } .semester-option { min-height: 34px; color: var(--uikit-text); border-radius: var(--uikit-radius-sm); } .semester-option input, .pj-grp-cb, .pj-item-cb { transition: transform var(--uikit-duration-fast) var(--uikit-ease-emphasized), opacity var(--uikit-duration-fast) var(--uikit-ease-standard); } .semester-option input:hover, .pj-grp-cb:hover, .pj-item-cb:not(:disabled):hover { transform: scale(1.1); } .semester-option input:active, .pj-grp-cb:active, .pj-item-cb:not(:disabled):active { transform: scale(.92); } .btn-modern, .uikit-dialog .btn-modern { min-height: 34px; padding: 7px 13px; color: #fff; font-size: 12px; font-weight: 750; background: linear-gradient(135deg, var(--uikit-brand-2), var(--uikit-brand)); border: 1px solid transparent; border-radius: var(--uikit-radius-sm); box-shadow: var(--uikit-shadow-sm); position: relative; overflow: hidden; isolation: isolate; transform: translateZ(0); backface-visibility: hidden; transition: transform var(--uikit-duration-fast) var(--uikit-ease-emphasized), box-shadow var(--uikit-duration-fast) var(--uikit-ease-standard), opacity var(--uikit-duration-fast) var(--uikit-ease-standard), color var(--uikit-duration-fast) var(--uikit-ease-standard), background-color var(--uikit-duration-fast) var(--uikit-ease-standard); } .btn-modern::before, .uikit-dialog .btn-modern::before { content: ''; position: absolute; inset: 0; background: linear-gradient(100deg, transparent 20%, rgb(255 255 255 / 22%) 50%, transparent 80%); transform: translateX(-115%); transition: transform 600ms var(--uikit-ease-standard); pointer-events: none; } .btn-modern:hover::before, .uikit-dialog .btn-modern:hover::before { transform: translateX(115%); } .btn-modern:hover, .uikit-dialog .btn-modern:hover { background: linear-gradient(135deg, var(--uikit-brand-2), var(--uikit-brand)); transform: translateY(-2px) translateZ(0); box-shadow: 0 6px 16px rgb(var(--uikit-brand-rgb) / 22%); } .btn-modern:active, .uikit-dialog .btn-modern:active { transform: translateY(0) scale(.98); } .btn-modern:disabled, .btn-modern.is-loading, .uikit-dialog .btn-modern:disabled { opacity: .52; cursor: not-allowed; transform: none; box-shadow: none; } .btn-outline, .uikit-dialog .btn-outline { color: var(--uikit-text); background: var(--uikit-bg); border-color: var(--uikit-border-strong); box-shadow: none; } .btn-outline:hover, .uikit-dialog .btn-outline:hover { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-color: var(--uikit-brand); } .btn-success { background: linear-gradient(135deg, #34d399, var(--uikit-success)); } .helper-content, .monitor-container, .gpa-container, .export-container, #pj-list-area { background: var(--uikit-bg-muted) !important; } .helper-content { padding: 14px 16px; } .monitor-container, .gpa-container, .export-container { padding: 16px; } .ui-state { min-height: 180px; color: var(--uikit-text-muted); } .ui-state-icon { color: var(--uikit-brand); background: var(--uikit-brand-soft); border-color: var(--uikit-border); } .ui-state.empty .ui-state-icon { color: var(--uikit-text-muted); background: var(--uikit-bg); border-color: var(--uikit-border); } .ui-state.warning .ui-state-icon { color: var(--uikit-warning); background: rgb(245 158 11 / 12%); border-color: rgb(245 158 11 / 24%); } .ui-state.error .ui-state-icon { color: var(--uikit-danger); background: rgb(239 68 68 / 10%); border-color: rgb(239 68 68 / 24%); } .ui-state-title { color: var(--uikit-text); } .ui-state-detail { color: var(--uikit-text-muted); } .course-item, .export-card, .gpa-card, .monitor-overview, .monitor-groups, #pj-list-area > .pj-group, .eval-config-card, .eval-summary { background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-md); box-shadow: var(--uikit-shadow-sm); } .course-item { margin-bottom: 9px; } .course-item:hover { border-color: var(--uikit-brand); transform: translateY(-2px); box-shadow: 0 8px 20px rgb(var(--uikit-brand-rgb) / 10%); } .course-summary { min-height: 78px; padding: 12px 14px; background: var(--uikit-bg-elevated); } .course-summary:hover { background: var(--uikit-brand-soft); } .course-title { color: var(--uikit-text); font-size: 13px; font-weight: 800; } .course-score { color: var(--uikit-brand); font-size: 18px; font-variant-numeric: tabular-nums; } .tag { color: var(--uikit-text-muted); background: var(--uikit-bg-muted); border-color: var(--uikit-border); border-radius: var(--uikit-radius-pill); } .tag-credit { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-color: transparent; } .tag-compulsory { color: #be185d; background: rgb(236 72 153 / 10%); border-color: transparent; } .tag-source-monitor { color: var(--uikit-success); background: rgb(16 185 129 / 12%); border-color: rgb(16 185 129 / 22%); } .tag-source-transcript { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); border-color: transparent; } .score-high { color: var(--uikit-success); } .score-med { color: #d97706; } .score-low { color: var(--uikit-danger); } .score-none { color: var(--uikit-text-faint); } .detail-panel { background: var(--uikit-bg-muted); border-top-color: var(--uikit-border); } .compare-table { background: var(--uikit-bg); border-color: var(--uikit-border); border-radius: var(--uikit-radius-md); } .compare-table th { color: var(--uikit-text-muted); background: var(--uikit-brand-soft); } .compare-table td { color: var(--uikit-text); border-color: var(--uikit-border); } .dist-track { background: var(--uikit-border); } .dist-fill { background: linear-gradient(90deg, var(--uikit-brand-2), var(--uikit-brand)); } .helper-footer { padding: 10px 14px; background: var(--uikit-bg-elevated); border-top-color: var(--uikit-border); box-shadow: none; } .footer-metric { padding: 8px 10px; background: var(--uikit-bg-muted); border-color: var(--uikit-border); border-radius: var(--uikit-radius-sm); } .footer-label { color: var(--uikit-text-faint); } .footer-value { color: var(--uikit-text); font-variant-numeric: tabular-nums; } .footer-metric.is-deep .footer-value { color: var(--uikit-brand-text); } .footer-metric.is-primary .footer-value { color: var(--uikit-brand); } .export-card, .gpa-card { padding: 14px; margin-bottom: 12px; } .export-card h3, .gpa-card-title { color: var(--uikit-text); } .export-card p { color: var(--uikit-text-muted); } .print-row { background: var(--uikit-bg-elevated); border-color: var(--uikit-border); border-radius: var(--uikit-radius-md); } .print-row:hover { background: var(--uikit-brand-soft); border-color: var(--uikit-brand); transform: translateY(-2px); box-shadow: 0 8px 20px rgb(var(--uikit-brand-rgb) / 10%); } .print-name { color: var(--uikit-text); } .gpa-item { background: var(--uikit-bg-muted); border-color: var(--uikit-border); } .gpa-item-value { color: var(--uikit-brand); } .gpa-item-value.accent, .gpa-item-value.compulsory-gpa-value { color: var(--uikit-brand-text); } .gpa-ranks { color: var(--uikit-text-muted); background: var(--uikit-bg-muted); } .eval-config-card { padding: 12px; } .pj-group-head, .pj-group-header, .pj-group-content, .pj-item { color: var(--uikit-text); background: var(--uikit-bg-elevated); border-color: var(--uikit-border); } .pj-group-header:hover, .pj-item:hover { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); } .pj-badge.ing { color: var(--uikit-success); background: rgb(16 185 129 / 14%); } .pj-badge.done { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); } .pj-badge.wait { color: var(--uikit-text-muted); background: var(--uikit-border); } .pj-badge.end { color: var(--uikit-danger); background: rgb(239 68 68 / 12%); } .monitor-overview { background: var(--uikit-bg-elevated); border-color: var(--uikit-border); border-radius: var(--uikit-radius-md); } .monitor-overview::after { display: none; } .monitor-overview-value, .monitor-section-title, .monitor-group-title { color: var(--uikit-text); } .monitor-status-pill.pass, .monitor-group-badge.pass { color: var(--uikit-success); background: rgb(16 185 129 / 12%); border-color: rgb(16 185 129 / 25%); } .monitor-status-pill.warn, .monitor-group-badge.warn { color: var(--uikit-danger); background: rgb(239 68 68 / 11%); border-color: rgb(239 68 68 / 25%); } .monitor-progress-track { background: var(--uikit-border); } .monitor-progress-fill { background: linear-gradient(90deg, var(--uikit-brand-2), var(--uikit-brand)); box-shadow: none; } .monitor-progress-meta, .monitor-status { color: var(--uikit-text-muted); } .monitor-groups { background: var(--uikit-bg-elevated); } .monitor-group-header { color: var(--uikit-text); background: var(--uikit-bg-elevated); } .monitor-group-header:hover { color: var(--uikit-brand-text); background: var(--uikit-brand-soft); } .monitor-group-credit { color: var(--uikit-success); } .monitor-course-table { color: var(--uikit-text); } .monitor-course-table th { color: var(--uikit-text-muted); background: var(--uikit-bg-muted); border-color: var(--uikit-border); } .monitor-course-table td { border-color: var(--uikit-border); } .monitor-course-table tbody tr:hover td { background: var(--uikit-brand-soft); } .monitor-course-row.pending td { color: var(--uikit-text-faint); background: var(--uikit-bg-muted); } .monitor-warning { color: var(--uikit-danger); background: rgb(239 68 68 / 9%); border-color: rgb(239 68 68 / 24%); } .monitor-warning.is-ok { color: var(--uikit-success); background: rgb(16 185 129 / 9%); border-color: rgb(16 185 129 / 24%); } #update-container, #update-container-modal, .update-card, .update-card p, .update-card h3, .update-notes, .update-notes *, #update-container-modal * { text-align: left !important; } .update-card, .update-notes { color: var(--uikit-text); } .update-card-intro, .update-notes ul { color: var(--uikit-text-muted); } .update-notes { background: var(--uikit-bg-muted); border-color: var(--uikit-border); border-radius: var(--uikit-radius-md); } #whut-toast-stack { top: 18px; right: auto; bottom: auto; left: 50%; width: min(430px, calc(100vw - 24px)); display: grid; justify-items: center; gap: 8px; transform: translateX(-50%); z-index: var(--uikit-z-toast); } .whut-helper-toast { width: auto; max-width: 100%; min-height: 38px; padding: 9px 34px 9px 13px; display: flex; align-items: center; gap: 8px; color: #fff; background: linear-gradient(135deg, rgb(var(--uikit-brand-rgb) / 96%), var(--uikit-brand)); border: 0; border-radius: var(--uikit-radius-pill); box-shadow: 0 10px 28px rgb(var(--uikit-brand-rgb) / 22%); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); animation: uikitToastIn var(--uikit-duration-normal) var(--uikit-ease-emphasized) both; } .whut-helper-toast::before, .whut-helper-toast-progress { display: none; } .whut-helper-toast.success { background: rgb(5 150 105 / 95%); } .whut-helper-toast.warning { background: rgb(217 119 6 / 95%); } .whut-helper-toast.error { background: rgb(220 38 38 / 95%); } .whut-helper-toast-icon { width: auto; height: auto; padding: 0; color: inherit; background: transparent !important; } .whut-helper-toast-title { color: #fff; font-size: 12px; font-weight: 700; } .whut-helper-toast-text { color: rgb(255 255 255 / 82%); font-size: 10px; } .whut-helper-toast-close { top: 50%; right: 6px; color: rgb(255 255 255 / 72%); transform: translateY(-50%); } .whut-helper-toast-close:hover { color: #fff; background: rgb(255 255 255 / 12%); } .whut-helper-toast.is-leaving { animation: uikitToastOut var(--uikit-duration-fast) ease both; } .uikit-dialog-backdrop { position: fixed; inset: 0; z-index: var(--uikit-z-dialog); padding: 20px; display: flex; align-items: center; justify-content: center; background: var(--uikit-overlay); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); opacity: 0; transition: opacity var(--uikit-duration-fast); pointer-events: auto; } .uikit-dialog-backdrop.is-show { opacity: 1; } .uikit-dialog { position: relative; width: min(420px, calc(100vw - 32px)); max-height: min(84vh, 760px); overflow: hidden; display: flex; flex-direction: column; color: var(--uikit-text); background: var(--uikit-bg-elevated); border: 1px solid var(--uikit-border); border-radius: var(--uikit-radius-lg); box-shadow: var(--uikit-shadow-lg); transform: translateY(8px) scale(.98); transition: transform var(--uikit-duration-normal) var(--uikit-ease-emphasized); } .uikit-dialog-backdrop.is-show .uikit-dialog { transform: translateY(0) scale(1); } .uikit-dialog::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 5px; background: var(--uikit-brand); } .uikit-dialog.is-danger::before { background: var(--uikit-danger); } .uikit-dialog__header { padding: 18px 42px 10px 24px; } .uikit-dialog__eyebrow { color: var(--uikit-brand); font-size: 10px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; } .uikit-dialog.is-danger .uikit-dialog__eyebrow { color: var(--uikit-danger); } .uikit-dialog__title { margin: 4px 0 0; color: var(--uikit-text); font-size: 18px; font-weight: 850; } .uikit-dialog__close { position: absolute; top: 9px; right: 9px; } .uikit-dialog__content { min-height: 0; padding: 4px 24px 16px; overflow: auto; color: var(--uikit-text-muted); font-size: 12px; line-height: 1.65; } .uikit-dialog__content > p { margin: 0; white-space: pre-wrap; } .uikit-dialog__validation { margin-top: 9px; padding: 7px 9px; color: var(--uikit-danger); background: rgb(239 68 68 / 9%); border-radius: var(--uikit-radius-sm); } .uikit-dialog__actions { padding: 11px 15px 15px 24px; display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--uikit-border); background: var(--uikit-bg-muted); } .uikit-dialog__spinner { width: 22px; height: 22px; margin: 18px auto; border: 2px solid var(--uikit-border); border-top-color: var(--uikit-brand); border-radius: 50%; animation: whutSpin .75s linear infinite; } :host([data-theme='dark']) .header-logo, :host([data-theme='dark']) .semester-menu, :host([data-theme='dark']) .course-item, :host([data-theme='dark']) .export-card, :host([data-theme='dark']) .gpa-card, :host([data-theme='dark']) .monitor-overview, :host([data-theme='dark']) .monitor-groups, :host([data-theme='dark']) .eval-config-card, :host([data-theme='dark']) .eval-summary, :host([data-theme='dark']) #pj-list-area > .pj-group { color: var(--uikit-text); background: var(--uikit-bg-elevated); } @keyframes uikitPanelIn { from { opacity: 0; transform: translateY(12px) scale(.96); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes uikitViewIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } @keyframes uikitToastIn { from { opacity: 0; transform: translateY(-8px) scale(.97); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes uikitToastOut { to { opacity: 0; transform: translateY(-8px) scale(.97); } } `, responsiveAndMotion: ` @media (max-width: 720px) { #whut-helper-panel { min-width: 0; width: calc(100vw - 24px) !important; height: min(72vh, 620px) !important; max-width: calc(100vw - 24px); max-height: calc(100vh - 24px); border-radius: var(--uikit-radius-lg); } #whut-fab { right: 12px; bottom: max(12px, env(safe-area-inset-bottom)); } .helper-header { padding: 0; } .header-top { margin: 0; } .header-logo { width: 34px; height: 34px; flex-basis: 34px; border-radius: 11px; } .header-name { font-size: 15px; } .helper-tabs { overflow-x: auto; } .helper-toolbar, .helper-content, .monitor-container, .gpa-container, .export-container { padding-left: 14px; padding-right: 14px; } #pj-list-area { padding-left: 14px !important; padding-right: 14px !important; } .monitor-group-header { grid-template-columns: 18px minmax(0, 1fr) auto; } .monitor-group-credit { grid-column: 2 / 4; grid-row: 2; } } @media (max-width: 560px) { .header-update { min-width: 86px; width: auto; padding: 0 9px; } .search-box { grid-template-columns: minmax(0, 1fr) 72px; } .filter-row { grid-template-columns: 1fr; } .action-row { display: grid; grid-template-columns: 1fr; } .monitor-toolbar-row, .eval-status-row { align-items: stretch; flex-direction: column; } .monitor-actions, .eval-status-actions { width: 100%; } .monitor-actions .btn-modern, .eval-status-actions .btn-modern { flex: 1; } .eval-status-note { display: none; } .eval-grid, .gpa-grid, .update-version-grid { grid-template-columns: 1fr; } .eval-comment { grid-column: auto; } .eval-summary { grid-template-columns: 1fr; } .eval-summary-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); } .course-summary { min-height: 82px; padding: 13px 14px; } .course-score { min-width: 62px; font-size: 17px; } .helper-footer { grid-template-columns: repeat(2, minmax(0, 1fr)); padding-left: 12px; padding-right: 12px; } .monitor-overview-value { font-size: 17px; } .monitor-progress-meta { flex-direction: column; gap: 3px; } .print-row { align-items: stretch; flex-direction: column; } .print-row .btn-modern { width: 100%; } } @media (max-width: 390px) { .header-logo { display: none; } .header-name { font-size: 14px; } .header-actions { gap: 5px; } .header-action { min-width: 35px; width: 35px; height: 35px; padding: 0; } .header-update { min-width: 86px; width: auto; padding: 0 9px; } .helper-toolbar { padding-top: 11px; padding-bottom: 11px; } .helper-content, .monitor-container, .gpa-container, .export-container { padding: 12px 10px; } #pj-list-area { padding: 12px 10px !important; } .eval-summary-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (prefers-reduced-motion: reduce) { #whut-helper-panel *, #whut-fab, #whut-toast-stack *, .uikit-dialog-backdrop * { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } } @keyframes whutPanelIn { from { opacity: 0; transform: translateY(10px) scale(.975); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes whutViewIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @keyframes whutDetailIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } @keyframes whutMenuIn { from { opacity: 0; transform: translateY(-6px) scale(.985); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes whutSpin { to { transform: rotate(360deg); } } @keyframes whutToastIn { from { opacity: 0; transform: translateX(-16px) scale(.98); } to { opacity: 1; transform: translateX(0) scale(1); } } @keyframes whutToastOut { to { opacity: 0; transform: translateX(-18px) scale(.98); } } @keyframes whutToastProgress { from { transform: scaleX(1); } to { transform: scaleX(0); } } ` }); const APP_STYLE = Object.values(STYLE_MODULES).join('\n'); // ================= UIKit Core:单一 Host、Shadow Root 与独立浮层 ================= const UIKitRuntime = (() => { const HOST_ID = 'whut-uikit-host'; const THEME_KEY = 'uikit:whut-jw-helper:theme'; const layerNames = ['panel', 'menu', 'dialog', 'toast']; let host = null; let shadow = null; let root = null; let layers = null; let styleNode = null; let themeMode = 'auto'; let mediaQuery = null; let mediaListener = null; const storageGet = (key, fallback) => { try { return localStorage.getItem(key) || fallback; } catch (e) { return fallback; } }; const storageSet = (key, value) => { try { localStorage.setItem(key, value); } catch (e) { /* 页面可能限制存储 */ } }; const validTheme = value => ['light', 'dark', 'auto'].includes(value) ? value : 'auto'; function resolveTheme() { if (themeMode !== 'auto') return themeMode; return mediaQuery?.matches ? 'dark' : 'light'; } function applyTheme() { if (!host) return; host.dataset.themeMode = themeMode; host.dataset.theme = resolveTheme(); } function bindThemeListener() { if (mediaQuery || typeof matchMedia !== 'function') return; mediaQuery = matchMedia('(prefers-color-scheme: dark)'); mediaListener = () => { if (themeMode === 'auto') applyTheme(); }; if (typeof mediaQuery.addEventListener === 'function') mediaQuery.addEventListener('change', mediaListener); else mediaQuery.addListener?.(mediaListener); } function appendHost() { const parent = document.documentElement || document.body; if (parent && host && !host.isConnected) parent.appendChild(host); } function createRuntime(cssText = APP_STYLE) { host = document.createElement('div'); host.id = HOST_ID; // layout/paint containment 会把 fixed 子元素错误约束在 0×0 Host 内, // 因此这里只隔离样式,让面板和浮层继续以浏览器视口为定位基准。 host.style.cssText = 'all:initial;position:fixed;inset:0 auto auto 0;width:0;height:0;z-index:2147483647;pointer-events:none;contain:style;'; shadow = host.attachShadow({ mode: 'open' }); styleNode = document.createElement('style'); styleNode.dataset.uikitStyle = 'whut-jw-helper'; styleNode.textContent = cssText; root = document.createElement('div'); root.className = 'uikit-root'; root.setAttribute('data-uikit-version', '1.0.0'); layers = Object.create(null); layerNames.forEach(name => { const layer = document.createElement('div'); layer.className = `uikit-layer uikit-layer--${name}`; layer.dataset.layer = name; root.appendChild(layer); layers[name] = layer; }); shadow.append(styleNode, root); appendHost(); bindThemeListener(); themeMode = validTheme(storageGet(THEME_KEY, 'auto')); applyTheme(); return api; } function ensure(cssText = APP_STYLE) { if (host && shadow && root && layers) { if (!host.isConnected) appendHost(); if (styleNode && styleNode.textContent !== cssText) styleNode.textContent = cssText; return api; } const existing = document.getElementById(HOST_ID); if (existing) existing.remove(); return createRuntime(cssText); } function setTheme(mode) { themeMode = validTheme(mode); storageSet(THEME_KEY, themeMode); ensure(); applyTheme(); shadow?.dispatchEvent(new CustomEvent('uikit-theme-change', { detail: { mode: themeMode, resolved: resolveTheme() } })); } function cycleTheme() { const modes = ['auto', 'light', 'dark']; setTheme(modes[(modes.indexOf(themeMode) + 1) % modes.length]); return themeMode; } function mount(layerName, node) { ensure(); const layer = layers[layerName] || layers.panel; layer.appendChild(node); return node; } function destroy() { if (mediaQuery && mediaListener) { if (typeof mediaQuery.removeEventListener === 'function') mediaQuery.removeEventListener('change', mediaListener); else mediaQuery.removeListener?.(mediaListener); } host?.remove(); host = shadow = root = layers = styleNode = mediaQuery = mediaListener = null; } const api = { ensure, mount, setTheme, cycleTheme, getTheme: () => themeMode, getResolvedTheme: () => resolveTheme(), getById: id => (ensure(), shadow.getElementById(id)), query: selector => (ensure(), shadow.querySelector(selector)), queryAll: selector => (ensure(), Array.from(shadow.querySelectorAll(selector))), get shadow() { ensure(); return shadow; }, get root() { ensure(); return root; }, get layers() { ensure(); return layers; }, destroy }; return api; })(); const uiGet = id => UIKitRuntime.getById(id); const uiQuery = selector => UIKitRuntime.query(selector); const uiQueryAll = selector => UIKitRuntime.queryAll(selector); // ================= UIKit Dialog:焦点锁定、恢复与异步校验 ================= const AppDialog = (() => { let current = null; let queue = Promise.resolve(); const normalizeOptions = args => { if (typeof args[0] === 'string') return { title: args[0], text: args[1] || '', icon: args[2] || 'info' }; return { ...(args[0] || {}) }; }; function focusableOf(dialog) { return Array.from(dialog.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])')) .filter(node => !node.hidden && node.getAttribute('aria-hidden') !== 'true'); } function closeCurrent(result = { isDismissed: true }) { if (!current) return; const entry = current; current = null; clearTimeout(entry.timer); entry.backdrop.classList.remove('is-show'); setTimeout(() => entry.backdrop.remove(), 170); entry.previousFocus?.focus?.({ preventScroll: true }); entry.resolve(result); } async function open(options) { UIKitRuntime.ensure(); const previousFocus = UIKitRuntime.shadow.activeElement || document.activeElement; const backdrop = document.createElement('div'); backdrop.className = 'uikit-dialog-backdrop'; const dialog = document.createElement('section'); dialog.className = `uikit-dialog${options.icon === 'error' || options.tone === 'danger' ? ' is-danger' : ''}`; dialog.setAttribute('role', options.icon === 'warning' || options.icon === 'error' ? 'alertdialog' : 'dialog'); dialog.setAttribute('aria-modal', 'true'); if (options.width) dialog.style.width = typeof options.width === 'number' ? `${options.width}px` : String(options.width); const titleId = `uikit-dialog-title-${Date.now().toString(36)}`; const descId = `uikit-dialog-desc-${Date.now().toString(36)}`; const header = document.createElement('header'); header.className = 'uikit-dialog__header'; const eyebrow = document.createElement('div'); eyebrow.className = 'uikit-dialog__eyebrow'; eyebrow.textContent = options.eyebrow || ({ success: '操作完成', error: '发生错误', warning: '请注意', question: '请确认' }[options.icon] || 'WHUT 助手'); const title = document.createElement('h2'); title.className = 'uikit-dialog__title'; title.id = titleId; title.textContent = options.title || '提示'; header.append(eyebrow, title); dialog.setAttribute('aria-labelledby', titleId); if (options.showCloseButton) { const close = document.createElement('button'); close.type = 'button'; close.className = 'header-action uikit-dialog__close'; close.setAttribute('aria-label', '关闭对话框'); close.textContent = '×'; close.addEventListener('click', () => closeCurrent({ isDismissed: true, dismiss: 'close' })); header.appendChild(close); } const content = document.createElement('div'); content.className = 'uikit-dialog__content'; content.id = descId; dialog.setAttribute('aria-describedby', descId); if (options.unsafeHTML) content.innerHTML = String(options.unsafeHTML); else { const paragraph = document.createElement('p'); paragraph.textContent = options.text || ''; content.appendChild(paragraph); } const validation = document.createElement('div'); validation.className = 'uikit-dialog__validation'; validation.setAttribute('role', 'alert'); validation.hidden = true; content.appendChild(validation); const actions = document.createElement('footer'); actions.className = 'uikit-dialog__actions'; const makeAction = (label, className) => { const button = document.createElement('button'); button.type = 'button'; button.className = `btn-modern ${className}`.trim(); button.textContent = label; return button; }; let cancelButton = null; let denyButton = null; let confirmButton = null; if (options.showCancelButton) { cancelButton = makeAction(options.cancelButtonText || '取消', 'btn-outline'); cancelButton.addEventListener('click', () => closeCurrent({ isDismissed: true, dismiss: 'cancel' })); actions.appendChild(cancelButton); } if (options.showDenyButton) { denyButton = makeAction(options.denyButtonText || '稍后', 'btn-outline'); denyButton.addEventListener('click', () => closeCurrent({ isDenied: true, value: false })); actions.appendChild(denyButton); } if (options.showConfirmButton !== false) { confirmButton = makeAction(options.confirmButtonText || '确定', options.tone === 'danger' ? 'btn-danger' : ''); actions.appendChild(confirmButton); } if (actions.children.length) dialog.append(header, content, actions); else dialog.append(header, content); backdrop.appendChild(dialog); UIKitRuntime.mount('dialog', backdrop); const resultPromise = new Promise(resolve => { current = { backdrop, dialog, content, validation, actions, confirmButton, cancelButton, denyButton, previousFocus, resolve, timer: 0 }; }); const showValidation = message => { validation.textContent = String(message || '请检查输入内容'); validation.hidden = false; }; if (confirmButton) confirmButton.addEventListener('click', async () => { validation.hidden = true; let value = true; if (typeof options.preConfirm === 'function') { confirmButton.disabled = true; confirmButton.classList.add('is-loading'); try { value = await options.preConfirm(); } catch (error) { showValidation(error?.message || String(error)); value = false; } finally { confirmButton.disabled = false; confirmButton.classList.remove('is-loading'); } if (value === false || !validation.hidden) return; } closeCurrent({ isConfirmed: true, value }); }); const keyHandler = event => { if (!current || current.dialog !== dialog) return; if (event.key === 'Escape' && options.closeOnEscape !== false) { event.preventDefault(); closeCurrent({ isDismissed: true, dismiss: 'escape' }); return; } if (event.key !== 'Tab') return; const focusable = focusableOf(dialog); if (!focusable.length) { event.preventDefault(); dialog.focus(); return; } const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && UIKitRuntime.shadow.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && UIKitRuntime.shadow.activeElement === last) { event.preventDefault(); first.focus(); } }; backdrop.addEventListener('keydown', keyHandler); backdrop.addEventListener('mousedown', event => { if (event.target === backdrop && options.allowOutsideClick !== false && options.closeOnBackdrop !== false) { closeCurrent({ isDismissed: true, dismiss: 'backdrop' }); } }); requestAnimationFrame(() => { if (!current || current.dialog !== dialog || !backdrop.isConnected) return; backdrop.classList.add('is-show'); const initial = options.initialFocus === 'cancel' ? cancelButton : (confirmButton || cancelButton || denyButton || dialog); if (!initial.hasAttribute('tabindex') && initial === dialog) initial.tabIndex = -1; initial?.focus?.({ preventScroll: true }); options.didOpen?.(dialog); }); if (options.timer > 0) current.timer = setTimeout(() => closeCurrent({ isDismissed: true, dismiss: 'timer' }), options.timer); AppDialog._showValidationMessage = showValidation; return resultPromise; } const api = { fire(...args) { const options = normalizeOptions(args); const task = () => open(options); const result = queue.then(task, task); queue = result.catch(() => ({})); return result; }, close: () => closeCurrent({ isDismissed: true, dismiss: 'programmatic' }), showLoading() { if (!current) return; current.content.innerHTML = '
'; Array.from(current.actions.children).forEach(button => { button.disabled = true; }); }, showValidationMessage(message) { AppDialog._showValidationMessage?.(message); }, getCurrentDialog: () => current?.dialog || null, _showValidationMessage: null }; return api; })(); // ================= 🧠 统一状态与全局接口 ================= function showHelperToast({ icon = 'info', title = '', text = '', timer = 3000 } = {}) { const type = ['success', 'error', 'warning', 'info'].includes(icon) ? icon : 'info'; const iconMap = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }; const duration = Math.max(1200, type === 'error' ? Math.max(timer, 5000) : timer); const dedupeKey = `${type}|${title}|${text}`; let stack = uiGet('whut-toast-stack'); if (!stack) { stack = document.createElement('div'); stack.id = 'whut-toast-stack'; stack.setAttribute('aria-live', 'polite'); stack.setAttribute('aria-atomic', 'false'); UIKitRuntime.mount('toast', stack); } const duplicate = Array.from(stack.children).find(node => node.dataset.dedupeKey === dedupeKey); if (duplicate) { duplicate.classList.remove('is-leaving'); duplicate._uikitRefresh?.(); duplicate.animate?.([{ transform: 'scale(.98)' }, { transform: 'scale(1)' }], { duration: 160, easing: 'ease-out' }); return { element: duplicate, close: () => duplicate.querySelector('.whut-helper-toast-close')?.click(), update: () => {} }; } while (stack.children.length >= 3) stack.firstElementChild?.remove(); const toast = document.createElement('div'); toast.className = `whut-helper-toast ${type}`; toast.setAttribute('role', type === 'error' ? 'alert' : 'status'); toast.dataset.dedupeKey = dedupeKey; toast.style.setProperty('--whut-toast-duration', `${duration}ms`); const iconNode = document.createElement('span'); iconNode.className = 'whut-helper-toast-icon'; iconNode.setAttribute('aria-hidden', 'true'); iconNode.textContent = iconMap[type]; const body = document.createElement('span'); body.className = 'whut-helper-toast-body'; const titleNode = document.createElement('span'); titleNode.className = 'whut-helper-toast-title'; titleNode.textContent = title || text || ''; body.appendChild(titleNode); let textNode = null; if (title && text) { textNode = document.createElement('span'); textNode.className = 'whut-helper-toast-text'; textNode.textContent = text; body.appendChild(textNode); } const closeButton = document.createElement('button'); closeButton.type = 'button'; closeButton.className = 'whut-helper-toast-close'; closeButton.setAttribute('aria-label', '关闭通知'); closeButton.textContent = '×'; const progress = document.createElement('span'); progress.className = 'whut-helper-toast-progress'; progress.setAttribute('aria-hidden', 'true'); toast.append(iconNode, body, closeButton, progress); let timerId = 0; const dismiss = () => { if (!toast.isConnected || toast.classList.contains('is-leaving')) return; clearTimeout(timerId); toast.classList.add('is-leaving'); setTimeout(() => { toast.remove(); if (!stack.children.length) stack.remove(); }, 190); }; closeButton.addEventListener('click', dismiss); stack.appendChild(toast); const scheduleDismiss = () => { clearTimeout(timerId); timerId = setTimeout(dismiss, duration); }; toast._uikitRefresh = scheduleDismiss; scheduleDismiss(); return { element: toast, close: dismiss, update(message, nextType = type) { titleNode.textContent = String(message || ''); toast.className = `whut-helper-toast ${['success', 'error', 'warning', 'info'].includes(nextType) ? nextType : type}`; } }; } const Toast = { fire: (opts = {}) => showHelperToast(opts) }; const UIKit = Object.freeze({ ensure: () => UIKitRuntime.ensure(APP_STYLE), mount: (layer, node) => UIKitRuntime.mount(layer, node), query: selector => UIKitRuntime.query(selector), toast: options => showHelperToast(options), dialog: AppDialog, setTheme: mode => UIKitRuntime.setTheme(mode), cycleTheme: () => UIKitRuntime.cycleTheme(), getTheme: () => UIKitRuntime.getTheme(), destroy: () => UIKitRuntime.destroy() }); const EVAL_DEFAULT_CONFIG = { comment: "老师讲课重点突出,条理清晰,内容丰富,受益匪浅。", delay: 1500, randomScore: false }; const SCRIPT_UPDATE = { infoURL: 'https://home.haoli.site/assets/scripts/whut-jw-helper/whut-jw-helper.latest.json', downloadURL: 'https://home.haoli.site/assets/scripts/whut-jw-helper/whut-jw-helper.user.js', projectURL: 'https://scriptcat.org/zh-CN/script-show-page/6588' }; const SCRIPT_VERSION = '3.8.4'; const readStorage = (key, fallback = null) => { try { const value = localStorage.getItem(key); return value === null ? fallback : value; } catch (e) { return fallback; } }; let state = { userId: null, userName: null, dataMap: new Map(), rawData: [], filteredData: [], filters: { search: '', semesters: new Set(), semesterAll: true, type: 'all', sort: 'semester_desc' }, semesters: new Set(), types: new Set(), isFetching: false, isPrintLoaded: false, isPrintLoading: false, stats: { totalCredit: 0, avgGPA: 0, compulsoryGPA: 0, count: 0 }, monitor: { isLoaded: false, isRefreshing: false, isLoading: false, renderAfterLoad: false, overview: { required: 0, earned: 0, passed: true, calculatedTime: "" }, categories: [], warnings: [], raw: null, scoreFallbacks: [], openGroups: new Set(), openGroupsTouched: false }, transcript: { isLoading: false, isLoaded: false, promise: null, report: null, records: [], rows: [], error: '', lastAttempt: 0 }, update: { isLoaded: false, isChecking: false, info: null, error: '', hasNew: false }, eval: { isLoaded: false, isLoading: false, isLoadingHistory: false, isRunning: false, loadPromise: null, historyPromise: null, lastError: '', courses: [], pendingCourses: [], completedCourses: [], completedSemesters: [], completedLoaded: false, types: [], termCode: '', detailCache: new Map(), config: { comment: readStorage('whut_pj_comment', EVAL_DEFAULT_CONFIG.comment) || EVAL_DEFAULT_CONFIG.comment, delay: parseInt(readStorage('whut_pj_delay', ''), 10) || EVAL_DEFAULT_CONFIG.delay, randomScore: readStorage('whut_pj_random', String(EVAL_DEFAULT_CONFIG.randomScore)) !== 'false' } } }; const debounce = (func, wait) => { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }; const escapeHTML = (val) => String(val ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); const safeStorage = Object.freeze({ get(key, fallback = null) { return readStorage(key, fallback); }, set(key, value) { try { localStorage.setItem(key, String(value)); return true; } catch (e) { return false; } }, remove(key) { try { localStorage.removeItem(key); return true; } catch (e) { return false; } }, getJSON(key, fallback = {}) { try { const value = JSON.parse(readStorage(key, 'null')); return value && typeof value === 'object' && !Array.isArray(value) ? value : fallback; } catch (e) { return fallback; } }, setJSON(key, value) { return this.set(key, JSON.stringify(value)); } }); const UI = Object.freeze({ state(type, title, detail = '', compact = false) { const normalized = ['loading', 'empty', 'error', 'warning', 'success'].includes(type) ? type : 'empty'; const icons = { empty: '◇', error: '!', warning: '!', success: '✓' }; const icon = normalized === 'loading' ? '' : escapeHTML(icons[normalized] || '◇'); const role = normalized === 'error' ? 'alert' : 'status'; return `