Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const items: DropdownMenuItem[][] = [
[
{
label: 'View',
icon: 'i-lucide-eye'
},
{
label: 'Copy',
icon: 'i-lucide-copy'
},
{
label: 'Edit',
icon: 'i-lucide-pencil'
}
],
[
{
label: 'Delete',
color: 'error',
icon: 'i-lucide-trash'
}
]
]
</script>

<template>
<UDropdownMenu :items="items" :ui="{ content: 'w-(--reka-dropdown-menu-trigger-width)' }">
<UButton
label="Open"
class="w-46"
color="neutral"
variant="outline"
block
trailing-icon="i-lucide-chevron-down"
/>
</UDropdownMenu>
</template>
13 changes: 13 additions & 0 deletions docs/content/docs/2.components/dropdown-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,19 @@ defineShortcuts(extractShortcuts(items))
In this example, :kbd{value="meta"} :kbd{value="E"}, :kbd{value="meta"} :kbd{value="I"} and :kbd{value="meta"} :kbd{value="N"} would trigger the `select` function of the corresponding item.
::

### Match content & trigger width

You can set `--reka-dropdown-menu-trigger-width` css variable as content width to match the trigger button width.

::component-example
---

collapse: true
name: 'dropdown-menu-match-content-width-with-trigger-width-example'
---

::

## API

### Props
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
],
"scripts": {
"build": "nuxt-module-build build",
"build:repl:prepare": "node scripts/generate-repl-exports.mjs",
"build:repl": "pnpm build:repl:prepare && vite build -c vite.repl.config.ts",
"prepack": "pnpm build",
"dev": "nuxt dev playgrounds/nuxt --uiDev",
"dev:build": "nuxt build playgrounds/nuxt",
Expand Down Expand Up @@ -171,7 +173,8 @@
"release-it": "^19.0.4",
"vitest": "^3.2.4",
"vitest-environment-nuxt": "^1.0.1",
"vue-tsc": "^3.0.6"
"vue-tsc": "^3.0.6",
"@vitejs/plugin-vue": "^5.1.4"
},
"peerDependencies": {
"@inertiajs/vue3": "^2.0.7",
Expand Down
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions scripts/generate-repl-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { writeFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join, resolve } from 'pathe'
import { pascalCase } from 'scule'
import { glob } from 'tinyglobby'

const rootDir = dirname(fileURLToPath(import.meta.url))
const r = (...p) => resolve(rootDir, '..', ...p)

const componentsDir = 'src/runtime/components'
const outFile = r('src/repl/components.generated.ts')

// Collect only top-level .vue files (no recursion) to exclude typography/prose etc.
const files = (await glob('*.vue', { cwd: componentsDir, absolute: false })).sort()

// Map base component name -> relative path (prefer shallower path if duplicate)
const seen = new Map()
for (const rel of files) {
const baseFile = rel.split('/').pop()
if (!baseFile) continue
const rawBase = baseFile.replace(/\.vue$/i, '')
// Convert to PascalCase in case filenames have dashes (e.g., page-hero.vue)
const base = pascalCase(rawBase)
// All are depth 1 in this mode; simply record if not already present
if (seen.has(base)) continue
seen.set(base, { rel, depth: 1 })
}

const names = Array.from(seen.keys()).sort()
const lines = [
'// AUTO-GENERATED FILE. DO NOT EDIT.',
'// Generated by scripts/generate-repl-exports.mjs',
''
]
for (const base of names) {
if (base.startsWith('_')) continue
const exportName = 'U' + base
const rel = seen.get(base).rel
const relPath = join('..', 'runtime', 'components', rel)
lines.push(`export { default as ${exportName} } from ${JSON.stringify(relPath)}`)
}
lines.push('')

writeFileSync(outFile, lines.join('\n'), 'utf8')
console.log(`Generated ${outFile} (top-level only) with ${names.length} component exports (scanned ${files.length} files).`)
15 changes: 15 additions & 0 deletions src/repl/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Minimal app.config used by Vue REPL builds
// Mirrors a tiny subset of Nuxt appConfig consumed by useAppConfig and components
const appConfig = {
ui: {
icons: {
dark: 'i-lucide-moon',
light: 'i-lucide-sun'
}
},
colorMode: {
preference: 'system'
}
}

export default appConfig
2 changes: 2 additions & 0 deletions src/repl/components.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// AUTO-GENERATED FILE. DO NOT EDIT.
// Generated by scripts/generate-repl-exports.mjs
13 changes: 13 additions & 0 deletions src/repl/image-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineComponent, h } from 'vue'

// Minimal image component stub for REPL builds
export default defineComponent({
name: 'UiImageStub',
props: {
src: { type: String, default: '' },
alt: { type: String, default: '' }
},
setup(props, { attrs }) {
return () => h('img', { ...attrs, src: props.src, alt: props.alt })
}
})
12 changes: 12 additions & 0 deletions src/repl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// ESM entry for Vue REPL usage
// Expose a curated set of Vue-compatible components and utilities.

// Vue overrides live under runtime/vue/components
// Generated top-level component exports
export * from './components.generated'

// Minimal composables often used in examples
export { useAppConfig } from '../runtime/vue/composables/useAppConfig'

// Types re-export for TS users in REPL-like setups (optional)
export type * from '../runtime/types'
81 changes: 81 additions & 0 deletions vite.repl.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// This Vite config builds a single-file ESM bundle that can be loaded by the Vue REPL.
// The REPL expects a plain ES module URL and already provides its own Vue runtime.
// Our goal here is to:
// - Compile our .vue Single File Components into JavaScript (via @vitejs/plugin-vue)
// - Stub/alias Nuxt-only virtual imports so the code can run outside Nuxt
// - Produce dist/repl-esm/index.js that you can host and reference in an import map
import vue from '@vitejs/plugin-vue'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'pathe'
import { defineConfig } from 'vite'

const rootDir = dirname(fileURLToPath(import.meta.url))
const r = (...p: string[]) => join(rootDir, ...p)

export default defineConfig({
// Plugins run in order.
// - emptyTheme(): treats all `#build/ui/*` imports as an empty theme object.
// - vue(): compiles .vue files.
plugins: [emptyTheme(), vue()],
resolve: {
alias: [
// Nuxt auto-imports (`#imports`) don't exist in a plain Vite build.
// Point them to a small stub that provides minimal replacements used by our components.
{ find: '#imports', replacement: r('src/runtime/vue/stubs.ts') },

// The library uses `useAppConfig()` which reads from `#build/app.config` in Nuxt.
// For REPL builds, we provide a tiny `app.config.ts` with the few values our examples need.
{ find: '#build/app.config', replacement: r('src/repl/app.config.ts') },

// Some components import an image component via a virtual module.
// In the REPL, we alias it to a simple <img> wrapper.
{ find: '#build/ui-image-component', replacement: r('src/repl/image-component.ts') }
]
},
build: {
lib: {
// A small, curated entry that re-exports only the components and utilities
// we want to expose in the REPL (see src/repl/index.ts).
entry: r('src/repl/index.ts'),
// We only need ESM for the Vue REPL.
formats: ['es'],
// Create an easy-to-reference file name.
fileName: () => 'index.js',
// UMD name is unused for ESM, but Vite requires a name in lib mode.
name: 'NuxtUiRepl'
},
// Output folder for the REPL bundle.
outDir: r('dist/repl-esm'),
// Clean the folder before each build.
emptyOutDir: true,
rollupOptions: {
// IMPORTANT: Do not bundle Vue. The Vue REPL provides its own Vue runtime.
// Marking it external keeps our bundle lightweight and avoids version conflicts.
external: ['vue'],
output: {
globals: { vue: 'Vue' }
}
}
}
})

// Simplest possible theme handling for REPL builds:
// We do NOT expose theme customization and we don't run Nuxt's code generator.
// Map ALL `#build/ui/*` imports to a single virtual module returning an empty theme
// object with the minimal shape expected by components that call tv(theme).
// This eliminates the need for any files under src/repl/theme.
export function emptyTheme() {
const VIRTUAL_ID = '\0nuxt-ui-empty-theme'
const THEME_CODE = 'export default { base: "", variants: {} }\n'
return {
name: 'nuxt-ui:repl-empty-theme',
resolveId(id: string) {
if (/^#build\/ui\//.test(id)) return VIRTUAL_ID
return null
},
load(id: string) {
if (id === VIRTUAL_ID) return THEME_CODE
return null
}
}
}