From c12ed802569cc671a897937b2c0f78b830bd9283 Mon Sep 17 00:00:00 2001 From: Melonify Date: Thu, 21 Sep 2023 17:07:23 -0400 Subject: [PATCH 01/15] Framework Blocking --- .eslintrc.cjs | 1 + .github/pull_request_template.md | 2 + .prettierrc | 3 +- package.json | 8 +- theming/README.md | 3 + theming/package.json | 10 +- theming/src/backend/css.ts | 212 +++++++ theming/src/composable.ts | 12 + theming/src/index.ts | 7 +- theming/src/manager.ts | 256 ++++++++ theming/src/storage/localstorage.ts | 37 ++ theming/src/themes/auto.ts | 7 + theming/src/themes/base.scss | 7 + theming/src/themes/base.ts | 15 + theming/src/themes/dark.ts | 10 + theming/src/themes/light.ts | 10 + theming/src/transformers/basic.ts | 73 +++ theming/src/transformers/index.ts | 1 + theming/src/transformers/media-query.ts | 53 ++ theming/src/transformers/rgb.ts | 70 ++ theming/src/types/themes.d.ts | 23 + theming/tsconfig.json | 18 +- theming/tsconfig.node.json | 10 - ui/README.md | 1 + ui/package.json | 3 + ui/src/components/UiDraggable.vue | 176 ++++++ ui/src/components/UiFloating.vue | 88 +++ ui/src/components/UiLogo.vue | 135 ++++ ui/src/components/UiScrollable.vue | 317 ++++++++++ ui/src/components/UiSlider.vue | 94 +++ ui/src/components/UiTextInput.vue | 121 ++++ ui/src/index.ts | 6 + ui/tsconfig.app.json | 12 - ui/tsconfig.json | 18 +- ui/tsconfig.node.json | 10 - util-vue/README.md | 1 + util-vue/modules/animation/index.ts | 1 + .../modules/animation/useWebAnimations.ts | 153 +++++ .../composable/createInjectionComposable.ts | 54 ++ .../composable/createMappedComposable.ts | 98 +++ util-vue/modules/composable/index.ts | 2 + util-vue/modules/index.ts | 4 + util-vue/modules/reactivity/class.ts | 41 ++ util-vue/modules/reactivity/index.ts | 1 + util-vue/modules/router/defineAsyncPage.ts | 22 + util-vue/modules/router/getFirstParam.ts | 9 + util-vue/modules/router/getUniqueRouteKey.ts | 29 + util-vue/modules/router/index.ts | 3 + util-vue/package.json | 28 + .../tsconfig.json | 7 +- util/README.md | 1 + util/modules/index.ts | 2 + util/modules/string/findWordAt.ts | 30 + util/modules/string/index.ts | 2 + util/modules/string/toTitleCase.ts | 5 + util/modules/time/index.ts | 1 + util/modules/time/toStrings.ts | 25 + util/package.json | 23 + util/tsconfig.json | 9 + yarn.lock | 597 +++++++++++------- 60 files changed, 2684 insertions(+), 293 deletions(-) create mode 100644 theming/README.md create mode 100644 theming/src/backend/css.ts create mode 100644 theming/src/composable.ts create mode 100644 theming/src/manager.ts create mode 100644 theming/src/storage/localstorage.ts create mode 100644 theming/src/themes/auto.ts create mode 100644 theming/src/themes/base.scss create mode 100644 theming/src/themes/base.ts create mode 100644 theming/src/themes/dark.ts create mode 100644 theming/src/themes/light.ts create mode 100644 theming/src/transformers/basic.ts create mode 100644 theming/src/transformers/index.ts create mode 100644 theming/src/transformers/media-query.ts create mode 100644 theming/src/transformers/rgb.ts create mode 100644 theming/src/types/themes.d.ts delete mode 100644 theming/tsconfig.node.json create mode 100644 ui/README.md create mode 100644 ui/src/components/UiDraggable.vue create mode 100644 ui/src/components/UiFloating.vue create mode 100644 ui/src/components/UiLogo.vue create mode 100644 ui/src/components/UiScrollable.vue create mode 100644 ui/src/components/UiSlider.vue create mode 100644 ui/src/components/UiTextInput.vue delete mode 100644 ui/tsconfig.app.json delete mode 100644 ui/tsconfig.node.json create mode 100644 util-vue/README.md create mode 100644 util-vue/modules/animation/index.ts create mode 100644 util-vue/modules/animation/useWebAnimations.ts create mode 100644 util-vue/modules/composable/createInjectionComposable.ts create mode 100644 util-vue/modules/composable/createMappedComposable.ts create mode 100644 util-vue/modules/composable/index.ts create mode 100644 util-vue/modules/index.ts create mode 100644 util-vue/modules/reactivity/class.ts create mode 100644 util-vue/modules/reactivity/index.ts create mode 100644 util-vue/modules/router/defineAsyncPage.ts create mode 100644 util-vue/modules/router/getFirstParam.ts create mode 100644 util-vue/modules/router/getUniqueRouteKey.ts create mode 100644 util-vue/modules/router/index.ts create mode 100644 util-vue/package.json rename theming/tsconfig.app.json => util-vue/tsconfig.json (55%) create mode 100644 util/README.md create mode 100644 util/modules/index.ts create mode 100644 util/modules/string/findWordAt.ts create mode 100644 util/modules/string/index.ts create mode 100644 util/modules/string/toTitleCase.ts create mode 100644 util/modules/time/index.ts create mode 100644 util/modules/time/toStrings.ts create mode 100644 util/package.json create mode 100644 util/tsconfig.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e3f0f9e..217de8b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,6 +23,7 @@ module.exports = { "no-debugger": "error", "no-undef": "off", quotes: [1, "double"], + "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-namespace": "off", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 915a0d2..74be372 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,3 +3,5 @@ - [ ] Includes breaking changes **(Requires major version bump)** - [ ] Includes changes which modify the shape of the exposed api, in a non breaking way _(Internal packages don't need to be updated)_ **(Requires minor version bump)** - [ ] If relevant, version has been bumped + +For co-development, ensure you link to destination project(s) using `yarn dev:link-project -a PATH` after checking out this PR. diff --git a/.prettierrc b/.prettierrc index 737d821..1777518 100644 --- a/.prettierrc +++ b/.prettierrc @@ -22,5 +22,6 @@ ], "importOrderGroupNamespaceSpecifiers": true, "importOrderSeparation": false, - "importOrderSortSpecifiers": true + "importOrderSortSpecifiers": true, + "importOrderParserPlugins": ["typescript", "decorators-legacy"] } diff --git a/package.json b/package.json index 188333d..7daa657 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ }, "workspaces": [ "theming", - "ui" + "ui", + "util", + "util-vue" ], "dependencies": { "@npmcli/promise-spawn": "^6.0.2", @@ -33,8 +35,8 @@ "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.2.0", "@types/node": "^20.4.9", - "@typescript-eslint/eslint-plugin": "^6.3.0", - "@typescript-eslint/parser": "^6.3.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-typescript": "^11.0.2", "eslint": "^8.45.0", diff --git a/theming/README.md b/theming/README.md new file mode 100644 index 0000000..097555e --- /dev/null +++ b/theming/README.md @@ -0,0 +1,3 @@ +This package requires typescript transpiling by the end user, ensure that your typescript config is compatable with this package's `tsconfig.json`. + +This package relies on import functionality provided by Vite, ensure that your compiler supports these. diff --git a/theming/package.json b/theming/package.json index d28dc63..13c55a8 100644 --- a/theming/package.json +++ b/theming/package.json @@ -2,7 +2,7 @@ "name": "@seventv/theming", "version": "0.1.0", "scripts": { - "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false" + "type-check": "vue-tsc --noEmit --composite false" }, "repository": { "type": "git", @@ -10,15 +10,19 @@ }, "main": "src/index.ts", "files": [ - "src/" + "src/", + "tsconfig.json" ], "dependencies": { - "@types/node": "^20.4.9", + "@seventv/util": "~0", + "@seventv/util-vue": "~0", "typescript": "^5.1.6", + "vite": "^4.4.9", "vue": "^3.3.4" }, "devDependencies": { "@tsconfig/node18": "^18.2.0", + "@types/node": "^20.4.9", "@vue/tsconfig": "^0.4.0", "vue-tsc": "^1.8.6" } diff --git a/theming/src/backend/css.ts b/theming/src/backend/css.ts new file mode 100644 index 0000000..ee483e1 --- /dev/null +++ b/theming/src/backend/css.ts @@ -0,0 +1,212 @@ +import { type Directive, type EffectScope, effectScope, reactive, ref, watch } from "vue"; +import { type ThemeManager } from "../manager"; +import type { Theme } from "../types/themes"; + +export class ThemeCSSBackend { + public readonly manager: ThemeManager; + + private scope: EffectScope; + private disposed: boolean; + + public readonly rootAttribute: string; + public readonly vThemeRoot: Directive; + + public readonly baseStylesheet: OwnedStyleSheet; + public readonly themeStylesheet: OwnedStyleSheet; + + constructor(manager: ThemeManager) { + this.manager = manager; + + this.scope = effectScope(true); + this.disposed = false; + + this.rootAttribute = `${manager.prefix}-root-${manager.id}`; + this.vThemeRoot = (el) => el.setAttribute(this.rootAttribute, ""); + + this.baseStylesheet = this.createOwnedStyleSheet(`${manager.prefix}-${manager.id}-base`); + this.themeStylesheet = this.createOwnedStyleSheet(`${manager.prefix}-${manager.id}-theme`); + + this.scope.run(() => { + watch( + () => [manager.base, this.baseStylesheet.sheet] as const, + ([baseTheme, sheet]) => { + if (sheet) this.updateStylesheet(sheet, baseTheme); + }, + { deep: true, immediate: true }, + ); + + watch( + () => [manager.active, this.themeStylesheet.sheet] as const, + ([active, sheet]) => { + if (sheet) this.updateStylesheet(sheet, active ?? null); + }, + { deep: true, immediate: true }, + ); + }); + } + + dispose() { + if (this.disposed) return false; + + this.scope.stop(); + this.baseStylesheet.dispose(); + this.themeStylesheet.dispose(); + + return true; + } + + createOwnedStyleSheet(id?: string, append = true): OwnedStyleSheet { + const element = document.createElement("link"); + if (id) element.id = id; + element.rel = "stylesheet"; + element.href = "data:text/css,"; + element.type = "text/css"; + + if (append) document.head.appendChild(element); + + const sheet = ref(element.sheet); + const onLoad = () => { + sheet.value = element.sheet; + }; + + element.addEventListener("load", onLoad); + + return reactive({ + ownerNode: element, + sheet, + dispose: () => { + element.removeEventListener("load", onLoad); + element.remove(); + }, + }); + } + + insertStyleSheetRule(sheet: CSSStyleSheet, rule: string, index?: number): CSSRule { + return sheet.cssRules[sheet.insertRule(rule, index ?? sheet.cssRules.length)]; + } + + clearStyleSheet(sheet: CSSStyleSheet) { + try { + for (let x = 0; x < sheet.cssRules.length; x++) { + sheet.deleteRule(x); + } + } catch (e) { + return false; + } + + return true; + } + + updateStylesheet(sheet: CSSStyleSheet, theme: Theme | null) { + if (this.disposed) throw new ThemeCSSBackendAlreadyDisposedError(); + + this.clearStyleSheet(sheet); + + if (!theme) return true; + + try { + const parser = new CSSStyleSheet({ disabled: true }); + parser.replaceSync(theme.cssStyles ?? ""); + + const variableRule = this.insertStyleSheetRule(parser, ":root {}"); + + for (const [color, value] of Object.entries(theme.colors)) { + const str = `rgba(${value.r}, ${value.g}, ${value.b}, ${value.a / 255})`; + variableRule.style.setProperty(`--theme-color-${color}`, str); + } + + if (theme.cssVars) { + for (const [variable, value] of Object.entries(theme.cssVars)) { + variableRule.style.setProperty(`--theme-${variable}`, value); + } + } + + for (const rule of parser.cssRules) { + this.attributizeRule(rule); + + this.insertStyleSheetRule(sheet, rule.cssText); + } + + // Constructed stylesheet parser wont parse @import statements, we'll pass these through verbatim + if (theme.cssStyles) { + for (const rule of theme.cssStyles.matchAll(/@import .*;/g)) { + this.insertStyleSheetRule(sheet, rule[0], 0); + } + } + } catch (e) { + this.clearStyleSheet(sheet); + + return false; + } + + return true; + } + + attributizeRule(rule: CSSRule) { + if (rule instanceof CSSStyleRule) { + const newSelectors: string[] = []; + const selectors = rule.selectorText.split(","); + + for (const selector of selectors) { + const trimmed = selector.trim(); + + if (trimmed.startsWith("#--theme-global ")) { + newSelectors.push(trimmed.replace("#--theme-global ", "")); + continue; + } + + if (trimmed == ":root") { + newSelectors.push(`[${this.rootAttribute}]`); + continue; + } + + newSelectors.push(`[${this.rootAttribute}] ${trimmed}`); + } + + rule.selectorText = newSelectors.join(", "); + } + + if (rule instanceof CSSKeyframesRule) { + rule.name = rule.name.replace(/--theme-/g, `--${this.manager.prefix}-`); + } + + if ("style" in rule) { + const declaration = rule.style as CSSStyleDeclaration; + for (const field of Array.from(declaration)) { + const value = declaration.getPropertyValue(field); + const priority = declaration.getPropertyPriority(field); + + const newField = field.replace(/--theme-/g, `--${this.manager.prefix}-`); + const newValue = value.replace(/--theme-/g, `--${this.manager.prefix}-`); + + if (field != newField) declaration.removeProperty(field); + declaration.setProperty(newField, newValue, priority); + } + } + + if ("cssRules" in rule) { + const children = rule.cssRules as CSSRuleList; + for (const child of children) { + this.attributizeRule(child); + } + } + } +} + +export interface OwnedStyleSheet { + ownerNode: HTMLLinkElement; + sheet: CSSStyleSheet | null; + dispose(): void; +} + +export class ThemeCSSBackendError extends Error { + constructor(readableReason: string) { + super(`ThemeManagerCSSBackend: ${readableReason}`); + } +} + +export class ThemeCSSBackendAlreadyDisposedError extends ThemeCSSBackendError { + constructor() { + super("Tried to access after disposal"); + } +} diff --git a/theming/src/composable.ts b/theming/src/composable.ts new file mode 100644 index 0000000..85478d9 --- /dev/null +++ b/theming/src/composable.ts @@ -0,0 +1,12 @@ +import { type InjectionKey } from "vue"; +import ThemeManager, { type ThemeManagerConfig } from "./manager"; +import { createInjectionComposable } from "@seventv/util-vue/modules/composable"; + +export const THEME_STORE_COMPOSABLE_KEY: InjectionKey = Symbol("composable.themeManager"); + +export const useThemeManager = createInjectionComposable( + THEME_STORE_COMPOSABLE_KEY, + (init?: ThemeManagerConfig) => new ThemeManager(init), + (current) => current.dispose(), + false, +); diff --git a/theming/src/index.ts b/theming/src/index.ts index 2fd1448..18943d0 100644 --- a/theming/src/index.ts +++ b/theming/src/index.ts @@ -1 +1,6 @@ -export const PLACEHOLDER = "This is a placeholder"; +export type * from "./types/themes"; + +export * from "./transformers"; + +export * from "./manager"; +export * from "./composable"; diff --git a/theming/src/manager.ts b/theming/src/manager.ts new file mode 100644 index 0000000..76f89eb --- /dev/null +++ b/theming/src/manager.ts @@ -0,0 +1,256 @@ +import { EffectScope, effectScope, watch } from "vue"; +import { ThemeCSSBackend } from "./backend/css"; +import { ThemeLocalStorageProvider } from "./storage/localstorage"; +import AutoTheme from "./themes/auto"; +import BaseTheme from "./themes/base"; +import DarkTheme from "./themes/dark"; +import LightTheme from "./themes/light"; +import type { Theme, ThemeColor, ThemeDefinition, ThemeStorageProvider } from "./types/themes"; +import { ReactiveClass } from "@seventv/util-vue/modules/reactivity"; + +export class ThemeManager extends ReactiveClass { + private static reactiveFields = { + disposed: ["readonly"], + ready: ["readonly"], + themes: ["readonly"], + active: ["readonly"], + activeId: ["readonly"], + lastUpdateTime: ["readonly"], + lastSaveTime: ["readonly"], + } as const; + + public readonly id: string; + public readonly prefix: string; + public readonly disposed: boolean; + public readonly ready: boolean; + + public readonly css: ThemeCSSBackend; + private storageProvider?: ThemeStorageProvider; + + public readonly defaultTheme: string | null; + public readonly themes: Map; + + public readonly base: Theme; + + private activeScope?: EffectScope; + public readonly active?: Theme; + public readonly activeId?: string; + public readonly lastUpdateTime: number = 0; + public readonly lastSaveTime: number = 0; + + constructor(init: ThemeManagerConfig = {}) { + super(); + + this.id = Math.random().toString(16).slice(2, 10); + this.prefix = init.prefix ?? "seventv"; + this.disposed = false; + this.ready = false; + + if (init.storage?.type == "custom") { + this.storageProvider = init.storage.provider; + } else if (init.storage?.type == "localstorage") { + this.storageProvider = new ThemeLocalStorageProvider(init.storage.key ?? `${this.prefix}-theme`); + } + + this.defaultTheme = init.defaultTheme !== undefined ? init.defaultTheme : "dark"; + this.themes = new Map(); + + this.base = init.baseTheme ?? BaseTheme; + + this.exposeState(ThemeManager.reactiveFields); + + this.css = new ThemeCSSBackend(this); + + this.addThemes( + init.themes ?? { + auto: AutoTheme, + light: LightTheme, + dark: DarkTheme, + }, + ); + + if (init.loadOnInit ?? true) this.loadTheme(); + } + + dispose() { + if (this.stateRaw.disposed) return false; + + this.unloadTheme(); + + this.state.disposed = true; + return true; + } + + // State management + setTheme(id: string | null, save = true) { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + const now = Date.now(); + this.state.lastUpdateTime = now; + if (save) this.saveStorage(id, now); + + this.unloadTheme(); + + let themeDef: ThemeDefinition | undefined; + if (id && this.stateRaw.themes.has(id)) { + themeDef = this.stateRaw.themes.get(id); + } else if (this.defaultTheme) { + id = this.defaultTheme; + themeDef = this.stateRaw.themes.get(this.defaultTheme); + } + + if (id && themeDef) { + this.state.activeId = id; + + if (typeof themeDef == "function") { + const composable = themeDef; + + this.activeScope = effectScope(true); + this.activeScope.run(() => { + watch( + composable(), + (theme) => { + this.state.active = theme; + }, + { immediate: true }, + ); + }); + } else { + this.state.active = themeDef; + } + } + + if (!this.stateRaw.ready) this.state.ready = true; + } + + unloadTheme() { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + if (this.activeScope) this.activeScope.stop(); + this.activeScope = undefined; + this.state.active = undefined; + this.state.activeId = undefined; + } + + // Storage / Initialization + async loadTheme() { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + const loadedFromStorage = await this.loadStorage(); + if (!loadedFromStorage) { + this.setTheme(null, false); + } + } + + async loadStorage() { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + if (!this.storageProvider) return false; + + const result = await this.storageProvider.load(); + if (result) { + this.setTheme(result.value, false); + this.state.lastSaveTime = result.time; + + return true; + } else { + this.state.lastSaveTime = 0; + + return false; + } + } + + async saveStorage(id: string | null, time: number) { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + if (!this.storageProvider) return false; + + const result = await this.storageProvider.save(id, time); + if (result) { + this.state.lastSaveTime = time; + + return true; + } + + return false; + } + + // Definition management + addTheme(id: string, theme: ThemeDefinition) { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + if (this.stateRaw.themes.has(id)) return false; + + this.state.themes.set(id, theme); + return true; + } + + addThemes(themes: Record) { + for (const [id, theme] of Object.entries(themes)) this.addTheme(id, theme); + } + + removeTheme(id: string) { + if (this.stateRaw.disposed) throw new ThemeManagerAlreadyDisposedError(); + + if (!this.stateRaw.themes.has(id)) return false; + + this.state.themes.delete(id); + + if (this.stateRaw.activeId == id) { + this.setTheme(null, false); + } + + return true; + } + + // Helpers + getColor(key: string): ThemeColor | undefined { + return this.active?.colors[key] ?? this.base.colors[key]; + } +} + +export interface ThemeManagerConfig { + prefix?: string; + storage?: ThemeManagerConfigStorage; + defaultTheme?: string | null; + baseTheme?: Theme; + themes?: Record; + loadOnInit?: boolean; +} + +export interface ThemeManagerConfigStorageBase { + type: string; +} + +export interface ThemeManagerConfigStorageNone extends ThemeManagerConfigStorageBase { + type: "none"; +} + +export interface ThemeManagerConfigStorageLocal extends ThemeManagerConfigStorageBase { + type: "localstorage"; + key?: string; +} + +export interface ThemeManagerConfigStorageCustom extends ThemeManagerConfigStorageBase { + type: "custom"; + provider: ThemeStorageProvider; +} + +export type ThemeManagerConfigStorage = + | ThemeManagerConfigStorageNone + | ThemeManagerConfigStorageLocal + | ThemeManagerConfigStorageCustom; + +export class ThemeManagerError extends Error { + constructor(readableReason: string) { + super(`ThemeManager: ${readableReason}`); + } +} + +export class ThemeManagerAlreadyDisposedError extends ThemeManagerError { + constructor() { + super("Tried to access after disposal"); + } +} + +export default ThemeManager; diff --git a/theming/src/storage/localstorage.ts b/theming/src/storage/localstorage.ts new file mode 100644 index 0000000..9f71af0 --- /dev/null +++ b/theming/src/storage/localstorage.ts @@ -0,0 +1,37 @@ +import type { ThemeStorageProvider } from "src"; + +export class ThemeLocalStorageProvider implements ThemeStorageProvider { + public readonly key: string; + + constructor(key: string) { + this.key = key; + } + + async load() { + const raw = window.localStorage.getItem(this.key); + if (!raw) return undefined; + + const reMatch = raw.match(/(\d+),(.*)/); + if (!reMatch) return undefined; + + const time = parseInt(reMatch[1]); + const value = reMatch[2]; + if (isNaN(time) || !value) return undefined; + + return { + time, + value, + }; + } + + async save(value: string | null, time: number) { + if (value) { + const raw = `${time.toString()},${value}`; + window.localStorage.setItem(this.key, raw); + } else { + window.localStorage.removeItem(this.key); + } + + return true; + } +} diff --git a/theming/src/themes/auto.ts b/theming/src/themes/auto.ts new file mode 100644 index 0000000..6467d6d --- /dev/null +++ b/theming/src/themes/auto.ts @@ -0,0 +1,7 @@ +import DarkTheme from "./dark"; +import LightTheme from "./light"; +import mediaQueryTransformer from "../transformers/media-query"; + +export const AutoTheme = mediaQueryTransformer(LightTheme, ["(prefers-color-scheme: dark)", DarkTheme]); + +export default AutoTheme; diff --git a/theming/src/themes/base.scss b/theming/src/themes/base.scss new file mode 100644 index 0000000..be4ed79 --- /dev/null +++ b/theming/src/themes/base.scss @@ -0,0 +1,7 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap"); + +:root { + font-family: "Inter"; + color: var(--theme-color-text); + background-color: var(--theme-color-background); +} diff --git a/theming/src/themes/base.ts b/theming/src/themes/base.ts new file mode 100644 index 0000000..1be79c2 --- /dev/null +++ b/theming/src/themes/base.ts @@ -0,0 +1,15 @@ +import BaseStylesheet from "./base.scss?inline"; +import { basicTheme } from "../transformers/basic"; + +export const BaseTheme = basicTheme({ + colors: { + primary: "#29B6F6", + brand: { + "7tv": "#29B6F6", + subscription: "#DCAA32", + }, + }, + cssStyles: BaseStylesheet, +}); + +export default BaseTheme; diff --git a/theming/src/themes/dark.ts b/theming/src/themes/dark.ts new file mode 100644 index 0000000..ee1ebbd --- /dev/null +++ b/theming/src/themes/dark.ts @@ -0,0 +1,10 @@ +import { basicTheme } from "../transformers/basic"; + +export const DarkTheme = basicTheme({ + colors: { + text: "#FFFFFF", + background: "#000000", + }, +}); + +export default DarkTheme; diff --git a/theming/src/themes/light.ts b/theming/src/themes/light.ts new file mode 100644 index 0000000..4276158 --- /dev/null +++ b/theming/src/themes/light.ts @@ -0,0 +1,10 @@ +import { basicTheme } from "../transformers/basic"; + +export const LightTheme = basicTheme({ + colors: { + text: "#0A0A0A", + background: "#FFFFFF", + }, +}); + +export default LightTheme; diff --git a/theming/src/transformers/basic.ts b/theming/src/transformers/basic.ts new file mode 100644 index 0000000..4609373 --- /dev/null +++ b/theming/src/transformers/basic.ts @@ -0,0 +1,73 @@ +import { colorFromString } from "./rgb"; +import type { Theme, ThemeColor } from "../types/themes"; + +export function basicTheme(input: BasicTheme): Theme { + const colors = >{}; + + for (const [key, def] of Object.entries(input.colors)) { + parseColorDefinition(colors, key, def); + } + + return { + ...input, + colors, + }; +} + +function parseColorDefinition( + map: Record, + key: string, + def: BasicThemeColorDefinition, + parent?: string, +) { + key = processColorPath(key); + + if (parent) { + if (key == "&") key = parent; + else key = `${parent}-${key}`; + } + + if (typeof def == "string") { + map[key] = colorFromString(def); + } else if (isThemeColor(def)) { + map[key] = def; + } else { + for (const [childKey, child] of Object.entries(def)) { + parseColorDefinition(map, childKey, child, key); + } + } +} + +function processColorPath(path: string) { + const first = path.slice(0, 1).toLowerCase(); + const rest = path.slice(1).replace(/[A-Z1-9]/g, (str) => `-${str.toLowerCase()}`); + + return first + rest; +} + +function isThemeColor(object: object): object is ThemeColor { + const structure: Record = { r: "number", g: "number", b: "number", a: "number" }; + + for (const key of new Set([...Object.keys(structure), ...Object.keys(object)])) { + if (structure[key] != typeof Reflect.get(object, key)) return false; + } + + return true; +} + +type BasicTheme = Modify< + Theme, + { + colors: Record; + } +>; + +type ThemeColorShorthand = string; + +type BasicThemeColor = ThemeColor | ThemeColorShorthand; + +type BasicThemeColorDefinition = + | BasicThemeColor + | ({ [x: string]: BasicThemeColorDefinition } & { "&"?: BasicThemeColor }); + +type Modify = Omit & R; diff --git a/theming/src/transformers/index.ts b/theming/src/transformers/index.ts new file mode 100644 index 0000000..c142f6b --- /dev/null +++ b/theming/src/transformers/index.ts @@ -0,0 +1 @@ +export const PLACEHOLDER = ""; diff --git a/theming/src/transformers/media-query.ts b/theming/src/transformers/media-query.ts new file mode 100644 index 0000000..6e000dc --- /dev/null +++ b/theming/src/transformers/media-query.ts @@ -0,0 +1,53 @@ +import { type Ref, computed, onScopeDispose, ref } from "vue"; +import type { Theme } from "../types/themes"; + +export function mediaQueryTransformer(fallback: Theme, ...states: [string, Theme][]) { + return () => { + const queryStates: { + query: MediaQueryList; + callback: (ev: MediaQueryListEvent) => void; + matches: Ref; + result: Theme; + }[] = []; + + for (const state of states) { + if (state instanceof Array) { + const query = window.matchMedia(state[0]); + const matches = ref(query.matches); + const callback = (ev: MediaQueryListEvent) => { + matches.value = ev.matches; + }; + + query.addEventListener("change", callback); + + queryStates.push({ + query, + matches, + callback, + result: state[1], + }); + } else { + fallback = state; + break; + } + } + + onScopeDispose(() => { + for (const state of queryStates) { + state.query.removeEventListener("change", state.callback); + } + }); + + return computed(() => { + for (const state of queryStates) { + if (state.matches.value) { + return state.result; + } + } + + return fallback; + }); + }; +} + +export default mediaQueryTransformer; diff --git a/theming/src/transformers/rgb.ts b/theming/src/transformers/rgb.ts new file mode 100644 index 0000000..fca7442 --- /dev/null +++ b/theming/src/transformers/rgb.ts @@ -0,0 +1,70 @@ +import type { ThemeColor } from "../types/themes"; + +export function colorRGB(r: number, g: number, b: number): ThemeColor { + return colorRGBA(r, g, b, 255); +} + +export function colorRGBA(r: number, g: number, b: number, a: number): ThemeColor { + return { + r, + g, + b, + a, + }; +} + +export function colorFromString(color: string): ThemeColor { + try { + return colorFromHex(color); + } catch { + void 0; + } + + try { + return colorFromHexShort(color); + } catch { + void 0; + } + + throw new SyntaxError(); +} + +export function colorFromHex(hex: string): ThemeColor { + const match = hex.match(/^#?([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})?$/); + if (!match) throw new SyntaxError(); + + const r = parseInt(match[1], 16); + const g = parseInt(match[2], 16); + const b = parseInt(match[3], 16); + const a = parseInt(match[4], 16); + + if (isNaN(r) || isNaN(g) || isNaN(b)) { + throw new SyntaxError(); + } + + if (isNaN(a)) { + return colorRGB(r, g, b); + } else { + return colorRGBA(r, g, b, a); + } +} + +export function colorFromHexShort(hex: string): ThemeColor { + const match = hex.match(/^#?([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])?$/); + if (!match) throw new SyntaxError(); + + const r = parseInt(`${match[1]}${match[1]}`, 16); + const g = parseInt(`${match[2]}${match[2]}`, 16); + const b = parseInt(`${match[3]}${match[3]}`, 16); + const a = parseInt(`${match[4]}${match[4]}`, 16); + + if (isNaN(r) || isNaN(g) || isNaN(b)) { + throw new SyntaxError(); + } + + if (isNaN(a)) { + return colorRGB(r, g, b); + } else { + return colorRGBA(r, g, b, a); + } +} diff --git a/theming/src/types/themes.d.ts b/theming/src/types/themes.d.ts new file mode 100644 index 0000000..b2ece73 --- /dev/null +++ b/theming/src/types/themes.d.ts @@ -0,0 +1,23 @@ +import type { Ref } from "vue"; + +export interface Theme { + colors: Record; + cssVars?: Record; + cssStyles?: string; +} + +export interface ThemeColor { + r: number; + g: number; + b: number; + a: number; +} + +export interface ThemeStorageProvider { + save: (value: string | null, time: number) => Promise; + load: () => Promise<{ value: string; time: number } | undefined>; +} + +export type ComposedTheme = () => Ref; + +export type ThemeDefinition = Theme | ComposedTheme; diff --git a/theming/tsconfig.json b/theming/tsconfig.json index ae91ebf..e6a74a7 100644 --- a/theming/tsconfig.json +++ b/theming/tsconfig.json @@ -1,11 +1,11 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.node.json" - }, - { - "path": "./tsconfig.app.json" - } - ] + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "types": ["vite/client"], + "baseUrl": ".", + "experimentalDecorators": true + } } diff --git a/theming/tsconfig.node.json b/theming/tsconfig.node.json deleted file mode 100644 index fe10763..0000000 --- a/theming/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@tsconfig/node18/tsconfig.json", - "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "types": ["node"] - } -} diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..8f7d09c --- /dev/null +++ b/ui/README.md @@ -0,0 +1 @@ +This package requires typescript transpiling by the end user, ensure that your typescript config is compatable with this package's `tsconfig.json`. diff --git a/ui/package.json b/ui/package.json index abb1697..36e84d6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,8 +13,11 @@ "src/" ], "dependencies": { + "@floating-ui/dom": "^1.5.2", "@seventv/theming": "~0", + "@seventv/util-vue": "~0", "@types/node": "^20.4.9", + "@vueuse/core": "^10.3.0", "typescript": "^5.1.6", "vue": "^3.3.4", "vue-router": "^4.2.4" diff --git a/ui/src/components/UiDraggable.vue b/ui/src/components/UiDraggable.vue new file mode 100644 index 0000000..d2a403a --- /dev/null +++ b/ui/src/components/UiDraggable.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/ui/src/components/UiFloating.vue b/ui/src/components/UiFloating.vue new file mode 100644 index 0000000..b77220b --- /dev/null +++ b/ui/src/components/UiFloating.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/ui/src/components/UiLogo.vue b/ui/src/components/UiLogo.vue new file mode 100644 index 0000000..23a7e75 --- /dev/null +++ b/ui/src/components/UiLogo.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/ui/src/components/UiScrollable.vue b/ui/src/components/UiScrollable.vue new file mode 100644 index 0000000..5346dd9 --- /dev/null +++ b/ui/src/components/UiScrollable.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/ui/src/components/UiSlider.vue b/ui/src/components/UiSlider.vue new file mode 100644 index 0000000..6ded4bf --- /dev/null +++ b/ui/src/components/UiSlider.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/ui/src/components/UiTextInput.vue b/ui/src/components/UiTextInput.vue new file mode 100644 index 0000000..0270ba9 --- /dev/null +++ b/ui/src/components/UiTextInput.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/ui/src/index.ts b/ui/src/index.ts index 5c6ca42..fb46b11 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -1,2 +1,8 @@ export { default as UiButton } from "./components/UiButton.vue"; +export { default as UiDraggable } from "./components/UiDraggable.vue"; +export { default as UiFloating } from "./components/UiFloating.vue"; export { default as UiLoadingBar } from "./components/UiLoadingBar.vue"; +export { default as UiLogo } from "./components/UiLogo.vue"; +export { default as UiScrollable } from "./components/UiScrollable.vue"; +export { default as UiSlider } from "./components/UiSlider.vue"; +export { default as UiTextInput } from "./components/UiTextInput.vue"; diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json deleted file mode 100644 index d1610cd..0000000 --- a/ui/tsconfig.app.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], - "exclude": ["src/**/__tests__/*"], - "compilerOptions": { - "composite": true, - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - } -} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index ae91ebf..e6a74a7 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,11 +1,11 @@ { - "files": [], - "references": [ - { - "path": "./tsconfig.node.json" - }, - { - "path": "./tsconfig.app.json" - } - ] + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "types": ["vite/client"], + "baseUrl": ".", + "experimentalDecorators": true + } } diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json deleted file mode 100644 index fe10763..0000000 --- a/ui/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "@tsconfig/node18/tsconfig.json", - "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "types": ["node"] - } -} diff --git a/util-vue/README.md b/util-vue/README.md new file mode 100644 index 0000000..8f7d09c --- /dev/null +++ b/util-vue/README.md @@ -0,0 +1 @@ +This package requires typescript transpiling by the end user, ensure that your typescript config is compatable with this package's `tsconfig.json`. diff --git a/util-vue/modules/animation/index.ts b/util-vue/modules/animation/index.ts new file mode 100644 index 0000000..b98e08a --- /dev/null +++ b/util-vue/modules/animation/index.ts @@ -0,0 +1 @@ +export * from "./useWebAnimations"; diff --git a/util-vue/modules/animation/useWebAnimations.ts b/util-vue/modules/animation/useWebAnimations.ts new file mode 100644 index 0000000..860bf1c --- /dev/null +++ b/util-vue/modules/animation/useWebAnimations.ts @@ -0,0 +1,153 @@ +import { EffectScope, onScopeDispose, watch } from "vue"; + +export function useWebAnimations>(animationList?: L) { + function getAnimation(animation: AnimationInput | keyof L | undefined | null): AnimationInput | undefined { + if (!animation) return undefined; + + if (typeof animation == "object") { + return animation; + } else if (animationList) { + return animationList[animation]; + } + + return undefined; + } + + let activeScope: EffectScope | undefined; + const active = new Set(); + + let queued: AnimationInput | keyof L | undefined | null; + + function finishedCallback(event: AnimationPlaybackEvent) { + const anim = event.target as Animation; + + anim.removeEventListener("finish", finishedCallback); + anim.cancel(); + active.delete(anim); + + if (active.size < 1 && queued) { + play(queued); + } + } + + function stop() { + if (activeScope) { + activeScope.stop(); + activeScope = undefined; + } + + queued = undefined; + + for (const anim of active) { + anim.removeEventListener("finish", finishedCallback); + anim.cancel(); + } + + active.clear(); + } + + function play(target: AnimationInput | keyof L | undefined | null) { + stop(); + + activeScope = new EffectScope(); + activeScope.run(() => { + watch( + () => getAnimation(target), + (input) => { + play(input); + }, + { deep: true }, + ); + }); + + const input = getAnimation(target); + if (!input) return false; + + const animations: Animation[] = []; + + for (const animation of input) { + if (!animation.element) return false; + + animations.push( + new Animation(new KeyframeEffect(animation.element, animation.keyframes, animation.options)), + ); + } + + for (const animation of animations) { + animation.addEventListener("finish", finishedCallback); + animation.play(); + + active.add(animation); + } + + return true; + } + + function pause() { + for (const animation of active) { + animation.pause(); + } + } + + function resume() { + for (const animation of active) { + if (animation.playState == "paused") animation.play(); + } + } + + function playNext(target: AnimationInput | keyof L | undefined | null) { + queued = target; + + if (active.size > 0) { + finishIteration(); + } else { + play(queued); + } + } + + function finishIteration() { + for (const animation of active) { + if (!animation.effect) continue; + if (animation.playState == "finished") continue; + if (typeof animation.currentTime != "number") continue; + + const time = animation.currentTime; + const timing = animation.effect.getComputedTiming(); + const duration = timing.duration; + + let newTime = time; + if (typeof duration == "number") { + const delay = timing.delay ?? 0; + newTime = ((time - delay) % duration) + delay; + } + + animation.effect.updateTiming({ + iterations: 1, + endDelay: 0, + }); + + animation.currentTime = newTime; + } + } + + onScopeDispose(() => { + stop(); + }); + + return { + stop, + play, + pause, + resume, + playNext, + finishIteration, + }; +} + +type AnimationInput = { + element?: Element; + keyframes: Keyframe[]; + options: KeyframeEffectOptions; +}[]; + +export default useWebAnimations; diff --git a/util-vue/modules/composable/createInjectionComposable.ts b/util-vue/modules/composable/createInjectionComposable.ts new file mode 100644 index 0000000..1042148 --- /dev/null +++ b/util-vue/modules/composable/createInjectionComposable.ts @@ -0,0 +1,54 @@ +import { type InjectionKey, inject, onScopeDispose, provide } from "vue"; + +type ResultDefault = { + (createIfUnprovided?: true | undefined, ...args: F): T; + (createIfUnprovided: false): T | undefined; +}; + +type ResultNoDefault = { + (createIfUnprovided?: false | undefined): T | undefined; + (createIfUnprovided: true, ...args: F): T; +}; + +export function createInjectionComposable( + key: InjectionKey | null | undefined, + factory: (...args: F) => T, + disposal?: (current: T) => void, + defaultCreateIfUnprovided?: true | undefined, +): ResultDefault; +export function createInjectionComposable( + key: InjectionKey | null | undefined, + factory: (...args: F) => T, + disposal: ((current: T) => void) | undefined, + defaultCreateIfUnprovided: false, +): ResultNoDefault; +export function createInjectionComposable( + key: InjectionKey | null | undefined, + factory: (...args: F) => T, + disposal?: (current: T) => void, + defaultCreateIfUnprovided = true, +) { + const symbol = key ?? Symbol(); + + return (createIfUnprovided = defaultCreateIfUnprovided, ...args: F) => { + const currentState = inject(symbol, undefined); + + if (currentState) { + return currentState; + } else if (createIfUnprovided) { + const newState = factory(...args); + + if (disposal) { + onScopeDispose(() => { + disposal(newState); + }); + } + + provide(symbol, newState); + + return newState; + } + }; +} + +export default createInjectionComposable; diff --git a/util-vue/modules/composable/createMappedComposable.ts b/util-vue/modules/composable/createMappedComposable.ts new file mode 100644 index 0000000..9c960a4 --- /dev/null +++ b/util-vue/modules/composable/createMappedComposable.ts @@ -0,0 +1,98 @@ +import { type EffectScope, effectScope, onScopeDispose } from "vue"; + +type ResultDefault = undefined extends I + ? { + (key?: I, createIfUnset?: true | undefined, ...args: F): T; + (key: I, createIfUnset: false): T | undefined; + } + : { + (key: I, createIfUnset?: true | undefined, ...args: F): T; + (key: I, createIfUnset: false): T | undefined; + }; + +type ResultNoDefault = undefined extends I + ? { + (key?: I, createIfUnset?: false | undefined): T | undefined; + (key: I, createIfUnset: true, ...args: F): T; + } + : { + (key: I, createIfUnset?: false | undefined): T | undefined; + (key: I, createIfUnset: true, ...args: F): T; + }; + +export function createMappedComposable( + factory: (key: K, ...args: F) => T, + disposal?: (key: K, current: T) => void, + processKey?: (input: I) => K, + defaultCreateIfUnset?: true | undefined, +): ResultDefault; +export function createMappedComposable( + factory: (key: K, ...args: F) => T, + disposal: ((key: K, current: T) => void) | undefined, + processKey: ((input: I) => K) | undefined, + defaultCreateIfUnset: false, +): ResultNoDefault; +export function createMappedComposable( + factory: (key: K, ...args: F) => T, + disposal?: (key: K, current: T) => void, + processKey?: (input: I) => K, + defaultCreateIfUnset = true, +) { + const map = new Map(); + + function free(key: K) { + const current = map.get(key); + if (current) { + const [state, scope, users] = current; + const newUsers = users - 1; + + if (newUsers < 1) { + map.delete(key); + scope.stop(); + } else { + map.set(key, [state, scope, newUsers]); + } + } + } + + function processor(input: unknown): K { + return processKey ? processKey(input) : input; + } + + return (key: I, createIfUnset = defaultCreateIfUnset, ...args: F) => { + const itemKey = processor(key); + const current = map.get(itemKey); + + if (current) { + const [state, scope, users] = current; + + map.set(itemKey, [state, scope, users + 1]); + onScopeDispose(() => free(itemKey)); + + return state; + } else if (createIfUnset) { + const scope = effectScope(true); + const state = scope.run(() => { + const state = factory(itemKey, ...args); + + if (disposal) { + onScopeDispose(() => disposal(itemKey, state)); + } + + return state; + }); + + if (state == undefined) { + scope.stop(); + return; + } + + map.set(itemKey, [state, scope, 1]); + onScopeDispose(() => free(itemKey)); + + return state; + } + }; +} + +export default createMappedComposable; diff --git a/util-vue/modules/composable/index.ts b/util-vue/modules/composable/index.ts new file mode 100644 index 0000000..dc98522 --- /dev/null +++ b/util-vue/modules/composable/index.ts @@ -0,0 +1,2 @@ +export * from "./createInjectionComposable"; +export * from "./createMappedComposable"; diff --git a/util-vue/modules/index.ts b/util-vue/modules/index.ts new file mode 100644 index 0000000..9e279b1 --- /dev/null +++ b/util-vue/modules/index.ts @@ -0,0 +1,4 @@ +export * as animation from "./animation"; +export * as composable from "./composable"; +export * as reactivity from "./reactivity"; +export * as router from "./router"; diff --git a/util-vue/modules/reactivity/class.ts b/util-vue/modules/reactivity/class.ts new file mode 100644 index 0000000..c3169ea --- /dev/null +++ b/util-vue/modules/reactivity/class.ts @@ -0,0 +1,41 @@ +import { type UnwrapNestedRefs, reactive, readonly } from "vue"; + +type StateStore = { -readonly [key in keyof R & keyof T]: T[key] }; + +export class ReactiveClass { + protected state!: UnwrapNestedRefs>; + protected stateRaw!: StateStore; + protected stateReadonly!: UnwrapNestedRefs>; + + protected exposeState(stateProps: R) { + this.stateRaw = {} as StateStore; + this.state = reactive(this.stateRaw); + this.stateReadonly = readonly(this.state) as UnwrapNestedRefs>; + + for (const [prop, state] of Object.entries(stateProps)) { + const key = prop as keyof StateStore; + + const readOnly = state.includes("readonly"); + + Reflect.set(this.stateRaw, key, this[key]); + + Reflect.defineProperty(this, key, { + get: () => { + if (readOnly) return this.stateReadonly[key]; + else return this.state[key]; + }, + set: (val: this[typeof key]) => { + if (readOnly) Reflect.set(this.stateReadonly, key, val); + else Reflect.set(this.state, key, val); + }, + }); + } + } +} + +export interface ClassReactivityProps { + [key: string]: readonly ClassReactivityProp[]; +} + +export type ClassReactivityPropReadonly = "readonly"; +export type ClassReactivityProp = ClassReactivityPropReadonly; diff --git a/util-vue/modules/reactivity/index.ts b/util-vue/modules/reactivity/index.ts new file mode 100644 index 0000000..936ab95 --- /dev/null +++ b/util-vue/modules/reactivity/index.ts @@ -0,0 +1 @@ +export * from "./class"; diff --git a/util-vue/modules/router/defineAsyncPage.ts b/util-vue/modules/router/defineAsyncPage.ts new file mode 100644 index 0000000..a3ff416 --- /dev/null +++ b/util-vue/modules/router/defineAsyncPage.ts @@ -0,0 +1,22 @@ +import { type Component, defineComponent, h } from "vue"; + +interface ComponentConstructor { + new (): Component; +} + +export function defineAsyncPage(moduleResolver: () => Promise<{ default: ComponentConstructor }>) { + const name = `AsyncPage-${Math.random().toString(36).substring(2)}`; + + return defineComponent({ + displayName: name, + async setup() { + const module = await moduleResolver(); + + const component = module.default; + + return () => h(component); + }, + }); +} + +export default defineAsyncPage; diff --git a/util-vue/modules/router/getFirstParam.ts b/util-vue/modules/router/getFirstParam.ts new file mode 100644 index 0000000..fee20d1 --- /dev/null +++ b/util-vue/modules/router/getFirstParam.ts @@ -0,0 +1,9 @@ +import type { RouteLocationNormalizedLoaded } from "vue-router"; + +export function getFirstParam(route: RouteLocationNormalizedLoaded, param: string): string | undefined { + const value = route.params[param]; + + return value instanceof Array ? value[0] : value; +} + +export default getFirstParam; diff --git a/util-vue/modules/router/getUniqueRouteKey.ts b/util-vue/modules/router/getUniqueRouteKey.ts new file mode 100644 index 0000000..f522238 --- /dev/null +++ b/util-vue/modules/router/getUniqueRouteKey.ts @@ -0,0 +1,29 @@ +import type { RouteLocationNormalizedLoaded } from "vue-router"; + +export function getUniqueRouteKey(route: RouteLocationNormalizedLoaded) { + if (route.meta.dependsOn instanceof Array) { + const deps = route.meta.dependsOn; + + const parts = []; + + for (const [param, value] of Object.entries(route.params)) { + if (deps.includes(param)) { + let string = `${param}`; + + if (value instanceof Array) { + string += `:${value.join(":")}`; + } else { + string += `:${value}`; + } + + parts.push(string); + } + } + + return parts.join("-"); + } + + return undefined; +} + +export default getUniqueRouteKey; diff --git a/util-vue/modules/router/index.ts b/util-vue/modules/router/index.ts new file mode 100644 index 0000000..24f5ae9 --- /dev/null +++ b/util-vue/modules/router/index.ts @@ -0,0 +1,3 @@ +export * from "./defineAsyncPage"; +export * from "./getFirstParam"; +export * from "./getUniqueRouteKey"; diff --git a/util-vue/package.json b/util-vue/package.json new file mode 100644 index 0000000..cd64407 --- /dev/null +++ b/util-vue/package.json @@ -0,0 +1,28 @@ +{ + "name": "@seventv/util-vue", + "version": "0.1.0", + "scripts": { + "type-check": "vue-tsc --noEmit --composite false" + }, + "repository": { + "type": "git", + "url": "https://github.com/SevenTV/WebComponents.git" + }, + "main": "modules/index.ts", + "files": [ + "modules/", + "tsconfig.json" + ], + "dependencies": { + "typescript": "^5.1.6", + "vite": "^4.4.9", + "vue": "^3.3.4", + "vue-router": "^4.2.4" + }, + "devDependencies": { + "@tsconfig/node18": "^18.2.0", + "@types/node": "^20.4.9", + "@vue/tsconfig": "^0.4.0", + "vue-tsc": "^1.8.6" + } +} diff --git a/theming/tsconfig.app.json b/util-vue/tsconfig.json similarity index 55% rename from theming/tsconfig.app.json rename to util-vue/tsconfig.json index d1610cd..6a49c20 100644 --- a/theming/tsconfig.app.json +++ b/util-vue/tsconfig.json @@ -1,12 +1,11 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "include": ["env.d.ts", "modules/**/*", "modules/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "types": ["vite/client"], "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } + "experimentalDecorators": true } } diff --git a/util/README.md b/util/README.md new file mode 100644 index 0000000..8f7d09c --- /dev/null +++ b/util/README.md @@ -0,0 +1 @@ +This package requires typescript transpiling by the end user, ensure that your typescript config is compatable with this package's `tsconfig.json`. diff --git a/util/modules/index.ts b/util/modules/index.ts new file mode 100644 index 0000000..2b83717 --- /dev/null +++ b/util/modules/index.ts @@ -0,0 +1,2 @@ +export * as string from "./string"; +export * as time from "./time"; diff --git a/util/modules/string/findWordAt.ts b/util/modules/string/findWordAt.ts new file mode 100644 index 0000000..d3bac8b --- /dev/null +++ b/util/modules/string/findWordAt.ts @@ -0,0 +1,30 @@ +export function findWordAt(searchText: string, searchStart: number) { + let start = 0; + let end = 0; + + if (searchText.length === 0) return; + + for (let i = searchStart; ; i--) { + if (i < 1 || (searchText.charAt(i - 1) === " " && i !== searchStart)) { + start = i; + break; + } + } + + for (let i = searchStart + 1; ; i++) { + if (i > searchText.length || searchText.charAt(i - 1) === " ") { + end = i - 1; + break; + } + } + + if (searchStart != start) { + const word = searchText.substring(start, end); + + if (word !== " ") { + return { start, end, word }; + } + } +} + +export default findWordAt; diff --git a/util/modules/string/index.ts b/util/modules/string/index.ts new file mode 100644 index 0000000..f5939a6 --- /dev/null +++ b/util/modules/string/index.ts @@ -0,0 +1,2 @@ +export * from "./findWordAt"; +export * from "./toTitleCase"; diff --git a/util/modules/string/toTitleCase.ts b/util/modules/string/toTitleCase.ts new file mode 100644 index 0000000..252126d --- /dev/null +++ b/util/modules/string/toTitleCase.ts @@ -0,0 +1,5 @@ +export function toTitleCase(string: string) { + return string.replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.substring(1)); +} + +export default toTitleCase; diff --git a/util/modules/time/index.ts b/util/modules/time/index.ts new file mode 100644 index 0000000..5f7a14d --- /dev/null +++ b/util/modules/time/index.ts @@ -0,0 +1 @@ +export * from "./toStrings"; diff --git a/util/modules/time/toStrings.ts b/util/modules/time/toStrings.ts new file mode 100644 index 0000000..3df383c --- /dev/null +++ b/util/modules/time/toStrings.ts @@ -0,0 +1,25 @@ +export function secondsToString(time: number) { + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) % 60; + const seconds = Math.floor(time) % 60; + + const hoursS = hours.toString().padStart(2, "0"); + const minutesS = minutes.toString().padStart(2, "0"); + const secondsS = seconds.toString().padStart(2, "0"); + + return `${hours > 0 ? `${hoursS}:` : ""}${minutesS}:${secondsS}`; +} + +export function secondsToDurationString(time: number) { + const hours = Math.floor(time / 60 / 60); + const minutes = Math.floor(time / 60) % 60; + const seconds = Math.floor(time) % 60; + + const words: string[] = []; + + if (hours > 0) words.push(`${hours}h`); + if (minutes > 0) words.push(`${minutes}m`); + if (seconds > 0 || words.length == 0) words.push(`${seconds}s`); + + return words.join(" "); +} diff --git a/util/package.json b/util/package.json new file mode 100644 index 0000000..424ed6a --- /dev/null +++ b/util/package.json @@ -0,0 +1,23 @@ +{ + "name": "@seventv/util", + "version": "0.1.0", + "scripts": { + "type-check": "tsc --noEmit --composite false" + }, + "repository": { + "type": "git", + "url": "https://github.com/SevenTV/WebComponents.git" + }, + "main": "modules/index.ts", + "files": [ + "modules/", + "tsconfig.json" + ], + "dependencies": { + "typescript": "^5.1.6" + }, + "devDependencies": { + "@tsconfig/node18": "^18.2.0", + "@types/node": "^20.4.9" + } +} diff --git a/util/tsconfig.json b/util/tsconfig.json new file mode 100644 index 0000000..065ae56 --- /dev/null +++ b/util/tsconfig.json @@ -0,0 +1,9 @@ +{ + "include": ["env.d.ts", "modules/**/*"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "experimentalDecorators": true + } +} diff --git a/yarn.lock b/yarn.lock index 316c8d4..bb6a32a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,12 +7,12 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" - integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== dependencies: - "@babel/highlight" "^7.22.10" + "@babel/highlight" "^7.22.13" chalk "^2.4.2" "@babel/generator@7.17.7": @@ -25,11 +25,11 @@ source-map "^0.5.0" "@babel/generator@^7.17.3": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" - integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.15.tgz#1564189c7ec94cb8f77b5e8a90c4d200d21b2339" + integrity sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA== dependencies: - "@babel/types" "^7.22.10" + "@babel/types" "^7.22.15" "@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" @@ -66,33 +66,33 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== -"@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" - integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== +"@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.22.15", "@babel/helper-validator-identifier@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz#601fa28e4cc06786c18912dca138cec73b882044" + integrity sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ== -"@babel/highlight@^7.22.10": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" - integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== +"@babel/highlight@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" + integrity sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ== dependencies: "@babel/helper-validator-identifier" "^7.22.5" chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.17.3", "@babel/parser@^7.20.15", "@babel/parser@^7.20.5", "@babel/parser@^7.21.3", "@babel/parser@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" - integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== +"@babel/parser@^7.17.3", "@babel/parser@^7.20.15", "@babel/parser@^7.20.5", "@babel/parser@^7.21.3", "@babel/parser@^7.22.15": + version "7.22.16" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95" + integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA== "@babel/template@^7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" "@babel/traverse@7.17.3": version "7.17.3" @@ -118,26 +118,26 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@babel/types@^7.17.0", "@babel/types@^7.22.10", "@babel/types@^7.22.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" - integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== +"@babel/types@^7.17.0", "@babel/types@^7.22.15", "@babel/types@^7.22.5": + version "7.22.17" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.17.tgz#f753352c4610ffddf9c8bc6823f9ff03e2303eee" + integrity sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg== dependencies: "@babel/helper-string-parser" "^7.22.5" - "@babel/helper-validator-identifier" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.15" to-fast-properties "^2.0.0" -"@csstools/css-parser-algorithms@^2.3.0": +"@csstools/css-parser-algorithms@^2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.1.tgz#ec4fc764ba45d2bb7ee2774667e056aa95003f3a" integrity sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA== -"@csstools/css-tokenizer@^2.1.1": +"@csstools/css-tokenizer@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.0.tgz#9d70e6dcbe94e44c7400a2929928db35c4de32b5" integrity sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA== -"@csstools/media-query-list-parser@^2.1.2": +"@csstools/media-query-list-parser@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.4.tgz#0017f99945f6c16dd81a7aacf6821770933c3a5c" integrity sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw== @@ -147,6 +147,116 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz#798622546b63847e82389e473fd67f2707d82247" integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -154,10 +264,10 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": - version "4.6.2" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" - integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.8.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.1.tgz#8c4bb756cc2aa7eaf13cfa5e69c83afb3260c20c" + integrity sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ== "@eslint/eslintrc@^2.1.2": version "2.1.2" @@ -174,15 +284,35 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.47.0": - version "8.47.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d" - integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og== +"@eslint/js@8.49.0": + version "8.49.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333" + integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w== -"@humanwhocodes/config-array@^0.11.10": - version "0.11.10" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" - integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== +"@floating-ui/core@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.4.1.tgz#0d633f4b76052668afb932492ac452f7ebe97f17" + integrity sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ== + dependencies: + "@floating-ui/utils" "^0.1.1" + +"@floating-ui/dom@^1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.2.tgz#6812e89d1d4d4ea32f10d15c3b81feb7f9836d89" + integrity sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog== + dependencies: + "@floating-ui/core" "^1.4.1" + "@floating-ui/utils" "^0.1.1" + +"@floating-ui/utils@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.2.tgz#b7e9309ccce5a0a40ac482cb894f120dba2b357f" + integrity sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ== + +"@humanwhocodes/config-array@^0.11.11": + version "0.11.11" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844" + integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -283,11 +413,11 @@ lodash "^4.17.21" "@tsconfig/node18@^18.2.0": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.0.tgz#d6b5358b3fa85fe89b13b46cb1e996e4d79d6a07" - integrity sha512-yhxwIlFVSVcMym3O31HoMnRXpoenmpIxcj4Yoes2DUpe+xCJnA7ECQP1Vw889V0jTt/2nzvpLQ/UuMYCd3JPIg== + version "18.2.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.1.tgz#ebf5e6b8d94e9de072e712bc197d6441a325ed61" + integrity sha512-RDDZFuofwkcKpl8Vpj5wFbY+H53xOtqK7ckEL1sXsbPwvKwDdjQf3LkHbtt9sxIHn9nWIEwkmCwBRZ6z5TKU2A== -"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.9": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== @@ -298,21 +428,26 @@ integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== "@types/node@^20.4.9": - version "20.4.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.9.tgz#c7164e0f8d3f12dfae336af0b1f7fdec8c6b204f" - integrity sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ== + version "20.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16" + integrity sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg== "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== -"@types/semver@^7.3.12", "@types/semver@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" - integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== +"@types/semver@^7.3.12": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367" + integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg== + +"@types/web-bluetooth@^0.0.17": + version "0.0.17" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz#5c9f3c617f64a9735d7b72a7cc671e166d900c40" + integrity sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA== -"@typescript-eslint/eslint-plugin@^5.59.1": +"@typescript-eslint/eslint-plugin@^5.59.1", "@typescript-eslint/eslint-plugin@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== @@ -328,25 +463,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/eslint-plugin@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.3.0.tgz#e751e148aab7ccaf8a7bfd370f7ce9e6bdd1f3f4" - integrity sha512-IZYjYZ0ifGSLZbwMqIip/nOamFiWJ9AH+T/GYNZBWkVcyNQOFGtSMoWV7RvY4poYCMZ/4lHzNl796WOSNxmk8A== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.3.0" - "@typescript-eslint/type-utils" "6.3.0" - "@typescript-eslint/utils" "6.3.0" - "@typescript-eslint/visitor-keys" "6.3.0" - debug "^4.3.4" - graphemer "^1.4.0" - ignore "^5.2.4" - natural-compare "^1.4.0" - natural-compare-lite "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/parser@^5.59.1": +"@typescript-eslint/parser@^5.59.1", "@typescript-eslint/parser@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== @@ -356,17 +473,6 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" -"@typescript-eslint/parser@^6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.3.0.tgz#359684c443f4f848db3c4f14674f544f169c8f46" - integrity sha512-ibP+y2Gr6p0qsUkhs7InMdXrwldjxZw66wpcQq9/PzAroM45wdwyu81T+7RibNCh8oc0AgrsyCwJByncY0Ongg== - dependencies: - "@typescript-eslint/scope-manager" "6.3.0" - "@typescript-eslint/types" "6.3.0" - "@typescript-eslint/typescript-estree" "6.3.0" - "@typescript-eslint/visitor-keys" "6.3.0" - debug "^4.3.4" - "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" @@ -375,14 +481,6 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.3.0.tgz#6b74e338c4b88d5e1dfc1a28c570dd5cf8c86b09" - integrity sha512-WlNFgBEuGu74ahrXzgefiz/QlVb+qg8KDTpknKwR7hMH+lQygWyx0CQFoUmMn1zDkQjTBBIn75IxtWss77iBIQ== - dependencies: - "@typescript-eslint/types" "6.3.0" - "@typescript-eslint/visitor-keys" "6.3.0" - "@typescript-eslint/type-utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" @@ -393,26 +491,11 @@ debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/type-utils@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.3.0.tgz#3bf89ccd36621ddec1b7f8246afe467c67adc247" - integrity sha512-7Oj+1ox1T2Yc8PKpBvOKWhoI/4rWFd1j7FA/rPE0lbBPXTKjdbtC+7Ev0SeBjEKkIhKWVeZSP+mR7y1Db1CdfQ== - dependencies: - "@typescript-eslint/typescript-estree" "6.3.0" - "@typescript-eslint/utils" "6.3.0" - debug "^4.3.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/types@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.3.0.tgz#84517f1427923e714b8418981e493b6635ab4c9d" - integrity sha512-K6TZOvfVyc7MO9j60MkRNWyFSf86IbOatTKGrpTQnzarDZPYPVy0oe3myTMq7VjhfsUAbNUW8I5s+2lZvtx1gg== - "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -426,19 +509,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.3.0.tgz#20e1e10e2f51cdb9e19a2751215cac92c003643c" - integrity sha512-Xh4NVDaC4eYKY4O3QGPuQNp5NxBAlEvNQYOqJquR2MePNxO11E5K3t5x4M4Mx53IZvtpW+mBxIT0s274fLUocg== - dependencies: - "@typescript-eslint/types" "6.3.0" - "@typescript-eslint/visitor-keys" "6.3.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/utils@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" @@ -453,19 +523,6 @@ eslint-scope "^5.1.1" semver "^7.3.7" -"@typescript-eslint/utils@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.3.0.tgz#0898c5e374372c2092ca1b979ea7ee9cc020ce84" - integrity sha512-hLLg3BZE07XHnpzglNBG8P/IXq/ZVXraEbgY7FM0Cnc1ehM8RMdn9mat3LubJ3KBeYXXPxV1nugWbQPjGeJk6Q== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.3.0" - "@typescript-eslint/types" "6.3.0" - "@typescript-eslint/typescript-estree" "6.3.0" - semver "^7.5.4" - "@typescript-eslint/visitor-keys@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" @@ -474,34 +531,26 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.3.0.tgz#8d09aa3e389ae0971426124c155ac289afbe450a" - integrity sha512-kEhRRj7HnvaSjux1J9+7dBen15CdWmDnwrpyiHsFX6Qx2iW5LOBUgNefOFeh2PjWPlNwN8TOn6+4eBU3J/gupw== +"@volar/language-core@1.10.1", "@volar/language-core@~1.10.0": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.10.1.tgz#76789c5b0c214eeff8add29cbff0333d89b6fc4a" + integrity sha512-JnsM1mIPdfGPxmoOcK1c7HYAsL6YOv0TCJ4aW3AXPZN/Jb4R77epDyMZIVudSGjWMbvv/JfUa+rQ+dGKTmgwBA== dependencies: - "@typescript-eslint/types" "6.3.0" - eslint-visitor-keys "^3.4.1" + "@volar/source-map" "1.10.1" -"@volar/language-core@1.10.0", "@volar/language-core@~1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.10.0.tgz#fb6b3ad22e75c53a1ae4d644c4a788b47d411b9d" - integrity sha512-ddyWwSYqcbEZNFHm+Z3NZd6M7Ihjcwl/9B5cZd8kECdimVXUFdFi60XHWD27nrWtUQIsUYIG7Ca1WBwV2u2LSQ== - dependencies: - "@volar/source-map" "1.10.0" - -"@volar/source-map@1.10.0", "@volar/source-map@~1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.10.0.tgz#2413eb190ce69fc1a382f58524a3f82306668024" - integrity sha512-/ibWdcOzDGiq/GM1JU2eX8fH1bvAhl66hfe8yEgLEzg9txgr6qb5sQ/DEz5PcDL75tF5H5sCRRwn8Eu8ezi9mw== +"@volar/source-map@1.10.1", "@volar/source-map@~1.10.0": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.10.1.tgz#b806845782cc615f2beba94624ff34a700f302f5" + integrity sha512-3/S6KQbqa7pGC8CxPrg69qHLpOvkiPHGJtWPkI/1AXCsktkJ6gIk/5z4hyuMp8Anvs6eS/Kvp/GZa3ut3votKA== dependencies: muggle-string "^0.3.1" "@volar/typescript@~1.10.0": - version "1.10.0" - resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.10.0.tgz#3b16cf7c4c1802eac023ba4e57fe52bdb6d3016f" - integrity sha512-OtqGtFbUKYC0pLNIk3mHQp5xWnvL1CJIUc9VE39VdZ/oqpoBh5jKfb9uJ45Y4/oP/WYTrif/Uxl1k8VTPz66Gg== + version "1.10.1" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.10.1.tgz#b20341c1cc5785b4de0669ea645e1619c97a4764" + integrity sha512-+iiO9yUSRHIYjlteT+QcdRq8b44qH19/eiUZtjNtuh6D9ailYM7DVR0zO2sEgJlvCaunw/CF9Ov2KooQBpR4VQ== dependencies: - "@volar/language-core" "1.10.0" + "@volar/language-core" "1.10.1" "@vue/compiler-core@3.3.4": version "3.3.4" @@ -567,10 +616,10 @@ "@typescript-eslint/parser" "^5.59.1" vue-eslint-parser "^9.1.1" -"@vue/language-core@1.8.8": - version "1.8.8" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.8.tgz#5a8aa8363f4dfacdfcd7808a9926744d7c310ae6" - integrity sha512-i4KMTuPazf48yMdYoebTkgSOJdFraE4pQf0B+FTOFkbB+6hAfjrSou/UmYWRsWyZV6r4Rc6DDZdI39CJwL0rWw== +"@vue/language-core@1.8.11": + version "1.8.11" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.11.tgz#d10cc6f8f32e30991e0430f0d91db9416dc2e9a6" + integrity sha512-+MZOBGqGwfld6hpo0DB47x8eNM0dNqk15ZdfOhj19CpvuYuOWCeVdOEGZunKDyo3QLkTn3kLOSysJzg7FDOQBA== dependencies: "@volar/language-core" "~1.10.0" "@volar/source-map" "~1.10.0" @@ -634,13 +683,35 @@ resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.4.0.tgz#f01e2f6089b5098136fb084a0dd0cdd4533b72b0" integrity sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg== -"@vue/typescript@1.8.8": - version "1.8.8" - resolved "https://registry.yarnpkg.com/@vue/typescript/-/typescript-1.8.8.tgz#8efb375d448862134492a044f4e96afada547500" - integrity sha512-jUnmMB6egu5wl342eaUH236v8tdcEPXXkPgj+eI/F6JwW/lb+yAU6U07ZbQ3MVabZRlupIlPESB7ajgAGixhow== +"@vue/typescript@1.8.11": + version "1.8.11" + resolved "https://registry.yarnpkg.com/@vue/typescript/-/typescript-1.8.11.tgz#b2de6760a0e6d829a5328cd0123b1763ce3e66ef" + integrity sha512-skUmMDiPUUtu1flPmf2YybF+PX8IzBtMioQOaNn6Ck/RhdrPJGj1AX/7s3Buf9G6ln+/KHR1XQuti/FFxw5XVA== dependencies: "@volar/typescript" "~1.10.0" - "@vue/language-core" "1.8.8" + "@vue/language-core" "1.8.11" + +"@vueuse/core@^10.3.0": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.4.1.tgz#fc2c8a83a571c207aaedbe393b22daa6d35123f2" + integrity sha512-DkHIfMIoSIBjMgRRvdIvxsyboRZQmImofLyOHADqiVbQVilP8VVHDhBX2ZqoItOgu7dWa8oXiNnScOdPLhdEXg== + dependencies: + "@types/web-bluetooth" "^0.0.17" + "@vueuse/metadata" "10.4.1" + "@vueuse/shared" "10.4.1" + vue-demi ">=0.14.5" + +"@vueuse/metadata@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.4.1.tgz#9d2ff5c67abf17a8c07865c2413fbd0e92f7b7d7" + integrity sha512-2Sc8X+iVzeuMGHr6O2j4gv/zxvQGGOYETYXEc41h0iZXIRnRbJZGmY/QP8dvzqUelf8vg0p/yEA5VpCEu+WpZg== + +"@vueuse/shared@10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.4.1.tgz#d5ce33033c156efb60664b5d6034d6cd4e2f530c" + integrity sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg== + dependencies: + vue-demi ">=0.14.5" acorn-jsx@^5.3.2: version "5.3.2" @@ -846,13 +917,13 @@ concat-map@0.0.1: integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== cosmiconfig@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" - integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + version "8.3.5" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.5.tgz#3b3897ddd042d022d5a207d4c8832e54f5301977" + integrity sha512-A5Xry3xfS96wy2qbiLkQLAg4JUrR2wvfybxj6yqLmrUfMAvhS3MZxIP2oQn0grgYIvJqzpeTEWu4vK0t+12NNw== dependencies: - import-fresh "^3.2.1" + import-fresh "^3.3.0" js-yaml "^4.1.0" - parse-json "^5.0.0" + parse-json "^5.2.0" path-type "^4.0.0" cross-spawn@^7.0.2, cross-spawn@^7.0.3: @@ -1006,6 +1077,34 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +esbuild@^0.18.10: + version "0.18.20" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -1064,15 +1163,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== eslint@^8.45.0: - version "8.47.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806" - integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q== + version "8.49.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42" + integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "^8.47.0" - "@humanwhocodes/config-array" "^0.11.10" + "@eslint/js" "8.49.0" + "@humanwhocodes/config-array" "^0.11.11" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" ajv "^6.12.4" @@ -1189,7 +1288,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.2.9, fast-glob@^3.3.0: +fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== @@ -1245,14 +1344,15 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + version "3.1.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" + integrity sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew== dependencies: - flatted "^3.1.0" + flatted "^3.2.7" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: +flatted@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== @@ -1262,6 +1362,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -1412,7 +1517,7 @@ ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -1552,9 +1657,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-tokens@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-8.0.1.tgz#f068fde9bd2f9f4a24ad78f3b4fa787216b433e3" - integrity sha512-3AGrZT6tuMm1ZWWn9mLXh7XMfi2YtiLNPALCVxBCiUVq0LD1OQMxV/AdS/s7rLJU5o9i/jBZw/N4vXXL5dm29A== + version "8.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-8.0.2.tgz#86a19e09d81c64f1f4a3af489b8c1b67d0c7c588" + integrity sha512-Olnt+V7xYdvGze9YTbGFZIfQXuGV4R3nQwwl8BrtgaPE/wq8UFpUHWuTNc05saowhSr1ZO6tx+V6RjE9D5YQog== js-yaml@^4.1.0: version "4.1.0" @@ -1568,6 +1673,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -1588,15 +1698,22 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +keyv@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" + integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -known-css-properties@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" - integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== +known-css-properties@^0.28.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.28.0.tgz#8a8be010f368b3036fe6ab0ef4bbbed972bd6274" + integrity sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ== levn@^0.4.1: version "0.4.1" @@ -1641,9 +1758,9 @@ lru-cache@^6.0.0: yallist "^4.0.0" magic-string@^0.30.0: - version "0.30.2" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.2.tgz#dcf04aad3d0d1314bc743d076c50feb29b3c7aca" - integrity sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug== + version "0.30.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.3.tgz#403755dfd9d6b398dfa40635d52e96c5ac095b85" + integrity sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -1866,7 +1983,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -1937,9 +2054,9 @@ postcss-safe-parser@^6.0.0: integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== postcss-scss@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.6.tgz#5d62a574b950a6ae12f2aa89b60d63d9e4432bfd" - integrity sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ== + version "4.0.8" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.8.tgz#12a4991a902a782d4e9b86b1f217d5181c6c4f32" + integrity sha512-Cr0X8Eu7xMhE96PJck6ses/uVVXDtE5ghUTKNUYgm8ozgP2TkgV3LWs3WgLV1xaSSLq8ZFiXaUrj0LVgG1fGEA== postcss-selector-parser@^6.0.13: version "6.0.13" @@ -1954,10 +2071,10 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.1.10, postcss@^8.4.0, postcss@^8.4.19, postcss@^8.4.25: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== +postcss@^8.1.10, postcss@^8.4.0, postcss@^8.4.19, postcss@^8.4.27: + version "8.4.29" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.29.tgz#33bc121cf3b3688d4ddef50be869b2a54185a1dd" + integrity sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" @@ -1976,9 +2093,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.1.tgz#65271fc9320ce4913c57747a70ce635b30beaa40" - integrity sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ== + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== punycode@^2.1.0: version "2.3.0" @@ -2049,6 +2166,13 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" +rollup@^3.27.1: + version "3.29.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.1.tgz#ba53a179d46ac3cd79e162dca6ab70d93cd26f78" + integrity sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg== + optionalDependencies: + fsevents "~2.3.2" + run-applescript@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" @@ -2260,13 +2384,13 @@ stylelint-scss@^5.0.0, stylelint-scss@^5.0.1: postcss-value-parser "^4.2.0" stylelint@^15.10.2: - version "15.10.2" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.2.tgz#0ee5a8371d3a2e1ff27fefd48309d3ddef7c3405" - integrity sha512-UxqSb3hB74g4DTO45QhUHkJMjKKU//lNUAOWyvPBVPZbCknJ5HjOWWZo+UDuhHa9FLeVdHBZXxu43eXkjyIPWg== + version "15.10.3" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.3.tgz#995e4512fdad450fb83e13f3472001f6edb6469c" + integrity sha512-aBQMMxYvFzJJwkmg+BUUg3YfPyeuCuKo2f+LOw7yYbU8AZMblibwzp9OV4srHVeQldxvSFdz0/Xu8blq2AesiA== dependencies: - "@csstools/css-parser-algorithms" "^2.3.0" - "@csstools/css-tokenizer" "^2.1.1" - "@csstools/media-query-list-parser" "^2.1.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/media-query-list-parser" "^2.1.4" "@csstools/selector-specificity" "^3.0.0" balanced-match "^2.0.0" colord "^2.9.3" @@ -2274,7 +2398,7 @@ stylelint@^15.10.2: css-functions-list "^3.2.0" css-tree "^2.3.1" debug "^4.3.4" - fast-glob "^3.3.0" + fast-glob "^3.3.1" fastest-levenshtein "^1.0.16" file-entry-cache "^6.0.1" global-modules "^2.0.0" @@ -2285,13 +2409,13 @@ stylelint@^15.10.2: import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.27.0" + known-css-properties "^0.28.0" mathml-tag-names "^2.1.3" meow "^10.1.5" micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.25" + postcss "^8.4.27" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" postcss-selector-parser "^6.0.13" @@ -2378,20 +2502,15 @@ trim-newlines@^4.0.2: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== -ts-api-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" - integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== - tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.5.0, tslib@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" - integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== tsutils@^3.21.0: version "3.21.0" @@ -2418,9 +2537,9 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== typescript@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== untildify@^4.0.0: version "4.0.0" @@ -2454,6 +2573,22 @@ validate-npm-package-name@^5.0.0: dependencies: builtins "^5.0.0" +vite@^4.4.9: + version "4.4.9" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d" + integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA== + dependencies: + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" + optionalDependencies: + fsevents "~2.3.2" + +vue-demi@>=0.14.5: + version "0.14.6" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92" + integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w== + vue-eslint-parser@^9.1.1, vue-eslint-parser@^9.3.1: version "9.3.1" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz#429955e041ae5371df5f9e37ebc29ba046496182" @@ -2483,12 +2618,12 @@ vue-template-compiler@^2.7.14: he "^1.2.0" vue-tsc@^1.8.6: - version "1.8.8" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.8.tgz#67317693eb2ef6747e89e6d834eeb6d2deb8871d" - integrity sha512-bSydNFQsF7AMvwWsRXD7cBIXaNs/KSjvzWLymq/UtKE36697sboX4EccSHFVxvgdBlI1frYPc/VMKJNB7DFeDQ== + version "1.8.11" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.11.tgz#4a972e58a9eaae72d73e63e0bedacb56391b7cb4" + integrity sha512-BzfiMdPqDHBlysx4g26NkfVHSQwGD/lTRausmxN9sFyjXz34OWfsbkh0YsVkX84Hu65In1fFlxHiG39Tr4Vojg== dependencies: - "@vue/language-core" "1.8.8" - "@vue/typescript" "1.8.8" + "@vue/language-core" "1.8.11" + "@vue/typescript" "1.8.11" semver "^7.3.8" vue@^3.3.4: From 07419f3742324c05198db95a52fc8c31636f16bc Mon Sep 17 00:00:00 2001 From: Melonify Date: Thu, 21 Sep 2023 17:12:39 -0400 Subject: [PATCH 02/15] Lint --- theming/src/themes/base.scss | 4 ++-- ui/src/components/UiSlider.vue | 6 +++--- ui/src/components/UiTextInput.vue | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/theming/src/themes/base.scss b/theming/src/themes/base.scss index be4ed79..dc2fe85 100644 --- a/theming/src/themes/base.scss +++ b/theming/src/themes/base.scss @@ -1,7 +1,7 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap"); +@import "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;700&display=swap"; :root { - font-family: "Inter"; + font-family: Inter, sans-serif; color: var(--theme-color-text); background-color: var(--theme-color-background); } diff --git a/ui/src/components/UiSlider.vue b/ui/src/components/UiSlider.vue index 6ded4bf..90f2e14 100644 --- a/ui/src/components/UiSlider.vue +++ b/ui/src/components/UiSlider.vue @@ -51,7 +51,7 @@ function onInput(ev: Event) { height: $height; width: $height; margin-top: -$offset; - background: currentColor; + background: currentcolor; border-radius: 50%; border: none; outline: none; @@ -60,7 +60,7 @@ function onInput(ev: Event) { @mixin track($height, $track-height) { width: 100%; height: $track-height; - background-color: currentColor; + background-color: currentcolor; border-radius: var(--theme-chip-radius); outline: none; } @@ -73,7 +73,7 @@ function onInput(ev: Event) { background: transparent; appearance: none; cursor: pointer; - color: currentColor; + color: currentcolor; &::-webkit-slider-thumb { @include thumb($height, $track-height); diff --git a/ui/src/components/UiTextInput.vue b/ui/src/components/UiTextInput.vue index 0270ba9..24fd768 100644 --- a/ui/src/components/UiTextInput.vue +++ b/ui/src/components/UiTextInput.vue @@ -103,7 +103,6 @@ defineExpose({ input { flex: 1 1 auto; width: 0; - color: var(--theme-text-base); padding: $padding 0; background: none; From 6ebf58238a9b2f99de7780a8672b581d04f10a97 Mon Sep 17 00:00:00 2001 From: Melonify Date: Thu, 21 Sep 2023 18:28:57 -0400 Subject: [PATCH 03/15] Switch workspaces to glob pattern --- .documentation/Contributing.md | 2 +- package.json | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.documentation/Contributing.md b/.documentation/Contributing.md index 55f523c..ef8aa18 100644 --- a/.documentation/Contributing.md +++ b/.documentation/Contributing.md @@ -32,7 +32,7 @@ This repository provides multiple tools to make co-development of packages easie ## Workspaces -When new packages are created they should be defined in the `workspaces` field in the root `package.json` for the repository so that npm will resolve internal dependancies correctly. +All packages are captured by the `workspaces` field in the root `package.json` for the repository so that npm will resolve internal dependancies correctly. ## Package versioning diff --git a/package.json b/package.json index 7daa657..ea55143 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,7 @@ "gh-packages-key:undo": "git update-index --no-skip-worktree .npmrc && git restore .npmrc" }, "workspaces": [ - "theming", - "ui", - "util", - "util-vue" + "*/" ], "dependencies": { "@npmcli/promise-spawn": "^6.0.2", From 67181a19dc4867abe7b68cf9802d933985e7a108 Mon Sep 17 00:00:00 2001 From: Melonify Date: Thu, 21 Sep 2023 19:37:13 -0400 Subject: [PATCH 04/15] Export transformers from @seventv/theming --- theming/src/transformers/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/theming/src/transformers/index.ts b/theming/src/transformers/index.ts index c142f6b..7e48e44 100644 --- a/theming/src/transformers/index.ts +++ b/theming/src/transformers/index.ts @@ -1 +1,3 @@ -export const PLACEHOLDER = ""; +export * from "./basic"; +export * from "./media-query"; +export * from "./rgb"; From fcde7e474bcfe195d4c436bd0b91af4d0e6fc1e7 Mon Sep 17 00:00:00 2001 From: Melonify Date: Fri, 22 Sep 2023 12:43:47 -0400 Subject: [PATCH 05/15] Additional UI components --- .../assets/icons/third-party/ChevronIcon.vue | 26 ++++ ui/src/assets/style/icon-direction.scss | 11 ++ ui/src/components/UiCarousel.vue | 136 ++++++++++++++++++ ui/src/index.ts | 1 + 4 files changed, 174 insertions(+) create mode 100644 ui/src/assets/icons/third-party/ChevronIcon.vue create mode 100644 ui/src/assets/style/icon-direction.scss create mode 100644 ui/src/components/UiCarousel.vue diff --git a/ui/src/assets/icons/third-party/ChevronIcon.vue b/ui/src/assets/icons/third-party/ChevronIcon.vue new file mode 100644 index 0000000..d1f2ca3 --- /dev/null +++ b/ui/src/assets/icons/third-party/ChevronIcon.vue @@ -0,0 +1,26 @@ +/* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license +(Commercial License) Copyright 2023 Fonticons, Inc. */ + + + + + diff --git a/ui/src/assets/style/icon-direction.scss b/ui/src/assets/style/icon-direction.scss new file mode 100644 index 0000000..f0d1fb3 --- /dev/null +++ b/ui/src/assets/style/icon-direction.scss @@ -0,0 +1,11 @@ +svg[direction="up"] { + transform: rotate(180deg); +} + +svg[direction="left"] { + transform: rotate(90deg); +} + +svg[direction="right"] { + transform: rotate(-90deg); +} diff --git a/ui/src/components/UiCarousel.vue b/ui/src/components/UiCarousel.vue new file mode 100644 index 0000000..ed1b6b0 --- /dev/null +++ b/ui/src/components/UiCarousel.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/ui/src/index.ts b/ui/src/index.ts index fb46b11..64038fe 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -1,4 +1,5 @@ export { default as UiButton } from "./components/UiButton.vue"; +export { default as UiCarousel } from "./components/UiCarousel.vue"; export { default as UiDraggable } from "./components/UiDraggable.vue"; export { default as UiFloating } from "./components/UiFloating.vue"; export { default as UiLoadingBar } from "./components/UiLoadingBar.vue"; From f5ebed1e328b44af8c348a3a5901ba2b332ccaf6 Mon Sep 17 00:00:00 2001 From: Melonify Date: Fri, 22 Sep 2023 12:54:11 -0400 Subject: [PATCH 06/15] Target common ES2020 across TS packages --- theming/tsconfig.json | 3 +++ ui/tsconfig.json | 3 +++ util-vue/tsconfig.json | 3 +++ util/tsconfig.json | 3 +++ 4 files changed, 12 insertions(+) diff --git a/theming/tsconfig.json b/theming/tsconfig.json index e6a74a7..1ade608 100644 --- a/theming/tsconfig.json +++ b/theming/tsconfig.json @@ -4,6 +4,9 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", "types": ["vite/client"], "baseUrl": ".", "experimentalDecorators": true diff --git a/ui/tsconfig.json b/ui/tsconfig.json index e6a74a7..1ade608 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -4,6 +4,9 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", "types": ["vite/client"], "baseUrl": ".", "experimentalDecorators": true diff --git a/util-vue/tsconfig.json b/util-vue/tsconfig.json index 6a49c20..31082a1 100644 --- a/util-vue/tsconfig.json +++ b/util-vue/tsconfig.json @@ -4,6 +4,9 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", "types": ["vite/client"], "baseUrl": ".", "experimentalDecorators": true diff --git a/util/tsconfig.json b/util/tsconfig.json index 065ae56..8e31e9c 100644 --- a/util/tsconfig.json +++ b/util/tsconfig.json @@ -3,6 +3,9 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", "baseUrl": ".", "experimentalDecorators": true } From d70b015da489b552c2fc62fc699ba73eae068390 Mon Sep 17 00:00:00 2001 From: Melonify Date: Fri, 22 Sep 2023 12:55:58 -0400 Subject: [PATCH 07/15] Additional asyncronous utilities --- util/modules/index.ts | 2 ++ util/modules/observer/ClassObserver.ts | 37 ++++++++++++++++++++++ util/modules/observer/index.ts | 1 + util/modules/promise/ExtendedPromise.ts | 33 +++++++++++++++++++ util/modules/promise/ObserverPromise.ts | 42 +++++++++++++++++++++++++ util/modules/promise/index.ts | 2 ++ 6 files changed, 117 insertions(+) create mode 100644 util/modules/observer/ClassObserver.ts create mode 100644 util/modules/observer/index.ts create mode 100644 util/modules/promise/ExtendedPromise.ts create mode 100644 util/modules/promise/ObserverPromise.ts create mode 100644 util/modules/promise/index.ts diff --git a/util/modules/index.ts b/util/modules/index.ts index 2b83717..e05a7ff 100644 --- a/util/modules/index.ts +++ b/util/modules/index.ts @@ -1,2 +1,4 @@ +export * as observer from "./observer"; +export * as promise from "./promise"; export * as string from "./string"; export * as time from "./time"; diff --git a/util/modules/observer/ClassObserver.ts b/util/modules/observer/ClassObserver.ts new file mode 100644 index 0000000..cc2f2be --- /dev/null +++ b/util/modules/observer/ClassObserver.ts @@ -0,0 +1,37 @@ +export class ClassObserver extends MutationObserver { + constructor(callback: (changes: Set) => void) { + super((records) => { + for (const record of records) { + if (record.target instanceof Element == false) continue; + if (record.attributeName !== "class") continue; + + const target = record.target as Element; + + const oldClasses = new Set(record.oldValue?.split(" ")); + const newClasses = new Set(target.getAttribute(record.attributeName)?.split(" ")); + const changedClasses = new Set(); + + for (const cl of newClasses) { + if (!oldClasses.has(cl)) changedClasses.add(cl); + } + + for (const cl of oldClasses) { + if (!newClasses.has(cl)) changedClasses.add(cl); + } + + callback(changedClasses); + break; + } + }); + } + + observe(target: Node): void { + super.observe(target, { + attributes: true, + attributeFilter: ["class"], + attributeOldValue: true, + }); + } +} + +export default ClassObserver; diff --git a/util/modules/observer/index.ts b/util/modules/observer/index.ts new file mode 100644 index 0000000..ef04dbc --- /dev/null +++ b/util/modules/observer/index.ts @@ -0,0 +1 @@ +export * from "./ClassObserver"; diff --git a/util/modules/promise/ExtendedPromise.ts b/util/modules/promise/ExtendedPromise.ts new file mode 100644 index 0000000..50efac6 --- /dev/null +++ b/util/modules/promise/ExtendedPromise.ts @@ -0,0 +1,33 @@ +export class ExtendedPromise implements PromiseLike { + protected isResolved = false; + protected resolve?: (value: V | PromiseLike) => void; + protected reject?: (reason?: Error) => void; + + public then: Promise["then"]; + public catch: Promise["catch"]; + public finally: Promise["finally"]; + + constructor() { + const promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.then = (...args: any[]) => promise.then(...args); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.catch = (...args: any[]) => promise.catch(...args); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.finally = (...args: any[]) => promise.finally(...args); + } + + protected emit(v: V) { + this.isResolved = true; + this.resolve?.(v); + + this.resolve = undefined; + this.reject = undefined; + } +} + +export default ExtendedPromise; diff --git a/util/modules/promise/ObserverPromise.ts b/util/modules/promise/ObserverPromise.ts new file mode 100644 index 0000000..e3d77f1 --- /dev/null +++ b/util/modules/promise/ObserverPromise.ts @@ -0,0 +1,42 @@ +import { ExtendedPromise } from "./ExtendedPromise"; + +export class ObserverPromise extends ExtendedPromise { + private observer: MutationObserver | undefined; + + constructor( + callback: (mutations: MutationRecord[], emit: (v: V) => void) => void, + target: Node, + options?: MutationObserverInit, + ) { + super(); + + this.observer = new MutationObserver((mutations) => { + callback(mutations, this.emit.bind(this)); + }); + + this.observer.observe(target, options); + } + + protected emit(v: V) { + super.emit(v); + + this.disconnect(); + } + + disconnect() { + this.observer?.disconnect(); + this.observer = undefined; + + if (!this.isResolved) { + this.reject?.(new ObserverPromiseNotResolvedError()); + } + } +} + +export class ObserverPromiseNotResolvedError extends Error { + constructor() { + super("ObserverPromise: Observer disconnected before resolving."); + } +} + +export default ObserverPromise; diff --git a/util/modules/promise/index.ts b/util/modules/promise/index.ts new file mode 100644 index 0000000..c7f63ac --- /dev/null +++ b/util/modules/promise/index.ts @@ -0,0 +1,2 @@ +export * from "./ExtendedPromise"; +export * from "./ObserverPromise"; From 60914d441b9351e9027185014aa885bf6778be51 Mon Sep 17 00:00:00 2001 From: Melonify Date: Fri, 22 Sep 2023 13:07:41 -0400 Subject: [PATCH 08/15] Remove old unreachable code from mediaQueryTransformer --- theming/src/transformers/media-query.ts | 33 +++++++++++-------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/theming/src/transformers/media-query.ts b/theming/src/transformers/media-query.ts index 6e000dc..3961eaf 100644 --- a/theming/src/transformers/media-query.ts +++ b/theming/src/transformers/media-query.ts @@ -11,25 +11,20 @@ export function mediaQueryTransformer(fallback: Theme, ...states: [string, Theme }[] = []; for (const state of states) { - if (state instanceof Array) { - const query = window.matchMedia(state[0]); - const matches = ref(query.matches); - const callback = (ev: MediaQueryListEvent) => { - matches.value = ev.matches; - }; - - query.addEventListener("change", callback); - - queryStates.push({ - query, - matches, - callback, - result: state[1], - }); - } else { - fallback = state; - break; - } + const query = window.matchMedia(state[0]); + const matches = ref(query.matches); + const callback = (ev: MediaQueryListEvent) => { + matches.value = ev.matches; + }; + + query.addEventListener("change", callback); + + queryStates.push({ + query, + matches, + callback, + result: state[1], + }); } onScopeDispose(() => { From a13132c3e840f788a47f5e1f31884e831b334c45 Mon Sep 17 00:00:00 2001 From: Melonify Date: Mon, 25 Sep 2023 18:21:51 -0400 Subject: [PATCH 09/15] More resiliant unique route key serialization --- util-vue/modules/router/getUniqueRouteKey.ts | 24 +++++++++----------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/util-vue/modules/router/getUniqueRouteKey.ts b/util-vue/modules/router/getUniqueRouteKey.ts index f522238..472c017 100644 --- a/util-vue/modules/router/getUniqueRouteKey.ts +++ b/util-vue/modules/router/getUniqueRouteKey.ts @@ -1,23 +1,21 @@ import type { RouteLocationNormalizedLoaded } from "vue-router"; export function getUniqueRouteKey(route: RouteLocationNormalizedLoaded) { - if (route.meta.dependsOn instanceof Array) { - const deps = route.meta.dependsOn; - + const deps = route.meta.dependsOn; + if (deps instanceof Array) { const parts = []; - for (const [param, value] of Object.entries(route.params)) { - if (deps.includes(param)) { - let string = `${param}`; - - if (value instanceof Array) { - string += `:${value.join(":")}`; - } else { - string += `:${value}`; - } + for (const param of deps) { + let string = `${param}`; - parts.push(string); + const value = route.params[param]; + if (value instanceof Array) { + string += `:${value.join(":")}`; + } else if (value) { + string += `:${value}`; } + + parts.push(string); } return parts.join("-"); From 82227b61290e848fea38e2af7d906cfa411d4d53 Mon Sep 17 00:00:00 2001 From: Melonify Date: Wed, 27 Sep 2023 22:36:24 -0400 Subject: [PATCH 10/15] Move RouterView for async pages into util --- util-vue/modules/router/AsyncRouterView.vue | 29 +++++++++++++++++++++ util-vue/modules/router/index.ts | 1 + 2 files changed, 30 insertions(+) create mode 100644 util-vue/modules/router/AsyncRouterView.vue diff --git a/util-vue/modules/router/AsyncRouterView.vue b/util-vue/modules/router/AsyncRouterView.vue new file mode 100644 index 0000000..a0c7a80 --- /dev/null +++ b/util-vue/modules/router/AsyncRouterView.vue @@ -0,0 +1,29 @@ + + + diff --git a/util-vue/modules/router/index.ts b/util-vue/modules/router/index.ts index 24f5ae9..03ad80f 100644 --- a/util-vue/modules/router/index.ts +++ b/util-vue/modules/router/index.ts @@ -1,3 +1,4 @@ +export { default as AsyncRouterView } from "./AsyncRouterView.vue"; export * from "./defineAsyncPage"; export * from "./getFirstParam"; export * from "./getUniqueRouteKey"; From fb3880c8aa3fea44ef080b6d4172fc60da415deb Mon Sep 17 00:00:00 2001 From: Melonify Date: Sun, 1 Oct 2023 23:05:19 -0400 Subject: [PATCH 11/15] Add type utilities to util --- util/modules/index.ts | 1 + util/modules/types/index.d.ts | 1 + util/modules/types/keys.d.ts | 8 ++++++++ 3 files changed, 10 insertions(+) create mode 100644 util/modules/types/index.d.ts create mode 100644 util/modules/types/keys.d.ts diff --git a/util/modules/index.ts b/util/modules/index.ts index e05a7ff..f17fd07 100644 --- a/util/modules/index.ts +++ b/util/modules/index.ts @@ -2,3 +2,4 @@ export * as observer from "./observer"; export * as promise from "./promise"; export * as string from "./string"; export * as time from "./time"; +export type * as types from "./types"; diff --git a/util/modules/types/index.d.ts b/util/modules/types/index.d.ts new file mode 100644 index 0000000..9c06c80 --- /dev/null +++ b/util/modules/types/index.d.ts @@ -0,0 +1 @@ +export type * from "./keys"; diff --git a/util/modules/types/keys.d.ts b/util/modules/types/keys.d.ts new file mode 100644 index 0000000..e8b2dba --- /dev/null +++ b/util/modules/types/keys.d.ts @@ -0,0 +1,8 @@ +// Resolves to never if the key is part of an indexed type, returns the key if it does not +export type DefiniteKey = string extends Key + ? never + : number extends Key + ? never + : symbol extends Key + ? never + : Key; From ffb340dd9cf26c4d41b607ddf0a3fcd093d372f1 Mon Sep 17 00:00:00 2001 From: Melonify Date: Mon, 2 Oct 2023 03:18:43 -0400 Subject: [PATCH 12/15] Use strict typescript in all packages --- theming/tsconfig.json | 1 + ui/tsconfig.json | 1 + util-vue/tsconfig.json | 1 + util/tsconfig.json | 1 + 4 files changed, 4 insertions(+) diff --git a/theming/tsconfig.json b/theming/tsconfig.json index 1ade608..e8cd7ee 100644 --- a/theming/tsconfig.json +++ b/theming/tsconfig.json @@ -4,6 +4,7 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "strict": true, "target": "ES2020", "module": "ESNext", "moduleResolution": "Node", diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 1ade608..e8cd7ee 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -4,6 +4,7 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "strict": true, "target": "ES2020", "module": "ESNext", "moduleResolution": "Node", diff --git a/util-vue/tsconfig.json b/util-vue/tsconfig.json index 31082a1..2152a16 100644 --- a/util-vue/tsconfig.json +++ b/util-vue/tsconfig.json @@ -4,6 +4,7 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "strict": true, "target": "ES2020", "module": "ESNext", "moduleResolution": "Node", diff --git a/util/tsconfig.json b/util/tsconfig.json index 8e31e9c..c5ccef3 100644 --- a/util/tsconfig.json +++ b/util/tsconfig.json @@ -3,6 +3,7 @@ "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, + "strict": true, "target": "ES2020", "module": "ESNext", "moduleResolution": "Node", From dca853bbeb2e24f45d106e5680ec2b3f4c760e68 Mon Sep 17 00:00:00 2001 From: Melonify Date: Tue, 3 Oct 2023 23:21:34 -0400 Subject: [PATCH 13/15] Add prototype helpers to util --- util/modules/index.ts | 1 + util/modules/prototype/index.ts | 1 + util/modules/prototype/isPrototypeOf.ts | 5 +++++ 3 files changed, 7 insertions(+) create mode 100644 util/modules/prototype/index.ts create mode 100644 util/modules/prototype/isPrototypeOf.ts diff --git a/util/modules/index.ts b/util/modules/index.ts index f17fd07..e5ea0f9 100644 --- a/util/modules/index.ts +++ b/util/modules/index.ts @@ -1,5 +1,6 @@ export * as observer from "./observer"; export * as promise from "./promise"; +export * as prototype from "./prototype"; export * as string from "./string"; export * as time from "./time"; export type * as types from "./types"; diff --git a/util/modules/prototype/index.ts b/util/modules/prototype/index.ts new file mode 100644 index 0000000..b2cb79a --- /dev/null +++ b/util/modules/prototype/index.ts @@ -0,0 +1 @@ +export * from "./isPrototypeOf"; diff --git a/util/modules/prototype/isPrototypeOf.ts b/util/modules/prototype/isPrototypeOf.ts new file mode 100644 index 0000000..e11ed71 --- /dev/null +++ b/util/modules/prototype/isPrototypeOf.ts @@ -0,0 +1,5 @@ +export function isPrototypeOf

(prototype: P, object: object): object is P { + return Object.prototype.isPrototypeOf.call(prototype, object); +} + +export default isPrototypeOf; From 3bd029d731aff76f12484ca09b879bbe83b03514 Mon Sep 17 00:00:00 2001 From: Melonify Date: Wed, 4 Oct 2023 01:30:54 -0400 Subject: [PATCH 14/15] Basic API Hydrator implementation --- api/README.md | 1 + api/package.json | 24 +++++ api/src/hydrator/HydratedObject.ts | 47 ++++++++++ api/src/hydrator/hydration.ts | 145 +++++++++++++++++++++++++++++ api/src/index.ts | 5 + api/src/types/hydrator.d.ts | 128 +++++++++++++++++++++++++ api/tsconfig.json | 13 +++ 7 files changed, 363 insertions(+) create mode 100644 api/README.md create mode 100644 api/package.json create mode 100644 api/src/hydrator/HydratedObject.ts create mode 100644 api/src/hydrator/hydration.ts create mode 100644 api/src/index.ts create mode 100644 api/src/types/hydrator.d.ts create mode 100644 api/tsconfig.json diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..8f7d09c --- /dev/null +++ b/api/README.md @@ -0,0 +1 @@ +This package requires typescript transpiling by the end user, ensure that your typescript config is compatable with this package's `tsconfig.json`. diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..4c434f4 --- /dev/null +++ b/api/package.json @@ -0,0 +1,24 @@ +{ + "name": "@seventv/api", + "version": "0.1.0", + "scripts": { + "type-check": "tsc --noEmit --composite false" + }, + "repository": { + "type": "git", + "url": "https://github.com/SevenTV/WebComponents.git" + }, + "main": "src/index.ts", + "files": [ + "src/", + "tsconfig.json" + ], + "dependencies": { + "typescript": "^5.1.6", + "@seventv/util": "~0" + }, + "devDependencies": { + "@tsconfig/node18": "^18.2.0", + "@types/node": "^20.4.9" + } +} diff --git a/api/src/hydrator/HydratedObject.ts b/api/src/hydrator/HydratedObject.ts new file mode 100644 index 0000000..242b1d1 --- /dev/null +++ b/api/src/hydrator/HydratedObject.ts @@ -0,0 +1,47 @@ +import { hydrateValue } from "./hydration"; +import type { HydratorSchema, HydratorValue } from "../types/hydrator"; +import type { DefiniteKey } from "@seventv/util/modules/types"; + +type HydratedObjectConstructor = typeof HydratedObjectClass; +class HydratedObjectClass { + static SCHEMA: HydratedObjectSchema = {}; + + constructor(json: object) { + const cls = this.constructor; + + for (const [prop, schema] of Object.entries(cls.SCHEMA)) { + const isOwn = Object.prototype.hasOwnProperty.call(json, prop); + const raw = isOwn ? Reflect.get(json, prop) : undefined; + + Reflect.set(this, prop, hydrateValue(raw, schema, prop, [json])); + } + } +} + +export type HydratedObjectSchema = Record; + +/* Assert to TypeScript that the implementation will always define props from the schema + * + * It is impossible to genericly extend the final type, as that would require the compiler to know ahead of time what generic the constructor + * would be instantiated with, so we export a mapper type with a generic so downstream abstract classes can also exhibit this property + * This does not affect implementors, only classes which wish to "pass along" the final implementor to the constructor's generic if they are abstract + */ +type HydratedObjectSchemaFields = { + [Key in keyof S as DefiniteKey]: HydratorValue; +}; + +type MappedHydratedObjectClass = Inst & + HydratedObjectSchemaFields; + +export type MappedHydratedObjectConstructor = { + new ( + ...args: ConstructorParameters + ): MappedHydratedObjectClass, Impl>; +} & { + [Prop in keyof Ctor]: Ctor[Prop]; +}; + +export const HydratedObject = HydratedObjectClass; +export type HydratedObject = MappedHydratedObjectClass; + +export default HydratedObject; diff --git a/api/src/hydrator/hydration.ts b/api/src/hydrator/hydration.ts new file mode 100644 index 0000000..1cc8e44 --- /dev/null +++ b/api/src/hydrator/hydration.ts @@ -0,0 +1,145 @@ +import HydratedObject from "./HydratedObject"; +import type { HydratedObjectConstructor, HydratorSchema, HydratorSchemaFull, HydratorValue } from "../types/hydrator"; +import { isPrototypeOf } from "@seventv/util/modules/prototype"; + +export function hydrateValue( + input: unknown, + schema: S, + throwPath?: string, + ancestors?: unknown[], +): HydratorValue { + const resolved = resolveHydratorSchema(schema, input, ancestors); + + try { + const type = resolved.type; + switch (typeof type) { + case "string": { + switch (type) { + case "object": { + if (typeof input !== "object" || !input) throw void 0; + + const hydrated = {}; + const newAncestors = ancestors ? [input, ...ancestors] : [input]; + + if ("schema" in resolved) { + for (const [prop, schema] of Object.entries(resolved.schema)) { + const isOwn = Object.prototype.hasOwnProperty.call(input, prop); + const raw = isOwn ? Reflect.get(input, prop) : undefined; + + Reflect.set(hydrated, prop, hydrateValue(raw, schema, prop, newAncestors)); + } + } else if ("children" in resolved) { + for (const prop of Reflect.ownKeys(input)) { + const raw = Reflect.get(input, prop); + + Reflect.set( + hydrated, + prop, + hydrateValue(raw, resolved.children, prop.toString(), newAncestors), + ); + } + } else { + throw void 0; + } + + return >hydrated; + } + + case "array": { + if (input instanceof Array) { + const hydrated: unknown[] = []; + const newAncestors = ancestors ? [input, ...ancestors] : [input]; + + for (let x = 0; x < input.length; x++) { + try { + hydrated.push(hydrateValue(input[x], resolved.children, `[${x}]`, newAncestors)); + } catch (err) { + if (resolved.skipInvalid) continue; + throw err; + } + } + + return >hydrated; + } + + throw void 0; + } + + case "never": { + throw void 0; + } + + case "null": { + if (input === null) return >input; + throw void 0; + } + + default: { + if (typeof input === type) return >input; + throw void 0; + } + } + } + + case "function": { + if (isPrototypeOf(HydratedObject, type)) { + if (typeof input !== "object" || !input) throw void 0; + + return >new type(input); + } + + throw void 0; + } + } + } catch (err) { + if (resolved.required === false) { + if (resolved.default !== undefined) return >resolved.default; + return >undefined; + } + + if (err instanceof HydrationError) { + if (throwPath) { + if (err.throwPath) err.throwPath = `${throwPath}.${err.throwPath}`; + else err.throwPath = throwPath; + } + + throw err; + } else { + throw new HydrationError(throwPath, err); + } + } +} + +export function resolveHydratorSchema( + schema: HydratorSchema, + current: unknown, + ancestors?: unknown[], +): HydratorSchemaFull { + if (typeof schema == "function" && !isPrototypeOf(HydratedObject, schema)) { + schema = schema(current, ancestors ?? []); + } + + if (typeof schema === "string" || typeof schema === "function") { + return { type: schema }; + } + + return schema; +} + +export class HydrationError extends Error { + constructor( + public throwPath?: string, + public subError?: unknown, + ) { + super(); + this.name = "Hydration Error"; + } + + get message() { + let msg = "Failed to parse required property"; + if (this.throwPath) msg += ` at path '${this.throwPath}'`; + if (this.subError instanceof Error) msg += ": " + this.subError.message; + + return msg; + } +} diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 0000000..3e8b121 --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,5 @@ +export { HydratedObject } from "./hydrator/HydratedObject"; + +export * from "./hydrator/hydration"; + +export type * from "./types/hydrator"; diff --git a/api/src/types/hydrator.d.ts b/api/src/types/hydrator.d.ts new file mode 100644 index 0000000..30292e6 --- /dev/null +++ b/api/src/types/hydrator.d.ts @@ -0,0 +1,128 @@ +import type HydratedObject from "../hydrator/HydratedObject"; + +export interface HydratorSchemaBase { + required?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default?: any; +} + +export interface HydratorSchemaOptional extends HydratorSchemaBase { + required: false; +} + +export interface HydratorSchemaOptionalWithDefault extends HydratorSchemaOptional { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default?: any; +} + +export type HydratorValueOptional = S extends HydratorSchemaOptionalWithDefault + ? V | S["default"] + : S extends HydratorSchemaOptional + ? V | undefined + : V; + +type HydratorSchemaPrimitives = "boolean" | "string" | "number" | "bigint" | "symbol" | "undefined" | "null" | "never"; +type PrimitiveType = T extends "boolean" + ? boolean + : T extends "string" + ? string + : T extends "number" + ? number + : T extends "bigint" + ? bigint + : T extends "symbol" + ? symbol + : T extends "undefined" + ? undefined + : T extends "null" + ? null + : T extends "never" + ? never + : unknown; + +export interface HydratorSchemaPrimitive extends HydratorSchemaBase { + type: HydratorSchemaPrimitives; +} + +export type HydratorValuePrimitive = PrimitiveType; + +export type HydratorSchemaPrimitiveShorthand = HydratorSchemaPrimitives; + +export type HydratorValuePrimitiveShorthand = PrimitiveType; + +export interface HydratorSchemaArray extends HydratorSchemaBase { + type: "array"; + children: HydratorSchema; + skipInvalid?: boolean; +} + +export type HydratorValueArray = HydratorValue[]; + +export interface HydratorSchemaObjectMap extends HydratorSchemaBase { + type: "object"; + children: HydratorSchema; +} + +export type HydratorValueObjectMap = { + [x: PropertyKey]: HydratorValue; +}; + +export interface HydratorSchemaObjectKeys extends HydratorSchemaBase { + type: "object"; + schema: Record; +} + +export type HydratorValueObjectKeys = { + [Prop in keyof S["schema"]]: HydratorValue; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type HydratedObjectConstructor = new (json: object, ...args: any[]) => HydratedObject; + +export interface HydratorSchemaHydratedObject extends HydratorSchemaBase { + type: HydratedObjectConstructor; +} + +export type HydratorValueHydratedObject = InstanceType; + +export type HydratorSchemaHydratedObjectShorthand = HydratedObjectConstructor; + +export type HydratorValueHydratedObjectShorthand = InstanceType; + +export type HydratorSchemaTransformer = (current: unknown, ancestors: unknown[]) => HydratorSchemaStatic; + +export type HydratorSchemaTransformerValue = HydratorValue>; + +export type HydratorSchemaFull = + | HydratorSchemaPrimitive + | HydratorSchemaArray + | HydratorSchemaObjectMap + | HydratorSchemaObjectKeys + | HydratorSchemaHydratedObject; + +export type HydratorSchemaShorthand = HydratorSchemaPrimitiveShorthand | HydratorSchemaHydratedObjectShorthand; + +export type HydratorSchemaStatic = HydratorSchemaFull | HydratorSchemaShorthand; + +export type HydratorSchema = HydratorSchemaStatic | HydratorSchemaTransformer; + +export type HydratorValue = HydratorValueOptional< + S, + S extends HydratorSchemaPrimitive + ? HydratorValuePrimitive + : S extends HydratorSchemaPrimitiveShorthand + ? HydratorValuePrimitiveShorthand + : S extends HydratorSchemaArray + ? HydratorValueArray + : S extends HydratorSchemaObjectMap + ? HydratorValueObjectMap + : S extends HydratorSchemaObjectKeys + ? HydratorValueObjectKeys + : S extends HydratorSchemaHydratedObject + ? HydratorValueHydratedObject + : S extends HydratorSchemaHydratedObjectShorthand + ? HydratorValueHydratedObjectShorthand + : S extends HydratorSchemaTransformer + ? HydratorSchemaTransformerValue + : unknown +>; diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..848bcd5 --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "include": ["env.d.ts", "src/**/*"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "strict": true, + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "baseUrl": ".", + "experimentalDecorators": true + } +} From 80761a57c3d95d7e6b63357c9a718ceebb1280b8 Mon Sep 17 00:00:00 2001 From: Melonify Date: Sat, 7 Oct 2023 20:25:20 -0400 Subject: [PATCH 15/15] Clonability for hydrator values --- api/src/hydrator/HydratedObject.ts | 71 +++++++++++++++++++++++------- api/src/hydrator/hydration.ts | 40 ++++++++++++++++- api/src/types/hydrator.d.ts | 2 +- 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/api/src/hydrator/HydratedObject.ts b/api/src/hydrator/HydratedObject.ts index 242b1d1..3086565 100644 --- a/api/src/hydrator/HydratedObject.ts +++ b/api/src/hydrator/HydratedObject.ts @@ -1,24 +1,63 @@ -import { hydrateValue } from "./hydration"; +import { cloneValue, hydrateValue } from "./hydration"; import type { HydratorSchema, HydratorValue } from "../types/hydrator"; import type { DefiniteKey } from "@seventv/util/modules/types"; -type HydratedObjectConstructor = typeof HydratedObjectClass; -class HydratedObjectClass { +type BaseHydratedObjectConstructor = typeof BaseHydratedObject; +export class BaseHydratedObject { static SCHEMA: HydratedObjectSchema = {}; - constructor(json: object) { - const cls = this.constructor; + constructor(input: object | BaseHydratedObject | HydratedObjectCloningContext) { + const cls = this.constructor; + + if (input instanceof HydratedObjectCloningContext) { + cls.fromClone(this, input.source, input.seen); + } else if (input instanceof cls) { + cls.fromClone(this, input, new WeakMap()); + } else { + cls.fromJSON(this, input); + } + } + + protected static fromJSON(instance: BaseHydratedObject, json: object) { + const cls = instance.constructor; for (const [prop, schema] of Object.entries(cls.SCHEMA)) { const isOwn = Object.prototype.hasOwnProperty.call(json, prop); const raw = isOwn ? Reflect.get(json, prop) : undefined; - Reflect.set(this, prop, hydrateValue(raw, schema, prop, [json])); + Reflect.set(instance, prop, hydrateValue(raw, schema, prop, [json])); } } + + protected static fromClone( + instance: BaseHydratedObject, + source: BaseHydratedObject, + seen: WeakMap, + ) { + const cls = instance.constructor; + + seen.set(source, instance); + + for (const prop of Object.keys(cls.SCHEMA)) { + const raw = Reflect.get(source, prop); + + Reflect.set(instance, prop, cloneValue(raw, seen)); + } + } + + clone() { + const cls = typeof this>this.constructor; + + return new cls(this); + } } -export type HydratedObjectSchema = Record; +export class HydratedObjectCloningContext { + constructor( + public source: BaseHydratedObject, + public seen: WeakMap, + ) {} +} /* Assert to TypeScript that the implementation will always define props from the schema * @@ -26,22 +65,22 @@ export type HydratedObjectSchema = Record; * would be instantiated with, so we export a mapper type with a generic so downstream abstract classes can also exhibit this property * This does not affect implementors, only classes which wish to "pass along" the final implementor to the constructor's generic if they are abstract */ +type HydratedObjectSchema = Record; + type HydratedObjectSchemaFields = { [Key in keyof S as DefiniteKey]: HydratorValue; }; -type MappedHydratedObjectClass = Inst & +type MappedHydratedObjectClass = Inst & HydratedObjectSchemaFields; -export type MappedHydratedObjectConstructor = { - new ( - ...args: ConstructorParameters - ): MappedHydratedObjectClass, Impl>; -} & { - [Prop in keyof Ctor]: Ctor[Prop]; +export type MappedHydratedObjectConstructor< + Ctor extends BaseHydratedObjectConstructor = BaseHydratedObjectConstructor, +> = Ctor & { + new (...args: ConstructorParameters): MappedHydratedObjectClass, Impl>; }; -export const HydratedObject = HydratedObjectClass; -export type HydratedObject = MappedHydratedObjectClass; +export const HydratedObject = BaseHydratedObject; +export type HydratedObject = InstanceType; export default HydratedObject; diff --git a/api/src/hydrator/hydration.ts b/api/src/hydrator/hydration.ts index 1cc8e44..88de266 100644 --- a/api/src/hydrator/hydration.ts +++ b/api/src/hydrator/hydration.ts @@ -1,4 +1,4 @@ -import HydratedObject from "./HydratedObject"; +import HydratedObject, { HydratedObjectCloningContext } from "./HydratedObject"; import type { HydratedObjectConstructor, HydratorSchema, HydratorSchemaFull, HydratorValue } from "../types/hydrator"; import { isPrototypeOf } from "@seventv/util/modules/prototype"; @@ -143,3 +143,41 @@ export class HydrationError extends Error { return msg; } } + +export function cloneValue(value: V, seen?: WeakMap): V { + // Primitives always share identity + if (typeof value !== "object" || value === null) return value; + + // Clones of objects that share identity, will share identity + seen = seen ?? new WeakMap(); + if (seen.has(value)) return seen.get(value); + + if (value instanceof HydratedObject) { + const cls = value.constructor; + const context = new HydratedObjectCloningContext(value, seen); + + return new cls(context); + } + + if (value instanceof Array) { + const cloned: HydratorValue[] = []; + seen.set(value, cloned); + + for (let x = 0; x < value.length; x++) { + cloned[x] = cloneValue(value[x], seen); + } + + return cloned; + } else { + const cloned: Record = {}; + seen.set(value, cloned); + + for (const prop of Reflect.ownKeys(value)) { + const raw = Reflect.get(value, prop); + + Reflect.set(cloned, prop, cloneValue(raw, seen)); + } + + return cloned; + } +} diff --git a/api/src/types/hydrator.d.ts b/api/src/types/hydrator.d.ts index 30292e6..af41f2a 100644 --- a/api/src/types/hydrator.d.ts +++ b/api/src/types/hydrator.d.ts @@ -106,7 +106,7 @@ export type HydratorSchemaStatic = HydratorSchemaFull | HydratorSchemaShorthand; export type HydratorSchema = HydratorSchemaStatic | HydratorSchemaTransformer; -export type HydratorValue = HydratorValueOptional< +export type HydratorValue = HydratorValueOptional< S, S extends HydratorSchemaPrimitive ? HydratorValuePrimitive