All checks were successful
Build and Deploy Front / deploy (push) Successful in 8m13s
Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
6.0 KiB
TypeScript
232 lines
6.0 KiB
TypeScript
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<boolean>;
|
||
fallback: Ref<boolean>;
|
||
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<SiteLiquidGlassApi> = 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<HTMLElement>();
|
||
|
||
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);
|
||
}
|