@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 +413 -328
- package/dist/module.cjs +49 -22
- package/dist/module.json +1 -1
- package/dist/module.mjs +49 -22
- package/dist/runtime/composables/useNavigation.js +21 -13
- package/dist/runtime/plugins/navigation.js +0 -1
- package/package.json +15 -2
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
|
[](https://www.npmjs.com/package/@xyz/navigation)
|
|
6
6
|
[](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
|
-
-
|
|
15
|
-
-
|
|
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.
|
|
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.
|
|
41
|
+
### 2. Create Unified Config
|
|
39
42
|
|
|
40
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{{
|
|
131
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
**navigation.config.ts** is your single source of truth for both module options and navigation data.
|
|
139
114
|
|
|
140
|
-
|
|
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
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
205
|
+
### Automatic Translation
|
|
174
206
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
251
|
+
Use in templates:
|
|
209
252
|
|
|
210
253
|
```vue
|
|
211
254
|
<template>
|
|
212
255
|
<NuxtLink
|
|
213
|
-
v-for="item in sections.main.
|
|
256
|
+
v-for="item in sections.value.main.items"
|
|
214
257
|
:to="item.to"
|
|
215
|
-
:class="{ 'font-bold': item.active }"
|
|
258
|
+
:class="{ 'font-bold': item.active }"
|
|
216
259
|
>
|
|
217
260
|
{{ item.label }}
|
|
218
261
|
</NuxtLink>
|
|
219
262
|
</template>
|
|
220
263
|
```
|
|
221
264
|
|
|
222
|
-
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
283
|
+
workspace: (ctx) => ctx.workspaceSlug || 'default',
|
|
284
|
+
tenant: (ctx) => ctx.activeTenant?.id || ''
|
|
270
285
|
}
|
|
271
286
|
}
|
|
272
287
|
})
|
|
273
|
-
```
|
|
274
288
|
|
|
275
|
-
|
|
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
|
-
##
|
|
293
|
+
## Advanced Features
|
|
303
294
|
|
|
304
|
-
|
|
295
|
+
### Feature Flags
|
|
305
296
|
|
|
306
297
|
```typescript
|
|
307
298
|
{
|
|
308
|
-
label: '
|
|
309
|
-
to: '/
|
|
310
|
-
|
|
299
|
+
label: 'Analytics',
|
|
300
|
+
to: '/analytics',
|
|
301
|
+
features: ['analytics'] // Only shown if analytics feature enabled
|
|
311
302
|
}
|
|
312
303
|
```
|
|
313
304
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
Show/hide items based on enabled features:
|
|
305
|
+
Provide features via context:
|
|
317
306
|
|
|
318
307
|
```typescript
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
Use custom conditions for complex logic:
|
|
315
|
+
### Role-Based Filtering
|
|
336
316
|
|
|
337
317
|
```typescript
|
|
338
318
|
{
|
|
339
|
-
label: '
|
|
340
|
-
to: '/
|
|
341
|
-
|
|
319
|
+
label: 'Admin Panel',
|
|
320
|
+
to: '/admin',
|
|
321
|
+
roles: ['admin', 'owner'] // Only for admins/owners
|
|
342
322
|
}
|
|
343
323
|
```
|
|
344
324
|
|
|
345
|
-
|
|
325
|
+
Configure role hierarchy:
|
|
346
326
|
|
|
347
|
-
|
|
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: '
|
|
346
|
+
to: '/settings',
|
|
353
347
|
children: [
|
|
354
|
-
{ label: 'General', to: '
|
|
355
|
-
{ label: '
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
Built-in composable for managing sidebar state:
|
|
354
|
+
### Custom Conditions
|
|
368
355
|
|
|
369
356
|
```typescript
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
368
|
+
Main composable for navigation.
|
|
425
369
|
|
|
426
370
|
**Parameters:**
|
|
427
|
-
- `config` - Navigation configuration object
|
|
428
|
-
- `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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
452
|
-
|
|
453
|
-
Manage sidebar view state.
|
|
401
|
+
### Navigation Item Config
|
|
454
402
|
|
|
455
|
-
**Parameters:**
|
|
456
403
|
```typescript
|
|
457
|
-
{
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
430
|
+
### Section Header
|
|
431
|
+
|
|
464
432
|
```typescript
|
|
465
|
-
{
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
444
|
+
### Processed Section
|
|
476
445
|
|
|
477
|
-
|
|
446
|
+
All sections return this normalized structure:
|
|
478
447
|
|
|
479
448
|
```typescript
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
{
|
|
501
|
-
|
|
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
|
-
|
|
543
|
+
## Module Configuration
|
|
508
544
|
|
|
509
545
|
```typescript
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
:
|
|
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
|
-
|
|
578
|
+
## TypeScript
|
|
579
|
+
|
|
580
|
+
Full type safety with inference:
|
|
519
581
|
|
|
520
582
|
```typescript
|
|
521
|
-
{
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
615
|
+
MIT
|
|
531
616
|
|
|
532
617
|
## Contributing
|
|
533
618
|
|
|
534
|
-
Contributions
|
|
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
|
|
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
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
|
|
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
|
|
32
|
+
const sectionsRaw = Object.keys(config).reduce((acc, key) => {
|
|
33
33
|
const sectionKey = key;
|
|
34
34
|
const section = config[sectionKey];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
55
|
-
});
|
|
56
|
-
} else {
|
|
57
|
-
acc[sectionKey] = computed(() => {
|
|
54
|
+
} else {
|
|
58
55
|
const items = processNavigationItems(
|
|
59
56
|
section,
|
|
60
|
-
|
|
57
|
+
ctx,
|
|
61
58
|
processingOptions
|
|
62
59
|
);
|
|
63
|
-
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyz/navigation",
|
|
3
|
-
"version": "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
|
},
|