Skip to content

Commit 3c2cc75

Browse files
kirushkinxIMB11
andauthored
feat: add collapsible library groups in app (#5739)
* feat: add collapsible library groups in app * feat: use accordion rather than custom --------- Co-authored-by: Calum H. <[email protected]> Co-authored-by: Calum H. (IMB11) <[email protected]>
1 parent 7b5c746 commit 3c2cc75

4 files changed

Lines changed: 88 additions & 75 deletions

File tree

apps/app-frontend/src/components/GridDisplay.vue

Lines changed: 31 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
TrashIcon,
1111
} from '@modrinth/assets'
1212
import {
13+
Accordion,
1314
DropdownSelect,
1415
formatLoader,
1516
injectNotificationManager,
@@ -133,12 +134,33 @@ const state = useStorage(
133134
{
134135
group: 'Group',
135136
sortBy: 'Name',
137+
collapsedGroups: [],
136138
},
137139
localStorage,
138140
{ mergeDefaults: true },
139141
)
140142
141143
const search = ref('')
144+
const collapsedSectionKeys = computed(() => new Set(state.value.collapsedGroups ?? []))
145+
146+
const getSectionKey = (sectionName) => `${state.value.group}:${sectionName}`
147+
148+
const isSectionCollapsed = (sectionName) => {
149+
return collapsedSectionKeys.value.has(getSectionKey(sectionName))
150+
}
151+
152+
const setSectionCollapsed = (sectionName, collapsed) => {
153+
const sectionKey = getSectionKey(sectionName)
154+
const collapsedSections = new Set(state.value.collapsedGroups ?? [])
155+
156+
if (collapsed) {
157+
collapsedSections.add(sectionKey)
158+
} else {
159+
collapsedSections.delete(sectionKey)
160+
}
161+
162+
state.value.collapsedGroups = [...collapsedSections]
163+
}
142164
143165
const filteredResults = computed(() => {
144166
const { group = 'Group', sortBy = 'Name' } = state.value
@@ -280,18 +302,21 @@ const filteredResults = computed(() => {
280302
<span class="font-semibold text-secondary">{{ selected }}</span>
281303
</DropdownSelect>
282304
</div>
283-
<div
305+
<Accordion
284306
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
285307
key,
286308
value,
287309
}))"
288310
:key="instanceSection.key"
311+
:divider="instanceSection.key !== 'None'"
312+
:open-by-default="!isSectionCollapsed(instanceSection.key)"
289313
class="row"
314+
@on-open="setSectionCollapsed(instanceSection.key, false)"
315+
@on-close="setSectionCollapsed(instanceSection.key, true)"
290316
>
291-
<div v-if="instanceSection.key !== 'None'" class="divider">
292-
<p>{{ instanceSection.key }}</p>
293-
<hr aria-hidden="true" />
294-
</div>
317+
<template v-if="instanceSection.key !== 'None'" #title>
318+
<span class="text-base">{{ instanceSection.key }}</span>
319+
</template>
295320
<section class="instances">
296321
<Instance
297322
v-for="instance in instanceSection.value"
@@ -301,7 +326,7 @@ const filteredResults = computed(() => {
301326
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
302327
/>
303328
</section>
304-
</div>
329+
</Accordion>
305330
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteProfile" />
306331
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
307332
<template #play> <PlayIcon /> Play </template>
@@ -316,73 +341,7 @@ const filteredResults = computed(() => {
316341
</template>
317342
<style lang="scss" scoped>
318343
.row {
319-
display: flex;
320-
flex-direction: column;
321-
align-items: flex-start;
322344
width: 100%;
323-
324-
.divider {
325-
display: flex;
326-
justify-content: space-between;
327-
align-items: center;
328-
width: 100%;
329-
gap: 1rem;
330-
margin-bottom: 1rem;
331-
332-
p {
333-
margin: 0;
334-
font-size: 1rem;
335-
white-space: nowrap;
336-
color: var(--color-contrast);
337-
}
338-
339-
hr {
340-
background-color: var(--color-gray);
341-
height: 1px;
342-
width: 100%;
343-
border: none;
344-
}
345-
}
346-
}
347-
348-
.header {
349-
display: flex;
350-
flex-direction: row;
351-
flex-wrap: wrap;
352-
justify-content: space-between;
353-
gap: 1rem;
354-
align-items: inherit;
355-
margin: 1rem 1rem 0 !important;
356-
padding: 1rem;
357-
width: calc(100% - 2rem);
358-
359-
.iconified-input {
360-
flex-grow: 1;
361-
362-
input {
363-
min-width: 100%;
364-
}
365-
}
366-
367-
.sort-dropdown {
368-
width: 10rem;
369-
}
370-
371-
.filter-dropdown {
372-
width: 15rem;
373-
}
374-
375-
.group-dropdown {
376-
width: 10rem;
377-
}
378-
379-
.labeled_button {
380-
display: flex;
381-
flex-direction: row;
382-
align-items: center;
383-
gap: 0.5rem;
384-
white-space: nowrap;
385-
}
386345
}
387346
388347
.instances {

packages/assets/generated-icons.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
import type { FunctionalComponent, SVGAttributes } from 'vue'
55

6-
export type IconComponent = FunctionalComponent<SVGAttributes>
7-
86
import _AffiliateIcon from './icons/affiliate.svg?component'
97
import _AlignLeftIcon from './icons/align-left.svg?component'
108
import _ArchiveIcon from './icons/archive.svg?component'
@@ -394,6 +392,8 @@ import _XCircleIcon from './icons/x-circle.svg?component'
394392
import _ZoomInIcon from './icons/zoom-in.svg?component'
395393
import _ZoomOutIcon from './icons/zoom-out.svg?component'
396394

395+
export type IconComponent = FunctionalComponent<SVGAttributes>
396+
397397
export const AffiliateIcon = _AffiliateIcon
398398
export const AlignLeftIcon = _AlignLeftIcon
399399
export const ArchiveIcon = _ArchiveIcon

packages/ui/src/components/base/Accordion.vue

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
11
<template>
22
<div v-bind="$attrs">
3+
<div v-if="divider && !!slots.title" class="flex items-center gap-4 mb-4">
4+
<button
5+
:class="
6+
buttonClass ??
7+
'group flex items-center gap-1 bg-transparent m-0 p-0 border-none cursor-pointer'
8+
"
9+
@click="() => (forceOpen ? undefined : toggledOpen ? close() : open())"
10+
>
11+
<slot name="button" :open="isOpen">
12+
<div
13+
class="flex items-center gap-1 whitespace-nowrap transition-colors text-primary group-hover:text-contrast"
14+
>
15+
<slot name="title" :open="isOpen" />
16+
<DropdownIcon
17+
v-if="!forceOpen"
18+
class="size-5 transition-transform duration-300 shrink-0 text-secondary group-hover:text-primary"
19+
:class="{ 'rotate-180': isOpen }"
20+
/>
21+
</div>
22+
</slot>
23+
</button>
24+
<hr class="h-px w-full border-none bg-divider" aria-hidden="true" />
25+
</div>
326
<button
4-
v-if="!!slots.title"
27+
v-else-if="!!slots.title"
528
:class="buttonClass ?? 'flex flex-col gap-2 bg-transparent m-0 p-0 border-none'"
629
@click="() => (forceOpen ? undefined : toggledOpen ? close() : open())"
730
>
@@ -44,6 +67,7 @@ const props = withDefaults(
4467
titleWrapperClass?: string
4568
forceOpen?: boolean
4669
overflowVisible?: boolean
70+
divider?: boolean
4771
}>(),
4872
{
4973
type: 'standard',
@@ -53,6 +77,7 @@ const props = withDefaults(
5377
titleWrapperClass: null,
5478
forceOpen: false,
5579
overflowVisible: false,
80+
divider: false,
5681
},
5782
)
5883

packages/ui/src/stories/base/Accordion.stories.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,35 @@ export const NestedContent: StoryObj = {
156156
}),
157157
}
158158

159+
export const Divider: StoryObj = {
160+
render: () => ({
161+
components: { Accordion },
162+
template: /*html*/ `
163+
<div class="flex flex-col gap-4">
164+
<Accordion divider open-by-default>
165+
<template #title>
166+
<span class="text-base font-semibold text-contrast">Category A</span>
167+
</template>
168+
<div class="mt-2 flex flex-wrap gap-2">
169+
<span class="px-3 py-2 bg-bg-raised rounded text-sm">Item 1</span>
170+
<span class="px-3 py-2 bg-bg-raised rounded text-sm">Item 2</span>
171+
<span class="px-3 py-2 bg-bg-raised rounded text-sm">Item 3</span>
172+
</div>
173+
</Accordion>
174+
<Accordion divider>
175+
<template #title>
176+
<span class="text-base font-semibold text-contrast">Category B</span>
177+
</template>
178+
<div class="mt-2 flex flex-wrap gap-2">
179+
<span class="px-3 py-2 bg-bg-raised rounded text-sm">Item 4</span>
180+
<span class="px-3 py-2 bg-bg-raised rounded text-sm">Item 5</span>
181+
</div>
182+
</Accordion>
183+
</div>
184+
`,
185+
}),
186+
}
187+
159188
export const AllStates: StoryObj = {
160189
render: () => ({
161190
components: { Accordion },

0 commit comments

Comments
 (0)