diff --git a/.github/ISSUE_TEMPLATE/bug-template.md b/.github/ISSUE_TEMPLATE/bug-template.md
index c3d9c85583..1cce22b15b 100644
--- a/.github/ISSUE_TEMPLATE/bug-template.md
+++ b/.github/ISSUE_TEMPLATE/bug-template.md
@@ -6,7 +6,7 @@ assignees: ''
---
-**Vuestic-ui version:** 1.9.12
+**Vuestic-ui version:** 1.10.2
### Description
diff --git a/.gitignore b/.gitignore
index 806b0de0bc..619451ca64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
+
+# Local Netlify folder
+.netlify
diff --git a/.nvmrc b/.nvmrc
index 6a41e95fc6..fb3e6603b5 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v18.20.2
+v18.20.2
diff --git a/README.md b/README.md
index 7a3972428d..ed41ddc72a 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,12 @@
+
+
+
+
+
+
Documentation
|
@@ -66,6 +72,8 @@ Documentation, guides, examples and tutorials are available on [ui.vuestic.dev](
+
+
Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test using all possible browsers.
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and prevent visual regressions.
@@ -107,7 +115,12 @@ Say hi: hello@epicmax.co. We will be happy
[Other work](https://epicmax.co) we’ve done 🤘
-[Meet the Team](https://vuestic.dev/team)
+[Meet the Team](https://ui.vuestic.dev/introduction/team)
+
+### Premium Support and Consulting
+Get Premium Support & Consulting services through our official development partner, Epicmax. As the main contributor to Vuestic UI and Vuestic Admin, Epicmax brings a wealth of expertise and experience to help you achieve your project goals efficiently and effectively.
+
+[Get a quote](https://www.epicmax.co/?ref=vuestic-consulting)
### Follow us
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/bundlers-tests/package.json b/packages/bundlers-tests/package.json
index c80cb49320..0792fb68a8 100644
--- a/packages/bundlers-tests/package.json
+++ b/packages/bundlers-tests/package.json
@@ -3,20 +3,20 @@
"private": true,
"version": "0.0.0",
"scripts": {
- "build:local-packages-18": "docker image prune -f && docker container prune -f && docker-compose --profile local-packages-18 build --no-cache",
- "build:local-packages-lts": "docker image prune -f && docker container prune -f && docker-compose --profile local-packages-lts build --no-cache",
+ "build:local-packages-18": "docker image prune -f && docker container prune -f && docker compose --profile local-packages-18 build --no-cache",
+ "build:local-packages-lts": "docker image prune -f && docker container prune -f && docker compose --profile local-packages-lts build --no-cache",
"build:test-page": "tsx ./kitchensink-builder/create-test-page.ts",
- "run:vite": "docker-compose --profile run-vite up",
- "run:webpack": "docker-compose --profile run-webpack up",
- "run:nuxt-ssr": "docker-compose --profile nuxt-ssr up",
- "run:nuxt-spa": "docker-compose --profile nuxt-spa up",
- "test:vite": "docker-compose --profile vite up --build --exit-code-from cypress_vite",
- "test:vite-cjs": "docker-compose --profile vite-cjs up --build --exit-code-from cypress_vite_cjs",
- "test:cli": "docker-compose --profile cli up --build --exit-code-from cypress_cli",
+ "run:vite": "docker compose --profile run-vite up",
+ "run:webpack": "docker compose --profile run-webpack up",
+ "run:nuxt-ssr": "docker compose --profile nuxt-ssr up",
+ "run:nuxt-spa": "docker compose --profile nuxt-spa up",
+ "test:vite": "docker compose --profile vite up --build --exit-code-from cypress_vite",
+ "test:vite-cjs": "docker compose --profile vite-cjs up --build --exit-code-from cypress_vite_cjs",
+ "test:cli": "docker compose --profile cli up --build --exit-code-from cypress_cli",
"test:nuxt": "yarn test:nuxt-ssr",
- "test:webpack": "docker-compose --profile webpack up --build --exit-code-from cypress_webpack",
- "test:nuxt-ssr": "docker-compose --profile nuxt-ssr up --build --exit-code-from cypress_nuxt_ssr",
- "test:nuxt-spa": "docker-compose --profile nuxt-spa up --build --exit-code-from cypress_nuxt_spa",
+ "test:webpack": "docker compose --profile webpack up --build --exit-code-from cypress_webpack",
+ "test:nuxt-ssr": "docker compose --profile nuxt-ssr up --build --exit-code-from cypress_nuxt_ssr",
+ "test:nuxt-spa": "docker compose --profile nuxt-spa up --build --exit-code-from cypress_nuxt_spa",
"test": "yarn build:test-page && yarn build:local-packages-18 && yarn build:local-packages-lts && yarn test:nuxt && yarn test:vite && yarn test:18",
"test:18": "yarn build:local-packages-18 && yarn test:vite"
}
diff --git a/packages/compiler/Readme.md b/packages/compiler/Readme.md
new file mode 100644
index 0000000000..97e9d7b1f4
--- /dev/null
+++ b/packages/compiler/Readme.md
@@ -0,0 +1,59 @@
+# Vuestic Plugin
+
+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
+
+#### Usages
+
+Run vite project in dev mode
+Press ALT/Option + F12 in browser
+
+#### 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/parseSource.ts b/packages/compiler/devtools/client/parser/parseSource.ts
new file mode 100644
index 0000000000..a5fabfa5ae
--- /dev/null
+++ b/packages/compiler/devtools/client/parser/parseSource.ts
@@ -0,0 +1,266 @@
+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 attributes: Record = {}
+
+ let tagContent = getTagContent(source)
+
+ if (!tagContent.includes(' ')) {
+ tagName = tagContent
+ return { tagName, attributes }
+ }
+
+ tagContent += ' '
+
+ let i = 0
+
+ while (tagContent[i] !== ' ') {
+ tagName += tagContent[i]
+ i++
+ }
+
+ i++
+
+ let key = ''
+ // Might not have value
+ let value: string | null = null
+ 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 !== '') {
+ attributes[key] = value
+ }
+ key = ''
+ 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
+ }
+
+ if (isInQuotes) {
+ value += tagContent[i]
+ } else {
+ key += tagContent[i]
+ }
+
+ i++
+ }
+
+ return { tagName, attributes }
+}
+
+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
+ /** null if no attribute value */
+ 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 content = source.slice(current, startTag)
+
+ if (isValidContent(content)) {
+ tokens.push({
+ type: 'content',
+ loc: {
+ start: { offset: current },
+ end: { offset: startTag },
+ source: source.slice(current, startTag),
+ },
+ })
+ }
+
+ if (isSelfClosing) {
+ tokens.push({
+ type: 'tag:self-closing',
+ loc: {
+ start: { offset: startTag },
+ end: { offset: endTag + 1 },
+ source: tagContent,
+ },
+ tag: tagContent.slice(1, -2),
+ })
+ } else if (isClosingTag) {
+ tokens.push({
+ type: 'tag:close',
+ loc: {
+ start: { offset: startTag },
+ end: { offset: endTag + 1 },
+ source: tagContent,
+ },
+ tag: tagContent.slice(2, -1),
+ })
+ } else {
+ tokens.push({
+ type: 'tag:open',
+ loc: {
+ start: { offset: startTag },
+ end: { offset: endTag + 1 },
+ source: tagContent,
+ },
+ tag: superTrim(tagContent.slice(1, -1).split(' ')[0]),
+ })
+ }
+
+ current = endTag + 1
+ }
+
+ return tokens
+}
+
+const tokensToTree = (tokens: HTMLToken[]) => {
+ 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 { attributes } = parseOpenTag(token.loc.source)
+
+ const node: HTMLElementNode = {
+ type: 'element',
+ tag: token.tag,
+ children: [],
+ attributes,
+ 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 { attributes, tagName } = parseOpenTag(token.loc.source)
+
+ parent.children.push({
+ type: 'element',
+ tag: tagName,
+ children: [],
+ attributes,
+ parent,
+ })
+ }
+ }
+
+ return root
+}
+
+export const parseSource = (source: string) => {
+ const tokens = parseTokens(source)
+ const tree = tokensToTree(tokens)
+
+ return tree
+}
diff --git a/packages/compiler/devtools/client/parser/printSource.ts b/packages/compiler/devtools/client/parser/printSource.ts
new file mode 100644
index 0000000000..de5896373c
--- /dev/null
+++ b/packages/compiler/devtools/client/parser/printSource.ts
@@ -0,0 +1,67 @@
+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').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 === null) {
+ 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 === null) {
+ 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() + `${child.tag}>\n`
+ }
+ }
+
+ return result
+ }
+
+ if (source.type === 'content') {
+ return source.text
+ }
+
+ return print(source)
+}
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/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..fa01900216
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/Devtools.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/AppToolbar.vue b/packages/compiler/devtools/client/ui/components/AppToolbar.vue
new file mode 100644
index 0000000000..c0530eb63d
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/AppToolbar.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..068ba8a672
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/AppTree.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
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..5d071d4f98
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/AppTreeItem.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+ {{ props.item.name }}
+
+ (repeated)
+
+
+
+
+
+
+
+
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..3ef338fd94
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/ComponentView.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ tab.name }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/History.vue b/packages/compiler/devtools/client/ui/components/History.vue
new file mode 100644
index 0000000000..8955793e0b
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/History.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
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..9053b5c721
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/base/CodeView.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
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..d05bf27bbb
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/base/DraggableWindow.vue
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
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..84835bff1f
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/base/Outline.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ Current source
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/compiler/devtools/client/ui/components/base/Toolbar.vue b/packages/compiler/devtools/client/ui/components/base/Toolbar.vue
new file mode 100644
index 0000000000..99544a825d
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/base/Toolbar.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/ComponentFile.vue b/packages/compiler/devtools/client/ui/components/component-options/ComponentFile.vue
new file mode 100644
index 0000000000..d0e568d6ab
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/ComponentFile.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ {{ item.name }}
+
+ /
+
+
+
+ selectAppTreeItem(item)"
+ />
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/ComponentProps.vue b/packages/compiler/devtools/client/ui/components/component-options/ComponentProps.vue
new file mode 100644
index 0000000000..1e79fb6e73
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/ComponentProps.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ prop.name }}
+
+ This prop can be edited only in Source Code tab for now.
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/ComponentSlots.vue b/packages/compiler/devtools/client/ui/components/component-options/ComponentSlots.vue
new file mode 100644
index 0000000000..7e564ca166
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/ComponentSlots.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue b/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue
new file mode 100644
index 0000000000..537068f6ab
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/components-config.ts b/packages/compiler/devtools/client/ui/components/component-options/components-config.ts
new file mode 100644
index 0000000000..a45488866f
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/components-config.ts
@@ -0,0 +1,55 @@
+type PropConfigSelect = {
+ type: 'select',
+ options: string[]
+}
+
+type PropConfigText = {
+ type: 'text'
+}
+
+type PropConfigMultiText = {
+ type: 'multi-text'
+}
+
+type PropConfigNumber = {
+ type: 'number'
+}
+
+type PropConfigColor = {
+ type: 'color'
+}
+
+type PropConfigCheckbox = {
+ type: 'checkbox'
+}
+
+type PropConfigDisabled = {
+ type: 'disabled'
+}
+
+type PropConfig = PropConfigSelect | PropConfigText | PropConfigNumber | PropConfigColor | PropConfigMultiText | PropConfigCheckbox | PropConfigDisabled
+type PropsConfig = Record
+
+const globalConfig: PropsConfig = {
+ color: { type: 'color' },
+ textColor: { type: 'color' },
+ background: { type: 'color' },
+ backgroundColor: { type: 'color' },
+ stripeColor: { type: 'color' },
+ to: { type: 'text' },
+ preset: { type: 'multi-text' },
+ sizesConfig: { type: 'disabled' },
+ fontSizesConfig: { type: 'disabled'}
+}
+
+const componentsConfig: Record = {
+
+}
+
+export const getPropConfig = (component: string, prop: string): PropConfig | undefined => {
+ if (componentsConfig[component] && componentsConfig[component][prop]) {
+ return componentsConfig[component][prop]
+ }
+
+ return globalConfig[prop]
+}
diff --git a/packages/compiler/devtools/client/ui/components/component-options/options/Checkbox.vue b/packages/compiler/devtools/client/ui/components/component-options/options/Checkbox.vue
new file mode 100644
index 0000000000..623662cfb4
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/options/Checkbox.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
{{ props.label }}
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/options/Color.vue b/packages/compiler/devtools/client/ui/components/component-options/options/Color.vue
new file mode 100644
index 0000000000..b95b422939
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/options/Color.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ {{ option }}
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/options/MultiText.vue b/packages/compiler/devtools/client/ui/components/component-options/options/MultiText.vue
new file mode 100644
index 0000000000..15c532e214
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/options/MultiText.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/component-options/options/NotAvaliable.vue b/packages/compiler/devtools/client/ui/components/component-options/options/NotAvaliable.vue
new file mode 100644
index 0000000000..7d04b5aacc
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/options/NotAvaliable.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
{{ props.label }}
+
+ Not avaliable
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue b/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue
new file mode 100644
index 0000000000..ea9d3a1c13
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/layout-editor/LayoutEditor.vue b/packages/compiler/devtools/client/ui/components/layout-editor/LayoutEditor.vue
new file mode 100644
index 0000000000..a9f5a08173
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/layout-editor/LayoutEditor.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue
new file mode 100644
index 0000000000..99589a20c2
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue
new file mode 100644
index 0000000000..b869ed91fd
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue b/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue
new file mode 100644
index 0000000000..79c588e4ae
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue b/packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue
new file mode 100644
index 0000000000..008cb1c76a
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/BasicDiv.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/BasicDiv.vue
new file mode 100644
index 0000000000..f5d61e6d05
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/BasicDiv.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue
new file mode 100644
index 0000000000..61644690f0
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Button
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue
new file mode 100644
index 0000000000..a5f008bf88
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Button 1
+ Button 2
+ Button 3
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue
new file mode 100644
index 0000000000..d5d1fbde9e
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue
new file mode 100644
index 0000000000..86eb14ce0a
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue
new file mode 100644
index 0000000000..5a3d5c8e8d
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue
new file mode 100644
index 0000000000..a0d410eaf6
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue
new file mode 100644
index 0000000000..6868687a43
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue
@@ -0,0 +1,14 @@
+
+
+
+
+ KT
+
+
+
diff --git a/packages/compiler/devtools/client/ui/components/toolbar/types.ts b/packages/compiler/devtools/client/ui/components/toolbar/types.ts
new file mode 100644
index 0000000000..9308de8ca3
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/components/toolbar/types.ts
@@ -0,0 +1,12 @@
+export type HumanState = {
+ direction: 'horizontal' | 'vertical',
+ horizontal: 'left' | 'center' | 'right',
+ vertical: 'top' | 'center' | 'bottom',
+}
+
+export type FlexState = {
+ display: 'flex',
+ 'flex-direction': 'row' | 'column',
+ 'justify-content': 'flex-start' | 'center' | 'flex-end',
+ 'align-items': 'flex-start' | 'center' | 'flex-end',
+}
diff --git a/packages/compiler/devtools/client/ui/composables/base/defineGlobal.ts b/packages/compiler/devtools/client/ui/composables/base/defineGlobal.ts
new file mode 100644
index 0000000000..a51ecc8426
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/base/defineGlobal.ts
@@ -0,0 +1,7 @@
+// TODO: MAybe use pinia here
+/** Make composable values global */
+export const defineGlobal = (composable: () => R): () => R => {
+ const result = composable()
+
+ return () => result
+}
diff --git a/packages/compiler/devtools/client/ui/composables/base/useAsyncComputed.ts b/packages/compiler/devtools/client/ui/composables/base/useAsyncComputed.ts
new file mode 100644
index 0000000000..355c9c607a
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/base/useAsyncComputed.ts
@@ -0,0 +1,39 @@
+import { ref, watchEffect, computed, type UnwrapRef } from 'vue'
+
+/**
+ * @notice not optimized for first load
+ */
+export const useAsyncComputed = (fn: () => Promise, defaultValue: D) => {
+ const value = ref(defaultValue)
+ const isLoading = ref(false)
+ const error = ref(null)
+
+ const load = async () => {
+ try {
+ isLoading.value = true
+ value.value = await fn() as UnwrapRef
+ } catch (e) {
+ if (e instanceof Error) {
+ error.value = e
+ }
+ } finally {
+ isLoading.value = false
+ }
+ }
+
+ watchEffect(() => {
+ load()
+ })
+
+ const comp = computed(() => {
+ return value.value
+ })
+
+ Object.defineProperty(comp, 'loading', {
+ get() {
+ return isLoading.value
+ },
+ })
+
+ return comp as typeof comp & { loading: boolean }
+}
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/useHasListener.ts b/packages/compiler/devtools/client/ui/composables/base/useHasListener.ts
new file mode 100644
index 0000000000..1645a84e01
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/base/useHasListener.ts
@@ -0,0 +1,9 @@
+import { getCurrentInstance, toHandlerKey } from 'vue'
+
+export const useHasListener = (eventName: string) => {
+ const vm = getCurrentInstance()!
+
+ const props = vm.vnode?.props
+
+ return props && toHandlerKey(eventName) in props
+}
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..633e18e45c
--- /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..82a696fad0
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useAppTransform.ts
@@ -0,0 +1,86 @@
+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) => {
+ // right button
+ if (event.button === 2) {
+ pressed = true
+ }
+ }
+
+ const onMouseUp = (event: MouseEvent) => {
+ // right button
+ if (event.button === 2) {
+ pressed = false
+ }
+ }
+
+ const onBlur = () => {
+ pressed = false
+ }
+
+ const onContextMenu = (event: MouseEvent) => {
+ event.preventDefault()
+ }
+
+ onMounted(() => {
+ window.addEventListener('blur', onBlur)
+ })
+
+ onBeforeUnmount(() => {
+ window.removeEventListener('blur', onBlur)
+ })
+
+ return {
+ zoom,
+ listeners: {
+ wheel: onWheel,
+ mousemove: onMouseMove,
+ mousedown: onMouseDown,
+ mouseup: onMouseUp,
+ contextmenu: onContextMenu,
+ },
+ zoomIn,
+ zoomOut,
+ translate,
+ }
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts
new file mode 100644
index 0000000000..b2c654b13d
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts
@@ -0,0 +1 @@
+export * from './useAppTree'
diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts
new file mode 100644
index 0000000000..68a3f54a74
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts
@@ -0,0 +1,289 @@
+import { onMounted, VNode, VNodeNormalizedChildren, VNodeArrayChildren, isVNode, Fragment, App, ref } from 'vue';
+import { PREFIX } from '../../../../shared/CONST';
+import { useSelectedAppTreeItem } from './useSelectedAppTreeItem';
+
+export type AppTreeItemComponent = {
+ ids: string[],
+ name: string,
+ el: Element | null,
+ vnode: VNode,
+ children: AppTreeItem[],
+ repeated?: number,
+ repeatedElements?: Element[],
+}
+
+export type AppTreeItemText = {
+ text: string
+}
+
+export type AppTreeItem = AppTreeItemComponent | AppTreeItemText
+
+const isDevtoolsComponent = (vnode: VNode) => {
+ const el = vnode.el as Element | null
+
+ if (!el) {
+ return false
+ }
+
+ if ('dataset' in el) {
+ return Object.keys((el as HTMLElement).dataset).includes(PREFIX)
+ }
+
+ return false
+}
+
+const getItemName = (vnode: VNode) => {
+ if (typeof vnode.type === 'string') {
+ return vnode.type
+ }
+
+ if (typeof vnode.type === 'object') {
+ if ('name' in vnode.type) {
+ return vnode.type.name
+ }
+
+ if ('__name' in vnode.type) {
+ return vnode.type.__name
+ }
+ }
+}
+
+const getItemChildren = (vnode: VNode) => {
+ if (vnode.component) {
+ if ((vnode.type as any).__hmrId) {
+ return [(vnode.component.subTree)]
+ }
+
+ return getItemChildren(vnode.component.subTree)
+ }
+
+ if (Array.isArray(vnode.children)) {
+ return vnode.children
+ }
+
+ if (vnode.children === null) {
+ return null
+ }
+
+ if (typeof vnode.children === 'object') {
+ // throw new Error('Object children not supported')
+ return null
+ }
+
+ return [vnode.children]
+}
+
+const getAppTree = async () => {
+ const app = document.querySelector('#app')
+
+ if (!app) { throw new Error('App element not found when building app tree') }
+
+ const traverseChildren = (vnode: VNodeNormalizedChildren): AppTreeItem[] | AppTreeItem => {
+ if (vnode === null) {
+ return [] as AppTreeItem[]
+ }
+
+ if (Array.isArray(vnode)) {
+ return vnode
+ .filter((vnode) => !!vnode)
+ .map((vnode) => {
+ if (Array.isArray(vnode)) {
+ return traverseChildren(vnode)
+ }
+
+ if (isVNode(vnode)) {
+ return traverse(vnode)
+ }
+
+ return {
+ text: String(vnode),
+ }
+ })
+ .flat()
+ }
+
+ return {
+ text: String(vnode),
+ }
+ }
+
+ const traverse = (vnode: VNode): AppTreeItem | AppTreeItem[] => {
+ if (vnode.type === Fragment) {
+ return traverseChildren(vnode.children)
+ }
+
+ const name = getItemName(vnode)
+
+ if (!isDevtoolsComponent(vnode)) {
+ return traverseChildren(getItemChildren(vnode))
+ }
+
+
+ const ids = vnode.props && Object
+ .keys(vnode.props)
+ .filter((p) => p.startsWith(`data-${PREFIX}-`))
+ .map((p) => p.replace(`data-`, ''))
+
+ if (!ids) {
+ return traverseChildren(getItemChildren(vnode))
+ }
+
+ const removeInheritedId = (children: AppTreeItem[]) => {
+ if (ids) {
+ children.forEach((child) => {
+ if ('ids' in child) {
+ child.ids = child.ids.filter((id) => !ids.includes(id))
+ }
+ })
+ }
+
+ const grouped = [] as AppTreeItem[]
+
+ children.forEach((child) => {
+ if ('ids' in child) {
+ const [id] = child.ids
+
+ const existing = grouped.find((group) => 'ids' in group && group.ids.includes(id)) as AppTreeItemComponent | undefined
+
+ if (!existing) {
+ grouped.push(child)
+ } else {
+ existing.repeated = (existing.repeated || 1) + 1
+ existing.repeatedElements = existing.repeatedElements || []
+ existing.repeatedElements.push(child.el as Element)
+ }
+ } else {
+ grouped.push(child)
+ }
+ })
+
+ return grouped
+ }
+
+ return {
+ ids: ids ?? [],
+ el: vnode.el as Element | null,
+ vnode,
+ name: name ?? 'unknown',
+ children: removeInheritedId(getItemChildren(vnode)?.map((child) => {
+ if (Array.isArray(child)) {
+ return traverseChildren(child)
+ }
+
+ if (isVNode(child)) {
+ return traverse(child)
+ }
+
+ return {
+ text: String(child),
+ }
+ })
+ .flat() ?? [])
+ }
+ }
+
+ const getAppVNode = () => {
+ return new Promise((resolve) => {
+ if ('_vnode' in app) {
+ return resolve(app._vnode as VNode)
+ }
+
+ requestAnimationFrame(async () => {
+ resolve(await getAppVNode())
+ })
+ })
+ }
+
+ const vnode = await getAppVNode()
+
+ return traverse(vnode)
+}
+
+
+export const walkTree = (search: AppTreeItem | HTMLElement | string, tree: AppTreeItem[] = appTree.value): AppTreeItem | null => {
+ for (const item of tree) {
+ if ('text' in item) {
+ continue
+ }
+
+ if (typeof search === 'string') {
+ if (item.ids.includes(search)) {
+ return item
+ }
+ } else if (search instanceof HTMLElement) {
+ if (item.el?.isEqualNode(search)) {
+ return item
+ }
+
+ if (item.repeatedElements?.some((el) => el.isEqualNode(search))) {
+ return item
+ }
+ } else if ('name' in search) {
+ const searchEl = search.el
+ const searchName = search.name
+
+ const isSameNode = item.el?.isEqualNode(searchEl) || item.repeatedElements?.some((el) => el.isEqualNode(el))
+ const isSameName = item.name === searchName
+
+ if (isSameName && isSameNode) {
+ return item
+ }
+ }
+
+ const child = walkTree(search, item.children)
+
+ if (child) {
+ return child
+ }
+ }
+
+ return null
+}
+
+// TODO: Right now global, for better performance, should be moved to a composable
+const appTree = ref([])
+
+export const _appTree = appTree
+
+export const useAppTree = () => {
+ const { selectedAppTreeItem, sameNodeItems, selectAppTreeItem } = useSelectedAppTreeItem()
+
+ const refresh = async () => {
+ const oldSelectedAppTreeItem = selectedAppTreeItem.value
+ const tree = await getAppTree()
+
+ if (!Array.isArray(tree)) {
+ appTree.value = [tree]
+ } else {
+ appTree.value = tree
+ }
+
+ // Keep node selected when app tree is refreshed
+ if (oldSelectedAppTreeItem) {
+ const selectedNode = walkTree(oldSelectedAppTreeItem)
+
+ if (selectedNode && 'el' in selectedNode) {
+ selectedAppTreeItem.value = selectedNode
+ }
+ }
+ }
+
+ onMounted(() => {
+ if (appTree.value.length === 0) {
+ if (import.meta.hot) {
+ import.meta.hot.on('vite:afterUpdate', () => {
+ refresh()
+ })
+ }
+ refresh()
+ }
+ })
+
+ return {
+ appTree,
+ refresh,
+ selectAppTreeItem,
+ selectedAppTreeItem,
+ sameNodeItems,
+ }
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts
new file mode 100644
index 0000000000..d4cb636934
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts
@@ -0,0 +1,60 @@
+import { ref, Ref, computed } from 'vue';
+import { AppTreeItemComponent, AppTreeItem, _appTree, walkTree } from './useAppTree'
+
+const selectedAppTreeItem = ref(null) as Ref
+
+export const useSelectedAppTreeItem = () => {
+ return {
+ selectedAppTreeItem,
+ sameNodeItems: computed(() => {
+ if (!selectedAppTreeItem.value) {
+ return []
+ }
+
+ const items: AppTreeItemComponent[] = []
+
+ const walk = (tree: AppTreeItem[]) => {
+ for (const item of tree) {
+ if ('text' in item) {
+ continue
+ }
+
+ if (item.el?.isEqualNode(selectedAppTreeItem.value!.el)) {
+ items.push(item)
+ }
+
+ if (item.repeatedElements?.some((el) => el.isEqualNode(selectedAppTreeItem.value!.el))) {
+ items.push(item)
+ }
+
+ walk(item.children)
+ }
+ }
+
+ walk(_appTree.value)
+
+ return items
+ }),
+ selectAppTreeItem(search: string | HTMLElement | AppTreeItem | null) {
+ if (search === null) {
+ selectedAppTreeItem.value = null
+ return
+ }
+
+ const item = typeof search === 'string' || search instanceof HTMLElement ? walkTree(search) : search
+
+ if (!item) {
+ console.log(_appTree.value,
+ search
+ )
+ throw new Error('Could not find item')
+ }
+
+ if ('text' in item) {
+ throw new Error('Can not select text item')
+ }
+
+ selectedAppTreeItem.value = item
+ }
+ }
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useAppVuesticConfig.ts b/packages/compiler/devtools/client/ui/composables/useAppVuesticConfig.ts
new file mode 100644
index 0000000000..e16b756823
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useAppVuesticConfig.ts
@@ -0,0 +1,32 @@
+import { type GlobalConfig } from "vuestic-ui"
+import { ref, computed, App } from 'vue'
+
+const useApp = () => {
+ const app = document.querySelector('#app')
+
+ if (!app) { throw new Error('VuesticDevtools: App element not found when accessing Vuestic Config') }
+
+ const vueApp = ref()
+
+ const tryGetVueApp = () => {
+ vueApp.value = (app as any).__vue_app__
+
+ if (!vueApp.value) {
+ requestAnimationFrame(tryGetVueApp)
+ } else {
+
+ }
+ }
+
+ tryGetVueApp()
+
+ return vueApp
+}
+
+export const useAppVuesticConfig = () => {
+ const vueApp = useApp()
+
+ return computed(() => {
+ return vueApp.value?.config.globalProperties.$vaConfig.globalConfig as unknown as GlobalConfig
+ })
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/api.ts b/packages/compiler/devtools/client/ui/composables/useComponent/api.ts
new file mode 100644
index 0000000000..cb57b8d26f
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/api.ts
@@ -0,0 +1,32 @@
+import { API_PREFIX } from '../../../../shared/CONST';
+
+const API_URL = new URL(import.meta.url).origin + API_PREFIX
+
+export const getNodeSource = (q: string) => {
+ return fetch(`${API_URL}/node-source?q=${q}`)
+}
+
+export const setNodeSource = (q: string, source: string) => {
+ return fetch(`${API_URL}/node-source?q=${q}`, {
+ method: 'PATCH',
+ body: source,
+ })
+}
+
+export const getFileName = (q: string) => {
+ return fetch(`${API_URL}/file-name?q=${q}`)
+}
+
+export const getVSCodePath = (q: string) => {
+ return fetch(`${API_URL}/vscode-path?q=${q}`)
+}
+
+export const deleteNodeSource = (q: string) => {
+ return fetch(`${API_URL}/node-source?q=${q}`, {
+ method: 'DELETE',
+ })
+}
+
+export const getFileRelativePath = (q: string) => {
+ return fetch(`${API_URL}/relative-file-path?q=${q}`)
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/index.ts b/packages/compiler/devtools/client/ui/composables/useComponent/index.ts
new file mode 100644
index 0000000000..0d5b697266
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/index.ts
@@ -0,0 +1,3 @@
+export { getElementMinifiedPaths as getElementMinfiedPaths } from './useComponentPaths';
+export { useComponent, type ComponentAttribute, type ComponentProp } from './useComponent'
+export { setNodeSource } from './api'
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts
new file mode 100644
index 0000000000..4a5b17f9aa
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts
@@ -0,0 +1,43 @@
+import { computed } from 'vue';
+import { useComponentCode } from "./useComponentCode"
+import { useComponentSource } from "./useComponentSource"
+import { defineGlobal } from '../base/defineGlobal'
+import { useAppTree } from '../useAppTree';
+import { useComponentOptions } from './useComponentOptions';
+
+export const useComponent = defineGlobal(() => {
+ const {
+ selectedAppTreeItem,
+ selectAppTreeItem
+ } = useAppTree()
+
+ const vNode = computed(() => {
+ return selectedAppTreeItem.value?.vnode ?? null
+ })
+
+ const element = computed(() => {
+ return (selectedAppTreeItem.value?.el as HTMLElement) ?? null
+ })
+
+ const uid = computed(() => {
+ return selectedAppTreeItem.value?.ids[0]
+ })
+
+ const name = computed(() => {
+ return selectedAppTreeItem.value?.name
+ })
+
+ const source = useComponentSource(uid)
+ const code = useComponentCode(source, vNode)
+ const options = useComponentOptions(code, source, vNode)
+
+ return {
+ uid,
+ name,
+ element,
+ source,
+ code,
+ options,
+ setComponent: selectAppTreeItem
+ }
+})
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts
new file mode 100644
index 0000000000..a56c5c1d22
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts
@@ -0,0 +1,293 @@
+import { type Ref, computed, type VNode, type ComputedRef } from 'vue';
+import { HTMLContentNode, HTMLRootNode, parseSource } from '../../../parser/parseSource';
+import { useComponentMeta } from './useComponentMeta'
+import { printSource } from '../../../parser/printSource';
+import { ComponentSource } from './useComponentSource'
+
+const transformPropName = (name: string, type: 'attr' | 'bind' | 'v-model' | 'event') => {
+ switch (type) {
+ case 'attr':
+ return name
+ case 'bind':
+ return `:${name}`
+ case 'v-model':
+ return `v-model:${name}`
+ case 'event':
+ return `@${name}`
+ }
+}
+
+const extractSlotName = (keys: string[]) => {
+ for (const key of keys) {
+ if (key.startsWith('v-slot:')) {
+ return key.slice(7)
+ }
+ if (key.startsWith('#')) {
+ return key.slice(1)
+ }
+ }
+}
+
+/** Mutates attrs */
+const removeDuplicatedAttributes = (attrs: Record) => {
+ Object
+ .keys(attrs)
+ .forEach((attributeName, i, keys) => {
+ const attributeNameNormalized = attributeName.replace(/^:|^v-model:|^v-bind:/, '')
+
+ if (attributeNameNormalized === attributeName) { return }
+
+ if (keys.includes(attributeNameNormalized)) {
+ delete attrs[attributeName]
+ }
+ })
+
+ return attrs
+}
+
+export const useComponentCode = (source: ComponentSource, vNode: Ref) => {
+ const ast = computed(() => {
+ if (!source.value) return null
+
+ try {
+ return parseSource(source.value)
+ }
+ catch (e) {
+ console.log(e)
+ return null
+ }
+ })
+
+ const meta = useComponentMeta(vNode)
+
+ const attributes = computed(() => {
+ if (!ast.value) return null
+ const element = ast.value.children[0]
+ if (!element) return null
+
+ if (element.type === 'content') { return null }
+
+ return element.attributes
+ })
+
+ const slots = computed(() => {
+ if (!ast.value) return null
+ const element = ast.value.children[0]
+
+ if (element.type === 'content') {
+ return [{ name: 'default', text: element.text }]
+ }
+
+ const slots = [] as { name: string, text: string }[]
+
+ for (const child of element.children) {
+ if (child.type === 'content') {
+ slots.push({ name: 'default', text: child.text.trim() })
+ continue
+ }
+
+ if (child.tag === 'template') {
+ const slotName = extractSlotName(Object.keys(child.attributes))
+
+ const slotText = child.children.length === 1 && child.children[0].type === 'content' ? child.children[0].text : null
+
+ if (slotName && slotText) {
+ slots.push({ name: slotName, text: slotText.trim() })
+ }
+ }
+ }
+
+ return slots
+ })
+
+ /**
+ * Updated attribute value in the source code
+ *
+ * string - meaning attribute have value - `to="value"`
+ * null - meaning attribute don't have a value `to`
+ * undefined - meaning attribute should be removed ``
+ */
+ const updateAttribute = (name: string, value: string | null | undefined, type: 'attr' | 'bind' | 'v-model' | 'event' = 'attr') => {
+ if (!ast.value) {
+ throw new Error('Unable to update attribute: no AST available')
+ }
+
+ if (ast.value.children.length !== 1) {
+ throw new Error('Unable to update attribute: multi-node')
+ }
+
+ const element = ast.value.children[0]
+
+ if (element.type === 'content') {
+ throw new Error('Unable to update attribute: content node can not have attributes')
+ }
+
+ const currentAttributesNames = Object.keys(element.attributes)
+ const newAttributes = currentAttributesNames.reduce((acc, attributeName) => {
+ if (attributeName === name) { return acc }
+ acc[attributeName] = element.attributes[attributeName]
+ return acc
+ }, {} as Record)
+
+ const newRoot: HTMLRootNode = {
+ type: 'root',
+ children: []
+ }
+
+ const propsMeta = meta.value.props
+
+ if (propsMeta && name in propsMeta) {
+ const propMeta = propsMeta[name]
+
+ // If new value is default, remove the attribute
+ if (propMeta.default === value && value !== undefined) {
+ newRoot.children.push({
+ ...element,
+ attributes: removeDuplicatedAttributes(newAttributes),
+ parent: newRoot,
+ })
+
+ return printSource(newRoot)
+ }
+ }
+
+ const child = {
+ ...element,
+ attributes: removeDuplicatedAttributes({
+ ...newAttributes,
+ [transformPropName(name, type)]: value
+ }),
+ parent: newRoot,
+ }
+
+ if (value === undefined) {
+ delete child.attributes[name]
+ }
+
+ newRoot.children.push(child)
+
+ return printSource(newRoot)
+ }
+
+ const updateSlot = (name: string, value: string) => {
+ if (!ast.value) {
+ throw new Error('Unable to update slot: no AST available')
+ }
+
+ if (ast.value.children.length !== 1) {
+ throw new Error('Unable to update slot: multi-node')
+ }
+
+ const element = ast.value.children[0]
+
+ const newRoot: HTMLRootNode = {
+ type: 'root',
+ children: []
+ }
+
+
+ if (element.type === 'content') {
+ if (name !== 'default') {
+ throw new Error('Unable to update slot: content node can only have default slot')
+ }
+
+ newRoot.children.push({
+ type: 'content',
+ text: value,
+ parent: newRoot,
+ })
+
+ return printSource(newRoot)
+ }
+
+ const newChildren = element.children.map((child) => {
+ // Replace content with default slot
+ if (child.type === 'content') {
+ if (name === 'default') {
+ return {
+ ...child,
+ text: value,
+ }
+ }
+
+ return child
+ }
+
+ if (child.tag === 'template') {
+ const slotName = extractSlotName(Object.keys(child.attributes))
+
+ if (slotName === name) {
+ return {
+ ...child,
+ children: [{ type: 'content' as const, text: value, parent: child }],
+ }
+ }
+ }
+
+ return child
+ })
+
+ newRoot.children.push({
+ ...element,
+ children: newChildren,
+ parent: newRoot,
+ })
+
+ return printSource(newRoot)
+ }
+
+ const appendChild = (code: string) => {
+ if (!ast.value) {
+ throw new Error('Unable to append child: no AST available')
+ }
+
+ if (ast.value.children.length !== 1) {
+ throw new Error('Unable to append child: multi-node')
+ }
+
+ const element = ast.value.children[0]
+
+ const newRoot: HTMLRootNode = {
+ type: 'root',
+ children: []
+ }
+
+ if (element.type === 'content') {
+ throw new Error('Unable to append child: content node can not have children')
+ }
+
+ const newChildren = [
+ ...element.children,
+ {
+ type: 'content',
+ text: code,
+ parent: element,
+ } satisfies HTMLContentNode
+ ]
+
+ newRoot.children.push({
+ ...element,
+ children: newChildren,
+ parent: newRoot,
+ })
+
+ return printSource(newRoot)
+ }
+
+ const isParsed = computed(() => {
+ return attributes.value !== null && slots.value !== null && !source.isLoading && source.value !== null
+ })
+
+ return {
+ isParsed,
+ meta,
+ // ast,
+ attributes,
+ slots,
+ updateAttribute,
+ updateSlot,
+ appendChild,
+ }
+}
+
+export type ComponentCode = ReturnType
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentMeta.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentMeta.ts
new file mode 100644
index 0000000000..12713e3809
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentMeta.ts
@@ -0,0 +1,38 @@
+import { Ref, VNode, computed } from 'vue';
+
+const getVNodeComponent = (vNode: VNode | null) => {
+ if (!vNode) return null
+
+ if (typeof vNode.type === 'string') {
+ return { name: vNode.type, props: {} }
+ }
+
+ if (typeof vNode.type === 'object') {
+ if ('__name' in vNode.type) {
+ return {
+ name: vNode.type.__name as string || undefined,
+ props: vNode.type.props as Record,
+ }
+ }
+
+ if ('name' in vNode.type) {
+ return {
+ name: vNode.type.name as string || undefined,
+ props: vNode.props as Record,
+ }
+ }
+ }
+
+
+ return {
+ name: vNode.el?.tagName ?? 'unknown',
+ props: {} as Record,
+ }
+}
+
+export const useComponentMeta = (vNode: Ref) => {
+ return computed(() => ({
+ type: vNode.value?.type,
+ ...getVNodeComponent(vNode.value),
+ }))
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentOptions.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentOptions.ts
new file mode 100644
index 0000000000..72c613cf7e
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentOptions.ts
@@ -0,0 +1,127 @@
+import { computed, type PropType, type ComputedRef, VNode } from 'vue';
+import type { useComponentCode } from './useComponentCode'
+import type { useComponentSource } from './useComponentSource'
+
+type Code = ReturnType
+type Source = ReturnType
+
+export type ComponentAttribute = {
+ name: string,
+ readonly currentValue: any,
+ codeValue: string | null | undefined,
+ codeAttribute?: {
+ name: string,
+ value: string | null,
+ isDynamic: boolean,
+ isVModel: boolean,
+ isEvent: boolean,
+ },
+ updateAttribute: Code['updateAttribute'],
+}
+
+export type ComponentProp = {
+ meta: {
+ default?: any,
+ type?: PropType,
+ }
+} & ComponentAttribute
+
+const findPropFromAttributes = (attributes: Record, propName: string) => {
+ const possibleNames = [
+ propName,
+ `:${propName}`,
+ `v-bind:${propName}`,
+ `@${propName}`,
+ `v-model:${propName}`,
+ ]
+
+ for (const name of possibleNames) {
+ if (name in attributes) {
+ return {
+ name: name,
+ value: attributes[name],
+ isDynamic: name.startsWith(':') || name.startsWith('v-bind'),
+ isVModel: name.startsWith('v-model'),
+ isEvent: name.startsWith('@'),
+ }
+ }
+ }
+
+ return null
+}
+
+export const useComponentOptions = (code: Code, source: Source, vNode: ComputedRef) => {
+ const props = computed(() => {
+ if (!code.attributes.value) { return {} }
+
+ const props = {} as Record
+
+ for (const name in code.meta.value.props) {
+ const propMeta = code.meta.value.props?.[name as string]!
+
+ const attributeFromCode = findPropFromAttributes(code.attributes.value!, name)
+
+ props[name] = {
+ name: name,
+ meta: propMeta,
+ get currentValue() {
+ return vNode.value?.props?.[name]
+ },
+ get codeValue() {
+ return attributeFromCode?.value
+ },
+ set codeValue(newCodeValue: string | null | undefined) {
+ source.update(code.updateAttribute(name, newCodeValue))
+ },
+ codeAttribute: attributeFromCode ?? undefined,
+ updateAttribute: code.updateAttribute,
+ }
+ }
+
+ return props
+ })
+
+ const slots = computed(() => {
+ return code.slots.value?.map((slot) => {
+ let timeout: ReturnType
+
+ return {
+ name: slot.name,
+ get codeValue() {
+ return slot.text
+ },
+ set codeValue(newCodeValue: string) {
+ clearTimeout(timeout)
+ timeout = setTimeout(() => {
+ source.update(code.updateSlot(slot.name, newCodeValue))
+ }, 300)
+ },
+ }
+ }) ?? []
+ })
+
+ const style = computed({
+ get() {
+ if (!code.attributes.value?.style) { return {} }
+
+ return code.attributes.value.style.split(';').reduce((acc, style) => {
+ const [key, value] = style.split(':')
+ if (key && value) {
+ acc[key.trim()] = value.trim()
+ }
+ return acc
+ }, {} as Record)
+ },
+
+ set(newStyle: Record) {
+ const style = Object.entries(newStyle).map(([key, value]) => `${key}: ${value}`).join('; ')
+ source.update(code.updateAttribute('style', style))
+ }
+ })
+
+ return {
+ props,
+ slots,
+ style,
+ }
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentPaths.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentPaths.ts
new file mode 100644
index 0000000000..c5fcbd9b8a
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentPaths.ts
@@ -0,0 +1,35 @@
+import { type Ref, computed, ref, watch } from 'vue'
+import { PREFIX } from '../../../../shared/CONST';
+
+import { getFileName } from './api'
+
+export const getElementMinifiedPaths = (htmlElement: HTMLElement | null) => {
+ if (!htmlElement) { return null }
+
+ return Object.keys(htmlElement.dataset)
+ .filter((key) => key.startsWith(`${PREFIX}-`))
+}
+
+export const useComponentPaths = (htmlElement: Ref) => {
+ const minifiedPaths = computed(() => getElementMinifiedPaths(htmlElement.value))
+
+ const paths = ref<{
+ path: string,
+ minified: string
+ tagName: string
+ }[]>()
+
+ watch(minifiedPaths, async (minifiedPaths) => {
+ if (!minifiedPaths) { return }
+
+ paths.value = await Promise.all(minifiedPaths.map(async (minifiedPath) => {
+ return {
+ path: await (await getFileName(minifiedPath)).text(),
+ minified: minifiedPath,
+ tagName: htmlElement.value!.dataset[minifiedPath]!
+ }
+ }))
+ })
+
+ return paths
+}
diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts
new file mode 100644
index 0000000000..e1670add6a
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts
@@ -0,0 +1,76 @@
+import { ref, watch, computed, Ref } from 'vue';
+import { getNodeSource, setNodeSource, deleteNodeSource, getVSCodePath, getFileRelativePath } from './api';
+import { useAsyncComputed } from '../base/useAsyncComputed';
+
+export const useComponentSource = (uid: Ref) => {
+ /** @notice Source is async and may not be available until loaded */
+ const source = ref(null)
+ const isSourceLoading = ref(false)
+
+ const resetSource = () => {
+ source.value = null
+ }
+
+ const loadSource = async () => {
+ if (!uid.value) {
+ resetSource()
+ return
+ }
+
+ try {
+ isSourceLoading.value = true
+ const response = await getNodeSource(uid.value)
+ source.value = await response.text()
+ } finally {
+ isSourceLoading.value = false
+ }
+ }
+
+ const saveSource = async (source: string) => {
+ if (!uid.value) { throw new Error('Can not save source: no q available') }
+
+ await setNodeSource(uid.value, source)
+
+ await loadSource()
+ }
+
+ const removeFromSource = async () => {
+ // TODO: Handle history here
+ if (!uid.value) { throw new Error('Can not delete source: no q available') }
+
+ source.value = ''
+ await deleteNodeSource(uid.value)
+ }
+
+ watch(uid, async () => {
+ resetSource()
+ loadSource()
+ }, { immediate: true })
+
+ const openInVSCode = async () => {
+ if (!uid.value) { throw new Error('Can not open in VSCode: no q available') }
+
+ const path = await (await getVSCodePath(uid.value)).text()
+
+ fetch(`/__open-in-editor?file=${path}`)
+ }
+
+ const fileName = useAsyncComputed(async () => {
+ if (!uid.value) { return null }
+
+ return await (await getFileRelativePath(uid.value)).text()
+ }, '')
+
+ return {
+ content: source,
+ get value () { return source.value },
+ get isLoading () { return isSourceLoading.value },
+ fileName,
+ refresh: loadSource,
+ update: saveSource,
+ remove: removeFromSource,
+ openInVSCode,
+ }
+}
+
+export type ComponentSource = ReturnType
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..4d405f802f
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts
@@ -0,0 +1,59 @@
+import { onBeforeMount, onMounted, ref, watch } from "vue"
+import { PREFIX } from "../../../shared/CONST"
+import { useAppTree } from "./useAppTree"
+
+export const useHoveredElement = () => {
+ const { selectedAppTreeItem } = useAppTree()
+
+ 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 (selectedAppTreeItem.value?.el && el.contains(selectedAppTreeItem.value.el)) {
+ 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/index.ts b/packages/compiler/devtools/client/ui/index.ts
new file mode 100644
index 0000000000..d525b9bf43
--- /dev/null
+++ b/packages/compiler/devtools/client/ui/index.ts
@@ -0,0 +1,29 @@
+import { createApp, Plugin, markRaw } from 'vue'
+import App from './Devtools.vue'
+import { createVuestic } 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(createVuestic({
+ config: {
+ colors: {
+ variables: {
+ outlinePrimary: '#00b4d8',
+ outlineSecondary: '#90e0ef',
+ outlinePrimaryBackground: '#00b4d811',
+ outlineSecondaryBackground: '#90e0ef01',
+ backgroundToolbar: '#252422',
+ }
+ },
+ }
+ }))
+ .mount(appRoot)
+ }
+}) satisfies Plugin
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..3893e0cf27
--- /dev/null
+++ b/packages/compiler/devtools/plugin/add-vue-plugin.ts
@@ -0,0 +1,24 @@
+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) {
+ return null
+ }
+
+ 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..e2de37cdba
--- /dev/null
+++ b/packages/compiler/devtools/plugin/compiler.ts
@@ -0,0 +1,102 @@
+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'
+import { stringifyFileQuery } from '../shared/file-query'
+
+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) => {
+ // TODO: remove old minified paths
+ 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 = stringifyFileQuery(id, node.loc.start.offset, node.loc.end.offset)
+
+ const withAttribute = ` data-${PREFIX}="" data-${minifyPath(nodeId)}="${node.tag}"`
+
+ 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..306dcff36e
--- /dev/null
+++ b/packages/compiler/devtools/plugin/plugin.ts
@@ -0,0 +1,80 @@
+import { Plugin, ResolvedConfig, createLogger } 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'
+import { formatString } from '../../shared/color'
+
+export type PluginOptions = {
+ include?: FilterPattern
+ exclude?: FilterPattern
+}
+
+const ALT_KEY = process.platform === 'darwin' ? 'Option ⌥' : 'Alt'
+
+const logger = createLogger('info', {
+ prefix: '[vuestic:devtools]'
+})
+
+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 (/\/src\/main\.(ts|js|mjs|mts)$/.test(id)) {
+ const newCode = addVuePlugin(code)
+
+ if (newCode) {
+ logger.info(formatString(`Vuestic Devtools installed. Open your application and press [${ALT_KEY}] + [F12] to open devtools`), {
+ timestamp: true,
+ })
+ } else {
+ logger.error(formatString('! Devtools plugin can not find createApp(App) in your main file. This is likely a bug, please, open an issue on GitHub.'), {
+ timestamp: true,
+ })
+ logger.error(formatString('[https://github.com/epicmaxco/vuestic-ui/issues/new?assignees=&labels=BUG&projects=&template=bug-template.md]'), {
+ timestamp: true,
+ })
+ }
+
+ return newCode
+ }
+
+ 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..5919c584ce
--- /dev/null
+++ b/packages/compiler/devtools/server/file.ts
@@ -0,0 +1,112 @@
+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 => {
+ if (source.length === 0) {
+ return source;
+ }
+
+ 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);
+
+ return {
+ path,
+ start,
+ end: start + sourceWithIntent.length,
+ }
+}
+
+export const deleteComponentSource = async (path: string, start: number, end: number) => {
+ const fileSource = await requestSource(path);
+ const intent = getIntent(fileSource, start);
+
+ if (intent === 0) {
+ await writeFile(path, fileSource.slice(0, start) + fileSource.slice(end));
+ return {
+ path,
+ start,
+ end: start,
+ }
+ }
+
+ const fileSourceStart = fileSource.slice(0, start - intent - '\n'.length);
+ const fileSourceEnd = fileSource.slice(end);
+
+ await writeFile(path, fileSourceStart + fileSourceEnd);
+
+ return {
+ path,
+ start: start - intent - '\n'.length,
+ end: start - intent - '\n'.length,
+ }
+}
+
+export const getComponentLineAndCol = async (path: string, start: number) => {
+ const fileSource = await requestSource(path);
+ const intent = getIntent(fileSource, start);
+ const lines = fileSource.slice(0, start).split('\n');
+ const line = lines.length;
+ const col = intent;
+ return { line, col };
+}
+
+export const getRelativeFilePath = (path: string) => {
+ return '.' + path.replace(process.cwd(), '');
+}
diff --git a/packages/compiler/devtools/server/server-middleware.ts b/packages/compiler/devtools/server/server-middleware.ts
new file mode 100644
index 0000000000..f9387cafe8
--- /dev/null
+++ b/packages/compiler/devtools/server/server-middleware.ts
@@ -0,0 +1,82 @@
+import { Connect } from 'vite'
+import { readBody } from './utils'
+import { API_PREFIX } from '../shared/CONST'
+import { getComponentLineAndCol, getComponentSource, setComponentSource, deleteComponentSource, getRelativeFilePath } from './file'
+import { replacePath, unminifyPath } from '../shared/slug'
+import { parseFileQuery, stringifyFileQuery } from '../shared/file-query'
+
+
+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 } = parseFileQuery(unminified);
+
+ if (req.method === 'GET' && req.url.startsWith(`${API_PREFIX}/node-source`)) {
+ res.writeHead(200)
+ res.end(await getComponentSource(path, start, end));
+ return;
+ }
+
+ if (req.method === 'PATCH' && req.url.startsWith(`${API_PREFIX}/node-source`)) {
+ const body = await readBody(req);
+
+ if (!(typeof body === 'string')) {
+ throw new Error('Body is required.');
+ }
+
+ const newPath = await setComponentSource(path, start, end, body);
+
+ replacePath(minified, stringifyFileQuery(newPath.path, newPath.start, newPath.end));
+
+ res.writeHead(200)
+ res.end(minified);
+ return
+ }
+
+ if (req.method === 'DELETE' && req.url.startsWith(`${API_PREFIX}/node-source`)) {
+ const newPath = await deleteComponentSource(path, start, end);
+
+ replacePath(minified, stringifyFileQuery(newPath.path, newPath.start, newPath.end));
+
+ res.writeHead(200)
+ res.end();
+ return
+ }
+
+ if (req.method === 'GET' && req.url.startsWith(`${API_PREFIX}/file-name`)) {
+ res.writeHead(200)
+ res.end(unminified);
+ return;
+ }
+
+ if (req.method === 'GET' && req.url.startsWith(`${API_PREFIX}/relative-file-path`)) {
+ res.writeHead(200)
+ res.end(getRelativeFilePath(path));
+ return
+ }
+
+ if (req.method === 'GET' && req.url.startsWith(`${API_PREFIX}/vscode-path`)) {
+ const { line, col } = await getComponentLineAndCol(path, Number(start));
+
+ res.writeHead(200)
+ // File Editor path for vite internal openInEditor method
+ res.end(`${path}:${line}:${col}`);
+ 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/file-query.ts b/packages/compiler/devtools/shared/file-query.ts
new file mode 100644
index 0000000000..0f900e8688
--- /dev/null
+++ b/packages/compiler/devtools/shared/file-query.ts
@@ -0,0 +1,11 @@
+export const stringifyFileQuery = (path: string, start: number, end: number) => `${path}:${start}:${end}`
+
+export const parseFileQuery = (query: string) => {
+ const params = query.split(':')
+
+ const end = params.pop()
+ const start = params.pop()
+ const path = params.join(':')
+
+ return { path, start: Number(start), end: Number(end) } as const
+}
diff --git a/packages/compiler/devtools/shared/slug.ts b/packages/compiler/devtools/shared/slug.ts
new file mode 100644
index 0000000000..2468890333
--- /dev/null
+++ b/packages/compiler/devtools/shared/slug.ts
@@ -0,0 +1,26 @@
+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) => {
+ 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..3616c9f7ba
--- /dev/null
+++ b/packages/compiler/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "@vuestic/compiler",
+ "version": "0.1.0",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.1.0",
+ "acorn": "^8.12.1",
+ "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"
+ },
+ "default": "./dist/vite/index.mjs",
+ "node": "./dist/vite/index.mjs"
+ },
+ "./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..2fbf6e8b15
--- /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.6",
+ "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..f6dd3d2042
--- /dev/null
+++ b/packages/compiler/playground/src/App.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Vuestic UI Devtools demo
+
+
+
+
+ Test
+
+
+
+
+ Contact
+
+
+
+
+
+
+ Default slot
+
+ Footer slot
+
+
+
+
diff --git a/packages/compiler/playground/src/main.ts b/packages/compiler/playground/src/main.ts
new file mode 100644
index 0000000000..34933135ba
--- /dev/null
+++ b/packages/compiler/playground/src/main.ts
@@ -0,0 +1,8 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import { createVuestic } from 'vuestic-ui'
+import 'vuestic-ui/css'
+
+createApp(App)
+ .use(createVuestic({}) as any)
+ .mount('#app')
diff --git a/packages/compiler/playground/src/pages/TestButton.vue b/packages/compiler/playground/src/pages/TestButton.vue
new file mode 100644
index 0000000000..edf70656af
--- /dev/null
+++ b/packages/compiler/playground/src/pages/TestButton.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/packages/compiler/playground/src/pages/TestButtonBase.vue b/packages/compiler/playground/src/pages/TestButtonBase.vue
new file mode 100644
index 0000000000..0b71143119
--- /dev/null
+++ b/packages/compiler/playground/src/pages/TestButtonBase.vue
@@ -0,0 +1,16 @@
+
+
+
+
+ Test
+
+
diff --git a/packages/compiler/playground/src/pages/TestPage.vue b/packages/compiler/playground/src/pages/TestPage.vue
new file mode 100644
index 0000000000..aa2decbf90
--- /dev/null
+++ b/packages/compiler/playground/src/pages/TestPage.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ Login
+
+
+
+
+
+
+
+
+
+ Test
+
+
+ Test 2
+
+
+ Button
+
+
+
+
+ {{ i }}
+
+
+
+
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..53f154d740
--- /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/vuestic.config.ts b/packages/compiler/playground/vuestic.config.ts
new file mode 100644
index 0000000000..5af9f02f50
--- /dev/null
+++ b/packages/compiler/playground/vuestic.config.ts
@@ -0,0 +1,21 @@
+import { defineVuesticConfig, createIconsConfig } from "vuestic-ui";
+
+export default defineVuesticConfig({
+ colors: {
+ variables: {
+ primary: '#ff00ff',
+ myCustomColor: '#ff00ff',
+ }
+ },
+ i18n: {
+ 'test': 'test',
+ },
+ icons: createIconsConfig({
+ aliases: [
+ {
+ name: 'custom',
+ to: 'font-awesome',
+ },
+ ],
+ })
+})
diff --git a/packages/compiler/playground/yarn.lock b/packages/compiler/playground/yarn.lock
new file mode 100644
index 0000000000..31017ad76b
--- /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.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz#8b613b9725e8f9479d142970b106b6ae878610d5"
+ integrity sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==
+
+"@rollup/rollup-android-arm64@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz#654ca1049189132ff602bfcf8df14c18da1f15fb"
+ integrity sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==
+
+"@rollup/rollup-darwin-arm64@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz#6d241d099d1518ef0c2205d96b3fa52e0fe1954b"
+ integrity sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==
+
+"@rollup/rollup-darwin-x64@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz#42bd19d292a57ee11734c980c4650de26b457791"
+ integrity sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz#f23555ee3d8fe941c5c5fd458cd22b65eb1c2232"
+ integrity sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==
+
+"@rollup/rollup-linux-arm-musleabihf@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz#f3bbd1ae2420f5539d40ac1fde2b38da67779baa"
+ integrity sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==
+
+"@rollup/rollup-linux-arm64-gnu@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz#7abe900120113e08a1f90afb84c7c28774054d15"
+ integrity sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==
+
+"@rollup/rollup-linux-arm64-musl@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz#9e655285c8175cd44f57d6a1e8e5dedfbba1d820"
+ integrity sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==
+
+"@rollup/rollup-linux-powerpc64le-gnu@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz#9a79ae6c9e9d8fe83d49e2712ecf4302db5bef5e"
+ integrity sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==
+
+"@rollup/rollup-linux-riscv64-gnu@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz#67ac70eca4ace8e2942fabca95164e8874ab8128"
+ integrity sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==
+
+"@rollup/rollup-linux-s390x-gnu@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz#9f883a7440f51a22ed7f99e1d070bd84ea5005fc"
+ integrity sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==
+
+"@rollup/rollup-linux-x64-gnu@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz#70116ae6c577fe367f58559e2cffb5641a1dd9d0"
+ integrity sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==
+
+"@rollup/rollup-linux-x64-musl@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz#f473f88219feb07b0b98b53a7923be716d1d182f"
+ integrity sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==
+
+"@rollup/rollup-win32-arm64-msvc@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz#4349482d17f5d1c58604d1c8900540d676f420e0"
+ integrity sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==
+
+"@rollup/rollup-win32-ia32-msvc@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz#a6fc39a15db618040ec3c2a24c1e26cb5f4d7422"
+ integrity sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==
+
+"@rollup/rollup-win32-x64-msvc@4.22.4":
+ version "4.22.4"
+ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz#3dd5d53e900df2a40841882c02e56f866c04d202"
+ integrity sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==
+
+"@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.22.4"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.22.4.tgz#4135a6446671cd2a2453e1ad42a45d5973ec3a0f"
+ integrity sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==
+ dependencies:
+ "@types/estree" "1.0.5"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.22.4"
+ "@rollup/rollup-android-arm64" "4.22.4"
+ "@rollup/rollup-darwin-arm64" "4.22.4"
+ "@rollup/rollup-darwin-x64" "4.22.4"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.22.4"
+ "@rollup/rollup-linux-arm-musleabihf" "4.22.4"
+ "@rollup/rollup-linux-arm64-gnu" "4.22.4"
+ "@rollup/rollup-linux-arm64-musl" "4.22.4"
+ "@rollup/rollup-linux-powerpc64le-gnu" "4.22.4"
+ "@rollup/rollup-linux-riscv64-gnu" "4.22.4"
+ "@rollup/rollup-linux-s390x-gnu" "4.22.4"
+ "@rollup/rollup-linux-x64-gnu" "4.22.4"
+ "@rollup/rollup-linux-x64-musl" "4.22.4"
+ "@rollup/rollup-win32-arm64-msvc" "4.22.4"
+ "@rollup/rollup-win32-ia32-msvc" "4.22.4"
+ "@rollup/rollup-win32-x64-msvc" "4.22.4"
+ 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.6:
+ version "5.3.6"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.6.tgz#e097c0a7b79adb2e60bec9ef7907354f09d027bd"
+ integrity sha512-es78AlrylO8mTVBygC0gTC0FENv0C6T496vvd33ydbjF/mIi9q3XQ9A3NWo5qLGFKywvz10J26813OkLvcQleA==
+ 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/shared/color.ts b/packages/compiler/shared/color.ts
new file mode 100644
index 0000000000..ce11c9b20e
--- /dev/null
+++ b/packages/compiler/shared/color.ts
@@ -0,0 +1,19 @@
+const white = '\x1b[0m'
+const reset = '\x1bRESET'
+
+const red = '\x1b[31m'
+const yellow = '\x1b[33m'
+const cyan = '\x1b[36m'
+
+export const formatString = (str: string) => {
+ str = str.replace(/\[([^\]]*)\]/g, (_, p1) => cyan + p1 + reset)
+ if (str.startsWith('!!! ')) {
+ str = red + str.slice(4).replaceAll(reset, red) + reset
+ }
+
+ if (str.startsWith('! ')) {
+ str = yellow + str.slice(2).replaceAll(reset, yellow) + reset
+ }
+
+ return str.replaceAll(reset, white)
+}
\ No newline at end of file
diff --git a/packages/compiler/shared/merge-deep.ts b/packages/compiler/shared/merge-deep.ts
new file mode 100644
index 0000000000..00fd2df243
--- /dev/null
+++ b/packages/compiler/shared/merge-deep.ts
@@ -0,0 +1,35 @@
+// Copied from ui/src/utils/merge-deep.ts
+
+const isObject = (obj: any) => obj && typeof obj === 'object' && !Array.isArray(obj)
+
+/**
+ * Merge objects deep
+ * If property is array, it will replace target value
+ */
+export const mergeDeep = (target: any, source: any): any => {
+ if (!isObject(target)) {
+ target = {}
+ }
+
+ Object.keys(source).forEach(key => {
+ const targetValue = target[key]
+ const sourceValue = source[key]
+
+ if (sourceValue instanceof RegExp || sourceValue instanceof Date) {
+ target[key] = sourceValue
+ } else if (isObject(targetValue) && isObject(sourceValue)) {
+ target[key] = mergeDeep(Object.create(
+ Object.getPrototypeOf(targetValue),
+ Object.getOwnPropertyDescriptors(targetValue),
+ ), sourceValue)
+ } else {
+ target[key] = sourceValue
+ }
+ })
+
+ return target
+}
+
+export const mergeDeepMultiple = (...objects: any[]): any => {
+ return objects.reduce((acc, obj) => mergeDeep(acc, obj), {})
+}
diff --git a/packages/compiler/shared/plugin/code.ts b/packages/compiler/shared/plugin/code.ts
new file mode 100644
index 0000000000..bf2b3cb7cd
--- /dev/null
+++ b/packages/compiler/shared/plugin/code.ts
@@ -0,0 +1,86 @@
+import { parse, type VariableDeclaration, type ObjectExpression, type Property } from 'acorn'
+
+export const getContentInParenthesis = (content: string) => {
+ let text = ''
+
+ let bracketCount = 0
+
+ for (let i = 0; i < content.length; i++) {
+ if (content[i] === '(') {
+ bracketCount++
+ continue
+ }
+
+ if (content[i] === ')') {
+ bracketCount--
+ }
+
+ if (bracketCount === 0) {
+ break
+ }
+
+ text += content[i]
+ }
+
+ return text
+}
+
+const isObject = (t: any): t is ObjectExpression => {
+ return t.type === 'ObjectExpression'
+}
+
+const isProperty = (t: any): t is Property => {
+ return t.type === 'Property'
+}
+
+const OBJECT_DECLARATION = 'const obj = '
+
+export const replaceOrAddConfigPropertyValue = (content: string, newValue: string) => {
+ const code = OBJECT_DECLARATION + content
+
+ try {
+ const program = parse(code, { ecmaVersion: 2020 })
+
+ const object = (program.body[0] as VariableDeclaration).declarations[0].init
+
+ let configProperty = null
+
+ if (isObject(object)) {
+ const properties = object.properties
+
+ for (const property of properties) {
+ if (isProperty(property)) {
+ const name = 'name' in property.key
+ ? property.key.name
+ : 'value' in property.key ? property.key.value
+ : null
+
+ if (name === 'config') {
+ configProperty = property
+ break
+ }
+ }
+ }
+
+ if (!configProperty) {
+ return `{
+ config: ${newValue},
+ ${
+ properties.map((p) => {
+ return p.loc?.source
+ }).join(', ')
+ }
+ }`
+ }
+
+ return (code.slice(0, configProperty.start) + `config: ` + newValue + code.slice(configProperty.end)).slice(OBJECT_DECLARATION.length)
+ }
+
+ return content
+ }
+ catch (e) {
+ console.error('Unable to parse code:', code)
+ console.error(e)
+ return content
+ }
+}
diff --git a/packages/compiler/shared/plugin/is-entry-file.ts b/packages/compiler/shared/plugin/is-entry-file.ts
new file mode 100644
index 0000000000..4a27704a6b
--- /dev/null
+++ b/packages/compiler/shared/plugin/is-entry-file.ts
@@ -0,0 +1,3 @@
+export const isEntryFile = (id: string) => {
+ return /\/src\/main\.(ts|js|mjs|mts)$/.test(id)
+}
diff --git a/packages/compiler/shared/plugin/js.ts b/packages/compiler/shared/plugin/js.ts
new file mode 100644
index 0000000000..2532255d5a
--- /dev/null
+++ b/packages/compiler/shared/plugin/js.ts
@@ -0,0 +1,72 @@
+import MagicString from 'magic-string'
+import { getContentInParenthesis, replaceOrAddConfigPropertyValue } from './code'
+
+const CREATE_APP_TEMPLATE = 'createApp(App)'
+
+type Code = string | MagicString
+const createMagicString = (code: Code) => {
+ return typeof code === 'string' ? new MagicString(code) : code
+}
+
+export const addImport = (originalCode: Code, code: string) => {
+ const ms = createMagicString(originalCode)
+ ms.appendLeft(0, code + '\n')
+ return ms
+}
+
+/**
+ * Add devtools plugin to the Vue app
+ */
+export const addVuePlugin = (originalCode: Code, code: string) => {
+ const ms = createMagicString(originalCode)
+
+ const createAppIndex = ms.original.indexOf(CREATE_APP_TEMPLATE)
+
+ if (createAppIndex === -1) {
+ throw new Error('createApp not found')
+ }
+
+ ms.appendRight(createAppIndex + CREATE_APP_TEMPLATE.length, `.use(${code})`)
+
+ return ms
+}
+
+export const mergeVuesticPluginConfigOption = (originalCode: Code, pluginName: string, content: string = '') => {
+ const ms = createMagicString(originalCode)
+
+ const createPluginCode = `\.use\(${pluginName}\(`
+ const existingPluginIndex = ms.original.indexOf(createPluginCode)
+
+ if (existingPluginIndex === -1) {
+ return null
+ }
+
+ const nextCode = ms.original.slice(existingPluginIndex + createPluginCode.length - 1)
+
+ const contentInParenthesis = getContentInParenthesis(nextCode)
+
+ let newCode
+
+ if (contentInParenthesis) {
+ newCode = replaceOrAddConfigPropertyValue(contentInParenthesis, content)
+ } else {
+ newCode = `{ config: ${content} }`
+ }
+
+ // const newCode = replaceOrAddConfigPropertyValue(contentInParenthesis, content)
+
+ return ms
+ .remove(existingPluginIndex, existingPluginIndex + createPluginCode.length + contentInParenthesis.length + 2)
+ .appendLeft(existingPluginIndex + createPluginCode.length, `.use(${pluginName}(${newCode}))`)
+}
+
+export const compileCode = (ms: MagicString | string) => {
+ if (typeof ms === 'string') {
+ return { code: ms }
+ }
+
+ return {
+ code: ms.toString(),
+ map: ms.generateMap()
+ }
+}
diff --git a/packages/compiler/tsconfig.json b/packages/compiler/tsconfig.json
new file mode 100644
index 0000000000..04576fae70
--- /dev/null
+++ b/packages/compiler/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "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",
+ ],
+ "lib": [
+ "esnext",
+ "dom",
+ "dom.iterable"
+ ],
+ "paths": {
+ "vuestic-ui": ["../ui/src/index.ts"]
+ }
+ },
+ "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..6d93537d4c
--- /dev/null
+++ b/packages/compiler/vite-plugin/index.ts
@@ -0,0 +1,85 @@
+import { createLogger, Plugin } from "vite"
+import { devtools, PluginOptions as DevtoolsPluginOptions } from "../devtools"
+import { cssLayers } from "../css-layers"
+import { vuesticConfig, Options as VuesticConfigPluginOptions } from "../vuestic-config"
+import { mergeDeep } from "../shared/merge-deep"
+
+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,
+
+ /**
+ * Path to the Vuestic config file
+ *
+ * @default 'vuestic.config.ts'
+ *
+ * Make sure to include generated types to your tsconfig.json
+ *
+ * ```json
+ * {
+ * "include": ["node_modules/.vuestic/*.d.ts", "src/**\/*.d.ts"]
+ * }
+ * ```
+ */
+ config?: boolean | VuesticConfigPluginOptions,
+}
+
+const logger = createLogger('info', {
+ prefix: '[vuestic:compiler]'
+})
+
+const defaultOptions: Required = {
+ devtools: false,
+ cssLayers: false,
+ config: {
+ configPath: 'vuestic.config.ts'
+ },
+}
+
+export const vuestic = (options: Options = {}): Plugin[] => {
+ options = mergeDeep(defaultOptions, options)
+
+ 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) {
+ logger.info('Using vuestic:devtools', {
+ timestamp: true,
+ })
+ plugins.push(devtools(extractOptions('devtools')))
+ }
+
+ if (options.cssLayers !== false) {
+ logger.info('Using vuestic:css-layers', {
+ timestamp: true,
+ })
+ plugins.push(cssLayers)
+ }
+
+ if (Boolean(options.config)) {
+ logger.info('Using vuestic:config', {
+ timestamp: true,
+ })
+ plugins.push(...vuesticConfig(extractOptions('config')))
+
+ }
+
+ return plugins
+}
diff --git a/packages/compiler/vuestic-config/index.ts b/packages/compiler/vuestic-config/index.ts
new file mode 100644
index 0000000000..a0ed13843f
--- /dev/null
+++ b/packages/compiler/vuestic-config/index.ts
@@ -0,0 +1 @@
+export * from './plugin'
diff --git a/packages/compiler/vuestic-config/plugin.ts b/packages/compiler/vuestic-config/plugin.ts
new file mode 100644
index 0000000000..9add243b57
--- /dev/null
+++ b/packages/compiler/vuestic-config/plugin.ts
@@ -0,0 +1,17 @@
+import { Plugin } from 'vite'
+import { configResolver } from './plugins/config-resolver'
+import { useConfig } from './plugins/use-config'
+import { configTypes } from './plugins/config-types'
+
+export type Options = {
+ configPath?: string
+}
+
+/** Add css layers to Vuestic files */
+export const vuesticConfig = (options: Options = {}): Plugin[] => {
+ return [
+ configTypes(options),
+ configResolver(options),
+ useConfig(options),
+ ]
+}
diff --git a/packages/compiler/vuestic-config/plugins/config-resolver.ts b/packages/compiler/vuestic-config/plugins/config-resolver.ts
new file mode 100644
index 0000000000..fbbb2bd8d8
--- /dev/null
+++ b/packages/compiler/vuestic-config/plugins/config-resolver.ts
@@ -0,0 +1,76 @@
+import { Plugin } from 'vite'
+import { existsSync } from 'node:fs'
+import { readFile } from 'node:fs/promises'
+
+const VUESTIC_CONFIG_ALIAS = '#vuestic-config'
+
+export const resolveVuesticConfigPath = () => {
+ if (existsSync('./vuestic.config.ts')) {
+ return './vuestic.config.ts'
+ } else if (existsSync('./vuestic.config.js')) {
+ return './vuestic.config.js'
+ } else if (existsSync('./vuestic.config.mjs')) {
+ return './vuestic.config.mjs'
+ } else if (existsSync('./vuestic.config.cjs')) {
+ return './vuestic.config.cjs'
+ } else if (existsSync('./vuestic.config.mts')) {
+ return './vuestic.config.mts'
+ } else {
+ return undefined
+ }
+}
+
+export const tryToReadConfig = async (path: string) => {
+ if (existsSync(path)) {
+ return readFile(path, 'utf-8')
+ }
+
+ return null
+}
+
+export const isConfigExists = (configPath: string | undefined) => {
+ if (!configPath) {
+ return resolveVuesticConfigPath()
+ }
+
+ return existsSync(configPath)
+}
+
+/** This plugin is used to resolve path to vuestic config if it is imported with `#vuestic-config` */
+export const configResolver = (options: {
+ configPath?: string
+} = {}): Plugin => {
+
+ return {
+ name: 'vuestic:config-resolver',
+
+ // Resolve vuestic config alias
+ async resolveId(source) {
+ if (source === VUESTIC_CONFIG_ALIAS) {
+ return `virtual:vuestic-config`
+ }
+ },
+
+ async load(id) {
+ const {
+ configPath = resolveVuesticConfigPath()
+ } = options
+
+ if (id === `virtual:vuestic-config`) {
+ if (!configPath) {
+ return 'export default {}'
+ }
+
+ const config = await tryToReadConfig(configPath)
+
+ if (config) {
+ return config
+ } else if (options.configPath) {
+ throw new Error(`Vuestic config file not found at ${configPath}`)
+ } else {
+ 'export default {}'
+ }
+ }
+ },
+ }
+}
diff --git a/packages/compiler/vuestic-config/plugins/config-types.ts b/packages/compiler/vuestic-config/plugins/config-types.ts
new file mode 100644
index 0000000000..4262a7315c
--- /dev/null
+++ b/packages/compiler/vuestic-config/plugins/config-types.ts
@@ -0,0 +1,65 @@
+import { Plugin } from 'vite'
+import { resolveVuesticConfigPath} from './config-resolver'
+import { writeFile, mkdir } from 'fs/promises'
+import { resolve } from 'path'
+
+const typeModule = (configPath: string) => {
+ return `
+import config from '${configPath}';
+import { PartialGlobalConfig, type IconConfiguration } from 'vuestic-ui';
+
+type ExtractColorNames = T['colors'] extends { variables: infer U } ? keyof U : never
+type ExtractIconNames = T['icons'] extends IconConfiguration[] ? U : never
+type ExtractI18nKeys = keyof T['i18n']
+
+
+type Config = typeof config
+
+type ColorNames = ExtractColorNames
+
+declare module 'vuestic-ui' {
+ export interface CustomColorVariables extends Record {}
+}
+
+type IconNames = ExtractIconNames
+
+declare module 'vuestic-ui' {
+ export interface CustomIconVariables extends Record {}
+}
+
+type I18nKeys = ExtractI18nKeys
+
+declare module 'vuestic-ui' {
+ export interface CustomI18NKeys extends Record {}
+}
+`.trim()
+}
+
+/** Generate TS types for colors, icons and i18n messages from `vuestic.config.ts` */
+export const configTypes = (options: {
+ configPath?: string
+} = {}): Plugin => {
+
+ return {
+ name: 'vuestic:config-types',
+
+ async buildStart() {
+ let configPath = options.configPath || resolveVuesticConfigPath()
+
+ if (!configPath) {
+ return
+ }
+
+ configPath = resolve(configPath)
+
+ const module = typeModule(configPath)
+
+ await mkdir('node_modules/.vuestic', { recursive: true })
+
+ await writeFile('node_modules/.vuestic/config.d.ts', module, {
+ encoding: 'utf-8',
+ flag: 'w',
+ })
+ },
+ }
+}
diff --git a/packages/compiler/vuestic-config/plugins/use-config.ts b/packages/compiler/vuestic-config/plugins/use-config.ts
new file mode 100644
index 0000000000..7dd0d281c4
--- /dev/null
+++ b/packages/compiler/vuestic-config/plugins/use-config.ts
@@ -0,0 +1,40 @@
+import { Plugin } from 'vite'
+import { isEntryFile } from '../../shared/plugin/is-entry-file'
+import { addImport, compileCode, mergeVuesticPluginConfigOption } from '../../shared/plugin/js'
+import { isConfigExists } from './config-resolver'
+
+const CONFIG_IMPORT_NAME = 'vuesticConfig$va1'
+
+/** Tries to import `#vuestic-config` into main.ts file, adds it to createVuestic() */
+export const useConfig = (options: {
+ configPath?: string
+} = {}): Plugin => {
+ return {
+ name: 'vuestic:use-config',
+
+ transform(code, id) {
+ if (!isEntryFile(id)) {
+ return
+ }
+
+ if (!isConfigExists(options.configPath)) {
+ return
+ }
+
+ let newCode = addImport(code, `import ${CONFIG_IMPORT_NAME} from '#vuestic-config'`)
+
+ const createVuestic = mergeVuesticPluginConfigOption(newCode, 'createVuestic', CONFIG_IMPORT_NAME)
+ const createVuesticEssential = mergeVuesticPluginConfigOption(newCode, 'createVuesticEssential', CONFIG_IMPORT_NAME)
+
+ if (createVuestic) {
+ return compileCode(createVuestic)
+ }
+
+ if (createVuesticEssential) {
+ return compileCode(createVuesticEssential)
+ }
+
+ throw new Error('createVuestic or createVuesticEssential not found')
+ },
+ }
+}
diff --git a/packages/create-vuestic/src/steps/1.scaffoldProject.ts b/packages/create-vuestic/src/steps/1.scaffoldProject.ts
index 19715a06c5..361be8c6f6 100644
--- a/packages/create-vuestic/src/steps/1.scaffoldProject.ts
+++ b/packages/create-vuestic/src/steps/1.scaffoldProject.ts
@@ -3,11 +3,12 @@ import { UserAnswers } from './../prompts';
import { createVue } from "../generators/create-vue"
import { createNuxt3 } from "../generators/create-nuxt"
import { createVuesticAdmin } from '../generators/create-vuestic-admin';
+import { resolvePath } from "../utils/resolve-path"
export const scaffoldProject = async (options: UserAnswers) => {
const { projectName, projectType, projectFeatures = [] } = options
- const path = [process.cwd(), projectName].join('/')
+ const path = resolvePath(process.cwd(), projectName)!
try {
if (projectType === 'create-vue') {
diff --git a/packages/create-vuestic/src/steps/3.initGit.ts b/packages/create-vuestic/src/steps/3.initGit.ts
index b6098ab077..2bf8cb0572 100644
--- a/packages/create-vuestic/src/steps/3.initGit.ts
+++ b/packages/create-vuestic/src/steps/3.initGit.ts
@@ -1,5 +1,6 @@
import { useUserAnswers } from '../composables/useUserAnswers';
import { execp } from './../utils/exacp';
+import { resolvePath } from "../utils/resolve-path"
export const initGit = async () => {
const { runGitInit, projectName } = await useUserAnswers()
@@ -7,6 +8,6 @@ export const initGit = async () => {
if (!runGitInit) { return }
return execp('git init', {
- cwd: `${process.cwd()}/${projectName}`,
+ cwd: resolvePath(process.cwd(), projectName)!,
})
}
diff --git a/packages/create-vuestic/src/steps/4.installDeps.ts b/packages/create-vuestic/src/steps/4.installDeps.ts
index 03b804d100..8e7a9ee4ed 100644
--- a/packages/create-vuestic/src/steps/4.installDeps.ts
+++ b/packages/create-vuestic/src/steps/4.installDeps.ts
@@ -2,6 +2,7 @@ import { useUserAnswers } from '../composables/useUserAnswers';
import { getPackageManagerName } from '../utils/package-manager';
import { execp } from '../utils/exacp';
import { withSpinner } from '../utils/with-spinner';
+import { resolvePath } from "../utils/resolve-path"
export const installDeps = async () => {
const { runInstall, projectName } = await useUserAnswers()
@@ -12,7 +13,7 @@ export const installDeps = async () => {
return await withSpinner('Installing dependencies...', async () => {
await execp(`${packageManager} install`, {
- cwd: `${process.cwd()}/${projectName}`,
+ cwd: resolvePath(process.cwd(), projectName)!,
})
})
return
diff --git a/packages/docs/components/landing/Footer.vue b/packages/docs/components/landing/Footer.vue
index 5e8ddbfc04..e14c899eca 100644
--- a/packages/docs/components/landing/Footer.vue
+++ b/packages/docs/components/landing/Footer.vue
@@ -71,6 +71,11 @@
•
Cookie Policy
+
@@ -115,6 +120,7 @@ const sitemap = computed(() => ([
{
title: 'Support',
items: [
+ { label: 'Support & Consulting', component: 'router-link', prop: 'to', value: '/support/consulting' },
{ label: 'Give us a star\u00A0\u2B50', component: 'a', prop: 'href', value: 'https://github.com/epicmaxco/vuestic-ui/' },
{ label: 'Report an issue', component: 'a', prop: 'href', value: 'https://github.com/epicmaxco/vuestic-ui/issues/new/choose' },
{ label: 'Contribute', component: 'router-link', prop: 'to', value: '/contribution/guide' },
diff --git a/packages/docs/components/landing/Header.vue b/packages/docs/components/landing/Header.vue
index b027a072b8..ea8f556d5d 100644
--- a/packages/docs/components/landing/Header.vue
+++ b/packages/docs/components/landing/Header.vue
@@ -35,6 +35,13 @@
>
Contribute
+
+
+
+