@xyz/navigation-toolkit
v0.1.7
Published
Renderless, UI-agnostic navigation toolkit for Nuxt, with key-based actions and optional i18n.
Downloads
16
Readme
@xyz/navigation-toolkit
Renderless, UI-agnostic navigation toolkit for Nuxt, focused on DX, performance, and portability.
One flat list per context, key based actions by default, optional i18n at the edges.
Features
- Single list per context, simpler mental model
- Key based actions out of the box, clickId then actionId then key
- Renderless component that works with any UI library
- Optional i18n, pass
tandlocalePathonly where you render - SSR safe store on
nuxtApp, no serialization issues - Nuxt module options,
enabled,contexts,componentPrefix - Types exported for clean TypeScript integration
Install
Add the module to your Nuxt app.
# when published
pnpm add @xyz/navigation-toolkitEnable it in nuxt.config.ts.
export default defineNuxtConfig({
modules: ['@xyz/navigation-toolkit'],
'navigation-toolkit': {
enabled: true, // set false to make the store a no-op
contexts: ['sidebar','header','quickAction','settings'], // optional DX hints
componentPrefix: '' // for example 'X' gives <XNavItems />
}
})Types
Import types via a stable path, or a virtual alias.
// path export
import type { NavItem, NormalizedItem, Ctx } from '@xyz/navigation-toolkit/types'
// virtual alias
import type { NavItem, NormalizedItem } from '#nav-toolkit'Core concepts
- Register
NavItemdata into named contexts, for examplesidebar,header,quickAction. - Register click handlers by id once. Click resolution tries
clickId, thenactionId, thenkey. - Render with
<NavItems>which returnsitems, plus helpers forlabel,href, andonClick. - i18n is optional. If
tandlocalePathare provided, labels and links localize at render time. If not, labels fall back toitem.labelor a humanized key, and links fall back toitem.to.
API
useNav()
The store lives on nuxtApp. All methods are safe on server and client.
const nav = useNav()
nav.add(items) // NavItem | NavItem[]
nav.on('qa.new', fn) // register handler by id
nav.onKey('qa.logout', fn) // sugar for id === key
nav.attach(i => i.key.startsWith('qa.'), 'qa:shared') // assign a shared clickId by predicate
nav.off('qa.new') // remove handler
nav.clear() // clear all contexts
nav.clear('sidebar') // clear one context
const sidebarItems = nav.items('sidebar').value // NormalizedItem[]
const has = nav.has('qa.new') // boolean
nav.exec('qa.new', { item, event }) // manual invoke if neededClick handler signature
type ClickHandler = (ctx: { item: NormalizedItem, event: MouseEvent }) => void<NavItems /> renderless
Propsctx string, requiredcloseOnClick boolean, optionalt optional translate functionlocalePath optional route localizer
Default slot receives
{
items: NormalizedItem[],
label: (it) => string, // i18n aware
href: (it) => string, // locale aware
onClick: (e, it) => void // resolves action id and executes handler
}Data model
export interface NavItem {
key: string
label?: string
params?: Record<string, unknown>
ctx: string | string[]
to?: unknown
icon?: string
target?: string
position?: 'start' | 'end' | number
priority?: number
disabled?: boolean
shortcut?: string | string[]
children?: NavItem[]
clickId?: string
actionId?: string
onSelect?: (e: MouseEvent) => void
}Notesposition orders items, start is first, end is last, numbers sort in between.priority is a tiebreaker within the same position.
Children are nested items inside the same context.
Examples, Nuxt UI v3
Quick actions with UDropdownMenu (use onSelect)
<template>
<NavItems ctx="quickAction" v-slot="{ items, label, onClick }">
<UDropdownMenu
:items="[ items.map(it => ({
label: label(it),
icon: it.icon,
onSelect: (e: Event) => onClick(e as unknown as MouseEvent, it)
})) ]"
>
<UButton icon="i-lucide-command" label="Commands" color="neutral" variant="outline" />
</UDropdownMenu>
</NavItems>
</template>Sidebar with UNavigationMenu inside USlideover (use onClick for links)
<NavItems ctx="sidebar" :t="t" :localePath="localePath" v-slot="{ items, label, href, onClick }">
<UNavigationMenu
orientation="vertical"
:items="items.map(it => ({
label: label(it),
icon: it.icon,
to: href(it),
type: 'link',
onClick: (e: MouseEvent) => onClick(e, it),
children: (it.children || []).map(c => ({
label: label(c),
icon: c.icon,
to: href(c),
onClick: (e: MouseEvent) => onClick(e, c)
}))
}))"
/>
</NavItems>Flat picker with USelectMenu
<NavItems ctx="sidebar" v-slot="{ items, label, onClick }">
<USelectMenu
v-model="selected"
:items="items.map(it => ({ label: label(it), value: it.key, icon: it.icon }))"
searchable
@update:model-value="(val) => {
const it = items.find(i => i.key === val?.value)
if (it) onClick(new MouseEvent('click'), it)
}"
/>
</NavItems>i18n, optional
Pass :t="t" and :localePath="localePath" to <NavItems>.
If omitted, label(it) falls back to item.label or a humanized key, href(it) falls back to item.to.
Module options at runtime
Options are exposed at useRuntimeConfig().public['navigation-toolkit'].
{
enabled: boolean, // default true
contexts: string[], // default ['sidebar','header','footer','topbar','breadcrumb','quickAction','settings']
componentPrefix: string // optional, empty string by default
}If enabled is false, useNav() returns a no-op store with the same API.
If contexts is set, adding an item to an unknown context logs a warning in dev.
Usage, end to end
// register items
const nav = useNav()
nav.add([
{ key: 'nav.home', label: 'Home', to: '/', ctx: ['sidebar','header'], position: 'start' },
{ key: 'nav.settings', label: 'Settings', to: '/settings', ctx: 'sidebar', children: [
{ key: 'nav.settings.profile', label: 'Profile', to: '/settings/profile', ctx: 'sidebar' }
]},
{ key: 'qa.billing', label: 'Billing', to: '/billing', ctx: 'quickAction' }
])
// register handlers by key, no attach needed
nav.on('qa.billing', ({ event }) => { event.preventDefault(); navigateTo('/billing') })
nav.on('nav.settings.profile', ({ event }) => { event.preventDefault(); /* open pane */ })Build and publish
This module uses @nuxt/module-builder.
# from module root
rm -rf dist
pnpm prepack # runs nuxt-module-build build
# then publish
pnpm publish --access publicEnsure tsconfig.json sets "declarationMap": false, this avoids sourcemap JSON being mistaken for declarations during DTS bundling.
License
MIT
FAQ
Do I need attach if I register handlers by key
No for common cases. Use attach when a subset should share a different handler id than the key, for example a feature flag or a tenant override.
What happens if I reuse the same key in multiple contexts
The same handler fires for all those items. If you need context specific behavior, set clickId per item, for example billing:sidebar and billing:header, or use attach(i => i.ctx === 'sidebar' && i.key === 'qa.billing', 'billing:sidebar').
Can I skip the label and href helpers
Yes. They centralize i18n and link rules. If you do not use i18n or localized routes, read item.label and item.to directly. Keeping the helpers gives you a single extension point for later.
