64 lines
1.5 KiB
Vue
64 lines
1.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
interface Props {
|
|
title: string
|
|
/** localStorage key for the open/closed state; defaults to one derived from the title. */
|
|
storageKey?: string
|
|
}
|
|
|
|
const icons = ['mdi-chevron-right', 'mdi-chevron-down']
|
|
const { title, storageKey } = defineProps<Props>()
|
|
|
|
const key = computed(() => `expansion-panel:${storageKey ?? title}`)
|
|
|
|
function readStoredOpen(): boolean {
|
|
try {
|
|
return localStorage.getItem(key.value) !== 'closed'
|
|
} catch {
|
|
// ignore storage errors (e.g. disabled in private mode)
|
|
return true
|
|
}
|
|
}
|
|
|
|
const isOpen = ref(readStoredOpen())
|
|
|
|
watch(isOpen, (value) => {
|
|
try {
|
|
localStorage.setItem(key.value, value ? 'open' : 'closed')
|
|
} catch {
|
|
// ignore storage errors (e.g. disabled in private mode)
|
|
}
|
|
})
|
|
|
|
const icon = computed(() => {
|
|
return isOpen.value ? icons[1] : icons[0]
|
|
})
|
|
|
|
const open = () => {
|
|
isOpen.value = !isOpen.value
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="d-flex flex-column ga-1">
|
|
<div class="d-flex align-center ga-1">
|
|
<v-btn density="compact" size="small" :icon="icon" @click="open" />
|
|
<div @click="open" class="font-weight-bold text-capitalize text-title-medium cursor-pointer">{{ title }}</div>
|
|
</div>
|
|
<div v-if="isOpen" class="panel__body ml-6 mt-2">
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
// Lets the body fill the panel when the panel itself is stretched by its parent
|
|
.panel__body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
</style>
|