bug fixes, half-finished admin ui, and a more
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
39
ui/components/vlAccordion/content.vue
Executable file
39
ui/components/vlAccordion/content.vue
Executable 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>
|
||||
139
ui/components/vlAccordion/index.vue
Executable file
139
ui/components/vlAccordion/index.vue
Executable 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>
|
||||
19
ui/components/vlAccordion/item.vue
Executable file
19
ui/components/vlAccordion/item.vue
Executable 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>
|
||||
29
ui/components/vlAccordion/trigger.vue
Executable file
29
ui/components/vlAccordion/trigger.vue
Executable 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>
|
||||
Reference in New Issue
Block a user