|
|
|
|
import { Context } from 'runed';
|
|
|
|
|
import type {
|
|
|
|
|
ActiveDom,
|
|
|
|
|
ActiveDomStyleHost,
|
|
|
|
|
Breakpoint,
|
|
|
|
|
DomAttrValue,
|
|
|
|
|
ResponsiveProp
|
|
|
|
|
} from '$adom';
|
|
|
|
|
import type { ActivePrefs } from '$prefs';
|
|
|
|
|
import type { Density } from '$libs/density';
|
|
|
|
|
import type { ThemeEffective } from '$libs/theme';
|
|
|
|
|
import { matches } from '$libs/errs';
|
|
|
|
|
import { getActiveUix, type ActiveUix } from '$active-uix';
|
|
|
|
|
import type { ActiveLangs } from '$langs';
|
|
|
|
|
import type { ActiveFormat } from '$format';
|
|
|
|
|
import { UIX_ERR_DOM_DISABLED } from '$active-uix/errors';
|
|
|
|
|
import {
|
|
|
|
|
assertValidEidosConfig,
|
|
|
|
|
createEidosConfigDocumentFromConfig,
|
|
|
|
|
getEidosColorRoleScale,
|
|
|
|
|
getEidosColorScale,
|
|
|
|
|
getEidosCssContract,
|
|
|
|
|
getEidosRecipeTokens,
|
|
|
|
|
getEidosTheme,
|
|
|
|
|
listEidosColorRoles,
|
|
|
|
|
listEidosColorScales,
|
|
|
|
|
listEidosRecipes,
|
|
|
|
|
listEidosThemes,
|
|
|
|
|
readEidosConfigFromDocument,
|
|
|
|
|
serializeEidosConfig,
|
|
|
|
|
snapshotEidosConfig,
|
|
|
|
|
validateEidosConfig
|
|
|
|
|
} from './lib/config';
|
|
|
|
|
import { isEidosConfigDocumentLike, type EidosConfigDocument } from './lib/persistence';
|
|
|
|
|
import {
|
|
|
|
|
renderContractCss as renderEidosContractCss,
|
|
|
|
|
renderCssVariables as renderEidosCssVariables,
|
|
|
|
|
renderStaticCss as renderEidosStaticCss,
|
|
|
|
|
renderThemeCss as renderEidosThemeCss,
|
|
|
|
|
type RenderContractCssOptions,
|
|
|
|
|
type RenderCssVariablesOptions,
|
|
|
|
|
type RenderThemeCssOptions
|
|
|
|
|
} from './lib/render-css';
|
|
|
|
|
import { createThemeBaseEidosConfig } from './lib/themes/base';
|
|
|
|
|
import type { EidosConfigPatch } from './lib/options';
|
|
|
|
|
import type {
|
|
|
|
|
ColorRole,
|
|
|
|
|
ColorScale,
|
|
|
|
|
EidosCssContract,
|
|
|
|
|
EidosCssVariableMap,
|
|
|
|
|
EidosValidationReport,
|
|
|
|
|
EidosConfig,
|
|
|
|
|
EidosConfigSnapshot,
|
|
|
|
|
RecipeTokenMap,
|
|
|
|
|
ThemeDefinition
|
|
|
|
|
} from './lib/config-types';
|
|
|
|
|
import { ActiveEidosConfigError, ActiveEidosNoContextError } from './errors';
|
|
|
|
|
|
|
|
|
|
export { ActiveEidosConfigError, ActiveEidosNoContextError } from './errors';
|
|
|
|
|
|
|
|
|
|
export interface ActiveEidosThemeContext {
|
|
|
|
|
readonly theme: string;
|
|
|
|
|
readonly mode: ThemeEffective;
|
|
|
|
|
readonly density: Density;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ActiveEidosThemeResolver = (
|
|
|
|
|
context: ActiveEidosThemeContext,
|
|
|
|
|
active: ActiveEidos
|
|
|
|
|
) => string;
|
|
|
|
|
|
|
|
|
|
export interface ActiveEidosPreferenceSource {
|
|
|
|
|
getTheme(): string;
|
|
|
|
|
getMode(): ThemeEffective;
|
|
|
|
|
getDensity(): Density;
|
|
|
|
|
onPreferenceChange(handler: () => void): () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ActiveEidosValueSource<T> {
|
|
|
|
|
get(): T;
|
|
|
|
|
onChange(handler: (value: T) => void): () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ActiveEidosLastAttrs {
|
|
|
|
|
readonly target: HTMLElement;
|
|
|
|
|
readonly themeId: string;
|
|
|
|
|
readonly mode: ThemeEffective;
|
|
|
|
|
readonly density: Density;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ActiveEidosStyleHost = ActiveDomStyleHost;
|
|
|
|
|
export type ActiveEidosThemeSource = 'auto' | 'config' | 'css';
|
|
|
|
|
export type ActiveEidosCssVariablesOptions = Omit<RenderCssVariablesOptions, 'contract'>;
|
|
|
|
|
|
|
|
|
|
export interface ActiveEidosOptions {
|
|
|
|
|
readonly config?: EidosConfig | EidosConfigDocument;
|
|
|
|
|
readonly themeBase?: EidosConfigPatch;
|
|
|
|
|
readonly uix?: ActiveUix;
|
|
|
|
|
readonly prefs?: ActivePrefs;
|
|
|
|
|
readonly langs?: ActiveLangs;
|
|
|
|
|
readonly format?: ActiveFormat;
|
|
|
|
|
readonly preferences?: ActiveEidosPreferenceSource;
|
|
|
|
|
readonly theme?: string;
|
|
|
|
|
readonly mode?: ThemeEffective;
|
|
|
|
|
readonly density?: Density;
|
|
|
|
|
readonly modeSource?: ActiveEidosValueSource<ThemeEffective>;
|
|
|
|
|
readonly densitySource?: ActiveEidosValueSource<Density>;
|
|
|
|
|
readonly dom?: ActiveDom;
|
|
|
|
|
readonly applyDom?: boolean;
|
|
|
|
|
readonly styleId?: string;
|
|
|
|
|
readonly styleHost?: ActiveEidosStyleHost;
|
|
|
|
|
readonly themeSource?: ActiveEidosThemeSource;
|
|
|
|
|
readonly cssVariables?: EidosCssVariableMap;
|
|
|
|
|
readonly cssVariablesSelector?: string;
|
|
|
|
|
readonly cssVariablesStrict?: boolean;
|
|
|
|
|
readonly themeResolver?: ActiveEidosThemeResolver;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ActiveEidosUserOptions = Omit<
|
|
|
|
|
ActiveEidosOptions,
|
|
|
|
|
'uix' | 'prefs' | 'langs' | 'format' | 'dom'
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
const DEFAULT_STYLE_ID = 'uix-eidos';
|
|
|
|
|
const DEFAULT_THEME = 'base';
|
|
|
|
|
const DEFAULT_MODE: ThemeEffective = 'light';
|
|
|
|
|
const DEFAULT_DENSITY: Density = 'comfortable';
|
|
|
|
|
const EIDOS_THEME_ATTR = 'data-theme';
|
|
|
|
|
const EIDOS_MODE_ATTR = 'data-mode';
|
|
|
|
|
const EIDOS_DENSITY_ATTR = 'data-density';
|
|
|
|
|
const _ctx = new Context<ActiveEidos>('ActiveEidos');
|
|
|
|
|
|
|
|
|
|
export class ActiveEidos {
|
|
|
|
|
readonly #config: EidosConfig;
|
|
|
|
|
readonly #uix: ActiveUix | undefined;
|
|
|
|
|
readonly #prefs: ActivePrefs | undefined;
|
|
|
|
|
readonly #langs: ActiveLangs | undefined;
|
|
|
|
|
readonly #format: ActiveFormat | undefined;
|
|
|
|
|
readonly #preferences: ActiveEidosPreferenceSource;
|
|
|
|
|
#dom: ActiveDom | undefined;
|
|
|
|
|
readonly #styleId: string;
|
|
|
|
|
readonly #styleHost: ActiveEidosStyleHost | undefined;
|
|
|
|
|
readonly #themeSource: ActiveEidosThemeSource;
|
|
|
|
|
readonly #themeResolver: ActiveEidosThemeResolver;
|
|
|
|
|
readonly #applyDom: boolean;
|
|
|
|
|
readonly #ownedStyleIds = new Set<string>();
|
|
|
|
|
|
|
|
|
|
#cssVariables: EidosCssVariableMap | undefined;
|
|
|
|
|
#cssVariablesOptions: ActiveEidosCssVariablesOptions;
|
|
|
|
|
#unsubscribe: (() => void) | undefined;
|
|
|
|
|
#lastAttrs: ActiveEidosLastAttrs | undefined;
|
|
|
|
|
#disposed = false;
|
|
|
|
|
|
|
|
|
|
constructor(options: ActiveEidosOptions) {
|
|
|
|
|
this.#config = resolveEidosConfig(options.config, options.themeBase);
|
|
|
|
|
|
|
|
|
|
this.#uix = options.uix;
|
|
|
|
|
this.#prefs = options.prefs ?? options.uix?.prefs;
|
|
|
|
|
this.#langs = options.langs ?? options.uix?.langs;
|
|
|
|
|
this.#format = options.format ?? options.uix?.format;
|
|
|
|
|
this.#preferences = resolvePreferences(options);
|
|
|
|
|
this.#dom = options.dom ?? options.uix?.dom;
|
|
|
|
|
this.#styleId = options.styleId ?? DEFAULT_STYLE_ID;
|
|
|
|
|
this.#styleHost = options.styleHost;
|
|
|
|
|
this.#themeSource = options.themeSource ?? 'auto';
|
|
|
|
|
this.#themeResolver = options.themeResolver ?? defaultActiveEidosThemeResolver;
|
|
|
|
|
this.#applyDom = options.applyDom !== false;
|
|
|
|
|
this.#cssVariables = options.cssVariables ? { ...options.cssVariables } : undefined;
|
|
|
|
|
this.#cssVariablesOptions = {
|
|
|
|
|
selector: options.cssVariablesSelector,
|
|
|
|
|
strict: options.cssVariablesStrict
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (this.#applyDom && !this.#dom) {
|
|
|
|
|
throw new ActiveEidosConfigError('dom service is required when applyDom is enabled');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.#cssVariables) this.renderCssVariables(this.#cssVariables, this.#cssVariablesOptions);
|
|
|
|
|
|
|
|
|
|
if (this.#applyDom) {
|
|
|
|
|
this.apply();
|
|
|
|
|
this.#unsubscribe = this.#preferences.onPreferenceChange(() => this.apply());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get disposed(): boolean {
|
|
|
|
|
return this.#disposed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get uix(): ActiveUix | undefined {
|
|
|
|
|
return this.#uix;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get dom(): ActiveDom {
|
|
|
|
|
return this.#requireDom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get langs(): ActiveLangs {
|
|
|
|
|
if (!this.#langs) {
|
|
|
|
|
throw new ActiveEidosConfigError('langs service is required by ActiveEidos components');
|
|
|
|
|
}
|
|
|
|
|
return this.#langs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get format(): ActiveFormat | undefined {
|
|
|
|
|
return this.#format;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get prefs(): ActivePrefs {
|
|
|
|
|
if (!this.#prefs) {
|
|
|
|
|
throw new ActiveEidosConfigError('prefs service is required by ActiveEidos components');
|
|
|
|
|
}
|
|
|
|
|
return this.#prefs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getThemeContext(): ActiveEidosThemeContext {
|
|
|
|
|
return {
|
|
|
|
|
theme: this.#preferences.getTheme(),
|
|
|
|
|
mode: this.#preferences.getMode(),
|
|
|
|
|
density: this.#preferences.getDensity()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getThemeId(): string {
|
|
|
|
|
return this.#themeResolver(this.getThemeContext(), this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
snapshot(): EidosConfigSnapshot {
|
|
|
|
|
return snapshotEidosConfig(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validate(): EidosValidationReport {
|
|
|
|
|
return validateEidosConfig(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assertValid(): void {
|
|
|
|
|
assertValidEidosConfig(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listColorScales(): readonly string[] {
|
|
|
|
|
return listEidosColorScales(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listThemes(): readonly string[] {
|
|
|
|
|
return listEidosThemes(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listRecipes(): readonly string[] {
|
|
|
|
|
return listEidosRecipes(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getTheme(id: string): ThemeDefinition | undefined {
|
|
|
|
|
return getEidosTheme(this.#config, id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getRecipeTokens(component: string): RecipeTokenMap | undefined {
|
|
|
|
|
return getEidosRecipeTokens(this.#config, component);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listColorRoles(): readonly ColorRole[] {
|
|
|
|
|
return listEidosColorRoles();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getColorScale(name: string): ColorScale | undefined {
|
|
|
|
|
return getEidosColorScale(this.#config, name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getColorRoleScale(role: ColorRole, themeId?: string): ColorScale | undefined {
|
|
|
|
|
return getEidosColorRoleScale(this.#config, role, themeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderStaticCss(): string {
|
|
|
|
|
this.assertValid();
|
|
|
|
|
return renderEidosStaticCss(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCssContract(): EidosCssContract {
|
|
|
|
|
return getEidosCssContract(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toDocument(): EidosConfigDocument {
|
|
|
|
|
return createEidosConfigDocumentFromConfig(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serialize(): string {
|
|
|
|
|
return serializeEidosConfig(this.#config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderContractCss(renderOptions?: RenderContractCssOptions): string {
|
|
|
|
|
this.assertValid();
|
|
|
|
|
return renderEidosContractCss(this.#config, renderOptions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderCssVariables(
|
|
|
|
|
variables = this.#cssVariables ?? {},
|
|
|
|
|
renderOptions: ActiveEidosCssVariablesOptions = this.#cssVariablesOptions
|
|
|
|
|
): string {
|
|
|
|
|
this.assertValid();
|
|
|
|
|
return renderEidosCssVariables(variables, {
|
|
|
|
|
contract: this.getCssContract(),
|
|
|
|
|
...renderOptions
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderThemeCss(
|
|
|
|
|
themeId = this.getThemeId(),
|
|
|
|
|
renderOptions: RenderThemeCssOptions = { selector: ':root' }
|
|
|
|
|
): string {
|
|
|
|
|
if (this.#themeSource === 'css') return '';
|
|
|
|
|
if (this.#themeSource === 'auto' && !this.getTheme(themeId)) return '';
|
|
|
|
|
|
|
|
|
|
this.assertValid();
|
|
|
|
|
return renderEidosThemeCss(this.#config, themeId, renderOptions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderCss(themeId = this.getThemeId()): string {
|
|
|
|
|
const staticCss = this.renderStaticCss();
|
|
|
|
|
const themeCss = this.renderThemeCss(themeId);
|
|
|
|
|
|
|
|
|
|
return themeCss ? `${staticCss}\n\n${themeCss}` : staticCss;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resolve<T>(value: ResponsiveProp<T> | undefined): T | undefined;
|
|
|
|
|
resolve<T>(value: ResponsiveProp<T> | undefined, fallback: T): T;
|
|
|
|
|
resolve<T>(value: ResponsiveProp<T> | undefined, fallback?: T): T | undefined {
|
|
|
|
|
return this.dom.resolve(value) ?? fallback;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
breakpoint(name: Breakpoint): number {
|
|
|
|
|
return this.dom.breakpoints.current[name];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isAtLeast(name: Breakpoint): boolean {
|
|
|
|
|
return this.dom.isAtLeast(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isBelow(name: Breakpoint): boolean {
|
|
|
|
|
return !this.dom.isAtLeast(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setCssVariables(
|
|
|
|
|
variables: EidosCssVariableMap,
|
|
|
|
|
renderOptions: ActiveEidosCssVariablesOptions = {}
|
|
|
|
|
): void {
|
|
|
|
|
const nextVariables = { ...variables };
|
|
|
|
|
const nextOptions = {
|
|
|
|
|
...this.#cssVariablesOptions,
|
|
|
|
|
...renderOptions
|
|
|
|
|
};
|
|
|
|
|
this.renderCssVariables(nextVariables, nextOptions);
|
|
|
|
|
this.#cssVariables = nextVariables;
|
|
|
|
|
this.#cssVariablesOptions = nextOptions;
|
|
|
|
|
this.apply();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearCssVariables(): void {
|
|
|
|
|
this.#cssVariables = undefined;
|
|
|
|
|
this.apply();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
apply(): void {
|
|
|
|
|
if (this.#disposed || !this.#applyDom) return;
|
|
|
|
|
|
|
|
|
|
const host = this.#resolveStyleHost();
|
|
|
|
|
if (!host) return;
|
|
|
|
|
|
|
|
|
|
const themeContext = this.getThemeContext();
|
|
|
|
|
const themeId = this.#themeResolver(themeContext, this);
|
|
|
|
|
const staticCss = this.renderStaticCss();
|
|
|
|
|
const themeStyleId = `${this.#styleId}-theme`;
|
|
|
|
|
const themeCss = this.renderThemeCss(themeId);
|
|
|
|
|
const variablesStyleId = `${this.#styleId}-variables`;
|
|
|
|
|
const variablesCss = this.#cssVariables ? this.renderCssVariables() : '';
|
|
|
|
|
|
|
|
|
|
this.#writeThemeAttrs(themeId, themeContext.mode, themeContext.density);
|
|
|
|
|
this.#writeStyle(host, `${this.#styleId}-static`, staticCss);
|
|
|
|
|
|
|
|
|
|
if (themeCss) {
|
|
|
|
|
this.#writeStyle(host, themeStyleId, themeCss);
|
|
|
|
|
} else {
|
|
|
|
|
this.#removeStyle(host, themeStyleId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (variablesCss) {
|
|
|
|
|
this.#writeStyle(host, variablesStyleId, variablesCss);
|
|
|
|
|
} else {
|
|
|
|
|
this.#removeStyle(host, variablesStyleId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose(): void {
|
|
|
|
|
if (this.#disposed) return;
|
|
|
|
|
this.#disposed = true;
|
|
|
|
|
this.#unsubscribe?.();
|
|
|
|
|
|
|
|
|
|
const host = this.#applyDom ? this.#resolveStyleHost() : null;
|
|
|
|
|
|
|
|
|
|
for (const id of this.#ownedStyleIds) {
|
|
|
|
|
this.#dom?.removeStyle(id, host ? { host } : undefined);
|
|
|
|
|
}
|
|
|
|
|
this.#ownedStyleIds.clear();
|
|
|
|
|
if (this.#lastAttrs) {
|
|
|
|
|
const { target, themeId, mode, density } = this.#lastAttrs;
|
|
|
|
|
const attrs: Record<string, DomAttrValue> = {};
|
|
|
|
|
if (target.getAttribute(EIDOS_THEME_ATTR) === themeId) {
|
|
|
|
|
attrs[EIDOS_THEME_ATTR] = undefined;
|
|
|
|
|
}
|
|
|
|
|
if (target.getAttribute(EIDOS_MODE_ATTR) === mode) attrs[EIDOS_MODE_ATTR] = undefined;
|
|
|
|
|
if (target.getAttribute(EIDOS_DENSITY_ATTR) === density) {
|
|
|
|
|
attrs[EIDOS_DENSITY_ATTR] = undefined;
|
|
|
|
|
}
|
|
|
|
|
this.#dom?.apply({
|
|
|
|
|
target,
|
|
|
|
|
attrs
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
this.#lastAttrs = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#resolveStyleHost(): HTMLElement | null {
|
|
|
|
|
if (this.#styleHost) {
|
|
|
|
|
return typeof this.#styleHost === 'function' ? this.#styleHost() : this.#styleHost;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return this.#requireDom().getDocument().head;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (matches(error, UIX_ERR_DOM_DISABLED)) {
|
|
|
|
|
throw new ActiveEidosConfigError(
|
|
|
|
|
'dom service is disabled; pass applyDom:false or enable ActiveUix dom',
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#writeStyle(host: HTMLElement, id: string, css: string): void {
|
|
|
|
|
this.#requireDom().writeStyle(id, css, {
|
|
|
|
|
host,
|
|
|
|
|
attrs: { 'data-uix-eidos': true }
|
|
|
|
|
});
|
|
|
|
|
this.#ownedStyleIds.add(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#writeThemeAttrs(themeId: string, mode: ThemeEffective, density: Density): void {
|
|
|
|
|
const dom = this.#requireDom();
|
|
|
|
|
let target: HTMLElement;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
target = dom.getDocument().documentElement;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (matches(error, UIX_ERR_DOM_DISABLED)) {
|
|
|
|
|
throw new ActiveEidosConfigError(
|
|
|
|
|
'dom service is disabled; pass applyDom:false or enable ActiveUix dom',
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dom.apply({
|
|
|
|
|
target,
|
|
|
|
|
attrs: {
|
|
|
|
|
[EIDOS_THEME_ATTR]: themeId,
|
|
|
|
|
[EIDOS_MODE_ATTR]: mode,
|
|
|
|
|
[EIDOS_DENSITY_ATTR]: density
|
|
|
|
|
} satisfies Record<string, DomAttrValue>
|
|
|
|
|
});
|
|
|
|
|
this.#lastAttrs = { target, themeId, mode, density };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#removeStyle(host: HTMLElement, id: string): void {
|
|
|
|
|
this.#dom?.removeStyle(id, { host });
|
|
|
|
|
this.#ownedStyleIds.delete(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#requireDom(): ActiveDom {
|
|
|
|
|
if (!this.#dom) {
|
|
|
|
|
throw new ActiveEidosConfigError('dom service is required when applyDom is enabled');
|
|
|
|
|
}
|
|
|
|
|
return this.#dom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static create(opts: ActiveEidosUserOptions = {}): ActiveEidos {
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
const instance = new ActiveEidos({
|
|
|
|
|
...opts,
|
|
|
|
|
uix,
|
|
|
|
|
prefs: uix.prefs,
|
|
|
|
|
langs: uix.langs,
|
|
|
|
|
format: uix.format,
|
|
|
|
|
dom: uix.dom
|
|
|
|
|
});
|
|
|
|
|
return _ctx.set(instance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static set(active: ActiveEidos): ActiveEidos {
|
|
|
|
|
return _ctx.set(active);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static get(): ActiveEidos | undefined {
|
|
|
|
|
return _ctx.getOr(undefined) as ActiveEidos | undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static require(): ActiveEidos {
|
|
|
|
|
const active = _ctx.getOr(undefined) as ActiveEidos | undefined;
|
|
|
|
|
if (!active) throw new ActiveEidosNoContextError();
|
|
|
|
|
return active;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function defaultActiveEidosThemeResolver(
|
|
|
|
|
context: ActiveEidosThemeContext,
|
|
|
|
|
active: ActiveEidos
|
|
|
|
|
): string {
|
|
|
|
|
if (active.getTheme(context.theme)) return context.theme;
|
|
|
|
|
if (isModeQualifiedThemeId(context.theme)) return context.theme;
|
|
|
|
|
|
|
|
|
|
return `${context.theme}-${context.mode}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createActiveEidos(options: ActiveEidosOptions): ActiveEidos {
|
|
|
|
|
return new ActiveEidos(options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveEidosConfig(
|
|
|
|
|
config: EidosConfig | EidosConfigDocument | undefined,
|
|
|
|
|
themeBase: EidosConfigPatch | undefined
|
|
|
|
|
): EidosConfig {
|
|
|
|
|
if (!config) return createThemeBaseEidosConfig(themeBase);
|
|
|
|
|
if (themeBase) {
|
|
|
|
|
throw new ActiveEidosConfigError(
|
|
|
|
|
'pass either a complete config or a themeBase patch, not both'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (isEidosConfigDocumentLike(config)) return readEidosConfigFromDocument(config);
|
|
|
|
|
|
|
|
|
|
return snapshotEidosConfig(config as EidosConfig);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isModeQualifiedThemeId(themeId: string): boolean {
|
|
|
|
|
return /-(light|dark)$/.test(themeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolvePreferences(options: ActiveEidosOptions): ActiveEidosPreferenceSource {
|
|
|
|
|
if (options.preferences) return options.preferences;
|
|
|
|
|
|
|
|
|
|
return createComposedPreferenceSource({
|
|
|
|
|
theme: options.theme,
|
|
|
|
|
modeSource:
|
|
|
|
|
options.modeSource ??
|
|
|
|
|
(options.mode ? createStaticValueSource(options.mode) : createSystemColorSchemeSource()),
|
|
|
|
|
densitySource:
|
|
|
|
|
options.densitySource ?? createStaticValueSource(options.density ?? DEFAULT_DENSITY)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createComposedPreferenceSource(options: {
|
|
|
|
|
readonly theme?: string;
|
|
|
|
|
readonly modeSource: ActiveEidosValueSource<ThemeEffective>;
|
|
|
|
|
readonly densitySource: ActiveEidosValueSource<Density>;
|
|
|
|
|
}): ActiveEidosPreferenceSource {
|
|
|
|
|
return {
|
|
|
|
|
getTheme: () => options.theme ?? DEFAULT_THEME,
|
|
|
|
|
getMode: () => options.modeSource.get(),
|
|
|
|
|
getDensity: () => options.densitySource.get(),
|
|
|
|
|
onPreferenceChange(handler) {
|
|
|
|
|
const detachers = [
|
|
|
|
|
options.modeSource.onChange(handler),
|
|
|
|
|
options.densitySource.onChange(handler)
|
|
|
|
|
];
|
|
|
|
|
return () => {
|
|
|
|
|
for (const detach of detachers) detach();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createStaticValueSource<T>(value: T): ActiveEidosValueSource<T> {
|
|
|
|
|
return {
|
|
|
|
|
get: () => value,
|
|
|
|
|
onChange: () => () => {}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSystemColorSchemeSource(
|
|
|
|
|
fallback: ThemeEffective = DEFAULT_MODE
|
|
|
|
|
): ActiveEidosValueSource<ThemeEffective> {
|
|
|
|
|
const media = getColorSchemeMedia();
|
|
|
|
|
return {
|
|
|
|
|
get: () => (media ? (media.matches ? 'dark' : 'light') : fallback),
|
|
|
|
|
onChange(handler) {
|
|
|
|
|
if (!media) return () => {};
|
|
|
|
|
const listener = () => handler(media.matches ? 'dark' : 'light');
|
|
|
|
|
if (typeof media.addEventListener === 'function') {
|
|
|
|
|
media.addEventListener('change', listener);
|
|
|
|
|
return () => media.removeEventListener('change', listener);
|
|
|
|
|
}
|
|
|
|
|
media.addListener?.(listener);
|
|
|
|
|
return () => media.removeListener?.(listener);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getColorSchemeMedia(): MediaQueryList | undefined {
|
|
|
|
|
if (typeof globalThis.matchMedia !== 'function') return undefined;
|
|
|
|
|
return globalThis.matchMedia('(prefers-color-scheme: dark)');
|
|
|
|
|
}
|