@xyz/navigation 1.0.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.
@@ -0,0 +1,40 @@
1
+ import { computed, isRef, unref } from "vue";
2
+ import { useNavigationContext } from "./useNavigationContext.js";
3
+ import { processNavigationItems } from "../utils/processor.js";
4
+ import { createTemplateRegistry } from "../config/default-templates.js";
5
+ import { validateNavigationConfig } from "../utils/validators.js";
6
+ export function useNavigation(config, options = {}) {
7
+ if (import.meta.dev) {
8
+ validateNavigationConfig(config);
9
+ }
10
+ const baseContext = useNavigationContext();
11
+ const context = computed(() => {
12
+ const base = baseContext.value;
13
+ if (options.context) {
14
+ const customContext = isRef(options.context) ? unref(options.context) : typeof options.context === "function" ? options.context() : options.context;
15
+ return { ...base, ...customContext };
16
+ }
17
+ return base;
18
+ });
19
+ const templates = createTemplateRegistry(options.templates);
20
+ const processingOptions = {
21
+ templates
22
+ };
23
+ const sections = Object.keys(config).reduce((acc, key) => {
24
+ const sectionKey = key;
25
+ acc[sectionKey] = computed(() => {
26
+ const items = config[sectionKey];
27
+ return processNavigationItems(items, context.value, processingOptions);
28
+ });
29
+ return acc;
30
+ }, {});
31
+ let refreshTrigger = 0;
32
+ const refresh = () => {
33
+ refreshTrigger++;
34
+ };
35
+ return {
36
+ sections,
37
+ context,
38
+ refresh
39
+ };
40
+ }
@@ -0,0 +1,10 @@
1
+ import { type ComputedRef } from 'vue';
2
+ import type { NavigationContext } from '../types/index.js';
3
+ /**
4
+ * Get or create navigation context
5
+ * This composable provides reactive access to navigation context
6
+ *
7
+ * @param override - Optional context override
8
+ * @returns Computed navigation context
9
+ */
10
+ export declare function useNavigationContext(override?: Partial<NavigationContext>): ComputedRef<NavigationContext>;
@@ -0,0 +1,19 @@
1
+ import { computed } from "vue";
2
+ import { useRoute, useNuxtApp } from "#app";
3
+ export function useNavigationContext(override) {
4
+ const route = useRoute();
5
+ const nuxtApp = useNuxtApp();
6
+ return computed(() => {
7
+ const providedContext = nuxtApp.$navigation?.context || {
8
+ user: null,
9
+ activeOrganization: null,
10
+ features: {}
11
+ };
12
+ const baseContext = {
13
+ ...providedContext,
14
+ route,
15
+ ...override
16
+ };
17
+ return baseContext;
18
+ });
19
+ }
@@ -0,0 +1,17 @@
1
+ import type { SidebarState, SidebarStateOptions } from '../types/index.js';
2
+ /**
3
+ * Sidebar state management composable
4
+ * Manages multi-view sidebar state with history tracking and optional persistence
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const sidebar = useSidebarState({
9
+ * initialView: 'app',
10
+ * persist: true
11
+ * })
12
+ *
13
+ * await sidebar.switchToView('settings')
14
+ * sidebar.backToPrevious()
15
+ * ```
16
+ */
17
+ export declare function useSidebarState(options?: SidebarStateOptions): SidebarState;
@@ -0,0 +1,69 @@
1
+ import { ref, computed, watch } from "vue";
2
+ export function useSidebarState(options = {}) {
3
+ const {
4
+ initialView = "default",
5
+ persist = false,
6
+ onBeforeSwitch,
7
+ onAfterSwitch
8
+ } = options;
9
+ const storageKey = typeof persist === "object" && persist.key ? persist.key : "nuxt-navigation:sidebar-view";
10
+ const storage = typeof persist === "object" && persist.storage ? persist.storage : "localStorage";
11
+ const getPersistedView = () => {
12
+ if (!persist || typeof window === "undefined") return initialView;
13
+ try {
14
+ const stored = storage === "localStorage" ? localStorage.getItem(storageKey) : sessionStorage.getItem(storageKey);
15
+ return stored || initialView;
16
+ } catch {
17
+ return initialView;
18
+ }
19
+ };
20
+ const currentView = ref(getPersistedView());
21
+ const historyStack = ref([currentView.value]);
22
+ if (persist && typeof window !== "undefined") {
23
+ watch(currentView, (newView) => {
24
+ try {
25
+ const storageAPI = storage === "localStorage" ? localStorage : sessionStorage;
26
+ storageAPI.setItem(storageKey, newView);
27
+ } catch (error) {
28
+ console.warn("[xyz-navigation] Failed to persist sidebar view:", error);
29
+ }
30
+ });
31
+ }
32
+ const switchToView = async (view) => {
33
+ if (currentView.value === view) return;
34
+ if (onBeforeSwitch) {
35
+ const canSwitch = await onBeforeSwitch(currentView.value, view);
36
+ if (canSwitch === false) return;
37
+ }
38
+ currentView.value = view;
39
+ historyStack.value.push(view);
40
+ if (onAfterSwitch) {
41
+ onAfterSwitch(view);
42
+ }
43
+ };
44
+ const backToPrevious = () => {
45
+ if (historyStack.value.length <= 1) return;
46
+ historyStack.value.pop();
47
+ const previousView = historyStack.value[historyStack.value.length - 1];
48
+ currentView.value = previousView;
49
+ if (onAfterSwitch) {
50
+ onAfterSwitch(previousView);
51
+ }
52
+ };
53
+ const resetView = () => {
54
+ currentView.value = initialView;
55
+ historyStack.value = [initialView];
56
+ if (onAfterSwitch) {
57
+ onAfterSwitch(initialView);
58
+ }
59
+ };
60
+ const isView = (view) => computed(() => currentView.value === view);
61
+ return {
62
+ currentView,
63
+ history: computed(() => [...historyStack.value]),
64
+ switchToView,
65
+ backToPrevious,
66
+ resetView,
67
+ isView
68
+ };
69
+ }
@@ -0,0 +1,9 @@
1
+ import type { NavigationContext, TemplateResolver } from '../types/index.js';
2
+ /**
3
+ * Built-in template resolvers
4
+ */
5
+ export declare const defaultTemplates: TemplateResolver[];
6
+ /**
7
+ * Create template resolver registry from built-in and custom templates
8
+ */
9
+ export declare function createTemplateRegistry(customTemplates?: Record<string, (ctx: NavigationContext) => string>): TemplateResolver[];
@@ -0,0 +1,31 @@
1
+ export const defaultTemplates = [
2
+ {
3
+ pattern: "{org}",
4
+ resolve: (ctx) => ctx.activeOrganization?.slug ? `/app/${ctx.activeOrganization.slug}` : "/app",
5
+ description: "Organization context base path - resolves to /app or /app/{slug}"
6
+ },
7
+ {
8
+ pattern: "{slug}",
9
+ resolve: (ctx) => ctx.activeOrganization?.slug ?? "",
10
+ description: "Organization slug"
11
+ },
12
+ {
13
+ pattern: "{username}",
14
+ resolve: (ctx) => ctx.user?.name || ctx.user?.email || "",
15
+ description: "User name or email"
16
+ },
17
+ {
18
+ pattern: "{user.id}",
19
+ resolve: (ctx) => ctx.user?.id || "",
20
+ description: "User ID"
21
+ }
22
+ ];
23
+ export function createTemplateRegistry(customTemplates) {
24
+ const registry = [...defaultTemplates];
25
+ if (customTemplates) {
26
+ for (const [pattern, resolve] of Object.entries(customTemplates)) {
27
+ registry.push({ pattern, resolve });
28
+ }
29
+ }
30
+ return registry;
31
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Navigation plugin
3
+ * Initializes navigation context using user-defined or default context provider
4
+ */
5
+ declare const _default: any;
6
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "#app";
2
+ const defaultContextProvider = () => ({
3
+ user: null,
4
+ activeOrganization: null,
5
+ features: {}
6
+ });
7
+ export default defineNuxtPlugin((nuxtApp) => {
8
+ const config = useRuntimeConfig();
9
+ const contextProviderOption = config.public.navigation?.contextProvider;
10
+ let contextProvider = defaultContextProvider;
11
+ if (contextProviderOption && typeof contextProviderOption === "function") {
12
+ contextProvider = () => contextProviderOption(nuxtApp);
13
+ }
14
+ const providedContext = contextProvider();
15
+ return {
16
+ provide: {
17
+ navigation: {
18
+ context: providedContext
19
+ }
20
+ }
21
+ };
22
+ });
@@ -0,0 +1,83 @@
1
+ import type { NavigationContext } from './index.js';
2
+ /**
3
+ * Module options for xyz-navigation
4
+ */
5
+ export interface ModuleOptions {
6
+ /**
7
+ * Enable/disable the module
8
+ * @default true
9
+ */
10
+ enabled?: boolean;
11
+ /**
12
+ * Inline navigation items configuration
13
+ * Alternative to creating a separate navigation.config.ts file
14
+ *
15
+ * @example
16
+ * navigation: {
17
+ * items: {
18
+ * sections: {
19
+ * main: [
20
+ * { label: 'Dashboard', to: '{org}' },
21
+ * { label: 'Projects', to: '{org}/projects' }
22
+ * ]
23
+ * }
24
+ * }
25
+ * }
26
+ */
27
+ items?: any;
28
+ /**
29
+ * Default context provider
30
+ * - Function: Custom provider function that receives NuxtApp
31
+ * - undefined: Use default (null values for user/activeOrganization)
32
+ *
33
+ * @example
34
+ * // Define custom context provider
35
+ * contextProvider: (nuxtApp) => ({
36
+ * user: nuxtApp.$auth?.user ?? null,
37
+ * activeOrganization: nuxtApp.$org?.active ?? null,
38
+ * features: nuxtApp.$features?.all() ?? {}
39
+ * })
40
+ */
41
+ contextProvider?: ((nuxtApp: any) => Partial<NavigationContext>);
42
+ /**
43
+ * Custom template variables
44
+ * Extend or override default template resolvers
45
+ */
46
+ templates?: Record<string, (ctx: NavigationContext) => string>;
47
+ /**
48
+ * Global navigation configuration
49
+ */
50
+ config?: {
51
+ /**
52
+ * Default feature flag resolver
53
+ */
54
+ features?: Record<string, boolean> | (() => Record<string, boolean>);
55
+ /**
56
+ * Role definitions and hierarchy
57
+ */
58
+ roles?: {
59
+ hierarchy?: string[];
60
+ resolver?: (user: any) => string | string[];
61
+ };
62
+ /**
63
+ * Translation function
64
+ */
65
+ i18n?: {
66
+ enabled?: boolean;
67
+ resolver?: (key: string) => string;
68
+ };
69
+ };
70
+ /**
71
+ * Enable development mode features
72
+ */
73
+ dev?: {
74
+ /**
75
+ * Log navigation context changes
76
+ */
77
+ logContext?: boolean;
78
+ /**
79
+ * Validate configuration on hot reload
80
+ */
81
+ validateOnHotReload?: boolean;
82
+ };
83
+ }
File without changes
@@ -0,0 +1,225 @@
1
+ import type { RouteLocationNormalizedLoaded } from 'vue-router';
2
+ /**
3
+ * Navigation context containing user, organization, and route information
4
+ */
5
+ export interface NavigationContext {
6
+ /**
7
+ * Current authenticated user
8
+ */
9
+ user: {
10
+ id?: string;
11
+ name?: string;
12
+ email?: string;
13
+ [key: string]: any;
14
+ } | null;
15
+ /**
16
+ * Active organization/tenant
17
+ */
18
+ activeOrganization: {
19
+ id?: string;
20
+ slug?: string;
21
+ name?: string;
22
+ [key: string]: any;
23
+ } | null;
24
+ /**
25
+ * Current route
26
+ */
27
+ route: RouteLocationNormalizedLoaded;
28
+ /**
29
+ * Feature flags
30
+ */
31
+ features?: Record<string, boolean>;
32
+ /**
33
+ * Custom context data
34
+ */
35
+ [key: string]: any;
36
+ }
37
+ /**
38
+ * Template resolver function type
39
+ */
40
+ export interface TemplateResolver {
41
+ /**
42
+ * Template pattern to match (e.g., '{org}', '{slug}')
43
+ */
44
+ pattern: string;
45
+ /**
46
+ * Resolver function
47
+ */
48
+ resolve: (ctx: NavigationContext) => string;
49
+ /**
50
+ * Description for documentation
51
+ */
52
+ description?: string;
53
+ }
54
+ /**
55
+ * Navigation item configuration
56
+ */
57
+ export interface NavigationItemConfig {
58
+ /**
59
+ * Unique identifier (optional)
60
+ */
61
+ id?: string;
62
+ /**
63
+ * Label (translation key or static string)
64
+ */
65
+ label: string;
66
+ /**
67
+ * Route path
68
+ * - Static string: '/app/settings'
69
+ * - Template string: '{org}/dashboard'
70
+ * - Context function: (ctx) => `/custom/${ctx.org.slug}`
71
+ */
72
+ to?: string | ((ctx: NavigationContext) => string);
73
+ /**
74
+ * Icon identifier
75
+ */
76
+ icon?: string;
77
+ /**
78
+ * Badge configuration
79
+ */
80
+ badge?: {
81
+ label?: string;
82
+ color?: string;
83
+ variant?: string;
84
+ };
85
+ /**
86
+ * Tooltip configuration
87
+ */
88
+ tooltip?: {
89
+ text: string;
90
+ placement?: 'top' | 'bottom' | 'left' | 'right';
91
+ };
92
+ /**
93
+ * Feature flag requirement
94
+ * Supports dot notation: 'organizations.billing'
95
+ */
96
+ feature?: string;
97
+ /**
98
+ * Role requirements
99
+ */
100
+ roles?: string[];
101
+ /**
102
+ * Custom condition
103
+ */
104
+ if?: (ctx: NavigationContext) => boolean;
105
+ /**
106
+ * Click handler (for action items)
107
+ */
108
+ onSelect?: (ctx: NavigationContext) => void | Promise<void>;
109
+ /**
110
+ * Child items (for nested navigation)
111
+ */
112
+ children?: NavigationItemConfig[];
113
+ /**
114
+ * Exact route matching
115
+ * @default true
116
+ */
117
+ exact?: boolean;
118
+ /**
119
+ * Custom metadata
120
+ */
121
+ meta?: Record<string, any>;
122
+ /**
123
+ * Divider (if true, renders as divider instead of link)
124
+ */
125
+ divider?: boolean;
126
+ }
127
+ /**
128
+ * Processed navigation menu item (after resolution and filtering)
129
+ */
130
+ export interface NavigationMenuItem extends Omit<NavigationItemConfig, 'to' | 'children'> {
131
+ to?: string;
132
+ children?: NavigationMenuItem[];
133
+ }
134
+ /**
135
+ * Navigation configuration type
136
+ */
137
+ export type NavigationConfig = Record<string, NavigationItemConfig[]>;
138
+ /**
139
+ * Options for useNavigation composable
140
+ */
141
+ export interface UseNavigationOptions {
142
+ /**
143
+ * Custom context provider
144
+ * Overrides module-level context
145
+ */
146
+ context?: NavigationContext | (() => NavigationContext);
147
+ /**
148
+ * Enable reactive updates
149
+ * @default true
150
+ */
151
+ reactive?: boolean;
152
+ /**
153
+ * Custom template resolvers
154
+ */
155
+ templates?: Record<string, (ctx: NavigationContext) => string>;
156
+ }
157
+ /**
158
+ * Result from useNavigation composable
159
+ */
160
+ export interface NavigationResult<T extends NavigationConfig> {
161
+ /**
162
+ * Resolved navigation items for each section
163
+ */
164
+ sections: {
165
+ [K in keyof T]: import('vue').ComputedRef<NavigationMenuItem[]>;
166
+ };
167
+ /**
168
+ * Current navigation context
169
+ */
170
+ context: import('vue').ComputedRef<NavigationContext>;
171
+ /**
172
+ * Refresh navigation (re-process all items)
173
+ */
174
+ refresh: () => void;
175
+ }
176
+ /**
177
+ * Sidebar state options
178
+ */
179
+ export interface SidebarStateOptions {
180
+ /**
181
+ * Initial view
182
+ */
183
+ initialView?: string;
184
+ /**
185
+ * Persist view state
186
+ */
187
+ persist?: boolean | {
188
+ key?: string;
189
+ storage?: 'localStorage' | 'sessionStorage';
190
+ };
191
+ /**
192
+ * View transition hooks
193
+ */
194
+ onBeforeSwitch?: (from: string, to: string) => boolean | Promise<boolean>;
195
+ onAfterSwitch?: (view: string) => void;
196
+ }
197
+ /**
198
+ * Sidebar state management
199
+ */
200
+ export interface SidebarState {
201
+ /**
202
+ * Current active view
203
+ */
204
+ currentView: import('vue').Ref<string>;
205
+ /**
206
+ * View history stack
207
+ */
208
+ history: import('vue').ComputedRef<string[]>;
209
+ /**
210
+ * Switch to a specific view
211
+ */
212
+ switchToView: (view: string) => Promise<void>;
213
+ /**
214
+ * Go back to previous view
215
+ */
216
+ backToPrevious: () => void;
217
+ /**
218
+ * Reset to initial view
219
+ */
220
+ resetView: () => void;
221
+ /**
222
+ * Check if currently in a specific view
223
+ */
224
+ isView: (view: string) => import('vue').ComputedRef<boolean>;
225
+ }
File without changes
@@ -0,0 +1,43 @@
1
+ import type { NavigationContext, NavigationItemConfig, NavigationMenuItem, TemplateResolver } from '../types/index.js';
2
+ /**
3
+ * Processing options for navigation items
4
+ */
5
+ export interface ProcessingOptions {
6
+ /**
7
+ * Template resolvers
8
+ */
9
+ templates: TemplateResolver[];
10
+ /**
11
+ * Role hierarchy for role-based filtering
12
+ */
13
+ roleHierarchy?: string[];
14
+ /**
15
+ * Role resolver function
16
+ */
17
+ roleResolver?: (user: any) => string | string[];
18
+ }
19
+ /**
20
+ * Check if a feature flag is enabled
21
+ * Supports dot notation: 'organizations.billing'
22
+ */
23
+ export declare function checkFeatureFlag(item: NavigationItemConfig, ctx: NavigationContext): boolean;
24
+ /**
25
+ * Check if user has required roles
26
+ * Supports role hierarchy
27
+ */
28
+ export declare function checkRoles(item: NavigationItemConfig, ctx: NavigationContext, options: ProcessingOptions): boolean;
29
+ /**
30
+ * Check custom condition
31
+ */
32
+ export declare function checkCondition(item: NavigationItemConfig, ctx: NavigationContext): boolean;
33
+ /**
34
+ * Process navigation items through the filtering and resolution pipeline
35
+ *
36
+ * Pipeline:
37
+ * 1. Filter by feature flags
38
+ * 2. Filter by roles
39
+ * 3. Filter by custom conditions
40
+ * 4. Resolve paths
41
+ * 5. Process children recursively
42
+ */
43
+ export declare function processNavigationItems(items: NavigationItemConfig[], ctx: NavigationContext, options: ProcessingOptions): NavigationMenuItem[];
@@ -0,0 +1,55 @@
1
+ import { resolvePath } from "./resolver.js";
2
+ export function checkFeatureFlag(item, ctx) {
3
+ if (!item.feature) return true;
4
+ if (!ctx.features) return false;
5
+ const keys = item.feature.split(".");
6
+ let current = ctx.features;
7
+ for (const key of keys) {
8
+ if (current && typeof current === "object" && key in current) {
9
+ current = current[key];
10
+ } else {
11
+ return false;
12
+ }
13
+ }
14
+ return current === true;
15
+ }
16
+ export function checkRoles(item, ctx, options) {
17
+ if (!item.roles || item.roles.length === 0) return true;
18
+ if (!ctx.user) return false;
19
+ let userRoles;
20
+ if (options.roleResolver) {
21
+ const resolved = options.roleResolver(ctx.user);
22
+ userRoles = Array.isArray(resolved) ? resolved : [resolved];
23
+ } else {
24
+ const role = ctx.user.role || ctx.user.roles;
25
+ userRoles = Array.isArray(role) ? role : role ? [role] : [];
26
+ }
27
+ return item.roles.some((requiredRole) => {
28
+ if (userRoles.includes(requiredRole)) return true;
29
+ if (options.roleHierarchy) {
30
+ const userHighestRoleIndex = Math.max(
31
+ ...userRoles.map((r) => options.roleHierarchy.indexOf(r))
32
+ );
33
+ const requiredRoleIndex = options.roleHierarchy.indexOf(requiredRole);
34
+ return userHighestRoleIndex >= requiredRoleIndex;
35
+ }
36
+ return false;
37
+ });
38
+ }
39
+ export function checkCondition(item, ctx) {
40
+ if (!item.if) return true;
41
+ return item.if(ctx);
42
+ }
43
+ export function processNavigationItems(items, ctx, options) {
44
+ return items.filter((item) => checkFeatureFlag(item, ctx)).filter((item) => checkRoles(item, ctx, options)).filter((item) => checkCondition(item, ctx)).map((item) => {
45
+ const processed = {
46
+ ...item,
47
+ to: item.to ? resolvePath(item.to, ctx, options.templates) : void 0,
48
+ children: item.children ? processNavigationItems(item.children, ctx, options) : void 0
49
+ };
50
+ if (processed.children && processed.children.length === 0) {
51
+ delete processed.children;
52
+ }
53
+ return processed;
54
+ });
55
+ }
@@ -0,0 +1,16 @@
1
+ import type { NavigationContext, TemplateResolver } from '../types/index.js';
2
+ /**
3
+ * Resolve a path string or function to a final path
4
+ * Handles both template strings and context functions
5
+ *
6
+ * @example
7
+ * resolvePath('{org}', ctx, templates) // → '/app' or '/app/acme'
8
+ * resolvePath('{org}/dashboard', ctx, templates) // → '/app/dashboard' or '/app/acme/dashboard'
9
+ * resolvePath((ctx) => `/${ctx.org.slug}`, ctx, templates) // → '/acme'
10
+ */
11
+ export declare function resolvePath(path: string | ((ctx: NavigationContext) => string), ctx: NavigationContext, templates: TemplateResolver[]): string;
12
+ /**
13
+ * Resolve multiple template variables in a path
14
+ * This is a convenience function that wraps resolvePath
15
+ */
16
+ export declare function resolveTemplates(path: string, ctx: NavigationContext, templates: TemplateResolver[]): string;
@@ -0,0 +1,16 @@
1
+ export function resolvePath(path, ctx, templates) {
2
+ if (typeof path === "function") {
3
+ return path(ctx);
4
+ }
5
+ let resolved = path;
6
+ for (const template of templates) {
7
+ if (resolved.includes(template.pattern)) {
8
+ const value = template.resolve(ctx);
9
+ resolved = resolved.replace(new RegExp(template.pattern.replace(/[{}]/g, "\\$&"), "g"), value);
10
+ }
11
+ }
12
+ return resolved;
13
+ }
14
+ export function resolveTemplates(path, ctx, templates) {
15
+ return resolvePath(path, ctx, templates);
16
+ }