🌿 Collaborative wiki on ATProto lichen.wiki
atproto
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add theming for codemirror

juprodh a0cc4dae cadddb76

+102 -52
+32 -4
public/editor/editor.ts
··· 1 1 import { markdown } from "@codemirror/lang-markdown"; 2 2 import { EditorState } from "@codemirror/state"; 3 3 import { basicSetup, EditorView } from "codemirror"; 4 - import { THEME_HEX } from "../../src/views/theme/index.ts"; 5 4 import { renderPreview } from "./preview.ts"; 6 5 import { createToolbar } from "./toolbar.ts"; 7 6 import { showToast, syncBlobMetadata, uploadImage } from "./upload.ts"; 8 7 9 8 let activeView: EditorView | null = null; 10 9 10 + const themedEditor = EditorView.theme({ 11 + "&": { 12 + backgroundColor: "var(--surface)", 13 + color: "var(--text)", 14 + }, 15 + ".cm-content": { caretColor: "var(--text)" }, 16 + ".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--text)" }, 17 + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": 18 + { backgroundColor: "var(--accent-soft)" }, 19 + ".cm-activeLine": { backgroundColor: "var(--bg)" }, 20 + ".cm-activeLineGutter": { 21 + backgroundColor: "var(--bg)", 22 + color: "var(--text-secondary)", 23 + }, 24 + ".cm-gutters": { 25 + backgroundColor: "var(--bg)", 26 + color: "var(--text-muted)", 27 + borderRight: "1px solid var(--border-subtle)", 28 + }, 29 + ".cm-lineNumbers .cm-gutterElement": { color: "var(--text-muted)" }, 30 + ".cm-tooltip": { 31 + backgroundColor: "var(--surface)", 32 + color: "var(--text)", 33 + border: "1px solid var(--border)", 34 + }, 35 + }); 36 + 11 37 function initEditor(root: Document | Element = document): void { 12 38 const textarea = root.querySelector<HTMLTextAreaElement>("textarea#content"); 13 39 if (!textarea) return; ··· 30 56 editorPane.style.minWidth = "0"; 31 57 editorPane.style.minHeight = "0"; 32 58 editorPane.style.overflow = "hidden"; 33 - editorPane.style.border = `1px solid ${THEME_HEX.paneBorder}`; 59 + editorPane.style.border = "1px solid var(--border-input)"; 34 60 editorPane.style.borderRadius = "0.375rem"; 35 61 editorPane.style.display = "flex"; 36 62 editorPane.style.flexDirection = "column"; ··· 41 67 preview.style.minWidth = "0"; 42 68 preview.style.overflow = "auto"; 43 69 preview.style.padding = "0.75rem"; 44 - preview.style.border = `1px solid ${THEME_HEX.paneBorder}`; 70 + preview.style.border = "1px solid var(--border-input)"; 45 71 preview.style.borderRadius = "0.375rem"; 46 - preview.style.backgroundColor = THEME_HEX.previewBg; 72 + preview.style.backgroundColor = "var(--surface)"; 73 + preview.style.color = "var(--text)"; 47 74 48 75 wrapper.appendChild(editorPane); 49 76 wrapper.appendChild(preview); ··· 82 109 extensions: [ 83 110 basicSetup, 84 111 markdown(), 112 + themedEditor, 85 113 EditorView.lineWrapping, 86 114 EditorView.updateListener.of((update) => { 87 115 if (update.docChanged) {
+5 -5
public/editor/toolbar.ts
··· 1 1 import type { EditorView } from "codemirror"; 2 - import { THEME_HEX } from "../../src/views/theme/index.ts"; 3 2 4 3 function wrapSelection(view: EditorView, before: string, after: string): void { 5 4 const { from, to } = view.state.selection.main; ··· 78 77 toolbar.style.display = "flex"; 79 78 toolbar.style.gap = "2px"; 80 79 toolbar.style.padding = "4px 8px"; 81 - toolbar.style.backgroundColor = THEME_HEX.toolbarBg; 82 - toolbar.style.borderBottom = `1px solid ${THEME_HEX.toolbarBorder}`; 80 + toolbar.style.backgroundColor = "var(--bg)"; 81 + toolbar.style.borderBottom = "1px solid var(--border-input)"; 83 82 toolbar.style.flexShrink = "0"; 84 83 85 84 for (const btn of buttons) { ··· 88 87 el.textContent = btn.label; 89 88 el.title = btn.title; 90 89 el.style.padding = "2px 8px"; 91 - el.style.border = `1px solid ${THEME_HEX.buttonBorder}`; 90 + el.style.border = "1px solid var(--border-input)"; 92 91 el.style.borderRadius = "3px"; 93 - el.style.backgroundColor = THEME_HEX.buttonBg; 92 + el.style.backgroundColor = "var(--surface)"; 93 + el.style.color = "var(--text)"; 94 94 el.style.cursor = "pointer"; 95 95 el.style.fontSize = "14px"; 96 96 el.style.lineHeight = "1.5";
+21 -10
public/viz/renderers/force-graph.ts
··· 1 1 declare const d3: typeof import("d3"); 2 2 3 3 import type { ForceGraphNode as BaseForceGraphNode } from "../../../src/shared/viz-types.ts"; 4 - import { THEME_HEX } from "../../../src/views/theme/index.ts"; 5 4 6 5 type ForceGraphNode = BaseForceGraphNode & d3.SimulationNodeDatum; 7 6 ··· 16 15 links: ForceGraphLink[]; 17 16 } 18 17 19 - const GROUP_COLORS = THEME_HEX.graphGroupColors; 18 + function readVar(node: Element, name: string): string { 19 + return getComputedStyle(node).getPropertyValue(name).trim(); 20 + } 21 + 22 + function readVarList(node: Element, name: string): string[] { 23 + return readVar(node, name) 24 + .split(",") 25 + .map((s) => s.trim()) 26 + .filter(Boolean); 27 + } 20 28 21 29 export function renderForceGraph( 22 30 container: HTMLElement, ··· 25 33 const width = container.clientWidth || 800; 26 34 const height = 500; 27 35 36 + const textColor = readVar(container, "--text-secondary"); 37 + const linkStroke = readVar(container, "--text-muted"); 38 + const nodeStroke = readVar(container, "--surface"); 39 + const defaultColor = readVar(container, "--accent"); 40 + const groupColors = readVarList(container, "--viz-group-colors"); 41 + 28 42 const groups = [...new Set(data.nodes.map((n) => n.group ?? "default"))]; 29 43 const colorMap = new Map( 30 44 groups.map((g, i) => [ 31 45 g, 32 - GROUP_COLORS[i % GROUP_COLORS.length] ?? THEME_HEX.graphDefaultColor, 46 + groupColors[i % groupColors.length] ?? defaultColor, 33 47 ]), 34 48 ); 35 49 ··· 55 69 56 70 const link = svg 57 71 .append("g") 58 - .attr("stroke", THEME_HEX.graphLinkStroke) 72 + .attr("stroke", linkStroke) 59 73 .attr("stroke-opacity", 0.6) 60 74 .selectAll("line") 61 75 .data(data.links) ··· 68 82 .data(data.nodes) 69 83 .join("circle") 70 84 .attr("r", 8) 71 - .attr( 72 - "fill", 73 - (d) => colorMap.get(d.group ?? "default") ?? THEME_HEX.graphDefaultColor, 74 - ) 75 - .attr("stroke", THEME_HEX.graphNodeStroke) 85 + .attr("fill", (d) => colorMap.get(d.group ?? "default") ?? defaultColor) 86 + .attr("stroke", nodeStroke) 76 87 .attr("stroke-width", 1.5) 77 88 .call(drag(simulation)); 78 89 ··· 85 96 .attr("font-size", 11) 86 97 .attr("dx", 12) 87 98 .attr("dy", 4) 88 - .attr("fill", THEME_HEX.vizText); 99 + .attr("fill", textColor); 89 100 90 101 node.append("title").text((d) => d.label ?? d.id); 91 102
+15 -4
public/viz/renderers/sunburst.ts
··· 4 4 SunburstData, 5 5 SunburstNode, 6 6 } from "../../../src/shared/viz-types.ts"; 7 - import { THEME_HEX } from "../../../src/views/theme/index.ts"; 7 + 8 + function readVar(node: Element, name: string): string { 9 + return getComputedStyle(node).getPropertyValue(name).trim(); 10 + } 8 11 9 - const COLORS = d3.schemeTableau10; 12 + function readVarList(node: Element, name: string): string[] { 13 + return readVar(node, name) 14 + .split(",") 15 + .map((s) => s.trim()) 16 + .filter(Boolean); 17 + } 10 18 11 19 export function renderSunburst( 12 20 container: HTMLElement, ··· 16 24 const height = Math.min(width, 500); 17 25 const radius = height / 2; 18 26 27 + const textColor = readVar(container, "--text-secondary"); 28 + const palette = readVarList(container, "--viz-group-colors"); 29 + 19 30 const hierarchy = d3 20 31 .hierarchy<SunburstNode>(data.root) 21 32 .sum((d) => d.value ?? 1) ··· 28 39 const color = d3 29 40 .scaleOrdinal<string>() 30 41 .domain(root.children?.map((d) => d.data.name) ?? []) 31 - .range(COLORS); 42 + .range(palette); 32 43 33 44 const arc = d3 34 45 .arc<d3.HierarchyRectangularNode<SunburstNode>>() ··· 72 83 .append("g") 73 84 .attr("text-anchor", "middle") 74 85 .attr("font-size", 10) 75 - .attr("fill", THEME_HEX.vizText) 86 + .attr("fill", textColor) 76 87 .selectAll("text") 77 88 .data(root.descendants().filter((d) => d.depth && d.x1 - d.x0 > 0.1)) 78 89 .join("text")
+8 -2
src/views/theme/apply.ts
··· 11 11 12 12 function themeVars(theme: Theme): string { 13 13 return Object.entries(theme) 14 - .map(([k, v]) => `--${kebab(k)}: ${v};`) 14 + .map(([k, v]) => { 15 + const value = Array.isArray(v) ? v.join(", ") : (v as string); 16 + return `--${kebab(k)}: ${value};`; 17 + }) 15 18 .join(" "); 16 19 } 17 20 ··· 28 31 29 32 function themeStyleAttr(theme: Theme): string { 30 33 return Object.entries(theme) 31 - .map(([k, v]) => `--${kebab(k)}: ${v}`) 34 + .map(([k, v]) => { 35 + const value = Array.isArray(v) ? v.join(", ") : (v as string); 36 + return `--${kebab(k)}: ${value}`; 37 + }) 32 38 .join("; "); 33 39 } 34 40
-1
src/views/theme/index.ts
··· 13 13 sidebarLinkClass, 14 14 successBanner, 15 15 THEME, 16 - THEME_HEX, 17 16 } from "./tokens.ts";
+21
src/views/theme/themes.ts
··· 19 19 dangerText: string; 20 20 dangerAction: string; 21 21 dangerActionHover: string; 22 + vizGroupColors: readonly string[]; 22 23 }; 23 24 24 25 export const themes = { ··· 43 44 dangerText: "#b91c1c", 44 45 dangerAction: "#b91c1c", 45 46 dangerActionHover: "#991b1b", 47 + vizGroupColors: [ 48 + "#0f766e", 49 + "#b45309", 50 + "#047857", 51 + "#dc2626", 52 + "#7c3aed", 53 + "#0e7490", 54 + "#c2410c", 55 + "#be185d", 56 + ], 46 57 }, 47 58 dark: { 48 59 text: "#fafaf9", ··· 65 76 dangerText: "#fca5a5", 66 77 dangerAction: "#b91c1c", 67 78 dangerActionHover: "#dc2626", 79 + vizGroupColors: [ 80 + "#2dd4bf", 81 + "#fbbf24", 82 + "#34d399", 83 + "#f87171", 84 + "#a78bfa", 85 + "#22d3ee", 86 + "#fb923c", 87 + "#f472b6", 88 + ], 68 89 }, 69 90 } as const satisfies Record<string, Theme>;
-26
src/views/theme/tokens.ts
··· 56 56 if (!message) return ""; 57 57 return `<div class="mb-4 p-3 bg-[var(--accent-soft)] border border-[var(--accent-soft-border)] text-[var(--accent-hover)] text-sm rounded">${escapeHtml(message)}</div>`; 58 58 } 59 - 60 - export const THEME_HEX = { 61 - // Editor 62 - toolbarBg: "#f5f5f4", 63 - toolbarBorder: "#d6d3d1", 64 - buttonBg: "#ffffff", 65 - buttonBorder: "#d6d3d1", 66 - paneBorder: "#d6d3d1", 67 - previewBg: "#ffffff", 68 - 69 - // Viz 70 - vizText: "#44403c", 71 - graphLinkStroke: "#a8a29e", 72 - graphNodeStroke: "#ffffff", 73 - graphDefaultColor: "#0f766e", 74 - graphGroupColors: [ 75 - "#0f766e", 76 - "#b45309", 77 - "#047857", 78 - "#dc2626", 79 - "#7c3aed", 80 - "#0e7490", 81 - "#c2410c", 82 - "#be185d", 83 - ], 84 - } as const;