bug fixes, half-finished admin ui, and a more

This commit is contained in:
Zoe
2024-09-23 01:21:28 -05:00
parent 6e6bc1c45b
commit 66f8437351
35 changed files with 1039 additions and 141 deletions

View File

@@ -25,7 +25,7 @@ const crumbs = computed(() => {
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m9 6l6 6l-6 6" />
</svg>
<NuxtLink class="focus:outline-none focus:ring focus:ring-inset"
<NuxtLink class="focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
:class="index === crumbs.length - 1 ? 'text-foam' : 'text-subtle hover:text-text focus:text-text'"
:to="crumb.link">{{
crumb.name }}</NuxtLink>

View File

@@ -1,6 +1,6 @@
<template>
<div v-on:click="toggle()" v-on:keypress.enter="toggle()" v-on:keypress.space="toggle()" tabindex="0"
class="w-5 h-5 border rounded cursor-pointer flex items-center justify-center focus:outline-none focus:ring focus:ring-inset"
class="w-5 h-5 border rounded cursor-pointer flex items-center justify-center focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
:class="state === 'unchecked' ? 'hover:bg-muted/5 active:bg-muted/15' : 'bg-accent/10 hover:bg-accent/15 active:bg-accent/25 text-accent'">
<div v-if="state === 'some'" class="w-8/12 h-0.5 bg-current rounded-full"></div>
<span v-else-if="state === 'checked'">

View File

@@ -3,7 +3,10 @@ import { useUser } from '~/composables/useUser'
const { getUser } = useUser()
const props = defineProps({
usageBytes: Number,
usageBytes: {
type: Number,
required: true,
}
})
const user = await getUser()
@@ -47,7 +50,7 @@ const isInFolder = computed(() => route.path.startsWith('/home/') && route.path
<ul class="flex flex-col gap-y-2">
<li>
<NuxtLink to="/home"
class="flex py-1.5 px-4 rounded-lg transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset"
class="flex py-1.5 px-4 rounded-lg transition-bg duration-300 hover:bg-muted/10 focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
:class="{ 'bg-muted/10': isAllFilesActive }">
<div class="flex relative">
<svg class="m-0.5 mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20"

View File

@@ -1,24 +1,40 @@
<script setup lang="ts">
let user = await useUser().getUser()
defineEmits(["update:filenav"])
defineProps(["filenav"])
defineProps(["filenav", "user"])
let colorMode = useColorMode();
const changeTheme = () => {
if (colorMode.preference === "dark") {
// from dark => light
colorMode.preference = "light"
} else if (colorMode.preference === "light") {
// from light => system
colorMode.preference = "system";
} else {
// from system => dark
colorMode.preference = "dark";
}
return;
}
</script>
<template>
<header class="flex h-[var(--nav-height)] px-4 justify-center sticky top-0 z-10 border-b bg-base">
<div class="flex w-full items-center justify-between space-x-2.5">
<p
class="-ml-2.5 flex shrink-0 items-center px-2.5 py-1.5 focus:outline-none focus:ring rounded-m font-semiboldd">
<NuxtLink
class="-ml-2.5 flex shrink-0 items-center px-2.5 py-1.5 transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset font-semibold"
:to="user === undefined ? '/' : '/home'">
filething
</p>
</NuxtLink>
</div>
<nav class="flex md:hidden">
<ul class="flex items-center gap-3" role="list">
<li>
<li v-if="user">
<span class="group relative flex items-center">
<button
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset rounded-md">
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
@@ -31,13 +47,36 @@ defineProps(["filenav"])
</svg>
</span>
</button>
<NavUserDropdown :user="user" />
<NavUserDropdown :changeTheme="changeTheme" :user="user" />
</span>
</li>
<li class="h-6 border-r"></li>
<li>
<li v-else>
<button
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
v-on:click="changeTheme">
<span class="inline-block">
<svg v-if="$colorMode.preference === 'dark'" xmlns="http://www.w3.org/2000/svg" width="22"
height="22" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z" />
</svg>
<svg v-else-if="$colorMode.preference === 'light'" xmlns="http://www.w3.org/2000/svg"
width="22" height="22" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M14.828 14.828a4 4 0 1 0-5.656-5.656a4 4 0 0 0 5.656 5.656m-8.485 2.829l-1.414 1.414M6.343 6.343L4.929 4.929m12.728 1.414l1.414-1.414m-1.414 12.728l1.414 1.414M4 12H2m10-8V2m8 10h2m-10 8v2" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 256 256">
<path fill="currentColor"
d="M208 36H48a28 28 0 0 0-28 28v112a28 28 0 0 0 28 28h160a28 28 0 0 0 28-28V64a28 28 0 0 0-28-28Zm4 140a4 4 0 0 1-4 4H48a4 4 0 0 1-4-4V64a4 4 0 0 1 4-4h160a4 4 0 0 1 4 4Zm-40 52a12 12 0 0 1-12 12H96a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12Z" />
</svg>
</span>
</button>
</li>
<li v-if="filenav" class="h-6 border-r"></li>
<li v-if="filenav">
<button v-on:click="$emit('update:filenav', !filenav)"
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset rounded-md">
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M4 6h16M7 12h13m-10 6h10" />
@@ -50,21 +89,21 @@ defineProps(["filenav"])
<ul class="flex items-center gap-3" role="list">
<li>
<a href="#"
class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset rounded-md">Link</a>
class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Link</a>
</li>
<li>
<a href="#"
class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset rounded-md">Link</a>
class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Link</a>
</li>
<li>
<a href="#"
class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset rounded-md">Link</a>
class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Link</a>
</li>
<li class="h-6 border-r"></li>
<li>
<li v-if="user">
<span class="group relative flex items-center">
<button
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 focus:outline-none focus:ring focus:ring-inset rounded-md">
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
@@ -77,9 +116,32 @@ defineProps(["filenav"])
</svg>
</span>
</button>
<NavUserDropdown :user="user" />
<NavUserDropdown :changeTheme="changeTheme" :user="user" />
</span>
</li>
<li v-else>
<button
class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md"
v-on:click="changeTheme">
<span class="inline-block">
<svg v-if="$colorMode.preference === 'dark'" xmlns="http://www.w3.org/2000/svg" width="22"
height="22" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z" />
</svg>
<svg v-else-if="$colorMode.preference === 'light'" xmlns="http://www.w3.org/2000/svg"
width="22" height="22" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M14.828 14.828a4 4 0 1 0-5.656-5.656a4 4 0 0 0 5.656 5.656m-8.485 2.829l-1.414 1.414M6.343 6.343L4.929 4.929m12.728 1.414l1.414-1.414m-1.414 12.728l1.414 1.414M4 12H2m10-8V2m8 10h2m-10 8v2" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 256 256">
<path fill="currentColor"
d="M208 36H48a28 28 0 0 0-28 28v112a28 28 0 0 0 28 28h160a28 28 0 0 0 28-28V64a28 28 0 0 0-28-28Zm4 140a4 4 0 0 1-4 4H48a4 4 0 0 1-4-4V64a4 4 0 0 1 4-4h160a4 4 0 0 1 4 4Zm-40 52a12 12 0 0 1-12 12H96a12 12 0 0 1 0-24h64a12 12 0 0 1 12 12Z" />
</svg>
</span>
</button>
</li>
</ul>
</nav>
</header>

View File

@@ -1,21 +1,4 @@
<script setup lang="ts">
let colorMode = useColorMode();
const changeTheme = () => {
if (colorMode.preference === "dark") {
// from dark => light
colorMode.preference = "light"
} else if (colorMode.preference === "light") {
// from light => system
colorMode.preference = "system";
} else {
// from system => dark
colorMode.preference = "dark";
}
return;
}
const logout = async () => {
await $fetch('/api/logout', {
method: "POST"
@@ -31,6 +14,10 @@ defineProps({
type: Object,
required: true
},
changeTheme: {
type: Function,
required: true,
}
})
</script>
@@ -38,18 +25,19 @@ defineProps({
<div
class="invisible z-10 w-fit h-fit absolute -right-[4px] top-full opacity-0 group-hover:visible group-focus-within:visible group-focus-within:scale-100 group-focus-within:opacity-100 group-hover:scale-100 group-hover:opacity-100 transition">
<div class="mt-1 w-64 origin-top-right scale-[.97] rounded-xl bg-surface shadow-lg">
<div class="border-b max-w-64 overflow-hidden text-ellipsis p-2">
<div class="max-w-64 overflow-hidden text-ellipsis p-2">
<p class="text-lg font-semibold">{{ user.username }}</p>
<p class="text-subtle text-xs">{{ user.email }}</p>
<p class="text-subtle text-xs">
you have {{ formatBytes(user.plan.max_storage) }} of storage
</p>
</div>
<ul class="p-2 flex flex-col gap-x-1">
<li class="select-none">
<hr />
<ul class="py-2 flex flex-col gap-x-1">
<li class="select-none mx-2">
<button v-on:click="changeTheme"
class="flex items-center hover:bg-muted/10 active:bg-muted/20 transition-bg w-full px-2 py-1 rounded-md focus:outline-none focus:ring focus:ring-inset">
<span class="mr-1.5">
class="flex items-center hover:bg-muted/10 active:bg-muted/20 transition-bg w-full px-2 py-1 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<span class="mr-2">
<svg v-if="$colorMode.preference === 'dark'" xmlns="http://www.w3.org/2000/svg" width="18"
height="18" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -69,10 +57,36 @@ defineProps({
Change Theme
</button>
</li>
<li class="select-none">
<hr class="my-2" />
<li v-if="user.is_admin" class="select-none mx-2">
<NuxtLink
class="flex items-center hover:bg-muted/10 active:bg-muted/20 transition-bg w-full px-2 py-1 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
to="/admin">
<span class="mr-2">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M3 7a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm0 8a3 3 0 0 1 3-3h12a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3zm4-7v.01M7 16v.01M11 8h6m-6 8h6" />
</svg>
</span>
Site Administration
</NuxtLink>
</li>
<hr v-if="user.is_admin" class="my-2" />
<li class="select-none mx-2">
<button
class="flex hover:bg-muted/10 active:bg-muted/20 transition-bg w-full px-2 py-1 rounded-md focus:outline-none focus:ring focus:ring-inset"
class="flex items-center hover:bg-muted/10 active:bg-muted/20 transition-bg w-full px-2 py-1 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"
v-on:click="logout">
<span class="mr-2">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path
d="M14 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-2" />
<path d="M9 12h12l-3-3m0 6l3-3" />
</g>
</svg>
</span>
Logout
</button>
</li>

View File

@@ -16,7 +16,7 @@ const emit = defineEmits(['update:modelValue'])
<div class="flex justify-between mb-2 items-center">
<h3 class="text-xl font-semibold">{{ header }}</h3>
<button v-on:click=" $emit('update:modelValue', !modelValue)"
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg">
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M18 6L6 18M6 6l12 12" />

View File

@@ -114,14 +114,14 @@ let uploadFailed = computed(() => props.uploadingFiles.filter(x => x.status.erro
<h3 class="text-xl font-semibold">Upload</h3>
<div class="flex flex-row gap-x-2">
<button v-on:click="collapsed = !collapsed"
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg">
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="m6 9l6 6l6-6" />
</svg>
</button>
<button v-on:click="$emit('update:closed', true)" v-if="closeable"
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg">
class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M18 6L6 18M6 6l12 12" />
@@ -197,7 +197,7 @@ let uploadFailed = computed(() => props.uploadingFiles.filter(x => x.status.erro
</div>
<div class="flex items-center" v-if="upload.uploading">
<button v-on:click="abortUpload(upload.id)"
class="h-fit p-1 border rounded-md hover:bg-love/10 active:bg-love/20 hover:text-love transition-[background-color,color] text-sm py-1 px-2">
class="h-fit p-1 border rounded-md hover:bg-love/10 active:bg-love/20 hover:text-love focus-visible:text-love focus-visible:bg-love/10 transition-[background-color,color] text-sm py-1 px-2 focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">
Cancel
</button>
</div>

View File

@@ -0,0 +1,39 @@
<script setup>
const item = inject('accordionItem');
const contentHeight = ref(0);
const content = ref(null);
let timeout;
watch(item.hidden, () => {
if (!item.hidden.value) {
timeout = setTimeout(() => {
let styles = window.getComputedStyle(content.value);
let margin = parseFloat(styles['marginTop']) +
parseFloat(styles['marginBottom']);
contentHeight.value = content.value.offsetHeight + margin;
})
}
})
const attrs = useAttrs();
defineOptions({
inheritAttrs: false
});
onUnmounted(() => {
clearTimeout(timeout)
})
</script>
<template>
<div :id="`vueless-${item.index}`" role="region" :aria-labelledby="`vueless-${item.index}`"
class="vl-accordion-content" :style="`--vueless-accordion-content-height: ${contentHeight}px`"
:data-state="(item.isOpen.value) ? 'open' : 'closed'"
@animationend="(!item.isOpen.value) ? item.hidden.value = true : ''" :hidden="item.hidden.value">
<div ref="content" v-bind="attrs">
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,139 @@
<script setup>
const props = defineProps({
type: {
type: String,
default: "multiple"
},
defaultValue: {
type: String
}
})
const accordion = ref(null);
const accordionItems = ref([])
function toggleAccordion(index) {
const item = accordionItems.value[index];
if (props.type === "single") {
// close everything but the one we just opened
accordionItems.value.forEach((item, i) => {
if (i === index) return;
item.isOpen = false;
})
}
if (item.hidden) {
item.hidden = false;
}
item.isOpen = !item.isOpen;
}
const registerAccordionItem = (value) => {
const item = { isOpen: ref(false), hidden: ref(true), value }
accordionItems.value.push(item);
return { index: accordionItems.value.indexOf(item), isOpen: item.isOpen, hidden: item.hidden, value };
};
const unregisterAccordionItem = (index) => {
accordionItems.value.splice(index, 1);
};
provide('accordion', { registerAccordionItem, unregisterAccordionItem, toggleAccordion })
function keydown(event) {
const headers = Array.from(accordion.value.querySelectorAll(".vl-accordion-header"));
if (event.key === "ArrowUp") {
event.preventDefault();
const focusedElement = document.activeElement;
const currentIndex = headers.indexOf(focusedElement);
const nextIndex = currentIndex > 0 ? currentIndex - 1 : headers.length - 1;
console.log(nextIndex, headers)
const nextButton = headers[nextIndex];
nextButton.focus();
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
const focusedElement = document.activeElement;
const currentIndex = headers.indexOf(focusedElement);
const nextIndex = currentIndex < headers.length - 1 ? currentIndex + 1 : 0;
const nextButton = headers[nextIndex];
console.log(nextIndex, headers)
nextButton.focus();
return;
}
if (event.key === "End") {
event.preventDefault();
return headers[headers.length - 1].focus();
}
if (event.key === "Home") {
event.preventDefault();
return headers[0].focus();
}
}
onMounted(() => {
if (!!props.defaultValue) {
const item = accordionItems.value.filter(item => item.value === props.defaultValue)[0];
item.isOpen = true;
item.hidden = false;
}
})
watch(props, () => {
if (!!props.defaultValue) {
const item = accordionItems.value.filter(item => item.value === props.defaultValue)[0];
item.isOpen = true;
item.hidden = false;
}
})
</script>
<template>
<div class="vl-accordion" ref="accordion" @keydown="keydown($event)">
<slot />
</div>
</template>
<style>
.vl-accordion-content {
overflow: hidden;
transform-origin: top center;
height: 0;
}
.vl-accordion-content[data-state="closed"] {
animation: 300ms cubic-bezier(0.25, 1, 0.5, 1) 0s 1 normal forwards running closeAccordion;
}
.vl-accordion-content[data-state="open"] {
animation: 300ms cubic-bezier(0.25, 1, 0.5, 1) 0s 1 normal forwards running openAccordion;
}
@keyframes closeAccordion {
0% {
height: var(--vueless-accordion-content-height);
}
100% {
height: 0px;
}
}
@keyframes openAccordion {
0% {
height: 0px;
}
100% {
height: var(--vueless-accordion-content-height);
}
}
</style>

View File

@@ -0,0 +1,19 @@
<script setup>
const props = defineProps({
value: {
type: String,
default: "",
}
})
const accordion = inject('accordion')
const item = accordion.registerAccordionItem(props.value);
provide('accordionItem', item);
</script>
<template>
<div class="vl-accordion-item" :data-state="(item.isOpen.value) ? 'open' : 'closed'">
<slot />
</div>
</template>

View File

@@ -0,0 +1,29 @@
<script setup>
const item = inject('accordionItem');
const { toggleAccordion } = inject('accordion');
</script>
<template>
<button
class="vl-accordion-header focus-visible:outline-none focus-visible:ring focus-visible:ring-inset select-none cursor-pointer w-full"
@click="toggleAccordion(item.index)">
<div class="flex flex-1 justify-between items-center w-full" :id="`vueless-${item.index}`"
:aria-controls="`vueless-${item.index}`" :aria-expanded="item.isOpen.value">
<slot />
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="m6 9l6 6l6-6" />
</svg>
</div>
</button>
</template>
<style>
.vl-accordion-item button div svg {
transition: transform 300ms ease;
}
.vl-accordion-item[data-state="open"] button div svg {
transform: rotate(180deg);
}
</style>