@fastkit/vue-page
Version:
Middleware for more convenient control of routing in Vue applications.
1 lines • 72.1 kB
Source Map (JSON)
{"version":3,"file":"vue-page.mjs","names":["name"],"sources":["../src/composables/router/suspense-route-bucket.ts","../src/composables/router/scroll-behavior.ts","../src/utils.ts","../src/logger.ts","../src/injections.ts","../src/composables/page-error.ts","../src/components/VErrorPage/VErrorPage.tsx","../src/composables/page-control.ts","../src/composables/plugin.ts","../src/composables/state.ts","../src/components/VPage.tsx","../src/components/VPageProgress/VPageProgress.tsx","../src/components/VPageLink.ts","../src/components/VPageRoot/VPageRoot.tsx"],"sourcesContent":["import { EV } from '@fastkit/ev';\nimport { ref, VNode, ComputedRef, computed } from 'vue';\n\nexport type SuspenseRouteSource = VNode;\n\nexport interface SuspenseRouteBucketPayload {\n bucket: SuspenseRouteBucket;\n source: SuspenseRouteSource;\n}\n\nexport interface SuspenseRouteBucketEventMap {\n push: SuspenseRouteBucketPayload;\n remove: SuspenseRouteBucketPayload;\n start: SuspenseRouteBucketPayload;\n finish: SuspenseRouteBucketPayload;\n}\n\nexport interface SuspenseRouteBucketProgress {\n total: number;\n resolved: number;\n}\n\nclass SuspenseRouteBucket extends EV<SuspenseRouteBucketEventMap> {\n private readonly _sources = ref<SuspenseRouteSource[]>([]);\n\n private readonly _total = ref(0);\n\n private readonly _progress: ComputedRef<SuspenseRouteBucketProgress>;\n\n get sources() {\n return this._sources.value;\n }\n\n get total() {\n return this._total.value;\n }\n\n get progress() {\n return this._progress.value;\n }\n\n constructor() {\n super();\n this._progress = computed(() => {\n const { total, sources } = this;\n return {\n total,\n resolved: total - sources.length,\n };\n });\n this.push = this.push.bind(this);\n this.remove = this.remove.bind(this);\n }\n\n push(source?: SuspenseRouteSource) {\n if (!source) return;\n const { sources } = this;\n if (!sources.includes(source)) {\n sources.push(source);\n this._total.value++;\n const payload: SuspenseRouteBucketPayload = { bucket: this, source };\n this.emit('push', payload);\n if (sources.length === 1) {\n this.emit('start', payload);\n }\n }\n }\n\n remove(source?: SuspenseRouteSource) {\n if (!source) return;\n const { sources } = this;\n const index = sources.indexOf(source);\n if (index !== -1) {\n sources.splice(index, 1);\n const payload: SuspenseRouteBucketPayload = { bucket: this, source };\n this.emit('remove', payload);\n if (sources.length === 0) {\n this._total.value = 0;\n this.emit('finish', payload);\n }\n }\n }\n\n ensureReady() {\n return new Promise<void>((resolve) => {\n const { total } = this;\n if (total === 0) return resolve();\n this.once('finish', () => {\n resolve();\n });\n });\n }\n}\n\nlet _suspenseRouteBucket: SuspenseRouteBucket | undefined;\n\nexport function getSuspenseRouteBucket() {\n if (!_suspenseRouteBucket) {\n _suspenseRouteBucket = new SuspenseRouteBucket();\n }\n return _suspenseRouteBucket;\n}\n","import { ComponentCustomOptions } from 'vue';\nimport type { RouterScrollBehavior, RouteLocationNormalized } from 'vue-router';\nimport { getRouteMatchedComponents } from '@fastkit/vue-utils';\nimport { getSuspenseRouteBucket } from './suspense-route-bucket';\n\nexport type RawRouterScrollBehavior = 'top' | RouterScrollBehavior;\n\ndeclare module 'vue' {\n export interface ComponentCustomOptions {\n scrollBehavior?: RawRouterScrollBehavior;\n }\n}\n\nexport type ScrollBehaviorMessengerFn = (canceled: boolean) => any;\n\nexport class ScrollBehaviorMessenger {\n private static fns: ScrollBehaviorMessengerFn[] = [];\n\n static trigger() {\n this.fns.forEach((fn) => fn(false));\n this.fns = [];\n }\n\n static reserveNextPosition(position: any) {\n return new Promise<any>((resolve) => {\n this.fns.push((canceled) => {\n resolve(canceled ? undefined : position);\n });\n });\n }\n\n static release() {\n this.fns.forEach((fn) => fn(true));\n this.fns = [];\n }\n}\n\nfunction resolveRawRouterScrollBehavior(\n source: RawRouterScrollBehavior,\n): RouterScrollBehavior {\n if (source === 'top') {\n source = (to, from, savedPosition) => savedPosition || { top: 0, left: 0 };\n }\n return source;\n}\n\nfunction extractMatchedBehaviors(route: RouteLocationNormalized) {\n const components = getRouteMatchedComponents(\n route,\n ) as ComponentCustomOptions[];\n const behaviors: RouterScrollBehavior[] = [];\n components.forEach(({ scrollBehavior }) => {\n if (scrollBehavior != null) {\n behaviors.push(resolveRawRouterScrollBehavior(scrollBehavior));\n }\n });\n return behaviors;\n}\n\nfunction findLastMatchedBehavior(\n route: RouteLocationNormalized,\n): RouterScrollBehavior | undefined {\n const behaviors = extractMatchedBehaviors(route);\n return behaviors[behaviors.length - 1];\n}\n\nexport function createScrollBehavior(): RouterScrollBehavior {\n const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => {\n // If the returned position is falsy or an empty object, will retain current scroll position\n let position: any = false;\n const isRouteChanged = to !== from;\n\n // savedPosition is only available for popstate navigations (back button)\n if (savedPosition) {\n position = savedPosition;\n } else if (isRouteChanged) {\n const matchedBehavior =\n findLastMatchedBehavior(to) || resolveRawRouterScrollBehavior('top');\n position = matchedBehavior(to, from, savedPosition);\n } else if (to.path === from.path && to.hash !== from.hash) {\n // nextTick(() => nuxt.$emit('triggerScroll'));\n }\n\n return getSuspenseRouteBucket()\n .ensureReady()\n .then(async () => {\n // coords will be used if no selector is provided,\n // or if the selector didn't match any element.\n if (to.hash) {\n let { hash } = to;\n // CSS.escape() is not supported with IE and Edge.\n if (\n typeof window.CSS !== 'undefined' &&\n typeof window.CSS.escape !== 'undefined'\n ) {\n hash = `#${window.CSS.escape(hash.substr(1))}`;\n }\n try {\n const el = document.querySelector(hash);\n if (el) {\n // scroll to anchor by returning the selector\n position = { el: hash };\n // Respect any scroll-margin-top set in CSS when scrolling to anchor\n // const _scrollMarginTop = getComputedStyle(el).scrollMarginTop;\n // if (_scrollMarginTop != null) {\n // position.behavior =\n // }\n // const y = Number(getComputedStyle(el)['scroll-margin-top']?.replace('px', ''))\n // if (y) {\n // position.offset = { y }\n // }\n }\n } catch (e) {\n // eslint-disable-next-line no-console\n console.warn(\n 'Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).',\n );\n }\n }\n\n return ScrollBehaviorMessenger.reserveNextPosition(position);\n });\n };\n return scrollBehavior;\n}\n","import {\n stringifyQuery,\n RouteLocationMatched,\n RouteLocationNormalized,\n} from 'vue-router';\nimport { isObject, IN_WINDOW } from '@fastkit/helpers';\nimport { RouteMatchedItem, RawRouteComponent } from '@fastkit/vue-utils';\nimport {\n WatchQueryOption,\n RouterViewSlotProps,\n VuePageKeyOverride,\n} from './schemes';\n\nexport function parseWatchQueryOption(\n route: RouteLocationNormalized,\n opts?: WatchQueryOption,\n): string {\n // if (!opts) return route.path;\n if (opts === true) {\n opts = Object.keys(route.query);\n }\n if (!opts || !opts.length) return '';\n const query: RouteLocationNormalized['query'] = {};\n opts.forEach((row) => {\n query[row] = route.query[row];\n });\n const queryStr = stringifyQuery(query);\n // let key = route.path;\n let key = '&';\n if (queryStr) {\n key += `?${queryStr}`;\n }\n return key;\n}\n\nexport function extractWatchQueryOption(\n source: any,\n): WatchQueryOption | undefined {\n if (!isObject<any>(source)) return;\n if (source.watchQuery) return source.watchQuery;\n if (isObject(source.type) && source.type.watchQuery) {\n return source.type.watchQuery;\n }\n}\n\nexport function generateWatchQueryKeySuffix(routeProps: {\n Component: RouterViewSlotProps['Component'] | RawRouteComponent;\n route: RouteLocationNormalized;\n}): string {\n const option = extractWatchQueryOption(routeProps.Component);\n return parseWatchQueryOption(routeProps.route, option);\n}\n\nconst interpolatePath = (\n route: {\n params: RouteLocationNormalized['params'];\n },\n match: {\n path: RouteLocationMatched['path'];\n },\n) =>\n match.path\n .replace(/(:\\w+)\\([^)]+\\)/g, '$1')\n .replace(/(:\\w+)[?+*]/g, '$1')\n .replace(/:\\w+/g, (r) => route.params[r.slice(1)]?.toString() || '');\n\nexport const generateRouteKey = (\n routeProps: RouterViewSlotProps,\n override?: VuePageKeyOverride,\n): string => {\n const matchedRoute = routeProps.route.matched.find(\n (m) =>\n routeProps.Component &&\n m.components?.default === routeProps.Component.type,\n );\n if (!matchedRoute) {\n return '';\n }\n const source =\n override ??\n (matchedRoute?.meta.key as string | undefined) ??\n interpolatePath(routeProps.route, matchedRoute);\n return typeof source === 'function' ? source(routeProps.route) : source;\n};\n\nexport const generateRouteKeyWithWatchQuery = (\n routeProps: RouterViewSlotProps,\n override?: VuePageKeyOverride,\n) => {\n const routeKey = generateRouteKey(routeProps, override);\n if (!routeKey) return routeKey;\n return `${routeKey}${generateWatchQueryKeySuffix(routeProps)}`;\n};\n\nexport function routeKeyWithWatchQueryByRouteItem(\n route: RouteLocationNormalized,\n item: RouteMatchedItem,\n) {\n const routeKey = interpolatePath(route, item.route);\n const routeProps = {\n Component: item.Component,\n route,\n };\n return `${routeKey}${generateWatchQueryKeySuffix(routeProps)}`;\n}\n\nlet __forcePrefetchStates: {\n [pageKey: string]: boolean;\n} = {};\n\nexport function getForcePrefetchStates(pageKey: string) {\n return __forcePrefetchStates[pageKey];\n}\n\nexport function consumeForcePrefetchStates(pageKey: string) {\n const value = getForcePrefetchStates(pageKey);\n setForcePrefetchStates(pageKey, false);\n return value;\n}\n\nexport function setForcePrefetchStates(pageKey: string, value: boolean) {\n if (IN_WINDOW) return;\n if (value) {\n __forcePrefetchStates[pageKey] = value;\n } else {\n delete __forcePrefetchStates[pageKey];\n }\n}\n\nexport function resetForcePrefetchStates() {\n __forcePrefetchStates = {};\n}\n","import { TinyLogger, createTinyError } from '@fastkit/tiny-logger';\n\nconst name = 'vue-page';\n\nexport const logger = new TinyLogger(name);\n\nexport const VuePageError = createTinyError(name);\n","import { InjectionKey, inject, onBeforeUnmount } from 'vue';\nimport type { VuePageControl, UseVuePageControlOptions } from './composables';\nimport { VuePageError } from './logger';\n\nexport const VuePageControlInjectionKey: InjectionKey<VuePageControl> =\n Symbol('VuePageControl');\n\nexport function useVuePageControl(opts: UseVuePageControlOptions = {}) {\n const pageControl = inject(VuePageControlInjectionKey, null);\n if (!pageControl) {\n throw new VuePageError(`missing provided VuePageControl.`);\n }\n\n const { onStart, onFinish, onError } = opts;\n if (onStart || onFinish) {\n onStart && pageControl.on('start', onStart);\n onFinish && pageControl.on('finish', onFinish);\n onError && pageControl.on('error', onError);\n\n onBeforeUnmount(() => {\n onStart && pageControl.off('start', onStart);\n onFinish && pageControl.off('finish', onFinish);\n onError && pageControl.off('error', onError);\n });\n }\n return pageControl;\n}\n","const name = 'VuePageControlError';\n\nexport interface VuePageControlErrorSettings {\n statusCode?: number;\n message?: string;\n stack?: string;\n}\n\nexport function isVuePageControlError(\n source: unknown,\n): source is VuePageControlError {\n return source instanceof VuePageControlError;\n}\n\nexport class VuePageControlError extends Error {\n readonly name = name;\n\n readonly statusCode: number;\n\n constructor(\n rawSettings: VuePageControlErrorSettings | unknown | string | Error = {},\n ) {\n let settings: VuePageControlErrorSettings;\n if (rawSettings instanceof Error) {\n if (isVuePageControlError(rawSettings)) {\n settings = {\n statusCode: rawSettings.statusCode,\n message: rawSettings.message,\n stack: rawSettings.stack,\n };\n } else {\n settings = {\n message: rawSettings.message,\n stack: rawSettings.stack,\n };\n }\n } else {\n settings =\n typeof rawSettings === 'string'\n ? { message: rawSettings }\n : rawSettings && typeof rawSettings === 'object'\n ? {\n statusCode: (rawSettings as VuePageControlErrorSettings)\n .statusCode,\n message: (rawSettings as VuePageControlErrorSettings).message,\n stack: (rawSettings as VuePageControlErrorSettings).stack,\n }\n : {};\n }\n\n const {\n statusCode = 500,\n message = 'An error has occurred.',\n stack,\n } = settings;\n super(message);\n this.statusCode = statusCode;\n if (stack) {\n this.stack = stack;\n }\n }\n\n toJSON() {\n return {\n name: this.name,\n statusCode: this.statusCode,\n message: this.message,\n stack: this.stack,\n };\n }\n}\n","import { defineComponent } from 'vue';\nimport { useVuePageControl } from '../../injections';\nimport { VuePageControlError } from '../../composables/page-error';\n\nexport const VErrorPage = defineComponent({\n name: 'VErrorPage',\n setup() {\n const control = useVuePageControl();\n\n return () => {\n const pageError = control.pageError || new VuePageControlError();\n\n return (\n <div\n class=\"v-error-page\"\n style={{\n padding: '32px',\n fontSize: '16px',\n lineHeight: '1.5',\n }}>\n <h1\n class=\"v-error-page__status-code\"\n style={{\n fontSize: '3em',\n margin: '16px 0',\n }}>\n {pageError.statusCode}\n </h1>\n <p\n class=\"v-error-page__message\"\n style={{\n margin: '1em 0',\n }}>\n {pageError.message}\n </p>\n <pre\n class=\"v-error-page__stack\"\n style={{\n whiteSpace: 'pre-wrap',\n }}>\n <code\n style={{\n fontFamily: 'monospace, monospace',\n fontSize: '0.75em',\n }}>\n {pageError.stack}\n </code>\n </pre>\n </div>\n );\n };\n },\n});\n","import {\n App,\n ref,\n Ref,\n ComputedRef,\n computed,\n VNode,\n Component,\n InjectionKey,\n reactive,\n} from 'vue';\nimport type {\n Router,\n RouteLocationNormalized,\n RouteQueryAndHash,\n MatcherLocationAsPath,\n LocationQueryRaw,\n LocationAsRelativeRaw,\n} from 'vue-router';\nimport { stringifyQuery, useLink, RouterLink } from 'vue-router';\nimport { IN_WINDOW, isPromise } from '@fastkit/helpers';\nimport {\n extractRouteMatchedItems,\n RouteMatchedItem,\n getRouteQuery,\n RouteQueryType,\n isComponentCustomOptions,\n} from '@fastkit/vue-utils';\nimport { EV } from '@fastkit/ev';\nimport type { ServerResponse, IncomingMessage } from 'node:http';\nimport { Cookies, CookiesContext } from '@fastkit/cookies';\nimport { ResolvedRouteLocation, WatchQueryOption } from '../schemes';\nimport {\n routeKeyWithWatchQueryByRouteItem,\n setForcePrefetchStates,\n} from '../utils';\nimport { useVuePageControl } from '../injections';\nimport { VuePageControlError } from './page-error';\nimport { VErrorPage } from '../components/VErrorPage';\nimport { StateInjectionKey } from './state';\n\ntype InitialState = Record<string, unknown>;\n\nexport type VuePageInjectionKey<T> = string;\n\nexport type VuePagePrefetchFn = (\n this: void,\n queue: VuePagePrefetchQueue,\n) => void | Promise<void>;\n\nexport type RawPrefetchContext = VuePagePrefetchFn | PrefetchContext;\n\nexport interface WrittenResponse {\n status?: number;\n statusText?: string;\n headers?: Record<string, string>;\n}\n\nexport type WriteResponseFn = (params: WrittenResponse) => void;\n\nexport type RedirectFn = (location: string, status?: number) => void;\n\nexport interface PrefetchHandlerContext {\n control: VuePageControl;\n to: RouteLocationNormalized;\n from: RouteLocationNormalized;\n matched: {\n from?: RouteMatchedItem;\n to: RouteMatchedItem;\n };\n pageKey: string;\n}\n\nexport type PrefetchHandler = (ctx: PrefetchHandlerContext) => boolean | void;\n\ndeclare module 'vue' {\n interface ComponentCustomOptions {\n prefetch?: RawPrefetchContext;\n prefetchHandler?: PrefetchHandler;\n watchQuery?: WatchQueryOption;\n middleware?: VuePageControlMiddlewareFn | VuePageControlMiddlewareFn[];\n }\n}\n\nfunction extractPrefetch(Component: unknown): {\n prefetch?: VuePagePrefetchFn;\n middleware: VuePageControlMiddlewareFn[];\n prefetchHandler?: PrefetchHandler;\n} | void {\n if (!isComponentCustomOptions(Component)) return;\n let { prefetch, middleware = [] } = Component;\n\n if (!Array.isArray(middleware)) {\n middleware = [middleware];\n }\n\n const { prefetchHandler } = Component;\n if (prefetch && typeof prefetch === 'object') {\n prefetch = prefetch.prefetch;\n }\n if (typeof prefetch === 'function' || middleware.length) {\n return {\n prefetch,\n prefetchHandler,\n middleware,\n };\n }\n}\n\nfunction updateWatchQueryOption(Component: unknown, queries: string[]) {\n if (!isComponentCustomOptions(Component)) return;\n if (Component.watchQuery === true) return;\n Component.watchQuery = Component.watchQuery || [];\n const { watchQuery } = Component;\n for (const query of queries) {\n if (!watchQuery.includes(query)) {\n watchQuery.push(query);\n }\n }\n}\n\ninterface RouteMatchedItemWithPrefetch extends RouteMatchedItem {\n prefetch?: RawPrefetchContext;\n middleware: VuePageControlMiddlewareFn[];\n updateQueries(queries: string[]): void;\n}\n\ninterface RequiredRouteMatchedItemWithPrefetch extends RouteMatchedItemWithPrefetch {\n prefetch: RawPrefetchContext;\n}\n\nexport function extractRouteMatchedItemsWithPrefetch(\n control: VuePageControl,\n to: RouteLocationNormalized,\n from: RouteLocationNormalized,\n) {\n const extracted: RouteMatchedItemWithPrefetch[] = [];\n const fromMatched = extractRouteMatchedItems(from);\n const fromItems: { [key: string]: RouteMatchedItem } = {};\n\n fromMatched.forEach((_item) => {\n const fromKey = routeKeyWithWatchQueryByRouteItem(from, _item);\n fromItems[fromKey] = _item;\n });\n\n const matched = extractRouteMatchedItems(to);\n\n matched.forEach((_item) => {\n const prefetchSettings = extractPrefetch(_item.Component);\n if (!prefetchSettings) return;\n\n const { prefetchHandler, middleware } = prefetchSettings;\n let { prefetch } = prefetchSettings;\n\n const pageKey = routeKeyWithWatchQueryByRouteItem(to, _item);\n const fromItem = fromItems[pageKey];\n\n let forcePrefetch: boolean | undefined | void;\n\n if (prefetchHandler) {\n const ctx: PrefetchHandlerContext = {\n control,\n to,\n from,\n matched: {\n from: fromItem,\n to: _item,\n },\n pageKey,\n };\n\n forcePrefetch = prefetchHandler(ctx);\n }\n\n setForcePrefetchStates(pageKey, forcePrefetch || false);\n\n if (!forcePrefetch && fromItem) {\n // 遷移前にすでに実行されているprefetchの場合(ネストされたルートにおけるルートビュー等)はpreftechをキャンセルする\n prefetch = undefined;\n }\n\n function updateQueries(queries: string[]) {\n updateWatchQueryOption(_item.Component, queries);\n }\n extracted.push({ ..._item, prefetch, updateQueries, middleware });\n });\n return {\n extracted,\n matched,\n };\n}\n\nexport type VuePageControlMiddlewareFn = (\n ctx: VuePageControl,\n) => void | Promise<void>;\n\nexport function createMiddleware(source: VuePageControlMiddlewareFn) {\n return source;\n}\n\ntype RedirectOptions = {\n statusCode?: number;\n};\n\nexport type RawVuePageControlRedirectSpec =\n | string\n | (RouteQueryAndHash & MatcherLocationAsPath & RedirectOptions)\n | (RouteQueryAndHash & LocationAsRelativeRaw & RedirectOptions);\n\nexport interface VuePageControlRedirectSpec\n extends\n RouteQueryAndHash,\n MatcherLocationAsPath,\n Omit<LocationAsRelativeRaw, 'path'> {\n statusCode: number;\n query?: LocationQueryRaw;\n}\n\nconst DEFAULT_REDIRECT_STATUS = 302;\n\nconst STATE_BUCKET_SYMBOL = 'pgc-state';\nconst QUERY_BUCKET_SYMBOL = 'pgc-query';\n\nfunction resolveRawVuePageControlRedirectSpec(\n source: RawVuePageControlRedirectSpec,\n): VuePageControlRedirectSpec {\n if (typeof source === 'string') {\n source = { path: source };\n }\n const { statusCode = DEFAULT_REDIRECT_STATUS, query, hash } = source;\n const { path } = source as MatcherLocationAsPath;\n const { name, params } = source as LocationAsRelativeRaw;\n return {\n statusCode,\n query,\n hash,\n path,\n name,\n params,\n };\n}\n\nexport interface VuePageControlSettings {\n app: App;\n router: Router;\n RouterLink?: any;\n useLink?: typeof useLink;\n initialState?: InitialState;\n initialRoute: ResolvedRouteLocation;\n ErrorComponent?: Component;\n request?: IncomingMessage;\n response?: ServerResponse;\n serverRedirect?: RedirectFn;\n writeResponse?: WriteResponseFn;\n middleware?: VuePageControlMiddlewareFn[];\n}\n\ntype RawProvided<T> = T | ((queue: VuePagePrefetchQueue) => T | Promise<T>);\n\nexport interface VuePagePrefetchProgress {\n resolved: number;\n total: number;\n per: number;\n finished: boolean;\n}\n\nexport interface VuePageControlEventMap {\n start: VuePagePrefetchProgress;\n finish: VuePagePrefetchProgress;\n error: VuePageControlError;\n}\n\nconst ERROR_STATE_NAME = '__vpe__';\n\nexport class VuePageControl extends EV<VuePageControlEventMap> {\n readonly app: App;\n\n readonly router: Router;\n\n readonly cookies: Cookies;\n\n readonly RouterLink: typeof RouterLink;\n\n readonly useLink: typeof useLink;\n\n private _from: Ref<RouteLocationNormalized | null> = ref(null);\n\n private _to: Ref<RouteLocationNormalized | null> = ref(null);\n\n private _initialState: InitialState;\n\n private _beforeRoute: Ref<ResolvedRouteLocation | null> = ref(null);\n\n private _route: Ref<ResolvedRouteLocation>;\n\n private _stopFn?: () => void;\n\n private _prefetchQueues: Ref<(() => VuePagePrefetchQueue)[]> = ref([]);\n\n private _initialStateConsumed = false;\n\n private _runningQueues: ComputedRef<VuePagePrefetchQueue[]>;\n\n private _preftechProgress: ComputedRef<VuePagePrefetchProgress>;\n\n private _suspenseComponents: Ref<(() => VNode)[]> = ref([]);\n\n private _err: Ref<(() => VuePageControlError) | null> = ref(null);\n\n private _ErrorComponent: Component;\n\n readonly request?: IncomingMessage;\n\n readonly response?: ServerResponse;\n\n private readonly _serverRedirect?: RedirectFn;\n\n private readonly _writeResponse?: WriteResponseFn;\n\n readonly isClient: boolean;\n\n readonly middleware: VuePageControlMiddlewareFn[];\n\n private _redirectSpec?: VuePageControlRedirectSpec;\n\n private _providedMap: Map<string | InjectionKey<any>, any> = new Map();\n\n private _transitioning = ref(false);\n\n private _serverRedirected = ref(false);\n\n get isServer() {\n return !this.isClient;\n }\n\n get serverRedirected() {\n return this._serverRedirected.value;\n }\n\n get from() {\n return this._from.value;\n }\n\n get to() {\n return this._to.value;\n }\n\n get initialState() {\n return this._initialState;\n }\n\n get beforeRoute() {\n return this._beforeRoute.value;\n }\n\n get route() {\n return this._route.value;\n }\n\n get prefetchQueues() {\n return this._prefetchQueues.value.map((g) => g());\n }\n\n get initialStateConsumed() {\n return this._initialStateConsumed;\n }\n\n get runningQueues() {\n return this._runningQueues.value;\n }\n\n get preftechProgress() {\n return this._preftechProgress.value;\n }\n\n get suspenseComponents() {\n return this._suspenseComponents.value.map((g) => g());\n }\n\n get pageError() {\n const _err = this._err.value;\n return _err && _err();\n }\n\n get ErrorComponent() {\n return this._ErrorComponent;\n }\n\n get transitioning() {\n return this._transitioning.value;\n }\n\n constructor(settings: VuePageControlSettings) {\n super();\n\n const {\n app,\n router,\n initialState = {},\n initialRoute,\n ErrorComponent,\n request,\n response,\n serverRedirect,\n writeResponse,\n middleware = [],\n } = settings;\n\n this.app = app;\n this.router = router;\n this.request = request;\n this.response = response;\n this.isClient = typeof window !== 'undefined';\n this.RouterLink = settings.RouterLink || RouterLink;\n this.useLink = settings.useLink || useLink;\n\n const cookiesContext: CookiesContext = this.isClient\n ? document\n : { req: request, res: response };\n this.cookies = new Cookies(cookiesContext, { bucket: reactive({}) });\n\n this.middleware = middleware;\n this._serverRedirect = serverRedirect;\n\n this._writeResponse = writeResponse;\n\n this._ErrorComponent = ErrorComponent || VErrorPage;\n\n initialState[STATE_BUCKET_SYMBOL] = initialState[STATE_BUCKET_SYMBOL] || {};\n initialState[QUERY_BUCKET_SYMBOL] = initialState[QUERY_BUCKET_SYMBOL] || {};\n\n initialState[STATE_BUCKET_SYMBOL] = reactive<any>(\n initialState[STATE_BUCKET_SYMBOL],\n );\n this._initialState = initialState;\n\n this._route = ref(initialRoute as any) as Ref<ResolvedRouteLocation>;\n this._runningQueues = computed(() =>\n this.prefetchQueues.filter((q) => q.running),\n );\n this._preftechProgress = computed(() => {\n const { prefetchQueues, runningQueues } = this;\n const total = prefetchQueues.length;\n const resolved = total - runningQueues.length;\n const per = total === 0 ? 0 : resolved / total;\n return {\n total,\n resolved,\n per,\n finished: resolved >= total,\n };\n });\n\n (\n [\n '_handleBeforeResolve',\n '_handleAfterEach',\n 'setInitialState',\n 'injectData',\n 'onSuspensePending',\n 'redirect',\n ] as const\n ).forEach((fn) => {\n const _fn = this[fn];\n this[fn] = _fn.bind(this) as any;\n });\n\n if (IN_WINDOW) {\n const initialError = this.consumeInitialState(ERROR_STATE_NAME);\n if (initialError) {\n this._setPageError(initialError);\n }\n }\n\n this._init();\n }\n\n /** @private */\n _getQueryBucket(provideKey: string): string[] {\n const obj = this.initialState[QUERY_BUCKET_SYMBOL] as any;\n let bucket = obj[provideKey] as string[];\n if (!bucket) {\n bucket = [];\n obj[provideKey] = bucket;\n }\n return bucket;\n }\n\n /** @private */\n _pushQueryToBucket(provideKey: string, query: string) {\n const bucket = this._getQueryBucket(provideKey);\n if (!bucket.includes(query)) {\n bucket.push(query);\n }\n }\n\n async initState<T extends object>(\n key: StateInjectionKey<T>,\n initializer: () => T | Promise<T>,\n ): Promise<T> {\n const bucket = this.initialState[STATE_BUCKET_SYMBOL] as any;\n let data = bucket[key as any] as T;\n if (data === undefined) {\n data =\n typeof initializer === 'function' ? await initializer() : initializer;\n }\n bucket[key as any] = reactive(data);\n this.app.provide(key as any, data);\n return data;\n }\n\n useState<T extends object>(key: StateInjectionKey<T>): T | undefined;\n\n useState<T extends object>(\n key: StateInjectionKey<T>,\n defaultValue: T | (() => T),\n ): T;\n\n useState<T extends object>(\n key: StateInjectionKey<T>,\n defaultValue?: T | (() => T),\n ): T | undefined {\n const bucket = this.initialState[STATE_BUCKET_SYMBOL] as any;\n let data = bucket[key as any] as T | undefined;\n if (data === undefined) {\n data =\n typeof defaultValue === 'function'\n ? (defaultValue as any)() // @TODO TS2349: This expression is not callable.\n : defaultValue;\n }\n return data;\n }\n\n provide<T>(key: InjectionKey<T> | string, value: T) {\n this.app.provide(key, value);\n this._providedMap.set(key, value);\n }\n\n inject<T>(key: InjectionKey<T> | string): T | undefined;\n\n inject<T>(\n key: InjectionKey<T> | string,\n defaultValue: T,\n treatDefaultAsFactory?: false,\n ): T;\n\n inject<T>(\n key: InjectionKey<T> | string,\n defaultValue: T | (() => T),\n treatDefaultAsFactory: true,\n ): T;\n\n inject<T>(\n key: InjectionKey<T> | string,\n defaultValue?: T | (() => T),\n treatDefaultAsFactory?: boolean,\n ): T | undefined {\n let value = this._providedMap.get(key);\n if (value === undefined && defaultValue !== undefined) {\n value =\n treatDefaultAsFactory && typeof defaultValue === 'function'\n ? (defaultValue as () => T)()\n : defaultValue;\n }\n return value;\n }\n\n addMiddleware(middleware: VuePageControlMiddlewareFn) {\n this.middleware.push(middleware);\n }\n\n writeResponse(params: WrittenResponse) {\n const { _writeResponse } = this;\n if (!_writeResponse) {\n throw new Error('missing provided writeResponse function.');\n }\n return _writeResponse(params);\n }\n\n onSuspensePending(Component: VNode) {\n if (!this.suspenseComponents.includes(Component)) {\n this._suspenseComponents.value.push(() => Component);\n }\n }\n\n onSuspenseResolved(Component: VNode) {\n const index = this.suspenseComponents.indexOf(Component);\n if (index !== -1) {\n this._suspenseComponents.value.splice(index, 1);\n }\n }\n\n setInitialState(key: string, data: any) {\n if (this.initialState) {\n this.initialState[key] = data;\n }\n }\n\n consumeInitialState(key: string) {\n const { initialState } = this;\n if (!initialState) return;\n const v = initialState[key];\n delete initialState[key];\n return v;\n }\n\n injectData<T, D extends T | undefined, N extends boolean>(\n key: VuePageInjectionKey<T>,\n defaultData?: D,\n allowNull?: N,\n ): D extends T ? T : N extends true ? T | D : T {\n const queue = this.prefetchQueues.find(\n ({ _provideKey }) => _provideKey === key,\n );\n const data = queue && queue.resolvedData;\n const result = data === undefined ? defaultData : data;\n if (!allowNull && result == null) {\n throw new Error(`missing provided data. \"${key}\"`);\n }\n return result as any;\n }\n\n private _stop() {\n if (this._stopFn) {\n this._stopFn();\n delete this._stopFn;\n }\n }\n\n private _init() {\n this._stop();\n\n const fns: (() => void)[] = [];\n fns.push(\n this.router.beforeResolve(this._handleBeforeResolve),\n this.router.afterEach(this._handleAfterEach),\n );\n this._stopFn = () => {\n fns.forEach((fn) => fn());\n delete this._stopFn;\n };\n }\n\n private async _handleAfterEach(\n to: RouteLocationNormalized,\n from: RouteLocationNormalized,\n ) {\n delete this._redirectSpec;\n this._transitioning.value = false;\n\n const { router } = this;\n this._beforeRoute.value = router.resolve(from);\n this._route.value = router.resolve(to);\n this.emit('finish', this.preftechProgress);\n }\n\n private _closePrefetchs() {\n this.prefetchQueues.forEach((queue) => queue.close());\n this._prefetchQueues.value = [];\n }\n\n redirect(redirectSpec: RawVuePageControlRedirectSpec) {\n this._redirectSpec = resolveRawVuePageControlRedirectSpec(redirectSpec);\n if (!this.transitioning) {\n this._triggerRedirect();\n }\n }\n\n private _triggerRedirect() {\n const { _redirectSpec } = this;\n if (!_redirectSpec) return;\n let { path, query, hash } = _redirectSpec;\n const { statusCode, name, params } = _redirectSpec;\n if (name) {\n const tmp = this.router.resolve({ name, params, query, hash });\n path = tmp.path;\n query = tmp.query;\n hash = tmp.hash;\n }\n const queryStr = query ? stringifyQuery(query) : query;\n\n const queryAppends = queryStr\n ? path.includes('?')\n ? `&${queryStr}`\n : `?${queryStr}`\n : '';\n\n let fullPath = `${path}${queryAppends}`;\n\n // apply base url\n if (/^\\//.test(fullPath)) {\n const { base } = this.router.options.history;\n fullPath = `${base}/${fullPath.replace(/^\\//, '')}`;\n }\n\n if (this.isClient) {\n const isInSite = /(^[.]{1,2}\\/)|(^\\/(?!\\/))/.test(fullPath);\n if (isInSite) {\n this.router.replace({ path, query, hash });\n } else {\n window.location.replace(fullPath);\n }\n return false;\n }\n\n if (!this._serverRedirect) {\n throw new Error('required server redirect function.');\n }\n\n this._serverRedirected.value = true;\n this._serverRedirect(fullPath, statusCode);\n }\n\n private async _handleBeforeResolve(\n to: RouteLocationNormalized,\n from: RouteLocationNormalized,\n ) {\n this._to.value = to;\n this._from.value = from;\n\n this._closePrefetchs();\n delete this._redirectSpec;\n\n if (IN_WINDOW && !this.initialStateConsumed && this.pageError) {\n this._initialStateConsumed = true;\n return;\n }\n\n try {\n const { extracted, matched } = extractRouteMatchedItemsWithPrefetch(\n this,\n to,\n from,\n );\n if (!matched.length) {\n throw new VuePageControlError({\n statusCode: 404,\n message: 'This page could not be found',\n });\n }\n\n this._transitioning.value = true;\n\n const allMiddleware = [...this.middleware];\n\n extracted.forEach(({ middleware }) => {\n allMiddleware.push(...middleware);\n });\n\n for (const middlewareFn of allMiddleware) {\n await middlewareFn(this);\n if (this._redirectSpec) {\n break;\n }\n }\n\n if (this._redirectSpec) {\n return this._triggerRedirect();\n }\n\n const requiredItems: RequiredRouteMatchedItemWithPrefetch[] =\n extracted.filter(\n (e) => !!e.prefetch,\n ) as RequiredRouteMatchedItemWithPrefetch[];\n\n if (!requiredItems.length) {\n this._initialStateConsumed = true;\n this._deletePageError();\n return;\n }\n\n const queueSetups: Promise<any>[] = [];\n let queues = requiredItems.map((item) => {\n const queue = new VuePagePrefetchQueue(this, item, to);\n if (queue._setupPromise) {\n queueSetups.push(queue._setupPromise);\n }\n return queue;\n });\n\n this._prefetchQueues.value = queues.map((q) => () => q);\n\n this.emit('start', this.preftechProgress);\n\n await Promise.all(queueSetups);\n\n if (this._redirectSpec) {\n return this._triggerRedirect();\n }\n\n queues.forEach((queue) => {\n const { item, provideKey } = queue;\n if (provideKey) {\n item.updateQueries(this._getQueryBucket(provideKey));\n }\n });\n\n const providePromises: Promise<any>[] = [];\n\n queues = queues.filter(({ _providePromise }) => {\n if (!_providePromise) return false;\n providePromises.push(_providePromise);\n return true;\n });\n\n this._prefetchQueues.value = queues.map((q) => () => q);\n\n await Promise.all(providePromises);\n\n if (this._redirectSpec) {\n return this._triggerRedirect();\n }\n\n this._initialStateConsumed = true;\n this._deletePageError();\n } catch (_err) {\n this._setPageError(_err);\n }\n }\n\n private _deletePageError() {\n this._err.value = null;\n }\n\n private _setPageError(_err: unknown, withEmit = true) {\n const err = new VuePageControlError(_err);\n this._err.value = () => err;\n if (!IN_WINDOW) {\n this.setInitialState(ERROR_STATE_NAME, err.toJSON());\n this._writeStates(err.statusCode);\n }\n withEmit && this.emit('error', err);\n return err;\n }\n\n private _writeStates(status: number) {\n if (IN_WINDOW || !this.response) return;\n if (this.response.headersSent) return;\n this.response.statusCode = status;\n this.writeResponse({\n status,\n });\n }\n}\n\nexport interface VuePagePrefetchQueueEventMap {\n close: void;\n}\n\nexport class VuePagePrefetchQueue extends EV<VuePagePrefetchQueueEventMap> {\n readonly control: VuePageControl;\n\n readonly item: RouteMatchedItemWithPrefetch;\n\n readonly route: RouteLocationNormalized;\n\n private readonly _resolved = ref(false);\n\n private readonly _canceled = ref(false);\n\n private readonly _running: ComputedRef<boolean>;\n\n private _resolvedData?: any;\n\n /** @private */\n _providePromise?: Promise<any>;\n\n /** @private */\n _provideKey?: string;\n\n /** @private */\n _setupPromise?: void | Promise<void>;\n\n get resolved() {\n return this._resolved.value;\n }\n\n get canceled() {\n return this._canceled.value;\n }\n\n get provideKey() {\n return this._provideKey;\n }\n\n get resolvedData() {\n return this._resolvedData;\n }\n\n get params() {\n return this.route.params;\n }\n\n get query() {\n return this.route.query;\n }\n\n get running() {\n return this._running.value;\n }\n\n constructor(\n control: VuePageControl,\n item: RequiredRouteMatchedItemWithPrefetch,\n to: RouteLocationNormalized,\n ) {\n super();\n\n this.control = control;\n this.item = item;\n this.route = to;\n\n (\n ['close', 'cancel', 'provide', 'injectOtherQueue', 'getQuery'] as const\n ).forEach((fn) => {\n const _fn = this[fn];\n this[fn] = _fn.bind(this) as any;\n });\n this._running = computed(() => !this.resolved && !this.canceled);\n\n const prefetchFn =\n typeof item.prefetch === 'function'\n ? item.prefetch\n : item.prefetch.prefetch;\n\n this._setupPromise = prefetchFn(this);\n }\n\n getQuery(key: string): string | undefined;\n\n getQuery(key: string, type: undefined, defaultValue: string): string;\n\n getQuery(key: string, type: StringConstructor): string | undefined;\n\n getQuery(key: string, type: StringConstructor, defaultValue: string): string;\n\n getQuery(key: string, type: NumberConstructor): number | undefined;\n\n getQuery(key: string, type: NumberConstructor, defaultValue: number): number;\n\n getQuery(key: string, type: BooleanConstructor): boolean;\n\n getQuery(\n key: string,\n type: BooleanConstructor,\n defaultValue: boolean,\n ): boolean;\n\n getQuery(\n key: string,\n\n type: RouteQueryType = String,\n defaultValue?: string | number | boolean,\n ): string | number | boolean | undefined {\n const { provideKey } = this;\n if (!provideKey) {\n throw new Error('missing provideKey');\n }\n this.control._pushQueryToBucket(provideKey, key);\n return getRouteQuery(this.query, key, type as any, defaultValue as any);\n }\n\n close() {\n this._canceled.value = true;\n this.emit('close', undefined);\n this.offAll();\n delete this._providePromise;\n delete this._setupPromise;\n delete this._resolvedData;\n delete (this as any).item;\n delete (this as any).control;\n }\n\n cancel() {\n return this.close();\n }\n\n provide<T>(key: VuePageInjectionKey<T>, data: RawProvided<T>) {\n this._provideKey = key as string;\n let promise: Promise<any>;\n\n if (IN_WINDOW && !this.control.initialStateConsumed) {\n const consumed = this.control.consumeInitialState(key as string);\n promise = Promise.resolve(consumed);\n } else if (typeof data !== 'function') {\n promise = Promise.resolve(data);\n } else {\n const payload = (data as any)(this);\n promise = isPromise(payload) ? payload : Promise.resolve(payload);\n }\n\n this._providePromise = promise.then((v) => {\n this._resolved.value = true;\n this._resolvedData = v;\n if (!IN_WINDOW) {\n this.control.setInitialState(key as string, v);\n }\n return v;\n });\n return this._providePromise;\n }\n\n injectOtherQueue<T, D extends T | undefined>(\n key: VuePageInjectionKey<T>,\n defaultData?: D,\n ): Promise<T | D> {\n const queue = this.control.prefetchQueues.find(\n ({ _provideKey }) => _provideKey === key,\n );\n if (!queue || !queue._providePromise)\n return Promise.resolve(defaultData as D);\n\n return queue._providePromise.then((value) => {\n const v = (value === undefined ? defaultData : value) as T | D;\n return v;\n });\n }\n}\n\nexport interface UseVuePageControlOptions {\n onStart?: (progress: VuePagePrefetchProgress) => any;\n onFinish?: (progress: VuePagePrefetchProgress) => any;\n onError?: (error: VuePageControlError) => any;\n}\n\nexport type PrefetchContext<T = any> = {\n key: VuePageInjectionKey<T>;\n prefetch: VuePagePrefetchFn;\n inject: <D extends T | undefined, N extends boolean = false>(\n defaultValue?: D,\n allowNull?: N,\n ) => D extends T ? T : N extends true ? T | D : T;\n};\n\nexport function createPrefetch<T>(\n key: VuePageInjectionKey<T>,\n data: RawProvided<T>,\n): PrefetchContext<T> {\n const prefetch: VuePagePrefetchFn = function prefetch(queue) {\n return queue.provide(key, data);\n };\n\n function inject<D extends T | undefined, N extends boolean>(\n defaultValue?: D,\n allowNull?: N,\n ): D extends T ? T : N extends true ? T | D : T {\n const pageControl = useVuePageControl();\n return pageControl.injectData(key, defaultValue, allowNull);\n }\n\n return { key, prefetch, inject };\n}\n","import { VuePageControl, VuePageControlSettings } from './page-control';\nimport { VuePageControlInjectionKey } from '../injections';\n\ndeclare module 'vue' {\n export interface ComponentCustomProperties {\n $vpc: VuePageControl;\n }\n}\nexport function installVuePageControl(settings: VuePageControlSettings) {\n const pageControl = new VuePageControl(settings);\n settings.app.provide(VuePageControlInjectionKey, pageControl);\n settings.app.config.globalProperties.$vpc = pageControl;\n return pageControl;\n}\n","import { inject, provide } from 'vue';\n\nexport type StateInjectionKey<T extends object> = string;\n\nexport function useState<T extends object>(\n key: StateInjectionKey<T>,\n): T | undefined;\nexport function useState<T extends object>(\n key: StateInjectionKey<T>,\n defaultValue: T | (() => T),\n): T;\nexport function useState<T extends object>(\n key: StateInjectionKey<T>,\n defaultValue?: T | (() => T),\n): T | undefined {\n return inject(key as any, defaultValue, true) as any;\n}\n\nexport function provideState<T extends object>(\n key: StateInjectionKey<T>,\n value: T,\n) {\n return provide(key as any, value);\n}\n","import {\n defineComponent,\n VNode,\n Transition,\n ref,\n computed,\n h,\n PropType,\n} from 'vue';\nimport { RouterView } from 'vue-router';\nimport { withCtx } from '@fastkit/vue-utils';\nimport { RouterViewSlotProps, VuePageKeyOverride } from '../schemes';\nimport {\n generateRouteKeyWithWatchQuery,\n consumeForcePrefetchStates,\n} from '../utils';\nimport { ScrollBehaviorMessenger } from '../composables';\n\nconst DEFAULT_TRANSITION = 'page';\n\nexport const VPage = defineComponent({\n name: 'VPage',\n props: {\n pageKey: {\n type: [Function, String] as PropType<VuePageKeyOverride>,\n default: null,\n },\n },\n setup(props) {\n // const pageControl = useVuePageControl();\n const currentComponent = ref<VNode | null>(null);\n\n const transition = computed(() => {\n const vnode = currentComponent.value;\n if (!vnode) return DEFAULT_TRANSITION;\n return DEFAULT_TRANSITION;\n });\n\n /**\n * @TODO\n */\n // function onSuspensePending(Component: VNode) {\n // currentComponent.value = Component;\n // pageControl.onSuspensePending(Component);\n // }\n\n // function onSuspenseResolved(Component: VNode) {\n // pageControl.onSuspenseResolved(Component);\n // }\n\n return () => (\n <RouterView\n v-slots={{\n default: withCtx((routeProps: RouterViewSlotProps) => {\n const { Component } = routeProps;\n let key = generateRouteKeyWithWatchQuery(routeProps, props.pageKey);\n\n if (consumeForcePrefetchStates(key)) {\n key = `${key}:${Date.now()}`;\n }\n\n return [\n <Transition\n name={transition.value}\n mode=\"out-in\"\n onEnter={(ev) => {\n ScrollBehaviorMessenger.trigger();\n }}\n onEnterCancelled={() => {\n ScrollBehaviorMessenger.release();\n }}>\n {Component ? h(Component, { key }) : undefined}\n {/* {wrapInKeepAlive(\n true,\n Component ? h(Component, { key }) : undefined,\n )} */}\n {/* <KeepAlive>\n {Component ? h(Component, { key }) : undefined}\n </KeepAlive> */}\n {/* <Suspense\n onPending={() => onSuspensePending(Component)}\n onResolve={() => onSuspenseResolved(Component)}\n v-slots={{\n default: () =>\n Component\n ? // ? h(Component, { key: route.path })\n h(Component)\n : undefined,\n }}\n /> */}\n </Transition>,\n ];\n }),\n }}\n />\n );\n },\n});\n","import { defineComponent, computed, CSSProperties, ref, nextTick } from 'vue';\nimport { NumberishPropOption, resolveNumberish } from '@fastkit/vue-utils';\nimport { useVuePageControl } from '../../injections';\n\nexport const VPageProgress = defineComponent({\n name: 'VPageProgress',\n props: {\n color: String,\n failedColor: String,\n height: NumberishPropOption,\n throttle: NumberishPropOption,\n duration: NumberishPropOption,\n continuous: Boolean,\n rtl: Boolean,\n },\n setup(props) {\n useVuePageControl({\n onStart: () => {\n start();\n },\n onFinish: () => {\n finish();\n },\n onError: () => {\n canSucceed.value = false;\n },\n });\n\n let _timer: number | null = null;\n let _throttle: number | null = null;\n let _cut: number | undefined;\n const show = ref(false);\n const percent = ref(0);\n const skipTimerCount = ref(0);\n const canSucceed = ref(true);\n const throttle = computed(() => resolveNumberish(props.throttle, 200));\n const duration = computed(() => resolveNumberish(props.duration, 3000));\n const reversed = ref(props.rtl);\n const continuous = computed(() => props.continuous);\n const color = computed(() => props.color || 'black');\n const failedColor = computed(() => props.failedColor || 'red');\n const height = computed(() => resolveNumberish(props.height, 2));\n const width = computed(() => `${percent.value}%`);\n const styles = computed<CSSProperties>(() => ({\n position: 'fixed',\n top: 0,\n left: props.rtl ? 'auto' : 0,\n right: 0,\n zIndex: 999999,\n width: width.value,\n height: `${height.value}px`,\n background: canSucceed.value ? color.value : failedColor.value,\n opacity: 1,\n // transition: `width ${duration.value}ms`,\n transition: `width 0.1s, opacity 0.4s`,\n }));\n\n function clear() {\n _timer !== null && clearInterval(_timer);\n _throttle !== null && clearTimeout(_throttle);\n _timer = null;\n }\n\n // function set(num: number) {\n // show.value = true;\n // canSucceed.value = true;\n // percent.value = Math.min(100, Math.max(0, Math.floor(num)));\n // }\n\n function increase(num: number) {\n percent.value = Math.min(100, Math.floor(percent.value + num));\n }\n\n function decrease(num: number) {\n percent.value = Math.max(0, Math.floor(percent.value - num));\n }\n\n // function pause() {\n // _timer !== null && window.clearInterval(_timer);\n // }\n\n // function resume() {\n // startTimer();\n // }\n\n function finish() {\n percent.value = reversed.value ? 0 : 100;\n hide();\n }\n\n function hide() {\n clear();\n window.setTimeout(() => {\n show.value = false