1<template>
2 <!-- The box eases to the note's height (0 when there is none) while new text
3 rises in. It keeps the last text so a closing note does not go blank. -->
4 <div
5 class="overflow-hidden transition-[height] duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] motion-reduce:transition-none"
6 :style="style"
7 aria-live="polite"
8 >
9 <div ref="inner" class="pt-3">
10 <Transition
11 enter-from-class="translate-y-1 opacity-0 blur-[2px]"
12 enter-active-class="transition-[opacity,transform,filter] duration-300 delay-100 ease-out motion-reduce:transition-none"
13 leave-active-class="hidden"
14 >
15 <p
16 :key="shown"
17 class="text-p-sm text-ink-gray-7 transition-[opacity,transform,filter] duration-150 ease-out motion-reduce:transition-none"
18 :class="{ 'translate-y-1 opacity-0 blur-[2px]': !note }"
19 >
20 {{ shown }}
21 </p>
22 </Transition>
23 </div>
24 </div>
25</template>
26
27<script setup lang="ts">
28import { computed, ref, watch } from "vue";
29import { useElementSize } from "@vueuse/core";
30
31const props = defineProps<{ note?: string }>();
32
33const shown = ref(props.note ?? "");
34watch(
35 () => props.note,
36 (note) => note && (shown.value = note)
37);
38
39const inner = ref<HTMLElement | null>(null);
40const { height } = useElementSize(inner, undefined, { box: "border-box" });
41// Unmeasured: natural height, so a note present on open does not grow in.
42const style = computed(() => {
43 if (!props.note) return { height: "0px" };
44 return height.value ? { height: `${height.value}px` } : undefined;
45});
46</script>
47