picocrank 1.10.0 → 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 +1 -1
- package/router.js +37 -0
- package/vue/components/Login.vue +279 -0
- package/vue/components/Navigation.vue +4 -2
- package/vue/components/NavigationGrid.vue +294 -0
- package/vue/components/Tabs.vue +233 -0
- package/vue/views/Admin.vue +14 -14
- package/vue/views/App.vue +8 -1
- 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
package/router.js
CHANGED
|
@@ -6,6 +6,7 @@ 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
8
|
import { EditIcon } from '@hugeicons/core-free-icons';
|
|
9
|
+
import { Menu01Icon } from '@hugeicons/core-free-icons';
|
|
9
10
|
|
|
10
11
|
const routes = [
|
|
11
12
|
{
|
|
@@ -56,6 +57,15 @@ const routes = [
|
|
|
56
57
|
icon: CalendarIcon,
|
|
57
58
|
}
|
|
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
|
+
},
|
|
59
69
|
{
|
|
60
70
|
name: 'FormExample',
|
|
61
71
|
path: '/form-example',
|
|
@@ -64,6 +74,33 @@ const routes = [
|
|
|
64
74
|
title: 'Form Example',
|
|
65
75
|
icon: EditIcon,
|
|
66
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
|
+
}
|
|
67
104
|
}
|
|
68
105
|
];
|
|
69
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>
|
|
@@ -46,7 +46,8 @@ function addRouterLink(link, altTitle = null, options = {}) {
|
|
|
46
46
|
to: toValue,
|
|
47
47
|
icon: foundRoute.meta?.icon || PinIcon,
|
|
48
48
|
type: 'route',
|
|
49
|
-
props: options.props || {}
|
|
49
|
+
props: options.props || {},
|
|
50
|
+
description: options.description || foundRoute.meta?.description || null
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
addNavigationLink(routeLink)
|
|
@@ -67,7 +68,8 @@ function addCallback(title, callback, options = {}) {
|
|
|
67
68
|
type: 'callback',
|
|
68
69
|
icon: options.icon || PinIcon,
|
|
69
70
|
callback: callback || (() => {}),
|
|
70
|
-
title: title
|
|
71
|
+
title: title,
|
|
72
|
+
description: options.description || null
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
addNavigationLink(callbackLink)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="navigation-grid-container">
|
|
3
|
+
<div class="navigation-grid" :class="{ 'compact': compact }">
|
|
4
|
+
<button
|
|
5
|
+
v-for="link in filteredLinks"
|
|
6
|
+
:key="link.name"
|
|
7
|
+
:class="['nav-button', { active: isLinkActive(link) }]"
|
|
8
|
+
:title="link.title"
|
|
9
|
+
@click="handleLinkClick(link)"
|
|
10
|
+
>
|
|
11
|
+
<div class="nav-button-icon">
|
|
12
|
+
<HugeiconsIcon
|
|
13
|
+
:icon="link.icon"
|
|
14
|
+
:width="iconSize"
|
|
15
|
+
:height="iconSize"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="nav-button-label">{{ link.title }}</div>
|
|
19
|
+
<div v-if="link.description" class="nav-button-description">
|
|
20
|
+
{{ link.description }}
|
|
21
|
+
</div>
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="filteredLinks.length === 0" class="no-links">
|
|
25
|
+
<p>No navigation links available</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import { computed, inject, ref } from 'vue';
|
|
32
|
+
import { useRouter, useRoute } from 'vue-router';
|
|
33
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
34
|
+
|
|
35
|
+
const props = defineProps({
|
|
36
|
+
// Filter links by type: 'route', 'callback', or null for all
|
|
37
|
+
filterType: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: null,
|
|
40
|
+
validator: (value) => value === null || ['route', 'callback'].includes(value)
|
|
41
|
+
},
|
|
42
|
+
// Custom filter function
|
|
43
|
+
filter: {
|
|
44
|
+
type: Function,
|
|
45
|
+
default: null
|
|
46
|
+
},
|
|
47
|
+
// Compact mode (smaller buttons)
|
|
48
|
+
compact: {
|
|
49
|
+
type: Boolean,
|
|
50
|
+
default: false
|
|
51
|
+
},
|
|
52
|
+
// Icon size
|
|
53
|
+
iconSize: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: '2em'
|
|
56
|
+
},
|
|
57
|
+
// Exclude separator and html items
|
|
58
|
+
excludeNonButtons: {
|
|
59
|
+
type: Boolean,
|
|
60
|
+
default: true
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
const route = useRoute();
|
|
66
|
+
|
|
67
|
+
// Inject navigation from Navigation component
|
|
68
|
+
const navigation = inject('navigation', null);
|
|
69
|
+
|
|
70
|
+
// Get navigation links (it's a ref from the Navigation component)
|
|
71
|
+
const navigationLinks = navigation ? navigation.navigationLinks : ref([]);
|
|
72
|
+
|
|
73
|
+
// Get isActive function from navigation
|
|
74
|
+
const isActive = navigation ? navigation.isActive : (() => false);
|
|
75
|
+
|
|
76
|
+
// Filter links based on props
|
|
77
|
+
const filteredLinks = computed(() => {
|
|
78
|
+
if (!navigationLinks.value || navigationLinks.value.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let links = [...navigationLinks.value];
|
|
83
|
+
|
|
84
|
+
// Exclude separators and HTML items by default
|
|
85
|
+
if (props.excludeNonButtons) {
|
|
86
|
+
links = links.filter(link =>
|
|
87
|
+
link.type !== 'separator' && link.type !== 'html'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Filter by type if specified
|
|
92
|
+
if (props.filterType) {
|
|
93
|
+
links = links.filter(link => link.type === props.filterType);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Apply custom filter if provided
|
|
97
|
+
if (props.filter && typeof props.filter === 'function') {
|
|
98
|
+
links = links.filter(props.filter);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return links;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
function isLinkActive(link) {
|
|
105
|
+
return isActive(link);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleLinkClick(link) {
|
|
109
|
+
if (link.type === 'route') {
|
|
110
|
+
const to = link.to || link.path;
|
|
111
|
+
if (to) {
|
|
112
|
+
router.push(to);
|
|
113
|
+
}
|
|
114
|
+
} else if (link.type === 'callback' && link.callback) {
|
|
115
|
+
link.callback();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<style scoped>
|
|
121
|
+
.navigation-grid-container {
|
|
122
|
+
width: 100%;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.navigation-grid {
|
|
126
|
+
display: grid;
|
|
127
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
128
|
+
gap: 1rem;
|
|
129
|
+
padding: 1rem 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.navigation-grid.compact {
|
|
133
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
134
|
+
gap: 0.75rem;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.nav-button {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
align-items: top;
|
|
141
|
+
justify-content: flex-start;
|
|
142
|
+
gap: 0.75rem;
|
|
143
|
+
padding: 1.25rem 0.75rem;
|
|
144
|
+
background: transparent;
|
|
145
|
+
border: 1px solid var(--border-color, #e1e5e9);
|
|
146
|
+
border-radius: 8px;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
transition: all 0.2s ease;
|
|
149
|
+
text-align: left;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.navigation-grid.compact .nav-button {
|
|
153
|
+
padding: 0.75rem 0.5rem;
|
|
154
|
+
gap: 0.5rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.nav-button:hover {
|
|
158
|
+
border-color: var(--primary-color, #007bff);
|
|
159
|
+
transform: translateY(-2px);
|
|
160
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.nav-button:active {
|
|
164
|
+
transform: translateY(0);
|
|
165
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.nav-button.active {
|
|
169
|
+
background: var(--primary-color, #007bff);
|
|
170
|
+
border-color: var(--primary-color, #007bff);
|
|
171
|
+
color: #fff;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.nav-button.active .nav-button-icon {
|
|
175
|
+
color: #fff;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.nav-button-icon {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: start;
|
|
181
|
+
justify-content: start;
|
|
182
|
+
color: var(--primary-color, #007bff);
|
|
183
|
+
transition: color 0.2s ease;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.nav-button.active .nav-button-icon {
|
|
188
|
+
color: #fff;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.nav-button-label {
|
|
192
|
+
font-size: normal;
|
|
193
|
+
font-weight: bold;
|
|
194
|
+
word-break: break-word;
|
|
195
|
+
text-align: left;
|
|
196
|
+
line-height: 1.2;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.navigation-grid.compact .nav-button-label {
|
|
200
|
+
font-size: 0.85em;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.nav-button.active .nav-button-label {
|
|
204
|
+
color: #fff;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.nav-button-description {
|
|
208
|
+
font-size: small;
|
|
209
|
+
font-weight: normal;
|
|
210
|
+
color: var(--text-muted, #666);
|
|
211
|
+
word-break: break-word;
|
|
212
|
+
text-align: left;
|
|
213
|
+
line-height: 1.5;
|
|
214
|
+
opacity: 0.8;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.navigation-grid.compact .nav-button-description {
|
|
218
|
+
font-size: 0.7em;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.nav-button.active .nav-button-description {
|
|
222
|
+
color: rgba(255, 255, 255, 0.9);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.no-links {
|
|
226
|
+
padding: 2rem;
|
|
227
|
+
text-align: center;
|
|
228
|
+
color: var(--text-muted, #666);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Dark theme support */
|
|
232
|
+
@media (prefers-color-scheme: dark) {
|
|
233
|
+
.nav-button {
|
|
234
|
+
background: transparent;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.nav-button:hover {
|
|
238
|
+
background: var(--hover-background-color, rgba(96, 165, 250, 0.1));
|
|
239
|
+
border-color: var(--primary-color, #60a5fa);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.nav-button.active {
|
|
243
|
+
background: var(--primary-color, #60a5fa);
|
|
244
|
+
border-color: var(--primary-color, #60a5fa);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.nav-button-description {
|
|
248
|
+
color: var(--text-muted, #999);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.nav-button-icon {
|
|
252
|
+
color: var(--primary-color, #60a5fa);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Responsive */
|
|
257
|
+
@media (max-width: 768px) {
|
|
258
|
+
.navigation-grid {
|
|
259
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
260
|
+
gap: 0.75rem;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.nav-button {
|
|
264
|
+
padding: 1rem 0.5rem;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.nav-button-label {
|
|
268
|
+
font-size: normal;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.nav-button-description {
|
|
272
|
+
font-size: 0.7em;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@media (max-width: 480px) {
|
|
277
|
+
.navigation-grid {
|
|
278
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
279
|
+
gap: 0.5rem;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.nav-button {
|
|
283
|
+
padding: 0.75rem 0.25rem;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.nav-button-label {
|
|
287
|
+
font-size: 0.8em;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.nav-button-description {
|
|
291
|
+
font-size: 0.65em;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
</style>
|