You've already forked AstralRinth
3c2cc7568d
* feat: add collapsible library groups in app * feat: use accordion rather than custom --------- Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
151 lines
3.4 KiB
Vue
151 lines
3.4 KiB
Vue
<template>
|
|
<div v-bind="$attrs">
|
|
<div v-if="divider && !!slots.title" class="flex items-center gap-4 mb-4">
|
|
<button
|
|
:class="
|
|
buttonClass ??
|
|
'group flex items-center gap-1 bg-transparent m-0 p-0 border-none cursor-pointer'
|
|
"
|
|
@click="() => (forceOpen ? undefined : toggledOpen ? close() : open())"
|
|
>
|
|
<slot name="button" :open="isOpen">
|
|
<div
|
|
class="flex items-center gap-1 whitespace-nowrap transition-colors text-primary group-hover:text-contrast"
|
|
>
|
|
<slot name="title" :open="isOpen" />
|
|
<DropdownIcon
|
|
v-if="!forceOpen"
|
|
class="size-5 transition-transform duration-300 shrink-0 text-secondary group-hover:text-primary"
|
|
:class="{ 'rotate-180': isOpen }"
|
|
/>
|
|
</div>
|
|
</slot>
|
|
</button>
|
|
<hr class="h-px w-full border-none bg-divider" aria-hidden="true" />
|
|
</div>
|
|
<button
|
|
v-else-if="!!slots.title"
|
|
:class="buttonClass ?? 'flex flex-col gap-2 bg-transparent m-0 p-0 border-none'"
|
|
@click="() => (forceOpen ? undefined : toggledOpen ? close() : open())"
|
|
>
|
|
<slot name="button" :open="isOpen">
|
|
<div class="flex items-center gap-1 w-full text-contrast">
|
|
<slot name="title" :open="isOpen" />
|
|
<DropdownIcon
|
|
v-if="!forceOpen"
|
|
class="ml-auto size-5 transition-transform duration-300 shrink-0 text-contrast"
|
|
:class="{ 'rotate-180': isOpen }"
|
|
/>
|
|
</div>
|
|
</slot>
|
|
<slot name="summary" />
|
|
</button>
|
|
<div
|
|
class="accordion-content"
|
|
:class="{ open: isOpen, 'overflow-visible': overflowVisible && showOverflow }"
|
|
@transitionend="onTransitionEnd"
|
|
>
|
|
<div>
|
|
<div :class="contentClass ? contentClass : ''" :inert="!isOpen">
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { DropdownIcon } from '@modrinth/assets'
|
|
import { computed, ref, useSlots, watch } from 'vue'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
openByDefault?: boolean
|
|
type?: 'standard' | 'outlined' | 'transparent'
|
|
buttonClass?: string
|
|
contentClass?: string
|
|
titleWrapperClass?: string
|
|
forceOpen?: boolean
|
|
overflowVisible?: boolean
|
|
divider?: boolean
|
|
}>(),
|
|
{
|
|
type: 'standard',
|
|
openByDefault: false,
|
|
buttonClass: null,
|
|
contentClass: null,
|
|
titleWrapperClass: null,
|
|
forceOpen: false,
|
|
overflowVisible: false,
|
|
divider: false,
|
|
},
|
|
)
|
|
|
|
const toggledOpen = ref(props.openByDefault)
|
|
const isOpen = computed(() => toggledOpen.value || props.forceOpen)
|
|
const showOverflow = ref(props.openByDefault)
|
|
const emit = defineEmits(['onOpen', 'onClose'])
|
|
|
|
const slots = useSlots()
|
|
|
|
watch(
|
|
() => props.openByDefault,
|
|
(newValue) => {
|
|
if (newValue !== toggledOpen.value) {
|
|
toggledOpen.value = newValue
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
function open() {
|
|
toggledOpen.value = true
|
|
emit('onOpen')
|
|
}
|
|
function close() {
|
|
showOverflow.value = false
|
|
toggledOpen.value = false
|
|
emit('onClose')
|
|
}
|
|
function onTransitionEnd() {
|
|
if (isOpen.value) {
|
|
showOverflow.value = true
|
|
}
|
|
}
|
|
|
|
defineExpose({
|
|
open,
|
|
close,
|
|
isOpen: toggledOpen,
|
|
})
|
|
|
|
defineOptions({
|
|
inheritAttrs: false,
|
|
})
|
|
</script>
|
|
<style scoped>
|
|
.accordion-content {
|
|
display: grid;
|
|
grid-template-rows: 0fr;
|
|
transition: grid-template-rows 0.3s ease-in-out;
|
|
}
|
|
|
|
@media (prefers-reduced-motion) {
|
|
.accordion-content {
|
|
transition: none !important;
|
|
}
|
|
}
|
|
|
|
.accordion-content.open {
|
|
grid-template-rows: 1fr;
|
|
}
|
|
|
|
.accordion-content > div {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.accordion-content.overflow-visible > div {
|
|
overflow: visible;
|
|
}
|
|
</style>
|