You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
7.5 KiB
285 lines
7.5 KiB
import { createActiveDom, type ActiveDom, type ActiveDomStyleHost } from '$adom'
|
|
import type {
|
|
ActiveFrontend,
|
|
FrontendDensity,
|
|
ResolvedFrontendMode
|
|
} from '$frontend/active-frontend.svelte'
|
|
import {
|
|
EngineEidos,
|
|
createEngineEidos,
|
|
createEngineEidosFromDocument,
|
|
createThemeBaseEngineEidos,
|
|
isEngineEidosDocumentLike,
|
|
type EidosCssContract,
|
|
type EidosCssVariableMap,
|
|
type EngineEidosDocument,
|
|
type EngineEidosOptions,
|
|
type EngineEidosOptionsPatch,
|
|
type RenderContractCssOptions,
|
|
type RenderCssVariablesOptions,
|
|
type RenderThemeCssOptions
|
|
} from './engine'
|
|
|
|
export interface ActiveEidosThemeContext {
|
|
readonly theme: string
|
|
readonly mode: ResolvedFrontendMode
|
|
readonly density: FrontendDensity
|
|
}
|
|
|
|
export type ActiveEidosThemeResolver = (
|
|
context: ActiveEidosThemeContext,
|
|
engine: EngineEidos
|
|
) => string
|
|
|
|
export type ActiveEidosStyleHost = ActiveDomStyleHost
|
|
export type ActiveEidosThemeSource = 'auto' | 'engine' | 'css'
|
|
export type ActiveEidosCssVariablesOptions = Omit<RenderCssVariablesOptions, 'contract'>
|
|
|
|
export interface ActiveEidosOptions {
|
|
readonly engine?: EngineEidos | EngineEidosOptions | EngineEidosDocument
|
|
readonly themeBase?: EngineEidosOptionsPatch
|
|
readonly frontend: ActiveFrontend
|
|
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, 'frontend' | 'dom'>
|
|
|
|
const DEFAULT_STYLE_ID = 'uix-eidos'
|
|
|
|
export class ActiveEidos {
|
|
readonly engine: EngineEidos
|
|
|
|
readonly #frontend: ActiveFrontend
|
|
#dom: ActiveDom | undefined
|
|
#ownedDom: ActiveDom | undefined
|
|
readonly #styleId: string
|
|
readonly #styleHost: ActiveEidosStyleHost | undefined
|
|
readonly #themeSource: ActiveEidosThemeSource
|
|
readonly #themeResolver: ActiveEidosThemeResolver
|
|
readonly #ownedStyleIds = new Set<string>()
|
|
|
|
#cssVariables: EidosCssVariableMap | undefined
|
|
#cssVariablesOptions: ActiveEidosCssVariablesOptions
|
|
#unsubscribe: (() => void) | undefined
|
|
#disposed = false
|
|
|
|
constructor(options: ActiveEidosOptions) {
|
|
this.engine = resolveEngine(options.engine, options.themeBase)
|
|
this.engine.assertValid()
|
|
|
|
this.#frontend = options.frontend
|
|
this.#dom = options.dom
|
|
this.#styleId = options.styleId ?? DEFAULT_STYLE_ID
|
|
this.#styleHost = options.styleHost
|
|
this.#themeSource = options.themeSource ?? 'auto'
|
|
this.#themeResolver = options.themeResolver ?? defaultActiveEidosThemeResolver
|
|
this.#cssVariables = options.cssVariables
|
|
this.#cssVariablesOptions = {
|
|
selector: options.cssVariablesSelector,
|
|
strict: options.cssVariablesStrict
|
|
}
|
|
|
|
if (options.applyDom !== false) {
|
|
this.apply()
|
|
this.#unsubscribe = this.#frontend.onPreferenceChange(() => this.apply())
|
|
}
|
|
}
|
|
|
|
get disposed(): boolean {
|
|
return this.#disposed
|
|
}
|
|
|
|
getThemeContext(): ActiveEidosThemeContext {
|
|
return {
|
|
theme: this.#frontend.getTheme(),
|
|
mode: this.#frontend.getMode(),
|
|
density: this.#frontend.getDensity()
|
|
}
|
|
}
|
|
|
|
getThemeId(): string {
|
|
return this.#themeResolver(this.getThemeContext(), this.engine)
|
|
}
|
|
|
|
renderStaticCss(): string {
|
|
return this.engine.renderStaticCss()
|
|
}
|
|
|
|
getCssContract(): EidosCssContract {
|
|
return this.engine.getCssContract()
|
|
}
|
|
|
|
toDocument(): EngineEidosDocument {
|
|
return this.engine.toDocument()
|
|
}
|
|
|
|
serialize(): string {
|
|
return this.engine.serialize()
|
|
}
|
|
|
|
renderContractCss(renderOptions?: RenderContractCssOptions): string {
|
|
return this.engine.renderContractCss(renderOptions)
|
|
}
|
|
|
|
renderCssVariables(
|
|
variables = this.#cssVariables ?? {},
|
|
renderOptions: ActiveEidosCssVariablesOptions = this.#cssVariablesOptions
|
|
): string {
|
|
return this.engine.renderCssVariables(variables, renderOptions)
|
|
}
|
|
|
|
renderThemeCss(
|
|
themeId = this.getThemeId(),
|
|
renderOptions: RenderThemeCssOptions = { selector: ':root' }
|
|
): string {
|
|
if (this.#themeSource === 'css') return ''
|
|
if (this.#themeSource === 'auto' && !this.engine.getTheme(themeId)) return ''
|
|
|
|
return this.engine.renderThemeCss(themeId, renderOptions)
|
|
}
|
|
|
|
renderCss(themeId = this.getThemeId()): string {
|
|
const staticCss = this.renderStaticCss()
|
|
const themeCss = this.renderThemeCss(themeId)
|
|
|
|
return themeCss ? `${staticCss}\n\n${themeCss}` : staticCss
|
|
}
|
|
|
|
setCssVariables(
|
|
variables: EidosCssVariableMap,
|
|
renderOptions: ActiveEidosCssVariablesOptions = {}
|
|
): void {
|
|
this.#cssVariables = { ...variables }
|
|
this.#cssVariablesOptions = {
|
|
...this.#cssVariablesOptions,
|
|
...renderOptions
|
|
}
|
|
this.apply()
|
|
}
|
|
|
|
clearCssVariables(): void {
|
|
this.#cssVariables = undefined
|
|
this.apply()
|
|
}
|
|
|
|
apply(): void {
|
|
if (this.#disposed) return
|
|
|
|
const host = this.#resolveStyleHost()
|
|
if (!host) return
|
|
|
|
this.#writeStyle(host, `${this.#styleId}-static`, this.renderStaticCss())
|
|
|
|
const themeStyleId = `${this.#styleId}-theme`
|
|
const themeCss = this.renderThemeCss()
|
|
|
|
if (themeCss) {
|
|
this.#writeStyle(host, themeStyleId, themeCss)
|
|
} else {
|
|
this.#removeStyle(host, themeStyleId)
|
|
}
|
|
|
|
const variablesStyleId = `${this.#styleId}-variables`
|
|
const variablesCss = this.#cssVariables ? this.renderCssVariables() : ''
|
|
|
|
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.#resolveStyleHost()
|
|
|
|
for (const id of this.#ownedStyleIds) {
|
|
this.#dom?.removeStyle(id, host ? { host } : undefined)
|
|
}
|
|
this.#ownedStyleIds.clear()
|
|
this.#ownedDom?.dispose()
|
|
}
|
|
|
|
#resolveStyleHost(): HTMLElement | null {
|
|
if (this.#styleHost) {
|
|
return typeof this.#styleHost === 'function' ? this.#styleHost() : this.#styleHost
|
|
}
|
|
|
|
try {
|
|
return this.#getDom().getDocument().head
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
#writeStyle(host: HTMLElement, id: string, css: string): void {
|
|
this.#getDom().writeStyle(id, css, {
|
|
host,
|
|
attrs: { 'data-uix-eidos': true }
|
|
})
|
|
this.#ownedStyleIds.add(id)
|
|
}
|
|
|
|
#removeStyle(host: HTMLElement, id: string): void {
|
|
this.#dom?.removeStyle(id, { host })
|
|
this.#ownedStyleIds.delete(id)
|
|
}
|
|
|
|
#getDom(): ActiveDom {
|
|
if (this.#dom) return this.#dom
|
|
this.#ownedDom = createActiveDom()
|
|
this.#dom = this.#ownedDom
|
|
return this.#dom
|
|
}
|
|
}
|
|
|
|
export class ActiveEidosConfigError extends Error {
|
|
constructor(message: string) {
|
|
super(`[eidos] ${message}`)
|
|
this.name = 'ActiveEidosConfigError'
|
|
}
|
|
}
|
|
|
|
export function defaultActiveEidosThemeResolver(
|
|
context: ActiveEidosThemeContext,
|
|
engine: EngineEidos
|
|
): string {
|
|
if (engine.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 resolveEngine(
|
|
engine: EngineEidos | EngineEidosOptions | EngineEidosDocument | undefined,
|
|
themeBase: EngineEidosOptionsPatch | undefined
|
|
): EngineEidos {
|
|
if (!engine) return createThemeBaseEngineEidos(themeBase)
|
|
if (themeBase) {
|
|
throw new ActiveEidosConfigError('pass either a complete engine or a themeBase patch, not both')
|
|
}
|
|
if (engine instanceof EngineEidos) return engine
|
|
if (isEngineEidosDocumentLike(engine)) return createEngineEidosFromDocument(engine)
|
|
|
|
return createEngineEidos(engine as EngineEidosOptions)
|
|
}
|
|
|
|
function isModeQualifiedThemeId(themeId: string): boolean {
|
|
return /-(light|dark)$/.test(themeId)
|
|
}
|