diff --git a/docs/content/docs/1.getting-started/2.installation/1.nuxt.md b/docs/content/docs/1.getting-started/2.installation/1.nuxt.md index 1de59af3af..b095402f70 100644 --- a/docs/content/docs/1.getting-started/2.installation/1.nuxt.md +++ b/docs/content/docs/1.getting-started/2.installation/1.nuxt.md @@ -349,6 +349,28 @@ export default defineNuxtConfig({ This option adds the `transition-colors` class on components with hover or active states. :: +### `theme.unstyled` :badge{label="Soon" class="align-text-top"} + +Use the `theme.unstyled` option to remove all default theme classes from components, keeping only their structure and the classes you provide through `class`, `ui` or `app.config.ui`. + +- Default: `false`{lang="ts-type"} + +```ts [nuxt.config.ts] {4-8} +export default defineNuxtConfig({ + modules: ['@nuxt/ui'], + css: ['~/assets/css/main.css'], + ui: { + theme: { + unstyled: true + } + } +}) +``` + +::warning +This strips **structural** classes too (positioning, transitions, flex/grid), not just cosmetic ones. Layout-heavy components like `Modal`, `Drawer` or `Calendar` will need you to re-supply their layout, similar to PrimeVue's unstyled mode. +:: + ### `theme.defaultVariants` Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components. diff --git a/docs/content/docs/1.getting-started/2.installation/2.vue.md b/docs/content/docs/1.getting-started/2.installation/2.vue.md index 86bbd13ab2..0e053c3ea7 100644 --- a/docs/content/docs/1.getting-started/2.installation/2.vue.md +++ b/docs/content/docs/1.getting-started/2.installation/2.vue.md @@ -644,6 +644,33 @@ export default defineConfig({ This option adds the `transition-colors` class on components with hover or active states. :: +### `theme.unstyled` :badge{label="Soon" class="align-text-top"} + +Use the `theme.unstyled` option to remove all default theme classes from components, keeping only their structure and the classes you provide through `class`, `ui` or `app.config.ui`. + +- Default: `false`{lang="ts-type"} + +```ts [vite.config.ts] {9-11} +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import ui from '@nuxt/ui/vite' + +export default defineConfig({ + plugins: [ + vue(), + ui({ + theme: { + unstyled: true + } + }) + ] +}) +``` + +::warning +This strips **structural** classes too (positioning, transitions, flex/grid), not just cosmetic ones. Layout-heavy components like `Modal`, `Drawer` or `Calendar` will need you to re-supply their layout, similar to PrimeVue's unstyled mode. +:: + ### `theme.defaultVariants` Use the `theme.defaultVariants` option to override the default `color` and `size` variants for components. diff --git a/src/module.ts b/src/module.ts index 26fdbc8efc..2a160a634c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -52,6 +52,14 @@ export interface ModuleOptions { */ transitions?: boolean + /** + * Remove all default theme classes from components, keeping only their + * structure and the classes you supply via `class`, `ui` or `app.config.ui`. + * @defaultValue `false` + * @see https://ui.nuxt.com/docs/getting-started/installation/nuxt#themeunstyled + */ + unstyled?: boolean + /** * The default variants to use for components * @see https://ui.nuxt.com/docs/getting-started/installation/nuxt#themedefaultvariants diff --git a/src/templates.ts b/src/templates.ts index 02a9b7607c..dfafcf6906 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -6,7 +6,7 @@ import { addTemplate, addTypeTemplate, hasNuxtModule, logger, updateTemplates, g import type { Nuxt, NuxtTemplate, NuxtTypeTemplate } from '@nuxt/schema' import type { Resolver } from '@nuxt/kit' import type { ModuleOptions } from './module' -import { applyDefaultVariants, applyPrefixToObject } from './utils/theme' +import { applyDefaultVariants, applyPrefixToObject, applyUnstyled } from './utils/theme' import { detectUsedComponents } from './utils/components' import * as theme from './theme' import * as themeProse from './theme/prose' @@ -34,6 +34,8 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record { @@ -65,15 +67,17 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record (value && typeof value === 'object' && !Array.isArray(value)) + ? Object.fromEntries(Object.keys(value as Record).map(slot => [slot, ''])) + : '' + + if (result.slots) { + result.slots = Object.fromEntries(Object.keys(result.slots).map(slot => [slot, ''])) + } + + if (result.variants) { + result.variants = Object.fromEntries( + Object.entries(result.variants).map(([name, group]) => [ + name, + Object.fromEntries(Object.entries(group as Record).map(([key, value]) => [key, blank(value)])) + ]) + ) + } + + if (result.compoundVariants) { + result.compoundVariants = result.compoundVariants.map((entry: Record) => { + const { class: cls, ...selectors } = entry + return { ...selectors, class: blank(cls) } + }) + } + + return result +} + /** * Override default variants from module options * @param result - The theme result object diff --git a/test/utils/theme.spec.ts b/test/utils/theme.spec.ts new file mode 100644 index 0000000000..4ef3979d8e --- /dev/null +++ b/test/utils/theme.spec.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest' +import { applyUnstyled } from '../../src/utils/theme' + +describe('applyUnstyled', () => { + const theme = () => ({ + slots: { + base: 'inline-flex rounded-md', + label: 'truncate' + }, + variants: { + color: { + primary: 'bg-primary text-inverted', + neutral: { base: 'bg-inverted', label: 'text-default' } + }, + size: { + md: { base: 'px-2.5 text-sm' } + } + }, + compoundVariants: [ + { color: 'primary', variant: 'solid', class: 'bg-primary' }, + { size: 'md', class: { base: 'gap-1.5' } } + ], + defaultVariants: { + color: 'primary', + size: 'md' + } + }) + + it('returns the theme untouched when unstyled is falsy', () => { + const input = theme() + expect(applyUnstyled(input, false)).toBe(input) + expect(applyUnstyled(input, undefined)).toBe(input) + expect(input).toEqual(theme()) + }) + + it('blanks every slot class but keeps the slot keys', () => { + const result = applyUnstyled(theme(), true) + expect(result.slots).toEqual({ base: '', label: '' }) + }) + + it('blanks variant classes in both string and slot-object forms', () => { + const result = applyUnstyled(theme(), true) + expect(result.variants.color.primary).toBe('') + expect(result.variants.color.neutral).toEqual({ base: '', label: '' }) + expect(result.variants.size.md).toEqual({ base: '' }) + }) + + it('blanks compoundVariants classes but keeps the selectors', () => { + const result = applyUnstyled(theme(), true) + expect(result.compoundVariants).toEqual([ + { color: 'primary', variant: 'solid', class: '' }, + { size: 'md', class: { base: '' } } + ]) + }) + + it('preserves defaultVariants and variant keys so props still validate', () => { + const result = applyUnstyled(theme(), true) + expect(result.defaultVariants).toEqual({ color: 'primary', size: 'md' }) + expect(Object.keys(result.variants)).toEqual(['color', 'size']) + expect(Object.keys(result.variants.color)).toEqual(['primary', 'neutral']) + }) +})