// ==UserScript==
// @name WTR-Lab Reader & UI Enhancer
// @namespace http://tampermonkey.net/
// @version 3.5
// @description Adds a responsive configuration panel to adjust reader width, navigation width, and optionally override font style on wtr-lab.com.
// @author MasuRii
// @license MIT
// @match https://wtr-lab.com/en/novel/*/*/chapter-*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @connect gwfh.mranftl.com
// @connect fonts.googleapis.com
// @icon https://www.google.com/s2/favicons?sz=64&domain=wtr-lab.com
// ==/UserScript==
;(function () {
'use strict'
// --- CONFIGURATION ---
const DEBUG_KEY = 'wtr_lab_enhancer_debug'
const STEP_WIDTH = 50
const MIN_WIDTH = 300
const FONTS_API_URL = 'https://gwfh.mranftl.com/api/fonts'
const RECOMMENDED_FONTS = {
serif: ['Merriweather', 'Lora', 'Crimson Text', 'Libre Baskerville', 'Spectral', 'EB Garamond', 'Noto Serif'],
sansSerif: ['Roboto', 'Open Sans', 'Source Sans Pro']
}
const configs = {
reader: {key: 'wtr_lab_reader_width', selector: '.fix-size.card', defaultWidth: 760, label: 'Reader Content Width'},
nav: {
key: 'wtr_lab_nav_width',
selector: 'nav.bottom-reader-nav .fix-size',
defaultWidth: 760,
label: 'Bottom Navigator Width'
},
navConstraint: {
key: 'wtr_lab_nav_constraint',
selector: 'nav.bottom-reader-nav',
defaultState: false,
label: 'Constrain Navigator Background'
},
fontToggle: {key: 'wtr_lab_font_style_enabled', defaultState: false, label: 'Enable Custom Font Style'},
font: {key: 'wtr_lab_font_family', selector: '.chapter-body', defaultFont: 'Merriweather', label: 'Font Style'},
blockAddTerm: {
key: 'wtr_lab_block_add_term',
selector: '.floating-add-term-btn',
defaultState: false,
label: 'Block "Add Term" Button'
},
hideBookBtn: {
key: 'wtr_lab_hide_book_btn',
selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#book"])',
defaultState: false,
label: 'Book',
iconHTML: '<svg><use href="/icons/sprite.svg#book"></use></svg>'
},
hideTextFieldsBtn: {
key: 'wtr_lab_hide_text_fields_btn',
selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#text_fields"])',
defaultState: false,
label: 'Text',
iconHTML: '<svg><use href="/icons/sprite.svg#text_fields"></use></svg>'
},
hideTtsBtn: {
key: 'wtr_lab_hide_tts_btn',
selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#tts"])',
defaultState: false,
label: 'TTS',
iconHTML: '<svg><use href="/icons/sprite.svg#tts"></use></svg>'
},
hideCogBtn: {
key: 'wtr_lab_hide_cog_btn',
selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#cog-outline"])',
defaultState: false,
label: 'Settings',
iconHTML: '<svg><use href="/icons/sprite.svg#cog-outline"></use></svg>'
},
hideListBtn: {
key: 'wtr_lab_hide_list_btn',
selector: 'div.btn-group button:has(svg use[href="/icons/sprite.svg#list"])',
defaultState: false,
label: 'List',
iconHTML: '<svg><use href="/icons/sprite.svg#list"></use></svg>'
},
debug: {
key: DEBUG_KEY,
defaultState: false,
label: 'Enable Debug Logging'
}
}
// --- DEBUG LOGGER ---
let isDebugEnabled = GM_getValue(DEBUG_KEY, false)
const syncDebugState = () => {
isDebugEnabled = GM_getValue(DEBUG_KEY, false)
}
const log = (...args) => {
if (isDebugEnabled) console.log('[WTR-Lab Enhancer]', ...args)
}
const toggleDebugLogging = () => {
isDebugEnabled = !isDebugEnabled
GM_setValue(DEBUG_KEY, isDebugEnabled)
alert(`Debug logging is now ${isDebugEnabled ? 'ENABLED' : 'DISABLED'}.`)
}
// --- CORE LOGIC ---
const saveValue = (key, value) => GM_setValue(key, value)
const loadValue = (key, defaultValue) => GM_getValue(key, defaultValue)
const applyWidthStyle = (configName, width) => {
const styleId = `custom-width-styler-${configName}`
let styleElement = document.getElementById(styleId)
if (!styleElement) {
styleElement = document.createElement('style')
styleElement.id = styleId
document.head.appendChild(styleElement)
}
styleElement.textContent = `${configs[configName].selector} { max-width: ${width}px !important; }`
}
const updateNavConstraint = () => {
const isConstrained = loadValue(configs.navConstraint.key, configs.navConstraint.defaultState)
const styleId = 'custom-nav-constraint-styler'
let styleElement = document.getElementById(styleId)
if (isConstrained) {
if (!styleElement) {
styleElement = document.createElement('style')
styleElement.id = styleId
document.head.appendChild(styleElement)
}
const navContentWidth = loadValue(configs.nav.key, configs.nav.defaultWidth)
const marginValue = Math.max(0, (window.innerWidth - navContentWidth) / 2)
styleElement.textContent = `${configs.navConstraint.selector} { margin-left: ${marginValue}px !important; margin-right: ${marginValue}px !important; }`
} else if (styleElement) {
styleElement.remove()
}
}
const updateBlockAddTerm = () => {
const isBlocked = loadValue(configs.blockAddTerm.key, configs.blockAddTerm.defaultState)
const styleId = 'custom-block-add-term-styler'
let styleElement = document.getElementById(styleId)
if (isBlocked) {
if (!styleElement) {
styleElement = document.createElement('style')
styleElement.id = styleId
document.head.appendChild(styleElement)
}
styleElement.textContent = `${configs.blockAddTerm.selector} { display: none !important; }`
} else if (styleElement) {
styleElement.remove()
}
}
const updateButtonVisibilityStyles = () => {
const styleId = 'custom-button-visibility-styler'
let styleElement = document.getElementById(styleId)
const buttonConfigs = [
configs.hideBookBtn,
configs.hideTextFieldsBtn,
configs.hideTtsBtn,
configs.hideCogBtn,
configs.hideListBtn
]
const selectorsToHide = buttonConfigs
.filter(config => loadValue(config.key, config.defaultState))
.map(config => config.selector)
if (!styleElement && selectorsToHide.length > 0) {
styleElement = document.createElement('style')
styleElement.id = styleId
document.head.appendChild(styleElement)
}
if (styleElement) {
styleElement.textContent =
selectorsToHide.length > 0 ? `${selectorsToHide.join(', ')} { display: none !important; }` : ''
}
}
const fetchFonts = () =>
new Promise(resolve =>
GM_xmlhttpRequest({
method: 'GET',
url: FONTS_API_URL,
onload: r => {
try {
const d = JSON.parse(r.responseText)
const rec = [...RECOMMENDED_FONTS.serif, ...RECOMMENDED_FONTS.sansSerif]
resolve({
recommendedSerif: RECOMMENDED_FONTS.serif,
recommendedSansSerif: RECOMMENDED_FONTS.sansSerif,
other: d
.map(f => f.family)
.filter(f => !rec.includes(f))
.sort()
})
} catch (e) {
resolve(getFallbackFonts())
}
},
onerror: () => resolve(getFallbackFonts())
})
)
const getFallbackFonts = () => ({
recommendedSerif: RECOMMENDED_FONTS.serif,
recommendedSansSerif: RECOMMENDED_FONTS.sansSerif,
other: ['Georgia', 'Times New Roman', 'Arial', 'Verdana']
})
const removeFontStyle = () => {
log('Removing custom font style elements.')
const styleElement = document.getElementById('custom-font-styler')
if (styleElement) styleElement.remove()
const linkElement = document.getElementById('userscript-font-link')
if (linkElement) linkElement.remove()
}
const applyFontStyle = fontFamily => {
const isEnabled = loadValue(configs.fontToggle.key, configs.fontToggle.defaultState)
if (!isEnabled) {
removeFontStyle()
return
}
log(`Applying font: ${fontFamily}`)
const primaryFont = fontFamily.split(',')[0].trim()
const styleId = 'custom-font-styler'
let styleElement = document.getElementById(styleId)
if (!styleElement) {
styleElement = document.createElement('style')
styleElement.id = styleId
document.head.appendChild(styleElement)
}
const fontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(primaryFont)}&display=swap`
let linkElement = document.getElementById('userscript-font-link')
if (!linkElement) {
linkElement = document.createElement('link')
linkElement.id = 'userscript-font-link'
linkElement.rel = 'stylesheet'
document.head.appendChild(linkElement)
}
linkElement.href = fontUrl
styleElement.textContent = `${configs.font.selector} { font-family: "${primaryFont}", serif, sans-serif !important; }`
}
// --- UI CONFIGURATION PANEL ---
const createConfigPanel = () => {
const panelHTML = `<div id="wtr-config-container" class="wtr-panel-container"><div id="wtr-config-overlay" style="display: none;"><div id="wtr-config-panel"><h2>WTR-Lab Enhancer Settings</h2><div id="wtr-config-sections"></div><button id="wtr-config-close-btn" class="wtr-config-button">Close</button></div></div></div>`
document.body.insertAdjacentHTML('beforeend', panelHTML)
GM_addStyle(getPanelCSS())
const sectionsContainer = document.getElementById('wtr-config-sections')
// Section 1: Layout & Sizing
const layoutSection = document.createElement('div')
layoutSection.className = 'wtr-config-section'
layoutSection.innerHTML = `<label class="wtr-section-title">Layout & Sizing</label>
<div class="wtr-control-group">
<label for="wtr-reader-width-input">${configs.reader.label} (px)</label>
<div class="wtr-config-controls"><button id="wtr-reader-decrease-btn" class="wtr-config-button control">-</button><input type="number" id="wtr-reader-width-input" min="${MIN_WIDTH}" step="10"><button id="wtr-reader-increase-btn" class="wtr-config-button control">+</button><button id="wtr-reader-reset-btn" class="wtr-config-button reset">Reset</button></div>
</div>
<div class="wtr-control-group">
<label for="wtr-nav-width-input">${configs.nav.label} (px)</label>
<div class="wtr-config-controls"><button id="wtr-nav-decrease-btn" class="wtr-config-button control">-</button><input type="number" id="wtr-nav-width-input" min="${MIN_WIDTH}" step="10"><button id="wtr-nav-increase-btn" class="wtr-config-button control">+</button><button id="wtr-nav-reset-btn" class="wtr-config-button reset">Reset</button></div>
</div>`
sectionsContainer.appendChild(layoutSection)
// Section 2: Font Customization
const fontSection = document.createElement('div')
fontSection.className = 'wtr-config-section'
fontSection.innerHTML = `<label class="wtr-section-title">Font Customization</label>
<div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-fontToggle-toggle"><label for="wtr-fontToggle-toggle">${configs.fontToggle.label}</label></div>
<div class="wtr-control-group">
<label for="wtr-font-select">${configs.font.label}</label>
<div class="wtr-config-controls font-controls"><select id="wtr-font-select"></select></div>
<div class="wtr-config-controls font-controls"><div class="wtr-button-group"><button id="wtr-font-refresh-btn" class="wtr-config-button">Refresh</button><button id="wtr-font-reset-btn" class="wtr-config-button reset">Reset</button></div></div>
</div>`
sectionsContainer.appendChild(fontSection)
// Section 3: Element Visibility
const visibilitySection = document.createElement('div')
visibilitySection.className = 'wtr-config-section'
visibilitySection.innerHTML = `<label class="wtr-section-title">Element Visibility</label>
<div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-navConstraint-toggle"><label for="wtr-navConstraint-toggle">${configs.navConstraint.label}</label></div>
<div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-blockAddTerm-toggle"><label for="wtr-blockAddTerm-toggle">${configs.blockAddTerm.label}</label></div>
<div class="wtr-control-group"><label class="wtr-subsection-title">Hide Toolbar Buttons</label><div class="wtr-button-hide-controls"></div></div>`
const buttonControlsContainer = visibilitySection.querySelector('.wtr-button-hide-controls')
Object.entries(configs)
.filter(([n]) => n.startsWith('hide'))
.forEach(([name, config]) => {
buttonControlsContainer.insertAdjacentHTML(
'beforeend',
`<div class="wtr-config-controls checkbox-control icon-checkbox"><input type="checkbox" id="wtr-${name}-toggle"><label for="wtr-${name}-toggle">${config.iconHTML}<span>${config.label}</span></label></div>`
)
})
sectionsContainer.appendChild(visibilitySection)
// Section 4: Debug & Advanced
const debugSection = document.createElement('div')
debugSection.className = 'wtr-config-section'
debugSection.innerHTML = `<label class="wtr-section-title">Debug & Advanced</label>
<div class="wtr-config-controls checkbox-control"><input type="checkbox" id="wtr-debug-toggle"><label for="wtr-debug-toggle">Enable Debug Logging</label></div>`
sectionsContainer.appendChild(debugSection)
populateFontDropdown(getFallbackFonts())
attachPanelEventListeners()
}
const populateFontDropdown = async (initialFontGroups = null) => {
const fontSelect = document.getElementById('wtr-font-select')
if (!fontSelect) return
const currentFont = loadValue(configs.font.key, configs.font.defaultFont)
fontSelect.innerHTML = ''
const fontGroups = initialFontGroups || (await fetchFonts())
const groupLabels = {
recommendedSerif: 'Recommended (Serif)',
recommendedSansSerif: 'Recommended (Sans-serif)',
other: 'All Other Fonts'
}
for (const groupKey in fontGroups) {
if (fontGroups[groupKey].length === 0) continue
const optgroup = document.createElement('optgroup')
optgroup.label = groupLabels[groupKey] || 'Fonts'
fontGroups[groupKey].forEach(font => {
const option = document.createElement('option')
option.value = font
option.textContent = font
optgroup.appendChild(option)
})
fontSelect.appendChild(optgroup)
}
fontSelect.value = Object.values(fontGroups).flat().includes(currentFont) ? currentFont : configs.font.defaultFont
}
const attachPanelEventListeners = () => {
document.getElementById('wtr-config-overlay').addEventListener('click', e => {
if (e.target.id === 'wtr-config-overlay') hideConfigPanel()
})
document.getElementById('wtr-config-close-btn').addEventListener('click', hideConfigPanel)
const updateSetting = (configName, value) => {
const config = configs[configName]
saveValue(config.key, value)
if (configName === 'font') {
applyFontStyle(value)
document.getElementById('wtr-font-select').value = value
} else if (configName === 'fontToggle') {
updateFontControlsState(value)
applyFontStyle(loadValue(configs.font.key, configs.font.defaultFont))
} else if (configName === 'debug') {
isDebugEnabled = value
GM_setValue(DEBUG_KEY, isDebugEnabled)
log(`Debug logging ${isDebugEnabled ? 'ENABLED' : 'DISABLED'}`)
} else if (configName === 'navConstraint') {
updateNavConstraint()
} else if (configName === 'blockAddTerm') {
updateBlockAddTerm()
} else if (configName.startsWith('hide')) {
updateButtonVisibilityStyles()
} else {
const validatedWidth = Math.max(MIN_WIDTH, parseInt(value, 10))
if (isNaN(validatedWidth)) return
applyWidthStyle(configName, validatedWidth)
document.getElementById(`wtr-${configName}-width-input`).value = validatedWidth
saveValue(config.key, validatedWidth)
if (configName === 'nav') updateNavConstraint()
}
}
for (const [name, config] of Object.entries(configs)) {
if (name === 'font') {
const select = document.getElementById('wtr-font-select')
select.addEventListener('change', () => updateSetting(name, select.value))
document
.getElementById('wtr-font-reset-btn')
.addEventListener('click', () => updateSetting(name, config.defaultFont))
const refreshBtn = document.getElementById('wtr-font-refresh-btn')
refreshBtn.addEventListener('click', async () => {
refreshBtn.textContent = 'Fetching...'
refreshBtn.disabled = true
await populateFontDropdown()
refreshBtn.textContent = 'Refresh'
refreshBtn.disabled = false
})
} else if (['fontToggle', 'navConstraint', 'blockAddTerm', 'debug'].includes(name) || name.startsWith('hide')) {
const toggle = document.getElementById(`wtr-${name}-toggle`)
if (toggle) toggle.addEventListener('change', () => updateSetting(name, toggle.checked))
} else if (['reader', 'nav'].includes(name)) {
const input = document.getElementById(`wtr-${name}-width-input`)
document
.getElementById(`wtr-${name}-increase-btn`)
.addEventListener('click', () => updateSetting(name, parseInt(input.value, 10) + STEP_WIDTH))
document
.getElementById(`wtr-${name}-decrease-btn`)
.addEventListener('click', () => updateSetting(name, parseInt(input.value, 10) - STEP_WIDTH))
document
.getElementById(`wtr-${name}-reset-btn`)
.addEventListener('click', () => updateSetting(name, config.defaultWidth))
input.addEventListener('change', () => updateSetting(name, input.value))
}
}
}
const updateFontControlsState = isEnabled => {
;['wtr-font-select', 'wtr-font-refresh-btn', 'wtr-font-reset-btn'].forEach(id => {
document.getElementById(id).disabled = !isEnabled
})
}
const showConfigPanel = () => {
for (const [name, config] of Object.entries(configs)) {
if (name === 'font') {
document.getElementById('wtr-font-select').value = loadValue(config.key, config.defaultFont)
} else if (name === 'fontToggle') {
const isEnabled = loadValue(config.key, config.defaultState)
document.getElementById('wtr-fontToggle-toggle').checked = isEnabled
updateFontControlsState(isEnabled)
} else if (['navConstraint', 'blockAddTerm', 'debug'].includes(name) || name.startsWith('hide')) {
const toggle = document.getElementById(`wtr-${name}-toggle`)
if (toggle) toggle.checked = loadValue(config.key, config.defaultState)
} else if (['reader', 'nav'].includes(name)) {
document.getElementById(`wtr-${name}-width-input`).value = loadValue(config.key, config.defaultWidth)
}
}
document.getElementById('wtr-config-overlay').style.display = 'flex'
}
const hideConfigPanel = () => (document.getElementById('wtr-config-overlay').style.display = 'none')
// --- MODERN CSS FEATURE DETECTION & PROGRESSIVE ENHANCEMENT ---
const detectCSSFeatures = () => {
const features = {
containerQueries: CSS.supports('container-type: inline-size'),
grid: CSS.supports('display: grid'),
backdropFilter: CSS.supports('backdrop-filter: blur(1px)'),
customProperties: CSS.supports('--custom: 0'),
motionPreferences: window.matchMedia('(prefers-reduced-motion: reduce)'),
touchDevice: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
if (isDebugEnabled) {
console.log('[WTR-Lab Enhancer] CSS Feature Detection:', features)
}
return features
}
const initializeModernFeatures = () => {
const features = detectCSSFeatures()
// Add feature flags to the document for CSS fallbacks
document.documentElement.setAttribute('data-container-queries', features.containerQueries)
document.documentElement.setAttribute('data-grid', features.grid)
document.documentElement.setAttribute('data-backdrop-filter', features.backdropFilter)
document.documentElement.setAttribute('data-touch-device', features.touchDevice)
// Initialize modern features if supported
if (features.containerQueries) {
log('Container queries supported - enabling responsive container design')
initializeContainerQueryResponsive()
}
if (features.grid) {
log('CSS Grid supported - enabling modern grid layouts')
initializeGridLayouts()
}
if (features.backdropFilter) {
log('Backdrop filter supported - enabling blur effects')
initializeBackdropEffects()
}
if (features.touchDevice) {
log('Touch device detected - optimizing for touch interactions')
initializeTouchOptimizations()
}
// Set up motion preference listeners
features.motionPreferences.addEventListener('change', e => {
const isReducedMotion = e.matches
document.documentElement.setAttribute('data-reduced-motion', isReducedMotion)
if (isDebugEnabled) {
console.log('[WTR-Lab Enhancer] Motion preference changed:', isReducedMotion ? 'Reduce motion' : 'Allow motion')
}
})
// Set initial motion preference
document.documentElement.setAttribute('data-reduced-motion', features.reducedMotion)
}
const initializeContainerQueryResponsive = () => {
const container = document.getElementById('wtr-config-container')
if (container) {
container.classList.add('wtr-container-responsive')
}
}
const initializeGridLayouts = () => {
const controls = document.querySelectorAll('.wtr-config-controls')
controls.forEach(control => {
control.classList.add('wtr-grid-enabled')
})
}
const initializeBackdropEffects = () => {
const overlay = document.getElementById('wtr-config-overlay')
if (overlay) {
overlay.classList.add('wtr-backdrop-filter')
}
}
const initializeTouchOptimizations = () => {
const buttons = document.querySelectorAll('.wtr-config-button')
buttons.forEach(button => {
button.classList.add('wtr-touch-optimized')
})
const inputs = document.querySelectorAll('input[type="number"], select')
inputs.forEach(input => {
input.classList.add('wtr-touch-optimized')
})
}
const getPanelCSS = () => `
/* Modern CSS Design System with Fallbacks */
:root {
/* Container Queries Support Detection */
--supports-container-queries: false;
/* Design Tokens - Colors */
--panel-bg-primary: var(--bs-component-bg, #ffffff);
--panel-bg-secondary: var(--bs-tertiary-bg, #f8f9fa);
--panel-bg-elevated: var(--bs-body-bg, #ffffff);
--panel-text-primary: var(--bs-body-color, #212529);
--panel-text-secondary: var(--bs-secondary-color, #6c757d);
--panel-border: var(--bs-border-color, #dee2e6);
--panel-accent: var(--bs-primary, #0d6efd);
--panel-accent-hover: #0b5ed7;
--panel-success: #198754;
--panel-danger: #dc3545;
--panel-warning: #fd7e14;
/* Design Tokens - Spacing */
--panel-spacing-xs: 0.25rem;
--panel-spacing-sm: 0.5rem;
--panel-spacing-md: 1rem;
--panel-spacing-lg: 1.5rem;
--panel-spacing-xl: 2rem;
/* Design Tokens - Typography */
--panel-font-family: var(--bs-font-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
--panel-font-size-xs: 0.75rem;
--panel-font-size-sm: 0.875rem;
--panel-font-size-md: 1rem;
--panel-font-size-lg: 1.125rem;
--panel-font-size-xl: 1.25rem;
--panel-font-weight-normal: 400;
--panel-font-weight-medium: 500;
--panel-font-weight-bold: 600;
--panel-line-height-tight: 1.2;
--panel-line-height-normal: 1.5;
--panel-line-height-relaxed: 1.75;
/* Design Tokens - Border Radius */
--panel-radius-sm: var(--bs-border-radius-sm, 0.25rem);
--panel-radius-md: var(--bs-border-radius, 0.375rem);
--panel-radius-lg: var(--bs-border-radius-lg, 0.5rem);
--panel-radius-xl: 0.75rem;
--panel-radius-full: 9999px;
/* Design Tokens - Shadows */
--panel-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--panel-shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1);
--panel-shadow-lg: var(--bs-box-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
--panel-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Design Tokens - Transitions */
--panel-transition-fast: 0.15s ease;
--panel-transition-normal: 0.3s ease;
--panel-transition-slow: 0.5s ease;
--panel-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
/* Design Tokens - Z-Index */
--panel-z-overlay: 9999;
--panel-z-panel: 10000;
--panel-z-tooltip: 10001;
}
/* Container Queries Support Detection */
@supports (container-type: inline-size) {
:root {
--supports-container-queries: true;
}
}
/* Panel Container for Container Queries */
#wtr-config-container {
container-name: wtr-panel;
container-type: inline-size;
container-index: 0;
}
/* Enhanced panel styling with design tokens */
#wtr-config-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: var(--panel-z-overlay);
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
contain: layout style paint;
}
#wtr-config-panel {
background: var(--panel-bg-primary);
color: var(--panel-text-primary);
padding: var(--panel-spacing-xl);
border-radius: var(--panel-radius-lg);
width: 90%;
max-width: 550px;
box-shadow: var(--panel-shadow-xl);
font-family: var(--panel-font-family);
display: flex;
flex-direction: column;
gap: var(--panel-spacing-lg);
max-height: 90vh;
border: 1px solid var(--panel-border);
/* Performance optimizations */
contain: layout style paint;
will-change: transform, opacity;
transform: translateZ(0);
}
/* Enhanced Typography with Design Tokens */
#wtr-config-panel h2 {
margin: 0;
text-align: center;
font-weight: var(--panel-font-weight-medium);
font-size: var(--panel-font-size-xl);
line-height: var(--panel-line-height-tight);
color: var(--panel-text-primary);
flex-shrink: 0;
}
/* Enhanced Sections with Containment */
#wtr-config-panel #wtr-config-sections {
overflow-y: auto;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: var(--panel-spacing-lg);
padding-right: var(--panel-spacing-md);
margin-right: calc(-1 * var(--panel-spacing-md));
/* Performance optimization */
contain: layout style;
}
#wtr-config-panel .wtr-config-section {
display: flex;
flex-direction: column;
gap: var(--panel-spacing-lg);
padding: var(--panel-spacing-lg);
border: 1px solid var(--panel-border);
border-radius: var(--panel-radius-lg);
background: var(--panel-bg-secondary);
/* Performance optimization */
contain: layout style paint;
}
#wtr-config-panel .wtr-control-group {
display: flex;
flex-direction: column;
gap: var(--panel-spacing-sm);
}
/* Modern Control Layout with Grid Fallback */
#wtr-config-panel .wtr-config-controls {
display: flex;
gap: var(--panel-spacing-sm);
align-items: center;
flex-wrap: wrap;
}
#wtr-config-panel .wtr-config-controls.font-controls {
flex-wrap: nowrap;
display: grid;
grid-template-columns: 1fr;
gap: var(--panel-spacing-sm);
}
#wtr-config-panel .wtr-config-controls.checkbox-control {
justify-content: flex-start;
cursor: pointer;
padding: var(--panel-spacing-sm) 0;
display: grid;
grid-template-columns: auto 1fr;
gap: var(--panel-spacing-sm);
cursor: pointer;
}
#wtr-config-panel .wtr-config-controls.checkbox-control label {
user-select: none;
}
#wtr-config-panel .wtr-config-controls.checkbox-control input {
margin-right: var(--panel-spacing-sm);
}
/* Enhanced Form Controls with Touch Optimization */
#wtr-config-panel input[type="number"],
#wtr-config-panel select {
flex-grow: 1;
min-width: 100px;
min-height: 44px; /* Touch target size */
text-align: center;
background: var(--panel-bg-secondary);
color: var(--panel-text-primary);
border: 1px solid var(--panel-border);
border-radius: var(--panel-radius-md);
padding: var(--panel-spacing-sm) var(--panel-spacing-md);
font-family: var(--panel-font-family);
font-size: var(--panel-font-size-sm);
/* Performance */
contain: layout style;
}
#wtr-config-panel select:disabled {
background: var(--panel-bg-secondary);
color: var(--panel-text-secondary);
cursor: not-allowed;
}
/* Modern Button Layout */
#wtr-config-panel .wtr-button-group {
display: grid;
grid-auto-flow: column;
gap: var(--panel-spacing-sm);
justify-content: end;
flex-shrink: 0;
}
/* Enhanced Button Styling with Motion Controls */
#wtr-config-panel .wtr-config-button {
min-height: 44px; /* iOS guideline */
min-width: 44px;
padding: var(--panel-spacing-sm) var(--panel-spacing-md);
border: none;
border-radius: var(--panel-radius-md);
cursor: pointer;
background-color: var(--panel-accent);
color: white;
font-weight: var(--panel-font-weight-bold);
font-size: var(--panel-font-size-sm);
flex-shrink: 0;
transition: background-color var(--panel-transition-fast),
transform var(--panel-transition-fast);
/* Touch optimization */
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
/* Performance */
contain: layout style paint;
will-change: transform;
transform: translateZ(0);
}
#wtr-config-panel .wtr-config-button:hover,
#wtr-config-panel .wtr-config-button:focus-visible {
background-color: var(--panel-accent-hover);
transform: translateY(-1px);
}
#wtr-config-panel .wtr-config-button:active {
transform: translateY(0);
}
#wtr-config-panel .wtr-config-button:disabled {
background-color: var(--panel-text-secondary);
cursor: not-allowed;
transform: none;
}
#wtr-config-panel .wtr-config-button.control {
width: 44px;
aspect-ratio: 1;
}
#wtr-config-panel .wtr-config-button.reset {
background-color: var(--panel-danger);
}
#wtr-config-panel #wtr-config-close-btn {
background-color: var(--panel-text-secondary);
align-self: center;
width: 100px;
flex-shrink: 0;
}
/* Enhanced Typography */
#wtr-config-panel .wtr-section-title {
font-weight: var(--panel-font-weight-medium);
text-align: center;
margin-bottom: var(--panel-spacing-sm);
display: block;
font-size: var(--panel-font-size-lg);
color: var(--panel-text-primary);
}
#wtr-config-panel .wtr-subsection-title {
font-weight: var(--panel-font-weight-medium);
text-align: left;
margin-top: var(--panel-spacing-lg);
display: block;
border-top: 1px solid var(--panel-border);
padding-top: var(--panel-spacing-lg);
color: var(--panel-text-primary);
}
/* Enhanced Button Hide Controls Layout */
#wtr-config-panel .wtr-button-hide-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--panel-spacing-md);
justify-content: flex-start;
}
#wtr-config-panel .icon-checkbox label {
display: flex;
align-items: center;
gap: var(--panel-spacing-sm);
}
#wtr-config-panel .icon-checkbox svg {
width: 20px;
height: 20px;
stroke: currentColor;
fill: none;
}
#wtr-config-panel .icon-checkbox svg:has(use[href*="text_fields"], use[href*="tts"], use[href*="list"]) {
fill: currentColor;
stroke: none;
}
/* Motion Preference Handling - WCAG 2.2/2.3 Compliance */
@media (prefers-reduced-motion: reduce) {
#wtr-config-overlay,
#wtr-config-panel,
.wtr-config-button {
transition: none !important;
animation: none !important;
transform: none !important;
}
}
/* Enable motion for users who haven't disabled it */
@supports not (prefers-reduced-motion: reduce) {
#wtr-config-panel {
transition: transform var(--panel-transition-normal) var(--panel-timing-function),
opacity var(--panel-transition-normal) ease;
transform: scale(0.95);
}
#wtr-config-panel.visible {
transform: scale(1);
}
}
/* Container Query Responsive Design */
@container wtr-panel (max-width: 480px) {
#wtr-config-panel {
width: 95%;
padding: var(--panel-spacing-lg);
}
.wtr-config-controls {
flex-direction: column;
gap: var(--panel-spacing-xs);
}
.wtr-config-controls.font-controls {
flex-direction: column;
align-items: stretch;
}
.wtr-button-group {
margin-left: 0;
justify-content: center;
grid-auto-flow: row;
}
.wtr-button-hide-controls {
grid-template-columns: 1fr;
gap: var(--panel-spacing-sm);
}
}
@container wtr-panel (max-width: 360px) {
#wtr-config-panel {
width: 98%;
margin: var(--panel-spacing-md);
padding: var(--panel-spacing-md);
}
.wtr-config-section {
padding: var(--panel-spacing-md);
}
.wtr-config-button {
min-width: 40px;
}
}
/* Grid Layout Fallbacks */
@supports not (display: grid) {
.wtr-config-controls {
display: flex;
flex-wrap: wrap;
}
.wtr-button-group {
display: flex;
flex-wrap: wrap;
}
.wtr-button-hide-controls {
display: flex;
flex-wrap: wrap;
}
}
/* Container Queries Fallback */
@supports not (container-type: inline-size) {
#wtr-config-container {
/* Fallback - styles remain as above for viewport-based responsiveness */
}
/* Legacy media queries as fallback */
@media (max-width: 480px) {
#wtr-config-panel {
width: 95%;
padding: var(--panel-spacing-lg);
}
.wtr-config-controls {
flex-direction: column;
gap: var(--panel-spacing-xs);
}
}
}
/* Backdrop Filter Fallback */
@supports not (backdrop-filter: blur(1px)) {
#wtr-config-overlay {
background-color: rgba(0, 0, 0, 0.8);
}
}
/* Touch-specific enhancements */
@media (hover: none) and (pointer: coarse) {
.wtr-config-button {
min-height: 48px;
padding: var(--panel-spacing-md) var(--panel-spacing-lg);
font-size: var(--panel-font-size-md);
}
.wtr-config-controls input[type="number"],
.wtr-config-controls select {
min-height: 48px;
font-size: var(--panel-font-size-md);
}
}
`
// --- INITIALIZATION ---
const init = async () => {
log('Initializing script...')
syncDebugState() // Sync debug state from stored values
createConfigPanel()
applyWidthStyle('reader', loadValue(configs.reader.key, configs.reader.defaultWidth))
applyWidthStyle('nav', loadValue(configs.nav.key, configs.nav.defaultWidth))
updateNavConstraint()
updateBlockAddTerm()
updateButtonVisibilityStyles()
window.addEventListener('resize', updateNavConstraint)
const fontGroups = await fetchFonts()
const allAvailableFonts = Object.values(fontGroups).flat()
let initialFont = loadValue(configs.font.key, configs.font.defaultFont)
if (!allAvailableFonts.includes(initialFont)) {
initialFont = configs.font.defaultFont
saveValue(configs.font.key, initialFont)
}
applyFontStyle(initialFont)
populateFontDropdown(fontGroups)
GM_registerMenuCommand('Configure Settings', showConfigPanel)
// Initialize modern CSS features with progressive enhancement
initializeModernFeatures()
log('Initialization complete.')
}
init()
})()