Model graph: dark/light theme toggle on the page
- sun/moon button next to Export; the choice is remembered in the page state and written to the archify-theme key the diagram pages use - explicit dark and light token sets; without a choice the page follows VS Code's theme as before - Cytoscape restyles from the page tokens whenever the theme changes
This commit is contained in:
+70
-14
@@ -18,6 +18,17 @@ interface SavedView {
|
||||
query?: string;
|
||||
labels?: boolean;
|
||||
hidden?: string[];
|
||||
/** A theme chosen on the page; absent means follow VS Code. */
|
||||
theme?: 'dark' | 'light';
|
||||
}
|
||||
|
||||
type Theme = 'dark' | 'light';
|
||||
|
||||
/** The theme in effect: the page's own choice, else VS Code's. */
|
||||
function currentTheme(): Theme {
|
||||
const chosen = document.documentElement.dataset.theme;
|
||||
if (chosen === 'dark' || chosen === 'light') return chosen;
|
||||
return document.body.classList.contains('vscode-light') ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
cytoscape.use(fcose);
|
||||
@@ -56,17 +67,17 @@ function cssVar(name: string, fallback: string): string {
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
/** Theme colours Cytoscape needs as literals; read again when VS Code switches theme. */
|
||||
/** Theme colours Cytoscape needs as literals: the page's tokens, read again whenever the theme changes. */
|
||||
function palette() {
|
||||
const dark = !document.body.classList.contains('vscode-light');
|
||||
const dark = currentTheme() === 'dark';
|
||||
return {
|
||||
bg: cssVar('--vscode-editor-background', dark ? '#0b1020' : '#ffffff'),
|
||||
fg: cssVar('--vscode-foreground', dark ? '#e5e7eb' : '#1f2937'),
|
||||
muted: cssVar('--vscode-descriptionForeground', dark ? '#9ca3af' : '#6b7280'),
|
||||
panel: cssVar('--vscode-editorWidget-background', dark ? '#131c31' : '#f3f4f6'),
|
||||
accent: cssVar('--vscode-textLink-foreground', '#22d3ee'),
|
||||
bg: cssVar('--ag-bg', dark ? '#0b1020' : '#ffffff'),
|
||||
fg: cssVar('--ag-fg', dark ? '#e5e7eb' : '#1f2937'),
|
||||
muted: cssVar('--ag-muted', dark ? '#9ca3af' : '#6b7280'),
|
||||
panel: cssVar('--ag-panel', dark ? '#131c31' : '#f3f4f6'),
|
||||
accent: cssVar('--ag-accent', '#22d3ee'),
|
||||
edge: dark ? '#64748b' : '#94a3b8',
|
||||
font: cssVar('--vscode-font-family', 'system-ui, sans-serif'),
|
||||
font: cssVar('--ag-font', 'system-ui, sans-serif'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,6 +97,7 @@ class GraphPage {
|
||||
private readonly queryInput = el('input', 'ag-query');
|
||||
private readonly countEl = el('span', 'ag-count');
|
||||
private readonly labelsButton = el('button', 'ag-button', 'Labels');
|
||||
private readonly themeButton = el('button', 'ag-button ag-theme');
|
||||
private readonly legend = el('div', 'ag-legend');
|
||||
private readonly side = el('aside', 'ag-side');
|
||||
private readonly main = el('div', 'ag-main');
|
||||
@@ -98,6 +110,7 @@ class GraphPage {
|
||||
if (initial.query !== undefined) this.saved.query = initial.query;
|
||||
if (this.saved.labels === undefined) this.saved.labels = initial.graph.edges.length <= LABELS_UP_TO;
|
||||
this.hiddenTypes = new Set(this.saved.hidden ?? []);
|
||||
this.applyTheme(this.saved.theme ?? archifyThemePreference());
|
||||
this.index();
|
||||
this.buildChrome();
|
||||
this.buildGraph(true);
|
||||
@@ -177,7 +190,10 @@ class GraphPage {
|
||||
exportButton.type = 'button';
|
||||
exportButton.title = 'Save the graph as it is on screen, as a PNG';
|
||||
exportButton.addEventListener('click', () => this.exportPng());
|
||||
actions.append(sw, exportButton);
|
||||
this.themeButton.type = 'button';
|
||||
this.themeButton.addEventListener('click', () => this.setTheme(currentTheme() === 'dark' ? 'light' : 'dark'));
|
||||
this.syncThemeButton();
|
||||
actions.append(sw, this.themeButton, exportButton);
|
||||
top.append(titles, actions);
|
||||
|
||||
const bar = el('div', 'ag-bar');
|
||||
@@ -311,6 +327,39 @@ class GraphPage {
|
||||
.run();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- theme
|
||||
|
||||
/** Sets the page's own theme (or follows VS Code when undefined) without touching the saved choice. */
|
||||
private applyTheme(theme: Theme | undefined): void {
|
||||
if (theme) document.documentElement.dataset.theme = theme;
|
||||
else delete document.documentElement.dataset.theme;
|
||||
}
|
||||
|
||||
private setTheme(theme: Theme): void {
|
||||
this.applyTheme(theme);
|
||||
this.saved.theme = theme;
|
||||
this.save();
|
||||
try {
|
||||
localStorage.setItem('archify-theme', theme); // the diagram pages read the same key
|
||||
} catch {
|
||||
// storage may be unavailable; the page state keeps the choice
|
||||
}
|
||||
this.restyle();
|
||||
}
|
||||
|
||||
private syncThemeButton(): void {
|
||||
const dark = currentTheme() === 'dark';
|
||||
this.themeButton.textContent = dark ? '\u263e' : '\u2600';
|
||||
this.themeButton.title = dark ? 'Switch to the light theme' : 'Switch to the dark theme';
|
||||
this.themeButton.setAttribute('aria-label', this.themeButton.title);
|
||||
}
|
||||
|
||||
private restyle(): void {
|
||||
this.cy?.style(this.stylesheet());
|
||||
this.syncLabels();
|
||||
this.syncThemeButton();
|
||||
}
|
||||
|
||||
private applyHidden(): void {
|
||||
this.cy.nodes().forEach((n) => {
|
||||
n.toggleClass('hidden', this.hiddenTypes.has(String(n.data('type') || 'other')));
|
||||
@@ -639,10 +688,7 @@ class GraphPage {
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', () => this.cy.resize());
|
||||
new MutationObserver(() => {
|
||||
this.cy.style(this.stylesheet());
|
||||
this.syncLabels();
|
||||
}).observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
new MutationObserver(() => this.restyle()).observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
||||
window.addEventListener('message', (event) => {
|
||||
const message = event.data || {};
|
||||
if (message.type === 'graph' && message.graph) this.replace(message.graph as ModelGraph);
|
||||
@@ -651,7 +697,17 @@ class GraphPage {
|
||||
}
|
||||
|
||||
private save(): void {
|
||||
this.host.setState({ graphView: { query: this.saved.query, labels: this.saved.labels, hidden: this.saved.hidden ?? [] } });
|
||||
this.host.setState({ graphView: { query: this.saved.query, labels: this.saved.labels, hidden: this.saved.hidden ?? [], theme: this.saved.theme } });
|
||||
}
|
||||
}
|
||||
|
||||
/** The theme the Archify diagram pages last used, when they stored one. */
|
||||
function archifyThemePreference(): Theme | undefined {
|
||||
try {
|
||||
const value = localStorage.getItem('archify-theme');
|
||||
return value === 'dark' || value === 'light' ? value : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user