From fb0667f99094892ddeaa7e8ee3c30017c85ee9fa Mon Sep 17 00:00:00 2001 From: Mike Newbon Date: Sun, 5 Jul 2026 00:20:33 +0200 Subject: [PATCH 001/185] docs: add theme studio master plan Plan for the Nuxt UI theme customizer: standalone /theme page in the main header nav, ThemeDoc + provenance resolver engine, OKLCH palette curve editor, inheritance drill-down, presets + shuffle, minimal CSS/app.config exports, bento + template previews, and a phased roadmap with upstream core-gap PRs. Co-Authored-By: Claude Fable 5 --- THEME_STUDIO_PLAN.md | 328 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 THEME_STUDIO_PLAN.md diff --git a/THEME_STUDIO_PLAN.md b/THEME_STUDIO_PLAN.md new file mode 100644 index 0000000000..6cd4a11f20 --- /dev/null +++ b/THEME_STUDIO_PLAN.md @@ -0,0 +1,328 @@ +# Nuxt UI Theme Studio — Master Plan + +> Working plan for the "ultimate theme customizer" contribution, targeting Nuxt UI v5. +> Status: draft for discussion. + +## 1. Vision + +Bootstrap sites "all looked the same" only when people shipped the default CSS. The fix was +never more components — it was making deep customization *approachable*. Nuxt UI has the +best raw material of any component system today: a clean chain of Tailwind palette → color +aliases → semantic CSS variables → component variants, split across CSS and `app.config`. +What it lacks is a rich theming ecosystem and a tool that exposes that chain. + +The Theme Studio is a standalone page (main header navigation, not part of the docs) that: + +- lets you go from "pick a primary color" to "edit the OKLCH curve of your neutral palette" + on one continuous slope of drill-down, Blender-style; +- shows *where every value comes from* (inherited vs. overridden), Bootstrap-variable-chain-style; +- ships opinionated presets (Shadcn, Neo-brutalist, Anthropic, Spotify, …) and a taste-constrained shuffle; +- exports the **minimal** `main.css` + `app.config.ts` diff — only what you changed; +- previews live on a component bento and on template-scale pages (SaaS, Chat, Dashboard). + +North star: someone who has never opened the theming docs produces a theme that doesn't +look like default Nuxt UI in 60 seconds — and someone who cares can drill all the way down +to a single semantic token or component slot without leaving the page. + +## 2. Ground truth: how Nuxt UI theming works today (v4) + +The studio must be a *lens over the real system*, not a parallel one. The real system is a +five-level inheritance chain: + +| Level | What | Where | Mechanism | +|---|---|---|---| +| L0 | Palette: `--color-{name}-{50..950}` | Tailwind `@theme` / CSS | Tailwind defaults, overridable in `@theme static` | +| L1 | Alias shades: `--ui-color-{alias}-{shade}` | `src/runtime/plugins/colors.ts` | Generated at runtime from `app.config ui.colors` (`primary`, `secondary`, `success`, `info`, `warning`, `error`, `neutral`), each shade falls back through `var(--color-{value}-{shade}, hex)` | +| L2 | Semantic tokens: `--ui-text[-dimmed/muted/toned/highlighted/inverted]`, `--ui-bg[-muted/elevated/accented/inverted]`, `--ui-border[-muted/accented/inverted]`, `--ui-primary` (=shade 500 light / 400 dark), `--ui-radius`, `--font-sans` | `src/runtime/index.css` (`:root` / `.dark`) | Plain CSS custom properties referencing L1 | +| L3 | Component themes: slots / variants / compoundVariants | `src/theme/*.ts` via `tv()` | Tailwind classes referencing L2 utilities (`text-muted`, `bg-elevated`, `rounded-md`…) | +| L4 | Defaults & overrides: `defaultVariants` + `app.config ui.` | components spread `appConfig.ui?.` into their `tv()` options | Runtime-mergeable, including `defaultVariants` per component | + +Facts that matter for the design (verified in source): + +- **Per-component `defaultVariants` are already overridable at runtime** via + `app.config ui.button.defaultVariants` etc. (`Button.vue:95-108` spreads `appConfig.ui?.button` + into the `tv()` options). +- **Global `defaultVariants`** exist only as a *build-time* module option and only for + `color` and `size` (`src/module.ts:67`) — there is no global "default variant = subtle" + switch. Gap → §7. +- The docs site already live-applies themes with zero flash: `useHead` inline ` diff --git a/docs/app/components/theme-studio/ThemeStudioViewSaas.vue b/docs/app/components/theme-studio/ThemeStudioViewSaas.vue index 193dc33783..6d94a33289 100644 --- a/docs/app/components/theme-studio/ThemeStudioViewSaas.vue +++ b/docs/app/components/theme-studio/ThemeStudioViewSaas.vue @@ -10,10 +10,12 @@ const navItems: NavigationMenuItem[] = [ const heroLinks = [{ label: 'Get started', - trailingIcon: 'i-lucide-arrow-right' + trailingIcon: 'i-lucide-arrow-right', + size: 'xl' as const }, { label: 'Use this template', icon: 'i-lucide-github', + size: 'xl' as const, color: 'neutral' as const, variant: 'subtle' as const }] @@ -114,6 +116,12 @@ const testimonials = [{ }, { quote: 'Perfect Lighthouse scores out of the box. Our Core Web Vitals improved dramatically just by switching to Nuxt UI components.', user: { name: 'Emily Zhang', description: 'Lead Architect at ScaleForce' } +}, { + quote: 'The Tailwind Variants system makes customization so intuitive. We can override any component style while keeping the functionality intact.', + user: { name: 'James Wilson', description: 'DevOps Lead at CloudPro' } +}, { + quote: 'From the SaaS template to production in days. The pre-built layouts, navigation and content structure accelerated our launch.', + user: { name: 'Lisa Patel', description: 'CEO at AutoScale' } }] const ctaLinks = [{ @@ -152,6 +160,40 @@ const footerColumns = [{ }] const email = ref('') + +// Hero background beam fades in on mount, like the template's HeroBackground. +const heroAppear = ref(false) +const heroAppeared = ref(false) + +onMounted(() => { + setTimeout(() => { + heroAppear.value = true + setTimeout(() => { + heroAppeared.value = true + }, 1000) + }, 0) +}) + +// Rising stars behind the CTA, ported from the template's StarsBg. +interface Star { + x: number + y: number + size: number +} + +function generateStars(count: number): Star[] { + return Array.from({ length: count }, () => ({ + x: Math.floor(Math.random() * 2000), + y: Math.floor(Math.random() * 2000), + size: Math.random() + 1 + })) +} + +const starLayers = [ + { stars: generateStars(80), duration: 100, opacity: 1 }, + { stars: generateStars(60), duration: 150, opacity: 0.75 }, + { stars: generateStars(60), duration: 200, opacity: 0.5 } +]