Files
tehran_tala/apps/user-app-v2/src/components/ui/DropDown.vue
T

163 lines
4.8 KiB
Vue
Raw Normal View History

<!--
Ported from legacy henza_pwa/src/components/dropDown.vue, restyled into the
redesigned design language (see components/home/* and HomeView) — previously
raw Tailwind utility classes with legacy tokens (text-primary,
border-lightGray/20), the one accordion in the app that didn't match the
LESS-token look every other surface uses.
FIXED — the collapse itself. It used to v-if the body in and out and animate
`max-height` from 0 to a hard-coded 240px, which broke three ways: content
taller than 240px was cut off for the whole animation and then jumped to full
height at the end; content shorter than 240px opened fast and then closed
with a dead pause (the first stretch of the shrink happened above the real
height, so nothing moved); and the close used `animation: … reverse` on a
`leave-to` class, which Vue does not time leave transitions by, so it
usually just snapped shut. The body's own padding also popped in and out.
Now the body stays mounted and the panel animates `grid-template-rows` from
0fr to 1fr — the browser interpolates to the content's real height, whatever
it is, in both directions, with no measuring and no cap. All the padding
below the header lives inside the clipped row, so it is part of what
animates. Browsers too old to interpolate grid rows (pre-2022) simply open
and close without the slide.
STRUCTURE IS SHARED, LOOK IS NOT: the template, script and the collapse
mechanics are identical in user-app and user-app-v2; only the "Look" block
at the bottom differs, each in its own app's design language.
-->
<template>
<div class="dd" :class="{ 'is-open': show }">
<button type="button" class="dd__head" :aria-expanded="show" @click="show = !show">
<h3 class="dd__title">
<slot name="text">{{ text }}</slot>
</h3>
<span class="dd__chevron" aria-hidden="true">
<Icon icon="vuesax-linear:arrow-down" size="16" />
</span>
</button>
<div class="dd__panel" :inert="!show">
<div class="dd__clip">
<div class="dd__body">
<slot />
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Icon from '../icon/Icon.vue'
const props = withDefaults(defineProps<{ text?: string; defaultOpen?: boolean }>(), { defaultOpen: false })
const show = ref(props.defaultOpen)
</script>
<style scoped lang="less">
/* ── Mechanics (same in both apps) ──────────────────────────── */
// One curve for the whole collapse: the iOS sheet curve the app's sheets use —
// quick off the mark, soft landing.
@dd-ease: cubic-bezier(0.32, 0.72, 0, 1);
@dd-time: 0.34s;
.dd__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
border: 0;
cursor: pointer;
background: none;
font-family: inherit;
text-align: start;
-webkit-tap-highlight-color: transparent;
}
.dd__title {
margin: 0;
}
.dd__chevron {
flex: 0 0 auto;
display: grid;
place-items: center;
transition: transform @dd-time @dd-ease, color 0.2s ease, background-color 0.2s ease;
}
.dd.is-open .dd__chevron {
transform: rotate(180deg);
}
.dd__panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows @dd-time @dd-ease;
}
.dd.is-open .dd__panel {
grid-template-rows: 1fr;
}
// The row's content box: `min-height: 0` is what lets the 0fr row actually
// shrink below the content's height, and the clip hides what is outside it.
.dd__clip {
min-height: 0;
overflow: hidden;
}
// Fades and settles in a touch behind the height, and out a touch ahead of it,
// so the content never shows half-cut at the moving edge.
.dd__body {
opacity: 0;
transform: translateY(-6px);
transition: opacity 0.18s ease, transform @dd-time @dd-ease;
}
.dd.is-open .dd__body {
opacity: 1;
transform: none;
transition: opacity 0.24s ease 0.06s, transform @dd-time @dd-ease;
}
@media (prefers-reduced-motion: reduce) {
.dd__panel,
.dd__body,
.dd__chevron {
transition: none !important;
}
}
/* ── Look: user-app-v2 ──────────────────────────────────────── */
// A borderless @hairlineSoft panel, the detail page's spec-table radius; the
// chevron sits in a small white squircle that fills @ink when open, the way
// the app's chips do when chosen.
.dd {
border-radius: 22px;
background: @hairlineSoft;
}
.dd__head {
padding: 14px 16px;
}
.dd__title {
font-size: 14px;
font-weight: 700;
color: @ink;
}
.dd__chevron {
width: 28px;
height: 28px;
border-radius: 9px;
color: @inkMuted;
background: @canvas;
}
.dd.is-open .dd__chevron {
color: @onInk;
background: @ink;
}
.dd__body {
padding: 0 16px 16px;
}
</style>