@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.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/module.cjs +98 -0
- package/dist/module.d.cts +8 -0
- package/dist/module.d.mts +8 -0
- package/dist/module.d.ts +8 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +95 -0
- package/dist/runtime/composables/useNavigation.d.ts +19 -0
- package/dist/runtime/composables/useNavigation.js +40 -0
- package/dist/runtime/composables/useNavigationContext.d.ts +10 -0
- package/dist/runtime/composables/useNavigationContext.js +19 -0
- package/dist/runtime/composables/useSidebarState.d.ts +17 -0
- package/dist/runtime/composables/useSidebarState.js +69 -0
- package/dist/runtime/config/default-templates.d.ts +9 -0
- package/dist/runtime/config/default-templates.js +31 -0
- package/dist/runtime/plugins/navigation.d.ts +6 -0
- package/dist/runtime/plugins/navigation.js +22 -0
- package/dist/runtime/types/config.d.ts +83 -0
- package/dist/runtime/types/config.js +0 -0
- package/dist/runtime/types/index.d.ts +225 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/runtime/utils/processor.d.ts +43 -0
- package/dist/runtime/utils/processor.js +55 -0
- package/dist/runtime/utils/resolver.d.ts +16 -0
- package/dist/runtime/utils/resolver.js +16 -0
- package/dist/runtime/utils/validators.d.ts +13 -0
- package/dist/runtime/utils/validators.js +27 -0
- package/dist/types.d.mts +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +63 -0
|
@@ -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,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
|
+
}
|