Solarized themes for Hacker News
ChatGPT wrote this lol.unknown
javascript
9 months ago
13 kB
13
No Index
// ==UserScript==
// @name Hacker News Dark Theme (Dark + Solarized + Auto + Header Band)
// @namespace https://news.ycombinator.com/
// @version 1.3.0
// @description Clean themes for Hacker News: Dark, Solarized Dark, and Solarized Light. Header controls, keyboard shortcuts, auto-follow system, and a full-width background band behind the top menu for readability.
// @author you
// @match https://news.ycombinator.com/*
// @run-at document-start
// @grant GM_addStyle
// ==/UserScript==
(function () {
'use strict';
// --- Storage keys
const ENABLE_KEY = 'hn_dark_enabled_v1';
const AUTO_KEY = 'hn_auto_follow_system_v1';
const DARK_PREF_KEY = 'hn_pref_dark_theme_v1'; // 'dark' | 'solarized-dark'
const LIGHT_PREF_KEY = 'hn_pref_light_theme_v1'; // 'solarized-light'
// --- Themes
const THEMES = ['dark', 'solarized-dark', 'solarized-light'];
const isDarkTheme = (t) => t === 'dark' || t === 'solarized-dark';
// --- Initial state
const mql = matchMedia('(prefers-color-scheme: dark)');
const prefersDark = mql.matches;
const storedEnabled = localStorage.getItem(ENABLE_KEY);
const storedAuto = localStorage.getItem(AUTO_KEY);
let enabled = storedEnabled === null ? prefersDark : storedEnabled === 'true';
let autoFollow = storedAuto === 'true';
let darkPref = (localStorage.getItem(DARK_PREF_KEY) || 'dark');
if (!isDarkTheme(darkPref)) darkPref = 'dark';
let lightPref = (localStorage.getItem(LIGHT_PREF_KEY) || 'solarized-light');
if (isDarkTheme(lightPref)) lightPref = 'solarized-light';
let manualTheme = darkPref; // when autoFollow is off
// --- CSS (scoped by data attributes)
const css = `
/* Base wiring */
html[data-hn-enabled='1'], html[data-hn-enabled='1'] body {
background: var(--bg) !important;
color: var(--text) !important;
}
html[data-hn-enabled='1'] body,
html[data-hn-enabled='1'] table,
html[data-hn-enabled='1'] tr,
html[data-hn-enabled='1'] td {
background: transparent !important;
color: var(--text) !important;
border-color: var(--border) !important;
}
/* Palette: Modern Dark */
html[data-hn-enabled='1'][data-hn-theme='dark'] {
--bg: #0f1115;
--bg-elev: #161922;
--bg-soft: #141824;
--text: #e7eaf0;
--text-dim: #b7c0d1;
--link: #9ec1ff;
--link-visited: #c7a0ff;
--accent: #ff8a3d;
--muted: #2a2f3a;
--border: #262b36;
--upvote: #ffbd7a;
}
/* Palette: Solarized Dark */
html[data-hn-enabled='1'][data-hn-theme='solarized-dark'] {
--bg: #002b36; /* base03 */
--bg-elev: #073642; /* base02 */
--bg-soft: #073642;
--text: #93a1a1; /* base1 */
--text-dim: #839496; /* base0 */
--link: #268bd2; /* blue */
--link-visited: #6c71c4; /* violet */
--accent: #b58900; /* yellow */
--muted: #0a3942;
--border: #0c3f49;
--upvote: #b58900;
}
/* Palette: Solarized Light */
html[data-hn-enabled='1'][data-hn-theme='solarized-light'] {
--bg: #fdf6e3; /* base3 */
--bg-elev: #eee8d5; /* base2 */
--bg-soft: #efe9d9;
--text: #586e75; /* base00 */
--text-dim: #657b83; /* base0 */
--link: #268bd2; /* blue */
--link-visited: #6c71c4; /* violet */
--accent: #cb4b16; /* orange */
--muted: #e6dfcb;
--border: #e1d9c5;
--upvote: #b58900;
}
/* Top bar + full-width background band */
#hn-header-band {
position: fixed;
top: 0; left: 0; right: 0; height: 34px; /* fits HN header height */
background: var(--bg-elev);
border-bottom: 1px solid var(--border);
z-index: 999; /* beneath controls, above page bg */
pointer-events: none; /* don't block clicks */
}
/* make the actual header sit above the band */
html[data-hn-enabled='1'] .pagetop,
html[data-hn-enabled='1'] .topcolor,
html[data-hn-enabled='1'] table[bgcolor='#ff6600'] { position: relative; z-index: 1000; background: transparent !important; }
/* Story rows */
html[data-hn-enabled='1'] .athing { background: transparent !important; }
html[data-hn-enabled='1'] .titleline a { color: var(--text) !important; }
html[data-hn-enabled='1'] .titleline a:hover { text-decoration: underline; }
html[data-hn-enabled='1'] .subtext,
html[data-hn-enabled='1'] .subline,
html[data-hn-enabled='1'] .age { color: var(--text-dim) !important; }
/* Links */
html[data-hn-enabled='1'] a { color: var(--link) !important; }
html[data-hn-enabled='1'] a:visited { color: var(--link-visited) !important; }
/* Comment pages */
html[data-hn-enabled='1'] .comment-tree td { border-color: var(--muted) !important; }
html[data-hn-enabled='1'] .commtext { color: var(--text) !important; }
html[data-hn-enabled='1'] .commtext a { color: var(--link) !important; }
html[data-hn-enabled='1'] .comhead { color: var(--text-dim) !important; }
/* Inputs (login, search, reply, submit) */
html[data-hn-enabled='1'] input,
html[data-hn-enabled='1'] textarea,
html[data-hn-enabled='1'] select {
background: var(--bg-elev) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
}
html[data-hn-enabled='1'] input[type='submit'],
html[data-hn-enabled='1'] button {
background: var(--bg-soft) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
cursor: pointer;
}
/* Misc */
html[data-hn-enabled='1'] .yclinks { color: var(--text-dim) !important; }
html[data-hn-enabled='1'] .morelink:link,
html[data-hn-enabled='1'] a.morelink { color: var(--accent) !important; }
html[data-hn-enabled='1'] .votelinks a[href^='vote'] { color: var(--upvote) !important; }
/* Light borders for structure */
html[data-hn-enabled='1'] td[bgcolor='#f6f6ef'] { background: var(--bg) !important; }
html[data-hn-enabled='1'] td[bgcolor='#ffffff'] { background: var(--bg-elev) !important; }
html[data-hn-enabled='1'] .spacer,
html[data-hn-enabled='1'] .morespace { background: transparent !important; }
html[data-hn-enabled='1'] img { background: transparent !important; }
/* Header buttons */
#hn-dark-toggle, #hn-theme-toggle, #hn-auto-toggle {
font: 12px system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
padding: 2px 6px;
border-radius: 6px;
border: 1px solid var(--border);
margin-left: 8px;
background: var(--bg-soft);
color: var(--text);
text-decoration: none !important;
display: inline-block;
line-height: 1.4;
}
#hn-dark-toggle:hover, #hn-theme-toggle:hover, #hn-auto-toggle:hover { filter: brightness(1.05); }
`;
let styleNode = null;
// --- Helpers
const effectiveTheme = () => {
if (autoFollow) return mql.matches ? darkPref : lightPref;
return manualTheme;
};
const setAttrs = () => {
const html = document.documentElement;
if (enabled) {
html.setAttribute('data-hn-enabled', '1');
html.setAttribute('data-hn-theme', effectiveTheme());
} else {
html.removeAttribute('data-hn-enabled');
html.removeAttribute('data-hn-theme');
}
};
const persist = () => {
localStorage.setItem(ENABLE_KEY, String(enabled));
localStorage.setItem(AUTO_KEY, String(autoFollow));
localStorage.setItem(DARK_PREF_KEY, darkPref);
localStorage.setItem(LIGHT_PREF_KEY, lightPref);
};
const apply = () => {
if (enabled) {
if (!styleNode) {
try { styleNode = GM_addStyle ? GM_addStyle(css) : null; } catch (_) { styleNode = null; }
if (!styleNode) {
styleNode = document.createElement('style');
styleNode.id = 'hn-dark-style';
styleNode.textContent = css;
(document.head || document.documentElement).appendChild(styleNode);
}
}
} else {
if (styleNode && styleNode.parentNode) styleNode.parentNode.removeChild(styleNode);
styleNode = null;
}
setAttrs();
persist();
updateToggleLabels();
ensureHeaderBand();
};
const cycleTheme = () => {
const list = THEMES;
const current = effectiveTheme();
const idx = Math.max(0, list.indexOf(current));
const next = list[(idx + 1) % list.length];
if (autoFollow) {
if (mql.matches) { darkPref = isDarkTheme(next) ? next : 'dark'; }
else { lightPref = isDarkTheme(next) ? 'solarized-light' : next; }
} else {
manualTheme = next;
}
apply();
};
const toggleEnabled = () => { enabled = !enabled; apply(); };
const toggleAuto = () => { autoFollow = !autoFollow; apply(); };
const labelTheme = (t) => t.replace('-', ' ');
const updateToggleLabels = () => {
const onOff = enabled ? 'on' : 'off';
const btn = document.getElementById('hn-dark-toggle');
if (btn) btn.textContent = `dark: ${onOff}`;
const themeBtn = document.getElementById('hn-theme-toggle');
if (themeBtn) themeBtn.textContent = `theme: ${labelTheme(effectiveTheme())}`;
const autoBtn = document.getElementById('hn-auto-toggle');
if (autoBtn) autoBtn.textContent = `auto: ${autoFollow ? 'system' : 'off'}`;
};
const ensureHeaderToggle = () => {
const haveAll = document.getElementById('hn-dark-toggle') && document.getElementById('hn-theme-toggle') && document.getElementById('hn-auto-toggle');
if (haveAll) { updateToggleLabels(); return true; }
const candidates = [
document.querySelector('span.pagetop'),
document.querySelector('td.pagetop'),
document.querySelector('table tr td span > b')?.parentElement,
].filter(Boolean);
const host = candidates[0];
if (!host) return false;
const sep = () => document.createTextNode(' | ');
const btn = document.createElement('a');
btn.id = 'hn-dark-toggle';
btn.href = '#';
btn.addEventListener('click', (e) => { e.preventDefault(); toggleEnabled(); });
const themeBtn = document.createElement('a');
themeBtn.id = 'hn-theme-toggle';
themeBtn.href = '#';
themeBtn.addEventListener('click', (e) => { e.preventDefault(); cycleTheme(); });
const autoBtn = document.createElement('a');
autoBtn.id = 'hn-auto-toggle';
autoBtn.href = '#';
autoBtn.addEventListener('click', (e) => { e.preventDefault(); toggleAuto(); });
host.appendChild(sep()); host.appendChild(btn);
host.appendChild(sep()); host.appendChild(themeBtn);
host.appendChild(sep()); host.appendChild(autoBtn);
updateToggleLabels();
return true;
};
// Full-width header background band
const ensureHeaderBand = () => {
// Only show band when theming is enabled
let band = document.getElementById('hn-header-band');
if (!enabled) { if (band) band.remove(); return; }
if (!band) {
band = document.createElement('div');
band.id = 'hn-header-band';
(document.body || document.documentElement).appendChild(band);
}
};
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
const t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
if (key === 'd') toggleEnabled();
if (key === 't') cycleTheme();
if (key === 'a') toggleAuto();
}
}, true);
// React to system theme changes when auto is on
mql.addEventListener?.('change', () => { if (autoFollow) apply(); });
// Apply ASAP
apply();
// Once DOM is ready, inject header toggle + band
const ready = () => { ensureHeaderToggle(); ensureHeaderBand(); };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ready);
} else {
ready();
}
// Mutation observer (throttled) — stops once buttons are present
let mo;
let pending = false;
const installObserver = () => {
if (mo) mo.disconnect();
mo = new MutationObserver(() => {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
pending = false;
const ok = ensureHeaderToggle();
ensureHeaderBand();
if (ok && mo) mo.disconnect();
});
});
mo.observe(document.documentElement, { childList: true, subtree: true });
};
if (!ensureHeaderToggle()) installObserver(); else ensureHeaderBand();
})();
Editor is loading...
Leave a Comment