2026-04-28 18:17:32 -07:00
|
|
|
/*
|
|
|
|
|
* theme-manager.ts — manages theme registration and activation for a Ribbit instance.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import type { RibbitTheme } from './types';
|
|
|
|
|
|
|
|
|
|
export class ThemeManager {
|
|
|
|
|
private registered: Map<string, RibbitTheme>;
|
|
|
|
|
private disabled: Set<string>;
|
|
|
|
|
private active: RibbitTheme;
|
|
|
|
|
private themeLink: HTMLLinkElement | null;
|
|
|
|
|
private themesPath: string;
|
2026-04-28 18:35:06 -07:00
|
|
|
private onSwitch: (theme: RibbitTheme, previous: RibbitTheme) => void;
|
2026-04-28 18:17:32 -07:00
|
|
|
|
2026-04-28 18:35:06 -07:00
|
|
|
constructor(initial: RibbitTheme, themesPath: string, onSwitch: (theme: RibbitTheme, previous: RibbitTheme) => void) {
|
2026-04-28 18:17:32 -07:00
|
|
|
this.registered = new Map();
|
|
|
|
|
this.disabled = new Set();
|
|
|
|
|
this.themeLink = null;
|
|
|
|
|
this.themesPath = themesPath;
|
|
|
|
|
this.onSwitch = onSwitch;
|
|
|
|
|
this.active = initial;
|
|
|
|
|
this.add(initial);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Register a theme. Themes must be added before they can be enabled.
|
|
|
|
|
*/
|
|
|
|
|
add(theme: RibbitTheme): void {
|
|
|
|
|
this.registered.set(theme.name, theme);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unregister a theme by name. Cannot remove the active theme.
|
|
|
|
|
*/
|
|
|
|
|
remove(name: string): void {
|
|
|
|
|
if (this.active.name === name) {
|
|
|
|
|
throw new Error(`Cannot remove the active theme "${name}".`);
|
|
|
|
|
}
|
|
|
|
|
this.registered.delete(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the names of all registered and enabled themes.
|
|
|
|
|
*/
|
|
|
|
|
list(): string[] {
|
|
|
|
|
return Array.from(this.registered.keys()).filter(name => !this.disabled.has(name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get a registered theme by name, or undefined if not found.
|
|
|
|
|
*/
|
|
|
|
|
get(name: string): RibbitTheme | undefined {
|
|
|
|
|
return this.registered.get(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return the currently active theme.
|
|
|
|
|
*/
|
|
|
|
|
current(): RibbitTheme {
|
|
|
|
|
return this.active;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Switch to a registered theme by name. The theme must be
|
|
|
|
|
* registered and enabled. Loads the theme's CSS and notifies
|
|
|
|
|
* the editor to rebuild its converter.
|
|
|
|
|
*/
|
|
|
|
|
set(name: string): void {
|
|
|
|
|
const theme = this.registered.get(name);
|
|
|
|
|
if (!theme) {
|
|
|
|
|
throw new Error(`Theme "${name}" is not registered. Call add() first.`);
|
|
|
|
|
}
|
|
|
|
|
if (this.disabled.has(name)) {
|
|
|
|
|
throw new Error(`Theme "${name}" is disabled. Call enable() first.`);
|
|
|
|
|
}
|
2026-04-28 18:35:06 -07:00
|
|
|
const previous = this.active;
|
2026-04-28 18:17:32 -07:00
|
|
|
this.active = theme;
|
2026-04-29 11:12:45 -07:00
|
|
|
// Only load CSS when switching themes, not on initial set
|
|
|
|
|
if (previous !== theme) {
|
|
|
|
|
this.loadCSS(name);
|
|
|
|
|
}
|
2026-04-28 18:35:06 -07:00
|
|
|
this.onSwitch(theme, previous);
|
2026-04-28 18:17:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark a theme as available for selection via set().
|
|
|
|
|
* Themes are enabled by default when added.
|
|
|
|
|
*/
|
|
|
|
|
enable(name: string): void {
|
|
|
|
|
if (!this.registered.has(name)) {
|
|
|
|
|
throw new Error(`Theme "${name}" is not registered. Call add() first.`);
|
|
|
|
|
}
|
|
|
|
|
this.disabled.delete(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mark a theme as unavailable for selection via set().
|
|
|
|
|
* Does not affect the current theme if it is already active.
|
|
|
|
|
*/
|
|
|
|
|
disable(name: string): void {
|
|
|
|
|
if (!this.registered.has(name)) {
|
|
|
|
|
throw new Error(`Theme "${name}" is not registered.`);
|
|
|
|
|
}
|
|
|
|
|
this.disabled.add(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private loadCSS(name: string): void {
|
|
|
|
|
if (this.themeLink) {
|
|
|
|
|
this.themeLink.remove();
|
|
|
|
|
}
|
|
|
|
|
const link = document.createElement('link');
|
|
|
|
|
link.rel = 'stylesheet';
|
|
|
|
|
link.href = `${this.themesPath}/${name}/theme.css`;
|
|
|
|
|
document.head.appendChild(link);
|
|
|
|
|
this.themeLink = link;
|
|
|
|
|
}
|
|
|
|
|
}
|