This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408// Translate known Bluesky palette declarations without changing React-owned styles.(() => { 'use strict';
const model = globalThis.BskyPlusThemes; const api = typeof browser !== 'undefined' ? browser : chrome; const STYLE_ID = 'bskyplus-theme-style'; const PREVIEW_STYLE_ID = 'bskyplus-theme-preview-style'; const MARKER = 'data-bskyplus-theme-inline'; const ROOT = 'data-bskyplus-theme-active'; const PREVIEW_TRANSITION_MS = 1000; const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)'); const COLORS = /#[\da-f]{8}\b|#[\da-f]{6}\b|#[\da-f]{4}\b|#[\da-f]{3}\b|rgba?\([^)]*\)|\bwhite\b|\bblack\b/gi; const PROPERTIES = new Set([ 'color', 'background-color', 'border-color', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color', 'outline-color', 'text-decoration-color', 'fill', 'stroke', 'caret-color', 'box-shadow', 'text-shadow', 'background-image', ]); let theme = null; let library = model.normalizeLibrary(null); let previewTheme = null; let mode = 'light'; let style; let viewTransition; let observer; let timer; let scheduled = false; let rescanSheets = false; let sourceSignature = ''; let inlineRules = new Map(); const colorCache = new Map(); const contextCache = new Map(); const pending = new Set(); let initialized = false;
function nativeMode() { const classes = document.documentElement.classList; if (classes.contains('theme--dim')) return 'dim'; if (classes.contains('theme--dark')) return 'dark'; if (classes.contains('theme--light')) return 'light'; return matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }
function recolor(value, property) { const key = property + ':' + value; if (colorCache.has(key)) return colorCache.get(key); let changed = false; const output = value.replace(COLORS, color => { const mapped = model.mapColor(color, property, theme, mode); if (!mapped) return color; changed = true; const parsed = model.parseColor(color); if (['color', 'fill', 'stroke'].includes(property) && parsed?.alpha === 1) { if (parsed.hex === '#FFFFFF') return `var(--bskyplus-theme-white-text, ${mapped})`; if (parsed.hex === '#000000') return `var(--bskyplus-theme-black-text, ${mapped})`; } return mapped; }); const result = changed ? output : null; colorCache.set(key, result); return result; }
function backgroundContext(value) { if (contextCache.has(value)) return contextCache.get(value); const parsed = model.parseColor(value); if (!parsed) return ''; if (parsed.hex === '#000000' && parsed.alpha >= 0.4 && parsed.alpha < 1) { return `--bskyplus-theme-white-text:${theme.colors.onPrimary};--bskyplus-theme-black-text:${theme.colors.onPrimary};`; } if (parsed.alpha !== 1) return ''; const palette = model.palette(mode); const entry = Object.entries(palette).find(([, color]) => color === parsed.hex); const colored = entry && /^(primary|positive|negative)_(400|500|600|700)$/.test(entry[0]); const inverse = entry && /^contrast_(700|800|900|950|975|1000)$/.test(entry[0]); const legacy = ['#1083FE', '#0085FF', '#20BC07', '#EC4868'].includes(parsed.hex); const foreground = colored || legacy ? theme.colors.onPrimary : inverse ? theme.colors.textInverse : null; const result = foreground ? `--bskyplus-theme-white-text:${foreground};--bskyplus-theme-black-text:${foreground};` : ''; contextCache.set(value, result); return result; }
function declarations(source, stylesheet = false) { let result = ''; for (const property of source) { if (!PROPERTIES.has(property)) continue; const value = source.getPropertyValue(property); if (property === 'background-image') { const stops = [...value.matchAll(COLORS)].map(match => model.parseColor(match[0])?.hex); // Theme single-color UI fades; preserve multicolor artwork and image URLs. if (!value.includes('gradient(') || value.includes('url(') || new Set(stops).size !== 1) continue; } const replacement = recolor(value, property); if (replacement) { // Keep explicit inline borders, text, and backgrounds above ordinary // stylesheet rules while still mapping known inline palette colors. const priority = stylesheet && (property.includes('border') || property === 'color' || property === 'background-color') && source.getPropertyPriority(property) !== 'important' ? '' : '!important'; result += `${property}:${replacement}${priority};`; } if (property === 'background-color') result += backgroundContext(value); } return result; }
function stylesheetRules(rules) { let output = ''; for (const rule of rules) { if (rule.selectorText && rule.style) { const body = declarations(rule.style, true); if (body) output += `${scopeSelector(rule.selectorText)}{${body}}\n`; } else if (rule.cssRules && (rule.type === CSSRule.MEDIA_RULE || rule.type === CSSRule.SUPPORTS_RULE || rule.constructor.name === 'CSSLayerBlockRule')) { const inner = stylesheetRules(rule.cssRules); if (inner) output += `${rule.cssText.slice(0, rule.cssText.indexOf('{'))}{${inner}}\n`; } else if (rule.type === CSSRule.IMPORT_RULE) { try { const inner = stylesheetRules(rule.styleSheet.cssRules); output += rule.media.mediaText ? `@media ${rule.media.mediaText}{${inner}}` : inner; } catch { /* Cross-origin sheets are not readable through CSSOM. */ } } } return output; }
function scopeSelector(value) { const selectors = []; let start = 0, depth = 0, quote = ''; for (let i = 0; i <= value.length; i++) { const char = value[i]; if (char === '\\') { i++; continue; } if (quote) { if (char === quote) quote = ''; continue; } if (char === '"' || char === "'") { quote = char; continue; } if (char === '(' || char === '[') depth++; if (char === ')' || char === ']') depth--; if (i === value.length || (char === ',' && depth === 0)) { const selector = value.slice(start, i).trim(); selectors.push(/^(html|:root)(?=[\s.#:[>+~]|$)/.test(selector) ? selector.replace(/^(html|:root)/, `html[${ROOT}]`) : `html[${ROOT}] ${selector}`); start = i + 1; } } return selectors.join(','); }
function sheets() { return [...document.styleSheets].filter(sheet => sheet.ownerNode !== style && sheet.ownerNode?.id !== PREVIEW_STYLE_ID); }
function signature() { return sheets().map(sheet => { try { return `${sheet.href || ''}:${sheet.cssRules.length}`; } catch { return sheet.href || 'unreadable'; } }).join('|'); }
function buildStyle() { if (!theme) return; inlineRules = new Map(); colorCache.clear(); contextCache.clear(); const c = theme.colors; const white = model.mapColor('#FFFFFF', 'color', theme, mode) || c.text; const black = model.mapColor('#000000', 'color', theme, mode) || c.text; let css = `:root[${ROOT}]{--bskyplus-theme-white-text:${white};--bskyplus-theme-black-text:${black};color-scheme:${theme.base === 'light' ? 'light' : 'dark'};}\n`; css += `:root[${ROOT}]{--text:${c.text}!important;--background:${c.background}!important;--backgroundLight:${c.surfaceRaised}!important;}\n`; css += `:root[${ROOT}]{--bskyplus-theme-text:${c.text};--bskyplus-theme-muted:${c.textMuted};}\n`; css += `html[${ROOT}],html[${ROOT}] body,html[${ROOT}] #root{background-color:${c.background};color:${c.text};}\n`; css += `html[${ROOT}] :focus-visible{outline-color:${c.focus}!important;}\n`; css += `html[${ROOT}] ::selection{background-color:${c.primary}!important;color:${c.onPrimary}!important;}\n`; for (const sheet of sheets()) { if (sheet.disabled) continue; try { const rules = stylesheetRules(sheet.cssRules); css += sheet.media.mediaText ? `@media ${sheet.media.mediaText}{${rules}}` : rules; } catch { /* Inline declarations still work when a sheet is cross-origin. */ } } style.textContent = css; sourceSignature = signature(); pending.add(document.documentElement); }
function skip(element) { return ['IMG', 'VIDEO', 'CANVAS', 'IFRAME', 'STYLE', 'SCRIPT'].includes(element.tagName) || element.closest('[data-bskyplus-theme-editor], [data-bskyplus-theme-ignore]') || element.closest('svg')?.querySelector('image'); }
function updateInline(element) { if (skip(element)) return; let body = declarations(element.style); if (element.tagName.toLowerCase() === 'svg' && element.querySelector('[fill="#fff"], [fill="#FFF"], [fill="#ffffff"], [fill="#FFFFFF"], [fill="white"]')) { const backing = [...element.querySelectorAll('[fill]')].find(node => { const value = node.getAttribute('fill'); return value !== 'none' && !['#fff', '#ffffff', 'white'].includes(value.toLowerCase()); }); if (backing) { const value = backing.getAttribute('fill'); const context = backgroundContext(value); const color = model.parseColor(value === 'currentColor' ? getComputedStyle(backing).fill : value)?.hex; const foreground = [theme.colors.primary, theme.colors.link, theme.colors.negative, theme.colors.positive] .includes(color) ? theme.colors.onPrimary : theme.colors.textInverse; body += context || `--bskyplus-theme-white-text:${foreground};`; } } for (const property of ['fill', 'stroke']) { // An inline CSS declaration takes precedence over SVG presentation attributes. if (!element.style.getPropertyValue(property) && element.hasAttribute(property)) { const value = recolor(element.getAttribute(property), property); if (value) body += `${property}:${value}!important;`; } } if (!body) { element.removeAttribute(MARKER); return; } let id = inlineRules.get(body); if (!id) { id = String(inlineRules.size + 1); inlineRules.set(body, id); style.sheet.insertRule(`html[${ROOT}] [${MARKER}="${id}"]{${body}}`, style.sheet.cssRules.length); } if (element.getAttribute(MARKER) !== id) element.setAttribute(MARKER, id); }
function flush() { scheduled = false; if (!theme) return; if (mode !== nativeMode()) { mode = nativeMode(); rescanSheets = true; } if (rescanSheets) { rescanSheets = false; buildStyle(); } const roots = [...pending].filter(node => { for (let ancestor = node.parentElement; ancestor; ancestor = ancestor.parentElement) { if (pending.has(ancestor)) return false; } return true; }); pending.clear(); for (const root of roots) { if (!root.isConnected) continue; updateInline(root); for (const element of root.querySelectorAll('[style], [fill], [stroke], svg')) updateInline(element); } }
function schedule() { if (!scheduled) { scheduled = true; requestAnimationFrame(flush); } }
function stop() { observer?.disconnect(); clearInterval(timer); pending.clear(); style?.remove(); document.documentElement.removeAttribute(ROOT); document.querySelectorAll(`[${MARKER}]`).forEach(element => element.removeAttribute(MARKER)); inlineRules.clear(); style = undefined; theme = null; }
function clearViewTransition() { if (!viewTransition) return; try { viewTransition.skipTransition(); } catch {} viewTransition = undefined; }
function motionPreferenceChanged(event) { if (event.matches) clearViewTransition(); }
if (typeof reducedMotion.addEventListener === 'function') { reducedMotion.addEventListener('change', motionPreferenceChanged); } else if (typeof reducedMotion.addListener === 'function') { reducedMotion.addListener(motionPreferenceChanged); }
function ensurePreviewStyle() { if (document.getElementById(PREVIEW_STYLE_ID)) return; const previewStyle = document.createElement('style'); previewStyle.id = PREVIEW_STYLE_ID; previewStyle.textContent = `::view-transition-old(root),::view-transition-new(root){animation-duration:${PREVIEW_TRANSITION_MS}ms!important;}`; (document.head || document.documentElement).append(previewStyle); }
function refreshPreview(animate) { if (!animate || reducedMotion.matches || typeof document.startViewTransition !== 'function') { clearViewTransition(); refresh(); return; } ensurePreviewStyle(); clearViewTransition(); const transition = document.startViewTransition(refresh); viewTransition = transition; const finish = () => { if (viewTransition === transition) viewTransition = undefined; }; transition.ready.catch(() => {}); transition.finished.then(finish, finish); }
function refresh() { const nextTheme = previewTheme || library.themes.find(item => item.id === library.activeId) || model.BUILTIN_THEMES[library.activeId] || null; if (JSON.stringify(nextTheme) === JSON.stringify(theme)) return; stop(); theme = nextTheme; if (!theme) { window.dispatchEvent(new Event('bskyplus:theme-changed')); return; } mode = nativeMode(); style = document.createElement('style'); style.id = STYLE_ID; (document.head || document.documentElement).append(style); document.documentElement.setAttribute(ROOT, theme.id); buildStyle(); flush(); window.dispatchEvent(new Event('bskyplus:theme-changed')); observer = new MutationObserver(records => { for (const record of records) { if (record.target === style || record.target?.id === PREVIEW_STYLE_ID || style.contains(record.target)) continue; if (record.type === 'attributes') { if (record.target === document.documentElement && record.attributeName === 'class') rescanSheets = true; else if (record.attributeName === 'class') continue; pending.add(record.target); if (record.target.tagName === 'LINK') rescanSheets = true; } else { if (record.target.nodeType === Node.ELEMENT_NODE && record.target.closest('style')) rescanSheets = true; for (const node of record.addedNodes) { if (node.nodeType !== Node.ELEMENT_NODE || node === style || node.id === PREVIEW_STYLE_ID) continue; if (node.matches('style, link') || node.querySelector('style, link')) rescanSheets = true; pending.add(node); } } } if (rescanSheets || pending.size) schedule(); }); observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['style', 'fill', 'stroke', 'class', 'href', 'disabled'], }); // React Native Web can insert CSSOM rules without a DOM mutation. timer = setInterval(() => { if (signature() !== sourceSignature || mode !== nativeMode()) { rescanSheets = true; schedule(); } }, 1500); }
function selectedTheme(value) { return value.themes.find(item => item.id === value.activeId) || model.BUILTIN_THEMES[value.activeId] || null; }
function paletteSignature(value) { return Object.keys(value.colors).sort().map(key => `${key}:${value.colors[key]}`).join(';'); }
function selectionPaletteChanged(previous, next) { const previousTheme = selectedTheme(previous); const nextTheme = selectedTheme(next); if (!previousTheme || !nextTheme) return previousTheme !== nextTheme; return paletteSignature(previousTheme) !== paletteSignature(nextTheme); }
function apply(value) { library = model.normalizeLibrary(value); refresh(); }
window.addEventListener('bskyplus:theme-preview', event => { if (event.detail === null) previewTheme = null; else { try { previewTheme = model.validateTheme(event.detail); } catch { return; } } const colorsChanged = JSON.stringify(previewTheme?.colors) !== JSON.stringify(theme?.colors); if (!previewTheme) { clearViewTransition(); document.getElementById(PREVIEW_STYLE_ID)?.remove(); refresh(); } else refreshPreview(colorsChanged); });
let changed = false; api.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes[model.STORAGE_KEY]) { changed = true; const previous = library; const next = model.normalizeLibrary(changes[model.STORAGE_KEY].newValue); const animate = initialized && !previewTheme && next.activeId !== previous.activeId && selectionPaletteChanged(previous, next); library = next; initialized = true; if (previewTheme) return; if (animate) refreshPreview(true); else refresh(); } }); api.storage.local.get(model.STORAGE_KEY).then(data => { if (!changed) apply(data[model.STORAGE_KEY]); initialized = true; }).catch(() => { initialized = true; /* A failed read leaves native styling intact. */ });})();