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.
svelte-kit-vice/src/uix/eidos/active-eidos.svelte.ts

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)
}

Powered by TurnKey Linux.