175 lines
4.6 KiB
Vue
175 lines
4.6 KiB
Vue
<script setup lang="ts">
|
|
import type { ComponentProps } from 'vue-component-type-helpers'
|
|
|
|
import Layout from '~/components/ui/Layout.vue'
|
|
import Spacer from '~/components/ui/Spacer.vue'
|
|
import Button from '~/components/ui/Button.vue'
|
|
import Link from '~/components/ui/Link.vue'
|
|
import Heading from '~/components/ui/Heading.vue'
|
|
|
|
const actionComponents
|
|
= { Button, Link }
|
|
|
|
const props = defineProps<{
|
|
columnsPerItem?: 1 | 2 | 3 | 4
|
|
alignLeft?: boolean
|
|
action?: { text: string } & (ComponentProps<typeof Link> | ComponentProps<typeof Button>)
|
|
icon?: string
|
|
} & {
|
|
[H in `h${ '1' | '2' | '3' | '4' | '5' | '6' }`]? : string
|
|
} & {
|
|
[S in 'page-heading' | 'section-heading' | 'large-section-heading' | 'subsection-heading' | 'caption' | 'title' | 'radio' | 'secondary' ]? : true
|
|
} & {
|
|
[Operation in 'expand' | 'collapse']?: () => void
|
|
}>()
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<section style="flex-grow: 1;">
|
|
<Layout
|
|
header
|
|
v-bind="columnsPerItem
|
|
? { grid: `auto / repeat(auto-fit, calc(46px * ${columnsPerItem} + 32px * ${(columnsPerItem) - 1}))` }
|
|
: { flex: true }
|
|
"
|
|
:class="[alignLeft && $style.left, expand || collapse ? $style.collapsible : $style.uncollapsible]"
|
|
>
|
|
<!-- The title row's width is a multiple of the expected items' column span -->
|
|
|
|
<Layout
|
|
flex
|
|
no-gap
|
|
style="
|
|
grid-column: 1 / -1;
|
|
align-self: baseline;
|
|
align-items: baseline;
|
|
position: relative;
|
|
flex-grow: 1;
|
|
"
|
|
>
|
|
<!-- Accordion? -->
|
|
|
|
<template v-if="expand || collapse">
|
|
<Button
|
|
full
|
|
align-text="start"
|
|
align-self="end"
|
|
:class="$style.summary"
|
|
:aria-pressed="!!collapse"
|
|
v-bind="action"
|
|
raised
|
|
@click="() => expand ? expand() : collapse ? collapse() : (() => { return })()"
|
|
>
|
|
<slot name="topleft" />
|
|
|
|
<!-- @vue-ignore -->
|
|
<Heading v-bind="props" />
|
|
</Button>
|
|
<i
|
|
:class="!!expand ? 'bi bi-chevron-down' : 'bi bi-chevron-up'"
|
|
style="
|
|
position: absolute;
|
|
top: 12px;
|
|
right: 0;
|
|
pointer-events: none;
|
|
"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Normal (non-accordion)? -->
|
|
|
|
<template v-else>
|
|
<!-- Set distance between baseline and previous row -->
|
|
<Spacer
|
|
v
|
|
:size="64"
|
|
/>
|
|
<div
|
|
v-if="icon"
|
|
style="display: flex; justify-content: center; align-items: center; width: 48px;"
|
|
>
|
|
<i
|
|
:class="['bi', icon]"
|
|
style="font-size: 18px;"
|
|
/>
|
|
</div>
|
|
<slot name="topleft" />
|
|
<Heading
|
|
v-bind="props"
|
|
style="
|
|
padding: 0 0 24px 0;
|
|
margin: 0;
|
|
"
|
|
/>
|
|
<Spacer grow />
|
|
</template>
|
|
<!-- Action! You can either specify `to` or `onClick`. -->
|
|
<component
|
|
:is="'onClick' in action ? actionComponents.Button : actionComponents.Link"
|
|
v-if="action"
|
|
thin-font
|
|
min-content
|
|
align-self="baseline"
|
|
:class="$style.action"
|
|
v-bind="action"
|
|
>
|
|
{{ action?.text }}
|
|
</component>
|
|
</Layout>
|
|
</Layout>
|
|
|
|
<!-- Love: https://css-tricks.com/css-grid-can-do-auto-height-transitions/ -->
|
|
|
|
<Layout
|
|
main
|
|
:inert="!!expand"
|
|
:style="`${
|
|
'alignLeft' in props && props.alignLeft
|
|
? 'justify-content: start;'
|
|
: ''
|
|
}${
|
|
!!expand
|
|
? 'grid-template-rows: 0fr; overflow: hidden; max-height: 0;'
|
|
: 'max-height: 4000px;'
|
|
}${
|
|
!!collapse
|
|
? 'padding: 12px 0;'
|
|
: ''
|
|
}
|
|
position: relative;
|
|
transition: max-height .5s, grid-template-rows .3s, padding .2s;
|
|
`"
|
|
v-bind="columnsPerItem
|
|
? { grid: `auto / repeat(auto-fit, 46px)` }
|
|
: { flex: true }
|
|
"
|
|
>
|
|
<slot />
|
|
</Layout>
|
|
</section>
|
|
</template>
|
|
|
|
<style module lang="scss">
|
|
// Thank you, css, for offering this weird alternative to !important
|
|
header.left.left {
|
|
justify-content: start;
|
|
}
|
|
|
|
.uncollapsible {
|
|
margin-top: -64px;
|
|
}
|
|
|
|
.summary {
|
|
align-self: baseline;
|
|
min-width: calc(100% + 32px);
|
|
margin: 0 -16px;
|
|
--fw-border-radius: 32px;
|
|
}
|
|
|
|
// Visually push ghost link and non-solid button to the edge
|
|
.action:global(.interactive:not(:is(.primary, .solid, .destructive, .secondary)):is(button, a.ghost)) {
|
|
margin-right: -16px;
|
|
}
|
|
</style>
|