From 0e196e63c206aed17151d4702385af133e1f6c19 Mon Sep 17 00:00:00 2001 From: Alexandra Petrova <32033964+Fsss126@users.noreply.github.com> Date: Thu, 27 Jun 2024 16:57:57 +0900 Subject: [PATCH 01/88] feat: ts types for colors (#3957) (#4270) --- .../code/color-typings-augmentation.ts | 5 ++++ .../services/colors-config/index.ts | 10 +++++--- packages/ui/src/main.ts | 2 +- packages/ui/src/services/color/types.ts | 23 ++++++++++++++++--- .../ui/src/services/global-config/types.ts | 12 +++++++--- 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 packages/docs/page-config/services/colors-config/code/color-typings-augmentation.ts diff --git a/packages/docs/page-config/services/colors-config/code/color-typings-augmentation.ts b/packages/docs/page-config/services/colors-config/code/color-typings-augmentation.ts new file mode 100644 index 0000000000..8596ac24fa --- /dev/null +++ b/packages/docs/page-config/services/colors-config/code/color-typings-augmentation.ts @@ -0,0 +1,5 @@ +declare module 'vuestic-ui' { + interface CustomColorVariables { + newColor: string + } +} diff --git a/packages/docs/page-config/services/colors-config/index.ts b/packages/docs/page-config/services/colors-config/index.ts index f8976e7840..7071bf2f41 100644 --- a/packages/docs/page-config/services/colors-config/index.ts +++ b/packages/docs/page-config/services/colors-config/index.ts @@ -147,14 +147,18 @@ export default definePageConfig({ block.paragraph("Methods like `getColors` return a computed reactives that can also be accessed by variable `colors`. The advantage is allow to change properties, instead of rewriting whole colors variable. For example, you can change `primary` color by writing `colors.primary = \"#ff0\"`. Multiple properties changes are also supported, you can write `setColors({'{ primary: \"#00f\", secondary: \"#0ff\" }'})`."), // otherServices - block.subtitle("Colors config with other services"), + block.subtitle("Adding new colors"), - block.paragraph("You can use your custom colors in Components config."), + block.paragraph("You can add new colors and use your custom colors in Components config."), block.code("components-config"), - block.paragraph("As well as in Icons config."), + block.paragraph("The same works for Icons config."), block.code("icons-config"), + block.subtitle("Getting typings for custom colors"), + block.paragraph("Color config is fully typed and provides type safety and autocompletion for all instances where color is specified. Color typings could be extended with your custom colors to rip the same benefits via Typescript [Module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)."), + block.code("color-typings-augmentation"), + // api block.subtitle("Colors config service API"), diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts index f88a9e85f1..99d87ec70b 100644 --- a/packages/ui/src/main.ts +++ b/packages/ui/src/main.ts @@ -27,5 +27,5 @@ export { defineVuesticConfig } from './services/global-config/types' export type { GlobalConfig, GlobalConfigUpdater, PartialGlobalConfig } from './services/global-config/types' export type { ComponentConfig } from './services/component-config' export type { IconConfig, IconConfiguration } from './services/icon/types' -export type { ColorConfig } from './services/color/types' +export type { ColorConfig, CustomColorVariables } from './services/color/types' export type { I18NKey, I18nConfig, CustomI18NKeys } from './services/i18n' diff --git a/packages/ui/src/services/color/types.ts b/packages/ui/src/services/color/types.ts index 66c577d2a5..cf9e72639d 100644 --- a/packages/ui/src/services/color/types.ts +++ b/packages/ui/src/services/color/types.ts @@ -25,14 +25,31 @@ export type EssentialVariables = { transparent: CssColor, } +/** + * This is a placeholder meant to be implemented via TypeScript's Module + * Augmentation feature to allow key type inference + * + * @example + * ```ts + * declare module 'vuestic-ui' { + * interface CustomColorVariables { + * newColor: string + * } + * } + * ``` + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ +export interface CustomColorVariables {} + type Capitalize = S extends `${infer First}${infer Rest}` ? `${Uppercase}${Rest}` : S -type OnColors = `on${Capitalize}` +type OnColors = `on${Capitalize}` -export type ColorVariables = { [colorName: string]: CssColor } & EssentialVariables & { +export type ColorVariables = EssentialVariables & CustomColorVariables & { [key in OnColors]?: CssColor -} +} & Record export type ColorConfig = { variables: ColorVariables, diff --git a/packages/ui/src/services/global-config/types.ts b/packages/ui/src/services/global-config/types.ts index ed6e974be8..5201e6d71b 100644 --- a/packages/ui/src/services/global-config/types.ts +++ b/packages/ui/src/services/global-config/types.ts @@ -1,8 +1,8 @@ import type { ComponentConfig } from '../component-config' -import type { ColorConfig } from '../color' +import type { ColorConfig, CustomColorVariables } from '../color' import type { IconConfig } from '../icon' import type { BreakpointConfig } from '../breakpoint' -import type { I18nConfig } from '../i18n' +import type { I18nConfig, CustomI18NKeys } from '../i18n' import type { Ref, Component } from 'vue' import type { ColorsClassesConfig } from '../colors-classes' @@ -21,7 +21,13 @@ type DeepPartial = T extends Record ? { [P in keyof T]?: P extends 'components' ? T[P] : DeepPartial; } : T; -export type PartialGlobalConfig = DeepPartial +export type PartialGlobalConfig = DeepPartial & { + // Need to maintain ability to extend through module augmentation + colors?: { + variables?: Partial + } + i18n?: Partial +} export type SizeConfig = { defaultSize?: number, From 69e3ab289d7a2372a523be57c5b14b5b403f1239 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Thu, 27 Jun 2024 10:59:13 +0300 Subject: [PATCH 02/88] feat(sticky-scrollbar): init component (#4286) * feat(sticky-scrollbar): init component * fix: expect scroll content to change * fix: remove extra console logs * fix: improve perf --- packages/docs/page-config/navigationRoutes.ts | 7 + .../sticky-scrollbar/examples/Horizontal.vue | 7 + .../ui-elements/sticky-scrollbar/index.ts | 8 ++ packages/nuxt/src/config/components.ts | 3 +- packages/ui/src/components/index.ts | 1 + .../StickyScrollbar.stories.ts | 61 +++++++++ .../va-sticky-scrollbar/StickyScrollbar.vue | 122 ++++++++++++++++++ .../components/va-sticky-scrollbar/index.ts | 1 + packages/ui/src/composables/index.ts | 1 + packages/ui/src/composables/useElementRect.ts | 41 ++++++ .../ui/src/composables/useResizeObserver.ts | 15 ++- .../ui/src/services/vue-plugin/components.ts | 1 + packages/ui/src/utils/scrollbar-size.ts | 7 + 13 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue create mode 100644 packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts create mode 100644 packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts create mode 100644 packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue create mode 100644 packages/ui/src/components/va-sticky-scrollbar/index.ts create mode 100644 packages/ui/src/composables/useElementRect.ts create mode 100644 packages/ui/src/utils/scrollbar-size.ts diff --git a/packages/docs/page-config/navigationRoutes.ts b/packages/docs/page-config/navigationRoutes.ts index 9f3d3e43f4..4a5a21231c 100644 --- a/packages/docs/page-config/navigationRoutes.ts +++ b/packages/docs/page-config/navigationRoutes.ts @@ -427,6 +427,13 @@ export const navigationRoutes: NavigationRoute[] = [ badge : navigationBadge.new('1.6.0 '), } }, + { + name: 'sticky-scrollbar', + displayName: 'Sticky Scrollbar', + meta: { + badge : navigationBadge.new('1.9.10'), + } + }, { name: "hover", displayName: "Hover", diff --git a/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue b/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue new file mode 100644 index 0000000000..9aad4cf63b --- /dev/null +++ b/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/Horizontal.vue @@ -0,0 +1,7 @@ + diff --git a/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts b/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts new file mode 100644 index 0000000000..b891fa6374 --- /dev/null +++ b/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts @@ -0,0 +1,8 @@ +export default definePageConfig({ + blocks: [ + block.title("StickyScrollbar"), + block.paragraph("This component adds floating scrollbar to element if it doesn't fit the screen. In case if you have large scroll container and want your scrollbar to be always visible, you need to use this component."), + + block.example('Horizontal') + ], +}); diff --git a/packages/nuxt/src/config/components.ts b/packages/nuxt/src/config/components.ts index 27ec9f4808..fd47a46669 100644 --- a/packages/nuxt/src/config/components.ts +++ b/packages/nuxt/src/config/components.ts @@ -90,5 +90,6 @@ export default [ 'VaMenuList', 'VaMenuItem', 'VaMenuGroup', - 'VaFormField' + 'VaFormField', + 'VaStickyScrollbar' ] diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 819f5ce235..263029f823 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -92,3 +92,4 @@ export * from './va-file-upload' export * from './va-textarea' export * from './va-menu' export * from './va-form-field' +export * from './va-sticky-scrollbar' diff --git a/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts new file mode 100644 index 0000000000..357b656c50 --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.stories.ts @@ -0,0 +1,61 @@ +import { defineComponent } from 'vue' +import { defineStory } from '../../../.storybook/types' +import VaStickyScrollbar from './StickyScrollbar.vue' + +export default { + title: 'VaStickyScrollbar', + component: VaStickyScrollbar, +} + +export const Horizontal = defineStory({ + story: () => ({ + components: { VaStickyScrollbar }, + + template: ` +
+
+ +
+ `, + }), +}) + +export const Vertical = defineStory({ + story: () => ({ + components: { VaStickyScrollbar }, + + template: ` +
+
+ +
+ `, + }), +}) + +/** Scrollbar reacts to content changes */ +export const DynamicContent = defineStory({ + story: () => ({ + data () { + return { + showBlock2: false, + } + }, + + mounted () { + setInterval(() => { + this.showBlock2 = !this.showBlock2 + }, 2000) + }, + + components: { VaStickyScrollbar }, + + template: ` +
+
+
+ +
+ `, + }), +}) diff --git a/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue new file mode 100644 index 0000000000..1165ccf38c --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue @@ -0,0 +1,122 @@ + + + diff --git a/packages/ui/src/components/va-sticky-scrollbar/index.ts b/packages/ui/src/components/va-sticky-scrollbar/index.ts new file mode 100644 index 0000000000..2e5eeb27ea --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/index.ts @@ -0,0 +1 @@ +export { default as VaStickyScrollbar } from './StickyScrollbar.vue' diff --git a/packages/ui/src/composables/index.ts b/packages/ui/src/composables/index.ts index 238410fd9e..9f92c7a5d8 100644 --- a/packages/ui/src/composables/index.ts +++ b/packages/ui/src/composables/index.ts @@ -75,3 +75,4 @@ export * from './useElementTextColor' export * from './useElementBackground' export * from './useImmediateFocus' export * from './useNumericProp' +export * from './useElementRect' diff --git a/packages/ui/src/composables/useElementRect.ts b/packages/ui/src/composables/useElementRect.ts new file mode 100644 index 0000000000..24b3db0513 --- /dev/null +++ b/packages/ui/src/composables/useElementRect.ts @@ -0,0 +1,41 @@ +import { Ref, onBeforeUnmount, onMounted, ref } from 'vue' + +export const useElementRect = (element: Ref) => { + const rect = ref({ top: 0, left: 0, width: 0, height: 0, bottom: 0, right: 0 }) satisfies Ref<{ + top: number + left: number + width: number + height: number + bottom: number + right: number + }> + + let resizeObserver: ResizeObserver | undefined + let mutationObserver: MutationObserver | undefined + + const updateRect = () => { + if (element.value) { + rect.value = element.value.getBoundingClientRect() + } + } + + onMounted(() => { + resizeObserver = new ResizeObserver(updateRect) + mutationObserver = new MutationObserver(updateRect) + + element.value && resizeObserver.observe(element.value) + element.value && mutationObserver.observe(element.value, { attributes: true, childList: true, subtree: true }) + + updateRect() + }) + + onBeforeUnmount(() => { + resizeObserver?.disconnect() + mutationObserver?.disconnect() + + resizeObserver = undefined + mutationObserver = undefined + }) + + return rect +} diff --git a/packages/ui/src/composables/useResizeObserver.ts b/packages/ui/src/composables/useResizeObserver.ts index 248d2bb8ee..c28b979e32 100644 --- a/packages/ui/src/composables/useResizeObserver.ts +++ b/packages/ui/src/composables/useResizeObserver.ts @@ -3,7 +3,15 @@ import { onBeforeUnmount, onMounted, ref, Ref, unref, watch } from 'vue' type MaybeRef = T | Ref type MaybeArray = T | T[] -export const useResizeObserver = (elementsList: MaybeRef[] | Ref, cb: ResizeObserverCallback) => { +const normalizeElements = (elements: MaybeRef[] | Ref>) => { + return Array.isArray(elements) + ? elements + : Array.isArray(elements.value) + ? elements.value + : [elements.value] +} + +export const useResizeObserver = (elementsList: MaybeRef[] | Ref>, cb: ResizeObserverCallback) => { let resizeObserver: ResizeObserver | undefined const observeAll = (elementsList: MaybeRef[]) => { @@ -16,12 +24,13 @@ export const useResizeObserver = (elementsLis watch(elementsList, (newValue) => { resizeObserver?.disconnect() - observeAll(Array.isArray(newValue) ? newValue : [newValue]) + + observeAll(normalizeElements(newValue)) }) onMounted(() => { resizeObserver = new ResizeObserver(cb) - observeAll(Array.isArray(elementsList) ? elementsList : [elementsList]) + observeAll(normalizeElements(elementsList)) }) onBeforeUnmount(() => resizeObserver?.disconnect()) diff --git a/packages/ui/src/services/vue-plugin/components.ts b/packages/ui/src/services/vue-plugin/components.ts index a1fd0f4e7f..28206df391 100644 --- a/packages/ui/src/services/vue-plugin/components.ts +++ b/packages/ui/src/services/vue-plugin/components.ts @@ -92,4 +92,5 @@ export { VaMenuItem, VaMenuGroup, VaFormField, + VaStickyScrollbar, } from '../../components' diff --git a/packages/ui/src/utils/scrollbar-size.ts b/packages/ui/src/utils/scrollbar-size.ts new file mode 100644 index 0000000000..77b03fe556 --- /dev/null +++ b/packages/ui/src/utils/scrollbar-size.ts @@ -0,0 +1,7 @@ +export const getScrollbarSize = (element: HTMLElement | null | undefined) => { + if (!element) { return 0 } + + const scrollbarWidth = element.offsetWidth - element.clientWidth + const scrollbarHeight = element.offsetHeight - element.clientHeight + return Math.max(scrollbarWidth, scrollbarHeight) +} From de9a907853a27d1e189b7822fc9aca76594944c6 Mon Sep 17 00:00:00 2001 From: Alexandra Petrova <32033964+Fsss126@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:06:45 +0900 Subject: [PATCH 03/88] Color css variables must be rendered on app root element, not body (#4233) (#4272) --- packages/ui/src/composables/useTeleported.ts | 5 +- .../plugin/create-color-config-plugin.ts | 48 +++++++++++++++---- packages/ui/src/utils/dom.ts | 4 ++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/composables/useTeleported.ts b/packages/ui/src/composables/useTeleported.ts index e0a7e08eb6..f2d61cd613 100644 --- a/packages/ui/src/composables/useTeleported.ts +++ b/packages/ui/src/composables/useTeleported.ts @@ -26,7 +26,9 @@ export const findTeleportedFrom = (el: HTMLElement | undefined | null): HTMLElem export const useTeleported = () => { const componentId = useCurrentComponentId() - const scopedDataV = getCurrentInstance()?.vnode.scopeId + const currentInstance = getCurrentInstance() + + const scopedDataV = currentInstance?.vnode.scopeId return { teleportFromAttrs: { @@ -35,6 +37,7 @@ export const useTeleported = () => { teleportedAttrs: { [TELEPORT_ATTR]: componentId, ...(scopedDataV ? { [scopedDataV]: '' } : undefined), + ...currentInstance?.appContext.config?.globalProperties?.$vaColorConfig.getAppStylesRootAttribute(), }, findTeleportedFrom, } diff --git a/packages/ui/src/services/color/plugin/create-color-config-plugin.ts b/packages/ui/src/services/color/plugin/create-color-config-plugin.ts index 361e07f266..1898c6b91b 100644 --- a/packages/ui/src/services/color/plugin/create-color-config-plugin.ts +++ b/packages/ui/src/services/color/plugin/create-color-config-plugin.ts @@ -1,11 +1,12 @@ import { PartialGlobalConfig } from './../../global-config/types' import { ColorVariables } from './../types' -import { App, watch, computed } from 'vue' +import { App, watch, computed, onMounted } from 'vue' import { isServer } from '../../../utils/ssr' import { cssVariableName } from '../utils' import { useColors } from '../../../composables' import { generateUniqueId } from '../../../utils/uuid' -import { addOrUpdateStyleElement } from '../../../utils/dom' +import { addOrUpdateStyleElement, removeStyleElement } from '../../../utils/dom' +import { isDev } from '../../../utils/env' export const setCSSVariable = (name: string, value: string, root: HTMLElement) => { root.style.setProperty(cssVariableName(name), value) @@ -15,6 +16,10 @@ export const generateCSSVariable = (key: string, value: string) => { return `${cssVariableName(key)}: ${value};\n` } +const stylesRootAttr = 'data-va-styles-root' + +const getStyleElementId = (id: string | number) => `va-color-variables-${id}` + export const createColorConfigPlugin = (app: App, config?: PartialGlobalConfig) => { const { colors: configColors, getTextColor, getColor, currentPresetName, applyPreset } = useColors() @@ -29,10 +34,11 @@ export const createColorConfigPlugin = (app: App, config?: PartialGlobalConfig) return `${renderedColors};${renderedOnColors}` } - const renderCSSVariablesStyleContent = (colors: ColorVariables = configColors) => { + const renderCSSVariablesStyleContent = (colors: ColorVariables = configColors, selector = ':root') => { const colorNames = Object.keys(colors) - let result = ':root {\n' + let result = `${selector} { +` colorNames.forEach((key) => { result += generateCSSVariable(key, colors[key]) }) @@ -44,15 +50,38 @@ export const createColorConfigPlugin = (app: App, config?: PartialGlobalConfig) return result } - const uniqueId = computed(generateUniqueId) + const uniqueId = computed(() => app._uid) + + const stylesRootSelector = computed(() => `[${stylesRootAttr}="${uniqueId.value}"]`) const updateColors = (newValue: ColorVariables | undefined) => { - if (!newValue) { return } - if (isServer()) { return } + if (!newValue || isServer()) { return } + + const styleContent = renderCSSVariablesStyleContent(newValue, stylesRootSelector.value) - const styleContent = renderCSSVariablesStyleContent(newValue) + addOrUpdateStyleElement(getStyleElementId(uniqueId.value), () => styleContent) + } + + function getAppStylesRootAttribute () { + return { [stylesRootAttr]: uniqueId.value } + } - addOrUpdateStyleElement(`va-color-variables-${uniqueId.value}`, () => styleContent) + const origMount = app.mount + + app.mount = function (...args: Parameters) { + const result = origMount.apply(this, args) + const appRootElement = app._container as HTMLElement + + // Remove previous styles when remounting to the same root element (happens on HMR) + const existingStylesId = appRootElement.getAttribute(stylesRootAttr) + + if (existingStylesId && existingStylesId !== uniqueId.value.toString()) { + removeStyleElement(getStyleElementId(existingStylesId)) + } + + appRootElement.setAttribute(stylesRootAttr, uniqueId.value.toString()) + + return result } watch(configColors, (newValue) => { @@ -62,6 +91,7 @@ export const createColorConfigPlugin = (app: App, config?: PartialGlobalConfig) return { colors: configColors, currentPresetName, + getAppStylesRootAttribute, renderCSSVariables, updateColors, renderCSSVariablesStyleContent, diff --git a/packages/ui/src/utils/dom.ts b/packages/ui/src/utils/dom.ts index fe79f7a8ad..895c7c9003 100644 --- a/packages/ui/src/utils/dom.ts +++ b/packages/ui/src/utils/dom.ts @@ -23,3 +23,7 @@ export const addOrUpdateStyleElement = (id: string, getStyles: () => string): vo document.head.append(stylesElement) } } + +export const removeStyleElement = (id: string): void => { + document.getElementById(id)?.remove() +} From 4aac12a8a75680c8f27cf717e945307dd8c9e66e Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Thu, 27 Jun 2024 11:07:35 +0300 Subject: [PATCH 04/88] feat: remove lodash (#4316) * feat: remove lodash * fix: correct clone deep function --- .../sandbox/configs/vite/common-config.ts | 2 +- .../src/components/va-affix/VaAffix-utils.ts | 2 +- .../ui/src/components/va-affix/VaAffix.vue | 2 +- .../va-avatar-group/VaAvatarGroup.vue | 3 +-- .../ui/src/components/va-avatar/VaAvatar.vue | 2 +- .../components/va-badge/VaBadge.stories.ts | 3 +-- .../ui/src/components/va-badge/VaBadge.vue | 2 +- .../va-button-dropdown/VaButtonDropdown.vue | 12 ++++----- .../va-button-group/VaButtonGroup.vue | 3 +-- .../ui/src/components/va-button/VaButton.vue | 3 +-- packages/ui/src/components/va-card/VaCard.vue | 3 +-- .../va-card-actions/VaCardActions.vue | 2 +- packages/ui/src/components/va-chip/VaChip.vue | 3 +-- .../src/components/va-collapse/VaCollapse.vue | 11 ++++---- .../va-color-input/VaColorInput.vue | 2 +- .../hooks/useGlobalConfigProvider.ts | 2 +- .../src/components/va-counter/VaCounter.vue | 12 ++++----- .../va-data-table/VaDataTable.demo.vue | 2 +- .../va-data-table/VaDataTable.new.demo.vue | 2 +- .../components/va-data-table/VaDataTable.vue | 4 +-- .../va-data-table/hooks/useColumns.ts | 6 ++--- .../components/va-date-input/VaDateInput.vue | 2 +- .../va-date-input/hooks/input-text-parser.ts | 4 +-- .../va-date-input/hooks/model-value-parser.ts | 3 +-- .../va-date-input/hooks/model-value.ts | 2 +- .../hooks/hovered-option-hook.ts | 2 +- .../components/va-date-picker/hooks/view.ts | 2 +- .../va-date-picker/utils/date-utils.ts | 3 +-- .../src/components/va-dropdown/VaDropdown.vue | 2 +- packages/ui/src/components/va-icon/VaIcon.vue | 2 +- .../ui/src/components/va-image/VaImage.vue | 2 +- .../va-image/hooks/useNativeImgAttributes.ts | 2 +- .../va-infinite-scroll/VaInfiniteScroll.vue | 4 +-- .../va-input-wrapper/VaInputWrapper.vue | 2 +- .../ui/src/components/va-input/VaInput.vue | 7 +++--- .../ui/src/components/va-list/VaListItem.vue | 2 +- .../components/va-modal/tests/VaModal.spec.ts | 1 - .../va-navbar/tests/VaNavbar.spec.ts | 1 - .../va-option-list/VaOptionList.vue | 2 +- .../components/va-pagination/VaPagination.vue | 6 ++--- .../va-progress-bar/VaProgressBar.vue | 2 +- .../va-progress-circle/VaProgressCircle.vue | 2 +- .../components/va-rating/hooks/useRating.ts | 2 +- .../ui/src/components/va-select/VaSelect.vue | 2 +- .../VaSelectOptionList/VaSelectOptionList.vue | 2 +- .../ui/src/components/va-slider/VaSlider.vue | 2 +- .../ui/src/components/va-split/VaSplit.vue | 6 ++--- .../ui/src/components/va-switch/VaSwitch.vue | 2 +- .../src/components/va-textarea/VaTextarea.vue | 6 ++--- .../VaTimePickerColumn/VaTimePickerColumn.vue | 2 +- .../va-virtual-scroller/VaVirtualScroller.vue | 2 +- packages/ui/src/composables/useBem.ts | 8 +++--- .../ui/src/composables/useCSSVariables.ts | 4 +-- packages/ui/src/composables/useColors.ts | 6 ++--- packages/ui/src/composables/useDebounce.ts | 2 +- packages/ui/src/composables/useFocus.ts | 6 ++--- packages/ui/src/composables/useFormField.ts | 2 +- packages/ui/src/composables/useSize.ts | 4 +-- packages/ui/src/composables/useSlotPassed.ts | 2 +- packages/ui/src/composables/useTrackBy.ts | 2 +- packages/ui/src/composables/useValidation.ts | 9 +++---- packages/ui/src/services/color/utils.ts | 7 +++--- .../config-transport/createSetupFn.ts | 2 +- .../services/global-config/global-config.ts | 2 +- .../icon/utils/get-icon-configuration.ts | 6 ++--- ...gister-vuestic-web-components-essential.ts | 4 +-- .../register-vuestic-web-components.ts | 2 -- packages/ui/src/utils/clamp.ts | 3 +++ packages/ui/src/utils/clone-deep.ts | 11 ++++++++ packages/ui/src/utils/debounce.ts | 17 +++++++++++++ packages/ui/src/utils/env.ts | 1 - packages/ui/src/utils/is-date.ts | 1 + packages/ui/src/utils/is-function.ts | 2 ++ packages/ui/src/utils/is-number.ts | 1 + packages/ui/src/utils/is-string.ts | 1 + packages/ui/src/utils/isNilValue.ts | 5 +++- packages/ui/src/utils/merge-deep.ts | 4 +++ packages/ui/src/utils/noop.ts | 1 + packages/ui/src/utils/omit.ts | 8 ++++++ packages/ui/src/utils/pick.ts | 8 ++++++ packages/ui/src/utils/resolveSlot.ts | 4 +-- packages/ui/src/utils/text-case.ts | 25 +++++++++++++++++++ packages/ui/src/utils/throttle.ts | 9 +++++++ packages/ui/src/utils/un-function.ts | 4 ++- packages/ui/src/utils/value-by-key.ts | 2 +- 85 files changed, 207 insertions(+), 132 deletions(-) create mode 100644 packages/ui/src/utils/clamp.ts create mode 100644 packages/ui/src/utils/clone-deep.ts create mode 100644 packages/ui/src/utils/debounce.ts create mode 100644 packages/ui/src/utils/is-date.ts create mode 100644 packages/ui/src/utils/is-function.ts create mode 100644 packages/ui/src/utils/is-number.ts create mode 100644 packages/ui/src/utils/is-string.ts create mode 100644 packages/ui/src/utils/noop.ts create mode 100644 packages/ui/src/utils/omit.ts create mode 100644 packages/ui/src/utils/pick.ts create mode 100644 packages/ui/src/utils/text-case.ts create mode 100644 packages/ui/src/utils/throttle.ts diff --git a/packages/sandbox/configs/vite/common-config.ts b/packages/sandbox/configs/vite/common-config.ts index a4aad48325..67507c8184 100644 --- a/packages/sandbox/configs/vite/common-config.ts +++ b/packages/sandbox/configs/vite/common-config.ts @@ -33,7 +33,7 @@ export default function createViteConfig (type: BuildType) { build: { outDir, sourcemap: isBase, - minify: 'terser', + minify: false, terserOptions: { keep_classnames: true, }, diff --git a/packages/ui/src/components/va-affix/VaAffix-utils.ts b/packages/ui/src/components/va-affix/VaAffix-utils.ts index 4181c2c48a..a399526c70 100644 --- a/packages/ui/src/components/va-affix/VaAffix-utils.ts +++ b/packages/ui/src/components/va-affix/VaAffix-utils.ts @@ -1,4 +1,4 @@ -import throttle from 'lodash/throttle.js' +import { throttle } from "../../utils/throttle"; export type State = { isTopAffixed: boolean; diff --git a/packages/ui/src/components/va-affix/VaAffix.vue b/packages/ui/src/components/va-affix/VaAffix.vue index 0c1ca23b23..b5ab4cd480 100644 --- a/packages/ui/src/components/va-affix/VaAffix.vue +++ b/packages/ui/src/components/va-affix/VaAffix.vue @@ -18,7 +18,7 @@ diff --git a/packages/ui/src/components/va-avatar/VaAvatar.vue b/packages/ui/src/components/va-avatar/VaAvatar.vue index f49fe3067d..cab477e775 100644 --- a/packages/ui/src/components/va-avatar/VaAvatar.vue +++ b/packages/ui/src/components/va-avatar/VaAvatar.vue @@ -33,7 +33,6 @@ diff --git a/packages/ui/src/components/va-badge/VaBadge.stories.ts b/packages/ui/src/components/va-badge/VaBadge.stories.ts index f019f46f73..81955c8261 100644 --- a/packages/ui/src/components/va-badge/VaBadge.stories.ts +++ b/packages/ui/src/components/va-badge/VaBadge.stories.ts @@ -1,7 +1,6 @@ import { VaBadge } from './' import { VaIcon } from '../va-icon' import { VaButton } from '../va-button' -import set from 'lodash/set' import { placementsPositionsWithAliases } from '../../composables' import { VaCard, VaCardContent } from '../va-card' import { VaAvatar } from '../va-avatar' @@ -16,7 +15,7 @@ const statusToText = (status?: StoryStatus) => { } const addText = (story: any, text?: string, status?: StoryStatus) => { - set(story, 'parameters.docs.description.story', `${statusToText(status)} ${text || ''}`) + story.parameters.docs.description.story = `${statusToText(status)} ${text || ''}` } export default { diff --git a/packages/ui/src/components/va-badge/VaBadge.vue b/packages/ui/src/components/va-badge/VaBadge.vue index 608b482905..daad767431 100644 --- a/packages/ui/src/components/va-badge/VaBadge.vue +++ b/packages/ui/src/components/va-badge/VaBadge.vue @@ -21,7 +21,6 @@ diff --git a/packages/ui/src/components/va-button/VaButton.vue b/packages/ui/src/components/va-button/VaButton.vue index 4cfc53ef59..9a50043f6d 100644 --- a/packages/ui/src/components/va-button/VaButton.vue +++ b/packages/ui/src/components/va-button/VaButton.vue @@ -51,8 +51,6 @@ diff --git a/packages/ui/src/components/va-config/hooks/useGlobalConfigProvider.ts b/packages/ui/src/components/va-config/hooks/useGlobalConfigProvider.ts index d6f6715b22..45ec1dbe33 100644 --- a/packages/ui/src/components/va-config/hooks/useGlobalConfigProvider.ts +++ b/packages/ui/src/components/va-config/hooks/useGlobalConfigProvider.ts @@ -1,5 +1,5 @@ import { mergeDeep } from './../../../utils/merge-deep' -import cloneDeep from 'lodash/cloneDeep' +import { cloneDeep } from '../../../utils/clone-deep' import { provide, computed, Ref } from 'vue' import { useGlobalConfig } from '../../../composables' import { GLOBAL_CONFIG, GlobalConfig, GlobalConfigUpdater, PartialGlobalConfig } from '../../../services/global-config' diff --git a/packages/ui/src/components/va-counter/VaCounter.vue b/packages/ui/src/components/va-counter/VaCounter.vue index 728e9e5d33..b788e70997 100644 --- a/packages/ui/src/components/va-counter/VaCounter.vue +++ b/packages/ui/src/components/va-counter/VaCounter.vue @@ -98,8 +98,6 @@ diff --git a/packages/ui/src/components/va-image/hooks/useNativeImgAttributes.ts b/packages/ui/src/components/va-image/hooks/useNativeImgAttributes.ts index 59cdc21848..4bf9ab9afe 100644 --- a/packages/ui/src/components/va-image/hooks/useNativeImgAttributes.ts +++ b/packages/ui/src/components/va-image/hooks/useNativeImgAttributes.ts @@ -1,5 +1,5 @@ import { computed, type ExtractPropTypes, type PropType } from 'vue' -import pick from 'lodash/pick.js' +import { pick } from '../../../utils/pick' export const useNativeImgAttributesProps = { src: { type: String, required: true }, diff --git a/packages/ui/src/components/va-infinite-scroll/VaInfiniteScroll.vue b/packages/ui/src/components/va-infinite-scroll/VaInfiniteScroll.vue index a7a6c7121d..f696f66794 100644 --- a/packages/ui/src/components/va-infinite-scroll/VaInfiniteScroll.vue +++ b/packages/ui/src/components/va-infinite-scroll/VaInfiniteScroll.vue @@ -33,7 +33,7 @@ diff --git a/packages/ui/src/components/va-pagination/VaPagination.vue b/packages/ui/src/components/va-pagination/VaPagination.vue index 6e195d9d7f..9986792da5 100644 --- a/packages/ui/src/components/va-pagination/VaPagination.vue +++ b/packages/ui/src/components/va-pagination/VaPagination.vue @@ -102,10 +102,8 @@ import { computed, watch, nextTick, - WritableComputedRef, ComputedRef, + ComputedRef, } from 'vue' -import clamp from 'lodash/clamp.js' -import pick from 'lodash/pick.js' import { isDev } from '../../utils/env' import { @@ -120,6 +118,8 @@ import { setPaginationRange } from './setPaginationRange' import { VaButton } from '../va-button' import { ExtractComponentPropTypes } from '../../utils/component-options' +import { pick } from '../../utils/pick' +import { clamp } from '../../utils/clamp' defineOptions({ name: 'VaPagination', diff --git a/packages/ui/src/components/va-progress-bar/VaProgressBar.vue b/packages/ui/src/components/va-progress-bar/VaProgressBar.vue index 9a63e26f2b..a1fba7498d 100644 --- a/packages/ui/src/components/va-progress-bar/VaProgressBar.vue +++ b/packages/ui/src/components/va-progress-bar/VaProgressBar.vue @@ -43,7 +43,7 @@ diff --git a/packages/ui/src/components/va-select/components/VaSelectOptionList/VaSelectOptionList.vue b/packages/ui/src/components/va-select/components/VaSelectOptionList/VaSelectOptionList.vue index 2f4e09b6f7..782ba06a0d 100644 --- a/packages/ui/src/components/va-select/components/VaSelectOptionList/VaSelectOptionList.vue +++ b/packages/ui/src/components/va-select/components/VaSelectOptionList/VaSelectOptionList.vue @@ -78,7 +78,6 @@ diff --git a/packages/docs/page-config/composables/input-mask/examples/Date.vue b/packages/docs/page-config/composables/input-mask/examples/Date.vue new file mode 100644 index 0000000000..951d456400 --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/examples/Date.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/docs/page-config/composables/input-mask/examples/DefaultRegex.vue b/packages/docs/page-config/composables/input-mask/examples/DefaultRegex.vue new file mode 100644 index 0000000000..418b87f469 --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/examples/DefaultRegex.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/docs/page-config/composables/input-mask/examples/Ipv6Regex.vue b/packages/docs/page-config/composables/input-mask/examples/Ipv6Regex.vue new file mode 100644 index 0000000000..d2ffdc1bf4 --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/examples/Ipv6Regex.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/docs/page-config/composables/input-mask/examples/Numeral.vue b/packages/docs/page-config/composables/input-mask/examples/Numeral.vue new file mode 100644 index 0000000000..e922f99601 --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/examples/Numeral.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/docs/page-config/composables/input-mask/examples/Phone.vue b/packages/docs/page-config/composables/input-mask/examples/Phone.vue new file mode 100644 index 0000000000..4f2a6103de --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/examples/Phone.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/docs/page-config/composables/input-mask/examples/PhoneExtended.vue b/packages/docs/page-config/composables/input-mask/examples/PhoneExtended.vue new file mode 100644 index 0000000000..a260973643 --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/examples/PhoneExtended.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/docs/page-config/composables/input-mask/index.ts b/packages/docs/page-config/composables/input-mask/index.ts new file mode 100644 index 0000000000..de4fa32d67 --- /dev/null +++ b/packages/docs/page-config/composables/input-mask/index.ts @@ -0,0 +1,69 @@ +export default definePageConfig({ + blocks: [ + block.title('Input Mask'), + block.paragraph('Masks are used to format the input value. Instead of having built-in masks we provide a composable, that you can use with any input.'), + block.paragraph('Vuestic UI comes with a few predefined masks. You need to manually import them and use with `useInputMask` composable.'), + block.paragraph('`useInputMask` composable is designed to work with Regex based masks. It is a flexible solution that allows you to create any mask you need.'), + + block.subtitle('Examples'), + + block.example('DefaultRegex', { + title: 'Regex mask', + description: 'When creating a mask we use regex syntax, that allows you to deeply customize the mask.', + }), + + block.collapse('Regex basic syntax', [ + block.paragraph('Here is a list of regex tokens that you can use to create a mask:'), + + block.list([ + '`\\d` - any digit', + '`\\w` - any word character', + '`.` - any character', + '`[a-z]` - any character from a to z. Notice, can be also `[a-f]` - any character from a to f', + '`[0-9]` - any digit', + ]), + + block.paragraph('Mask support quantifiers syntax, allowing you to specify how many times the character should appear. Here are some examples:'), + + block.list([ + '`\\d?` - any digit or nothing', + '`\\d*` - any number of digits', + '`\\d+` - at least one digit', + '`\\d{3}` - exactly 3 digits', + '`\\d{3,}` - at least 3 digits', + '`\\d{3,5}` - from 3 to 5 digits', + ]), + + block.paragraph('Obviously, you can replace `\\d` with any token. You can also use groups to group characters, for example `(\\d - \\d)?`'), + block.alert('Notice that maximum repetition is 10.', 'warning'), + + block.paragraph('In case you need to use brackets in the mask, you need to escape them: `\\(\\d{3}\\)`'), + + block.paragraph('You can also use `|` to separate different options: `\\d{3}|\\w{3}`'), + ]), + + block.example('CreditCard', { + title: 'Credit card', + description: 'You can easily define credit card mask with regex', + }), + + block.example('Date', { + title: 'Date mask', + description: 'Date mask is a predefined mask that allows you to format the date input.', + }), + + block.example('Numeral', { + title: 'Numeral mask', + description: 'Numeral mask is a predefined mask that allows you to format the number input. You can decide if decimal is allowed and how many decimal places are allowed.', + }), + + block.example('Phone', { + title: 'Phone mask', + description: 'There is no predefined phone mask. You can create your own mask using regex syntax. This is an example for Ukrainian phone number in internationl', + }), + + block.paragraph('You can also create regex masks for any other phone format and use format functions based on user input. You can also write format function in plain JS and we will handle the rest for you.'), + + block.example('PhoneExtended') + ] +}) diff --git a/packages/docs/page-config/navigationRoutes.ts b/packages/docs/page-config/navigationRoutes.ts index 4a5a21231c..e59c7e72c8 100644 --- a/packages/docs/page-config/navigationRoutes.ts +++ b/packages/docs/page-config/navigationRoutes.ts @@ -131,6 +131,20 @@ export const navigationRoutes: NavigationRoute[] = [ }, ], }, + { + name: 'composables', + displayName: 'Composables', + disabled: true, + children: [ + { + name: 'input-mask', + displayName: 'Input Mask', + meta: { + badge: navigationBadge.new('1.10.0'), + }, + } + ] + }, { name: "ui-elements", displayName: "UI Elements", diff --git a/packages/ui/src/composables/index.ts b/packages/ui/src/composables/index.ts index 9f92c7a5d8..b8566773be 100644 --- a/packages/ui/src/composables/index.ts +++ b/packages/ui/src/composables/index.ts @@ -75,4 +75,5 @@ export * from './useElementTextColor' export * from './useElementBackground' export * from './useImmediateFocus' export * from './useNumericProp' +export * from './useInputMask' export * from './useElementRect' diff --git a/packages/ui/src/composables/useInputMask/cursor.ts b/packages/ui/src/composables/useInputMask/cursor.ts new file mode 100644 index 0000000000..5084d903e1 --- /dev/null +++ b/packages/ui/src/composables/useInputMask/cursor.ts @@ -0,0 +1,97 @@ +import { MaskToken } from './mask' + +export enum CursorPosition { + BeforeChar = -1, + Any = 0, + AfterChar = 1 +} + +export class Cursor extends Number { + constructor (public position: number, private tokens: Token[], private reversed: boolean = false) { + super(position) + } + + private move (direction: -1 | 1, amount: number, cursorPosition = CursorPosition.Any) { + if (this.tokens.every((t) => t.static)) { + if (direction === 1) { + this.position = this.tokens.length + return this.position + } else { + this.position = 0 + return this.position + } + } + + for (let i = this.position; i <= this.tokens.length && i >= -1; i += direction) { + const current = this.tokens[i] + const next = this.tokens[i + direction] + const prev = this.tokens[i - direction] + + if (amount < 0) { + this.position = i + return this.position + } + + if (!current?.static) { + amount-- + } + + if (cursorPosition <= CursorPosition.Any) { + if (direction === -1 && !next?.static && current?.static) { + amount-- + } + if (direction === 1 && !prev?.static && current?.static) { + amount-- + } + } + + if (cursorPosition >= CursorPosition.Any) { + if (direction === 1 && !prev?.static && current === undefined) { + amount-- + } else if (direction === 1 && current === undefined && next?.static) { + amount-- + } else if (direction === 1 && current === undefined && next === undefined) { + amount-- + } + } + + if (amount < 0) { + this.position = i + return this.position + } + } + + return this.position + } + + moveBack (amount: number, cursorPosition = CursorPosition.Any) { + return this.move(-1, amount, cursorPosition) + } + + moveForward (amount: number, cursorPosition = CursorPosition.Any) { + return this.move(1, amount, cursorPosition) + } + + updateTokens (newTokens: Token[], fromEnd: boolean = false) { + if (fromEnd) { + // When reversed, we need to update position from the end + this.position = this.tokens.length - this.position + this.tokens = newTokens + this.position = this.tokens.length - this.position + } else { + this.tokens = newTokens + } + } + + valueOf () { + if (this.position < 0) { + return 0 + } + + if (this.position > this.tokens.length) { + return this.tokens.length + } + + return this.position + } +} diff --git a/packages/ui/src/composables/useInputMask/index.ts b/packages/ui/src/composables/useInputMask/index.ts new file mode 100644 index 0000000000..e4488a9432 --- /dev/null +++ b/packages/ui/src/composables/useInputMask/index.ts @@ -0,0 +1,4 @@ +export { useInputMask } from './useInputMask' +export { createMaskFromRegex, compareWithMask } from './masks/regex' +export { createNumeralMask } from './masks/numeral' +export { createMaskDate } from './masks/date' diff --git a/packages/ui/src/composables/useInputMask/mask.ts b/packages/ui/src/composables/useInputMask/mask.ts new file mode 100644 index 0000000000..7a327f308a --- /dev/null +++ b/packages/ui/src/composables/useInputMask/mask.ts @@ -0,0 +1,16 @@ +import { Cursor } from './cursor' + +export type MaskToken = { + static: boolean, +} + +export type Mask = { + format: (text: string) => { + text: string, + tokens: Token[] + data?: Data, + }, + handleCursor: (selectionStart: Cursor, selectionEnd: Cursor, oldTokens: Token[], newTokens: Token[], text: string, data?: Data) => any, + unformat: (text: string, tokens: Token[]) => string, + reverse: boolean +} diff --git a/packages/ui/src/composables/useInputMask/masks/date.ts b/packages/ui/src/composables/useInputMask/masks/date.ts new file mode 100644 index 0000000000..36fd091a03 --- /dev/null +++ b/packages/ui/src/composables/useInputMask/masks/date.ts @@ -0,0 +1,138 @@ +import { CursorPosition } from '../cursor' +import { Mask, MaskToken } from '../mask' + +type MaskTokenDate = MaskToken & { + expect: 'm' | 'd' | 'y' | string, +} + +const parseTokens = (format: string) => { + return format.split('').map((char) => { + if (char === 'm' || char === 'd' || char === 'y') { + return { static: false, expect: char } + } + + return { static: true, expect: char } + }) +} + +type MinorToken = { value: string, expect: string, static: boolean } +type MajorToken = { value: string, expect: string, tree: MinorToken[] } + +const getMaxDays = (year: number, month: number) => { + if (month === 2) { + return year % 4 === 0 ? 29 : 28 + } + + if ([4, 6, 9, 11].includes(month)) { + return 30 + } + + return 31 +} + +export const createMaskDate = (format: string = 'yyyy/mm/dd'): Mask => { + const tokens = parseTokens(format) + + return { + format: (text: string) => { + const minorTokens = [] as MinorToken[] + let additionalTokens = 0 + let valueOffset = 0 + let tokenOffset = 0 + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[tokenOffset] + + if (token.static) { + minorTokens.push({ value: token.expect, expect: token.expect, static: true }) + tokenOffset++ + + if (token.expect === text[i]) { + valueOffset++ + } else { + additionalTokens++ + } + continue + } + + if (text[valueOffset] === undefined) { + break + } + + if (!/\d/.test(text[valueOffset])) { + valueOffset++ + continue + } + + minorTokens.push({ value: text[valueOffset], expect: token.expect, static: false }) + valueOffset++ + tokenOffset++ + } + + const majorTokens = minorTokens.reduce((acc, p, index) => { + if (acc[acc.length - 1]?.expect === p.expect) { + acc[acc.length - 1].value += p.value + acc[acc.length - 1].tree.push(p) + return acc + } + + acc.push({ + value: p.value, + expect: p.expect, + tree: [p], + }) + + return acc + }, [] as MajorToken[]) + + const year = majorTokens.find((p) => p.expect === 'y') + const month = majorTokens.find((p) => p.expect === 'm') + + majorTokens.forEach((p) => { + if (p.expect === 'm') { + const num = parseInt(p.value) + + if (num > 12) { + p.value = '12' + } + + if (num > 1 && num < 10) { + p.value = '0' + num + additionalTokens += 1 + } + } + + if (p.expect === 'd') { + const num = parseInt(p.value) + + const maxDays = getMaxDays(Number(year?.value), Number(month?.value)) + + if (num > maxDays) { + p.value = maxDays.toString() + } + + if (num > 3 && num < 10) { + p.value = '0' + num + additionalTokens += 1 + } + } + }) + + return { + text: majorTokens.reduce((acc, p) => acc + p.value, ''), + tokens: tokens, + data: additionalTokens, + } + }, + handleCursor (cursorStart, cursorEnd, tokens, newTokens, data, additionalTokens = 0) { + cursorStart.updateTokens(newTokens) + cursorEnd.updateTokens(newTokens) + cursorStart.moveForward(data.length + additionalTokens, CursorPosition.Any) + cursorEnd.position = cursorStart.position + }, + unformat: (text: string, tokens: MaskTokenDate[]) => { + return text.replace(/\//g, '') + }, + reverse: false, + } +} diff --git a/packages/ui/src/composables/useInputMask/masks/numeral.ts b/packages/ui/src/composables/useInputMask/masks/numeral.ts new file mode 100644 index 0000000000..816f75cada --- /dev/null +++ b/packages/ui/src/composables/useInputMask/masks/numeral.ts @@ -0,0 +1,49 @@ +import { Mask, MaskToken } from '../mask' +import { createMaskFromRegex, RegexToken } from './regex' + +const DELIMITER = ' ' +const DECIMAL = '.' + +type NumeralToken = RegexToken & { isDecimal?: boolean} + +export const createNumeralMask = (): Mask => { + const intMask = createMaskFromRegex(/(\d{3} )*(\d{3})/, { reverse: true }) + const decimalMask = createMaskFromRegex(/(\d{3} )*(\d{3})/, { reverse: false }) + + return { + format: (text: string) => { + const hasDecimal = text.includes(DECIMAL) + + if (!hasDecimal) { + return intMask.format(text) + } + + const [int = '', decimal = '', ...rest] = text.split(DECIMAL) + + const intResult = intMask.format(int) + const decimalResult = decimalMask.format(decimal + rest.join('')) + + return { + text: intResult.text + DECIMAL + decimalResult.text, + tokens: [...intResult.tokens, { type: 'char', static: false, expect: DECIMAL, isDecimal: true }, ...decimalResult.tokens] as NumeralToken[], + } + }, + handleCursor (selectionStart, selectionEnd, oldTokens, newTokens, data) { + const decimalIndex = newTokens.findIndex((token) => token.isDecimal) + + if (decimalIndex === -1) { + return intMask.handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, data) + } + + if (selectionStart.position < decimalIndex) { + intMask.handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, data) + } else { + decimalMask.handleCursor(selectionStart, selectionEnd, oldTokens, newTokens, data) + } + }, + unformat: (text: string, tokens: MaskToken[]) => { + return parseFloat(text.replace(/ /g, '')).toString() + }, + reverse: false, + } +} diff --git a/packages/ui/src/composables/useInputMask/masks/parser.ts b/packages/ui/src/composables/useInputMask/masks/parser.ts new file mode 100644 index 0000000000..af5e21ec0d --- /dev/null +++ b/packages/ui/src/composables/useInputMask/masks/parser.ts @@ -0,0 +1,228 @@ +interface TokenBase { + type: string + expect: string +} + +interface TokenChar extends TokenBase { + type: 'char' +} + +interface TokenRegex extends TokenBase { + type: 'regex' +} + +interface TokenRepeated extends TokenBase { + type: 'repeated', + tree: Token[] + min: number, + max: number, + content: string +} + +interface TokenGroup extends TokenBase { + type: 'group', + tree: Token[] +} + +interface TokenOrRegex extends TokenBase { + type: 'or regex', + left: Token[], + right: Token[] +} + +export type Token = TokenChar | TokenRegex | TokenRepeated | TokenGroup | TokenOrRegex + +const or = (...args: RegExp[]) => new RegExp(args.map((r) => r.source).join("|"), 'g') + +const TOKEN_SPLIT_REGEX = or( + /(\{[^}]*\})/,// Token required to have limits {1, 3}, {1,}, {1} + /(\\[dws.])/, + /(^\([^)]*\)$)/, // group like (test) + /(\[[^\]]*\])/, // split by [^3]{1}, [a-z], [0-9]{1, 3} + /(?:)/ // split for each letter + ) + +/** + * Checks if the symbol contains correct (.) + * + * @example + * `(.)(.)` must be invalid - two groups + * `((.)(.))` is valid - single group with nested groups + */ +const isMaskSingleGroup = (symbol: string) => { + if (!symbol.startsWith('(' ) || !symbol.endsWith(')')) { return false } + + let groupDepth = 0 + for (let i = 0; i < symbol.length; i++) { + const char = symbol[i] + if (char === '(') { + groupDepth += 1 + } else if (char === ')') { + groupDepth -= 1 + + if (groupDepth === 0 && i !== symbol.length - 1) { + return false + } + } + } + + return groupDepth === 0 +} + +/** + * Parse raw tokens (strings). Split tokens into groups: char, (), {}, [], etc. + * + * @example + * + * `(.)(.){1,2}` -> ['(.)', '(.){1,2}'] + * `((.)|(.)){2}text(.)` -> ['((.)|(.)){2}', 'text', '(.)'] + */ +const parseRawTokens = (symbol: string) => { + let group = 0 + let groups = [] + let currentChunk = '' + + let i = 0 + + while (i < symbol.length) { + if (symbol[i] === '(' && symbol[i - 1] !== '\\') { + if (group === 0 && currentChunk.length > 0) { + groups.push(...currentChunk.split(TOKEN_SPLIT_REGEX).filter((v) => v !== '' && v !== undefined)) + currentChunk = '' + } + group += 1 + } + + currentChunk += symbol[i] + + if (symbol[i] === ')' && symbol[i - 1] !== '\\') { + group -= 1 + + if (group === 0 && currentChunk.length > 0) { + groups.push(currentChunk) + currentChunk = '' + } + } + + i++ + } + + if (currentChunk.length > 0) { + groups.push(...currentChunk.split(TOKEN_SPLIT_REGEX).filter((v) => v !== '' && v !== undefined)) + } + + return groups + .map((g) => g.replace(/^\?[:!>]/, '')) // Remove group modifiers + .filter((v) => v !== '' && v !== undefined) +} + +// TODO: Maybe add more symbols to support +const RESERVED_SLASH_SYMBOLS = ['d', 'D', 'w', 'W', 's', 'S', '.'] + +const MAX_REPEATED = 10 + +/** Build ast of tokens */ +export const parseTokens = (mask: string, directlyInGroup = false): Token[] => { + let tokens: Token[] = [] + + // Handle or in group, if single group - treat as directly in group + if (isMaskSingleGroup(mask)) { + mask = mask.slice(1, -1) // Remove brackets + directlyInGroup = true + } + + const rawTokens = parseRawTokens(mask) + + for (let i = 0; i < rawTokens.length; i++) { + const rawToken = rawTokens[i] + + if (rawToken === '\\') { + // Ignore \, it is used in pair with next token + continue + } + + if (!RESERVED_SLASH_SYMBOLS.includes(rawToken) && rawTokens[i - 1] === '\\') { + tokens.push({ type: 'char', expect: rawToken }) + continue + } + + if (rawToken === '|') { + if (directlyInGroup) { + tokens = [{ + type: 'or regex', + expect: mask, + left: [...tokens], + right: parseTokens(`(${rawTokens.slice(i + 1).join('')})`) + }] + + break + } + + const prevToken = tokens.pop()! + const nextToken = parseTokens(rawTokens[i + 1]) + + tokens.push({ + type: 'or regex', + expect: `${prevToken}|${rawTokens[i + 1]}`, + left: [prevToken], + right: nextToken + }) + + continue + } + + if (rawToken.startsWith('{') && rawToken.endsWith('}') && rawToken.length > 2) { + const [v, min, delimiter, max] = rawToken.split(/\{(\d+)(,\s?)?(\d+)?\}$/) + + const prevToken = tokens.pop()! + + tokens.push({ + type: 'repeated', + expect: prevToken.expect + rawToken, + tree: [prevToken], + min: parseInt(min), + max: max ? parseInt(max) : delimiter ? MAX_REPEATED : parseInt(min), + content: rawToken + }) + continue + } + + if (rawToken.endsWith('*')) { + const prevToken = tokens.pop()! + tokens.push({ type: 'repeated', expect: prevToken.expect + rawToken, tree: [prevToken], min: 0, max: MAX_REPEATED, content: prevToken.expect }) + continue + } + + if (rawToken.endsWith('+')) { + const prevToken = tokens.pop()! + tokens.push({ type: 'repeated', expect: prevToken.expect + rawToken, tree: [prevToken], min: 1, max: MAX_REPEATED, content: prevToken.expect }) + continue + } + + if (rawToken.endsWith('?')) { + const prevToken = tokens.pop()! + tokens.push({ type: 'repeated', expect: prevToken.expect + rawToken, tree: [prevToken], min: 0, max: 1, content: prevToken.expect }) + continue + } + + if (['$', '^'].includes(rawToken)) { + // Ignore start and end of the string - they're not important for masking + continue + } + + if (rawToken.startsWith('(') && rawToken.endsWith(')')) { + const tree = parseTokens(rawToken.slice(1, -1), true) + tokens.push({ type: 'group', expect: rawToken, tree }) + continue + } + + if (rawToken.length === 1 && rawToken !== '.') { + tokens.push({ type: 'char', expect: rawToken }) + continue + } + + tokens.push({ type: 'regex', expect: rawToken }) + } + + return tokens +} diff --git a/packages/ui/src/composables/useInputMask/masks/regex.ts b/packages/ui/src/composables/useInputMask/masks/regex.ts new file mode 100644 index 0000000000..90dcfc027b --- /dev/null +++ b/packages/ui/src/composables/useInputMask/masks/regex.ts @@ -0,0 +1,294 @@ +import { CursorPosition } from '../cursor' +import { type Mask } from '../mask' +import { Token, parseTokens } from './parser' + +export type RegexToken = { + /** + * Char means it is single char and we can compare input value using simple `===` + * Regex means we need to use regex to compare input value (e.g. `\d`, `[a-z]`) + */ + type: 'char' | 'regex', + /** + * Expected character or regex source + */ + expect: string, + /** + * Static means users forced to input this char, meaning masked input can suggest this char + */ + static: boolean, + + /** + * Dynamic means this char is not forced and can be skipped + */ + dynamic: boolean +} + +type PossibleResult = RegexToken[] + +export const normalizeTokens = (tokens: Token[], dynamic = false) => { + let possibleResults: PossibleResult[] = [[]] + + for (const token of tokens) { + if (token.type === 'group') { + const newResults: PossibleResult[] = [] + possibleResults.forEach((result) => { + normalizeTokens(token.tree, dynamic).forEach((result2) => { + newResults.push([...result, ...result2]) + }) + }) + possibleResults = newResults + } + + if (token.type === 'char' || token.type === 'regex') { + const newResults: PossibleResult[] = [] + possibleResults.forEach((result) => { + newResults.push([...result, { + type: token.type, + expect: token.expect, + static: token.type === 'char' && (!dynamic || result.length > 0), + dynamic: dynamic, + }]) + }) + possibleResults = newResults + } + + if (token.type === 'repeated') { + const possibleResults2: PossibleResult[] = [] + for (let i = token.min; i <= token.max && i <= 100; i++) { + const isDynamic = i !== token.min + + normalizeTokens(token.tree, isDynamic || dynamic).forEach((result) => { + const repeated = (new Array(i).fill(result)).flat() as RegexToken[] + possibleResults2.push(repeated) + }) + } + + const newResults: PossibleResult[] = [] + possibleResults.forEach((result) => { + possibleResults2.forEach((result2) => { + newResults.push([...result, ...result2]) + }) + }) + possibleResults = newResults + } + + if (token.type === 'or regex') { + const newPossibleResults: PossibleResult[] = [] + + possibleResults.forEach((existingResult) => { + normalizeTokens(token.left, true).forEach((result) => { + newPossibleResults.push([...existingResult, ...result]) + }) + + normalizeTokens(token.right, true).forEach((result) => { + newPossibleResults.push([...existingResult, ...result]) + }) + }) + + possibleResults = newPossibleResults + } + } + + return possibleResults + .reduce((acc, result) => { + if (acc.find((r) => r.length === result.length && r.every((t, i) => t.expect === result[i].expect))) { + return acc + } + + return [...acc, result] + }, [] as PossibleResult[]) +} + +export const compareWithMask = (mask: PossibleResult, value: string) => { + if (!value) { return true } + + for (let i = 0; i < mask.length; i++) { + if (value[i] === undefined) { + return true + } + if (mask[i].type === 'char' && mask[i].expect !== value?.[i]) { + return false + } + + if (mask[i].type === 'regex' && !new RegExp(mask[i].expect).test(value[i])) { + return false + } + } + + return value.length <= mask.length +} + +const compareWithToken = (token: Token, value: string) => { + if (token.type === 'char' && token.expect !== value) { + return false + } + + if (token.type === 'regex' && !new RegExp(token.expect).test(value)) { + return false + } + + return true +} + +const formatByRegexTokens = (possibleResults: PossibleResult[], value: string, reverse = false) => { + if (reverse) { + possibleResults = possibleResults.map((result) => result.slice().reverse()) + value = value.split('').reverse().join('') + } + + // TODO: Maybe optimize this? + let suggestedCharsCount = 0 + let text = '' + let valueOffset = 0 + let tokensOffset = 0 + + const maxPossibleMask = possibleResults.reduce((acc, mask) => Math.max(acc, mask.length), 0) + const foundTokens: (RegexToken)[] = [] + + while (valueOffset < value.length || tokensOffset < maxPossibleMask) { + // Filter out possible results that not match with current text + possibleResults = possibleResults + .filter((tokens) => { + return compareWithMask(tokens, text) + }) + + const possibleToken = possibleResults + .map((mask) => mask[tokensOffset]) + .filter((token) => token !== undefined) + + if (possibleToken.length === 0) { + break + } + + const possibleSuggestions = possibleToken.filter((token) => token.type === 'char') + + const staticCharts = possibleToken.filter((token) => token.static) + + const isOnePossibleStaticChar = staticCharts.reduce((acc, char) => { + if (acc === null) { + return char + } + + if (acc.expect !== char.expect) { + return null + } + + return acc + }, null as RegexToken | null) + + if (possibleSuggestions.length > 0) { + const suggestedChar = possibleSuggestions[0]?.expect ?? '' + let canBeSuggested = possibleSuggestions.every((token) => token.expect === suggestedChar) && value[valueOffset]?.length > 0 + + const onlyStaticLeft = possibleResults.length === 1 && possibleResults[0].slice(tokensOffset).every((token) => token.static) + + if (possibleSuggestions[0].dynamic) { + canBeSuggested = canBeSuggested && value[valueOffset]?.length > 0 + } + + if (isOnePossibleStaticChar && value[valueOffset]?.length > 0) { + canBeSuggested = value[valueOffset] !== isOnePossibleStaticChar.expect + } + + if (possibleToken.some((token) => compareWithToken(token, value[valueOffset]))) { + canBeSuggested = false + } + + if (onlyStaticLeft) { + canBeSuggested = true + } + + if (canBeSuggested) { + if (suggestedChar !== value[valueOffset]) { + text += suggestedChar + foundTokens.push(possibleSuggestions[0]) + tokensOffset += 1 + suggestedCharsCount += 1 + continue + } + } + } + + if (valueOffset >= value.length) { + break + } + + const charCorrectTokens = possibleToken.filter((token) => { + if (token.type === 'char') { + return token.expect === value[valueOffset] + } + + if (token.type === 'regex') { + return new RegExp(token.expect).test(value[valueOffset]) + } + + return false + }) + + if (value[valueOffset] !== undefined) { + if (charCorrectTokens.length > 0) { + text += value[valueOffset] + foundTokens.push(charCorrectTokens[0]) + tokensOffset++ + } + } + + valueOffset++ + } + + if (reverse) { + return { + text: text.split('').reverse().join(''), + tokens: foundTokens.reverse(), + data: suggestedCharsCount, + } + } + + return { + text, + tokens: foundTokens, + data: suggestedCharsCount, + } +} + +const unformat = (text: string, tokens: RegexToken[]) => { + const value = text + + if (!value) { return '' } + + return tokens.reduce((acc, token, i) => { + if (token.static) { + return acc + } + + if (compareWithToken(token, value[i]) && value[i] !== undefined) { + return acc + value[i] + } + + return acc + }, '') +} + +export const createMaskFromRegex = (regex: RegExp, options = { reverse: false }): Mask => { + const tokens = parseTokens(regex.source) + const possibleResults = normalizeTokens(tokens) + + return { + format: (text: string) => { + return formatByRegexTokens(possibleResults, text, options.reverse) + }, + handleCursor (cursorStart, cursorEnd, oldTokens, newTokens, data, suggestedCount = 0) { + cursorStart.updateTokens(newTokens, options.reverse) + cursorEnd.updateTokens(newTokens, options.reverse) + + if (!options.reverse) { + cursorStart.moveForward(data.length, CursorPosition.AfterChar) + cursorEnd.position = cursorStart.position + } else { + cursorStart.position = cursorEnd.position + } + }, + unformat, + reverse: options.reverse, + } +} diff --git a/packages/ui/src/composables/useInputMask/tests/PossibleTokens.vue b/packages/ui/src/composables/useInputMask/tests/PossibleTokens.vue new file mode 100644 index 0000000000..4841f89b29 --- /dev/null +++ b/packages/ui/src/composables/useInputMask/tests/PossibleTokens.vue @@ -0,0 +1,121 @@ + + + diff --git a/packages/ui/src/composables/useInputMask/tests/Tokens.vue b/packages/ui/src/composables/useInputMask/tests/Tokens.vue new file mode 100644 index 0000000000..404e523126 --- /dev/null +++ b/packages/ui/src/composables/useInputMask/tests/Tokens.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/ui/src/composables/useInputMask/useInputMask.stories.ts b/packages/ui/src/composables/useInputMask/useInputMask.stories.ts new file mode 100644 index 0000000000..60ca5bc42c --- /dev/null +++ b/packages/ui/src/composables/useInputMask/useInputMask.stories.ts @@ -0,0 +1,239 @@ +import { computed, ref } from 'vue' +import { defineStory } from '../../../.storybook/types' +import { useInputMask } from './useInputMask' +import TokensRenderer from './tests/Tokens.vue' +import PossibleTokens from './tests/PossibleTokens.vue' +import { createMaskFromRegex } from './masks/regex' +import { createMaskDate } from './masks/date' +import { createNumeralMask } from './masks/numeral' +import { parseTokens } from './masks/parser' + +export default { + title: 'composables/useInputMask', + tags: ['autodocs'], +} + +export const Default = defineStory({ + story: () => ({ + components: { TokensRenderer, PossibleTokens }, + setup () { + const reverse = ref(false) + const value = ref('3809312345678') + const input = ref() + const regex = ref(/(\d{1,3}( \d{3}){1,2},?\d{1,3}|\d{3}|\d{1,3}( \d{3}){1,2})/.source) + const mask = computed(() => { + try { + if (!regex.value) { return /./ } + + return new RegExp(regex.value) + } catch { + return /./ + } + }) + + const tokens = computed(() => parseTokens(mask.value.source)) + const regexMask = computed(() => createMaskFromRegex(mask.value, { reverse: reverse.value })) + + const text = ref('') + + const { masked, unmasked } = useInputMask(regexMask, input) + + return { value, regex, input, masked, unmasked, text, tokens, reverse } + }, + template: ` +

Regex

+ + Reverse ({{ reverse}}) +

Text

+ + +

Tokens

+ + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const Phone = defineStory({ + story: () => ({ + setup () { + const value = ref('3809312345678') + const input = ref() + + const phoneMask = createMaskFromRegex(/\+(\d{1,3}) \(\d{2,3}\) \d\d\d-\d\d-\d\d/) + + const { masked, unmasked } = useInputMask(phoneMask, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const CreditCard = defineStory({ + story: () => ({ + setup () { + const value = ref('1111222233334444') + const input = ref() + + const creditCardMask = createMaskFromRegex(/(\d{4} ){3}\d{4}/) + const { masked, unmasked } = useInputMask(creditCardMask, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const WithOptionalGroup = defineStory({ + story: () => ({ + setup () { + const value = ref('') + const input = ref() + + const { masked, unmasked } = useInputMask(createMaskFromRegex(/(\+(\d{1,3}) )?\(\d{2,3}\) (\d){3}-\d\d-\d\d/), input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const WithOrGroup = defineStory({ + story: () => ({ + setup () { + const value = ref('') + const input = ref() + + const { masked, unmasked } = useInputMask(createMaskFromRegex(/\+(7 \(\d{3}\)|380 \(\d{2}\)) (\d){3}-\d\d-\d\d/), input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const Ipv6 = defineStory({ + story: () => ({ + setup () { + const value = ref('1234567890123456') + const input = ref() + + const ipv6Regex = createMaskFromRegex(/(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))/) + + const { masked, unmasked } = useInputMask(ipv6Regex, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const Date = defineStory({ + story: () => ({ + setup () { + const value = ref('1234567890123456') + const input = ref() + + const dateMask = createMaskDate() + + const { masked, unmasked } = useInputMask(dateMask, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const Numeral = defineStory({ + story: () => ({ + setup () { + const value = ref('123456') + const input = ref() + + const numeralRegex = createMaskFromRegex(/(\d{3} - )*(\d{3})/) + const { masked, unmasked } = useInputMask(numeralRegex, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const ReversedNumeral = defineStory({ + story: () => ({ + setup () { + const value = ref('123456') + const input = ref() + + const numeralRegex = createMaskFromRegex(/(\d{3} - )*(\d{3})/, { reverse: true }) + const { masked, unmasked } = useInputMask(numeralRegex, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) + +export const NumeralWithDecimal = defineStory({ + story: () => ({ + setup () { + const value = ref('123456') + const input = ref() + + const numeralRegex = createNumeralMask() + const { masked, unmasked } = useInputMask(numeralRegex, input) + + return { value, input, masked, unmasked } + }, + template: ` + + +

masked: {{ masked }}

+

unmasked: {{ unmasked }}

+ `, + }), +}) diff --git a/packages/ui/src/composables/useInputMask/useInputMask.ts b/packages/ui/src/composables/useInputMask/useInputMask.ts new file mode 100644 index 0000000000..adc62c6b91 --- /dev/null +++ b/packages/ui/src/composables/useInputMask/useInputMask.ts @@ -0,0 +1,136 @@ +import { ComponentPublicInstance, MaybeRef, Ref, computed, isRef, ref, unref, watch } from 'vue' +import { Mask, MaskToken } from './mask' +import { Cursor, CursorPosition } from './cursor' +import { unwrapEl } from '../../utils/unwrapEl' + +const extractInput = (el: HTMLElement | null | undefined | ComponentPublicInstance) => { + const htmlEl = unwrapEl(el) + + if (!htmlEl) { + return null + } + + if (htmlEl.tagName === 'INPUT') { + return htmlEl as HTMLInputElement + } + + return htmlEl.querySelector('input') +} + +export const useInputMask = (mask: MaybeRef>, el: Ref) => { + const inputText = ref('') + + const formatted = ref({ + text: '', + tokens: [], + data: undefined, + }) as Ref<{ + text: string, + tokens: Token[], + data?: any + }> + + const input = computed(() => extractInput(el.value)) + + const setInputValue = (value: string, options?: InputEventInit) => { + input.value!.value = value + input.value!.dispatchEvent(new InputEvent('input', options)) + } + + const onBeforeInput = (e: InputEvent) => { + const { inputType } = e + const eventTarget = e.target as HTMLInputElement + + const data = e.data === null ? '' : e.data + + const currentValue = eventTarget.value + + const selectionStart = eventTarget.selectionStart ?? 0 + const selectionEnd = eventTarget.selectionEnd ?? 0 + + const cursorStart = new Cursor(selectionStart, formatted.value!.tokens) + const cursorEnd = new Cursor(selectionEnd, formatted.value!.tokens) + + // All input types: https://w3c.github.io/input-events/#interface-InputEvent-Attributes + + if (inputType === 'deleteContentBackward') { + if (+cursorStart === +cursorEnd) { + // From 1[]2 to [1]2 + cursorStart.moveBack(1, CursorPosition.AfterChar) + } + } else if (inputType === 'deleteContentForward' || inputType === 'deleteContent' || inputType === 'deleteByCut') { + if (+cursorStart === +cursorEnd) { + // From 1[]23 to 1[2]3 + cursorEnd.moveForward(1, CursorPosition.AfterChar) + } + } + + const tokens = formatted.value.tokens + inputText.value = currentValue.slice(0, +cursorStart) + data + currentValue.slice(+cursorEnd) + formatted.value = unref(mask).format(inputText.value) + + unref(mask).handleCursor(cursorStart, cursorEnd, tokens, formatted.value.tokens, data, formatted.value.data) + + setInputValue(formatted.value!.text, e) + + eventTarget.setSelectionRange(+cursorStart, +cursorEnd) + + e.preventDefault() + } + + const onKeydown = (e: KeyboardEvent) => { + const el = e.target as HTMLInputElement + + if (e.key === 'ArrowLeft') { + if (el.selectionStart === el.selectionEnd) { + const cursor = new Cursor((el.selectionStart ?? 0), formatted.value!.tokens) + cursor.moveBack(1) + el.setSelectionRange(+cursor, +cursor) + } else { + el.setSelectionRange(el.selectionStart, el.selectionStart) + } + + e.preventDefault() + } + + if (e.key === 'ArrowRight') { + if (el.selectionStart === el.selectionEnd) { + const cursor = new Cursor((el.selectionEnd ?? 0), formatted.value!.tokens) + cursor.moveForward(1) + el.setSelectionRange(+cursor, +cursor) + } else { + el.setSelectionRange(el.selectionEnd, el.selectionEnd) + } + e.preventDefault() + } + } + + watch(input, (newValue, oldValue) => { + if (newValue) { + const input = extractInput(newValue) + + formatted.value = unref(mask).format(newValue.value) + const cursor = new Cursor((newValue.selectionEnd ?? 0), formatted.value!.tokens) + cursor.moveForward(1) + setInputValue(formatted.value.text) + newValue.setSelectionRange(+cursor, +cursor) + + newValue.addEventListener('beforeinput', onBeforeInput) + newValue.addEventListener('keydown', onKeydown) + } + if (oldValue) { + oldValue.removeEventListener('beforeinput', onBeforeInput) + oldValue.removeEventListener('keydown', onKeydown) + } + }, { immediate: true }) + + const unmasked = computed(() => { + return unref(mask).unformat(formatted.value.text, formatted.value.tokens) + }) + + return { + inputText: formatted, + masked: computed(() => formatted.value?.text ?? ''), + unmasked, + } +} diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts index 99d87ec70b..44c1ed327f 100644 --- a/packages/ui/src/main.ts +++ b/packages/ui/src/main.ts @@ -6,6 +6,11 @@ export { useIcon as useIcons, type ValidationRule, useForm, + useInputMask, + createMaskFromRegex, + createNumeralMask, + createMaskDate, + compareWithMask, } from './composables' export * from './services/vue-plugin' From eaee0eebab740fe7c03144ad705b2ae05770b5b8 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Wed, 3 Jul 2024 11:10:50 +0300 Subject: [PATCH 16/88] docs(sticky-scrollbar): add example with data table --- .../examples/WithDataTable.vue | 66 +++++++++++++++++++ .../ui-elements/sticky-scrollbar/index.ts | 19 +++++- .../va-sticky-scrollbar/StickyScrollbar.vue | 2 +- packages/ui/src/composables/useElementRect.ts | 6 ++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/docs/page-config/ui-elements/sticky-scrollbar/examples/WithDataTable.vue diff --git a/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/WithDataTable.vue b/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/WithDataTable.vue new file mode 100644 index 0000000000..6c9e8fc68a --- /dev/null +++ b/packages/docs/page-config/ui-elements/sticky-scrollbar/examples/WithDataTable.vue @@ -0,0 +1,66 @@ + + + diff --git a/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts b/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts index b891fa6374..f7a47ff86e 100644 --- a/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts +++ b/packages/docs/page-config/ui-elements/sticky-scrollbar/index.ts @@ -3,6 +3,23 @@ export default definePageConfig({ block.title("StickyScrollbar"), block.paragraph("This component adds floating scrollbar to element if it doesn't fit the screen. In case if you have large scroll container and want your scrollbar to be always visible, you need to use this component."), - block.example('Horizontal') + block.example('Horizontal'), + block.example('WithDataTable', { + title: 'With DataTable', + description: '`VaStickyScrollbar` is designed to be used with scroll large containers like `VaDataTable`. Because having large scroll container is not common, we don\'t include sticky scroll by default.', + }), + + block.api('VaStickyScrollbar', { + props: { + direction: 'Defines the direction of the scrollbar. If you need both horizontal and vertical scrollbars, you need to use two components', + el: 'Where to attach the scrollbar. By default it attaches to the parent element of the component', + } + }, { + props: { + direction: { + types: "'horizontal' | 'vertical'", + } + } + }) ], }); diff --git a/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue index 1165ccf38c..ab0b58cae2 100644 --- a/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue +++ b/packages/ui/src/components/va-sticky-scrollbar/StickyScrollbar.vue @@ -5,7 +5,7 @@ import { useEvent, useElementRect, useResizeObserver } from '../../composables' const props = withDefaults(defineProps<{ el?: HTMLElement | null - direction: 'vertical' | 'horizontal' + direction?: 'vertical' | 'horizontal' }>(), { direction: 'horizontal', }) diff --git a/packages/ui/src/composables/useElementRect.ts b/packages/ui/src/composables/useElementRect.ts index 24b3db0513..71b9d58a4b 100644 --- a/packages/ui/src/composables/useElementRect.ts +++ b/packages/ui/src/composables/useElementRect.ts @@ -26,6 +26,9 @@ export const useElementRect = (element: Ref) => { element.value && resizeObserver.observe(element.value) element.value && mutationObserver.observe(element.value, { attributes: true, childList: true, subtree: true }) + window.addEventListener('resize', updateRect) + window.addEventListener('scroll', updateRect) + updateRect() }) @@ -33,6 +36,9 @@ export const useElementRect = (element: Ref) => { resizeObserver?.disconnect() mutationObserver?.disconnect() + window.removeEventListener('resize', updateRect) + window.removeEventListener('scroll', updateRect) + resizeObserver = undefined mutationObserver = undefined }) From a6a80f0c282be6dcaf0638573f13413c7f8678b2 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Wed, 3 Jul 2024 12:26:43 +0300 Subject: [PATCH 17/88] feat(sticky-headers): init sticky table headers composable --- .../sticky-table-headers/examples/Basic.vue | 33 +++++ .../examples/DataTable.vue | 67 ++++++++++ .../composables/sticky-table-headers/index.ts | 21 +++ packages/docs/page-config/navigationRoutes.ts | 7 + packages/ui/src/composables/index.ts | 1 + .../useStickyTableHeaders.ts | 123 ++++++++++++++++++ packages/ui/src/main.ts | 1 + 7 files changed, 253 insertions(+) create mode 100644 packages/docs/page-config/composables/sticky-table-headers/examples/Basic.vue create mode 100644 packages/docs/page-config/composables/sticky-table-headers/examples/DataTable.vue create mode 100644 packages/docs/page-config/composables/sticky-table-headers/index.ts create mode 100644 packages/ui/src/composables/useStickyTableHeaders/useStickyTableHeaders.ts diff --git a/packages/docs/page-config/composables/sticky-table-headers/examples/Basic.vue b/packages/docs/page-config/composables/sticky-table-headers/examples/Basic.vue new file mode 100644 index 0000000000..ab57a04085 --- /dev/null +++ b/packages/docs/page-config/composables/sticky-table-headers/examples/Basic.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/packages/docs/page-config/composables/sticky-table-headers/examples/DataTable.vue b/packages/docs/page-config/composables/sticky-table-headers/examples/DataTable.vue new file mode 100644 index 0000000000..878404ade3 --- /dev/null +++ b/packages/docs/page-config/composables/sticky-table-headers/examples/DataTable.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/packages/docs/page-config/composables/sticky-table-headers/index.ts b/packages/docs/page-config/composables/sticky-table-headers/index.ts new file mode 100644 index 0000000000..1b3a399501 --- /dev/null +++ b/packages/docs/page-config/composables/sticky-table-headers/index.ts @@ -0,0 +1,21 @@ +export default definePageConfig({ + blocks: [ + block.title('Sticky Table Headers'), + block.paragraph('Sticky table headers are used to make the table header always visible when you scroll the table content.'), + block.paragraph('This composables automatically takes background color of the table header and applies it to the sticky header.'), + + block.paragraph('In case if you have floating header, as we have on our documentation website, you can provide offset param.'), + + block.subtitle('Examples'), + + block.example('Basic', { + title: 'Basic example', + description: 'Basic example of sticky table headers on a simple HTML table.', + }), + + block.example('DataTable', { + title: 'VaDataTable example', + description: 'Example of sticky table headers with VaDataTable.', + }), + ] +}) diff --git a/packages/docs/page-config/navigationRoutes.ts b/packages/docs/page-config/navigationRoutes.ts index e59c7e72c8..df665d20e5 100644 --- a/packages/docs/page-config/navigationRoutes.ts +++ b/packages/docs/page-config/navigationRoutes.ts @@ -142,6 +142,13 @@ export const navigationRoutes: NavigationRoute[] = [ meta: { badge: navigationBadge.new('1.10.0'), }, + }, + { + name: 'sticky-table-headers', + displayName: 'Sticky Table Headers', + meta: { + badge: navigationBadge.new('1.10.0'), + }, } ] }, diff --git a/packages/ui/src/composables/index.ts b/packages/ui/src/composables/index.ts index b8566773be..362781eaa2 100644 --- a/packages/ui/src/composables/index.ts +++ b/packages/ui/src/composables/index.ts @@ -77,3 +77,4 @@ export * from './useImmediateFocus' export * from './useNumericProp' export * from './useInputMask' export * from './useElementRect' +export * from './useStickyTableHeaders/useStickyTableHeaders' diff --git a/packages/ui/src/composables/useStickyTableHeaders/useStickyTableHeaders.ts b/packages/ui/src/composables/useStickyTableHeaders/useStickyTableHeaders.ts new file mode 100644 index 0000000000..88d917e29d --- /dev/null +++ b/packages/ui/src/composables/useStickyTableHeaders/useStickyTableHeaders.ts @@ -0,0 +1,123 @@ +import { Ref, computed, watchEffect, type ComponentPublicInstance } from 'vue' +import { useEvent } from '../useEvent' +import { useElementBackground } from '../useElementBackground' + +const syncTh = (thead1: HTMLElement, thead2: HTMLElement) => { + const ths1 = thead1.querySelectorAll('th') + const ths2 = thead2.querySelectorAll('th') + + ths1.forEach((th, index) => { + const th2 = ths2[index] + + th2.style.width = `${th.getBoundingClientRect().width}px` + }) +} + +const recursiveGetOffset = (el: HTMLElement, offset = 0): number => { + if (!el) { return offset } + + return recursiveGetOffset(el.offsetParent as HTMLElement, offset + el.offsetTop) +} + +/** + * Creates virtual table headers, that are sticky when you scroll the table with position fixed. + * When standard table headers are visible, the virtual headers are hidden. + */ +export const useStickyTableHeaders = (tableEl: Ref, offset = 0) => { + let mutationObserver: MutationObserver | null = null + let headClone: HTMLElement | null = null + let head: HTMLElement | null = null + let table: HTMLTableElement | null = null + + let headOffset = 0 + let hasTransform = false + let tableHeight = 0 + let headHeight = 0 + + const htmlTable = computed(() => { + if (!tableEl.value) { return null } + + const el = '$el' in tableEl.value ? tableEl.value.$el as HTMLElement : tableEl.value + + return el.tagName === 'TABLE' ? el as HTMLTableElement : el.querySelector('table') as HTMLTableElement + }) + + const htmlTableHead = computed(() => { + if (!htmlTable.value) { return null } + + return htmlTable.value.querySelector('thead') as HTMLElement + }) + + const bg = useElementBackground(htmlTableHead) + + watchEffect(() => { + if (!tableEl.value) { return } + + const rootEl = htmlTable.value + + if (!rootEl) { return } + + table = htmlTable.value + head = htmlTableHead.value + + if (!head) { return } + + table.style.position = 'relative' + + headClone = head.cloneNode(true) as HTMLElement + + headClone.style.position = 'fixed' + headClone.style.top = '0px' + headClone.style.width = `${table.clientWidth}px` + headClone.style.zIndex = '1' + headClone.style.backgroundColor = bg.value + headClone.style.display = 'none' + + table.appendChild(headClone) + + syncTh(head, headClone) + headOffset = recursiveGetOffset(head) + hasTransform = window.getComputedStyle(table).transform !== 'none' + tableHeight = table.clientHeight + headHeight = head.clientHeight + + mutationObserver = new MutationObserver(() => { + if (!headClone) { return } + if (!head) { return } + if (!table) { return } + + headClone.style.width = `${table.clientWidth}px` + syncTh(head, headClone) + headOffset = recursiveGetOffset(head) + tableHeight = table.clientHeight + headHeight = head.clientHeight + }) + + mutationObserver.observe(table, { + childList: true, + subtree: true, + attributes: true, + }) + }) + + // TODO: Maybe optimize it with IntersectionObserver + useEvent('scroll', () => { + if (!headClone) { return } + + const y = window.scrollY + + if (y > headOffset - offset && y < headOffset + tableHeight - offset - headHeight) { + headClone.style.display = 'block' + + // Somehow transform prevents fixed positioning. + if (hasTransform) { + headClone.style.top = `${y - headOffset + offset - 2}px` + } else { + headClone.style.top = `${offset}px` + } + } else { + headClone.style.top = '0px' + headClone.style.display = 'none' + } + }) +} diff --git a/packages/ui/src/main.ts b/packages/ui/src/main.ts index 44c1ed327f..cd07ec9f35 100644 --- a/packages/ui/src/main.ts +++ b/packages/ui/src/main.ts @@ -11,6 +11,7 @@ export { createNumeralMask, createMaskDate, compareWithMask, + useStickyTableHeaders, } from './composables' export * from './services/vue-plugin' From 654133515f772af3f12c4facf250aac349629ddc Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Wed, 3 Jul 2024 13:08:50 +0300 Subject: [PATCH 18/88] fix: remove empty changelogs --- .../docs/modules/page-config/blocks/api/index.vue | 2 +- .../docs/page-config/introduction/roadmap/index.ts | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/docs/modules/page-config/blocks/api/index.vue b/packages/docs/modules/page-config/blocks/api/index.vue index ca9889ff1c..21353b95ae 100644 --- a/packages/docs/modules/page-config/blocks/api/index.vue +++ b/packages/docs/modules/page-config/blocks/api/index.vue @@ -231,7 +231,7 @@ const changeLogValue = ref(true) diff --git a/packages/docs/page-config/introduction/roadmap/index.ts b/packages/docs/page-config/introduction/roadmap/index.ts index 80448be7ad..d2ce659743 100644 --- a/packages/docs/page-config/introduction/roadmap/index.ts +++ b/packages/docs/page-config/introduction/roadmap/index.ts @@ -4,17 +4,14 @@ export default definePageConfig({ blocks: [ block.title("Vuestic UI Roadmap"), block.paragraph("Our team is constantly improving the existing code base and adding new features that help building applications even easier."), - block.subtitle("In development"), - block.paragraph("The following features are currently being in development:"), + // block.subtitle("In development"), + // block.paragraph("The following features are currently being in development:"), + + block.subtitle("Released"), block.headline("v1.8"), block.component("Roadmap", { roadmap: [ - { - title: "Color Picker", - type: "component", - image: "colorPicker", - }, { title: "Page Layout", type: "component", @@ -44,8 +41,6 @@ export default definePageConfig({ ] as RoadmapItem[], }), - block.subtitle("Released"), - block.headline("[v1.7](https://github.com/epicmaxco/vuestic-ui/milestone/27)"), block.paragraph("We focus on improving existing components and documentation pages."), block.component("Roadmap", { From 3d483b62f6a3144ae48c87cf4890ce0881dbc58e Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Wed, 3 Jul 2024 13:46:11 +0300 Subject: [PATCH 19/88] docs: add changelogs --- .../blocks/change-log/transform.ts | 20 +++++++++++++++---- .../ui/src/components/va-input/CHANGELOG.md | 4 ++++ packages/ui/src/services/color/CHANGELOG.md | 3 +++ packages/ui/src/services/i18n/CHANGELOG.md | 4 ++++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 packages/ui/src/services/color/CHANGELOG.md create mode 100644 packages/ui/src/services/i18n/CHANGELOG.md diff --git a/packages/docs/modules/page-config/blocks/change-log/transform.ts b/packages/docs/modules/page-config/blocks/change-log/transform.ts index 9e25167e92..031e239838 100644 --- a/packages/docs/modules/page-config/blocks/change-log/transform.ts +++ b/packages/docs/modules/page-config/blocks/change-log/transform.ts @@ -6,10 +6,16 @@ import { resolve, join } from 'pathe' const changelogs = () => glob(join(resolve(''), '..', 'ui/src/**/CHANGELOG.md')) const sortObjectKeys = >(obj: T) => { - return Object.keys(obj).sort().reverse().reduce((acc, key) => { - acc[key as keyof T] = obj[key] as T[keyof T] - return acc - }, {} as T) + return Object + .keys(obj) + .sort((a, b) => { + return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }) + }) + .reverse() + .reduce((acc, key) => { + acc[key as keyof T] = obj[key] as T[keyof T] + return acc + }, {} as T) } export const render = async () => { @@ -26,6 +32,8 @@ export const render = async () => { const mergedChangelog = logEntries .reduce((acc, [path, changelog]) => { const componentName = path.match(/components\/(.*)\/CHANGELOG.md/)?.[1] + const serviceName = path.match(/services\/(.*)\/CHANGELOG.md/)?.[1] + const composableName = path.match(/composables\/(.*)\/CHANGELOG.md/)?.[1] const spilledByVersion = changelog.split(/# ([0-9]*\.[0-9]*\.[0-9]*)/).filter(Boolean) @@ -37,6 +45,10 @@ export const render = async () => { if (componentName) { acc[version][componentName] = content.split('\n').filter(Boolean) + } else if (serviceName) { + acc[version][serviceName] = content.split('\n').filter(Boolean) + } else if (composableName) { + acc[version][composableName] = content.split('\n').filter(Boolean) } } return acc diff --git a/packages/ui/src/components/va-input/CHANGELOG.md b/packages/ui/src/components/va-input/CHANGELOG.md index 4560c38e47..dbc9add9ed 100644 --- a/packages/ui/src/components/va-input/CHANGELOG.md +++ b/packages/ui/src/components/va-input/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.10.0 +- Remove cleave.js and reduced bundle size +- Remove `mask` prop. Prefer useInputMask composable + # 1.9.0 - Fix style issues without vuestic/css or in web-components build - Fix empty value when model value initially set diff --git a/packages/ui/src/services/color/CHANGELOG.md b/packages/ui/src/services/color/CHANGELOG.md new file mode 100644 index 0000000000..d947ef103b --- /dev/null +++ b/packages/ui/src/services/color/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.10.0 + +- Removed colortranslator and reduced bundle size \ No newline at end of file diff --git a/packages/ui/src/services/i18n/CHANGELOG.md b/packages/ui/src/services/i18n/CHANGELOG.md new file mode 100644 index 0000000000..5c49ca7417 --- /dev/null +++ b/packages/ui/src/services/i18n/CHANGELOG.md @@ -0,0 +1,4 @@ +# 1.10.0 + +- Now i18n config uses `vuestic` key in `vue-i18n` automatically +- Custom strings can be added to i18n From 24b642c1323d7205dd38d16cd19fa88c0425cc3d Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Wed, 3 Jul 2024 19:06:21 +0300 Subject: [PATCH 20/88] docs: add more changelogs for services --- packages/ui/.eslintignore | 1 + packages/ui/src/components/va-sticky-scrollbar/CHANGELOG.md | 3 +++ packages/ui/src/services/color/CHANGELOG.md | 3 ++- packages/ui/src/services/global-config/CHANGELOG.md | 4 ++++ packages/ui/src/services/icon/CHANGELOG.md | 4 ++++ 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/components/va-sticky-scrollbar/CHANGELOG.md create mode 100644 packages/ui/src/services/global-config/CHANGELOG.md create mode 100644 packages/ui/src/services/icon/CHANGELOG.md diff --git a/packages/ui/.eslintignore b/packages/ui/.eslintignore index 4e976de89d..afe1a243d4 100644 --- a/packages/ui/.eslintignore +++ b/packages/ui/.eslintignore @@ -1 +1,2 @@ /src/services/api-docs/vue-src-no-flow/ +/src/**/*.md \ No newline at end of file diff --git a/packages/ui/src/components/va-sticky-scrollbar/CHANGELOG.md b/packages/ui/src/components/va-sticky-scrollbar/CHANGELOG.md new file mode 100644 index 0000000000..284b3724a9 --- /dev/null +++ b/packages/ui/src/components/va-sticky-scrollbar/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.10.0 + +- Init component with horizontal and vertical props \ No newline at end of file diff --git a/packages/ui/src/services/color/CHANGELOG.md b/packages/ui/src/services/color/CHANGELOG.md index d947ef103b..60548df196 100644 --- a/packages/ui/src/services/color/CHANGELOG.md +++ b/packages/ui/src/services/color/CHANGELOG.md @@ -1,3 +1,4 @@ # 1.10.0 -- Removed colortranslator and reduced bundle size \ No newline at end of file +- Removed colortranslator and reduced bundle size +- Color CSS variables now application specific if you have multiple vue apps on page \ No newline at end of file diff --git a/packages/ui/src/services/global-config/CHANGELOG.md b/packages/ui/src/services/global-config/CHANGELOG.md new file mode 100644 index 0000000000..95325db213 --- /dev/null +++ b/packages/ui/src/services/global-config/CHANGELOG.md @@ -0,0 +1,4 @@ +# 1.10.0 + +- Presets now can inherit other presets +- Now you can pass array of presets and combine two or more presets together diff --git a/packages/ui/src/services/icon/CHANGELOG.md b/packages/ui/src/services/icon/CHANGELOG.md new file mode 100644 index 0000000000..2583a5edf3 --- /dev/null +++ b/packages/ui/src/services/icon/CHANGELOG.md @@ -0,0 +1,4 @@ +# 1.10.0 + +- Added ts interface for custom icon aliases +- Now default aliases suggested in IDE when filling icon name prop \ No newline at end of file From 8a2c4bb865109450964b1849e18b9be5581f3111 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Thu, 4 Jul 2024 13:11:18 +0300 Subject: [PATCH 21/88] fix: improve docs SSR --- packages/docs/components/layout/Header.vue | 5 ++--- packages/nuxt/src/runtime/plugin.ts | 13 +++++------ packages/ui/src/services/color/CHANGELOG.md | 3 ++- .../plugin/create-color-config-plugin.ts | 2 +- .../ui/src/styles/global/css-variables.scss | 22 ------------------- 5 files changed, 11 insertions(+), 34 deletions(-) diff --git a/packages/docs/components/layout/Header.vue b/packages/docs/components/layout/Header.vue index e21c5b7795..df8f8cb083 100644 --- a/packages/docs/components/layout/Header.vue +++ b/packages/docs/components/layout/Header.vue @@ -31,8 +31,7 @@ @@ -62,7 +62,6 @@ import { useFocusable, useFocusableProps, useEvent, } from '../../composables' import type { ValidationProps } from '../../composables/useValidation' -import { useCleave, useCleaveProps } from './hooks/useCleave' import type { AnyStringPropType } from '../../utils/types/prop-type' @@ -100,7 +99,6 @@ const props = defineProps({ ...useFocusableProps, ...useValidationProps as ValidationProps, ...useClearableProps, - ...useCleaveProps, ...useComponentPresetProp, ...useStatefulProps, @@ -174,14 +172,11 @@ const { clearIconProps, } = useClearable(props, modelValue, input, computedError) -const { computedValue, onInput } = useCleave(input, props, valueComputed) - const inputListeners = createInputListeners(emit) const inputEvents = { ...inputListeners, onBlur: combineFunctions(onBlur, inputListeners.onBlur), - onInput: combineFunctions(onInput, inputListeners.onInput), } const setInputValue = (newValue: string) => { @@ -208,7 +203,7 @@ const setInputValue = (newValue: string) => { target.setSelectionRange(selectionStart, selectionEnd) } -watch(computedValue, (newValue) => { +watch(valueComputed, (newValue) => { setInputValue(String(newValue)) }, { immediate: true }) @@ -239,7 +234,7 @@ const computedInputAttributes = computed(() => (({ }) as InputHTMLAttributes)) const valueLengthComputed = computed(() => - props.counter && typeof computedValue.value === 'string' ? computedValue.value.length : undefined, + props.counter && typeof valueComputed.value === 'string' ? valueComputed.value.length : undefined, ) const onFieldClick = (e: MouseEvent) => { diff --git a/packages/ui/src/components/va-input/hooks/useCleave.ts b/packages/ui/src/components/va-input/hooks/useCleave.ts deleted file mode 100644 index 588aece99d..0000000000 --- a/packages/ui/src/components/va-input/hooks/useCleave.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { computed, onBeforeUnmount, PropType, ref, Ref, watchEffect, type ExtractPropTypes, type WritableComputedRef } from 'vue' -import Cleave from 'cleave.js' -import { type CleaveOptions } from 'cleave.js/options' - -const DEFAULT_MASK_TOKENS: Record> = { - creditCard: { - creditCard: true, - }, - date: { - date: true, - datePattern: ['d', 'm', 'Y'], - }, - time: { - time: true, - timePattern: ['h', 'm'], - timeFormat: '24', - }, - numeral: { - numeral: true, - numeralThousandsGroupStyle: 'thousand', - }, -} - -export const useCleaveProps = { - mask: { type: [String, Object] as PropType | CleaveOptions>, default: '' }, - returnRaw: { type: Boolean, default: true }, -} - -export const useCleave = ( - element: Ref, - props: ExtractPropTypes, - syncValue: WritableComputedRef, -) => { - const cleave = ref() - - const getMask = (mask: CleaveOptions | string) => { - if (typeof mask === 'string') { - return DEFAULT_MASK_TOKENS[mask] ? { ...DEFAULT_MASK_TOKENS[mask] } : null - } - return { ...mask } - } - - const destroyCleave = () => { - if (cleave.value) { cleave.value.destroy() } - } - - const mask = computed(() => getMask(props.mask)) - - const cleaveEnabled = computed(() => { - return mask.value && Object.keys(mask.value).length - }) - - watchEffect(() => { - destroyCleave() - - if (!element.value) { return } - - // Do not create cleave instance if mask is not defined - if (!cleaveEnabled.value || !mask.value) { return } - - cleave.value = new Cleave(element.value, mask.value) - - cleave.value!.properties.onValueChanged = ({ target: { rawValue, value } }) => { - if (props.returnRaw) { - syncValue.value = rawValue - } else { - syncValue.value = value - } - } - }) - - onBeforeUnmount(() => { destroyCleave() }) - - const computedValue = computed(() => { - if (cleave.value) { - if (props.returnRaw && syncValue.value === cleave.value.getRawValue()) { - return cleave.value.getFormattedValue() - } - } - - return syncValue.value ?? '' - }) - - const onInput = (event: Event) => { - const value = (event.target as HTMLInputElement).value - - if (!cleaveEnabled.value) { - syncValue.value = value - } - } - - return { - cleave, - cleaveEnabled, - computedValue, - onInput, - } -} From c53bce959d4c8cf61189688ed9ebc87f856b3c8f Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Tue, 9 Jul 2024 00:40:09 +0300 Subject: [PATCH 25/88] Feat/devtools and compiler (#4340) * raw * feat: use vite server middleware * chore: add readme * feat(compiler): add builded package * fix: correct parse self closing tags * fix(compiler): reset slots when save clicked * feat(compiler): improve devtools playground * chore: refactor to one global vuestic vite plugin * feat(compiler): add css layers * chore(compiler): add readme * chore: improve bundling * chore: improve readme and publish beta1 * fix: build --- packages/compiler/Readme.md | 34 + packages/compiler/css-layers/index.ts | 1 + packages/compiler/css-layers/plugin.ts | 29 + packages/compiler/devtools/Readme.md | 5 + .../devtools/client/build/append-style.ts | 36 + packages/compiler/devtools/client/env.d.ts | 1 + packages/compiler/devtools/client/index.ts | 1 + .../devtools/client/parser/parseHTML.ts | 68 ++ .../devtools/client/parser/parseSource.ts | 261 +++++++ .../devtools/client/parser/printHTML.ts | 122 ++++ .../devtools/client/parser/printSource.ts | 66 ++ .../compiler/devtools/client/parser/utils.ts | 13 + .../compiler/devtools/client/ui/Devtools.vue | 153 +++++ .../devtools/client/ui/components/AppTree.vue | 14 + .../client/ui/components/AppTreeItem.vue | 65 ++ .../client/ui/components/ComponentView.vue | 93 +++ .../ui/components/ComponentViewSettings.vue | 180 +++++ .../ui/components/ComponentViewSource.vue | 64 ++ .../client/ui/components/base/CodeView.vue | 22 + .../ui/components/base/DraggableWindow.vue | 115 ++++ .../client/ui/components/base/Outline.vue | 66 ++ .../client/ui/components/base/Overlay.vue | 14 + .../client/ui/components/base/SourceView.vue | 39 ++ .../ui/composables/base/useElementRect.ts | 30 + .../client/ui/composables/base/useEvent.ts | 11 + .../composables/base/useMutationObserver.ts | 18 + .../ui/composables/base/useWindowSize.ts | 20 + .../client/ui/composables/useAppTransform.ts | 79 +++ .../client/ui/composables/useAppTree.ts | 129 ++++ .../ui/composables/useHoveredElement.ts | 59 ++ .../client/ui/composables/useOutlines.ts | 22 + .../client/ui/composables/useVueElement.ts | 325 +++++++++ packages/compiler/devtools/client/ui/index.ts | 17 + .../client/ui/store/useTargetElementStore.ts | 8 + .../compiler/devtools/client/ui/styles.css | 2 + .../compiler/devtools/client/vite.config.ts | 33 + packages/compiler/devtools/index.ts | 1 + .../devtools/plugin/add-vue-plugin.ts | 25 + packages/compiler/devtools/plugin/compiler.ts | 100 +++ packages/compiler/devtools/plugin/plugin.ts | 58 ++ packages/compiler/devtools/server/file.ts | 68 ++ .../devtools/server/server-middleware.ts | 49 ++ packages/compiler/devtools/server/utils.ts | 14 + packages/compiler/devtools/shared/CONST.ts | 5 + packages/compiler/devtools/shared/slug.ts | 32 + packages/compiler/package.json | 42 ++ packages/compiler/playground/.gitignore | 30 + packages/compiler/playground/Readme.md | 5 + packages/compiler/playground/env.d.ts | 1 + packages/compiler/playground/index.html | 12 + packages/compiler/playground/package.json | 26 + packages/compiler/playground/src/App.vue | 26 + packages/compiler/playground/src/main.ts | 16 + .../playground/src/pages/TestPage.vue | 37 + .../compiler/playground/tsconfig.app.json | 14 + packages/compiler/playground/tsconfig.json | 11 + .../compiler/playground/tsconfig.node.json | 19 + packages/compiler/playground/vite.config.ts | 27 + packages/compiler/playground/yarn.lock | 647 ++++++++++++++++++ packages/compiler/tsconfig.json | 32 + packages/compiler/vite-plugin/index.ts | 42 ++ yarn.lock | 148 ++-- 62 files changed, 3653 insertions(+), 49 deletions(-) create mode 100644 packages/compiler/Readme.md create mode 100644 packages/compiler/css-layers/index.ts create mode 100644 packages/compiler/css-layers/plugin.ts create mode 100644 packages/compiler/devtools/Readme.md create mode 100644 packages/compiler/devtools/client/build/append-style.ts create mode 100644 packages/compiler/devtools/client/env.d.ts create mode 100644 packages/compiler/devtools/client/index.ts create mode 100644 packages/compiler/devtools/client/parser/parseHTML.ts create mode 100644 packages/compiler/devtools/client/parser/parseSource.ts create mode 100644 packages/compiler/devtools/client/parser/printHTML.ts create mode 100644 packages/compiler/devtools/client/parser/printSource.ts create mode 100644 packages/compiler/devtools/client/parser/utils.ts create mode 100644 packages/compiler/devtools/client/ui/Devtools.vue create mode 100644 packages/compiler/devtools/client/ui/components/AppTree.vue create mode 100644 packages/compiler/devtools/client/ui/components/AppTreeItem.vue create mode 100644 packages/compiler/devtools/client/ui/components/ComponentView.vue create mode 100644 packages/compiler/devtools/client/ui/components/ComponentViewSettings.vue create mode 100644 packages/compiler/devtools/client/ui/components/ComponentViewSource.vue create mode 100644 packages/compiler/devtools/client/ui/components/base/CodeView.vue create mode 100644 packages/compiler/devtools/client/ui/components/base/DraggableWindow.vue create mode 100644 packages/compiler/devtools/client/ui/components/base/Outline.vue create mode 100644 packages/compiler/devtools/client/ui/components/base/Overlay.vue create mode 100644 packages/compiler/devtools/client/ui/components/base/SourceView.vue create mode 100644 packages/compiler/devtools/client/ui/composables/base/useElementRect.ts create mode 100644 packages/compiler/devtools/client/ui/composables/base/useEvent.ts create mode 100644 packages/compiler/devtools/client/ui/composables/base/useMutationObserver.ts create mode 100644 packages/compiler/devtools/client/ui/composables/base/useWindowSize.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useAppTransform.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useAppTree.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useHoveredElement.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useOutlines.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useVueElement.ts create mode 100644 packages/compiler/devtools/client/ui/index.ts create mode 100644 packages/compiler/devtools/client/ui/store/useTargetElementStore.ts create mode 100644 packages/compiler/devtools/client/ui/styles.css create mode 100644 packages/compiler/devtools/client/vite.config.ts create mode 100644 packages/compiler/devtools/index.ts create mode 100644 packages/compiler/devtools/plugin/add-vue-plugin.ts create mode 100644 packages/compiler/devtools/plugin/compiler.ts create mode 100644 packages/compiler/devtools/plugin/plugin.ts create mode 100644 packages/compiler/devtools/server/file.ts create mode 100644 packages/compiler/devtools/server/server-middleware.ts create mode 100644 packages/compiler/devtools/server/utils.ts create mode 100644 packages/compiler/devtools/shared/CONST.ts create mode 100644 packages/compiler/devtools/shared/slug.ts create mode 100644 packages/compiler/package.json create mode 100644 packages/compiler/playground/.gitignore create mode 100644 packages/compiler/playground/Readme.md create mode 100644 packages/compiler/playground/env.d.ts create mode 100644 packages/compiler/playground/index.html create mode 100644 packages/compiler/playground/package.json create mode 100644 packages/compiler/playground/src/App.vue create mode 100644 packages/compiler/playground/src/main.ts create mode 100644 packages/compiler/playground/src/pages/TestPage.vue create mode 100644 packages/compiler/playground/tsconfig.app.json create mode 100644 packages/compiler/playground/tsconfig.json create mode 100644 packages/compiler/playground/tsconfig.node.json create mode 100644 packages/compiler/playground/vite.config.ts create mode 100644 packages/compiler/playground/yarn.lock create mode 100644 packages/compiler/tsconfig.json create mode 100644 packages/compiler/vite-plugin/index.ts diff --git a/packages/compiler/Readme.md b/packages/compiler/Readme.md new file mode 100644 index 0000000000..9e9fbc248a --- /dev/null +++ b/packages/compiler/Readme.md @@ -0,0 +1,34 @@ +# Vuestic Plugin + +Combination of bundling tools focusing on improving development experience when working with Vuestic UI + +## List of features + +### Devtools + +Devtools designed for intuitive visual control over application with Vuestic components + +#### Installation + +Install package + +Add `@vuestic/compiler/devtools/vite` plugin to `vite.config.ts` + +#### Plans + +- [x] Edit component props +- [x] Edit component slot content + - [] Add new slots +- [] Add new components +- [] Control layout +- [] Add event listeners + +### CSS layers + +Build plugin that allows controlling CSS order + +### Typescript auto-completion from config + +UNDER DEVELOPMENT + +Adds TS global types for colors, icons, etc. \ No newline at end of file diff --git a/packages/compiler/css-layers/index.ts b/packages/compiler/css-layers/index.ts new file mode 100644 index 0000000000..7dab3cdffe --- /dev/null +++ b/packages/compiler/css-layers/index.ts @@ -0,0 +1 @@ +export { cssLayers } from './plugin' diff --git a/packages/compiler/css-layers/plugin.ts b/packages/compiler/css-layers/plugin.ts new file mode 100644 index 0000000000..b3c0e92e45 --- /dev/null +++ b/packages/compiler/css-layers/plugin.ts @@ -0,0 +1,29 @@ +import { Plugin } from 'vite' +import MagicString from 'magic-string' + +const addLayer = (ms: MagicString, layer: string) => { + ms.prepend(`@layer ${layer} {\n`) + ms.append(`\n}`) + return { + code: ms.toString(), + map: ms.generateMap() + } +} + +/** Add css layers to Vuestic files */ +export const cssLayers: Plugin = { + name: 'vuestic:css-layer', + + transform(code, id) { + // Only transform CSS files + if (!id.endsWith('.css')) return null + + if (id.includes('vuestic-ui/dist/styles/')) { + return addLayer(new MagicString(code), 'vuestic.styles') + } + + if (id.includes('vuestic-ui/dist/es/')) { + return addLayer(new MagicString(code), 'vuestic.components') + } + } +} diff --git a/packages/compiler/devtools/Readme.md b/packages/compiler/devtools/Readme.md new file mode 100644 index 0000000000..300e45fae4 --- /dev/null +++ b/packages/compiler/devtools/Readme.md @@ -0,0 +1,5 @@ +# Vuestic Devtools + +Client - UI for devtools, injected to user code in DEV mode only providing utilities to work visually with Vuestic components +Server - used to update user's file trough client +Plugin - adds vuestic devtools vue plugin to dev bundle and run server \ No newline at end of file diff --git a/packages/compiler/devtools/client/build/append-style.ts b/packages/compiler/devtools/client/build/append-style.ts new file mode 100644 index 0000000000..3fcc79d320 --- /dev/null +++ b/packages/compiler/devtools/client/build/append-style.ts @@ -0,0 +1,36 @@ +import { Plugin } from "vite"; +import { writeFile, readFile } from 'fs/promises' +import { resolve } from 'path' + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +export const appendStyle = (outFile: string): Plugin => { + let outDir = '' + + const onBundleClose = async () => { + await sleep(1000) + const filePath = resolve(`${outDir}/${outFile}`) + const fileContent = (await readFile(filePath, 'utf8')).toString() + + const stylePath = `./style.css` + const styleImport = `import './${stylePath}'` + + if (!fileContent.includes(styleImport)) { + await writeFile(filePath, `${styleImport}\n${fileContent}`) + } + } + + return { + name: 'vuestic:append-style', + + enforce: 'post', + + configResolved(config) { + outDir = config.build.outDir + }, + + async closeBundle() { + onBundleClose() + } + } +} diff --git a/packages/compiler/devtools/client/env.d.ts b/packages/compiler/devtools/client/env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/compiler/devtools/client/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/compiler/devtools/client/index.ts b/packages/compiler/devtools/client/index.ts new file mode 100644 index 0000000000..470897df33 --- /dev/null +++ b/packages/compiler/devtools/client/index.ts @@ -0,0 +1 @@ +export { createVuesticDevtools } from './ui' diff --git a/packages/compiler/devtools/client/parser/parseHTML.ts b/packages/compiler/devtools/client/parser/parseHTML.ts new file mode 100644 index 0000000000..7822668340 --- /dev/null +++ b/packages/compiler/devtools/client/parser/parseHTML.ts @@ -0,0 +1,68 @@ +export class Node { + constructor( + public type: 'tag:open' | 'tag:close' | 'content', + public loc: { start: { offset: number }, end: { offset: number }, source: string } = { start: { offset: 0 }, end: { offset: 0 }, source: '' }, + public children: (Node | string)[] = [], + ) {} + + toString() { + return this.loc.source + } +} + +export const parseSource = (source: string) => { + const parse = (source: string): Node[] => { + const nodes: Node[] = [] + let current = 0 + + while (current < source.length) { + const startTag = source.indexOf('<', current) + + if (startTag === -1) { break } + + const endTag = source.indexOf('>', startTag) + + if (endTag === -1) { break } + + const tag = source.slice(startTag, endTag + 1) + const isSelfClosing = tag.endsWith('/>') + + if (isSelfClosing) { + nodes.push(new Node('content', { start: { offset: startTag }, end: { offset: endTag + 1 }, source: tag })) + } else { + const startTag = source.indexOf('<', current) + const endTag = source.indexOf('>', startTag) + const tag = source.slice(startTag, endTag + 1) + + nodes.push(new Node( + 'content', + { start: { offset: startTag }, end: { offset: endTag + 1}, source: tag } + )) + } + + current = endTag + 1 + } + + if (nodes.length > 1) { + const startTag = nodes[0] + const endTag = nodes[nodes.length - 1] + startTag.type = 'tag:open' + endTag.type = 'tag:close' + const text = new Node( + 'content', + { + start: { offset: startTag.loc.end.offset }, + end: { offset: endTag.loc.start.offset }, + source: source.slice(startTag.loc.end.offset, endTag.loc.start.offset), + }, + parse(source.slice(startTag.loc.end.offset, endTag.loc.start.offset)), + ) + + return [startTag, text, endTag] + } + + return nodes + } + + return parse(source) +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/parser/parseSource.ts b/packages/compiler/devtools/client/parser/parseSource.ts new file mode 100644 index 0000000000..6059626474 --- /dev/null +++ b/packages/compiler/devtools/client/parser/parseSource.ts @@ -0,0 +1,261 @@ +const getTagContent = (source: string) => { + if (source.endsWith('/>')) { + return source.slice(1, -2) + } + + return source.slice(1, -1) +} + +const parseOpenTag = (source: string) => { + source = source.trim().replace(/\n/g, '') + let tagName = '' + const rawAttributes: Record = {} + + let tagContent = getTagContent(source) + + if (!tagContent.includes(' ')) { + tagName = tagContent + return { tagName, rawAttributes } + } + + tagContent += ' ' + + let i = 0 + + while (tagContent[i] !== ' ') { + tagName += tagContent[i] + i++ + } + + i++ + + let key = '' + let value: string = '' + let isInQuotes = false + + while (i < tagContent.length) { + if (tagContent[i] === '"') { + isInQuotes = !isInQuotes + i++ + continue + } + + if (tagContent[i] === ' ' && !isInQuotes) { + // Key might be empty if there are multiple spaces or \n + if (key !== '') { + rawAttributes[key] = value === '' ? (true as const) : value + } + key = '' + value = '' + i++ + continue + } + + if (tagContent[i] === '=') { + i++ + continue + } + + if (isInQuotes) { + value += tagContent[i] + } else { + key += tagContent[i] + } + + i++ + } + + return { tagName, rawAttributes } +} + +export type Loc = { + start: { offset: number } + end: { offset: number } + source: string +} + +export type HTMLContentNode = { + type: 'content' + text: string + parent: HTMLElementNode | HTMLRootNode +} + +export type HTMLElementNode = { + type: 'element' + tag: string + attributes: Record + parent: HTMLElementNode | HTMLRootNode + children: (HTMLElementNode | HTMLContentNode)[] + sourcePath?: string +} + +export type HTMLRootNode = { + type: 'root' + children: (HTMLElementNode | HTMLContentNode)[] +} + +export type HTMLToken = { + type: 'tag:open' | 'tag:close' | 'tag:self-closing', + tag: string + loc: Loc +} | { + type: 'content', + loc: Loc, +} + +/** Removes \n and whitespace */ +const superTrim = (content: string) => { + return content.replace(/\n/gm, '').trim() +} + +const isValidContent = (content: string) => { + return superTrim(content) !== '' +} + +const parseTokens = (source: string) => { + let current = 0 + + const tokens: HTMLToken[] = [] + + while (current < source.length) { + const startTag = source.indexOf('<', current) + + if (startTag === -1) { break } + + const endTag = source.indexOf('>', startTag) + + if (endTag === -1) { break } + + const tagContent = source.slice(startTag, endTag + 1) + + const isSelfClosing = tagContent.endsWith('/>') + const isClosingTag = tagContent.startsWith(' { + const root: HTMLRootNode = { + type: 'root', + children: [], + } + + let parent: HTMLRootNode | HTMLElementNode = root + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + + if (token.type === 'tag:open') { + if (!parent.children || typeof parent.children === 'string') { + throw new Error('Unexpected error when parsing HTML') + } + + const { rawAttributes } = parseOpenTag(token.loc.source) + + const node: HTMLElementNode = { + type: 'element', + tag: token.tag, + attributes: rawAttributes, + children: [], + parent, + } + + parent.children.push(node) + + parent = node + } + + if (token.type === 'tag:close') { + if (!('parent' in parent)) { + throw new Error('Closing tag without parent node') + } + + parent = parent.parent + } + + if (token.type === 'content') { + if (!parent.children || typeof parent.children === 'string') { + throw new Error('Unexpected error when parsing HTML') + } + + parent.children.push({ + type: 'content', + text: token.loc.source, + parent + }) + } + + if (token.type === 'tag:self-closing') { + if (!parent.children || typeof parent.children === 'string') { + throw new Error('Unexpected error when parsing HTML') + } + + const { rawAttributes, tagName } = parseOpenTag(token.loc.source) + + parent.children.push({ + type: 'element', + tag: tagName, + attributes: rawAttributes, + children: [], + parent, + }) + } + } + + return root +} + +export const parseSource = (source: string) => { + const tokens = parseTokens(source) + const tree = tokensToTree(tokens) + + return tree +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/parser/printHTML.ts b/packages/compiler/devtools/client/parser/printHTML.ts new file mode 100644 index 0000000000..0a8cd23ac4 --- /dev/null +++ b/packages/compiler/devtools/client/parser/printHTML.ts @@ -0,0 +1,122 @@ +import { Node } from './parseHTML' + +const isVBind = (key: string) => key.startsWith(':') || key.startsWith('v-bind:') + +const getKeyName = (key: string) => { + if (key.startsWith(':')) { + return key.slice(1) + } + + if (key.startsWith('v-bind:')) { + return key.slice(7) + } + + return key +} + +const parseOpenTag = (source: string) => { + let tagName = '' + const rawAttributes: Record = {} + + let tagContent = source.slice(1, -1) + + if (!tagContent.includes(' ')) { + tagName = tagContent + return { tagName, rawAttributes } + } + + tagContent += ' ' + + let i = 0 + + while (tagContent[i] !== ' ') { + tagName += tagContent[i] + i++ + } + + i++ + + let key = '' + let value = '' + let isInQuotes = false + + while (i < tagContent.length) { + if (tagContent[i] === '"') { + isInQuotes = !isInQuotes + i++ + continue + } + + if (tagContent[i] === ' ' && !isInQuotes) { + rawAttributes[key] = value ?? 'true' + key = '' + value = '' + i++ + continue + } + + if (tagContent[i] === '=') { + i++ + continue + } + + if (isInQuotes) { + value += tagContent[i] + } else { + key += tagContent[i] + } + + i++ + } + + return { tagName, rawAttributes } +} + +export const printOpenTag = (node: Node, newAttributes: Record, propsMeta?: Record) => { + const { source } = node.loc + + if (!(source.startsWith('<') && source.endsWith('>'))) { + return source + } + + const { tagName, rawAttributes } = parseOpenTag(source) + + const newAttributesKeys = Object.keys(newAttributes) + + const attributes = Object.entries(rawAttributes).reduce((acc, [rawKey, value]) => { + const key = getKeyName(rawKey) + + if (newAttributesKeys.includes(key)) { + return acc + } + + acc[rawKey] = value ? value : true + + return acc + }, {} as Record) + + const attributesString = Object + .entries({ ...attributes, ...newAttributes }) + .reduce((acc, [key, value]) => { + if (propsMeta && propsMeta[key] && propsMeta[key].default === value) { + return acc + } + + // TODO: Check default value for boolean attributes + if (value === true) { + return `${acc} ${key}` + } + + if (value === false) { + return `${acc} :${key}="false"` + } + + return `${acc} ${key}="${value}"` + }, '') + + if (attributesString.length > 0) { + return `<${tagName}${attributesString}>` + } + + return `<${tagName}>` +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/parser/printSource.ts b/packages/compiler/devtools/client/parser/printSource.ts new file mode 100644 index 0000000000..0c56c1eb2d --- /dev/null +++ b/packages/compiler/devtools/client/parser/printSource.ts @@ -0,0 +1,66 @@ +import type { HTMLRootNode, HTMLElementNode, HTMLContentNode } from "./parseSource"; + +export const printSource = (source: HTMLRootNode | HTMLElementNode | HTMLContentNode) => { + let tabSize = 0 + + const printTabs = () => ' '.repeat(tabSize) + + const print = (node: HTMLRootNode | HTMLElementNode) => { + let result = '' + + for (const child of node.children) { + if ('text' in child) { + result += child.text.split('\n').map((line) => printTabs() + line).join('\n') + } else { + result += printTabs() + `<${child.tag}` + + const attributesCount = Object.keys(child.attributes).length + + if (attributesCount === 1) { + const [key, value] = Object.entries(child.attributes)[0] + if (value === true) { + result += ` ${key}` + } else { + result += ` ${key}="${value}"` + } + } else if (attributesCount > 1) { + result += '\n' + tabSize += 2 + for (const [key, value] of Object.entries(child.attributes)) { + if (value === true) { + result += printTabs() + ` ${key}` + } else { + result += printTabs() + ` ${key}="${value}"` + } + result += '\n' + } + tabSize -= 2 + result += printTabs() + } + + if (child.children.length === 0) { + if (attributesCount <= 1) { + result += ' ' + } + result += '/>\n' + continue + } + + result += '>\n' + + tabSize += 2 + result += printTabs() + print(child).trim() + tabSize -= 2 + result += '\n' + printTabs() + `\n` + } + } + + return result + } + + if (source.type === 'content') { + return source.text + } + + return print(source) +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/parser/utils.ts b/packages/compiler/devtools/client/parser/utils.ts new file mode 100644 index 0000000000..744e9afea3 --- /dev/null +++ b/packages/compiler/devtools/client/parser/utils.ts @@ -0,0 +1,13 @@ +import { HTMLContentNode, HTMLElementNode } from "./parseSource"; + +export const getSlotName = (node: HTMLElementNode | HTMLContentNode) => { + if (node.type === 'content') return 'default' + + const name = Object + .keys(node.attributes) + .find((key) => key.startsWith('#')) // TODO: Handle v-slot + + if (!name) return null + + return name +} diff --git a/packages/compiler/devtools/client/ui/Devtools.vue b/packages/compiler/devtools/client/ui/Devtools.vue new file mode 100644 index 0000000000..9db0236964 --- /dev/null +++ b/packages/compiler/devtools/client/ui/Devtools.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/AppTree.vue b/packages/compiler/devtools/client/ui/components/AppTree.vue new file mode 100644 index 0000000000..c82c711043 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/AppTree.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/AppTreeItem.vue b/packages/compiler/devtools/client/ui/components/AppTreeItem.vue new file mode 100644 index 0000000000..d30f8f9edd --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/AppTreeItem.vue @@ -0,0 +1,65 @@ + + + + + + diff --git a/packages/compiler/devtools/client/ui/components/ComponentView.vue b/packages/compiler/devtools/client/ui/components/ComponentView.vue new file mode 100644 index 0000000000..c8b2ff3d42 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/ComponentView.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/ComponentViewSettings.vue b/packages/compiler/devtools/client/ui/components/ComponentViewSettings.vue new file mode 100644 index 0000000000..9b4fb05f28 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/ComponentViewSettings.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/ComponentViewSource.vue b/packages/compiler/devtools/client/ui/components/ComponentViewSource.vue new file mode 100644 index 0000000000..7c4fc01b04 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/ComponentViewSource.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/components/base/CodeView.vue b/packages/compiler/devtools/client/ui/components/base/CodeView.vue new file mode 100644 index 0000000000..e27535a07d --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/base/CodeView.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/base/DraggableWindow.vue b/packages/compiler/devtools/client/ui/components/base/DraggableWindow.vue new file mode 100644 index 0000000000..98d022e469 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/base/DraggableWindow.vue @@ -0,0 +1,115 @@ + + + + + \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/components/base/Outline.vue b/packages/compiler/devtools/client/ui/components/base/Outline.vue new file mode 100644 index 0000000000..21fc5bdb6a --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/base/Outline.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/base/Overlay.vue b/packages/compiler/devtools/client/ui/components/base/Overlay.vue new file mode 100644 index 0000000000..05bdd7ff9e --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/base/Overlay.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/base/SourceView.vue b/packages/compiler/devtools/client/ui/components/base/SourceView.vue new file mode 100644 index 0000000000..6d63ee37d3 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/base/SourceView.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/composables/base/useElementRect.ts b/packages/compiler/devtools/client/ui/composables/base/useElementRect.ts new file mode 100644 index 0000000000..d06cb2fe29 --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/base/useElementRect.ts @@ -0,0 +1,30 @@ +import { ref, type Ref, watchEffect } from 'vue' + +export const useElementRect = (el: Ref) => { + const rect = ref() + + const resizeObserver = new ResizeObserver(([size]) => { + rect.value = size.contentRect + }) + + const setRect = (el: HTMLElement | null) => { + if (el) { + rect.value = el.getBoundingClientRect() + } + } + + window.addEventListener('resize', () => { + setRect(el.value) + }) + + watchEffect(() => { + setRect(el.value) + if (el.value) { + resizeObserver.observe(el.value) + } else { + resizeObserver.disconnect() + } + }) + + return rect +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/composables/base/useEvent.ts b/packages/compiler/devtools/client/ui/composables/base/useEvent.ts new file mode 100644 index 0000000000..b10d679dc7 --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/base/useEvent.ts @@ -0,0 +1,11 @@ +import { onBeforeUnmount, onMounted } from "vue" + +export const useEvent = (event: Name, handler: (e: GlobalEventHandlersEventMap[Name]) => any, options: AddEventListenerOptions = {}) => { + onMounted(() => { + window.addEventListener(event, handler, options) + }) + + onBeforeUnmount(() => { + window.removeEventListener(event, handler, options) + }) +} diff --git a/packages/compiler/devtools/client/ui/composables/base/useMutationObserver.ts b/packages/compiler/devtools/client/ui/composables/base/useMutationObserver.ts new file mode 100644 index 0000000000..3b3c37f591 --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/base/useMutationObserver.ts @@ -0,0 +1,18 @@ +import { watch, Ref } from 'vue'; + +export const useMutationObserver = (target: Ref, callback: () => void) => { + let observer = new MutationObserver(callback); + + watch(target, (newValue, oldValue) => { + if (oldValue) { + observer.disconnect(); + } + if (newValue) { + observer.observe(newValue, { + attributes: true, + childList: true, + subtree: true + }); + } + }) +} diff --git a/packages/compiler/devtools/client/ui/composables/base/useWindowSize.ts b/packages/compiler/devtools/client/ui/composables/base/useWindowSize.ts new file mode 100644 index 0000000000..64af3523af --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/base/useWindowSize.ts @@ -0,0 +1,20 @@ +import { onBeforeUnmount, onMounted, reactive } from "vue" + +export const useWindowSize = () => { + const size = reactive({ width: window.innerWidth, height: window.innerHeight }) + + const updateSize = () => { + size.width = window.innerWidth + size.height = window.innerHeight + } + + onMounted(() => { + window.addEventListener('resize', updateSize) + }) + + onBeforeUnmount(() => { + window.removeEventListener('resize', updateSize) + }) + + return size +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/composables/useAppTransform.ts b/packages/compiler/devtools/client/ui/composables/useAppTransform.ts new file mode 100644 index 0000000000..10c71a8666 --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/useAppTransform.ts @@ -0,0 +1,79 @@ +import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue" + +const MIN_ZOOM = 0.5 +const MAX_ZOOM = 2 + +export const useAppTransform = () => { + const zoom = ref(1) + + const zoomIn = () => { + if (zoom.value >= MAX_ZOOM) { + return + } + zoom.value += 0.1 + } + + const zoomOut = () => { + if (zoom.value <= MIN_ZOOM) { + return + } + zoom.value -= 0.1 + } + + const onWheel = (e: WheelEvent) => { + if (e.deltaY > 0) { + zoomOut() + } else { + zoomIn() + } + } + + const translate = reactive({ x: 0, y: 0 }) + + let pressed = false + + const onMouseMove = (event: MouseEvent) => { + if (!pressed) { + return + } + translate.x += event.movementX / (zoom.value) + translate.y += event.movementY / (zoom.value) + } + + const onMouseDown = (event: MouseEvent) => { + // middle button + if (event.button === 1) { + pressed = true + } + } + + const onMouseUp = (event: MouseEvent) => { + // middle button + if (event.button === 1) { + pressed = false + } + } + + const onBlur = () => { + pressed = false + } + + onMounted(() => { + window.addEventListener('blur', onBlur) + }) + + onBeforeUnmount(() => { + window.removeEventListener('blur', onBlur) + }) + + return { + zoom, + onWheel, + zoomIn, + zoomOut, + translate, + onMouseMove, + onMouseDown, + onMouseUp, + } +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree.ts b/packages/compiler/devtools/client/ui/composables/useAppTree.ts new file mode 100644 index 0000000000..a1ecda147c --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/useAppTree.ts @@ -0,0 +1,129 @@ +import { onMounted, ref, type VNode } from "vue"; +import { PREFIX } from "../../../shared/CONST"; + +export type AppTreeItem = { + type: 'vue:component' | 'native:element' | 'vue:element', + el: HTMLElement, + name: string, + children: AppTreeItem[], + node?: VNode + vFor?: number +} + +const getComponentName = (node: VNode) => { + if (!('ctx' in node) || typeof node.ctx !== 'object' || node.ctx === null) { return } + + if (!('type' in node.ctx) || typeof node.ctx.type !== 'object' || node.ctx.type === null) { return } + + if (!('name' in node.ctx.type) || typeof node.ctx.type.name !== 'string') { return } + + return node.ctx.type.name +} + +const getTagName = (node: VNode) => { + if (typeof node.type === 'string') { + return node.type + } +} + +const hasVNode = (el: HTMLElement): el is HTMLElement & { __vnode: VNode } => { + return '__vnode' in el +} + +const compareNodeUid = (a: VNode | undefined, b: VNode | undefined) => { + if (!a || !b) { return false } + + const aCtx = (a as any).ctx + const bCtx = (b as any).ctx + + if (!aCtx || !bCtx) { return false } + + const aUid = aCtx.uid + const bUid = bCtx.uid + + return aUid === bUid +} + +const getAppTree = () => { + const app = document.querySelector('#app') + + if (!app) { throw new Error('App element not found when building app tree') } + + const parseNode = (el: Element): AppTreeItem | null => { + if (!(el instanceof HTMLElement)) { + return null + } + + const children = (Array + .from(el.children) + .map(parseNode) + .filter(Boolean) as AppTreeItem[]) + .reduce((acc, node) => { + const sameVNode = acc.find((n) => compareNodeUid(n.node, node.node)) + + if (sameVNode) { + sameVNode.vFor = (sameVNode.vFor || 1) + 1 + return acc + } + + acc.push(node) + + return acc + }, [] as AppTreeItem[]) + + if (!(PREFIX in el.dataset) || !hasVNode(el)) { + return { + type: 'native:element', + el: el, + name: el.tagName.toLowerCase(), + children: children.filter(Boolean), + } + } + + const componentName = getComponentName(el.__vnode) + const tagName = getTagName(el.__vnode) + + if (componentName) { + return { + type: 'vue:component', + name: componentName, + el, + children, + node: el.__vnode, + } + } + + if (tagName) { + return { + type: 'vue:element', + name: tagName, + el, + children, + node: el.__vnode, + } + } + + return null + } + + return parseNode(app) +} + +export const useAppTree = () => { + const appTree = ref(null) + + const refresh = () => { + appTree.value = getAppTree() + + console.log(appTree.value) + } + + onMounted(() => { + refresh() + }) + + return { + appTree, + refresh, + } +} diff --git a/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts b/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts new file mode 100644 index 0000000000..ac193b71c7 --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts @@ -0,0 +1,59 @@ +import { onBeforeMount, onMounted, ref, watch } from "vue" +import { useTargetElementStore } from "../store/useTargetElementStore" +import { PREFIX } from "../../../shared/CONST" + +export const useHoveredElement = () => { + const { targetElement: selectedElement } = useTargetElementStore() + + const hoveredElement = ref(null) + + const elementHasPaddings = (el: HTMLElement) => { + const style = window.getComputedStyle(el) + const paddings = ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'] as const + return paddings.some((padding) => parseInt(style[padding]) > 0) + } + + const onMouseMove = (event: MouseEvent) => { + const elementsUnderPointer = document.elementsFromPoint(event.x, event.y) + .filter((el) => { + if (!(el instanceof HTMLElement)) { + return false + } + + return PREFIX in el.dataset + }) + .reverse() + + if (elementsUnderPointer.length === 0) { + hoveredElement.value = null + return + } + + const clickedElement = elementsUnderPointer.find((el) => { + if (el.contains(selectedElement.value)) { + return false + } + + // TODO: Check if element is HTML element + const hasPaddings = elementHasPaddings(el as HTMLElement) + + if (hasPaddings) { + return false + } + + return true + }) ?? elementsUnderPointer[elementsUnderPointer.length - 1] + + hoveredElement.value = clickedElement as HTMLElement + } + + onMounted(() => { + window.addEventListener('mousemove', onMouseMove, { capture: true }) + }) + + onBeforeMount(() => { + window.removeEventListener('mousemove', onMouseMove, { capture: true }) + }) + + return hoveredElement +} diff --git a/packages/compiler/devtools/client/ui/composables/useOutlines.ts b/packages/compiler/devtools/client/ui/composables/useOutlines.ts new file mode 100644 index 0000000000..5c075993c2 --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/useOutlines.ts @@ -0,0 +1,22 @@ +import { onBeforeUnmount, onMounted } from "vue" + +const outlines = [] as Array<() => unknown> + +export const useOutlines = () => { + return () => { + outlines.forEach(recalculate => recalculate()) + } +} + +export const useOutline = (recalculate: () => unknown) => { + onMounted(() => { + outlines.push(recalculate) + }) + + onBeforeUnmount(() => { + const index = outlines.indexOf(recalculate) + if (index !== -1) { + outlines.splice(index, 1) + } + }) +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/composables/useVueElement.ts b/packages/compiler/devtools/client/ui/composables/useVueElement.ts new file mode 100644 index 0000000000..48729bdd8e --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/useVueElement.ts @@ -0,0 +1,325 @@ +import { PREFIX, API_PREFIX } from '../../../shared/CONST'; +import { type Ref, computed, ref, reactive, watch, type VNode } from 'vue' +import { useTargetElementStore } from '../store/useTargetElementStore' +import { HTMLContentNode, HTMLElementNode, parseSource, type HTMLRootNode } from '../../parser/parseSource' +import { getSlotName } from '../../parser/utils' +import { printSource } from '../../parser/printSource' +import { useOutlines } from './useOutlines'; + +type SlotContent = string | HTMLElementNode + +const API_URL = new URL(import.meta.url).origin + API_PREFIX + +// FixMe: This is temporary. We need to store this globally. Maybe use pinia. + +const useVueElementSource = (element: Ref) => { + /** @notice Source is async and may not be available until loaded */ + const source = ref(null) + + const q = computed(() => { + if (!element.value) { return null } + const paths = Object.keys(element.value.dataset) + .filter((key) => key.startsWith(`${PREFIX}:`)) + + return paths[0] + }) + + const reset = () => { + source.value = null + } + + const load = async () => { + if (!element.value) { return reset() } + if (!(PREFIX in element.value.dataset)) { return reset() } + + const paths = Object.keys(element.value.dataset) + .filter((key) => key.startsWith(`${PREFIX}:`)) + + const response = await fetch(`${API_URL}/node-source?q=${paths[0]}`) + source.value = await response.text() + } + + watch(element, async () => { + load() + }, { immediate: true }) + + return { + q, + source, + refresh: load, + } +} + +const { targetElement } = useTargetElementStore() + +const vNode = computed(() => { + if (!targetElement.value) return null + + return (targetElement.value as any).__vnode as VNode ?? null +}) + +const props = computed(() => { + if (!vNode.value) return null + + if (!('ctx' in vNode.value)) { + return null + } + + return (vNode.value.ctx as any)?.props as Record ?? null +}) + +const getVNodeComponent = (vNode: VNode | null) => { + if (!vNode) return null + + if (!('ctx' in vNode)) { return null } + + const ctx = vNode.ctx as any + + if (!ctx) { return null } + + return { + name: ctx.type.__name as string || undefined, + props: ctx.type.props as Record, + } +} + +const meta = computed(() => { + return { + type: vNode.value?.type, + component: getVNodeComponent(vNode.value), + } +}) + +const { source, refresh, q } = useVueElementSource(targetElement) + +const settings = reactive({ + newProps: {} as Record, + newSlots: {} as Record, +}) + +const autoSave = ref(false) + +watch(settings, () => { + if (autoSave.value) { + save() + } +}, { deep: true }) + +const reset = async () => { + // Reminded: Make sure to reset all of the settings + settings.newProps = {} + settings.newSlots = {} + await refresh() +} + +const parsed = computed(() => { + if (!source.value) return null + + return parseSource(source.value) +}) + +const slots = computed(() => { + if (!parsed.value) return null + + const el = parsed.value.children[0] + + if (el.type === 'content') { return null } + + return el + .children + .map((child, index) => { + if (child.type === 'content') { + return { + index: index, + content: child.text, + name: 'default', + update: (content: string) => { + settings.newSlots['default'] = content + } + } + } + + if (child.tag === 'template') { + if (child.children.length === 1) { + if (child.children[0].type !== 'content') { + return null + } + + const name = Object.keys(child.attributes).find((key) => key.startsWith('#')) + + if (!name) { + return null + } + + return { + index: index, + content: child.children[0].text, + name: name, + update: (content: string) => { + settings.newSlots[name] = { + ...child, + children: [{ type: 'content', text: content, parent: child }], + } satisfies HTMLElementNode + } + } + } + } + + return null + }) + .filter((slot) => slot !== null) as { index: number, content: string, name: string, update: (content: SlotContent) => void }[] +}) + +const normalizeAttributeKey = (attribute: string) => { + if (attribute.startsWith(':')) { + return attribute.slice(1) + } + + if (attribute.startsWith('v-bind:')) { + return attribute.slice(7) + } + + if (attribute.startsWith('v-model:')) { + return attribute.slice(8) + } + + return attribute +} + +const newSource = computed(() => { + if (!parsed.value) return null + + if (parsed.value?.children.length !== 1) { + throw new Error('Cannot update props on multi-node vNode') + } + + const rootChild = parsed.value.children[0] + + if (rootChild.type === 'content') { + throw new Error('Cannot update props on content vNode') + } + + const propsMeta = meta.value.component?.props + const originalAttributeKeys = Object.keys(rootChild.attributes) + const newPropKeys = Object.keys(settings.newProps) + + const newAttributes = [...originalAttributeKeys, ...newPropKeys].reduce((acc, key) => { + if (key in settings.newProps) { + acc[normalizeAttributeKey(key)] = settings.newProps[key] + } else if (key in rootChild.attributes) { + acc[key] = rootChild.attributes[key] + } + + if (propsMeta && key in propsMeta) { + const propMeta = propsMeta[key] + + if (propMeta.default === acc[key]) { + delete acc[key] + } + } + + return acc + }, {} as Record) + + const newRoot: HTMLRootNode = { + type: 'root', + children: [] + } + + const newChildren = rootChild.children.map((child) => { + const name = getSlotName(child) + + if (!name) { + return child + } + + if (name in settings.newSlots) { + const slot = settings.newSlots[name] + + if (typeof slot === 'string') { + return { + type: 'content', + text: slot, + parent: newRoot, + } satisfies HTMLContentNode + } + + return slot + } + + return child + }) + + newRoot.children.push({ + type: 'element', + tag: rootChild.tag, + attributes: newAttributes, + children: newChildren, + parent: newRoot, + }) + + return printSource(newRoot) +}) + +const save = async () => { + const updated = await fetch(`${API_URL}/node-source?q=${q.value}`, { + method: 'PATCH', + body: newSource.value, + }) + + // const newDataAttribute = await updated.text() + + // targetElement.value!.dataset[PREFIX] = newDataAttribute + + reset() +} + +watch(targetElement, () => { + reset() +}) + +const isNativeElement = computed(() => { + if (!parsed.value) return false + + if (parsed.value.children.length === 1) { + if (parsed.value.children[0].type === 'element') { + return parsed.value.children[0].tag === meta.value.type + } + } + return true +}) + +const name = computed(() => { + if (parsed.value?.children.length === 1) { + // Get element tag from source, because component vnode not often contain correct name + if (parsed.value.children[0].type === 'element') { + const nameFromSourceCode = parsed.value.children[0].tag + + return nameFromSourceCode + } + } + + if (meta.value.component?.name) { + return `vue:${meta.value.component?.name}` + } + + return `html:${String(meta.value.type)}` +}) + +export const useVueElement = () => { + return { + isNativeElement, + settings, + element: targetElement, + parsed, + vNode, + name, + meta, + props, + slots, + source, + newSource, + save, + autoSave, + } +} diff --git a/packages/compiler/devtools/client/ui/index.ts b/packages/compiler/devtools/client/ui/index.ts new file mode 100644 index 0000000000..9bd4b2ea18 --- /dev/null +++ b/packages/compiler/devtools/client/ui/index.ts @@ -0,0 +1,17 @@ +import { createApp, Plugin } from 'vue' +import App from './Devtools.vue' +import { createVuesticEssential } from 'vuestic-ui' +import 'vuestic-ui/styles/essential.css' +import './styles.css' + +export const createVuesticDevtools = () => ({ + install(app) { + const appRoot = document.createElement('div') + appRoot.id = 'vuestic-devtools' + document.body.appendChild(appRoot) + + createApp(App) + .use(createVuesticEssential()) + .mount(appRoot) + } +}) satisfies Plugin diff --git a/packages/compiler/devtools/client/ui/store/useTargetElementStore.ts b/packages/compiler/devtools/client/ui/store/useTargetElementStore.ts new file mode 100644 index 0000000000..7c9e03c97d --- /dev/null +++ b/packages/compiler/devtools/client/ui/store/useTargetElementStore.ts @@ -0,0 +1,8 @@ +import { ref, type Ref } from 'vue' + +// Store the target element globally +const targetElement = ref(null) as Ref + +export const useTargetElementStore = () => { + return { targetElement } +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/styles.css b/packages/compiler/devtools/client/ui/styles.css new file mode 100644 index 0000000000..03c579e071 --- /dev/null +++ b/packages/compiler/devtools/client/ui/styles.css @@ -0,0 +1,2 @@ +@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;1,700&display=swap"; +@import "https://fonts.googleapis.com/icon?family=Material+Icons"; diff --git a/packages/compiler/devtools/client/vite.config.ts b/packages/compiler/devtools/client/vite.config.ts new file mode 100644 index 0000000000..700aff2dff --- /dev/null +++ b/packages/compiler/devtools/client/vite.config.ts @@ -0,0 +1,33 @@ + +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { appendStyle } from './build/append-style' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + appendStyle('vuestic-devtools.es.js') + ], + + build: { + minify: false, + sourcemap: false, + + lib: { + entry: fileURLToPath(new URL('./index.ts', import.meta.url)), + name: 'vuestic-devtools', + formats: ['es'], + fileName: (format) => `vuestic-devtools.es.js`, + }, + + rollupOptions: { + external: ['vue', 'shiki', 'vuestic-ui'], + + output: { + chunkFileNames: `[name].js`, + } + } + }, +}) diff --git a/packages/compiler/devtools/index.ts b/packages/compiler/devtools/index.ts new file mode 100644 index 0000000000..cac74d9f33 --- /dev/null +++ b/packages/compiler/devtools/index.ts @@ -0,0 +1 @@ +export { devtools, PluginOptions } from './plugin/plugin' diff --git a/packages/compiler/devtools/plugin/add-vue-plugin.ts b/packages/compiler/devtools/plugin/add-vue-plugin.ts new file mode 100644 index 0000000000..5427fd094b --- /dev/null +++ b/packages/compiler/devtools/plugin/add-vue-plugin.ts @@ -0,0 +1,25 @@ +import MagicString from 'magic-string' + +const CREATE_APP_TEMPLATE = 'createApp(App)' + +/** + * Add devtools plugin to the Vue app + */ +export const addVuePlugin = (code: string) => { + const ms = new MagicString(code) + + const createAppIndex = code.indexOf(CREATE_APP_TEMPLATE) + + if (createAppIndex === -1) { + console.warn('VuesticDevtools: createApp(App) not found in the file. Please, open an issue on GitHub.') + return + } + + ms.appendRight(createAppIndex + CREATE_APP_TEMPLATE.length, '.use(createVuesticDevtools())') + ms.appendLeft(0, 'import { createVuesticDevtools } from "@vuestic/compiler/devtools";\n') + + return { + code: ms.toString(), + map: ms.generateMap({ hires: true }), + } +} diff --git a/packages/compiler/devtools/plugin/compiler.ts b/packages/compiler/devtools/plugin/compiler.ts new file mode 100644 index 0000000000..e5eb2b4f43 --- /dev/null +++ b/packages/compiler/devtools/plugin/compiler.ts @@ -0,0 +1,100 @@ +import { parse } from '@vue/compiler-sfc' +import type { TemplateChildNode, RootNode } from '@vue/compiler-core' +import MagicString from 'magic-string' +import { minifyPath } from '../shared/slug' +import { PREFIX } from '../shared/CONST' + +const walk = (node: TemplateChildNode | RootNode, cb: (node: TemplateChildNode | RootNode) => void) => { + cb(node) + + if (!('children' in node)) { return } + + for (const child of node.children) { + if (typeof child === 'string') { continue } + if (typeof child === 'symbol') { continue } + if (child.type === 4) { continue } + + walk(child, cb) + } +} + +const findEndTagIndex = (source: string) => { + let inQuotes = false + + for (let i = 0; i < source.length; i++) { + if (source[i] === '"') { + inQuotes = !inQuotes + } + + if (source[i] === '>' && !inQuotes) { + return i + } + } + + return -1 +} + +const findSefCloseTagIndex = (source: string) => { + let inQuotes = false + + for (let i = 0; i < source.length; i++) { + if (source[i] === '"') { + inQuotes = !inQuotes + } + + if (source[i] === '/' && source[i + 1] === '>' && !inQuotes) { + return i + } + } + + return -1 +} + +const getNodeTagLoc = (source: string) => { + let selfCloseIndex = findSefCloseTagIndex(source) + let closeIndex = findEndTagIndex(source) + + if (selfCloseIndex === -1) { + selfCloseIndex = source.length + } + + if (closeIndex === -1) { + closeIndex = source.length + } + + return { + start: { offset: 0 }, + end: { offset: selfCloseIndex < closeIndex ? selfCloseIndex + 2 : closeIndex + 1 }, + source: source.slice(0, selfCloseIndex < closeIndex ? selfCloseIndex + 2 : closeIndex + 1), + endSymbol: selfCloseIndex < closeIndex ? '/>' : '>', + } +} + + +export const transformFile = async (code: string, id: string) => { + const result = parse(code) + const templateAst = result.descriptor.template?.ast + + if (!templateAst) { + return + } + + let source = new MagicString(code) + + // TODO: TS fix correct versions of @vue/compiler-core and @vue/compiler-sfc + walk(templateAst as unknown as RootNode, (node) => { + if (node.type === 1) { + const tagLoc = getNodeTagLoc(node.loc.source) + const nodeId = `${id}:${node.loc.start.offset}:${node.loc.end.offset}` as const + + const withAttribute = ` data-${PREFIX}="" data-${minifyPath(nodeId)}=""` + + source.appendLeft(node.loc.start.offset + tagLoc.end.offset - tagLoc.endSymbol.length, withAttribute) + } + }) + + return { + code: source.toString(), + map: source.generateMap(), + } +} diff --git a/packages/compiler/devtools/plugin/plugin.ts b/packages/compiler/devtools/plugin/plugin.ts new file mode 100644 index 0000000000..d11a981860 --- /dev/null +++ b/packages/compiler/devtools/plugin/plugin.ts @@ -0,0 +1,58 @@ +import { Plugin, ResolvedConfig } from 'vite' +import { createFilter, FilterPattern } from '@rollup/pluginutils' +import { transformFile } from './compiler' +import { devtoolsServerMiddleware } from '../server/server-middleware' +import { fileURLToPath, URL } from 'node:url' +import { addVuePlugin } from './add-vue-plugin' + +export type PluginOptions = { + include?: FilterPattern + exclude?: FilterPattern +} + +export const devtools = (options: PluginOptions = {}): Plugin => { + const filter = createFilter( + options.include ?? ['**/*.vue'], + options.exclude ?? ['node_modules/**'] + ) + + let config: ResolvedConfig + + return { + name: 'vuestic:devtools', + + configureServer(server) { + server.middlewares.use(devtoolsServerMiddleware()) + }, + + configResolved(resolvedConfig) { + config = resolvedConfig + }, + + async resolveId(id) { + if (id.startsWith('@vuestic/compiler/devtools')) { + if (config.isProduction) { + throw new Error('VuesticDevtools: devtools must not be imported in production') + } + + return fileURLToPath(new URL('../client/index.ts', import.meta.url)) + } + }, + + transform(code, id) { + if (config.isProduction) { + return + } + + if (id.endsWith('/src/main.ts')) { + return addVuePlugin(code) + } + + if (!filter(id)) { + return + } + + return transformFile(code, id) + }, + } +} diff --git a/packages/compiler/devtools/server/file.ts b/packages/compiler/devtools/server/file.ts new file mode 100644 index 0000000000..00ae629983 --- /dev/null +++ b/packages/compiler/devtools/server/file.ts @@ -0,0 +1,68 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +export const requestSource = async (path: string) => { + const source = await readFile(path, 'utf-8'); + return source.toString(); +}; + +export const getIntent = (source: string, lineStart: number) => { + let intent = 0; + for (let i = lineStart - 1; i > 0; i--) { + if (source[i] === ' ') { + intent++; + } else { + break; + } + } + return intent; +}; + +export const removeIntent = (source: string, intent: number): string => { + const lines = source.split('\n'); + const intentString = ' '.repeat(intent); + return lines + .map((line) => { + if (line.startsWith(intentString)) { + return line.slice(intentString.length); + } + return line; + }) + .join('\n'); +}; + +export const addIntent = (source: string, intent: number): string => { + const lines = source.split('\n'); + const intentString = ' '.repeat(intent); + return lines + .filter((line) => line.length > 0) + .map((line, index) => { + if (index === 0) { + return line; + } + return intentString + line; + }) + .join('\n'); +}; + +export const getComponentSource = async (path: string, start: number, end: number) => { + const fileSource = await requestSource(path); + const intent = getIntent(fileSource, start); + const componentSource = fileSource.slice(start, end); + return removeIntent(componentSource, intent); +} + +export const setComponentSource = async (path: string, start: number, end: number, source: string) => { + const fileSource = await requestSource(path); + const intent = getIntent(fileSource, start); + const sourceWithIntent = addIntent(source, intent); + const fileSourceStart = fileSource.slice(0, start); + const fileSourceEnd = fileSource.slice(end); + + const newFileContent = fileSourceStart + sourceWithIntent + fileSourceEnd; + + await writeFile(path, newFileContent); + + const newPath = `${path}:${start}:${start + sourceWithIntent.length}`; + + return newPath +} diff --git a/packages/compiler/devtools/server/server-middleware.ts b/packages/compiler/devtools/server/server-middleware.ts new file mode 100644 index 0000000000..1a6af4c213 --- /dev/null +++ b/packages/compiler/devtools/server/server-middleware.ts @@ -0,0 +1,49 @@ +import { Connect } from 'vite' +import { readBody } from './utils' +import { API_PREFIX } from '../shared/CONST' +import { getComponentSource, setComponentSource } from './file' +import { replacePath, unminifyPath } from '../shared/slug' + +export const devtoolsServerMiddleware = (): Connect.NextHandleFunction => { + return async (req, res, next) => { + if (!req.url ||!req.url.startsWith(API_PREFIX)) { + return next() // Ignore non-devtools requests + } + + const url = new URL(req.url, 'http://localhost:8088'); + const minified = url.searchParams.get('q') ?? '' + const unminified = unminifyPath(minified as `va:${string}`); + + if (!unminified) { + res.writeHead(400); + res.end(`No q provided. Got q="${url.searchParams.get('q')}"`); + return; + } + + const [path, start, end] = unminified.split(':'); + + if (req.method === 'GET' && req.url.startsWith(`${API_PREFIX}/node-source`)) { + res.writeHead(200) + res.end(await getComponentSource(path, Number(start), Number(end))); + return; + } + + if (req.method === 'PATCH' && req.url.startsWith(`${API_PREFIX}/node-source`)) { + const body = await readBody(req); + + if (!(typeof body === 'string') || body.length === 0) { + throw new Error('Body is required.'); + } + + const newPath = await setComponentSource(path, Number(start), Number(end), body); + + const newMinifiedPath = replacePath(minified, newPath) + + res.writeHead(200) + res.end(minified); + return + } + + next() + } +} diff --git a/packages/compiler/devtools/server/utils.ts b/packages/compiler/devtools/server/utils.ts new file mode 100644 index 0000000000..0117b6e7cb --- /dev/null +++ b/packages/compiler/devtools/server/utils.ts @@ -0,0 +1,14 @@ +import { Connect } from 'vite'; + +export const readBody = async (req: Connect.IncomingMessage) => { + return new Promise((resolve) => { + let body = ''; + req.on('data', chunk => { + body += chunk; + }); + + req.on('end', () => { + resolve(body); + }); + }); +} diff --git a/packages/compiler/devtools/shared/CONST.ts b/packages/compiler/devtools/shared/CONST.ts new file mode 100644 index 0000000000..0ad4b46ce0 --- /dev/null +++ b/packages/compiler/devtools/shared/CONST.ts @@ -0,0 +1,5 @@ +export const PREFIX = 'va' + +export const EDIT_MODE_CLASS = 'va-edit-mode' + +export const API_PREFIX = '/vuestic-devtools-api' diff --git a/packages/compiler/devtools/shared/slug.ts b/packages/compiler/devtools/shared/slug.ts new file mode 100644 index 0000000000..1c2ed6a7f6 --- /dev/null +++ b/packages/compiler/devtools/shared/slug.ts @@ -0,0 +1,32 @@ +import { PREFIX } from "./CONST" + +type Path = `${string}:${string}:${string}` | string +type MinifiedPath = `${typeof PREFIX}:${string}` | string +const knownPaths = new Map() + +export const minifyPath = (path: Path) => { + for (const [p, minifiedPath] of knownPaths.entries()) { + if (p === path) { + return minifiedPath + } + } + + const minified = `${PREFIX}:${knownPaths.size}` as const + + knownPaths.set(minified, path) + + return minified +} + +export const unminifyPath = (minified: MinifiedPath) => { + if (knownPaths.has(minified)) { + return knownPaths.get(minified)! + } + + return null +} + +export const replacePath = (minified: MinifiedPath, path: Path) => { + knownPaths.set(minified, path) + return minified +} diff --git a/packages/compiler/package.json b/packages/compiler/package.json new file mode 100644 index 0000000000..11615c22a8 --- /dev/null +++ b/packages/compiler/package.json @@ -0,0 +1,42 @@ +{ + "name": "@vuestic/compiler", + "version": "0.0.1-beta2", + "dependencies": { + "shiki": "^1.10.1" + }, + "scripts": { + "build": "rm -rf ./dist && yarn build:vite && yarn build:devtools:client", + "build:vite": "tsup-node vite-plugin/index.ts --dts --format esm,cjs --target node16 --shims --out-dir ./dist/vite", + "build:devtools:client": "vite build --config devtools/client/vite.config.ts --outDir ./dist/devtools/client", + "dev": "cd ./playground && yarn dev", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "@vue/compiler-core": "^3.4.21", + "@vue/compiler-sfc": "^3.4.21", + "tsup": "^8.1.0", + "vite": "^5.3.3" + }, + "exports": { + "./vite": { + "import": "./dist/vite/index.mjs", + "require": "./dist/vite/index.js", + "types": { + "import": "./dist/vite/index.d.ts", + "require": "./dist/vite/index.d.ts", + "default": "./dist/vite/index.d.ts" + } + }, + "./devtools": "./dist/devtools/client/vuestic-devtools.es.js" + }, + "keywords": [ + "vuestic", + "vue", + "devtools", + "vite" + ], + "files": [ + "dist", + "Readme.md" + ] +} diff --git a/packages/compiler/playground/.gitignore b/packages/compiler/playground/.gitignore new file mode 100644 index 0000000000..8ee54e8d34 --- /dev/null +++ b/packages/compiler/playground/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/packages/compiler/playground/Readme.md b/packages/compiler/playground/Readme.md new file mode 100644 index 0000000000..435e543775 --- /dev/null +++ b/packages/compiler/playground/Readme.md @@ -0,0 +1,5 @@ +# Playground + +Playground for testing `vite-plugin` + +> Notice that CSS layers will not work in playground, because it modifies `vuestic-ui` dist. \ No newline at end of file diff --git a/packages/compiler/playground/env.d.ts b/packages/compiler/playground/env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/packages/compiler/playground/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/compiler/playground/index.html b/packages/compiler/playground/index.html new file mode 100644 index 0000000000..1add8a2c79 --- /dev/null +++ b/packages/compiler/playground/index.html @@ -0,0 +1,12 @@ + + + + + + Vuestic Devtools Playground + + +
+ + + diff --git a/packages/compiler/playground/package.json b/packages/compiler/playground/package.json new file mode 100644 index 0000000000..18f0098748 --- /dev/null +++ b/packages/compiler/playground/package.json @@ -0,0 +1,26 @@ +{ + "name": "playground", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force" + }, + "dependencies": { + "vue": "^3.4.29" + }, + "devDependencies": { + "@tsconfig/node20": "^20.1.4", + "@types/node": "^20.14.5", + "@vitejs/plugin-vue": "^5.0.5", + "@vue/tsconfig": "^0.5.1", + "npm-run-all2": "^6.2.0", + "typescript": "~5.4.0", + "vite": "^5.3.1", + "vue-tsc": "^2.0.21" + } +} diff --git a/packages/compiler/playground/src/App.vue b/packages/compiler/playground/src/App.vue new file mode 100644 index 0000000000..cb9fd122b6 --- /dev/null +++ b/packages/compiler/playground/src/App.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/compiler/playground/src/main.ts b/packages/compiler/playground/src/main.ts new file mode 100644 index 0000000000..a37e34c9ca --- /dev/null +++ b/packages/compiler/playground/src/main.ts @@ -0,0 +1,16 @@ +import { createApp } from 'vue' +import App from './App.vue' +import { createVuestic } from 'vuestic-ui' +import 'vuestic-ui/css' + +createApp(App) + .use(createVuestic({ + config: { + colors: { + variables: { + primary: '#343a40', + } + } + } + })) + .mount('#app') diff --git a/packages/compiler/playground/src/pages/TestPage.vue b/packages/compiler/playground/src/pages/TestPage.vue new file mode 100644 index 0000000000..cef4c6a50f --- /dev/null +++ b/packages/compiler/playground/src/pages/TestPage.vue @@ -0,0 +1,37 @@ + + + diff --git a/packages/compiler/playground/tsconfig.app.json b/packages/compiler/playground/tsconfig.app.json new file mode 100644 index 0000000000..e14c754d3a --- /dev/null +++ b/packages/compiler/playground/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/compiler/playground/tsconfig.json b/packages/compiler/playground/tsconfig.json new file mode 100644 index 0000000000..66b5e5703e --- /dev/null +++ b/packages/compiler/playground/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/packages/compiler/playground/tsconfig.node.json b/packages/compiler/playground/tsconfig.node.json new file mode 100644 index 0000000000..f094063030 --- /dev/null +++ b/packages/compiler/playground/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/packages/compiler/playground/vite.config.ts b/packages/compiler/playground/vite.config.ts new file mode 100644 index 0000000000..2e96dc75c0 --- /dev/null +++ b/packages/compiler/playground/vite.config.ts @@ -0,0 +1,27 @@ + +import { fileURLToPath, URL } from 'node:url' +import Inspect from 'vite-plugin-inspect' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +import { vuestic } from '../vite-plugin' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vuestic({ + devtools: { + include: fileURLToPath(new URL('./src', import.meta.url)) + '/**/*.vue' + } + }), + vue(), + Inspect(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@vuestic/compiler/devtools': fileURLToPath(new URL('../devtools/client/index.ts', import.meta.url)), + } + } +}) diff --git a/packages/compiler/playground/yarn.lock b/packages/compiler/playground/yarn.lock new file mode 100644 index 0000000000..48fb0a94a4 --- /dev/null +++ b/packages/compiler/playground/yarn.lock @@ -0,0 +1,647 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" + integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@rollup/rollup-android-arm-eabi@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" + integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== + +"@rollup/rollup-android-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" + integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== + +"@rollup/rollup-darwin-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" + integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== + +"@rollup/rollup-darwin-x64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" + integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== + +"@rollup/rollup-linux-arm-gnueabihf@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" + integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== + +"@rollup/rollup-linux-arm-musleabihf@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" + integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== + +"@rollup/rollup-linux-arm64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" + integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== + +"@rollup/rollup-linux-arm64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" + integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== + +"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" + integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== + +"@rollup/rollup-linux-riscv64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" + integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== + +"@rollup/rollup-linux-s390x-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" + integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== + +"@rollup/rollup-linux-x64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" + integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== + +"@rollup/rollup-linux-x64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" + integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== + +"@rollup/rollup-win32-arm64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" + integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== + +"@rollup/rollup-win32-ia32-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" + integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== + +"@rollup/rollup-win32-x64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" + integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== + +"@tsconfig/node20@^20.1.4": + version "20.1.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928" + integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/node@^20.14.5": + version "20.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420" + integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg== + dependencies: + undici-types "~5.26.4" + +"@vitejs/plugin-vue@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz#e3dc11e427d4b818b7e3202766ad156e3d5e2eaa" + integrity sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ== + +"@volar/language-core@2.4.0-alpha.15", "@volar/language-core@~2.4.0-alpha.15": + version "2.4.0-alpha.15" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.0-alpha.15.tgz#d17dfac0014f5648dd9ccc090918795b03cde0e9" + integrity sha512-mt8z4Fm2WxfQYoQHPcKVjLQV6PgPqyKLbkCVY2cr5RSaamqCHjhKEpsFX66aL4D/7oYguuaUw9Bx03Vt0TpIIA== + dependencies: + "@volar/source-map" "2.4.0-alpha.15" + +"@volar/source-map@2.4.0-alpha.15": + version "2.4.0-alpha.15" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.0-alpha.15.tgz#b90dfd5a3ce30296dfcdcca647c6b41681b1b29b" + integrity sha512-8Htngw5TmBY4L3ClDqBGyfLhsB8EmoEXUH1xydyEtEoK0O6NX5ur4Jw8jgvscTlwzizyl/wsN1vn0cQXVbbXYg== + +"@volar/typescript@~2.4.0-alpha.15": + version "2.4.0-alpha.15" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.0-alpha.15.tgz#407e3ca2134188ab77a6c5505b9ccccb9465f3c2" + integrity sha512-U3StRBbDuxV6Woa4hvGS4kz3XcOzrWUKgFdEFN+ba1x3eaYg7+ytau8ul05xgA+UNGLXXsKur7fTUhDFyISk0w== + dependencies: + "@volar/language-core" "2.4.0-alpha.15" + path-browserify "^1.0.1" + vscode-uri "^3.0.8" + +"@vue/compiler-core@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.31.tgz#b51a76f1b30e9b5eba0553264dff0f171aedb7c6" + integrity sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/shared" "3.4.31" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.4.31", "@vue/compiler-dom@^3.4.0": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz#30961ca847f5d6ad18ffa26236c219f61b195f6b" + integrity sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ== + dependencies: + "@vue/compiler-core" "3.4.31" + "@vue/shared" "3.4.31" + +"@vue/compiler-sfc@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz#cc6bfccda17df8268cc5440842277f61623c591f" + integrity sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/compiler-core" "3.4.31" + "@vue/compiler-dom" "3.4.31" + "@vue/compiler-ssr" "3.4.31" + "@vue/shared" "3.4.31" + estree-walker "^2.0.2" + magic-string "^0.30.10" + postcss "^8.4.38" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz#f62ffecdf15bacb883d0099780cf9a1e3654bfc4" + integrity sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA== + dependencies: + "@vue/compiler-dom" "3.4.31" + "@vue/shared" "3.4.31" + +"@vue/language-core@2.0.26": + version "2.0.26" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.26.tgz#233793b2e0a9f33db6f4bdac030d9c164b3efc0f" + integrity sha512-/lt6SfQ3O1yDAhPsnLv9iSUgXd1dMHqUm/t3RctfqjuwQf1LnftZ414X3UBn6aXT4MiwXWtbNJ4Z0NZWwDWgJQ== + dependencies: + "@volar/language-core" "~2.4.0-alpha.15" + "@vue/compiler-dom" "^3.4.0" + "@vue/shared" "^3.4.0" + computeds "^0.0.1" + minimatch "^9.0.3" + muggle-string "^0.4.1" + path-browserify "^1.0.1" + vue-template-compiler "^2.7.14" + +"@vue/reactivity@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.31.tgz#eda80e90c4f9d7659efe1f5ed99c2dfdc9e93d77" + integrity sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q== + dependencies: + "@vue/shared" "3.4.31" + +"@vue/runtime-core@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.31.tgz#ad3a41ad76385c0429e3e4dbefb81918494e10cf" + integrity sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw== + dependencies: + "@vue/reactivity" "3.4.31" + "@vue/shared" "3.4.31" + +"@vue/runtime-dom@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz#bae7ad844f944af33699c73581bc36125bab96ce" + integrity sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw== + dependencies: + "@vue/reactivity" "3.4.31" + "@vue/runtime-core" "3.4.31" + "@vue/shared" "3.4.31" + csstype "^3.1.3" + +"@vue/server-renderer@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.31.tgz#bbe990f793c36d62d05bdbbaf142511d53e159fd" + integrity sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA== + dependencies: + "@vue/compiler-ssr" "3.4.31" + "@vue/shared" "3.4.31" + +"@vue/shared@3.4.31", "@vue/shared@^3.4.0": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.31.tgz#af9981f57def2c3f080c14bf219314fc0dc808a0" + integrity sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA== + +"@vue/tsconfig@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz#3124ec16cc0c7e04165b88dc091e6b97782fffa9" + integrity sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ== + +ansi-styles@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +computeds@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/computeds/-/computeds-0.0.1.tgz#215b08a4ba3e08a11ff6eee5d6d8d7166a97ce2e" + integrity sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +json-parse-even-better-errors@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" + integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== + +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + +minimatch@^9.0.0, minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +muggle-string@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328" + integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +npm-normalize-package-bin@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" + integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== + +npm-run-all2@^6.2.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/npm-run-all2/-/npm-run-all2-6.2.2.tgz#cd98d7c94dfa92e36724a1064609cca7a8991f5f" + integrity sha512-Q+alQAGIW7ZhKcxLt8GcSi3h3ryheD6xnmXahkMRVM5LYmajcUrSITm8h+OPC9RYWMV2GR0Q1ntTUCfxaNoOJw== + dependencies: + ansi-styles "^6.2.1" + cross-spawn "^7.0.3" + memorystream "^0.3.1" + minimatch "^9.0.0" + pidtree "^0.6.0" + read-package-json-fast "^3.0.2" + shell-quote "^1.7.3" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + +postcss@^8.4.38, postcss@^8.4.39: + version "8.4.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" + integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + +read-package-json-fast@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" + integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== + dependencies: + json-parse-even-better-errors "^3.0.0" + npm-normalize-package-bin "^3.0.0" + +rollup@^4.13.0: + version "4.18.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" + integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.18.0" + "@rollup/rollup-android-arm64" "4.18.0" + "@rollup/rollup-darwin-arm64" "4.18.0" + "@rollup/rollup-darwin-x64" "4.18.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" + "@rollup/rollup-linux-arm-musleabihf" "4.18.0" + "@rollup/rollup-linux-arm64-gnu" "4.18.0" + "@rollup/rollup-linux-arm64-musl" "4.18.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" + "@rollup/rollup-linux-riscv64-gnu" "4.18.0" + "@rollup/rollup-linux-s390x-gnu" "4.18.0" + "@rollup/rollup-linux-x64-gnu" "4.18.0" + "@rollup/rollup-linux-x64-musl" "4.18.0" + "@rollup/rollup-win32-arm64-msvc" "4.18.0" + "@rollup/rollup-win32-ia32-msvc" "4.18.0" + "@rollup/rollup-win32-x64-msvc" "4.18.0" + fsevents "~2.3.2" + +semver@^7.5.4: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +typescript@~5.4.0: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +vite@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2" + integrity sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.39" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vscode-uri@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + +vue-template-compiler@^2.7.14: + version "2.7.16" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz#c81b2d47753264c77ac03b9966a46637482bb03b" + integrity sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ== + dependencies: + de-indent "^1.0.2" + he "^1.2.0" + +vue-tsc@^2.0.21: + version "2.0.26" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.26.tgz#e071df725b02f1d72b3ef386518b2045a716d7c9" + integrity sha512-tOhuwy2bIXbMhz82ef37qeiaQHMXKQkD6mOF6CCPl3/uYtST3l6fdNyfMxipudrQTxTfXVPlgJdMENBFfC1CfQ== + dependencies: + "@volar/typescript" "~2.4.0-alpha.15" + "@vue/language-core" "2.0.26" + semver "^7.5.4" + +vue@^3.4.29: + version "3.4.31" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.31.tgz#83a3c4dab8302b0e974b0d4b92a2f6a6378ae797" + integrity sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ== + dependencies: + "@vue/compiler-dom" "3.4.31" + "@vue/compiler-sfc" "3.4.31" + "@vue/runtime-dom" "3.4.31" + "@vue/server-renderer" "3.4.31" + "@vue/shared" "3.4.31" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" diff --git a/packages/compiler/tsconfig.json b/packages/compiler/tsconfig.json new file mode 100644 index 0000000000..4a0781ba28 --- /dev/null +++ b/packages/compiler/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "esnext", + "strict": true, + "jsx": "preserve", + "importHelpers": true, + "moduleResolution": "node", + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "sourceMap": true, + "baseUrl": ".", + "allowJs": true, + "types": [ + "vite/client", + "jest", + "@testing-library/jest-dom" + ], + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + }, + "exclude": [ + "dist", + "node_modules" + ] +} diff --git a/packages/compiler/vite-plugin/index.ts b/packages/compiler/vite-plugin/index.ts new file mode 100644 index 0000000000..78c541ee87 --- /dev/null +++ b/packages/compiler/vite-plugin/index.ts @@ -0,0 +1,42 @@ +import { Plugin } from "vite" +import { devtools, PluginOptions as DevtoolsPluginOptions } from "../devtools" +import { cssLayers } from "../css-layers" + +type Options = { + /** @default true */ + devtools?: boolean | DevtoolsPluginOptions, + + /** + * Adds CSS layers to Vuestic UI + * Helps control the order of CSS in the final bundle + * + * @default false + * + * Add `vuestic.components` and `vuestic.styles` CSS layers + * + * @notice This will make Vuestic styles less important. Make sure you don't have any global conflicting styles. + * For example. tailwind have normalize css included. It may have higher priority than Vuestic styles and components might look broken. + */ + cssLayers?: boolean, +} + +export const vuestic = (options: Options = {}): Plugin[] => { + const extractOptions = (key: keyof Options) => { + // Build fails without as Record cast + return typeof options[key] === 'object' ? options[key] as Record : undefined + } + + const plugins = [] + + if (options.devtools !== false) { + console.log('Using vuestic:devtools') // TODO: Remove this log + plugins.push(devtools(extractOptions('devtools'))) + } + + if (options.cssLayers === true) { + console.log('Using vuestic:css-layers') // TODO: Remove this log + plugins.push(cssLayers) + } + + return plugins +} diff --git a/yarn.lock b/yarn.lock index 1334084fde..616d5b245c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -465,6 +465,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.1.tgz#1e416d3627393fab1cb5b0f2f1796a100ae9133a" integrity sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg== +"@babel/parser@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" + integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz#b645d9ba8c2bc5b7af50f0fe949f9edbeb07c8cf" @@ -4012,6 +4017,11 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.0.tgz#b88671c367284bb50b1cb6a842d7e0d98e9f4e55" integrity sha512-ih6xH1dOGBTP/RNc/QbnVLLa/s80SkXydI7tnyq+a2eIdd7gDsmSpD1Jz7T1oCa+GIsVTyQCR3cW0tlYB97iFg== +"@shikijs/core@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.10.1.tgz#f9d6001cb6f67ac1595b78d7af82e6153365e73f" + integrity sha512-qdiJS5a/QGCff7VUFIqd0hDdWly9rDp8lhVmXVrS11aazX8LOTRLHAXkkEeONNsS43EcCd7gax9LLoOz4vlFQA== + "@sigstore/bundle@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-2.2.0.tgz#e3f555a5c503fe176d8d1e0e829b00f842502e46" @@ -5608,7 +5618,18 @@ "@babel/parser" "^7.23.9" "@vue/compiler-sfc" "^3.4.15" -"@vue/compiler-core@3.4.21", "@vue/compiler-core@^3.0.0": +"@vue/compiler-core@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.31.tgz#b51a76f1b30e9b5eba0553264dff0f171aedb7c6" + integrity sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/shared" "3.4.31" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-core@^3.0.0": version "3.4.21" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.21.tgz#868b7085378fc24e58c9aed14c8d62110a62be1a" integrity sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og== @@ -5619,36 +5640,36 @@ estree-walker "^2.0.2" source-map-js "^1.0.2" -"@vue/compiler-dom@3.4.21", "@vue/compiler-dom@^3.2.0", "@vue/compiler-dom@^3.3.4", "@vue/compiler-dom@^3.4.0", "@vue/compiler-dom@^3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803" - integrity sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA== +"@vue/compiler-dom@3.4.21", "@vue/compiler-dom@3.4.31", "@vue/compiler-dom@^3.2.0", "@vue/compiler-dom@^3.3.4", "@vue/compiler-dom@^3.4.0", "@vue/compiler-dom@^3.4.21": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz#30961ca847f5d6ad18ffa26236c219f61b195f6b" + integrity sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ== dependencies: - "@vue/compiler-core" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-core" "3.4.31" + "@vue/shared" "3.4.31" "@vue/compiler-sfc@3.4.21", "@vue/compiler-sfc@^3.2.0", "@vue/compiler-sfc@^3.3.11", "@vue/compiler-sfc@^3.4.13", "@vue/compiler-sfc@^3.4.15", "@vue/compiler-sfc@^3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2" - integrity sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ== - dependencies: - "@babel/parser" "^7.23.9" - "@vue/compiler-core" "3.4.21" - "@vue/compiler-dom" "3.4.21" - "@vue/compiler-ssr" "3.4.21" - "@vue/shared" "3.4.21" + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz#cc6bfccda17df8268cc5440842277f61623c591f" + integrity sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/compiler-core" "3.4.31" + "@vue/compiler-dom" "3.4.31" + "@vue/compiler-ssr" "3.4.31" + "@vue/shared" "3.4.31" estree-walker "^2.0.2" - magic-string "^0.30.7" - postcss "^8.4.35" - source-map-js "^1.0.2" + magic-string "^0.30.10" + postcss "^8.4.38" + source-map-js "^1.2.0" -"@vue/compiler-ssr@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef" - integrity sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q== +"@vue/compiler-ssr@3.4.31": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz#f62ffecdf15bacb883d0099780cf9a1e3654bfc4" + integrity sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA== dependencies: - "@vue/compiler-dom" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-dom" "3.4.31" + "@vue/shared" "3.4.31" "@vue/devtools-api@^6.5.1": version "6.6.1" @@ -5740,44 +5761,45 @@ path-browserify "^1.0.1" vue-template-compiler "^2.7.14" -"@vue/reactivity@3.4.21", "@vue/reactivity@^3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.21.tgz#affd3415115b8ebf4927c8d2a0d6a24bccfa9f02" - integrity sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw== +"@vue/reactivity@3.4.31", "@vue/reactivity@^3.4.21": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.31.tgz#eda80e90c4f9d7659efe1f5ed99c2dfdc9e93d77" + integrity sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q== dependencies: - "@vue/shared" "3.4.21" + "@vue/shared" "3.4.31" "@vue/repl@^2.8.0": version "2.9.0" resolved "https://registry.yarnpkg.com/@vue/repl/-/repl-2.9.0.tgz#bc87593357784669e1201bf4bff813e74730f90b" integrity sha512-6gfklmJoQel9hNG2zdoyz/6WxgSJqhIx76vUUkXxegriTgkdKjBkRcm3khsW8FTkwMLkY6+NHaQPQRY6l7ovaA== -"@vue/runtime-core@3.4.21", "@vue/runtime-core@^3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz#3749c3f024a64c4c27ecd75aea4ca35634db0062" - integrity sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA== +"@vue/runtime-core@3.4.31", "@vue/runtime-core@^3.4.21": + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.31.tgz#ad3a41ad76385c0429e3e4dbefb81918494e10cf" + integrity sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw== dependencies: - "@vue/reactivity" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/reactivity" "3.4.31" + "@vue/shared" "3.4.31" "@vue/runtime-dom@3.4.21", "@vue/runtime-dom@^3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz#91f867ef64eff232cac45095ab28ebc93ac74588" - integrity sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw== + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz#bae7ad844f944af33699c73581bc36125bab96ce" + integrity sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw== dependencies: - "@vue/runtime-core" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/reactivity" "3.4.31" + "@vue/runtime-core" "3.4.31" + "@vue/shared" "3.4.31" csstype "^3.1.3" "@vue/server-renderer@3.4.21", "@vue/server-renderer@^3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz#150751579d26661ee3ed26a28604667fa4222a97" - integrity sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg== + version "3.4.31" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.31.tgz#bbe990f793c36d62d05bdbbaf142511d53e159fd" + integrity sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA== dependencies: - "@vue/compiler-ssr" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-ssr" "3.4.31" + "@vue/shared" "3.4.31" -"@vue/shared@3.4.21", "@vue/shared@^3.4.0", "@vue/shared@^3.4.21": +"@vue/shared@3.4.21", "@vue/shared@3.4.31", "@vue/shared@^3.4.0", "@vue/shared@^3.4.21": version "3.4.21" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1" integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g== @@ -13554,13 +13576,20 @@ magic-string@^0.26.3: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.30.0, magic-string@^0.30.2, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5, magic-string@^0.30.7, magic-string@^0.30.8: +magic-string@^0.30.0, magic-string@^0.30.2, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5, magic-string@^0.30.8: version "0.30.8" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + magicast@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.3.tgz#a15760f982deec9dabc5f314e318d7c6bddcb27b" @@ -15688,6 +15717,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -16373,7 +16407,7 @@ postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0. picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.2.1, postcss@^8.4.23, postcss@^8.4.26, postcss@^8.4.27, postcss@^8.4.33, postcss@^8.4.35, postcss@^8.4.36: +postcss@^8.2.1, postcss@^8.4.23, postcss@^8.4.26, postcss@^8.4.27, postcss@^8.4.33, postcss@^8.4.36: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -16382,6 +16416,15 @@ postcss@^8.2.1, postcss@^8.4.23, postcss@^8.4.26, postcss@^8.4.27, postcss@^8.4. picocolors "^1.0.0" source-map-js "^1.2.0" +postcss@^8.4.38: + version "8.4.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" + integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + preact@^10.0.0: version "10.20.1" resolved "https://registry.yarnpkg.com/preact/-/preact-10.20.1.tgz#1bc598ab630d8612978f7533da45809a8298542b" @@ -17983,6 +18026,13 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +shiki@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-1.10.1.tgz#5587f02c7577db018bb49f8bce3c971e157a5281" + integrity sha512-uafV7WCgN4YYrccH6yxpnps6k38sSTlFRrwc4jycWmhWxJIm9dPrk+XkY1hZ2t0I7jmacMNb15Lf2fspa/Y3lg== + dependencies: + "@shikijs/core" "1.10.1" + shortid@^2.2.8: version "2.2.16" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" From 6d752d90fe74441ce697ef297229480b65ff9c9c Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Mon, 15 Jul 2024 20:18:47 +0300 Subject: [PATCH 26/88] feat(devtools): improve dx and component tree parsing and docs (#4345) * feat(devtools): improve settings menu * chore: sync deps * feat(devtools): auto-save and file names * feat(devtools): add changes history * feat(compiler): improve history * fix: prevent cycle recalclations * feat(compiler): add text slot settings * fix(devtools): correct similar nodes * chore(devtools): minor improvements * docs(devtools): added devtools docs page * fix(devtools): use app tree instead of dom nodes * fix(input): inherit props * chore(devtools): improve dx * chore(devtools): improve dx * chore(devtools): remove extra files --- package.json | 15 +- packages/compiler/Readme.md | 33 +- .../devtools/client/parser/parseHTML.ts | 68 ---- .../devtools/client/parser/parseSource.ts | 31 +- .../devtools/client/parser/printHTML.ts | 122 ------- .../devtools/client/parser/printSource.ts | 17 +- .../devtools/client/parser/prittyfy.ts | 7 + .../compiler/devtools/client/ui/Devtools.vue | 140 ++++---- .../devtools/client/ui/components/AppTree.vue | 66 +++- .../client/ui/components/AppTreeItem.vue | 69 ++-- .../client/ui/components/ComponentView.vue | 132 ++++--- .../ui/components/ComponentViewSettings.vue | 180 ---------- .../ui/components/ComponentViewSource.vue | 64 ---- .../devtools/client/ui/components/History.vue | 94 +++++ .../client/ui/components/base/CodeView.vue | 137 +++++++- .../client/ui/components/base/Outline.vue | 12 +- .../component-options/ComponentProps.vue | 123 +++++++ .../component-options/ComponentSlots.vue | 24 ++ .../component-options/components-config.ts | 55 +++ .../component-options/options/Checkbox.vue | 54 +++ .../component-options/options/Color.vue | 61 ++++ .../component-options/options/MultiText.vue | 22 ++ .../options/NotAvaliable.vue | 26 ++ .../component-options/options/Number.vue | 61 ++++ .../ui/composables/base/defineGlobal.ts | 7 + .../ui/composables/base/useHasListener.ts | 9 + .../composables/base/useMutationObserver.ts | 2 +- .../client/ui/composables/useAppTree.ts | 129 ------- .../client/ui/composables/useAppTree/index.ts | 2 + .../ui/composables/useAppTree/useAppTree.ts | 222 ++++++++++++ .../useAppTree/useSelectedAppTreeItem.ts | 90 +++++ .../ui/composables/useAppVuesticConfig.ts | 32 ++ .../client/ui/composables/useComponent/api.ts | 22 ++ .../ui/composables/useComponent/index.ts | 3 + .../composables/useComponent/useComponent.ts | 148 ++++++++ .../useComponent/useComponentCode.ts | 232 +++++++++++++ .../useComponent/useComponentMeta.ts | 38 ++ .../useComponent/useComponentPaths.ts | 35 ++ .../useComponent/useComponentSource.ts | 56 +++ .../ui/composables/useHoveredElement.ts | 6 +- .../client/ui/composables/useVueElement.ts | 325 ------------------ packages/compiler/devtools/client/ui/index.ts | 26 +- .../client/ui/store/useTargetElementStore.ts | 8 - .../devtools/plugin/add-vue-plugin.ts | 3 +- packages/compiler/devtools/plugin/compiler.ts | 2 +- packages/compiler/devtools/plugin/plugin.ts | 28 +- packages/compiler/devtools/server/file.ts | 9 + .../devtools/server/server-middleware.ts | 18 +- packages/compiler/devtools/shared/slug.ts | 2 +- packages/compiler/package.json | 5 +- packages/compiler/playground/src/App.vue | 7 +- .../playground/src/pages/TestButton.vue | 11 + .../playground/src/pages/TestButtonBase.vue | 11 + .../playground/src/pages/TestPage.vue | 30 +- packages/compiler/playground/vite.config.ts | 2 +- packages/compiler/shared/color.ts | 19 + packages/compiler/tsconfig.json | 7 +- packages/compiler/vite-plugin/index.ts | 14 +- packages/docs/components/layout/Sidebar.vue | 10 +- .../page-config/compiler/devtools/index.ts | 52 +++ packages/docs/page-config/navigationRoutes.ts | 17 + packages/ui/.storybook/types.ts | 6 +- .../va-button-toggle/VaButtonToggle.vue | 2 +- .../src/components/va-counter/VaCounter.vue | 11 +- .../ui/src/components/va-input/VaInput.vue | 2 - .../composables/tests/createTestComposable.ts | 2 +- yarn.lock | 119 +++---- 67 files changed, 2188 insertions(+), 1206 deletions(-) delete mode 100644 packages/compiler/devtools/client/parser/parseHTML.ts delete mode 100644 packages/compiler/devtools/client/parser/printHTML.ts create mode 100644 packages/compiler/devtools/client/parser/prittyfy.ts delete mode 100644 packages/compiler/devtools/client/ui/components/ComponentViewSettings.vue delete mode 100644 packages/compiler/devtools/client/ui/components/ComponentViewSource.vue create mode 100644 packages/compiler/devtools/client/ui/components/History.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/ComponentProps.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/ComponentSlots.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/components-config.ts create mode 100644 packages/compiler/devtools/client/ui/components/component-options/options/Checkbox.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/options/Color.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/options/MultiText.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/options/NotAvaliable.vue create mode 100644 packages/compiler/devtools/client/ui/components/component-options/options/Number.vue create mode 100644 packages/compiler/devtools/client/ui/composables/base/defineGlobal.ts create mode 100644 packages/compiler/devtools/client/ui/composables/base/useHasListener.ts delete mode 100644 packages/compiler/devtools/client/ui/composables/useAppTree.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useAppTree/index.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useAppVuesticConfig.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/api.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/index.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/useComponentMeta.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/useComponentPaths.ts create mode 100644 packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts delete mode 100644 packages/compiler/devtools/client/ui/composables/useVueElement.ts delete mode 100644 packages/compiler/devtools/client/ui/store/useTargetElementStore.ts create mode 100644 packages/compiler/playground/src/pages/TestButton.vue create mode 100644 packages/compiler/playground/src/pages/TestButtonBase.vue create mode 100644 packages/compiler/shared/color.ts create mode 100644 packages/docs/page-config/compiler/devtools/index.ts diff --git a/package.json b/package.json index 8f770ebfa9..e50cdb07b5 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,13 @@ }, "resolutions": { "vue": "^3.4.21", - "@vue/shared": "^3.4.21", - "@vue/compiler-sfc": "^3.4.21", - "@vue/runtime-core": "^3.4.21", - "@vue/runtime-dom": "^3.4.21", - "@vue/reactivity": "^3.4.21", - "@vue/server-renderer": "^3.4.21", - "@vue/compiler-dom": "^3.4.21" + "@vue/shared": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/compiler-core": "3.4.21", + "@vue/runtime-core": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/reactivity": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/compiler-dom": "3.4.21" } } diff --git a/packages/compiler/Readme.md b/packages/compiler/Readme.md index 9e9fbc248a..97e9d7b1f4 100644 --- a/packages/compiler/Readme.md +++ b/packages/compiler/Readme.md @@ -2,17 +2,42 @@ Combination of bundling tools focusing on improving development experience when working with Vuestic UI +## Installation + +1. Install package + +```bash +npm i @vuestic/compiler@latest +``` + +2. Add `vuestic` plugin to vite config. + +`vite.config.ts` or `vite.config.js` +```js +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { vuestic } from '@vuestic/compiler/vite' + +export default defineConfig({ + plugins: [ + vuestic(), + vue(), + ], +}) +``` + +> Make sure to register vuestic plugin before `vue` + ## List of features ### Devtools Devtools designed for intuitive visual control over application with Vuestic components -#### Installation - -Install package +#### Usages -Add `@vuestic/compiler/devtools/vite` plugin to `vite.config.ts` +Run vite project in dev mode +Press ALT/Option + F12 in browser #### Plans diff --git a/packages/compiler/devtools/client/parser/parseHTML.ts b/packages/compiler/devtools/client/parser/parseHTML.ts deleted file mode 100644 index 7822668340..0000000000 --- a/packages/compiler/devtools/client/parser/parseHTML.ts +++ /dev/null @@ -1,68 +0,0 @@ -export class Node { - constructor( - public type: 'tag:open' | 'tag:close' | 'content', - public loc: { start: { offset: number }, end: { offset: number }, source: string } = { start: { offset: 0 }, end: { offset: 0 }, source: '' }, - public children: (Node | string)[] = [], - ) {} - - toString() { - return this.loc.source - } -} - -export const parseSource = (source: string) => { - const parse = (source: string): Node[] => { - const nodes: Node[] = [] - let current = 0 - - while (current < source.length) { - const startTag = source.indexOf('<', current) - - if (startTag === -1) { break } - - const endTag = source.indexOf('>', startTag) - - if (endTag === -1) { break } - - const tag = source.slice(startTag, endTag + 1) - const isSelfClosing = tag.endsWith('/>') - - if (isSelfClosing) { - nodes.push(new Node('content', { start: { offset: startTag }, end: { offset: endTag + 1 }, source: tag })) - } else { - const startTag = source.indexOf('<', current) - const endTag = source.indexOf('>', startTag) - const tag = source.slice(startTag, endTag + 1) - - nodes.push(new Node( - 'content', - { start: { offset: startTag }, end: { offset: endTag + 1}, source: tag } - )) - } - - current = endTag + 1 - } - - if (nodes.length > 1) { - const startTag = nodes[0] - const endTag = nodes[nodes.length - 1] - startTag.type = 'tag:open' - endTag.type = 'tag:close' - const text = new Node( - 'content', - { - start: { offset: startTag.loc.end.offset }, - end: { offset: endTag.loc.start.offset }, - source: source.slice(startTag.loc.end.offset, endTag.loc.start.offset), - }, - parse(source.slice(startTag.loc.end.offset, endTag.loc.start.offset)), - ) - - return [startTag, text, endTag] - } - - return nodes - } - - return parse(source) -} \ No newline at end of file diff --git a/packages/compiler/devtools/client/parser/parseSource.ts b/packages/compiler/devtools/client/parser/parseSource.ts index 6059626474..a5fabfa5ae 100644 --- a/packages/compiler/devtools/client/parser/parseSource.ts +++ b/packages/compiler/devtools/client/parser/parseSource.ts @@ -9,13 +9,13 @@ const getTagContent = (source: string) => { const parseOpenTag = (source: string) => { source = source.trim().replace(/\n/g, '') let tagName = '' - const rawAttributes: Record = {} + const attributes: Record = {} let tagContent = getTagContent(source) if (!tagContent.includes(' ')) { tagName = tagContent - return { tagName, rawAttributes } + return { tagName, attributes } } tagContent += ' ' @@ -30,7 +30,8 @@ const parseOpenTag = (source: string) => { i++ let key = '' - let value: string = '' + // Might not have value + let value: string | null = null let isInQuotes = false while (i < tagContent.length) { @@ -43,16 +44,19 @@ const parseOpenTag = (source: string) => { if (tagContent[i] === ' ' && !isInQuotes) { // Key might be empty if there are multiple spaces or \n if (key !== '') { - rawAttributes[key] = value === '' ? (true as const) : value + attributes[key] = value } key = '' - value = '' + value = null i++ continue } if (tagContent[i] === '=') { i++ + // If have equal sign, means it must have value in qoutes later + // May be case where user haven't finished typing like `to=` - we show empty string + value = '' continue } @@ -65,7 +69,7 @@ const parseOpenTag = (source: string) => { i++ } - return { tagName, rawAttributes } + return { tagName, attributes } } export type Loc = { @@ -83,7 +87,8 @@ export type HTMLContentNode = { export type HTMLElementNode = { type: 'element' tag: string - attributes: Record + /** null if no attribute value */ + attributes: Record parent: HTMLElementNode | HTMLRootNode children: (HTMLElementNode | HTMLContentNode)[] sourcePath?: string @@ -198,13 +203,13 @@ const tokensToTree = (tokens: HTMLToken[]) => { throw new Error('Unexpected error when parsing HTML') } - const { rawAttributes } = parseOpenTag(token.loc.source) + const { attributes } = parseOpenTag(token.loc.source) const node: HTMLElementNode = { type: 'element', tag: token.tag, - attributes: rawAttributes, children: [], + attributes, parent, } @@ -238,13 +243,13 @@ const tokensToTree = (tokens: HTMLToken[]) => { throw new Error('Unexpected error when parsing HTML') } - const { rawAttributes, tagName } = parseOpenTag(token.loc.source) + const { attributes, tagName } = parseOpenTag(token.loc.source) parent.children.push({ type: 'element', tag: tagName, - attributes: rawAttributes, children: [], + attributes, parent, }) } @@ -253,9 +258,9 @@ const tokensToTree = (tokens: HTMLToken[]) => { return root } -export const parseSource = (source: string) => { +export const parseSource = (source: string) => { const tokens = parseTokens(source) const tree = tokensToTree(tokens) return tree -} \ No newline at end of file +} diff --git a/packages/compiler/devtools/client/parser/printHTML.ts b/packages/compiler/devtools/client/parser/printHTML.ts deleted file mode 100644 index 0a8cd23ac4..0000000000 --- a/packages/compiler/devtools/client/parser/printHTML.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Node } from './parseHTML' - -const isVBind = (key: string) => key.startsWith(':') || key.startsWith('v-bind:') - -const getKeyName = (key: string) => { - if (key.startsWith(':')) { - return key.slice(1) - } - - if (key.startsWith('v-bind:')) { - return key.slice(7) - } - - return key -} - -const parseOpenTag = (source: string) => { - let tagName = '' - const rawAttributes: Record = {} - - let tagContent = source.slice(1, -1) - - if (!tagContent.includes(' ')) { - tagName = tagContent - return { tagName, rawAttributes } - } - - tagContent += ' ' - - let i = 0 - - while (tagContent[i] !== ' ') { - tagName += tagContent[i] - i++ - } - - i++ - - let key = '' - let value = '' - let isInQuotes = false - - while (i < tagContent.length) { - if (tagContent[i] === '"') { - isInQuotes = !isInQuotes - i++ - continue - } - - if (tagContent[i] === ' ' && !isInQuotes) { - rawAttributes[key] = value ?? 'true' - key = '' - value = '' - i++ - continue - } - - if (tagContent[i] === '=') { - i++ - continue - } - - if (isInQuotes) { - value += tagContent[i] - } else { - key += tagContent[i] - } - - i++ - } - - return { tagName, rawAttributes } -} - -export const printOpenTag = (node: Node, newAttributes: Record, propsMeta?: Record) => { - const { source } = node.loc - - if (!(source.startsWith('<') && source.endsWith('>'))) { - return source - } - - const { tagName, rawAttributes } = parseOpenTag(source) - - const newAttributesKeys = Object.keys(newAttributes) - - const attributes = Object.entries(rawAttributes).reduce((acc, [rawKey, value]) => { - const key = getKeyName(rawKey) - - if (newAttributesKeys.includes(key)) { - return acc - } - - acc[rawKey] = value ? value : true - - return acc - }, {} as Record) - - const attributesString = Object - .entries({ ...attributes, ...newAttributes }) - .reduce((acc, [key, value]) => { - if (propsMeta && propsMeta[key] && propsMeta[key].default === value) { - return acc - } - - // TODO: Check default value for boolean attributes - if (value === true) { - return `${acc} ${key}` - } - - if (value === false) { - return `${acc} :${key}="false"` - } - - return `${acc} ${key}="${value}"` - }, '') - - if (attributesString.length > 0) { - return `<${tagName}${attributesString}>` - } - - return `<${tagName}>` -} \ No newline at end of file diff --git a/packages/compiler/devtools/client/parser/printSource.ts b/packages/compiler/devtools/client/parser/printSource.ts index 0c56c1eb2d..de5896373c 100644 --- a/packages/compiler/devtools/client/parser/printSource.ts +++ b/packages/compiler/devtools/client/parser/printSource.ts @@ -7,18 +7,19 @@ export const printSource = (source: HTMLRootNode | HTMLElementNode | HTMLContent const print = (node: HTMLRootNode | HTMLElementNode) => { let result = '' - + for (const child of node.children) { if ('text' in child) { - result += child.text.split('\n').map((line) => printTabs() + line).join('\n') + result += child.text.split('\n').filter((line) => line.trim() !== '').map((line) => printTabs() + line.trim()).join('\n') + '\n' } else { result += printTabs() + `<${child.tag}` const attributesCount = Object.keys(child.attributes).length - + if (attributesCount === 1) { const [key, value] = Object.entries(child.attributes)[0] - if (value === true) { + + if (value === null) { result += ` ${key}` } else { result += ` ${key}="${value}"` @@ -27,7 +28,7 @@ export const printSource = (source: HTMLRootNode | HTMLElementNode | HTMLContent result += '\n' tabSize += 2 for (const [key, value] of Object.entries(child.attributes)) { - if (value === true) { + if (value === null) { result += printTabs() + ` ${key}` } else { result += printTabs() + ` ${key}="${value}"` @@ -54,13 +55,13 @@ export const printSource = (source: HTMLRootNode | HTMLElementNode | HTMLContent result += '\n' + printTabs() + `\n` } } - + return result } if (source.type === 'content') { return source.text } - + return print(source) -} \ No newline at end of file +} diff --git a/packages/compiler/devtools/client/parser/prittyfy.ts b/packages/compiler/devtools/client/parser/prittyfy.ts new file mode 100644 index 0000000000..80ff35e26f --- /dev/null +++ b/packages/compiler/devtools/client/parser/prittyfy.ts @@ -0,0 +1,7 @@ +import { parseSource } from "./parseSource" +import { printSource } from "./printSource" + +export const prettify = (source: string) => { + const parsed = parseSource(source) + return printSource(parsed) +} \ No newline at end of file diff --git a/packages/compiler/devtools/client/ui/Devtools.vue b/packages/compiler/devtools/client/ui/Devtools.vue index 9db0236964..22079d5880 100644 --- a/packages/compiler/devtools/client/ui/Devtools.vue +++ b/packages/compiler/devtools/client/ui/Devtools.vue @@ -1,61 +1,63 @@ - diff --git a/packages/compiler/devtools/client/ui/components/AppTree.vue b/packages/compiler/devtools/client/ui/components/AppTree.vue index c82c711043..05b5c7b0da 100644 --- a/packages/compiler/devtools/client/ui/components/AppTree.vue +++ b/packages/compiler/devtools/client/ui/components/AppTree.vue @@ -1,14 +1,68 @@ + + diff --git a/packages/compiler/devtools/client/ui/components/AppTreeItem.vue b/packages/compiler/devtools/client/ui/components/AppTreeItem.vue index d30f8f9edd..9c6978892e 100644 --- a/packages/compiler/devtools/client/ui/components/AppTreeItem.vue +++ b/packages/compiler/devtools/client/ui/components/AppTreeItem.vue @@ -1,7 +1,8 @@