feat(ui): add collapse (Accordion) feature to Section component

This commit is contained in:
upsiflu 2025-03-20 21:34:18 +01:00
parent 45d18dc493
commit c42f08babe
4 changed files with 134 additions and 35 deletions

View File

@ -111,6 +111,7 @@ store.dispatch('auth/fetchUser')
<ReportModal />
<UploadModal v-if="store.state.auth.authenticated" />
<SearchModal />
<ServiceMessages />
</div>
</template>

View File

@ -26,23 +26,25 @@ const size = computed(() => (Object.entries(props).find(([key, value]) => value
:is(h1, h2, h3, h4, h5, h6).heading { margin: 0; padding:0; vertical-align: baseline; align-self: baseline;}
/* Page heading */
.pageheading { font-size: 36px; font-weight: 900; letter-spacing: -1px; }
:is(*, .vp-doc h3).pageheading { font-size: 36px; font-weight: 900; letter-spacing: -1px; }
/* Section heading, Modal heading [DEFAULT] */
/* TODO: Decide on a size. All mockups on https://design.funkwhale.audio/ have 20px. */
.sectionheading { font-size: 28px; font-weight: 700; letter-spacing: -.5px; }
:is(*, .vp-doc h3).sectionheading { font-size: 20px; font-weight: 700; letter-spacing: -.5px; }
/* Form subsection */
.subsectionheading {font-size: 16px; font-weight: 600; letter-spacing: 0; }
:is(*, .vp-doc h3).subsectionheading {font-size: 16px; font-weight: 600; letter-spacing: 0; }
/* input caption */
.caption {font-size: 14px; font-weight: 600; letter-spacing: .25px; }
:is(*, .vp-doc h3).caption {font-size: 14px; font-weight: 600; letter-spacing: .25px; }
/* Tab title, Channel title, Card title, Activity title */
.title { font-size: 16px; font-weight: 700; line-height: 18px; }
:is(*, .vp-doc h3).title { font-size: 16px; font-weight: 700; line-height: 18px; }
/* Primary radio title */
.radio { font-size: 28px; font-weight: 900; letter-spacing: -.5px; }
:is(*, .vp-doc h3).radio { font-size: 28px; font-weight: 900; letter-spacing: -.5px; }
/* Secondary radio title */
.secondary { font-size: 28px; font-weight: 300; letter-spacing: -.5px; }
:is(*, .vp-doc h3).secondary { font-size: 28px; font-weight: 300; letter-spacing: -.5px; }
</style>

View File

@ -12,13 +12,17 @@ const actionComponents
= { Button, Link }
const props = defineProps<{
[M in 'no-items' | 'tiny-items' | 'small-items' | 'medium-items']?: true }
[M in 'no-items' | 'tiny-items' | 'small-items' | 'medium-items']?: true
}
& {
alignLeft?: boolean;
collapsed?: boolean;
action?: { text: string } & (RouterLinkProps | { onClick: (...args: any[]) => void | Promise<void> })
}
& {
[Operation in 'expand' | 'collapse']?: () => void
}
& { [H in 'h1' | 'h2' | 'h3']?: string }
& { action?: { text: string } & (RouterLinkProps | { onClick: (...args: any[]) => void | Promise<void> }) }>()
>()
const heading
= props.h1
@ -41,27 +45,65 @@ const headerGrid
<Layout
header
:grid="headerGrid"
:style="`${'alignLeft' in props && props.alignLeft ? 'justify-content: start' : ''};
margin-top: -64px;`"
:style="`
${ 'alignLeft' in props && props.alignLeft ? 'justify-content: start' : '' };
${ expand || collapse ? '' : 'margin-top: -64px;' }`
"
>
<!-- 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;"
style="
grid-column: 1 / -1;
align-self: baseline;
position: relative;
"
>
<!-- Set distance between baseline and previous row -->
<Spacer
v
:size="64"
style="align-self: baseline;"
/>
<Heading
v-if="heading"
v-bind="heading"
style="align-self: baseline; padding:0 0 24px 0; margin:0;"
/>
<Spacer grow />
<template v-if="expand || collapse">
<Button
full
align-text="left"
align-self="end"
:class="$style.summary"
:aria-pressed="!!collapse"
raised
@click="() => expand ? expand() : collapse ? collapse() : (() => { return })()"
>
<Heading
v-if="heading"
v-bind="heading"
caption
/>
</Button>
<i
:class="!!expand ? 'bi bi-chevron-down' : 'bi bi-chevron-up'"
style="
position: absolute;
top: 12px;
right: 0;
pointer-events: none;
"
/>
</template>
<template v-else>
<!-- Set distance between baseline and previous row -->
<Spacer
v
:size="64"
style="align-self: baseline;"
/>
<Heading
v-if="heading"
v-bind="heading"
style="
align-self: baseline;
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"
@ -69,14 +111,14 @@ const headerGrid
ghost
thin-font
align-self="baseline"
:align-text="'collapsed' in props ? 'left' : undefined"
:aria-pressed="props.collapsed === false || undefined"
:align-text="expand || collapse ? 'left' : undefined"
:aria-pressed="collapse"
:class="{
[$style.action]: true,
[$style.transparent]: 'primary' in props || 'secondary' in props || 'destructive' in props,
[$style.full]: 'collapsed' in props
[$style.full]: expand || collapse
}"
v-bind="{...fallthroughProps, ...action, ['collapsed' in props ? 'full' : 'min-content']: true}"
v-bind="{ ...fallthroughProps, ...action, [expand || collapse ? 'full' : 'min-content']: true }"
>
{{ action?.text }}
</component>
@ -87,18 +129,22 @@ const headerGrid
<Layout
main
:inert="'collapsed' in props && props.collapsed"
:inert="!!expand"
:style="`${
'alignLeft' in props && props.alignLeft
? 'justify-content: start;'
: ''
} ${
'collapsed' in props && props.collapsed
}${
!!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;`
transition: max-height .5s, grid-template-rows .3s, padding .2s;`
"
grid="auto / repeat(auto-fit, 46px)"
>
@ -108,6 +154,12 @@ const headerGrid
</template>
<style module lang="scss">
.summary {
align-self: baseline;
min-width: calc(100% + 32px);
margin: 0 -16px;
--fw-border-radius: 32px;
}
.action {
&.transparent {

View File

@ -99,6 +99,8 @@ const user: User = {
is_superuser: true,
privacy_level: "everyone"
}
const sections = ref<boolean[]>([false, false, false])
</script>
```ts
@ -238,13 +240,13 @@ Note the spacer above the layout. By default, sections begin at the baseline of
<Layout stack gap-64 class="preview" style="margin: 0 -40px; padding: 0 25px;">
<Section :alignLeft="alignLeft" small-items h3="Cards (small items)" :action="{ text:'Documentation on Cards', to:'../card' }">
<Card small title="Relatively Long Album Name">
<Card small default solid raised title="Relatively Long Album Name">
Artist Name
</Card>
<Card small title="Relatively Long Album Name">
<Card small default solid raised title="Relatively Long Album Name">
Artist Name
</Card>
<Card small title="Relatively Long Album Name">
<Card small default solid raised title="Relatively Long Album Name">
Artist Name
</Card>
</Section>
@ -257,6 +259,48 @@ Note the spacer above the layout. By default, sections begin at the baseline of
</Layout>
## Collapse and expand the section
By adding either `collapse` or `expand` to the props, you add Accordion behavior to the section.
The heading will become clickable.
```ts
const sections = ref([false, false, false])
```
```vue-html
<Section
v-for="(section, index) in sections"
:key="`${index}${section}`"
:h3="`Section ${index} (${section})`"
v-bind="
section
? { collapse: () => { console.log('collapse!'); sections[index] = false } }
: { expand: () => { console.log('expand!'); sections[index] = true } }
"
>
Content {{ section }}
</Section>
```
<Section
v-for="(section, index) in sections"
:key="`${index}${section}`"
:h3="`Section ${index}`"
align-left
no-items
v-bind="
section
? { collapse: () => { console.log('collapse!'); sections[index] = false } }
: { expand: () => { console.log('expand!'); sections[index] = true } }
"
>
<Card
title="Content"
full
/>
</Section>
## Responsivity
- Cards and Activities snap to the grid columns. They have intrinsic widths, expressed in the number of columns they span. For `Card`, it is `3` and for `Activity`, it is `4`.