Added libs

This commit is contained in:
Lucas
2026-01-25 13:55:46 +10:00
parent 575c682afc
commit f70af3c4ea
229 changed files with 26983 additions and 0 deletions

View File

View File

View File

@@ -0,0 +1,7 @@
export default function registerSocketEvents(socket) {
socket.on('connect', (data) => {
console.log('Connected LockboxClient')
})
}

View File

@@ -0,0 +1,125 @@
<script setup>
import { ref } from 'vue';
import { TabPanel} from '@headlessui/vue'
import * as secp from '@noble/secp256k1'
import { KeyIcon, ArrowPathIcon, ClipboardDocumentIcon } from '@heroicons/vue/24/solid'
import Card from '@/components/cards/Card.vue';
import CardHeader from '@/components/cards/CardHeader.vue';
import CardTitle from '@/components/cards/CardTitle.vue';
import CardDescription from '@/components/cards/CardDescription.vue';
import CardContent from '@/components/cards/CardContent.vue';
import Label from '@/components/labels/Label.vue';
import InputText from '@/components/inputs/InputText.vue';
import Button from '@/components/buttons/Button.vue';
import ToogleSwitch from '@/components/buttons/ToogleSwitch.vue';
const privateKeyInput = ref("")
const privateKeyB64 = ref("")
const publicKeyB64 = ref("")
const isTextEnabled = ref(true)
async function generate() {
var array = null
if (isTextEnabled.value){
const encoder = new TextEncoder();
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(privateKeyInput.value));
array = new Uint8Array(hashBuffer);
privateKeyB64.value = btoa(String.fromCharCode(...array))
}else{
try {
const decoded = atob(privateKeyInput.value)
const bytes = new Uint8Array(decoded.length)
for (let i = 0; i < decoded.length; i++) {
bytes[i] = decoded.charCodeAt(i)
}
if (bytes.length !== 32) {
throw new Error('Tamanho inválido')
}
array = bytes
} catch (e) {
array = new Uint8Array(32)
crypto.getRandomValues(array)
privateKeyInput.value = btoa(String.fromCharCode(...array))
}
}
generatePublicKey(array)
}
function generatePublicKey(privateKeyBytes) {
const publicKeyBytes = secp.getPublicKey(privateKeyBytes)
publicKeyB64.value = btoa(String.fromCharCode(...publicKeyBytes))
}
function onSwitchChanged(newValue) {
privateKeyInput.value = ""
privateKeyB64.value = ""
publicKeyB64.value = ""
}
function copy(text){
navigator.clipboard.writeText(text)
}
</script>
<template>
<TabPanel>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-yellow-400">
<KeyIcon class="h-5 w-5" />
Create New User
</CardTitle>
<CardDescription class="text-slate-200">
Generate a new ECDSA keypair for user authentication
</CardDescription>
</CardHeader>
<CardContent class=" space-y-2">
<!-- Private key Characteres input -->
<div class="space-y-2">
<div class="flex flex-row gap-4">
<Label>Private Key</Label>
<ToogleSwitch v-model="isTextEnabled" @update:modelValue="onSwitchChanged"/>
<Label>Text Password</Label>
</div>
<div class="flex gap-2">
<InputText id="privateKey" type="password" v-model="privateKeyInput" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
<Button @click="generate()" class="gap-2">
<ArrowPathIcon class="h-4 w-4"/>
<span v-if="!privateKeyInput">Generate</span>
</Button>
<Button v-if="privateKeyInput" @click="copy(privateKeyInput)" class="flex items-center gap-2">
<ClipboardDocumentIcon class="h-4 w-4"/>
</Button>
</div>
</div>
<!-- Public Key -->
<div class="space-y-2">
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
<div class="flex gap-2">
<InputText id="publicKey" type="text" v-model="publicKeyB64" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
<Button @click="copy(publicKeyB64)" class="flex items-center gap-2">
<ClipboardDocumentIcon class="h-4 w-4"/>
Copy
</Button>
</div>
</div>
<!-- Private Key -->
<div class="space-y-2" v-if="isTextEnabled">
<Label forId="privateKeyB64" class="text-slate-300">Private Key</Label>
<div class="flex gap-2">
<InputText id="privateKeyB64" type="password" v-model="privateKeyB64" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
<Button @click="copy(privateKeyB64)" class="flex items-center gap-2">
<ClipboardDocumentIcon class="h-4 w-4"/>
Copy
</Button>
</div>
</div>
</CardContent>
</Card>
</TabPanel>
</template>

View File

@@ -0,0 +1,259 @@
<script setup>
import { TabPanel} from '@headlessui/vue'
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
import Card from '@/components/cards/Card.vue';
import CardHeader from '@/components/cards/CardHeader.vue';
import CardTitle from '@/components/cards/CardTitle.vue';
import CardDescription from '@/components/cards/CardDescription.vue';
import CardContent from '@/components/cards/CardContent.vue';
import Label from '@/components/labels/Label.vue';
import InputText from '@/components/inputs/InputText.vue';
import Button from '@/components/buttons/Button.vue';
import CardFooter from '@/components/cards/CardFooter.vue';
import { ref } from 'vue';
import { getSocket } from '@/plugins/socketioManager'
import { onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
var socket = null
const publicKey = ref("")
const targetDifficulty = ref("4")
const lockboxServiceApiMineUrl = "/api/lockbox";
const tasks = ref(null)
// TODO Create api file
function start_mining(){
const data = {"public_key":publicKey.value, "force":targetDifficulty.value};
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiMineUrl+"/start", requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
})
.catch(error => {
console.error
('Error:', error);
});
}
function list_tasks(){
fetch(lockboxServiceApiMineUrl+"/list")
.then(response => {
if (!response.ok){
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
tasks.value = data;
})
.catch(error => {
console.error('Error:', error);
});
}
function pause_task(taskId){
const data = {};
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiMineUrl+"/pause/"+taskId, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
})
.catch(error => {
console.error
('Error:', error);
});
}
function resume_task(taskId){
const data = {};
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiMineUrl+"/resume/"+taskId, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
})
.catch(error => {
console.error
('Error:', error);
});
}
function cancel_task(taskId){
const data = {};
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiMineUrl+"/cancel/"+taskId, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
})
.catch(error => {
console.error
('Error:', error);
});
}
function onTaskUpdated(data){
console.log(data)
tasks.value[data.task_id] = data.result
}
onActivated(async () => {
list_tasks();
socket = await getSocket("lockbox_miner")
socket.on('taskUpdated', onTaskUpdated);
});
onDeactivated(()=>{
socket.off('taskUpdated', onTaskUpdated);
});
</script>
<template>
<TabPanel class=" space-y-5">
<!-- Mining user -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<BoltIcon class="h-5 w-5" />
Proof of Work
</CardTitle>
<CardDescription class="text-slate-200">
Generate proof of work for a data
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex flex-row w-full space-x-5 items-center justify-center text-center">
<div class="flex flex-col space-y-2 w-full">
<Label forId="publicKey" class="text-slate-300">Data</Label>
<div class="flex gap-2">
<InputText id="publicKey" v-model="publicKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
</div>
</div>
<div class="flex flex-col space-y-2 w-full">
<Label forId="targetDifficulty" class="text-slate-300">Target Difficulty (Leading Zeros)</Label>
<div class="flex gap-2">
<InputText id="targetDifficulty" v-model="targetDifficulty" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
</div>
</div>
</div>
</CardContent>
<CardFooter>
<Button @click="start_mining()" class="flex items-center gap-2 w-full">
Start Mining
</Button>
</CardFooter>
</Card>
<!-- Active jobs -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-yellow-400">
Active Jobs
</CardTitle>
<CardDescription class="text-slate-200">
Monitor and control proof of work jobs
</CardDescription>
</CardHeader>
<CardContent>
<div class="overflow-y-auto rounded-md p-4">
<CardContent v-for="([taskId, task]) in Object.entries(tasks)" :key="taskId" class="border border-slate-300 rounded mb-2 pt-2">
<div class="grid md:grid-cols-5 gap-4 items-center">
<div class="flex flex-col">
<p class="text-sm text-gray-300 w-full">Data</p>
<p class=" text-gray-300">{{task.data}}</p>
</div>
<div class="flex flex-col">
<p class="text-sm text-gray-300 w-full">Difficulty</p>
<p class=" text-gray-300">{{task.best_force}}/{{task.target_force}}</p>
</div>
<div class="flex flex-col">
<p class="text-sm text-gray-300 w-full">Nonce</p>
<p class=" text-gray-300">{{task.best_nonce}}</p>
</div>
<div class="flex flex-col">
<p class="text-sm text-gray-300 w-full">Status</p>
<p :class="{
'text-yellow-400': task.status === 'paused',
'text-blue-400': task.status === 'running',
'text-green-400': task.status === 'completed',
'text-red-400': task.status === 'cancelled',
'text-gray-300': !['paused', 'running', 'completed', 'cancelled'].includes(task.status)
}">
{{ task.status }}
</p>
</div>
<div class="flex flex-row space-x-3 items-center mt-2">
<div v-if="!['completed', 'cancelled', 'error'].includes(task.status)" class="space-x-3">
<!-- TODO Change to Icons -->
<Button v-if="['running'].includes(task.status)" @click="pause_task(taskId)">Pause</Button>
<Button v-if="['paused'].includes(task.status)" @click="resume_task(taskId)">Play</Button>
<Button @click="cancel_task(taskId)">Cancel</Button>
</div>
</div>
</div>
</CardContent>
</div>
</CardContent>
</Card>
</TabPanel>
</template>

View File

@@ -0,0 +1,136 @@
<script setup>
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
import Card from '@/components/cards/Card.vue';
import CardHeader from '@/components/cards/CardHeader.vue';
import CardTitle from '@/components/cards/CardTitle.vue';
import CardDescription from '@/components/cards/CardDescription.vue';
import CardContent from '@/components/cards/CardContent.vue';
import { onActivated, onDeactivated, ref } from 'vue';
import Button from '@/components/buttons/Button.vue';
import { useAuthStore } from '../../stores/auth';
import { io } from 'socket.io-client';
var socket = null
const lockboxServiceApiUrl = "https://127.0.0.1:5001";
const auth = useAuthStore()
const requestedSignatures = ref([])
async function listSignatureRequests(user){
const requestOptions = {
method: 'GET',
headers: {'Authorization': 'Bearer ' + auth.tokens[user]}
};
fetch(lockboxServiceApiUrl+"/users/"+user+"/signatures", requestOptions)
.then(response => {
if (!response.ok){
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
requestedSignatures.value = data
})
.catch(error => {
console.error('Error:', error);
});
}
function approveSignature(requestId, user, approved){
const data = {"approved":approved};
const requestOptions = {
method: 'POST',
headers: {'Content-Type': 'application/json',
'Authorization': 'Bearer ' + auth.tokens[user]},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiUrl+"/users/"+user+"/signatures/"+requestId, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
updateSignatures()
})
.catch(error => {
console.error
('Error:', error);
});
}
function onUserAdded(data){
console.log(data)
updateSignatures()
}
function onSignatureWaiting(data){
console.log(data)
updateSignatures()
}
onActivated(async () => {
// const socket = await getSocket("lockbox_lockboxClient")
socket = io(lockboxServiceApiUrl)
socket.on('userAdded', onUserAdded);
socket.on('signatureWaiting', onSignatureWaiting);
updateSignatures();
});
onDeactivated(()=>{
socket.off('userAdded', onUserAdded);
socket.off('signatureWaiting', onSignatureWaiting);
});
function updateSignatures(){
Object.entries(auth.tokens).forEach(([key, token]) => {
listSignatureRequests(key)
})
}
</script>
<template>
<TabPanel>
<!-- Head -->
<Card >
<CardHeader>
<CardTitle class="flex items-center gap-2 text-yellow-400">
<ClipboardDocumentListIcon class="h-5 w-5" />
Pending Signing Requests
</CardTitle>
<CardDescription class="text-slate-200">
Review and approve signing requests from logged-in users
</CardDescription>
</CardHeader>
<CardContent>
<CardContent v-for="request in requestedSignatures" class="border border-slate-300 rounded mb-2 pt-2">
<div class="flex flex-row space-x-5">
<div class="flex flex-col w-full">
<p class="font-mono text-sm text-gray-300">{{request.publicKey}}</p>
<p class="font-mono text-sm text-gray-300">{{request.requestId}}</p>
<p class="font-mono text-sm text-gray-300">{{request.info}}</p>
<div class="flex items-center gap-4 text-sm text-gray-400">
<span>{{request.data}}</span>
</div>
</div>
<div class="flex flex-col space-x-3 items-center">
<p class="font-mono text-sm text-gray-300">{{request.action}}</p>
<div class="flex flex-row gap-2">
<Button class="bg-green-600" @click="approveSignature(request.requestId, request.user, true)">Approve</Button>
<Button class="bg-red-600" @click="approveSignature(request.requestId, request.user, false)">Reject</Button>
</div>
</div>
</div>
</CardContent>
</CardContent>
</Card>
</TabPanel>
</template>

View File

@@ -0,0 +1,219 @@
<script setup>
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
import { useAuthStore } from '../../stores/auth';
import Card from '@/components/cards/Card.vue';
import CardHeader from '@/components/cards/CardHeader.vue';
import CardTitle from '@/components/cards/CardTitle.vue';
import CardDescription from '@/components/cards/CardDescription.vue';
import CardContent from '@/components/cards/CardContent.vue';
import Label from '@/components/labels/Label.vue';
import ToogleSwitch from '@/components/buttons/ToogleSwitch.vue';
import { onActivated, onDeactivated, ref } from 'vue';
import InputText from '@/components/inputs/InputText.vue';
import CardFooter from '@/components/cards/CardFooter.vue';
import Button from '@/components/buttons/Button.vue';
import { getSocket } from '@/plugins/socketioManager'
import { io } from 'socket.io-client';
var socket = null
const privateKey = ref("")
const publicKey = ref("")
const proofOfWork = ref("")
const credentialPassword = ref("")
const isLoginEnabled = ref(false)
const lockboxServiceApiUrl = "https://127.0.0.1:5001";
const auth = useAuthStore()
const users = ref({})
function onSwitchChanged(newValue) {
// privateKeyInput.value = ""
// privateKeyB64.value = ""
// publicKeyB64.value = ""
}
function set_login_user(userId){
if(!isLoginEnabled.value){
isLoginEnabled.value = true
}
publicKey.value = userId
}
function add_user(){
const data = {password:privateKey.value, data:{proof_of_work:proofOfWork.value}, credentialPassword:credentialPassword.value};
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiUrl+"/users", requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
auth.setToken(data.verifying_key, data.token);
})
.catch(error => {
console.error
('Error:', error);
});
}
function login_user(){
const data = {credentialPassword:credentialPassword.value};
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
fetch(lockboxServiceApiUrl+"/users/"+publicKey.value+"/login", requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
auth.setToken(data.verifying_key, data.token);
})
.catch(error => {
console.error
('Error:', error);
});
}
function logout_user(userId){
delete auth.tokens[userId]
list_users()
}
function list_users(){
fetch(lockboxServiceApiUrl+"/users")
.then(response => {
if (!response.ok){
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
users.value = data;
})
.catch(error => {
console.error('Error:', error);
});
}
function onUserAdded(data){
console.log(data)
list_users();
}
onActivated(async () => {
list_users();
// const socket = await getSocket("lockbox_lockboxClient")
socket = io(lockboxServiceApiUrl)
socket.on('userAdded', onUserAdded);
});
onDeactivated(()=>{
socket.off('userAdded', onUserAdded);
});
</script>
<template>
<TabPanel class="flex flex-col space-y-3">
<Card class="">
<CardHeader>
<CardTitle class="flex items-center gap-2 text-yellow-400">
<UserPlusIcon class="h-5 w-5" />
Add User
</CardTitle>
<CardDescription class="text-slate-200">
Add a user with private key or login with public key
</CardDescription>
</CardHeader>
<CardContent class=" space-y-2">
<div class="flex flex-row gap-4">
<Label>Add with Private Key</Label>
<ToogleSwitch v-model="isLoginEnabled" @update:modelValue="onSwitchChanged"/>
<Label>Login with Public Key</Label>
</div>
<div v-if="!isLoginEnabled" class="flex flex-col space-y-2 w-full">
<Label forId="privateKey" class="text-slate-300">Private Key</Label>
<InputText id="privateKey" type="password" v-model="privateKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
</div>
<div v-if="!isLoginEnabled" class="flex flex-col space-y-2 w-full">
<Label forId="proofOfWork" class="text-slate-300">Proof of Work</Label>
<InputText id="proofOfWork" v-model="proofOfWork" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
</div>
<div v-if="isLoginEnabled" class="flex flex-col space-y-2 w-full">
<Label forId="publicKey" class="text-slate-300">Public Key</Label>
<InputText id="publicKey" v-model="publicKey" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
</div>
<div class="flex flex-col space-y-2 w-full">
<Label forId="credentialPassword" class="text-slate-300">Credential Password</Label>
<InputText id="credentialPassword" type="password" v-model="credentialPassword" placeholder="" class="border-gray-700 bg-slate-300 w-full"/>
</div>
</CardContent>
<CardFooter>
<Button v-if="!isLoginEnabled" @click="add_user()" class="flex items-center gap-2 w-full">
Save
</Button>
<Button v-if="isLoginEnabled" @click="login_user()" class="flex items-center gap-2 w-full">
Login
</Button>
</CardFooter>
</Card>
<Card >
<CardHeader>
<CardTitle class="flex items-center gap-2 text-yellow-400">
<UsersIcon class="h-5 w-5" />
User Management
</CardTitle>
<CardDescription class="text-slate-200">
View and manage your users in the system
</CardDescription>
</CardHeader>
<div class="px-4">
<CardContent v-for="user in users" class="border border-yellow-400/20 rounded mb-2 pt-2">
<div class="flex flex-row items-start gap-x-5">
<div class="space-y-1">
<p class="font-mono text-sm text-gray-300">{{user.id}}</p>
<div class="flex items-center gap-4 text-sm text-gray-400">
<span>Added At: {{user.added_at}}</span>
</div>
</div>
<div class="flex flex-row space-x-3 items-center">
<!-- TODO Create field logged:bool -->
<div v-if="user.id in auth.tokens">
<Button class="bg-red-600" @click="logout_user(user.id)">Logout</Button>
</div>
<div v-if="!(user.id in auth.tokens)">
<Button class="bg-green-600" @click="set_login_user(user.id)">Login</Button>
</div>
</div>
</div>
</CardContent>
</div>
</Card>
</TabPanel>
</template>

View File

@@ -0,0 +1 @@
@noble/secp256k1

7
lockbox/vue/router.js Normal file
View File

@@ -0,0 +1,7 @@
import HomeView from "./views/HomeView.vue";
const routes = [
{path: '/', name:'lockbox', component: HomeView},
]
export {routes};

View File

@@ -0,0 +1,21 @@
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
tokens: {},
}),
actions: {
setToken(publicKey, token) {
this.tokens[publicKey] = token
localStorage.setItem(publicKey, token)
},
clearToken(publicKey) {
this.tokens[publicKey] = null
localStorage.removeItem(publicKey)
},
loadTokenFromStorage(publicKey) {
const token = localStorage.getItem(publicKey)
if (token) this.tokens[publicKey] = token
}
},
})

View File

@@ -0,0 +1,59 @@
<script setup>
import { TabGroup, TabList, Tab, TabPanels, TabPanel} from '@headlessui/vue'
import { KeyIcon, BoltIcon, ShieldCheckIcon, UserPlusIcon, UsersIcon, ClipboardDocumentListIcon} from '@heroicons/vue/24/solid'
import TabCreateUser from '../components/tabs/TabCreateUser.vue'
import TabProofOfWork from '../components/tabs/TabProofOfWork.vue'
import TabAddUser from '../components/tabs/TabUsers.vue'
import TabSigningRequests from '../components/tabs/TabSigningRequests.vue'
const tabItems = [
{label:"Create User", icon:KeyIcon, tabComponent:TabCreateUser},
{label:"Proof of Work", icon:BoltIcon, tabComponent:TabProofOfWork},
{label:"Users", icon:UserPlusIcon, tabComponent:TabAddUser},
{label:"Signing Requests", icon:ClipboardDocumentListIcon, tabComponent:TabSigningRequests},
]
</script>
<template>
<div class="min-h-screen bg-black w-screen text-yellow-400">
<header className="border-b border-yellow-400/20 py-6">
<div className="container mx-auto px-6">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="text-3xl font-black">
<span className="text-yellow-400">LOCK</span>
<span className="text-white">BOX</span>
</div>
<div className="w-1 h-8 bg-yellow-400"></div>
<span className="text-gray-400">Secure ECDSA Keypair Management</span>
</div>
</div>
</div>
</header>
<div className="container mx-auto px-6">
<TabGroup>
<TabList class="flex flex-wrap gap-2 mb-8 border-b border-yellow-400/20">
<Tab v-for="tab in tabItems" as="template" :key="tab" v-slot="{ selected }">
<button class="flex items-center space-x-2 px-6 py-3 font-semibold transition-all duration-300 border-b-2"
:class="{ 'text-yellow-400 border-yellow-400': selected, 'text-gray-400 border-transparent hover:text-yellow-400 hover:border-yellow-400/50': !selected }">
<component :is="tab.icon" class="h-4 w-4"></component>
<span>{{ tab.label }}</span>
</button>
</Tab>
</TabList>
<TabPanels class="mt-6 space-y-6 px-40" v-for="tab in tabItems" :key="tab.label">
<component :is="tab.tabComponent"></component>
</TabPanels>
</TabGroup>
</div>
</div>
</template>
<style scoped>
/* button {
transition: background 0.2s, color 0.2s;
} */
</style>