picocrank 1.10.0 → 1.11.1
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/Calendar.vue +1 -5
- 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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tabs-container">
|
|
3
|
+
<div class="tabs-header" role="tablist">
|
|
4
|
+
<button
|
|
5
|
+
v-for="(tab, index) in tabs"
|
|
6
|
+
:key="tab.id || index"
|
|
7
|
+
:class="['tab-button', { active: activeTabId === (tab.id || index) }]"
|
|
8
|
+
:aria-selected="activeTabId === (tab.id || index)"
|
|
9
|
+
:aria-controls="`tab-panel-${tab.id || index}`"
|
|
10
|
+
:id="`tab-${tab.id || index}`"
|
|
11
|
+
role="tab"
|
|
12
|
+
@click="selectTab(tab.id || index)"
|
|
13
|
+
>
|
|
14
|
+
<HugeiconsIcon
|
|
15
|
+
v-if="tab.icon"
|
|
16
|
+
:icon="tab.icon"
|
|
17
|
+
width="1em"
|
|
18
|
+
height="1em"
|
|
19
|
+
/>
|
|
20
|
+
<span>{{ tab.label }}</span>
|
|
21
|
+
<slot :name="`tab-${tab.id || index}-badge`" />
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="tabs-content">
|
|
25
|
+
<div
|
|
26
|
+
v-for="(tab, index) in tabs"
|
|
27
|
+
:key="tab.id || index"
|
|
28
|
+
:id="`tab-panel-${tab.id || index}`"
|
|
29
|
+
:class="['tab-panel', { active: activeTabId === (tab.id || index) }]"
|
|
30
|
+
role="tabpanel"
|
|
31
|
+
:aria-labelledby="`tab-${tab.id || index}`"
|
|
32
|
+
v-show="activeTabId === (tab.id || index)"
|
|
33
|
+
>
|
|
34
|
+
<slot :name="`tab-${tab.id || index}`" :tab="tab" />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup>
|
|
41
|
+
import { ref, onMounted, watch } from 'vue';
|
|
42
|
+
import { HugeiconsIcon } from '@hugeicons/vue';
|
|
43
|
+
|
|
44
|
+
const props = defineProps({
|
|
45
|
+
tabs: {
|
|
46
|
+
type: Array,
|
|
47
|
+
required: true,
|
|
48
|
+
validator: (tabs) => {
|
|
49
|
+
return tabs.every(tab =>
|
|
50
|
+
typeof tab === 'object' &&
|
|
51
|
+
(tab.id !== undefined || true) &&
|
|
52
|
+
typeof tab.label === 'string'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
defaultTab: {
|
|
57
|
+
type: [String, Number],
|
|
58
|
+
default: null
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits(['tab-change']);
|
|
63
|
+
|
|
64
|
+
// Initialize activeTabId with first tab by default
|
|
65
|
+
function getInitialTabId() {
|
|
66
|
+
if (props.defaultTab !== null) {
|
|
67
|
+
return props.defaultTab;
|
|
68
|
+
}
|
|
69
|
+
if (props.tabs.length > 0) {
|
|
70
|
+
return props.tabs[0].id ?? 0;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const activeTabId = ref(getInitialTabId());
|
|
76
|
+
|
|
77
|
+
function selectTab(tabId) {
|
|
78
|
+
if (activeTabId.value !== tabId) {
|
|
79
|
+
activeTabId.value = tabId;
|
|
80
|
+
const activeTab = props.tabs.find(tab => (tab.id ?? props.tabs.indexOf(tab)) === tabId);
|
|
81
|
+
emit('tab-change', activeTab, tabId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function setActiveTab(tabId) {
|
|
86
|
+
const tab = props.tabs.find(t => (t.id ?? props.tabs.indexOf(t)) === tabId);
|
|
87
|
+
if (tab) {
|
|
88
|
+
selectTab(tabId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initialize active tab on mount if not already set
|
|
93
|
+
onMounted(() => {
|
|
94
|
+
if (activeTabId.value === null && props.tabs.length > 0) {
|
|
95
|
+
activeTabId.value = props.tabs[0].id ?? 0;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Watch for tabs changes
|
|
100
|
+
watch(() => props.tabs, (newTabs) => {
|
|
101
|
+
if (newTabs.length > 0 && activeTabId.value === null) {
|
|
102
|
+
activeTabId.value = newTabs[0].id ?? 0;
|
|
103
|
+
}
|
|
104
|
+
}, { immediate: true });
|
|
105
|
+
|
|
106
|
+
defineExpose({
|
|
107
|
+
setActiveTab,
|
|
108
|
+
activeTabId
|
|
109
|
+
});
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
.tabs-container {
|
|
114
|
+
width: 100%;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.tabs-header {
|
|
118
|
+
display: flex;
|
|
119
|
+
border-bottom: 2px solid var(--border-color, #e1e5e9);
|
|
120
|
+
gap: 0.25rem;
|
|
121
|
+
overflow-x: auto;
|
|
122
|
+
scrollbar-width: thin;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.tabs-header::-webkit-scrollbar {
|
|
126
|
+
height: 4px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.tabs-header::-webkit-scrollbar-track {
|
|
130
|
+
background: transparent;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.tabs-header::-webkit-scrollbar-thumb {
|
|
134
|
+
background: var(--border-color, #ccc);
|
|
135
|
+
border-radius: 2px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.tab-button {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: 0.5rem;
|
|
142
|
+
padding: 0.75rem 1.25rem;
|
|
143
|
+
background: transparent;
|
|
144
|
+
border: none;
|
|
145
|
+
border-bottom: 2px solid transparent;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
font-size: 1em;
|
|
148
|
+
color: var(--text-muted, #666);
|
|
149
|
+
transition: all 0.2s ease;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
position: relative;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.tab-button:hover {
|
|
155
|
+
color: var(--text-color, #000);
|
|
156
|
+
background-color: var(--hover-background-color, #f5f5f5);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.tab-button.active {
|
|
160
|
+
color: var(--primary-color, #007bff);
|
|
161
|
+
border-bottom-color: var(--primary-color, #007bff);
|
|
162
|
+
font-weight: 500;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.tab-button:focus {
|
|
166
|
+
outline: 2px solid var(--primary-color, #007bff);
|
|
167
|
+
outline-offset: -2px;
|
|
168
|
+
border-radius: 4px 4px 0 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.tabs-content {
|
|
172
|
+
width: 100%;
|
|
173
|
+
min-height: 200px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.tab-panel {
|
|
177
|
+
padding: 1.5rem;
|
|
178
|
+
display: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.tab-panel.active {
|
|
182
|
+
display: block;
|
|
183
|
+
animation: fadeIn 0.2s ease-in;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@keyframes fadeIn {
|
|
187
|
+
from {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transform: translateY(4px);
|
|
190
|
+
}
|
|
191
|
+
to {
|
|
192
|
+
opacity: 1;
|
|
193
|
+
transform: translateY(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Dark theme support */
|
|
198
|
+
@media (prefers-color-scheme: dark) {
|
|
199
|
+
.tabs-header {
|
|
200
|
+
border-bottom-color: var(--border-color, #444);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.tab-button {
|
|
204
|
+
color: var(--text-muted, #999);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.tab-button:hover {
|
|
208
|
+
color: var(--text-color, #fff);
|
|
209
|
+
background-color: var(--hover-background-color, #333);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.tab-button.active {
|
|
213
|
+
color: var(--primary-color, #60a5fa);
|
|
214
|
+
border-bottom-color: var(--primary-color, #60a5fa);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Responsive */
|
|
219
|
+
@media (max-width: 768px) {
|
|
220
|
+
.tabs-header {
|
|
221
|
+
gap: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.tab-button {
|
|
225
|
+
padding: 0.75rem 1rem;
|
|
226
|
+
font-size: 0.9em;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.tab-panel {
|
|
230
|
+
padding: 1rem;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
</style>
|
package/vue/views/Admin.vue
CHANGED
|
@@ -9,29 +9,29 @@
|
|
|
9
9
|
<h3>System Overview</h3>
|
|
10
10
|
<p>Welcome to the administrative panel. This is a placeholder for administrative functionality.</p>
|
|
11
11
|
|
|
12
|
-
<div class="
|
|
13
|
-
<div class="stat-
|
|
12
|
+
<div class="grid-boxed">
|
|
13
|
+
<div class="stat-display">
|
|
14
14
|
<h4>Total Users</h4>
|
|
15
|
-
<div class="stat-
|
|
16
|
-
<div class="
|
|
15
|
+
<div class="stat fg-good">1,247</div>
|
|
16
|
+
<div class="subtle">+12% from last month</div>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
|
-
<div class="stat-
|
|
19
|
+
<div class="stat-display">
|
|
20
20
|
<h4>Active Sessions</h4>
|
|
21
|
-
<div class="stat-
|
|
22
|
-
<div class="
|
|
21
|
+
<div class="stat fg-bad">89</div>
|
|
22
|
+
<div class="subtle">-3% from last hour</div>
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
|
-
<div class="stat-
|
|
25
|
+
<div class="stat-display">
|
|
26
26
|
<h4>System Load</h4>
|
|
27
|
-
<div class="stat-
|
|
28
|
-
<div class="
|
|
27
|
+
<div class="stat fg-warning">67%</div>
|
|
28
|
+
<div class="subtle">Moderate</div>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
|
-
<div class="stat-
|
|
31
|
+
<div class="stat-display">
|
|
32
32
|
<h4>Storage Used</h4>
|
|
33
|
-
<div class="stat-
|
|
34
|
-
<div class="
|
|
33
|
+
<div class="stat fg-good">2.4 GB</div>
|
|
34
|
+
<div class="subtle">45% of total</div>
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
|
|
65
65
|
<div class="admin-section">
|
|
66
66
|
<h3>Quick Actions</h3>
|
|
67
|
-
<div
|
|
67
|
+
<div role = "toolbar">
|
|
68
68
|
<button class="action-btn">Create User</button>
|
|
69
69
|
<button class="action-btn">System Backup</button>
|
|
70
70
|
<button class="action-btn">View Logs</button>
|
package/vue/views/App.vue
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<script setup>
|
|
31
31
|
import { ref, onMounted, provide } from 'vue';
|
|
32
32
|
import { useRouter } from 'vue-router';
|
|
33
|
-
import { Pin02Icon } from '@hugeicons/core-free-icons'
|
|
33
|
+
import { Pin02Icon, SecurityValidationIcon, EditIcon, ViewIcon, HomeIcon } from '@hugeicons/core-free-icons'
|
|
34
34
|
|
|
35
35
|
import QuickSearch from './../components/QuickSearch.vue'
|
|
36
36
|
import Navigation from './../components/Navigation.vue'
|
|
@@ -63,11 +63,18 @@
|
|
|
63
63
|
navigation.value.addRouterLink('TableExample')
|
|
64
64
|
navigation.value.addRouterLink('CalendarExample')
|
|
65
65
|
navigation.value.addRouterLink('FormExample')
|
|
66
|
+
navigation.value.addRouterLink('TabsExample')
|
|
66
67
|
navigation.value.addSeparator('separator-1');
|
|
67
68
|
navigation.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
|
|
68
69
|
navigation.value.addSeparator('separator-2');
|
|
69
70
|
navigation.value.addCallback('Callback Example', helloWorld, { icon: Pin02Icon })
|
|
70
71
|
navigation.value.addSeparator('separator-3');
|
|
72
|
+
navigation.value.addHtml('<h2 style = "padding: 0.75em;">Authentication</h2>', { name: 'auth-heading' })
|
|
73
|
+
navigation.value.addRouterLink('LoginExample')
|
|
74
|
+
navigation.value.addSeparator('separator-4');
|
|
75
|
+
navigation.value.addHtml('<h2 style = "padding: 0.75em;">User Control Panel</h2>', { name: 'user-panel-heading' })
|
|
76
|
+
navigation.value.addRouterLink('UserControlPanel')
|
|
77
|
+
navigation.value.addSeparator('separator-5');
|
|
71
78
|
navigation.value.addHtml('<h2 style = "padding: 0.75em;">Administration</h2>', { name: 'admin-heading' })
|
|
72
79
|
navigation.value.addRouterLink('Admin')
|
|
73
80
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section title="Login" classes="">
|
|
3
|
+
<Login
|
|
4
|
+
@local-login="handleLocalLogin"
|
|
5
|
+
@oauth-login="handleOAuthLogin"
|
|
6
|
+
@tab-change="onTabChange"
|
|
7
|
+
/>
|
|
8
|
+
</Section>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup>
|
|
12
|
+
import { useRouter } from 'vue-router';
|
|
13
|
+
import Section from '../components/Section.vue';
|
|
14
|
+
import Login from '../components/Login.vue';
|
|
15
|
+
|
|
16
|
+
const router = useRouter();
|
|
17
|
+
|
|
18
|
+
// Handle local login
|
|
19
|
+
function handleLocalLogin(credentials) {
|
|
20
|
+
console.log('Local login attempt:', {
|
|
21
|
+
username: credentials.username,
|
|
22
|
+
password: '***' // Don't log passwords in production
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// TODO: Implement actual authentication logic
|
|
26
|
+
// Example: Simulate API call
|
|
27
|
+
// try {
|
|
28
|
+
// const response = await fetch('/api/auth/local', {
|
|
29
|
+
// method: 'POST',
|
|
30
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
// body: JSON.stringify(credentials)
|
|
32
|
+
// });
|
|
33
|
+
//
|
|
34
|
+
// if (response.ok) {
|
|
35
|
+
// const data = await response.json();
|
|
36
|
+
// // Store token, redirect, etc.
|
|
37
|
+
// router.push('/');
|
|
38
|
+
// } else {
|
|
39
|
+
// // Handle error
|
|
40
|
+
// }
|
|
41
|
+
// } catch (error) {
|
|
42
|
+
// // Handle error
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
// Placeholder success
|
|
46
|
+
alert(`Local login would authenticate: ${credentials.username}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle OAuth login
|
|
50
|
+
function handleOAuthLogin(provider) {
|
|
51
|
+
console.log('OAuth login attempt:', provider);
|
|
52
|
+
|
|
53
|
+
// TODO: Implement actual OAuth redirect logic
|
|
54
|
+
// Example: Redirect to OAuth provider
|
|
55
|
+
// window.location.href = provider.authUrl;
|
|
56
|
+
|
|
57
|
+
// Placeholder
|
|
58
|
+
alert(`OAuth login would redirect to: ${provider.name}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle tab change
|
|
62
|
+
function onTabChange(tab, tabId) {
|
|
63
|
+
console.log('Tab changed:', tab, tabId);
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Section title="Login Example" subtitle="A reusable login form component with local and OAuth2 authentication">
|
|
3
|
+
<Login
|
|
4
|
+
:oauth-providers="oauthProviders"
|
|
5
|
+
@local-login="handleLocalLogin"
|
|
6
|
+
@oauth-login="handleOAuthLogin"
|
|
7
|
+
@tab-change="onTabChange"
|
|
8
|
+
/>
|
|
9
|
+
</Section>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
import { ref } from 'vue';
|
|
14
|
+
import { useRouter } from 'vue-router';
|
|
15
|
+
import Section from '../components/Section.vue';
|
|
16
|
+
import Login from '../components/Login.vue';
|
|
17
|
+
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
|
|
20
|
+
// OAuth providers configuration
|
|
21
|
+
const oauthProviders = ref([
|
|
22
|
+
{
|
|
23
|
+
id: 'google',
|
|
24
|
+
name: 'Google',
|
|
25
|
+
icon: null, // Add icon from @hugeicons/core-free-icons if available
|
|
26
|
+
class: 'good',
|
|
27
|
+
authUrl: '/api/auth/google'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'github',
|
|
31
|
+
name: 'GitHub',
|
|
32
|
+
icon: null, // Add icon from @hugeicons/core-free-icons if available
|
|
33
|
+
class: 'neutral',
|
|
34
|
+
authUrl: '/api/auth/github'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'microsoft',
|
|
38
|
+
name: 'Microsoft',
|
|
39
|
+
icon: null, // Add icon from @hugeicons/core-free-icons if available
|
|
40
|
+
class: 'good',
|
|
41
|
+
authUrl: '/api/auth/microsoft'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'facebook',
|
|
45
|
+
name: 'Facebook',
|
|
46
|
+
icon: null, // Add icon from @hugeicons/core-free-icons if available
|
|
47
|
+
class: 'good',
|
|
48
|
+
authUrl: '/api/auth/facebook'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'twitter',
|
|
52
|
+
name: 'Twitter',
|
|
53
|
+
icon: null, // Add icon from @hugeicons/core-free-icons if available
|
|
54
|
+
class: 'neutral',
|
|
55
|
+
authUrl: '/api/auth/twitter'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'apple',
|
|
59
|
+
name: 'Apple',
|
|
60
|
+
icon: null, // Add icon from @hugeicons/core-free-icons if available
|
|
61
|
+
class: 'neutral',
|
|
62
|
+
authUrl: '/api/auth/apple'
|
|
63
|
+
}
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Handle local login
|
|
67
|
+
function handleLocalLogin(credentials) {
|
|
68
|
+
console.log('Local login attempt:', {
|
|
69
|
+
username: credentials.username,
|
|
70
|
+
password: '***' // Don't log passwords in production
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// TODO: Implement actual authentication logic
|
|
74
|
+
// Example: Simulate API call
|
|
75
|
+
// try {
|
|
76
|
+
// const response = await fetch('/api/auth/local', {
|
|
77
|
+
// method: 'POST',
|
|
78
|
+
// headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
// body: JSON.stringify(credentials)
|
|
80
|
+
// });
|
|
81
|
+
//
|
|
82
|
+
// if (response.ok) {
|
|
83
|
+
// const data = await response.json();
|
|
84
|
+
// // Store token, redirect, etc.
|
|
85
|
+
// router.push('/');
|
|
86
|
+
// } else {
|
|
87
|
+
// // Handle error - you can use the component's setLocalLoginError method
|
|
88
|
+
// }
|
|
89
|
+
// } catch (error) {
|
|
90
|
+
// // Handle error
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
// Placeholder success
|
|
94
|
+
alert(`Local login would authenticate: ${credentials.username}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle OAuth login
|
|
98
|
+
function handleOAuthLogin(provider) {
|
|
99
|
+
console.log('OAuth login attempt:', provider);
|
|
100
|
+
|
|
101
|
+
// TODO: Implement actual OAuth redirect logic
|
|
102
|
+
// Example: Redirect to OAuth provider
|
|
103
|
+
// window.location.href = provider.authUrl;
|
|
104
|
+
|
|
105
|
+
// Placeholder
|
|
106
|
+
alert(`OAuth login would redirect to: ${provider.name}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle tab change
|
|
110
|
+
function onTabChange(tab, tabId) {
|
|
111
|
+
console.log('Tab changed:', tab, tabId);
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
/* Additional styles if needed */
|
|
117
|
+
</style>
|