import { inject, provide, ref, type InjectionKey, type Ref } from 'vue'; import { attachSelectionLens, createGlassScene, type GlassSceneController, type SurfaceOptions, } from '@glass-sdk/liquid-glass/dom'; import '@glass-sdk/liquid-glass/styles.css'; export type SiteGlassSurfaceOpts = SurfaceOptions; type QueuedJob = | { kind: 'surface'; el: HTMLElement; opts?: SiteGlassSurfaceOpts; setRemove: (fn: () => void) => void } | { kind: 'indicator'; el: HTMLElement; opts?: SiteGlassSurfaceOpts; setRemove: (fn: () => void) => void }; export type SiteLiquidGlassApi = { ready: Ref; fallback: Ref; init: (root: HTMLElement, content: HTMLElement) => void; registerSurface: (el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) => () => void; registerTabIndicator: (el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) => () => void; dispose: () => void; }; const SITE_LIQUID_GLASS_KEY: InjectionKey = Symbol('site-liquid-glass'); /** 履约 / 权益门卡:低折射 + 关闭 fluid,减轻矮卡左右竖边光学校弯 */ export const SITE_GLASS_PANEL_OPTS: SiteGlassSurfaceOpts = { material: 'clear', appearance: 'light', radius: 12, refraction: 20, fluid: false, interactive: false, }; export const SITE_GLASS_TABS_OPTS: SiteGlassSurfaceOpts = { material: 'clear', appearance: 'light', radius: 'capsule', refraction: 56, interactive: true, }; export const SITE_GLASS_TAB_INDICATOR_OPTS: SiteGlassSurfaceOpts = { material: 'clear', appearance: 'light', radius: 'capsule', refraction: 40, interactive: false, }; /** Welcome input — frosted glass, no tint */ export const SITE_GLASS_COMPOSER_OPTS: SiteGlassSurfaceOpts = { material: 'regular', appearance: 'light', radius: 12, refraction: 32, fluid: true, interactive: false, }; export function provideSiteLiquidGlass(): SiteLiquidGlassApi { const ready = ref(false); const fallback = ref(false); let scene: GlassSceneController | null = null; let removers: Array<() => void> = []; let queue: QueuedJob[] = []; let readyObserver: MutationObserver | null = null; let fallbackTimer: number | null = null; const trackedSurfaces = new Set(); function clearFallbackTimer() { if (fallbackTimer != null) { window.clearTimeout(fallbackTimer); fallbackTimer = null; } } function isClipReady(el: HTMLElement) { return !!el.getAttribute('style')?.includes('clip-path'); } function syncReady() { if ([...trackedSurfaces].some(isClipReady)) { ready.value = true; fallback.value = false; clearFallbackTimer(); return true; } return false; } function observeSurface(el: HTMLElement) { trackedSurfaces.add(el); if (!readyObserver) { readyObserver = new MutationObserver(() => { syncReady(); }); } readyObserver.observe(el, { attributes: true, attributeFilter: ['style'] }); } function bindSurface(el: HTMLElement, opts?: SiteGlassSurfaceOpts) { if (!scene) return () => undefined; const remove = scene.addSurface(el, opts); observeSurface(el); syncReady(); return () => { trackedSurfaces.delete(el); remove(); }; } function bindIndicator(el: HTMLElement, opts?: SiteGlassSurfaceOpts) { if (!scene) return () => undefined; const removeSurface = bindSurface(el, opts ?? SITE_GLASS_TAB_INDICATOR_OPTS); const removeAnimator = scene.addAnimator(attachSelectionLens(el, () => 'full')); return () => { removeAnimator(); removeSurface(); }; } function enqueue( kind: 'surface' | 'indicator', el: HTMLElement, opts?: SiteGlassSurfaceOpts, ) { let removeFn = () => undefined; let flushed = false; const job: QueuedJob = { kind, el, opts, setRemove: (fn) => { flushed = true; removeFn = fn; }, }; queue.push(job); return () => { if (!flushed) { queue = queue.filter((item) => item !== job); return; } removeFn(); }; } function registerSurface(el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) { if (!el) return () => undefined; if (scene) { const remove = bindSurface(el, opts); removers.push(remove); return remove; } return enqueue('surface', el, opts); } function registerTabIndicator(el: HTMLElement | null | undefined, opts?: SiteGlassSurfaceOpts) { if (!el) return () => undefined; if (scene) { const remove = bindIndicator(el, opts); removers.push(remove); return remove; } return enqueue('indicator', el, opts); } function init(root: HTMLElement, content: HTMLElement) { if (scene) return; fallbackTimer = window.setTimeout(() => { if (!ready.value) fallback.value = true; }, 2200); scene = createGlassScene(root, { maxSurfaces: 16, onDiagnostic: (diagnostic) => { if (diagnostic.error) { fallback.value = true; ready.value = false; } else if (diagnostic.maps > 0) { syncReady(); } }, }); scene.setContent(content); for (const job of queue) { const remove = job.kind === 'indicator' ? bindIndicator(job.el, job.opts) : bindSurface(job.el, job.opts); job.setRemove(remove); removers.push(remove); } queue = []; syncReady(); } function dispose() { clearFallbackTimer(); readyObserver?.disconnect(); readyObserver = null; removers.forEach((remove) => remove()); removers = []; queue = []; trackedSurfaces.clear(); scene?.dispose(); scene = null; ready.value = false; fallback.value = false; } const api: SiteLiquidGlassApi = { ready, fallback, init, registerSurface, registerTabIndicator, dispose, }; provide(SITE_LIQUID_GLASS_KEY, api); return api; } export function useSiteLiquidGlass(): SiteLiquidGlassApi | null { return inject(SITE_LIQUID_GLASS_KEY, null); }