// ==UserScript== // @name UI Token Extractor // @namespace https://devops.cloudhost.es/Malin/userscripts // @version 1.0.0 // @description Extracts colors, typography, spacing, effects, and component styles from the current page's live DOM and shows them on-screen as selectable text. No AI, no network requests of any kind. // @author Malin // @match *://*/* // @grant none // @run-at document-idle // ==/UserScript== /* * UI Token Extractor * ------------------- * Inspired by uidrop.site's live-DOM design-token extraction, minus the * AI hand-off. This script never sends data anywhere -- it only reads * computed styles already present in the page you're viewing and renders * them in a local overlay panel. No fetch/XHR/WebSocket/GM_xmlhttpRequest * calls exist anywhere in this file. The one non-essential convenience * (a "Copy" button using navigator.clipboard.writeText) is 100% local * and can be deleted without affecting extraction if you'd rather rely * purely on manual select-and-copy from the readonly textareas. * * Trigger: floating button (bottom-right) or Alt+Shift+U. */ (function () { 'use strict'; const MAX_SAMPLE_ELEMENTS = 4000; // cap DOM walk cost on huge pages const TOP_N = 12; // how many distinct values to keep per category // --------------------------------------------------------------------- // Extraction // --------------------------------------------------------------------- function tally(map, key) { if (!key) return; map.set(key, (map.get(key) || 0) + 1); } function topEntries(map, n) { return Array.from(map.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, n); } function isNoiseColor(c) { if (!c) return true; return c === 'rgba(0, 0, 0, 0)' || c === 'transparent'; } function detectColorScheme() { const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const bodyBg = getComputedStyle(document.body).backgroundColor; const m = bodyBg.match(/\d+(\.\d+)?/g); let bodyIsDark = null; if (m && m.length >= 3) { const [r, g, b] = m.map(Number); // Perceived luminance const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; bodyIsDark = luminance < 128; } return { prefersDark, bodyBackground: bodyBg, bodyAppearsDark: bodyIsDark }; } function getRootCustomProperties() { const rootStyle = getComputedStyle(document.documentElement); const props = []; // documentElement's computed style doesn't enumerate custom props // directly in all browsers, so also scan inline :root declarations // from same-origin stylesheets where accessible. try { for (const sheet of document.styleSheets) { let rules; try { rules = sheet.cssRules; } catch (e) { continue; // cross-origin stylesheet, can't read rules -- skip silently } if (!rules) continue; for (const rule of rules) { if (rule.selectorText === ':root' && rule.style) { for (let i = 0; i < rule.style.length; i++) { const prop = rule.style[i]; if (prop.startsWith('--')) { props.push([prop, rule.style.getPropertyValue(prop).trim()]); } } } } } } catch (e) { // ignore -- best-effort only } return props; } function looksLikeButton(el) { if (el.tagName === 'BUTTON') return true; if (el.tagName === 'A' && /\bbtn\b|\bbutton\b/i.test(el.className || '')) return true; if (el.getAttribute && el.getAttribute('role') === 'button') return true; return false; } function looksLikeCard(el) { return /\bcard\b/i.test(el.className || ''); } function looksLikeChip(el) { return /\bchip\b|\bbadge\b|\btag\b/i.test(el.className || ''); } function snapshotComponent(el) { const cs = getComputedStyle(el); return { tag: el.tagName.toLowerCase(), class: (el.className && typeof el.className === 'string') ? el.className.slice(0, 80) : '', background: cs.backgroundColor, color: cs.color, padding: cs.padding, borderRadius: cs.borderRadius, border: cs.border, boxShadow: cs.boxShadow === 'none' ? null : cs.boxShadow, fontSize: cs.fontSize, fontWeight: cs.fontWeight, }; } function extract() { const colorTally = new Map(); const bgTally = new Map(); const fontFamilyTally = new Map(); const fontSizeTally = new Map(); const fontWeightTally = new Map(); const paddingTally = new Map(); const marginTally = new Map(); const radiusTally = new Map(); const shadowTally = new Map(); const gradientTally = new Map(); const opacityTally = new Map(); const buttons = []; const cards = []; const chips = []; const inputs = []; const all = document.querySelectorAll('*'); const limit = Math.min(all.length, MAX_SAMPLE_ELEMENTS); for (let i = 0; i < limit; i++) { const el = all[i]; if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE' || el.tagName === 'NOSCRIPT') continue; const cs = getComputedStyle(el); if (!isNoiseColor(cs.color)) tally(colorTally, cs.color); if (!isNoiseColor(cs.backgroundColor)) tally(bgTally, cs.backgroundColor); tally(fontFamilyTally, cs.fontFamily); tally(fontSizeTally, cs.fontSize); tally(fontWeightTally, cs.fontWeight); if (cs.padding && cs.padding !== '0px') tally(paddingTally, cs.padding); if (cs.margin && cs.margin !== '0px') tally(marginTally, cs.margin); if (cs.borderRadius && cs.borderRadius !== '0px') tally(radiusTally, cs.borderRadius); if (cs.boxShadow && cs.boxShadow !== 'none') tally(shadowTally, cs.boxShadow); if (cs.backgroundImage && cs.backgroundImage.includes('gradient')) tally(gradientTally, cs.backgroundImage); const op = parseFloat(cs.opacity); if (!isNaN(op) && op !== 1 && op !== 0) tally(opacityTally, cs.opacity); if (looksLikeButton(el) && buttons.length < 5) buttons.push(snapshotComponent(el)); else if (el.tagName === 'INPUT' && inputs.length < 5) inputs.push(snapshotComponent(el)); else if (looksLikeCard(el) && cards.length < 5) cards.push(snapshotComponent(el)); else if (looksLikeChip(el) && chips.length < 5) chips.push(snapshotComponent(el)); } return { url: location.href, extractedAt: new Date().toISOString(), truncated: all.length > MAX_SAMPLE_ELEMENTS, elementsScanned: limit, colorScheme: detectColorScheme(), colors: { textColors: topEntries(colorTally, TOP_N), backgroundColors: topEntries(bgTally, TOP_N), rootCustomProperties: getRootCustomProperties().slice(0, 40), }, typography: { fontFamilies: topEntries(fontFamilyTally, TOP_N), fontSizes: topEntries(fontSizeTally, TOP_N), fontWeights: topEntries(fontWeightTally, TOP_N), }, spacing: { paddings: topEntries(paddingTally, TOP_N), margins: topEntries(marginTally, TOP_N), }, radius: topEntries(radiusTally, TOP_N), effects: { shadows: topEntries(shadowTally, TOP_N), gradients: topEntries(gradientTally, TOP_N), opacities: topEntries(opacityTally, TOP_N), }, components: { buttons, inputs, cards, chips }, }; } // --------------------------------------------------------------------- // Formatting // --------------------------------------------------------------------- function fmtEntries(entries) { if (!entries.length) return ' (none found)'; return entries.map(([v, count]) => ` ${v} (x${count})`).join('\n'); } function fmtComponent(c) { return ( ` <${c.tag}${c.class ? ` class="${c.class}"` : ''}>\n` + ` background: ${c.background}\n` + ` color: ${c.color}\n` + ` padding: ${c.padding}\n` + ` border-radius: ${c.borderRadius}\n` + ` border: ${c.border}\n` + (c.boxShadow ? ` box-shadow: ${c.boxShadow}\n` : '') + ` font: ${c.fontWeight} ${c.fontSize}\n` ); } function fmtComponents(list, label) { if (!list.length) return `${label}:\n (none found)`; return `${label}:\n` + list.map(fmtComponent).join('\n'); } function toReport(data) { const lines = []; lines.push(`UI Token Extractor — ${data.url}`); lines.push(`Extracted: ${data.extractedAt}`); lines.push(`Elements scanned: ${data.elementsScanned}${data.truncated ? ' (capped, page has more)' : ''}`); lines.push(''); lines.push('=== COLOR SCHEME ==='); lines.push(`prefers-color-scheme: dark => ${data.colorScheme.prefersDark}`); lines.push(`body background => ${data.colorScheme.bodyBackground}`); lines.push(`body appears dark => ${data.colorScheme.bodyAppearsDark}`); lines.push(''); lines.push('=== COLORS: text ==='); lines.push(fmtEntries(data.colors.textColors)); lines.push(''); lines.push('=== COLORS: background ==='); lines.push(fmtEntries(data.colors.backgroundColors)); lines.push(''); lines.push('=== COLORS: :root custom properties (design tokens) ==='); if (!data.colors.rootCustomProperties.length) { lines.push(' (none found or stylesheets not readable)'); } else { data.colors.rootCustomProperties.forEach(([k, v]) => lines.push(` ${k}: ${v}`)); } lines.push(''); lines.push('=== TYPOGRAPHY: font families ==='); lines.push(fmtEntries(data.typography.fontFamilies)); lines.push(''); lines.push('=== TYPOGRAPHY: font sizes ==='); lines.push(fmtEntries(data.typography.fontSizes)); lines.push(''); lines.push('=== TYPOGRAPHY: font weights ==='); lines.push(fmtEntries(data.typography.fontWeights)); lines.push(''); lines.push('=== SPACING: padding values ==='); lines.push(fmtEntries(data.spacing.paddings)); lines.push(''); lines.push('=== SPACING: margin values ==='); lines.push(fmtEntries(data.spacing.margins)); lines.push(''); lines.push('=== BORDER RADIUS ==='); lines.push(fmtEntries(data.radius)); lines.push(''); lines.push('=== EFFECTS: shadows ==='); lines.push(fmtEntries(data.effects.shadows)); lines.push(''); lines.push('=== EFFECTS: gradients ==='); lines.push(fmtEntries(data.effects.gradients)); lines.push(''); lines.push('=== EFFECTS: non-default opacities ==='); lines.push(fmtEntries(data.effects.opacities)); lines.push(''); lines.push('=== COMPONENTS ==='); lines.push(fmtComponents(data.components.buttons, 'Buttons')); lines.push(''); lines.push(fmtComponents(data.components.inputs, 'Inputs')); lines.push(''); lines.push(fmtComponents(data.components.cards, 'Cards')); lines.push(''); lines.push(fmtComponents(data.components.chips, 'Chips / badges / tags')); return lines.join('\n'); } // --------------------------------------------------------------------- // UI overlay // --------------------------------------------------------------------- const STYLE = ` #uite-trigger { position: fixed; bottom: 20px; right: 20px; z-index: 2147483000; width: 48px; height: 48px; border-radius: 50%; background: #1a1a1a; color: #fff; border: 2px solid #444; font: 20px/48px system-ui, sans-serif; text-align: center; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,.4); user-select: none; } #uite-trigger:hover { background: #333; } #uite-overlay { position: fixed; inset: 0; z-index: 2147483001; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; } #uite-panel { width: min(880px, 92vw); height: min(720px, 88vh); background: #161616; color: #e6e6e6; border-radius: 10px; border: 1px solid #333; box-shadow: 0 10px 40px rgba(0,0,0,.5); display: flex; flex-direction: column; overflow: hidden; font: 13px/1.5 ui-monospace, 'Cascadia Code', 'Fira Code', monospace; } #uite-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid #333; font: 600 14px/1 system-ui, sans-serif; flex-shrink: 0; } #uite-header .uite-actions { display: flex; gap: 8px; } #uite-header button { font: 600 12px/1 system-ui, sans-serif; padding: 6px 10px; border-radius: 6px; border: 1px solid #444; background: #222; color: #eee; cursor: pointer; } #uite-header button:hover { background: #2c2c2c; } #uite-textarea { flex: 1; border: none; outline: none; resize: none; background: #0f0f0f; color: #d8d8d8; padding: 14px; white-space: pre; overflow: auto; } #uite-hint { padding: 6px 14px; font: 11px/1.4 system-ui, sans-serif; color: #888; border-top: 1px solid #262626; } `; function injectStyle() { const s = document.createElement('style'); s.textContent = STYLE; document.head.appendChild(s); } function buildOverlay(reportText) { const overlay = document.createElement('div'); overlay.id = 'uite-overlay'; const panel = document.createElement('div'); panel.id = 'uite-panel'; const header = document.createElement('div'); header.id = 'uite-header'; header.innerHTML = `UI Token Extractor`; const actions = document.createElement('div'); actions.className = 'uite-actions'; const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy all'; copyBtn.onclick = () => { navigator.clipboard.writeText(reportText).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => (copyBtn.textContent = 'Copy all'), 1200); }).catch(() => { // Clipboard API can fail silently in some contexts (e.g. insecure // origin) -- the textarea below is still fully selectable manually. }); }; const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close (Esc)'; closeBtn.onclick = () => overlay.remove(); actions.appendChild(copyBtn); actions.appendChild(closeBtn); header.appendChild(actions); const textarea = document.createElement('textarea'); textarea.id = 'uite-textarea'; textarea.readOnly = true; textarea.value = reportText; textarea.addEventListener('click', () => textarea.select()); const hint = document.createElement('div'); hint.id = 'uite-hint'; hint.textContent = 'Click the text to select all, then Ctrl/Cmd+C. Nothing here is sent anywhere -- extraction is entirely local.'; panel.appendChild(header); panel.appendChild(textarea); panel.appendChild(hint); overlay.appendChild(panel); overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); document.addEventListener('keydown', function escHandler(e) { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escHandler); } }); document.body.appendChild(overlay); textarea.focus(); textarea.select(); } function run() { const data = extract(); const report = toReport(data); buildOverlay(report); } function addTrigger() { const btn = document.createElement('div'); btn.id = 'uite-trigger'; btn.title = 'Extract UI tokens (Alt+Shift+U)'; btn.textContent = '🎨'; btn.addEventListener('click', run); document.body.appendChild(btn); } injectStyle(); if (document.body) addTrigger(); else document.addEventListener('DOMContentLoaded', addTrigger); document.addEventListener('keydown', (e) => { if (e.altKey && e.shiftKey && (e.key === 'U' || e.key === 'u')) { run(); } }); })();