picocrank 1.9.1 → 1.11.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/package.json +8 -8
- package/router.js +47 -0
- package/vue/components/Header.vue +4 -0
- package/vue/components/Login.vue +279 -0
- package/vue/components/Navigation.vue +167 -0
- package/vue/components/NavigationGrid.vue +294 -0
- package/vue/components/Sidebar.vue +6 -136
- package/vue/components/Table.vue +5 -1
- package/vue/components/Tabs.vue +233 -0
- package/vue/views/Admin.vue +14 -14
- package/vue/views/App.vue +36 -21
- package/vue/views/FormExample.vue +268 -0
- package/vue/views/Login.vue +65 -0
- package/vue/views/LoginExample.vue +117 -0
- package/vue/views/TabsExample.vue +337 -0
- package/vue/views/UserControlPanel.vue +79 -0
- package/vue/views/Welcome.vue +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picocrank",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
|
|
5
5
|
"main": "vite.config.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"url": "https://github.com/jamesread/picocrank/issues"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@hugeicons/core-free-icons": "^
|
|
22
|
-
"@hugeicons/vue": "^1.0.
|
|
23
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
24
|
-
"femtocrank": "^2.4.
|
|
21
|
+
"@hugeicons/core-free-icons": "^3.1.0",
|
|
22
|
+
"@hugeicons/vue": "^1.0.4",
|
|
23
|
+
"@vitejs/plugin-vue": "^6.0.3",
|
|
24
|
+
"femtocrank": "^2.4.12",
|
|
25
25
|
"unplugin-vue-components": "^30.0.0",
|
|
26
|
-
"vite": "^7.
|
|
27
|
-
"vue": "^3.5.
|
|
28
|
-
"vue-router": "^4.6.
|
|
26
|
+
"vite": "^7.3.0",
|
|
27
|
+
"vue": "^3.5.26",
|
|
28
|
+
"vue-router": "^4.6.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"semantic-release": "^25.0.2"
|
package/router.js
CHANGED
|
@@ -5,6 +5,8 @@ import { TableIcon } from '@hugeicons/core-free-icons';
|
|
|
5
5
|
import { ViewIcon } from '@hugeicons/core-free-icons';
|
|
6
6
|
import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
|
|
7
7
|
import { CalendarIcon } from '@hugeicons/core-free-icons';
|
|
8
|
+
import { EditIcon } from '@hugeicons/core-free-icons';
|
|
9
|
+
import { Menu01Icon } from '@hugeicons/core-free-icons';
|
|
8
10
|
|
|
9
11
|
const routes = [
|
|
10
12
|
{
|
|
@@ -54,6 +56,51 @@ const routes = [
|
|
|
54
56
|
title: 'Calendar Example',
|
|
55
57
|
icon: CalendarIcon,
|
|
56
58
|
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Login',
|
|
62
|
+
path: '/login',
|
|
63
|
+
component: () => import('./vue/views/Login.vue'),
|
|
64
|
+
meta: {
|
|
65
|
+
title: 'Login',
|
|
66
|
+
icon: SecurityValidationIcon,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'FormExample',
|
|
71
|
+
path: '/form-example',
|
|
72
|
+
component: () => import('./vue/views/FormExample.vue'),
|
|
73
|
+
meta: {
|
|
74
|
+
title: 'Form Example',
|
|
75
|
+
icon: EditIcon,
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'TabsExample',
|
|
80
|
+
path: '/tabs-example',
|
|
81
|
+
component: () => import('./vue/views/TabsExample.vue'),
|
|
82
|
+
meta: {
|
|
83
|
+
title: 'Tabs Example',
|
|
84
|
+
icon: Menu01Icon,
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'UserControlPanel',
|
|
89
|
+
path: '/user-control-panel',
|
|
90
|
+
component: () => import('./vue/views/UserControlPanel.vue'),
|
|
91
|
+
meta: {
|
|
92
|
+
title: 'User Control Panel',
|
|
93
|
+
icon: SecurityValidationIcon,
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'LoginExample',
|
|
98
|
+
path: '/login-example',
|
|
99
|
+
component: () => import('./vue/views/LoginExample.vue'),
|
|
100
|
+
meta: {
|
|
101
|
+
title: 'Login Example',
|
|
102
|
+
icon: SecurityValidationIcon,
|
|
103
|
+
}
|
|
57
104
|
}
|
|
58
105
|
];
|
|
59
106
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="login-container">
|
|
3
|
+
<Tabs :tabs="loginTabs" @tab-change="onTabChange">
|
|
4
|
+
<!-- Local Login Tab -->
|
|
5
|
+
<template #tab-local>
|
|
6
|
+
<div class="login-section">
|
|
7
|
+
<form @submit.prevent="handleLocalLogin" class="local-login-form">
|
|
8
|
+
<input
|
|
9
|
+
id="username"
|
|
10
|
+
v-model="localLogin.username"
|
|
11
|
+
required
|
|
12
|
+
autocomplete="username"
|
|
13
|
+
placeholder="Username"
|
|
14
|
+
/>
|
|
15
|
+
<input
|
|
16
|
+
id="password"
|
|
17
|
+
type="password"
|
|
18
|
+
v-model="localLogin.password"
|
|
19
|
+
required
|
|
20
|
+
autocomplete="current-password"
|
|
21
|
+
placeholder="Password"
|
|
22
|
+
/>
|
|
23
|
+
<button type="submit" class="good">Login</button>
|
|
24
|
+
<div v-if="localLoginError" class="error-message">
|
|
25
|
+
{{ localLoginError }}
|
|
26
|
+
</div>
|
|
27
|
+
</form>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<!-- OAuth 2 Login Tab -->
|
|
32
|
+
<template #tab-oauth>
|
|
33
|
+
<div class="login-section">
|
|
34
|
+
<div v-if="oauthProviders.length > 0" class="oauth-providers">
|
|
35
|
+
<button
|
|
36
|
+
v-for="provider in oauthProviders"
|
|
37
|
+
:key="provider.id"
|
|
38
|
+
class="oauth-button"
|
|
39
|
+
:class="provider.class || 'neutral'"
|
|
40
|
+
@click="handleOAuthLogin(provider)"
|
|
41
|
+
:title="`Sign in with ${provider.name}`"
|
|
42
|
+
>
|
|
43
|
+
<HugeiconsIcon
|
|
44
|
+
v-if="provider.icon"
|
|
45
|
+
:icon="provider.icon"
|
|
46
|
+
width="1.2em"
|
|
47
|
+
height="1.2em"
|
|
48
|
+
/>
|
|
49
|
+
<span>{{ provider.name }}</span>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div v-else class="no-providers">
|
|
53
|
+
<p>No OAuth providers configured.</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div v-if="oauthError" class="error-message">
|
|
56
|
+
{{ oauthError }}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
</Tabs>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup>
|
|
65
|
+
import { ref, reactive, watch } from 'vue';
|
|
66
|
+
import Tabs from './Tabs.vue';
|
|
67
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
68
|
+
|
|
69
|
+
const props = defineProps({
|
|
70
|
+
// Custom OAuth providers (optional)
|
|
71
|
+
oauthProviders: {
|
|
72
|
+
type: Array,
|
|
73
|
+
default: () => []
|
|
74
|
+
},
|
|
75
|
+
// Custom tab labels (optional)
|
|
76
|
+
tabLabels: {
|
|
77
|
+
type: Object,
|
|
78
|
+
default: () => ({
|
|
79
|
+
local: 'Username & Password',
|
|
80
|
+
oauth: 'OAuth2'
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const emit = defineEmits(['local-login', 'oauth-login', 'tab-change']);
|
|
86
|
+
|
|
87
|
+
// Login tabs configuration
|
|
88
|
+
const loginTabs = ref([
|
|
89
|
+
{ id: 'local', label: props.tabLabels.local || 'Username & Password' },
|
|
90
|
+
{ id: 'oauth', label: props.tabLabels.oauth || 'OAuth2' }
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
// Local login state
|
|
94
|
+
const localLogin = reactive({
|
|
95
|
+
username: '',
|
|
96
|
+
password: ''
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const localLoginError = ref('');
|
|
100
|
+
|
|
101
|
+
// OAuth providers - provided by parent component
|
|
102
|
+
const oauthProviders = ref(props.oauthProviders);
|
|
103
|
+
const oauthError = ref('');
|
|
104
|
+
|
|
105
|
+
// Watch for changes to oauthProviders prop
|
|
106
|
+
watch(() => props.oauthProviders, (newProviders) => {
|
|
107
|
+
oauthProviders.value = newProviders;
|
|
108
|
+
}, { deep: true });
|
|
109
|
+
|
|
110
|
+
// Handle local login
|
|
111
|
+
function handleLocalLogin() {
|
|
112
|
+
localLoginError.value = '';
|
|
113
|
+
|
|
114
|
+
// Emit event for parent component to handle
|
|
115
|
+
emit('local-login', {
|
|
116
|
+
username: localLogin.username,
|
|
117
|
+
password: localLogin.password
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reset local login form
|
|
122
|
+
function resetLocalForm() {
|
|
123
|
+
localLogin.username = '';
|
|
124
|
+
localLogin.password = '';
|
|
125
|
+
localLoginError.value = '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle OAuth login
|
|
129
|
+
function handleOAuthLogin(provider) {
|
|
130
|
+
oauthError.value = '';
|
|
131
|
+
|
|
132
|
+
// Emit event for parent component to handle
|
|
133
|
+
emit('oauth-login', provider);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle tab change
|
|
137
|
+
function onTabChange(tab, tabId) {
|
|
138
|
+
// Clear errors when switching tabs
|
|
139
|
+
localLoginError.value = '';
|
|
140
|
+
oauthError.value = '';
|
|
141
|
+
emit('tab-change', tab, tabId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Method to set error messages
|
|
145
|
+
function setLocalLoginError(message) {
|
|
146
|
+
localLoginError.value = message;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setOAuthError(message) {
|
|
150
|
+
oauthError.value = message;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Method to add custom OAuth providers programmatically
|
|
154
|
+
function addOAuthProvider(provider) {
|
|
155
|
+
oauthProviders.value.push(provider);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Expose methods for parent components
|
|
159
|
+
defineExpose({
|
|
160
|
+
addOAuthProvider,
|
|
161
|
+
resetLocalForm,
|
|
162
|
+
setLocalLoginError,
|
|
163
|
+
setOAuthError,
|
|
164
|
+
localLogin
|
|
165
|
+
});
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<style scoped>
|
|
169
|
+
.login-container {
|
|
170
|
+
max-width: 500px;
|
|
171
|
+
margin: 2rem auto;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.login-section {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
gap: 1rem;
|
|
178
|
+
align-items: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.local-login-form {
|
|
182
|
+
display: flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
gap: 1rem;
|
|
185
|
+
width: 100%;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.local-login-form input {
|
|
189
|
+
padding: 0.75rem;
|
|
190
|
+
border: 1px solid var(--border-color, #ccc);
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
font-size: 1em;
|
|
193
|
+
background-color: var(--input-bg, #fff);
|
|
194
|
+
color: var(--text-color, #000);
|
|
195
|
+
width: 100%;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.local-login-form input:focus {
|
|
199
|
+
outline: none;
|
|
200
|
+
border-color: var(--primary-color, #007bff);
|
|
201
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.local-login-form button {
|
|
205
|
+
width: 100%;
|
|
206
|
+
padding: 0.75rem 1.5rem;
|
|
207
|
+
font-size: 1em;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
border: none;
|
|
210
|
+
border-radius: 4px;
|
|
211
|
+
transition: opacity 0.2s;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.local-login-form button:hover {
|
|
215
|
+
opacity: 0.9;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.oauth-providers {
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column;
|
|
221
|
+
gap: 0.75rem;
|
|
222
|
+
width: 100%;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.oauth-button {
|
|
226
|
+
display: flex;
|
|
227
|
+
align-items: center;
|
|
228
|
+
justify-content: center;
|
|
229
|
+
gap: 0.75rem;
|
|
230
|
+
padding: 0.75rem 1.5rem;
|
|
231
|
+
font-size: 1em;
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
border: none;
|
|
234
|
+
border-radius: 4px;
|
|
235
|
+
transition: opacity 0.2s, transform 0.1s;
|
|
236
|
+
width: 100%;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.oauth-button:hover {
|
|
240
|
+
opacity: 0.9;
|
|
241
|
+
transform: translateY(-1px);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.oauth-button:active {
|
|
245
|
+
transform: translateY(0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.oauth-button span {
|
|
249
|
+
font-weight: 500;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.error-message {
|
|
253
|
+
padding: 0.75rem;
|
|
254
|
+
background-color: var(--error-bg, #fee);
|
|
255
|
+
color: var(--error-color, #c33);
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
font-size: 0.9em;
|
|
258
|
+
margin-top: 0.5rem;
|
|
259
|
+
width: 100%;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.no-providers {
|
|
263
|
+
padding: 2rem;
|
|
264
|
+
text-align: center;
|
|
265
|
+
color: var(--text-muted, #666);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@media (min-width: 768px) {
|
|
269
|
+
.login-container {
|
|
270
|
+
max-width: 600px;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.oauth-providers {
|
|
274
|
+
display: grid;
|
|
275
|
+
grid-template-columns: repeat(2, 1fr);
|
|
276
|
+
gap: 0.75rem;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
</style>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Navigation component has no visual output, it only manages navigation state -->
|
|
3
|
+
<slot />
|
|
4
|
+
</template>
|
|
5
|
+
|
|
6
|
+
<script setup>
|
|
7
|
+
import { ref, provide } from 'vue'
|
|
8
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
9
|
+
import { PinIcon } from '@hugeicons/core-free-icons'
|
|
10
|
+
|
|
11
|
+
const navigationLinks = ref([])
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const router = useRouter()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Add a router link to the navigation
|
|
17
|
+
* @param {string} link - Route name
|
|
18
|
+
* @param {string|null} altTitle - Optional alternative title
|
|
19
|
+
* @param {object} options - Additional options
|
|
20
|
+
* @param {object} options.params - Route params to pass (e.g., { id: 1 })
|
|
21
|
+
* @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
|
|
22
|
+
* @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
|
|
23
|
+
*/
|
|
24
|
+
function addRouterLink(link, altTitle = null, options = {}) {
|
|
25
|
+
const foundRoute = router.getRoutes().find(r => r.name === link)
|
|
26
|
+
|
|
27
|
+
if (!foundRoute) {
|
|
28
|
+
console.warn(`Route "${link}" not found`)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const title = altTitle || foundRoute.meta?.title || foundRoute.name
|
|
33
|
+
|
|
34
|
+
// Determine the 'to' value: use options.to if provided, otherwise construct from params, or fall back to path
|
|
35
|
+
let toValue = foundRoute.path
|
|
36
|
+
if (options.to) {
|
|
37
|
+
toValue = options.to
|
|
38
|
+
} else if (options.params) {
|
|
39
|
+
toValue = { name: link, params: options.params }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const routeLink = {
|
|
43
|
+
name: link,
|
|
44
|
+
title: title,
|
|
45
|
+
path: foundRoute.path,
|
|
46
|
+
to: toValue,
|
|
47
|
+
icon: foundRoute.meta?.icon || PinIcon,
|
|
48
|
+
type: 'route',
|
|
49
|
+
props: options.props || {},
|
|
50
|
+
description: options.description || foundRoute.meta?.description || null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addNavigationLink(routeLink)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addNavigationLink(link) {
|
|
57
|
+
const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
|
|
58
|
+
if (existingIndex >= 0) {
|
|
59
|
+
navigationLinks.value[existingIndex] = { ...link }
|
|
60
|
+
} else {
|
|
61
|
+
navigationLinks.value.push({ ...link })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addCallback(title, callback, options = {}) {
|
|
66
|
+
const callbackLink = {
|
|
67
|
+
name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
|
|
68
|
+
type: 'callback',
|
|
69
|
+
icon: options.icon || PinIcon,
|
|
70
|
+
callback: callback || (() => {}),
|
|
71
|
+
title: title,
|
|
72
|
+
description: options.description || null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
addNavigationLink(callbackLink)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function addSeparator(id) {
|
|
79
|
+
const separator = {
|
|
80
|
+
name: id || `nav-separator-${Date.now()}`,
|
|
81
|
+
type: 'separator',
|
|
82
|
+
title: 'Separator'
|
|
83
|
+
}
|
|
84
|
+
addNavigationLink(separator)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addHtml(html, options = {}) {
|
|
88
|
+
const htmlLink = {
|
|
89
|
+
name: options.name || `html-item-${Date.now()}`,
|
|
90
|
+
type: 'html',
|
|
91
|
+
html: html,
|
|
92
|
+
title: options.title || 'HTML Item'
|
|
93
|
+
}
|
|
94
|
+
addNavigationLink(htmlLink)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function removeNavigationLink(linkId) {
|
|
98
|
+
navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clearNavigationLinks() {
|
|
102
|
+
navigationLinks.value = []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getNavigationLinks() {
|
|
106
|
+
return [...navigationLinks.value]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isActive(link) {
|
|
110
|
+
// If link has a 'to' object with a name, compare route name and params
|
|
111
|
+
if (link.to && typeof link.to === 'object' && link.to.name) {
|
|
112
|
+
// Compare route names first
|
|
113
|
+
if (route.name !== link.to.name) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If params are specified in the link, all specified params must match
|
|
118
|
+
if (link.to.params && Object.keys(link.to.params).length > 0) {
|
|
119
|
+
const linkParams = link.to.params
|
|
120
|
+
const routeParams = route.params
|
|
121
|
+
|
|
122
|
+
// Check if all params in link.to.params match route.params
|
|
123
|
+
for (const key in linkParams) {
|
|
124
|
+
if (String(routeParams[key]) !== String(linkParams[key])) {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Route name matches, and either no params specified or all params match
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fall back to path comparison for string paths
|
|
135
|
+
const linkPath = typeof link.to === 'string' ? link.to : link.path
|
|
136
|
+
return route.path === linkPath
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Provide navigation API for child components
|
|
140
|
+
provide('navigation', {
|
|
141
|
+
navigationLinks,
|
|
142
|
+
addRouterLink,
|
|
143
|
+
addNavigationLink,
|
|
144
|
+
addCallback,
|
|
145
|
+
addSeparator,
|
|
146
|
+
addHtml,
|
|
147
|
+
removeNavigationLink,
|
|
148
|
+
clearNavigationLinks,
|
|
149
|
+
getNavigationLinks,
|
|
150
|
+
isActive
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Expose API for direct ref access
|
|
154
|
+
defineExpose({
|
|
155
|
+
navigationLinks,
|
|
156
|
+
addRouterLink,
|
|
157
|
+
addNavigationLink,
|
|
158
|
+
addCallback,
|
|
159
|
+
addSeparator,
|
|
160
|
+
addHtml,
|
|
161
|
+
removeNavigationLink,
|
|
162
|
+
clearNavigationLinks,
|
|
163
|
+
getNavigationLinks,
|
|
164
|
+
isActive
|
|
165
|
+
})
|
|
166
|
+
</script>
|
|
167
|
+
|