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](
+ Deploys by Netlify + 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 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() + `\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 @@ + + + + + + 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 @@ + + + + + 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 @@ + + +