fix(vue): eslint/tsconfig migration + resolve type errors

@robonen/vue (toolkit): migrate to eslint flat config + composite tsconfig;
fix composable + test type errors (writable computed returns, null guards,
overload-compatible signatures, typed test helpers) — all type-level.
This commit is contained in:
2026-06-07 16:29:39 +07:00
parent e6919de29e
commit c7644ade69
203 changed files with 23016 additions and 141 deletions
@@ -0,0 +1,145 @@
import { computed, ref, toValue, watch } from 'vue';
import type { ComputedRef, MaybeRef, MaybeRefOrGetter, Ref } from 'vue';
import { isFunction, isString } from '@robonen/stdlib';
import { defaultDocument } from '@/types';
import type { ConfigurableDocument } from '@/types';
import { useMutationObserver } from '@/composables/browser/useMutationObserver';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
export interface UseTitleOptionsBase extends ConfigurableDocument {
/**
* Observe the `<title>` element for external changes and sync them back to the ref.
* Ignored when `titleTemplate` is provided, to avoid a write/observe feedback loop.
*
* @default false
*/
observe?: boolean;
/**
* Template used to format the title. Every `%s` is replaced with the value.
*
* @default '%s'
*/
titleTemplate?: MaybeRef<string> | ((title: string) => string);
/**
* Restore the original document title when the active scope is disposed.
* Pass a function to compute the title to restore, or `false` to keep the
* last value in place.
*
* @default false
*/
restoreOnUnmount?: false | ((originalTitle: string, currentTitle: string) => string | null | undefined);
}
export type UseTitleOptions = UseTitleOptionsBase;
export type UseTitleReturn = Ref<string | null | undefined> | ComputedRef<string | null | undefined>;
/**
* @name useTitle
* @category Browser
* @description Reactive `document.title`. Pass a getter to derive the title from
* other reactive state (returns a read-only ref), or a plain value/ref for two-way binding.
*
* @param {MaybeRefOrGetter<string | null | undefined>} [newTitle] Initial title (getter source returns a read-only ref)
* @param {UseTitleOptions} [options={}] Options
* @returns {UseTitleReturn} A ref bound to the document title (read-only when a getter source is passed)
*
* @example
* const title = useTitle();
* title.value = 'New title';
*
* @example
* useTitle('Dashboard', { titleTemplate: '%s | My App' });
*
* @example
* // Derive from reactive state (read-only result)
* useTitle(() => `Inbox (${count.value})`);
*
* @example
* // Restore the previous title when the component unmounts
* useTitle('Checkout', { restoreOnUnmount: (original) => original });
*
* @since 0.0.15
*/
export function useTitle(
newTitle: () => string | null | undefined,
options?: UseTitleOptions,
): ComputedRef<string | null | undefined>;
export function useTitle(
newTitle?: MaybeRef<string | null | undefined>,
options?: UseTitleOptions,
): Ref<string | null | undefined>;
export function useTitle(
newTitle: MaybeRefOrGetter<string | null | undefined> = null,
options: UseTitleOptions = {},
): UseTitleReturn {
const {
document = defaultDocument,
observe = false,
titleTemplate = '%s',
restoreOnUnmount = false,
} = options;
const originalTitle = document?.title ?? '';
const hasTemplate = 'titleTemplate' in options;
const isReadonly = isFunction(newTitle);
const title = ref<string | null | undefined>(toValue(newTitle) ?? document?.title ?? null);
const format = (value: string): string => {
if (!hasTemplate)
return value;
return isFunction(titleTemplate)
? titleTemplate(value)
: toValue(titleTemplate).split('%s').join(value);
};
watch(
title,
(value, oldValue) => {
if (value !== oldValue && document)
document.title = format(isString(value) ? value : '');
},
{ immediate: true },
);
// Keep a read-only ref in sync when the getter source changes
if (isReadonly) {
watch(
() => toValue(newTitle),
(value) => {
title.value = value;
},
);
}
// Observing only makes sense without a template, otherwise the formatted
// write would feed back through the observer.
if (observe && !hasTemplate && document && !isReadonly) {
useMutationObserver(
document.head?.querySelector('title'),
() => {
if (document && document.title !== title.value)
title.value = document.title;
},
{ childList: true },
);
}
if (restoreOnUnmount) {
tryOnScopeDispose(() => {
const restored = restoreOnUnmount(originalTitle, isString(title.value) ? title.value : '');
if (restored !== null && restored !== undefined && document)
document.title = restored;
});
}
if (isReadonly)
return computed(() => title.value);
return title;
}