@xyz/navigation 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @xyz/navigation
2
2
 
3
- A flexible, context-aware navigation module for Nuxt 4 with zero performance overhead.
3
+ A flexible, context-aware navigation module for Nuxt 4 with zero performance overhead and zero boilerplate.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@xyz/navigation.svg)](https://www.npmjs.com/package/@xyz/navigation)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -11,8 +11,10 @@ A flexible, context-aware navigation module for Nuxt 4 with zero performance ove
11
11
  - 🔐 **Role-Based Filtering** - Show/hide items based on user roles
12
12
  - 🚩 **Feature Flags** - Conditional navigation based on enabled features
13
13
  - ⚡ **Zero Performance Overhead** - SSR-safe with reactive computed properties
14
- - 🎨 **Framework-Agnostic** - Works with any UI library (Nuxt UI, Tailwind, custom)
15
- - 🔧 **Flexible Configuration** - Inline or external config file
14
+ - 🌍 **Auto Translation** - Automatic i18n integration with zero config
15
+ - 🎨 **Normalized Structure** - Consistent API for all navigation sections
16
+ - ✨ **Active States** - Automatic route matching and active states
17
+ - 🔧 **Runtime Functions** - Use composables directly in config
16
18
  - 📘 **Full TypeScript Support** - Complete type safety and inference
17
19
 
18
20
  ## Installation
@@ -27,85 +29,50 @@ yarn add @xyz/navigation
27
29
 
28
30
  ## Quick Start
29
31
 
30
- ### 1. Add Module to nuxt.config.ts
32
+ ### 1. Install & Enable
31
33
 
32
34
  ```typescript
35
+ // nuxt.config.ts
33
36
  export default defineNuxtConfig({
34
- modules: ['@xyz/navigation']
37
+ modules: ['@xyz/navigation'] // That's it!
35
38
  })
36
39
  ```
37
40
 
38
- ### 2. Configure Navigation
41
+ ### 2. Create Unified Config
39
42
 
40
- **Option A: Inline Configuration** (Recommended for small configs)
41
-
42
- ```typescript
43
- export default defineNuxtConfig({
44
- modules: ['@xyz/navigation'],
45
-
46
- navigation: {
47
- items: {
48
- sections: {
49
- main: [
50
- { label: 'Dashboard', to: '{org}', icon: 'i-heroicons-home' },
51
- { label: 'Projects', to: '{org}/projects' },
52
- { label: 'Team', to: '{org}/team', features: ['team'] }
53
- ]
54
- }
55
- },
56
-
57
- contextProvider: () => ({
58
- user: useAuthUser(),
59
- activeOrganization: useActiveOrg(),
60
- features: useFeatureFlags()
61
- })
62
- }
63
- })
64
- ```
65
-
66
- **Option B: External File** (Recommended for larger configs)
67
-
68
- Create `navigation.config.ts` at your project root:
43
+ **navigation.config.ts** - Everything in one place:
69
44
 
70
45
  ```typescript
71
46
  export default {
47
+ // Module Options (optional, auto-detected)
48
+ translationResolver: () => useI18n().t, // Auto-detects $t if using @nuxtjs/i18n
49
+
50
+ // Navigation Sections
72
51
  sections: {
52
+ // Flat section (auto-normalized)
73
53
  main: [
74
- {
75
- label: 'Dashboard',
76
- to: '{org}',
77
- icon: 'i-heroicons-home'
78
- },
79
- {
80
- label: 'Projects',
81
- to: '{org}/projects',
82
- icon: 'i-heroicons-folder'
83
- },
84
- {
85
- label: 'Team',
86
- to: '{org}/team',
87
- icon: 'i-heroicons-user-group',
88
- features: ['team'] // Only shown if 'team' feature is enabled
89
- },
90
- {
91
- label: 'Settings',
92
- to: '{org}/settings',
93
- icon: 'i-heroicons-cog-6-tooth',
94
- children: [
95
- { label: 'General', to: '{org}/settings/general' },
96
- {
97
- label: 'Billing',
98
- to: '{org}/settings/billing',
99
- roles: ['admin', 'owner'] // Only for admins/owners
100
- }
101
- ]
102
- }
54
+ { label: 'nav.dashboard', to: '/app', icon: 'i-lucide-home' },
55
+ { label: 'nav.projects', to: '/projects', icon: 'i-lucide-folder' }
103
56
  ],
104
57
 
105
- profile: [
106
- { label: 'My Profile', to: '/profile' },
107
- { label: 'Preferences', to: '/preferences' }
108
- ]
58
+ // Nested section with header
59
+ organization: {
60
+ header: (ctx) => ({
61
+ title: ctx.activeOrganization?.name ?? 'Personal',
62
+ avatar: {
63
+ // ✅ Use composables in config - functions execute at runtime!
64
+ src: toAbsoluteUrl(ctx.activeOrganization?.logo || ''),
65
+ icon: 'i-lucide-building-2'
66
+ }
67
+ }),
68
+ items: [
69
+ { label: 'nav.overview', to: '{org}' },
70
+ { label: 'nav.team', to: '{org}/team', features: ['team'] }
71
+ ],
72
+ children: [
73
+ { label: 'nav.settings', to: '{org}/settings' }
74
+ ]
75
+ }
109
76
  }
110
77
  }
111
78
  ```
@@ -113,422 +80,540 @@ export default {
113
80
  ### 3. Use in Components
114
81
 
115
82
  ```vue
116
- <script setup lang="ts">
83
+ <script setup>
117
84
  import navigationConfig from '~/navigation.config'
118
85
 
119
86
  const { sections } = useNavigation(navigationConfig)
87
+ // sections.value = { main: {...}, organization: {...} }
120
88
  </script>
121
89
 
122
90
  <template>
123
91
  <nav>
124
- <NuxtLink
125
- v-for="item in sections.main"
126
- :key="item.label"
127
- :to="item.to"
128
- :class="{ 'active': item.active }" <!-- Active state auto-computed! -->
129
- >
130
- {{ item.label }}
131
- </NuxtLink>
92
+ <!-- All sections have normalized structure -->
93
+ <UNavigationMenu :items="sections.value.main.items" />
94
+
95
+ <!-- Headers with runtime-resolved data -->
96
+ <div v-if="sections.value.organization.header">
97
+ <UAvatar :src="sections.value.organization.header.avatar.src" />
98
+ <h2>{{ sections.value.organization.header.title }}</h2>
99
+ </div>
100
+
101
+ <!-- Items with auto-computed active states -->
102
+ <UNavigationMenu :items="sections.value.organization.items" />
132
103
  </nav>
133
104
  </template>
134
105
  ```
135
106
 
136
- ## New in v1.1
107
+ > **Note:** Labels are auto-translated if using `@nuxtjs/i18n` or a custom `translationResolver`.
108
+
109
+ > **Note:** Labels are auto-translated if using `@nuxtjs/i18n` or a custom `translationResolver`.
110
+
111
+ ## Unified Configuration
137
112
 
138
- ### Nested Section Structure
113
+ **navigation.config.ts** is your single source of truth for both module options and navigation data.
139
114
 
140
- Organize navigation with logical grouping:
115
+ ### All-in-One Config
141
116
 
142
117
  ```typescript
118
+ // navigation.config.ts
143
119
  export default {
120
+ // Module Options (build-time, optional)
121
+ translationResolver: () => useTranslations().t,
122
+ contextProvider: () => ({
123
+ user: useAuthUser(),
124
+ activeOrganization: useActiveOrg(),
125
+ features: useFeatureFlags()
126
+ }),
127
+ templates: {
128
+ workspace: (ctx) => ctx.workspaceSlug || 'default'
129
+ },
130
+
131
+ // Navigation Sections (runtime)
144
132
  sections: {
145
- // Flat sections (backward compatible)
146
133
  main: [...],
147
-
148
- // Nested sections with header, items, children, footer
134
+ organization: {...}
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### Override in nuxt.config.ts
140
+
141
+ **nuxt.config.ts** can override individual module options:
142
+
143
+ ```typescript
144
+ export default defineNuxtConfig({
145
+ navigation: {
146
+ translationResolver: () => customResolver() // Overrides navigation.config.ts
147
+ }
148
+ })
149
+ ```
150
+
151
+ **Priority:** `nuxt.config.ts` > `navigation.config.ts` module options > `navigation.config.ts` sections
152
+
153
+ ## Core Concepts
154
+
155
+ ### Section Normalization
156
+
157
+ All sections return a consistent structure:
158
+
159
+ ```typescript
160
+ {
161
+ items?: NavigationMenuItem[] // Main navigation items
162
+ header?: SectionHeader // Optional header with title, avatar
163
+ children?: NavigationMenuItem[] // Sub-navigation items
164
+ footer?: NavigationMenuItem[] // Footer items
165
+ }
166
+ ```
167
+
168
+ **Flat sections** (arrays) are auto-wrapped:
169
+ ```typescript
170
+ // Config
171
+ sections: {
172
+ main: [{ label: 'Dashboard', to: '/app' }]
173
+ }
174
+
175
+ // Result
176
+ sections.value.main = {
177
+ items: [{ label: 'Dashboard', to: '/app', active: true }],
178
+ header: undefined,
179
+ children: [],
180
+ footer: []
181
+ }
182
+ ```
183
+
184
+ ### Runtime Functions in Config
185
+
186
+ Config functions execute at runtime, so **composables work**:
187
+
188
+ ```typescript
189
+ export default {
190
+ sections: {
149
191
  organization: {
150
192
  header: (ctx) => ({
151
- title: ctx.activeOrganization?.name ?? 'Personal',
152
- subtitle: ctx.activeOrganization?.slug,
153
- avatar: {
154
- src: ctx.activeOrganization?.logo,
155
- icon: 'i-lucide-building-2'
193
+ title: ctx.activeOrganization?.name,
194
+ avatar: {
195
+ // Use composables directly!
196
+ src: toAbsoluteUrl(ctx.activeOrganization?.logo || '')
156
197
  }
157
198
  }),
158
- items: [
159
- { label: 'Overview', to: '{org}' },
160
- { label: 'Team', to: '{org}/team' }
161
- ],
162
- children: [
163
- { label: 'Settings', to: '{org}/settings' }
164
- ],
165
- footer: [
166
- { label: 'Help', to: '/help' }
167
- ]
199
+ items: [...]
168
200
  }
169
201
  }
170
202
  }
171
203
  ```
172
204
 
173
- Access nested sections:
205
+ ### Automatic Translation
174
206
 
175
- ```vue
176
- <template>
177
- <div>
178
- <!-- Header -->
179
- <div v-if="sections.organization.value.header">
180
- <UAvatar :src="sections.organization.value.header.avatar.src" />
181
- <h2>{{ sections.organization.value.header.title }}</h2>
182
- </div>
183
-
184
- <!-- Items -->
185
- <UNavigationMenu :items="sections.organization.value.items" />
186
-
187
- <!-- Children -->
188
- <UNavigationMenu :items="sections.organization.value.children" />
189
-
190
- <!-- Footer -->
191
- <UNavigationMenu :items="sections.organization.value.footer" />
192
- </div>
193
- </template>
207
+ **Zero config** with `@nuxtjs/i18n`:
208
+
209
+ ```typescript
210
+ // navigation.config.ts - use translation keys
211
+ export default {
212
+ sections: {
213
+ main: [
214
+ { label: 'nav.dashboard', to: '/app' }, // Auto-translated!
215
+ { label: 'nav.projects', to: '/projects' }
216
+ ]
217
+ }
218
+ }
219
+
220
+ // Component - translations applied automatically
221
+ const { sections } = useNavigation(config)
222
+ ```
223
+
224
+ **Custom translation resolver:**
225
+
226
+ ```typescript
227
+ // nuxt.config.ts
228
+ export default defineNuxtConfig({
229
+ navigation: {
230
+ translationResolver: () => {
231
+ const { t } = useTranslations()
232
+ return t
233
+ }
234
+ }
235
+ })
194
236
  ```
195
237
 
196
238
  ### Automatic Active States
197
239
 
198
- Navigation items now include an `active` field automatically:
240
+ Navigation items include `active` boolean based on current route:
199
241
 
200
242
  ```typescript
201
243
  {
202
244
  label: 'Blog',
203
245
  to: '/blog',
204
- exact: false // Active for /blog and /blog/*
246
+ exact: false, // Active for /blog and /blog/*
247
+ active: true // ✅ Auto-computed
205
248
  }
206
249
  ```
207
250
 
208
- All items have `active: boolean` computed based on current route:
251
+ Use in templates:
209
252
 
210
253
  ```vue
211
254
  <template>
212
255
  <NuxtLink
213
- v-for="item in sections.main.value"
256
+ v-for="item in sections.value.main.items"
214
257
  :to="item.to"
215
- :class="{ 'font-bold': item.active }" <!-- No manual route matching! -->
258
+ :class="{ 'font-bold': item.active }"
216
259
  >
217
260
  {{ item.label }}
218
261
  </NuxtLink>
219
262
  </template>
220
263
  ```
221
264
 
222
- ### Translation Support
223
-
224
- Auto-translate labels with your i18n function:
225
-
226
- ```typescript
227
- const { sections } = useNavigation(config, {
228
- translationFn: (key) => $i18n.t(key) // or t(key), or any translator
229
- })
230
-
231
- // All labels automatically translated!
232
- ```
233
-
234
- In your config, use translation keys:
235
-
236
- ```typescript
237
- export default {
238
- sections: {
239
- main: [
240
- { label: 'nav.dashboard', to: '/app' }, // → Translated
241
- { label: 'nav.projects', to: '/projects' } // → Translated
242
- ]
243
- }
244
- }
245
- ```
246
-
247
- ## Template Strings
265
+ ## Template Variables
248
266
 
249
- Built-in template variables for dynamic path resolution:
267
+ Built-in template variables for dynamic paths:
250
268
 
251
269
  | Template | Resolves To | Example |
252
270
  |----------|-------------|---------|
253
271
  | `{org}` | `/app` (personal) or `/app/:slug` (organization) | `{org}/projects` → `/app/acme/projects` |
254
- | `{slug}` | Current organization slug | `{slug}` → `acme` |
272
+ | `{slug}` | Current organization slug | `{slug}` → `acme` |
255
273
  | `{username}` | Current user's name or email | `/u/{username}` → `/u/john` |
256
274
  | `{user.id}` | Current user ID | `/users/{user.id}` → `/users/123` |
257
275
 
258
276
  ### Custom Templates
259
277
 
260
- Define custom template resolvers in `nuxt.config.ts`:
261
-
262
278
  ```typescript
279
+ // nuxt.config.ts
263
280
  export default defineNuxtConfig({
264
- modules: ['@xyz/navigation'],
265
-
266
281
  navigation: {
267
282
  templates: {
268
- '{workspace}': (ctx) => ctx.workspace?.slug ?? '',
269
- '{tenant}': (ctx) => ctx.tenant?.id ?? 'default'
283
+ workspace: (ctx) => ctx.workspaceSlug || 'default',
284
+ tenant: (ctx) => ctx.activeTenant?.id || ''
270
285
  }
271
286
  }
272
287
  })
273
- ```
274
288
 
275
- ## Context Provider
276
-
277
- Define how navigation context is resolved:
278
-
279
- ```typescript
280
- export default defineNuxtConfig({
281
- modules: ['@xyz/navigation'],
282
-
283
- navigation: {
284
- contextProvider: () => ({
285
- // User information
286
- user: useAuthUser(),
287
-
288
- // Active organization/tenant
289
- activeOrganization: useActiveOrg(),
290
-
291
- // Feature flags
292
- features: useFeatureFlags(),
293
-
294
- // Custom context
295
- workspace: useWorkspace(),
296
- permissions: usePermissions()
297
- })
298
- }
299
- })
289
+ // Use in config
290
+ { label: 'Workspace', to: '/w/{workspace}/dashboard' }
300
291
  ```
301
292
 
302
- ## Role-Based Access
293
+ ## Advanced Features
303
294
 
304
- Control navigation visibility based on user roles:
295
+ ### Feature Flags
305
296
 
306
297
  ```typescript
307
298
  {
308
- label: 'Admin Panel',
309
- to: '/admin',
310
- roles: ['admin', 'superadmin'] // Only shown to admins
299
+ label: 'Analytics',
300
+ to: '/analytics',
301
+ features: ['analytics'] // Only shown if analytics feature enabled
311
302
  }
312
303
  ```
313
304
 
314
- ## Feature Flags
315
-
316
- Show/hide items based on enabled features:
305
+ Provide features via context:
317
306
 
318
307
  ```typescript
319
- {
320
- label: 'Beta Feature',
321
- to: '/beta',
322
- features: ['betaAccess'] // Only shown if feature is enabled
323
- }
324
-
325
- // Multiple features (all must be enabled)
326
- {
327
- label: 'Advanced Analytics',
328
- to: '/analytics',
329
- features: ['analytics', 'premium']
330
- }
308
+ const { sections } = useNavigation(config, {
309
+ context: {
310
+ features: { analytics: true, team: false }
311
+ }
312
+ })
331
313
  ```
332
314
 
333
- ## Conditional Display
334
-
335
- Use custom conditions for complex logic:
315
+ ### Role-Based Filtering
336
316
 
337
317
  ```typescript
338
318
  {
339
- label: 'Upgrade',
340
- to: '/upgrade',
341
- if: (ctx) => !ctx.user?.isPremium // Show only to non-premium users
319
+ label: 'Admin Panel',
320
+ to: '/admin',
321
+ roles: ['admin', 'owner'] // Only for admins/owners
342
322
  }
343
323
  ```
344
324
 
345
- ## Nested Navigation
325
+ Configure role hierarchy:
346
326
 
347
- Create multi-level navigation structures:
327
+ ```typescript
328
+ // nuxt.config.ts
329
+ export default defineNuxtConfig({
330
+ navigation: {
331
+ config: {
332
+ roles: {
333
+ hierarchy: ['viewer', 'member', 'admin', 'owner'],
334
+ resolver: (user) => user?.role || 'viewer'
335
+ }
336
+ }
337
+ }
338
+ })
339
+ ```
340
+
341
+ ### Nested Navigation
348
342
 
349
343
  ```typescript
350
344
  {
351
345
  label: 'Settings',
352
- to: '{org}/settings',
346
+ to: '/settings',
353
347
  children: [
354
- { label: 'General', to: '{org}/settings/general' },
355
- { label: 'Team', to: '{org}/settings/team' },
356
- {
357
- label: 'Billing',
358
- to: '{org}/settings/billing',
359
- roles: ['admin']
360
- }
348
+ { label: 'General', to: '/settings/general' },
349
+ { label: 'Billing', to: '/settings/billing', roles: ['admin'] }
361
350
  ]
362
351
  }
363
352
  ```
364
353
 
365
- ## Sidebar State Management
366
-
367
- Built-in composable for managing sidebar state:
354
+ ### Custom Conditions
368
355
 
369
356
  ```typescript
370
- const sidebar = useSidebarState({
371
- initialView: 'main',
372
- persist: true // Persists to localStorage
373
- })
374
-
375
- // Switch views
376
- await sidebar.switchToView('settings')
377
-
378
- // Go back
379
- sidebar.backToPrevious()
380
-
381
- // Reset to initial
382
- sidebar.resetView()
383
-
384
- // Check current view
385
- if (sidebar.isView('settings')) {
386
- // ...
357
+ {
358
+ label: 'Upgrade',
359
+ to: '/upgrade',
360
+ condition: (ctx) => !ctx.user?.isPremium // Only for free users
387
361
  }
388
362
  ```
389
363
 
390
- ## Framework Integration
391
-
392
- Works seamlessly with any UI library:
393
-
394
- ### Nuxt UI
395
-
396
- ```vue
397
- <template>
398
- <UNavigationMenu :items="sections.main.value" orientation="vertical" />
399
- </template>
400
- ```
401
-
402
- ### Headless UI / Custom
403
-
404
- ```vue
405
- <template>
406
- <nav class="flex flex-col gap-2">
407
- <NuxtLink
408
- v-for="item in sections.main.value"
409
- :key="item.label"
410
- :to="item.to"
411
- class="nav-link"
412
- >
413
- <Icon :name="item.icon" />
414
- {{ item.label }}
415
- </NuxtLink>
416
- </nav>
417
- </template>
418
- ```
419
-
420
364
  ## API Reference
421
365
 
422
366
  ### `useNavigation(config, options?)`
423
367
 
424
- Main composable for processing navigation configuration.
368
+ Main composable for navigation.
425
369
 
426
370
  **Parameters:**
427
- - `config` - Navigation configuration object
428
- - `options` (optional) - Processing options
371
+ - `config`: `NavigationConfig` - Navigation configuration object
372
+ - `options?`: `UseNavigationOptions` - Optional configuration
429
373
 
430
374
  **Returns:**
431
375
  ```typescript
432
376
  {
433
- sections: Record<string, ComputedRef<NavigationItem[]>>,
434
- context: ComputedRef<NavigationContext>,
377
+ sections: ComputedRef<Record<string, ProcessedSection>>
378
+ context: ComputedRef<NavigationContext>
435
379
  refresh: () => void
436
380
  }
437
381
  ```
438
382
 
439
- ### `useNavigationContext(override?)`
440
-
441
- Access current navigation context.
383
+ **Options:**
442
384
 
443
- **Parameters:**
444
- - `override` (optional) - Override context values
445
-
446
- **Returns:**
447
385
  ```typescript
448
- ComputedRef<NavigationContext>
386
+ interface UseNavigationOptions {
387
+ // Custom context provider
388
+ context?: NavigationContext | (() => NavigationContext)
389
+
390
+ // Reactive updates (default: true)
391
+ reactive?: boolean
392
+
393
+ // Custom template resolvers
394
+ templates?: Record<string, (ctx: NavigationContext) => string>
395
+
396
+ // Override global translation function
397
+ translationFn?: (key: string) => string
398
+ }
449
399
  ```
450
400
 
451
- ### `useSidebarState(options?)`
452
-
453
- Manage sidebar view state.
401
+ ### Navigation Item Config
454
402
 
455
- **Parameters:**
456
403
  ```typescript
457
- {
458
- initialView?: string,
459
- persist?: boolean // Default: false
404
+ interface NavigationItemConfig {
405
+ label: string // Label (or translation key)
406
+ to?: string // Path (supports templates)
407
+ icon?: string // Icon name
408
+ exact?: boolean // Exact route matching (default: true)
409
+
410
+ // Filtering
411
+ features?: string[] // Required feature flags
412
+ roles?: string[] // Required user roles
413
+ condition?: (ctx: NavigationContext) => boolean // Custom condition
414
+
415
+ // Nested navigation
416
+ children?: NavigationItemConfig[]
417
+
418
+ // Custom handlers
419
+ onSelect?: (item: NavigationItemConfig) => void
420
+
421
+ // Divider
422
+ divider?: boolean
423
+
424
+ // Custom metadata
425
+ badge?: string | number
426
+ [key: string]: any // Additional custom fields
460
427
  }
461
428
  ```
462
429
 
463
- **Returns:**
430
+ ### Section Header
431
+
464
432
  ```typescript
465
- {
466
- currentView: Ref<string>,
467
- history: Ref<string[]>,
468
- switchToView: (view: string) => Promise<void>,
469
- backToPrevious: () => void,
470
- resetView: () => void,
471
- isView: (view: string) => boolean
433
+ interface SectionHeader {
434
+ title?: string
435
+ subtitle?: string
436
+ avatar?: {
437
+ src?: string | null
438
+ icon?: string
439
+ fallback?: string
440
+ }
472
441
  }
473
442
  ```
474
443
 
475
- ## TypeScript Support
444
+ ### Processed Section
476
445
 
477
- Full type safety with automatic inference:
446
+ All sections return this normalized structure:
478
447
 
479
448
  ```typescript
480
- import type { NavigationConfig, NavigationItem } from '@xyz/navigation'
481
-
482
- const config: NavigationConfig = {
483
- sections: {
484
- main: [
485
- // Fully typed items
486
- ]
487
- }
449
+ interface ProcessedSection {
450
+ items?: NavigationMenuItem[] // With active states
451
+ header?: SectionHeader
452
+ children?: NavigationMenuItem[]
453
+ footer?: NavigationMenuItem[]
488
454
  }
489
455
  ```
490
456
 
491
- ## Examples
492
-
493
- ### Multi-Tenant SaaS
457
+ ## Complete Example
494
458
 
495
459
  ```typescript
460
+ // navigation.config.ts
496
461
  export default {
497
462
  sections: {
498
- main: [
499
- { label: 'Dashboard', to: '{org}' },
500
- { label: 'Users', to: '{org}/users', roles: ['admin'] },
501
- { label: 'Billing', to: '{org}/billing', features: ['billing'] }
463
+ // App navigation (flat)
464
+ app: [
465
+ {
466
+ label: 'nav.dashboard',
467
+ to: '/app',
468
+ icon: 'i-lucide-home',
469
+ exact: true
470
+ },
471
+ {
472
+ label: 'nav.projects',
473
+ to: '/app/projects',
474
+ icon: 'i-lucide-folder',
475
+ exact: false // Active for /app/projects/*
476
+ }
477
+ ],
478
+
479
+ // Organization navigation (nested)
480
+ organization: {
481
+ header: (ctx) => ({
482
+ title: ctx.activeOrganization?.name ?? 'Personal',
483
+ subtitle: ctx.activeOrganization?.slug,
484
+ avatar: {
485
+ src: useImageUrl().toAbsoluteUrl(
486
+ ctx.activeOrganization?.logo || ''
487
+ ),
488
+ icon: 'i-lucide-building-2',
489
+ fallback: ctx.activeOrganization?.name?.charAt(0)
490
+ }
491
+ }),
492
+
493
+ items: [
494
+ {
495
+ label: 'nav.overview',
496
+ to: '{org}',
497
+ icon: 'i-lucide-layout-dashboard'
498
+ },
499
+ {
500
+ label: 'nav.team',
501
+ to: '{org}/team',
502
+ icon: 'i-lucide-users',
503
+ features: ['team']
504
+ },
505
+ {
506
+ label: 'nav.analytics',
507
+ to: '{org}/analytics',
508
+ icon: 'i-lucide-bar-chart',
509
+ features: ['analytics'],
510
+ roles: ['admin', 'owner']
511
+ }
512
+ ],
513
+
514
+ children: [
515
+ {
516
+ label: 'nav.settings',
517
+ to: '{org}/settings',
518
+ icon: 'i-lucide-settings',
519
+ children: [
520
+ { label: 'nav.settings.general', to: '{org}/settings/general' },
521
+ { label: 'nav.settings.billing', to: '{org}/settings/billing', roles: ['owner'] }
522
+ ]
523
+ }
524
+ ],
525
+
526
+ footer: [
527
+ { label: 'nav.help', to: '/help', icon: 'i-lucide-help-circle' },
528
+ { label: 'nav.docs', to: '/docs', icon: 'i-lucide-book' }
529
+ ]
530
+ },
531
+
532
+ // Profile navigation
533
+ profile: [
534
+ { label: 'nav.profile.account', to: '/profile', icon: 'i-lucide-user' },
535
+ { label: 'nav.profile.preferences', to: '/preferences', icon: 'i-lucide-sliders' },
536
+ { divider: true },
537
+ { label: 'nav.profile.logout', icon: 'i-lucide-log-out', onSelect: () => logout() }
502
538
  ]
503
539
  }
504
540
  }
505
541
  ```
506
542
 
507
- ### Context-Based Routing
543
+ ## Module Configuration
508
544
 
509
545
  ```typescript
510
- {
511
- label: 'Profile',
512
- to: (ctx) => ctx.activeOrganization
513
- ? `/${ctx.activeOrganization.slug}/profile`
514
- : `/u/${ctx.user?.username}/profile`
515
- }
546
+ // nuxt.config.ts
547
+ export default defineNuxtConfig({
548
+ navigation: {
549
+ // Translation resolver (runtime)
550
+ translationResolver: () => {
551
+ const { t } = useTranslations()
552
+ return t
553
+ },
554
+
555
+ // Custom templates
556
+ templates: {
557
+ workspace: (ctx) => ctx.workspaceSlug || 'default'
558
+ },
559
+
560
+ // Context provider
561
+ contextProvider: (nuxtApp) => ({
562
+ user: nuxtApp.$auth?.user,
563
+ activeOrganization: nuxtApp.$org?.active,
564
+ features: nuxtApp.$features?.all()
565
+ }),
566
+
567
+ // Role configuration
568
+ config: {
569
+ roles: {
570
+ hierarchy: ['viewer', 'member', 'admin', 'owner'],
571
+ resolver: (user) => user?.role || 'viewer'
572
+ }
573
+ }
574
+ }
575
+ })
516
576
  ```
517
577
 
518
- ### Progressive Disclosure
578
+ ## TypeScript
579
+
580
+ Full type safety with inference:
519
581
 
520
582
  ```typescript
521
- {
522
- label: 'Advanced',
523
- to: '/advanced',
524
- if: (ctx) => ctx.user?.role === 'developer' && ctx.features?.advanced
583
+ import type { NavigationConfig, NavigationMenuItem } from '@xyz/navigation'
584
+
585
+ const config: NavigationConfig = {
586
+ sections: {
587
+ main: [...] // Fully typed
588
+ }
525
589
  }
590
+
591
+ const { sections } = useNavigation(config)
592
+ sections.value.main.items // NavigationMenuItem[]
526
593
  ```
527
594
 
595
+ ## Migration Guide
596
+
597
+ ### From v1.0 to v1.2
598
+
599
+ All sections now return normalized structure:
600
+
601
+ ```typescript
602
+ // v1.0
603
+ sections.value.main // NavigationMenuItem[]
604
+ sections.value.org // { items, header, children, footer }
605
+
606
+ // v1.2 (normalized)
607
+ sections.value.main.items // NavigationMenuItem[]
608
+ sections.value.org.items // NavigationMenuItem[]
609
+ ```
610
+
611
+ Both flat and nested sections now have consistent API.
612
+
528
613
  ## License
529
614
 
530
- MIT License - see [LICENSE](./LICENSE) for details.
615
+ MIT
531
616
 
532
617
  ## Contributing
533
618
 
534
- Contributions are welcome! Please feel free to submit a Pull Request.
619
+ Contributions welcome! Please open an issue or PR.
package/dist/module.cjs CHANGED
@@ -64,6 +64,47 @@ export {}
64
64
  contextProvider: options.contextProvider
65
65
  }
66
66
  );
67
+ let navigationConfigFromFile = null;
68
+ try {
69
+ const { existsSync } = await import('fs');
70
+ const navConfigPath = resolver.resolve(nuxt.options.rootDir, "navigation.config.ts");
71
+ if (existsSync(navConfigPath)) {
72
+ const configModule = await import(navConfigPath).catch(() => null);
73
+ navigationConfigFromFile = configModule?.default || null;
74
+ if (navigationConfigFromFile) {
75
+ const { sections, ...moduleOptionsFromFile } = navigationConfigFromFile;
76
+ if (Object.keys(moduleOptionsFromFile).length > 0) {
77
+ Object.assign(options, {
78
+ ...moduleOptionsFromFile,
79
+ ...options
80
+ // nuxt.config.ts takes precedence
81
+ });
82
+ if (nuxt.options.dev) {
83
+ console.log("[xyz-navigation] Loaded module options from navigation.config.ts");
84
+ }
85
+ }
86
+ if (!options.items && sections) {
87
+ options.items = { sections };
88
+ }
89
+ }
90
+ if (nuxt.options.dev) {
91
+ console.log("[xyz-navigation] Found navigation.config.ts at project root");
92
+ }
93
+ } else if (nuxt.options.dev) {
94
+ console.warn("[xyz-navigation] No navigation.config.ts found. Add it or use inline config in nuxt.config.ts");
95
+ }
96
+ } catch (error) {
97
+ if (nuxt.options.dev) {
98
+ console.warn("[xyz-navigation] Error loading navigation.config.ts:", error);
99
+ }
100
+ }
101
+ kit.addTemplate({
102
+ filename: "navigation-translation.mjs",
103
+ getContents: () => {
104
+ const resolverFn = options.translationResolver?.toString() || "null";
105
+ return `export const translationResolver = ${resolverFn}`;
106
+ }
107
+ });
67
108
  if (options.items) {
68
109
  kit.addTemplate({
69
110
  filename: "navigation-config.mjs",
@@ -75,29 +116,15 @@ export {}
75
116
  from: "#build/navigation-config.mjs"
76
117
  });
77
118
  if (nuxt.options.dev) {
78
- console.log("[xyz-navigation] Using inline navigation configuration from nuxt.config.ts");
79
- }
80
- } else {
81
- try {
82
- const { existsSync } = await import('fs');
83
- const navConfigPath = resolver.resolve(nuxt.options.rootDir, "navigation.config.ts");
84
- if (existsSync(navConfigPath)) {
85
- kit.addImports({
86
- name: "default",
87
- as: "navigationConfig",
88
- from: navConfigPath
89
- });
90
- if (nuxt.options.dev) {
91
- console.log("[xyz-navigation] Found navigation.config.ts at project root - auto-importing");
92
- }
93
- } else if (nuxt.options.dev) {
94
- console.warn("[xyz-navigation] No navigation configuration found. Add navigation.config.ts or use inline config in nuxt.config.ts");
95
- }
96
- } catch (error) {
97
- if (nuxt.options.dev) {
98
- console.warn("[xyz-navigation] Error checking for navigation.config.ts:", error);
99
- }
119
+ console.log("[xyz-navigation] Using navigation configuration from config files");
100
120
  }
121
+ } else if (navigationConfigFromFile) {
122
+ const navConfigPath = resolver.resolve(nuxt.options.rootDir, "navigation.config.ts");
123
+ kit.addImports({
124
+ name: "default",
125
+ as: "navigationConfig",
126
+ from: navConfigPath
127
+ });
101
128
  }
102
129
  }
103
130
  });
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "1.1.1",
7
+ "version": "1.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -61,6 +61,47 @@ export {}
61
61
  contextProvider: options.contextProvider
62
62
  }
63
63
  );
64
+ let navigationConfigFromFile = null;
65
+ try {
66
+ const { existsSync } = await import('fs');
67
+ const navConfigPath = resolver.resolve(nuxt.options.rootDir, "navigation.config.ts");
68
+ if (existsSync(navConfigPath)) {
69
+ const configModule = await import(navConfigPath).catch(() => null);
70
+ navigationConfigFromFile = configModule?.default || null;
71
+ if (navigationConfigFromFile) {
72
+ const { sections, ...moduleOptionsFromFile } = navigationConfigFromFile;
73
+ if (Object.keys(moduleOptionsFromFile).length > 0) {
74
+ Object.assign(options, {
75
+ ...moduleOptionsFromFile,
76
+ ...options
77
+ // nuxt.config.ts takes precedence
78
+ });
79
+ if (nuxt.options.dev) {
80
+ console.log("[xyz-navigation] Loaded module options from navigation.config.ts");
81
+ }
82
+ }
83
+ if (!options.items && sections) {
84
+ options.items = { sections };
85
+ }
86
+ }
87
+ if (nuxt.options.dev) {
88
+ console.log("[xyz-navigation] Found navigation.config.ts at project root");
89
+ }
90
+ } else if (nuxt.options.dev) {
91
+ console.warn("[xyz-navigation] No navigation.config.ts found. Add it or use inline config in nuxt.config.ts");
92
+ }
93
+ } catch (error) {
94
+ if (nuxt.options.dev) {
95
+ console.warn("[xyz-navigation] Error loading navigation.config.ts:", error);
96
+ }
97
+ }
98
+ addTemplate({
99
+ filename: "navigation-translation.mjs",
100
+ getContents: () => {
101
+ const resolverFn = options.translationResolver?.toString() || "null";
102
+ return `export const translationResolver = ${resolverFn}`;
103
+ }
104
+ });
64
105
  if (options.items) {
65
106
  addTemplate({
66
107
  filename: "navigation-config.mjs",
@@ -72,29 +113,15 @@ export {}
72
113
  from: "#build/navigation-config.mjs"
73
114
  });
74
115
  if (nuxt.options.dev) {
75
- console.log("[xyz-navigation] Using inline navigation configuration from nuxt.config.ts");
76
- }
77
- } else {
78
- try {
79
- const { existsSync } = await import('fs');
80
- const navConfigPath = resolver.resolve(nuxt.options.rootDir, "navigation.config.ts");
81
- if (existsSync(navConfigPath)) {
82
- addImports({
83
- name: "default",
84
- as: "navigationConfig",
85
- from: navConfigPath
86
- });
87
- if (nuxt.options.dev) {
88
- console.log("[xyz-navigation] Found navigation.config.ts at project root - auto-importing");
89
- }
90
- } else if (nuxt.options.dev) {
91
- console.warn("[xyz-navigation] No navigation configuration found. Add navigation.config.ts or use inline config in nuxt.config.ts");
92
- }
93
- } catch (error) {
94
- if (nuxt.options.dev) {
95
- console.warn("[xyz-navigation] Error checking for navigation.config.ts:", error);
96
- }
116
+ console.log("[xyz-navigation] Using navigation configuration from config files");
97
117
  }
118
+ } else if (navigationConfigFromFile) {
119
+ const navConfigPath = resolver.resolve(nuxt.options.rootDir, "navigation.config.ts");
120
+ addImports({
121
+ name: "default",
122
+ as: "navigationConfig",
123
+ from: navConfigPath
124
+ });
98
125
  }
99
126
  }
100
127
  });
@@ -29,13 +29,13 @@ export function useNavigation(config, options = {}) {
29
29
  templates,
30
30
  translationFn: translateFn
31
31
  };
32
- const sections = Object.keys(config).reduce((acc, key) => {
32
+ const sectionsRaw = Object.keys(config).reduce((acc, key) => {
33
33
  const sectionKey = key;
34
34
  const section = config[sectionKey];
35
- if (isNestedSection(section)) {
36
- acc[sectionKey] = computed(() => {
37
- const ctx = context.value;
38
- const result = {};
35
+ acc[sectionKey] = computed(() => {
36
+ const ctx = context.value;
37
+ const result = {};
38
+ if (isNestedSection(section)) {
39
39
  if (section.header) {
40
40
  result.header = section.header(ctx);
41
41
  }
@@ -51,26 +51,34 @@ export function useNavigation(config, options = {}) {
51
51
  const footer = processNavigationItems(section.footer, ctx, processingOptions);
52
52
  result.footer = computeActiveStates(footer, ctx.route.path);
53
53
  }
54
- return result;
55
- });
56
- } else {
57
- acc[sectionKey] = computed(() => {
54
+ } else {
58
55
  const items = processNavigationItems(
59
56
  section,
60
- context.value,
57
+ ctx,
61
58
  processingOptions
62
59
  );
63
- return computeActiveStates(items, context.value.route.path);
64
- });
65
- }
60
+ result.items = computeActiveStates(items, ctx.route.path);
61
+ result.header = void 0;
62
+ result.children = [];
63
+ result.footer = [];
64
+ }
65
+ return result;
66
+ });
66
67
  return acc;
67
68
  }, {});
69
+ const sections = computed(() => {
70
+ return Object.keys(sectionsRaw).reduce((acc, key) => {
71
+ acc[key] = sectionsRaw[key].value;
72
+ return acc;
73
+ }, {});
74
+ });
68
75
  let refreshTrigger = 0;
69
76
  const refresh = () => {
70
77
  refreshTrigger++;
71
78
  };
72
79
  return {
73
80
  sections,
81
+ // Unwrapped: sections.app.items (no .value)
74
82
  context,
75
83
  refresh
76
84
  };
@@ -37,7 +37,6 @@ export default defineNuxtPlugin(async (nuxtApp) => {
37
37
  navigation: {
38
38
  context: providedContext,
39
39
  translate: translationFn
40
- // Make translation available globally
41
40
  }
42
41
  }
43
42
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyz/navigation",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Context-aware, type-safe navigation for multi-tenant Nuxt applications",
5
5
  "type": "module",
6
6
  "exports": {
@@ -39,9 +39,22 @@
39
39
  "multi-tenant",
40
40
  "saas",
41
41
  "dynamic-routing",
42
- "typescript"
42
+ "typescript",
43
+ "zero-config",
44
+ "i18n",
45
+ "role-based",
46
+ "feature-flags"
43
47
  ],
48
+ "author": "xyz",
44
49
  "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/xyz/xyz-navigation.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/xyz/xyz-navigation/issues"
56
+ },
57
+ "homepage": "https://github.com/xyz/xyz-navigation#readme",
45
58
  "dependencies": {
46
59
  "defu": "^6.1.4"
47
60
  },