@dvcol/neo-svelte
Version:
Neomorphic ui library for svelte 5
210 lines (209 loc) • 8.36 kB
JavaScript
import { wait } from '@dvcol/common-utils/common/promise';
import { getContext, setContext, untrack } from 'svelte';
import { getRemember, getReset, getSource, getTheme, getTransition, NeoThemeRoot, NeoThemeStorageKey } from './neo-theme-provider.model.js';
import { NeoErrorThemeContextNotFound, NeoErrorThemeInvalidTarget, NeoErrorThemeTargetNotFound } from '../utils/error.utils.js';
import styles from './neo-theme-provider.scss?url';
function isRootElement(root) {
return !!root && root instanceof HTMLElement;
}
export function computeCircleStart(element, { viewportWidth = window.innerWidth, viewportHeight = window.innerHeight } = {}) {
if (!element)
return {};
// Get button's position relative to the viewport
const rect = element.getBoundingClientRect();
// Calculate center of the button
const buttonCenterX = rect.left + (rect.width / 2);
const buttonCenterY = rect.top + (rect.height / 2);
// Convert to percentages
const xPercentage = (buttonCenterX / viewportWidth) * 100;
const yPercentage = (buttonCenterY / viewportHeight) * 100;
return {
x: Math.round(xPercentage),
y: Math.round(yPercentage),
};
}
async function transitionViewTheme(root, theme, trigger) {
if (trigger && isRootElement(root)) {
const { x, y } = computeCircleStart(trigger);
if (x)
root.style.setProperty('--neo-transition-trigger-x', `${x}%`);
if (y)
root.style.setProperty('--neo-transition-trigger-y', `${y}%`);
}
const transition = document.startViewTransition(() => {
if (!root)
throw new NeoErrorThemeTargetNotFound();
if (!('setAttribute' in root))
throw new NeoErrorThemeInvalidTarget();
root.setAttribute(NeoThemeStorageKey.InFlight, 'true');
root.setAttribute(NeoThemeStorageKey.Theme, theme);
});
await transition.finished;
if (!isRootElement(root))
return;
root.removeAttribute(NeoThemeStorageKey.InFlight);
root.style.removeProperty('--neo-transition-trigger-x');
root.style.removeProperty('--neo-transition-trigger-y');
}
export class NeoThemeProviderContext {
#reset = $state(getReset());
#theme = $state(getTheme());
#source = $state(getSource());
#remember = $state(getRemember());
#transition = $state(getTransition());
#root = $state(document?.documentElement);
#ready = $state(false);
get reset() {
return this.#reset;
}
get theme() {
return this.#theme;
}
get source() {
return this.#source;
}
get remember() {
return this.#remember;
}
get transition() {
return this.#transition;
}
get root() {
return typeof this.#root === 'function' ? this.#root() : this.#root;
}
get ready() {
return this.#ready;
}
get state() {
return {
reset: this.reset,
theme: this.theme,
source: this.source,
remember: this.remember,
transition: this.transition,
root: this.root,
};
}
constructor({ reset, theme, source, remember, transition, root }) {
this.#reset = reset ?? this.reset;
this.#theme = theme ?? this.theme;
this.#source = source ?? this.source;
this.#remember = remember ?? this.remember;
this.#transition = transition ?? this.transition;
this.#root = root ?? this.root;
}
update(partial, trigger) {
untrack(() => {
if (partial.reset !== undefined)
this.#reset = partial.reset;
if (partial.theme !== undefined)
this.#theme = partial.theme;
if (partial.source !== undefined)
this.#source = partial.source;
if (partial.remember !== undefined)
this.#remember = partial.remember;
if (partial.transition !== undefined)
this.#transition = partial.transition;
if (partial.root !== undefined)
this.#root = partial.root;
this.sync(trigger);
});
}
async setTheme(theme, trigger) {
if (!this.root)
throw new NeoErrorThemeTargetNotFound();
if (!('setAttribute' in this.root))
throw new NeoErrorThemeInvalidTarget();
if (this.theme === this.root.getAttribute(NeoThemeStorageKey.Theme)) {
return this.root.setAttribute(NeoThemeStorageKey.Transition, this.transition);
}
if ('startViewTransition' in document && this.root.getAttribute(NeoThemeStorageKey.Transition)?.startsWith('neo')) {
return transitionViewTheme(this.root, theme, trigger);
}
this.root.setAttribute(NeoThemeStorageKey.Transition, 'false');
this.root.setAttribute(NeoThemeStorageKey.Theme, theme);
await wait();
this.root.setAttribute(NeoThemeStorageKey.Transition, this.transition);
}
setSource(source) {
if (!this.root)
throw new NeoErrorThemeTargetNotFound();
if (!('setAttribute' in this.root))
throw new NeoErrorThemeInvalidTarget();
if (this.source === this.root.getAttribute(NeoThemeStorageKey.Source))
return;
this.root.setAttribute(NeoThemeStorageKey.Source, source);
}
import(target = this.root) {
if (!target)
throw new NeoErrorThemeTargetNotFound();
if (!('setAttribute' in target))
throw new NeoErrorThemeInvalidTarget();
if (target.parentElement?.querySelector('#neo-theme-provider'))
return;
const link = document.createElement('link');
link.setAttribute('type', 'text/css');
link.setAttribute('id', 'neo-theme-provider');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', new URL(styles, import.meta.url).href);
link.addEventListener('load', () => {
this.#ready = true;
});
if (target === document?.documentElement)
document.head.appendChild(link);
else
target.after(link);
}
sync(trigger) {
if (!this.root)
throw new NeoErrorThemeTargetNotFound();
if (!('setAttribute' in this.root))
throw new NeoErrorThemeInvalidTarget();
this.import(this.root);
this.root.setAttribute(NeoThemeRoot, '');
void this.setTheme(this.theme, trigger);
this.setSource(this.source);
if (this.reset)
this.root.setAttribute(NeoThemeStorageKey.Reset, '');
else
this.root.removeAttribute(NeoThemeStorageKey.Reset);
if (!localStorage)
return;
localStorage.setItem(NeoThemeStorageKey.Remember, Boolean(this.remember).toString());
if (this.remember) {
localStorage.setItem(NeoThemeStorageKey.Reset, Boolean(this.reset).toString());
localStorage.setItem(NeoThemeStorageKey.Theme, this.theme);
localStorage.setItem(NeoThemeStorageKey.Source, this.source);
localStorage.setItem(NeoThemeStorageKey.Transition, this.transition);
}
else {
localStorage.removeItem(NeoThemeStorageKey.Reset);
localStorage.removeItem(NeoThemeStorageKey.Theme);
localStorage.removeItem(NeoThemeStorageKey.Source);
localStorage.removeItem(NeoThemeStorageKey.Transition);
}
}
destroy() {
if (!this.root)
return;
if (!('removeAttribute' in this.root))
return;
this.root.removeAttribute(NeoThemeRoot);
this.root.removeAttribute(NeoThemeStorageKey.Reset);
this.root.removeAttribute(NeoThemeStorageKey.Theme);
this.root.removeAttribute(NeoThemeStorageKey.Source);
this.root.removeAttribute(NeoThemeStorageKey.Transition);
this.#ready = false;
}
}
const NeoContextKey = Symbol('NeoThemeProviderContext');
export function setNeoThemeContext(context) {
return setContext(NeoContextKey, new NeoThemeProviderContext(context));
}
export const getNeoThemeContext = () => getContext(NeoContextKey);
export function useNeoThemeContext() {
const context = getNeoThemeContext();
if (!context)
throw new NeoErrorThemeContextNotFound();
return context;
}