feat(vue): expand @robonen/vue composable collection

Composables, tests, category barrels, and README for @robonen/vue.
This commit is contained in:
2026-06-08 15:51:16 +07:00
parent 9a912f7a77
commit 59e995d0b5
369 changed files with 36554 additions and 188 deletions
@@ -0,0 +1,112 @@
import { computed, shallowRef, toValue, watch } from 'vue';
import type { ComputedRef, MaybeRefOrGetter, WritableComputedRef } from 'vue';
import type { ConfigurableWindow } from '@/types';
import { defaultWindow } from '@/types';
import type { MaybeComputedElementRef } from '@/composables/component/unrefElement';
import { unrefElement } from '@/composables/component/unrefElement';
import { useMutationObserver } from '@/composables/elements/useMutationObserver';
export interface UseCssVarOptions extends ConfigurableWindow {
/**
* Value used before the variable resolves (and the fallback when the
* computed value is empty).
*/
initialValue?: string;
/**
* Watch the target with a `MutationObserver` (filtered to `style`/`class`)
* so the ref reflects external changes to the variable.
*
* @default false
*/
observe?: boolean;
}
export interface UseCssVarReturn extends WritableComputedRef<string | null | undefined> {}
/**
* @name useCssVar
* @category Browser
* @description Read and write a CSS custom property on an element as a reactive ref.
* Defaults to `document.documentElement`. Set `observe` to react to external
* changes via a `MutationObserver`.
*
* @param {MaybeRefOrGetter<string | null | undefined>} prop The CSS variable name (e.g. `--color`)
* @param {MaybeComputedElementRef} [target] Element to read/write the variable on (defaults to `documentElement`)
* @param {UseCssVarOptions} [options={}] `initialValue`, `observe`, and a configurable `window`
* @returns {UseCssVarReturn} A writable ref; reading returns the current value, writing updates the property
*
* @example
* const color = useCssVar('--color', el);
* color.value = 'red';
*
* @example
* const theme = useCssVar('--theme', null, { initialValue: 'light', observe: true });
*
* @since 0.0.15
*/
export function useCssVar(
prop: MaybeRefOrGetter<string | null | undefined>,
target?: MaybeComputedElementRef,
options: UseCssVarOptions = {},
): UseCssVarReturn {
const { window = defaultWindow, initialValue, observe = false } = options;
// Backing store: only mutated on explicit reads / observed changes,
// so consumers reading `.value` never pay for `getComputedStyle`.
const store = shallowRef<string | null | undefined>(initialValue);
const elRef: ComputedRef<HTMLElement | SVGElement | undefined> = computed(
() => (unrefElement(target) as HTMLElement | SVGElement | undefined) ?? window?.document?.documentElement,
);
const read = (): void => {
const el = elRef.value;
const key = toValue(prop);
if (!el || !window || !key)
return;
const value = window.getComputedStyle(el).getPropertyValue(key)?.trim();
store.value = value || store.value || initialValue;
};
const write = (value: string | null | undefined): void => {
const el = elRef.value;
const key = toValue(prop);
store.value = value;
if (!el?.style || !key)
return;
if (value === null || value === undefined)
el.style.removeProperty(key);
else
el.style.setProperty(key, value);
};
if (observe) {
useMutationObserver(elRef, read, {
attributeFilter: ['style', 'class'],
window,
});
}
// Single watcher: when the element or prop changes, drop the old custom
// property and re-read the current value.
watch(
() => [elRef.value, toValue(prop)] as const,
([el, key], old) => {
const [oldEl, oldKey] = old ?? [];
if (oldEl?.style && oldKey && (oldEl !== el || oldKey !== key))
oldEl.style.removeProperty(oldKey);
read();
},
{ immediate: true },
);
return computed<string | null | undefined>({
get: () => store.value,
set: write,
});
}