mirror of
https://codeberg.org/Tilo-K/diff-highlighter.git
synced 2026-07-03 16:23:02 +00:00
Initial commit: Diff Highlighter Chrome extension (MV3, TypeScript)
Highlights .diff/.patch files inline — additions green, deletions red, hunk/file headers styled, with toggleable highlight.js syntax highlighting. - MV3, content script gated on text/plain + URL ending in .diff/.patch - Two-phase render: instant diff colors, then chunked idle-time syntax - Popup switches + Alt+D shortcut, persisted in chrome.storage.sync, live re-render - Light/dark via prefers-color-scheme; only the `storage` permission - TypeScript + esbuild build; Forgejo Actions CI (typecheck + build) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
src/background.ts
Normal file
18
src/background.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// Service worker: seeds default settings on install and handles the keyboard
|
||||
// shortcut by flipping the master toggle. Content scripts react via
|
||||
// chrome.storage.onChanged, so no direct messaging is needed.
|
||||
import { DEFAULT_SETTINGS } from './settings';
|
||||
import type { Settings } from './settings';
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.storage.sync.get(DEFAULT_SETTINGS, (current) => {
|
||||
chrome.storage.sync.set(current as Settings); // persist resolved defaults
|
||||
});
|
||||
});
|
||||
|
||||
chrome.commands.onCommand.addListener((command) => {
|
||||
if (command !== 'toggle-highlighting') return;
|
||||
chrome.storage.sync.get(DEFAULT_SETTINGS, (s) => {
|
||||
chrome.storage.sync.set({ enabled: !(s as Settings).enabled });
|
||||
});
|
||||
});
|
||||
136
src/content.ts
Normal file
136
src/content.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// Runs on text/plain pages whose URL ends in .diff/.patch. Replaces the raw
|
||||
// <pre> with a colored, optionally syntax-highlighted rendering, and reacts to
|
||||
// the popup/keyboard toggles live (no reload).
|
||||
import hljs from 'highlight.js/lib/common';
|
||||
import { parseDiff } from './diff-parser';
|
||||
import type { ParsedLine } from './diff-parser';
|
||||
import { DEFAULT_SETTINGS } from './settings';
|
||||
import type { Settings } from './settings';
|
||||
|
||||
const CHUNK = 300; // code lines highlighted per idle slice
|
||||
const MAX_SYNTAX_LINES = 50_000; // above this, keep diff colors but skip syntax
|
||||
|
||||
// --- Guards: only touch browser-rendered plain-text diffs. -------------------
|
||||
const pre =
|
||||
document.contentType === 'text/plain'
|
||||
? document.querySelector<HTMLPreElement>('body > pre') ??
|
||||
document.querySelector<HTMLPreElement>('pre')
|
||||
: null;
|
||||
|
||||
if (pre) run(pre);
|
||||
|
||||
function run(pre: HTMLPreElement): void {
|
||||
const raw = pre.textContent ?? '';
|
||||
|
||||
// Parse + build the base HTML once; reused on every (re)render.
|
||||
let baseHtml: string | null = null;
|
||||
const ensureBaseHtml = (): string => (baseHtml ??= buildHtml(parseDiff(raw)));
|
||||
|
||||
let idleHandle: number | null = null;
|
||||
const cancelHighlight = (): void => {
|
||||
if (idleHandle !== null) {
|
||||
cancelIdle(idleHandle);
|
||||
idleHandle = null;
|
||||
}
|
||||
};
|
||||
|
||||
const restorePlain = (): void => {
|
||||
cancelHighlight();
|
||||
if (pre.classList.contains('dh-on')) {
|
||||
pre.textContent = raw;
|
||||
pre.classList.remove('dh-on');
|
||||
}
|
||||
};
|
||||
|
||||
const render = (settings: Settings): void => {
|
||||
cancelHighlight();
|
||||
pre.innerHTML = ensureBaseHtml();
|
||||
pre.classList.add('dh-on');
|
||||
if (settings.syntax) scheduleSyntax();
|
||||
};
|
||||
|
||||
const scheduleSyntax = (): void => {
|
||||
const nodes = Array.from(
|
||||
pre.querySelectorAll<HTMLElement>('.dh-code[data-lang]'),
|
||||
);
|
||||
if (nodes.length > MAX_SYNTAX_LINES) {
|
||||
console.info(
|
||||
`[diff-highlighter] ${nodes.length} highlightable lines — skipping syntax for speed`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
const step = (): void => {
|
||||
const end = Math.min(i + CHUNK, nodes.length);
|
||||
for (; i < end; i++) {
|
||||
const el = nodes[i]!;
|
||||
const lang = el.dataset['lang']!;
|
||||
try {
|
||||
el.innerHTML = hljs.highlight(el.textContent ?? '', {
|
||||
language: lang,
|
||||
ignoreIllegals: true,
|
||||
}).value;
|
||||
} catch {
|
||||
/* leave the escaped text in place */
|
||||
}
|
||||
}
|
||||
idleHandle = i < nodes.length ? requestIdle(step) : null;
|
||||
};
|
||||
idleHandle = requestIdle(step);
|
||||
};
|
||||
|
||||
const apply = (settings: Settings): void => {
|
||||
if (settings.enabled) render(settings);
|
||||
else restorePlain();
|
||||
};
|
||||
|
||||
const loadAndApply = (): void => {
|
||||
chrome.storage.sync.get(DEFAULT_SETTINGS, (s) => apply(s as Settings));
|
||||
};
|
||||
|
||||
loadAndApply();
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'sync') return;
|
||||
if ('enabled' in changes || 'syntax' in changes) loadAndApply();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Rendering helpers -------------------------------------------------------
|
||||
|
||||
function buildHtml(lines: ParsedLine[]): string {
|
||||
let html = '';
|
||||
for (const line of lines) {
|
||||
const hasGutter = line.kind === 'add' || line.kind === 'del' || line.kind === 'ctx';
|
||||
const gutter = hasGutter
|
||||
? `<span class="dh-gutter">${escapeHtml(line.marker)}</span>`
|
||||
: '';
|
||||
const langAttr =
|
||||
line.lang && hljs.getLanguage(line.lang) ? ` data-lang="${line.lang}"` : '';
|
||||
html +=
|
||||
`<span class="dh-line dh-${line.kind}">${gutter}` +
|
||||
`<span class="dh-code"${langAttr}>${escapeHtml(line.code)}</span></span>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
const ESCAPE: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/[&<>]/g, (c) => ESCAPE[c]!);
|
||||
}
|
||||
|
||||
// --- Idle scheduling (with setTimeout fallback) ------------------------------
|
||||
|
||||
const requestIdle: (cb: () => void) => number =
|
||||
typeof requestIdleCallback === 'function'
|
||||
? (cb) => requestIdleCallback(cb)
|
||||
: (cb) => setTimeout(cb, 0) as unknown as number;
|
||||
|
||||
const cancelIdle: (handle: number) => void =
|
||||
typeof cancelIdleCallback === 'function'
|
||||
? (handle) => cancelIdleCallback(handle)
|
||||
: (handle) => clearTimeout(handle);
|
||||
119
src/diff-parser.ts
Normal file
119
src/diff-parser.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// Pure, DOM-free parsing of unified diff / patch text. Splitting this out keeps
|
||||
// the logic testable and the content script focused on rendering.
|
||||
|
||||
export type LineKind = 'add' | 'del' | 'hunk' | 'file' | 'meta' | 'ctx';
|
||||
|
||||
export interface ParsedLine {
|
||||
kind: LineKind;
|
||||
/** Gutter content: '+', '-', ' ', or '' for header lines. */
|
||||
marker: string;
|
||||
/** The remainder of the line to render (and maybe syntax-highlight). */
|
||||
code: string;
|
||||
/** Detected highlight.js language for this line's code, when known. */
|
||||
lang: string | null;
|
||||
}
|
||||
|
||||
/** File-extension → highlight.js language id (limited to the common bundle). */
|
||||
const EXT_TO_LANG: Record<string, string> = {
|
||||
js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
||||
ts: 'typescript', tsx: 'typescript', mts: 'typescript', cts: 'typescript',
|
||||
py: 'python', pyw: 'python',
|
||||
rb: 'ruby', go: 'go', rs: 'rust', java: 'java',
|
||||
kt: 'kotlin', kts: 'kotlin',
|
||||
c: 'c', h: 'c',
|
||||
cc: 'cpp', cpp: 'cpp', cxx: 'cpp', hpp: 'cpp', hh: 'cpp', hxx: 'cpp',
|
||||
cs: 'csharp', php: 'php', swift: 'swift',
|
||||
sh: 'bash', bash: 'bash', zsh: 'bash',
|
||||
json: 'json', yml: 'yaml', yaml: 'yaml',
|
||||
xml: 'xml', html: 'xml', htm: 'xml', svg: 'xml', vue: 'xml', xhtml: 'xml',
|
||||
css: 'css', scss: 'scss', sass: 'scss', less: 'less',
|
||||
md: 'markdown', markdown: 'markdown',
|
||||
sql: 'sql', lua: 'lua',
|
||||
pl: 'perl', pm: 'perl',
|
||||
r: 'r',
|
||||
ini: 'ini', toml: 'ini', cfg: 'ini', conf: 'ini',
|
||||
mk: 'makefile',
|
||||
graphql: 'graphql', gql: 'graphql',
|
||||
m: 'objectivec', mm: 'objectivec',
|
||||
vb: 'vbnet',
|
||||
};
|
||||
|
||||
const META_PREFIXES = [
|
||||
'diff ', 'index ', 'new file', 'deleted file', 'old mode', 'new mode',
|
||||
'rename ', 'copy ', 'similarity ', 'dissimilarity ', 'Binary files',
|
||||
'GIT binary patch', '\\ No newline',
|
||||
];
|
||||
|
||||
export function classifyLine(line: string): LineKind {
|
||||
if (line.startsWith('@@')) return 'hunk';
|
||||
// Must precede the +/- checks below.
|
||||
if (line.startsWith('+++') || line.startsWith('---')) return 'file';
|
||||
if (line.startsWith('+')) return 'add';
|
||||
if (line.startsWith('-')) return 'del';
|
||||
for (const p of META_PREFIXES) if (line.startsWith(p)) return 'meta';
|
||||
return 'ctx';
|
||||
}
|
||||
|
||||
function splitMarker(line: string, kind: LineKind): { marker: string; code: string } {
|
||||
if (kind === 'add' || kind === 'del') return { marker: line[0]!, code: line.slice(1) };
|
||||
if (kind === 'ctx') {
|
||||
if (line.startsWith(' ')) return { marker: ' ', code: line.slice(1) };
|
||||
return { marker: '', code: line }; // blank / no-prefix line
|
||||
}
|
||||
return { marker: '', code: line }; // hunk / file / meta shown whole
|
||||
}
|
||||
|
||||
/** Map a file path to a highlight.js language id, or null if unknown. */
|
||||
export function languageForPath(p: string): string | null {
|
||||
const base = (p.split('/').pop() ?? p).trim();
|
||||
if (base.toLowerCase() === 'makefile') return 'makefile';
|
||||
if (base.toLowerCase() === 'dockerfile') return null;
|
||||
const dot = base.lastIndexOf('.');
|
||||
if (dot <= 0) return null;
|
||||
return EXT_TO_LANG[base.slice(dot + 1).toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
/** Extract the relevant path from a `diff --git`, `---`, or `+++` header line. */
|
||||
function pathFromHeader(line: string): string | null {
|
||||
if (line.startsWith('diff --git')) {
|
||||
const m = line.match(/ b\/(.+)$/);
|
||||
return m ? m[1]! : null;
|
||||
}
|
||||
if (line.startsWith('+++ ') || line.startsWith('--- ')) {
|
||||
let p = line.slice(4).replace(/\t.*$/, '').trim(); // drop trailing tab+timestamp
|
||||
if (p === '/dev/null') return null;
|
||||
p = p.replace(/^[ab]\//, '');
|
||||
return p || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Parse raw diff text into classified lines, threading the per-file language. */
|
||||
export function parseDiff(raw: string): ParsedLine[] {
|
||||
const out: ParsedLine[] = [];
|
||||
let lang: string | null = null;
|
||||
|
||||
for (const line of raw.split('\n')) {
|
||||
const kind = classifyLine(line);
|
||||
|
||||
if (kind === 'meta' && line.startsWith('diff --git')) {
|
||||
// New file section in a git diff — reset the language (may be unknown).
|
||||
const p = pathFromHeader(line);
|
||||
lang = p ? languageForPath(p) : null;
|
||||
} else if (kind === 'file') {
|
||||
const p = pathFromHeader(line);
|
||||
if (line.startsWith('--- ')) {
|
||||
lang = p ? languageForPath(p) : null; // start of a plain unified-diff section
|
||||
} else if (p) {
|
||||
const refined = languageForPath(p); // +++ new path is most authoritative
|
||||
if (refined) lang = refined;
|
||||
}
|
||||
}
|
||||
|
||||
const { marker, code } = splitMarker(line, kind);
|
||||
const codeLang = kind === 'add' || kind === 'del' || kind === 'ctx' ? lang : null;
|
||||
out.push({ kind, marker, code, lang: codeLang });
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
7
src/hljs-common.d.ts
vendored
Normal file
7
src/hljs-common.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// The `highlight.js/lib/common` subpath (core + ~37 common languages) ships JS
|
||||
// but no dedicated typings, so reuse the main package's API type.
|
||||
declare module 'highlight.js/lib/common' {
|
||||
import type { HLJSApi } from 'highlight.js';
|
||||
const hljs: HLJSApi;
|
||||
export default hljs;
|
||||
}
|
||||
41
src/manifest.json
Normal file
41
src/manifest.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Diff Highlighter",
|
||||
"version": "1.0.0",
|
||||
"description": "Highlights .diff and .patch files — additions green, deletions red, with toggleable syntax highlighting.",
|
||||
"permissions": ["storage"],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Diff Highlighter",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://*/*", "http://*/*", "file:///*"],
|
||||
"include_globs": ["*.diff", "*.diff?*", "*.patch", "*.patch?*"],
|
||||
"js": ["content.js"],
|
||||
"css": ["diff.css", "hljs-theme.css"],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"toggle-highlighting": {
|
||||
"suggested_key": { "default": "Alt+D" },
|
||||
"description": "Toggle diff highlighting"
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/popup/popup.html
Normal file
32
src/popup/popup.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>Diff Highlighter</h1>
|
||||
|
||||
<label class="row">
|
||||
<span class="label">Highlighting</span>
|
||||
<input type="checkbox" id="enabled" class="switch" />
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span class="label">Syntax highlighting</span>
|
||||
<input type="checkbox" id="syntax" class="switch" />
|
||||
</label>
|
||||
|
||||
<p class="hint">
|
||||
Toggle anywhere with <kbd>Alt</kbd>+<kbd>D</kbd>
|
||||
(rebind at <code>chrome://extensions/shortcuts</code>).
|
||||
</p>
|
||||
<p class="hint">
|
||||
For local <code>file://</code> diffs, enable
|
||||
<em>“Allow access to file URLs”</em> on the extension’s details page.
|
||||
</p>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
29
src/popup/popup.ts
Normal file
29
src/popup/popup.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Drives the popup switches and persists them. The content script reads the
|
||||
// same chrome.storage.sync keys and re-renders the open diff live.
|
||||
import { DEFAULT_SETTINGS } from '../settings';
|
||||
import type { Settings } from '../settings';
|
||||
|
||||
const enabledEl = document.getElementById('enabled') as HTMLInputElement;
|
||||
const syntaxEl = document.getElementById('syntax') as HTMLInputElement;
|
||||
|
||||
function reflectDisabled(enabled: boolean): void {
|
||||
// Syntax highlighting only makes sense while the master toggle is on.
|
||||
syntaxEl.disabled = !enabled;
|
||||
syntaxEl.closest('.row')?.classList.toggle('muted', !enabled);
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(DEFAULT_SETTINGS, (s) => {
|
||||
const settings = s as Settings;
|
||||
enabledEl.checked = settings.enabled;
|
||||
syntaxEl.checked = settings.syntax;
|
||||
reflectDisabled(settings.enabled);
|
||||
});
|
||||
|
||||
enabledEl.addEventListener('change', () => {
|
||||
chrome.storage.sync.set({ enabled: enabledEl.checked });
|
||||
reflectDisabled(enabledEl.checked);
|
||||
});
|
||||
|
||||
syntaxEl.addEventListener('change', () => {
|
||||
chrome.storage.sync.set({ syntax: syntaxEl.checked });
|
||||
});
|
||||
12
src/settings.ts
Normal file
12
src/settings.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/** User-facing toggles, persisted in chrome.storage.sync. */
|
||||
export interface Settings {
|
||||
/** Master switch for the green/red diff layer. */
|
||||
enabled: boolean;
|
||||
/** Layer language-aware syntax highlighting on top of the diff colors. */
|
||||
syntax: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
enabled: true,
|
||||
syntax: true,
|
||||
};
|
||||
82
src/styles/diff.css
Normal file
82
src/styles/diff.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/* Diff layout + line colors. Everything is scoped under `pre.dh-on` so toggling
|
||||
the extension off (which removes that class) fully restores the plain view. */
|
||||
|
||||
pre.dh-on {
|
||||
margin: 0;
|
||||
padding: 12px 0;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
tab-size: 4;
|
||||
font: 12px/1.5 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
||||
"Liberation Mono", monospace;
|
||||
background: #ffffff;
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
pre.dh-on .dh-line {
|
||||
display: block;
|
||||
white-space: pre;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
pre.dh-on .dh-gutter {
|
||||
display: inline-block;
|
||||
width: 2ch;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
pre.dh-on .dh-add {
|
||||
background: #e6ffec;
|
||||
}
|
||||
pre.dh-on .dh-del {
|
||||
background: #ffebe9;
|
||||
}
|
||||
pre.dh-on .dh-add .dh-gutter {
|
||||
color: #1a7f37;
|
||||
opacity: 1;
|
||||
}
|
||||
pre.dh-on .dh-del .dh-gutter {
|
||||
color: #cf222e;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
pre.dh-on .dh-hunk {
|
||||
color: #8250df;
|
||||
background: #f1eeff;
|
||||
}
|
||||
pre.dh-on .dh-file {
|
||||
color: #57606a;
|
||||
font-weight: 600;
|
||||
}
|
||||
pre.dh-on .dh-meta {
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
pre.dh-on {
|
||||
background: #0d1117;
|
||||
color: #e6edf3;
|
||||
}
|
||||
pre.dh-on .dh-add {
|
||||
background: rgba(46, 160, 67, 0.15);
|
||||
}
|
||||
pre.dh-on .dh-del {
|
||||
background: rgba(248, 81, 73, 0.15);
|
||||
}
|
||||
pre.dh-on .dh-add .dh-gutter {
|
||||
color: #3fb950;
|
||||
}
|
||||
pre.dh-on .dh-del .dh-gutter {
|
||||
color: #f85149;
|
||||
}
|
||||
pre.dh-on .dh-hunk {
|
||||
color: #d2a8ff;
|
||||
background: rgba(56, 139, 253, 0.1);
|
||||
}
|
||||
pre.dh-on .dh-file,
|
||||
pre.dh-on .dh-meta {
|
||||
color: #8b949e;
|
||||
}
|
||||
}
|
||||
103
src/styles/hljs-theme.css
Normal file
103
src/styles/hljs-theme.css
Normal file
@@ -0,0 +1,103 @@
|
||||
/* Compact highlight.js token theme (GitHub-like), light + dark. Scoped to the
|
||||
diff view; the diff line background sits behind these foreground colors. */
|
||||
|
||||
pre.dh-on .hljs-comment,
|
||||
pre.dh-on .hljs-quote {
|
||||
color: #6e7781;
|
||||
font-style: italic;
|
||||
}
|
||||
pre.dh-on .hljs-keyword,
|
||||
pre.dh-on .hljs-selector-tag,
|
||||
pre.dh-on .hljs-literal,
|
||||
pre.dh-on .hljs-doctag,
|
||||
pre.dh-on .hljs-meta .hljs-keyword {
|
||||
color: #cf222e;
|
||||
}
|
||||
pre.dh-on .hljs-string,
|
||||
pre.dh-on .hljs-regexp,
|
||||
pre.dh-on .hljs-addition,
|
||||
pre.dh-on .hljs-symbol,
|
||||
pre.dh-on .hljs-bullet,
|
||||
pre.dh-on .hljs-link {
|
||||
color: #0a3069;
|
||||
}
|
||||
pre.dh-on .hljs-number,
|
||||
pre.dh-on .hljs-variable,
|
||||
pre.dh-on .hljs-template-variable,
|
||||
pre.dh-on .hljs-attr,
|
||||
pre.dh-on .hljs-attribute,
|
||||
pre.dh-on .hljs-property {
|
||||
color: #0550ae;
|
||||
}
|
||||
pre.dh-on .hljs-title,
|
||||
pre.dh-on .hljs-title.function_,
|
||||
pre.dh-on .hljs-section {
|
||||
color: #8250df;
|
||||
}
|
||||
pre.dh-on .hljs-type,
|
||||
pre.dh-on .hljs-class .hljs-title,
|
||||
pre.dh-on .hljs-title.class_,
|
||||
pre.dh-on .hljs-built_in {
|
||||
color: #953800;
|
||||
}
|
||||
pre.dh-on .hljs-name,
|
||||
pre.dh-on .hljs-tag {
|
||||
color: #116329;
|
||||
}
|
||||
pre.dh-on .hljs-meta {
|
||||
color: #6e7781;
|
||||
}
|
||||
pre.dh-on .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
pre.dh-on .hljs-strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
pre.dh-on .hljs-comment,
|
||||
pre.dh-on .hljs-quote {
|
||||
color: #8b949e;
|
||||
}
|
||||
pre.dh-on .hljs-keyword,
|
||||
pre.dh-on .hljs-selector-tag,
|
||||
pre.dh-on .hljs-literal,
|
||||
pre.dh-on .hljs-doctag,
|
||||
pre.dh-on .hljs-meta .hljs-keyword {
|
||||
color: #ff7b72;
|
||||
}
|
||||
pre.dh-on .hljs-string,
|
||||
pre.dh-on .hljs-regexp,
|
||||
pre.dh-on .hljs-addition,
|
||||
pre.dh-on .hljs-symbol,
|
||||
pre.dh-on .hljs-bullet,
|
||||
pre.dh-on .hljs-link {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
pre.dh-on .hljs-number,
|
||||
pre.dh-on .hljs-variable,
|
||||
pre.dh-on .hljs-template-variable,
|
||||
pre.dh-on .hljs-attr,
|
||||
pre.dh-on .hljs-attribute,
|
||||
pre.dh-on .hljs-property {
|
||||
color: #79c0ff;
|
||||
}
|
||||
pre.dh-on .hljs-title,
|
||||
pre.dh-on .hljs-title.function_,
|
||||
pre.dh-on .hljs-section {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
pre.dh-on .hljs-type,
|
||||
pre.dh-on .hljs-class .hljs-title,
|
||||
pre.dh-on .hljs-title.class_,
|
||||
pre.dh-on .hljs-built_in {
|
||||
color: #ffa657;
|
||||
}
|
||||
pre.dh-on .hljs-name,
|
||||
pre.dh-on .hljs-tag {
|
||||
color: #7ee787;
|
||||
}
|
||||
pre.dh-on .hljs-meta {
|
||||
color: #8b949e;
|
||||
}
|
||||
}
|
||||
75
src/styles/popup.css
Normal file
75
src/styles/popup.css
Normal file
@@ -0,0 +1,75 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 260px;
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
font: 13px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
background: #ffffff;
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 7px 0;
|
||||
}
|
||||
|
||||
.row .label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.row.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.switch {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
accent-color: #1a7f37;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 10px 0 0;
|
||||
font-size: 11px;
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 10px;
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #d0d7de;
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #1c2128;
|
||||
color: #e6edf3;
|
||||
}
|
||||
.hint {
|
||||
color: #8b949e;
|
||||
}
|
||||
kbd {
|
||||
background: #2d333b;
|
||||
border-color: #444c56;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user