diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 69c6549f..e8f4f2e3 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -18,9 +18,11 @@ module.exports = { // Typescript '@typescript-eslint/type-annotation-spacing': 'error', + '@typescript-eslint/semi': ['error', 'never'], // Vuejs 'vue/multi-word-component-names': 0, + 'vue/require-default-prop': 0, 'vue/html-indent': ['error', 2], 'vue/comma-spacing': ['error', { before: false, after: true }], 'vue/script-indent': ['error', 2, { baseIndent: 0 }], diff --git a/.gitignore b/.gitignore index e431e6c3..4a7f73a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,24 @@ -node_modules -*.log -.nuxt -nuxt.d.ts +# Nuxt dev/build outputs .output -dist -.DS_Store -.history -.vercel -.idea -.env .data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git a/.nuxtrc b/.nuxtrc index 820d356b..7bfc3d5c 100644 --- a/.nuxtrc +++ b/.nuxtrc @@ -1 +1 @@ -typescript.includeWorkspace=true +modules[]=nuxt-ui-dev-module diff --git a/cli/commands/init.mjs b/cli/commands/init.mjs new file mode 100644 index 00000000..a6e5855e --- /dev/null +++ b/cli/commands/init.mjs @@ -0,0 +1,50 @@ +import { existsSync, promises as fsp } from 'node:fs' +import { resolve } from 'pathe' +import { defineCommand } from 'citty' +import { consola } from 'consola' +import { camelCase } from 'scule' +import templates from '../utils/templates.mjs' + +export default defineCommand({ + meta: { + name: 'init', + description: 'Init a new component.' + }, + args: { + name: { + type: 'positional', + required: true, + description: 'Name of the component.' + } + }, + async setup ({ args }) { + const name = args.name + if (!name) { + consola.error('name argument is missing!') + process.exit(1) + } + + const path = resolve('.') + + for (const template of Object.keys(templates)) { + const { filename, contents } = templates[template]({ name }) + const filePath = resolve(path, filename) + + if (existsSync(filePath)) { + consola.error(`๐Ÿšจ ${filePath} already exists!`) + continue + } + + await fsp.writeFile(filePath, contents.trim() + '\n') + + consola.success(`๐Ÿช„ Generated ${filePath}!`) + } + + const themePath = resolve(path, 'src/theme/index.ts') + const theme = await fsp.readFile(themePath, 'utf-8') + const contents = `export { default as ${camelCase(name)} } from './${camelCase(name)}'` + if (!theme.includes(contents)) { + await fsp.writeFile(themePath, theme.trim() + '\n' + contents + '\n') + } + } +}) diff --git a/cli/index.mjs b/cli/index.mjs new file mode 100644 index 00000000..a816fbe8 --- /dev/null +++ b/cli/index.mjs @@ -0,0 +1,14 @@ +import { defineCommand, runMain } from 'citty' +import init from './commands/init.mjs' + +const main = defineCommand({ + meta: { + name: 'nuxtui', + description: 'Nuxt UI CLI' + }, + subCommands: { + init + } +}) + +runMain(main) diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 00000000..255877ec --- /dev/null +++ b/cli/package.json @@ -0,0 +1,12 @@ +{ + "name": "nuxt-ui-cli", + "exports": { + ".": "./index.mjs" + }, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "pathe": "^1.1.2", + "scule": "^1.3.0" + } +} diff --git a/cli/utils/templates.mjs b/cli/utils/templates.mjs new file mode 100644 index 00000000..0de57453 --- /dev/null +++ b/cli/utils/templates.mjs @@ -0,0 +1,120 @@ +import { splitByCase, upperFirst, camelCase, kebabCase } from 'scule' + +const playground = ({ name }) => { + const upperName = splitByCase(name).map(p => upperFirst(p)).join('') + const kebabName = kebabCase(name) + + return { + filename: `playground/pages/${kebabName}.vue`, + contents: ` + + ` + } +} + +const component = ({ name }) => { + const upperName = splitByCase(name).map(p => upperFirst(p)).join('') + const camelName = camelCase(name) + + return { + filename: `src/runtime/components/${upperName}.vue`, + contents: ` + + + + + + ` + } +} + +const theme = ({ name }) => { + const camelName = camelCase(name) + + return { + filename: `src/theme/${camelName}.ts`, + contents: ` +export default (config: { colors: string[] }) => ({ + slots: { + root: '' + }, + variants: { + + }, + defaultVariants: { + + } +}) + ` + } +} + +const test = ({ name }) => { + const upperName = splitByCase(name).map(p => upperFirst(p)).join('') + + return { + filename: `test/components/${upperName}.spec.ts`, + contents: ` +import { describe, it, expect } from 'vitest' +import ${upperName}, { type ${upperName}Props } from '../../src/runtime/components/${upperName}.vue' +import ComponentRender from '../component-render' + +describe('${upperName}', () => { + it.each([ + ['basic case', {}], + ['with class', { props: { class: '' } }], + ['with ui', { props: { ui: {} } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: ${upperName}Props, slots?: any }) => { + const html = await ComponentRender(nameOrHtml, options, ${upperName}) + expect(html).toMatchSnapshot() + }) +}) + ` + } +} + +export default { + playground, + component, + theme, + test +} diff --git a/modules/dev/index.ts b/modules/dev/index.ts new file mode 100644 index 00000000..768a345d --- /dev/null +++ b/modules/dev/index.ts @@ -0,0 +1,22 @@ +import { createResolver, defineNuxtModule, useNuxt } from '@nuxt/kit' +import { watch } from 'chokidar' +import { debounce } from 'perfect-debounce' + +/** + * This is an internal module aiming to make the DX of developing Nuxt UI better. + */ +export default defineNuxtModule({ + meta: { + name: 'nuxt-ui-dev-module' + }, + setup () { + const nuxt = useNuxt() + const resolver = createResolver(import.meta.url) + const watcher = watch(resolver.resolve('../../src/theme')) + + const generateApp = debounce(() => nuxt.hooks.callHook('builder:generateApp')) + + watcher.on('all', generateApp) + nuxt.hook('close', () => watcher.close()) + } +}) diff --git a/modules/dev/package.json b/modules/dev/package.json new file mode 100644 index 00000000..1bb61ad8 --- /dev/null +++ b/modules/dev/package.json @@ -0,0 +1,11 @@ +{ + "name": "nuxt-ui-dev-module", + "exports": { + ".": "./index.ts" + }, + "dependencies": { + "@nuxt/kit": "latest", + "chokidar": "^3.6.0", + "perfect-debounce": "^1.0.0" + } +} diff --git a/package.json b/package.json index 56236d9d..0d144b04 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "license": "MIT", "exports": { ".": { + "types": "./dist/types.d.ts", "import": "./dist/module.mjs", "require": "./dist/module.cjs" } @@ -20,56 +21,46 @@ "node": ">=v16.20.2" }, "scripts": { - "build": "nuxt-module-build build", - "prepack": "pnpm build", - "dev": "nuxi dev docs", - "play": "nuxi dev playground", - "build:docs": "nuxi generate docs", + "prepack": "nuxt-module-build build", + "dev": "nuxi dev playground", + "dev:build": "nuxi build playground", + "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground", + "docs": "nuxi dev docs", + "docs:build": "nuxi generate docs", + "cli": "node ./cli/index.mjs", "lint": "eslint .", "lint:fix": "eslint . --fix", - "typecheck": "vue-tsc --noEmit && nuxi typecheck docs", - "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare docs", - "release": "release-it", - "test": "vitest" + "typecheck": "vue-tsc --noEmit", + "test": "vitest", + "release": "release-it" }, "dependencies": { - "@egoist/tailwindcss-icons": "^1.7.4", - "@headlessui/tailwindcss": "^0.2.0", - "@headlessui/vue": "^1.7.19", - "@iconify-json/heroicons": "^1.1.20", "@nuxt/kit": "^3.11.1", "@nuxtjs/color-mode": "^3.3.3", - "@nuxtjs/tailwindcss": "^6.11.4", - "@popperjs/core": "^2.11.8", - "@tailwindcss/aspect-ratio": "^0.4.2", - "@tailwindcss/container-queries": "^0.1.1", - "@tailwindcss/forms": "^0.5.7", - "@tailwindcss/typography": "^0.5.10", + "@tailwindcss/postcss": "4.0.0-alpha.10", + "@tailwindcss/vite": "4.0.0-alpha.10", "@vueuse/core": "^10.9.0", - "@vueuse/integrations": "^10.9.0", - "@vueuse/math": "^10.9.0", "defu": "^6.1.4", - "fuse.js": "^6.6.2", - "nuxt-icon": "^0.6.10", + "nuxt-icon": "^0.6.9", "ohash": "^1.1.3", - "pathe": "^1.1.2", - "scule": "^1.3.0", - "tailwind-merge": "^2.2.2", - "tailwindcss": "^3.4.1" + "radix-vue": "^1.5.3", + "tailwind-variants": "^0.2.1", + "tailwindcss": "4.0.0-alpha.10" }, "devDependencies": { + "@types/node": "^20.11.29", "@nuxt/eslint-config": "^0.2.0", "@nuxt/module-builder": "^0.5.5", + "@nuxt/schema": "^3.11.1", "@nuxt/test-utils": "^3.12.0", "@release-it/conventional-changelog": "^8.0.1", "@vue/test-utils": "^2.4.5", "eslint": "^8.57.0", - "happy-dom": "^14.3.6", + "happy-dom": "^14.1.0", "joi": "^17.12.2", "nuxt": "^3.11.1", + "nuxt-ui-dev-module": "workspace:*", "release-it": "^17.1.1", - "typescript": "^5.4.3", - "unbuild": "^2.0.0", "valibot": "^0.30.0", "vitest": "^1.4.0", "vitest-environment-nuxt": "^1.0.0", @@ -78,10 +69,6 @@ "zod": "^3.22.4" }, "resolutions": { - "@nuxt/kit": "^3.11.1", - "@nuxt/schema": "3.11.1", - "tailwindcss": "3.4.1", - "@headlessui/vue": "1.7.19", - "vue": "3.4.21" + "@nuxt/ui": "workspace:*" } } diff --git a/playground/app.config.ts b/playground/app.config.ts index da83d005..dce4c0e6 100644 --- a/playground/app.config.ts +++ b/playground/app.config.ts @@ -1,6 +1,6 @@ export default defineAppConfig({ ui: { - primary: 'green', + primary: 'sky', gray: 'cool' } }) diff --git a/playground/app.vue b/playground/app.vue index 5c8f5a2b..a8db9412 100644 --- a/playground/app.vue +++ b/playground/app.vue @@ -1,23 +1,50 @@ -