From 663ffb1c09d2e37cd87aa4438f9767daa3cb935a Mon Sep 17 00:00:00 2001 From: FernandoJVideira <03.pleaser-minster@icloud.com> Date: Wed, 16 Sep 2026 10:57:18 +0100 Subject: [PATCH] Scaffold clients with vue-ui-kit instead of Nuxt UI / shadcn-vue Both `draft new client` and `draft new monorepo` (which reuses the same client scaffolding for apps/client) now wire up @fernandojvideira/vue-ui-kit as the component library on both frameworks, installed from its Gitea npm registry. Vue: drops the shadcn-vue init/add steps entirely - vue-ui-kit's theme.css is imported directly instead of a generated one, and the overlay's one shadcn Button usage now comes from vue-ui-kit. Nuxt: switches the nuxi starter from "ui" (Nuxt UI) to "minimal", wiring Tailwind v4 + vue-ui-kit by hand instead (mirrors vue-ui-kit's own Nuxt INSTALL.md instructions). Needed one real fix: the "minimal" starter has no typescript devDependency, and pnpm resolving it unpinned lands on TypeScript 7, which breaks @vue/compiler-sfc's cross-file type resolution that vue-ui-kit's components rely on - pinned to ~6.0.0 to match create-vue's own default for the Vue flow. The actual overlay content changes (token renames, icon swaps, a new useToast() composable for Nuxt, etc.) live in the project-template submodule, bumped here to its latest commit. Verified end-to-end: both frameworks install, typecheck, and build clean from a fresh `draft new client`, and were checked live in the browser. Co-Authored-By: Claude Sonnet 5 --- README.md | 4 +- assets/testdata/template | 2 +- cmd/client.go | 134 +++++++++++++++++++++------------------ 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 8f5eb79..b151ffe 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,10 @@ draft new client 1. Prompts for a project name 2. Prompts you to pick a framework — **Nuxt** or **Vue** -3. Scaffolds the base app via the official tooling (`nuxi init` for Nuxt, `create-vue` + `shadcn-vue` for Vue), so the generated boilerplate, dependency versions, and lockfile are always current rather than pinned by Draft +3. Scaffolds the base app via the official tooling (`nuxi init` for Nuxt, `create-vue` for Vue), so the generated boilerplate, dependency versions, and lockfile are always current rather than pinned by Draft 4. Layers a Draft-maintained overlay on top: an auth store (login/logout/register/token refresh), route guards, API composable, and pages for login/signup/profile/organizations/email verification/invites — all wired up to talk to a Draft-generated API out of the box -Both frameworks produce a comparable app: Nuxt UI + Tailwind for Nuxt, shadcn-vue + Tailwind for Vue. +Both frameworks produce a comparable app: [`vue-ui-kit`](https://git.fernandovideira.com/FernandoJVideira/vue-ui-kit) (installed from its own Gitea npm registry - see its README for the one-time `.npmrc` token setup) + Tailwind, the same component library and theme tokens on either framework. ### Create a New Monorepo Project diff --git a/assets/testdata/template b/assets/testdata/template index 3945b8b..c8c41b6 160000 --- a/assets/testdata/template +++ b/assets/testdata/template @@ -1 +1 @@ -Subproject commit 3945b8b64a9adcaedc7dd9421dab7b7f3a73a415 +Subproject commit c8c41b699644adef663ab3b148cbb0f6958392cc diff --git a/cmd/client.go b/cmd/client.go index 34cc6e2..51b67d3 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -54,10 +54,14 @@ func genClient(cmd *cobra.Command, args []string) { } func scaffoldClientNuxt(projectDir, serviceName string) error { + // "minimal", not "ui" - the Nuxt UI starter is what we'd otherwise get + // for free, but this project uses vue-ui-kit as its component library + // instead, wired up by hand below (Tailwind v4 + the overlay's own + // app/assets/css/main.css already import it). if err := runCommand("", "npx", "nuxi@latest", "init", projectDir, "--no-install", "--packageManager=pnpm", - "-t", "ui", + "-t", "minimal", "--gitInit=false", "-M", "@pinia/nuxt", ); err != nil { @@ -76,7 +80,21 @@ func scaffoldClientNuxt(projectDir, serviceName string) error { } fmt.Println("📦 Installing dependencies...") - return runCommand(projectDir, "pnpm", "add", "pinia", "@pinia/nuxt") + if err := runCommand(projectDir, "pnpm", "add", "pinia", "@pinia/nuxt", + "@fernandojvideira/vue-ui-kit", "lucide-vue-next", + ); err != nil { + return fmt.Errorf("error installing dependencies: %w", err) + } + + // typescript itself: the "minimal" starter (unlike "ui", and unlike + // create-vue's own output) doesn't include it, but @vue/compiler-sfc + // needs a real TypeScript install to resolve cross-file type extends - + // exactly what several vue-ui-kit components' Props interfaces do. + // Pinned below 7.x (matching create-vue's own "~6.0.0" default) since + // @vue/compiler-sfc's cross-file resolution isn't compatible with + // TypeScript 7 yet - it fails with "No fs option provided to + // compileScript in non-Node environment" on every extended interface. + return runCommand(projectDir, "pnpm", "add", "-D", "tailwindcss", "@tailwindcss/vite", "typescript@~6.0.0", "vue-tsc") } func patchNuxtConfig(projectDir, serviceName string) error { @@ -86,7 +104,25 @@ func patchNuxtConfig(projectDir, serviceName string) error { return fmt.Errorf("error reading nuxt.config.ts: %w", err) } + // The "minimal" starter registers no Tailwind plugin and no CSS entry + // point on its own (the "ui" starter did both, bundled inside @nuxt/ui) - + // both are added explicitly here since the overlay's own main.css needs + // to actually run. + newContent := strings.Replace(string(content), + "// https://nuxt.com/docs/api/configuration/nuxt-config\n", + "// https://nuxt.com/docs/api/configuration/nuxt-config\nimport tailwindcss from '@tailwindcss/vite'\n\n", + 1, + ) + if newContent == string(content) { + return fmt.Errorf("could not find the file-header anchor in nuxt.config.ts") + } + insertion := fmt.Sprintf(` + css: ['~/assets/css/main.css'], + vite: { + plugins: [tailwindcss()], + }, + app: { head: { title: %q, @@ -102,12 +138,12 @@ func patchNuxtConfig(projectDir, serviceName string) error { }, `, serviceName, serviceName) - newContent := strings.Replace(string(content), "defineNuxtConfig({", "defineNuxtConfig({"+insertion, 1) - if newContent == string(content) { + finalContent := strings.Replace(newContent, "defineNuxtConfig({", "defineNuxtConfig({"+insertion, 1) + if finalContent == newContent { return fmt.Errorf("could not find defineNuxtConfig({ anchor in nuxt.config.ts") } - return os.WriteFile(configPath, []byte(newContent), 0644) + return os.WriteFile(configPath, []byte(finalContent), 0644) } func scaffoldClientVue(projectDir, serviceName string) error { @@ -151,8 +187,8 @@ func scaffoldClientVue(projectDir, serviceName string) error { if err := os.MkdirAll(assetsDir, 0755); err != nil { return fmt.Errorf("error creating src/assets: %w", err) } - if err := os.WriteFile(filepath.Join(assetsDir, "main.css"), []byte("@import \"tailwindcss\";\n"), 0644); err != nil { - return fmt.Errorf("error writing main.css stub: %w", err) + if err := os.WriteFile(filepath.Join(assetsDir, "main.css"), []byte(vueUiKitMainCSS), 0644); err != nil { + return fmt.Errorf("error writing main.css: %w", err) } if err := patchViteConfigForTailwind(projectDir); err != nil { @@ -161,23 +197,6 @@ func scaffoldClientVue(projectDir, serviceName string) error { if err := patchTsconfigAlias(projectDir); err != nil { return err } - - fmt.Println("🎨 Setting up shadcn-vue...") - if err := runCommand(projectDir, "pnpm", "dlx", "shadcn-vue@latest", "init", - "-t", "vite", "-b", "neutral", "--icon-library", "lucide", "-y", "-d", - ); err != nil { - return fmt.Errorf("error running shadcn-vue init: %w", err) - } - - if err := runCommand(projectDir, "pnpm", "dlx", "shadcn-vue@latest", "add", - "button", "input", "label", "select", "-y", - ); err != nil { - return fmt.Errorf("error adding shadcn-vue components: %w", err) - } - - if err := patchMainCssTokens(projectDir); err != nil { - return err - } if err := patchEslintConfig(projectDir); err != nil { return err } @@ -190,9 +209,36 @@ func scaffoldClientVue(projectDir, serviceName string) error { } fmt.Println("📦 Installing remaining dependencies...") - return runCommand(projectDir, "pnpm", "add", "ofetch", "vue-sonner") + return runCommand(projectDir, "pnpm", "add", "ofetch", "@fernandojvideira/vue-ui-kit", "lucide-vue-next") } +// The library's theme brings its own tokens; --success/--warning are added on +// top the same way a consumer would (see vue-ui-kit's THEMING.md) since the +// base theme deliberately doesn't define them. +const vueUiKitMainCSS = `@import "tailwindcss"; +@source "../../node_modules/@fernandojvideira/vue-ui-kit/src"; +@import "@fernandojvideira/vue-ui-kit/theme.css"; + +:root { + --success: 142 71% 35%; + --success-foreground: 0 0% 98%; + --warning: 38 92% 40%; + --warning-foreground: 0 0% 98%; +} +.dark { + --success: 142 71% 45%; + --success-foreground: 0 0% 9%; + --warning: 38 92% 55%; + --warning-foreground: 0 0% 9%; +} +@theme inline { + --color-success: hsl(var(--success)); + --color-success-foreground: hsl(var(--success-foreground)); + --color-warning: hsl(var(--warning)); + --color-warning-foreground: hsl(var(--warning-foreground)); +} +` + func patchPackageManagerField(projectDir, version string) error { pkgPath := filepath.Join(projectDir, "package.json") content, err := os.ReadFile(pkgPath) @@ -253,44 +299,6 @@ func patchTsconfigAlias(projectDir string) error { return os.WriteFile(configPath, []byte(newContent), 0644) } -func patchMainCssTokens(projectDir string) error { - cssPath := filepath.Join(projectDir, "src", "assets", "main.css") - content, err := os.ReadFile(cssPath) - if err != nil { - return fmt.Errorf("error reading main.css: %w", err) - } - s := string(content) - - s2 := strings.Replace(s, - "--color-background: var(--background);", - "--color-background: var(--background);\n --color-success: var(--success);\n --color-success-foreground: var(--success-foreground);\n --color-warning: var(--warning);\n --color-warning-foreground: var(--warning-foreground);", - 1, - ) - if s2 == s { - return fmt.Errorf("could not find @theme anchor in main.css") - } - - s3 := strings.Replace(s2, - "}\n\n.dark {", - " --success: oklch(0.6 0.15 145);\n --success-foreground: oklch(0.985 0 0);\n --warning: oklch(0.75 0.15 80);\n --warning-foreground: oklch(0.145 0 0);\n}\n\n.dark {", - 1, - ) - if s3 == s2 { - return fmt.Errorf("could not find :root close anchor in main.css") - } - - s4 := strings.Replace(s3, - "}\n\n@layer base {", - " --success: oklch(0.65 0.15 145);\n --success-foreground: oklch(0.145 0 0);\n --warning: oklch(0.8 0.15 80);\n --warning-foreground: oklch(0.145 0 0);\n}\n\n@layer base {", - 1, - ) - if s4 == s3 { - return fmt.Errorf("could not find .dark close anchor in main.css") - } - - return os.WriteFile(cssPath, []byte(s4), 0644) -} - func patchEslintConfig(projectDir string) error { configPath := filepath.Join(projectDir, "eslint.config.ts") content, err := os.ReadFile(configPath)