feat: render list view with collapsible bucket sections
This commit is contained in:
parent
46676289e0
commit
f4883ef7c4
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
<template #default>
|
||||
<div
|
||||
:class="{ 'is-loading': loading }"
|
||||
:class="{ 'is-loading': hasBuckets ? bucketStore.isLoading : loading }"
|
||||
class="loader-container is-max-width-desktop list-view"
|
||||
>
|
||||
<Card
|
||||
|
|
@ -30,69 +30,252 @@
|
|||
:has-content="false"
|
||||
class="has-overflow"
|
||||
>
|
||||
<AddTask
|
||||
v-if="!project?.isArchived && canWrite"
|
||||
ref="addTaskRef"
|
||||
class="list-view__add-task d-print-none"
|
||||
:default-position="firstNewPosition"
|
||||
@taskAdded="updateTaskList"
|
||||
/>
|
||||
<!-- Sectioned mode (list view with manual bucket configuration) -->
|
||||
<template v-if="hasBuckets">
|
||||
<AddTask
|
||||
v-if="!project?.isArchived && canWrite"
|
||||
ref="addTaskRef"
|
||||
class="list-view__add-task d-print-none"
|
||||
:default-position="firstNewPosition"
|
||||
@taskAdded="updateTaskList"
|
||||
/>
|
||||
|
||||
<Nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
||||
{{ $t('project.list.empty') }}
|
||||
<ButtonLink
|
||||
v-if="project?.id > 0 && canWrite"
|
||||
@click="focusNewTaskInput()"
|
||||
<div
|
||||
v-for="bucket in buckets"
|
||||
:key="bucket.id"
|
||||
class="bucket-section"
|
||||
>
|
||||
{{ $t('project.list.newTaskCta') }}
|
||||
</ButtonLink>
|
||||
</Nothing>
|
||||
|
||||
<draggable
|
||||
v-if="tasks && tasks.length > 0"
|
||||
v-model="tasks"
|
||||
:group="{name: 'tasks', put: false}"
|
||||
:disabled="!canDragTasks || !isPositionSorting"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: {
|
||||
tasks: true,
|
||||
'dragging-disabled': !canDragTasks || !isPositionSorting
|
||||
},
|
||||
type: 'transition-group'
|
||||
}"
|
||||
:animation="100"
|
||||
:handle="dragHandle"
|
||||
:delay-on-touch-only="!isTouchDevice"
|
||||
:delay="isTouchDevice ? 0 : 1000"
|
||||
ghost-class="task-ghost"
|
||||
@start="handleDragStart"
|
||||
@end="saveTaskPosition"
|
||||
>
|
||||
<template #item="{element: t, index}">
|
||||
<SingleTaskInProject
|
||||
:ref="(el) => setTaskRef(el, index)"
|
||||
:show-list-color="false"
|
||||
:can-mark-as-done="canWrite || isPseudoProject"
|
||||
:the-task="t"
|
||||
:all-tasks="allTasks"
|
||||
@taskUpdated="updateTasks"
|
||||
<div
|
||||
class="bucket-section__header"
|
||||
@click="toggleBucketCollapse(bucket.id)"
|
||||
>
|
||||
<span
|
||||
v-if="canDragTasks && isPositionSorting"
|
||||
class="icon handle"
|
||||
class="icon bucket-section__collapse-icon"
|
||||
:class="{'is-collapsed': collapsedBuckets[bucket.id]}"
|
||||
>
|
||||
<Icon icon="grip-lines" />
|
||||
<Icon icon="chevron-down" />
|
||||
</span>
|
||||
</SingleTaskInProject>
|
||||
</template>
|
||||
</draggable>
|
||||
<h2
|
||||
class="bucket-section__title"
|
||||
:contenteditable="(canWrite && !collapsedBuckets[bucket.id]) ? true : undefined"
|
||||
:spellcheck="false"
|
||||
@keydown.enter.prevent.stop="!$event.isComposing && ($event.target as HTMLElement).blur()"
|
||||
@keydown.esc.prevent.stop="!$event.isComposing && ($event.target as HTMLElement).blur()"
|
||||
@blur="saveBucketTitle(bucket.id, ($event.target as HTMLElement).textContent as string)"
|
||||
@click.stop
|
||||
>
|
||||
{{ bucket.title }}
|
||||
</h2>
|
||||
<span
|
||||
v-if="bucket.limit > 0 || bucket.count > 0"
|
||||
:class="{'is-max': bucket.limit > 0 && bucket.count >= bucket.limit}"
|
||||
class="bucket-section__count"
|
||||
>
|
||||
{{ bucket.limit > 0 ? `${bucket.count}/${bucket.limit}` : bucket.count }}
|
||||
</span>
|
||||
<Dropdown
|
||||
v-if="canWrite && !collapsedBuckets[bucket.id]"
|
||||
class="is-right bucket-section__options"
|
||||
trigger-icon="ellipsis-v"
|
||||
@click.stop
|
||||
>
|
||||
<DropdownItem
|
||||
v-tooltip="$t('project.kanban.defaultBucketHint')"
|
||||
:icon-class="{'has-text-primary': bucket.id === currentView?.defaultBucketId}"
|
||||
icon="th"
|
||||
@click.stop="toggleDefaultBucket(bucket)"
|
||||
>
|
||||
{{ $t('project.kanban.defaultBucket') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
icon="angles-up"
|
||||
@click.stop="toggleBucketCollapse(bucket.id)"
|
||||
>
|
||||
{{ $t('project.list.collapseSection') }}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="buckets.length <= 1 ? $t('project.kanban.deleteLast') : ''"
|
||||
class="has-text-danger"
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
icon-class="has-text-danger"
|
||||
icon="trash-alt"
|
||||
@click.stop="() => deleteBucketModal(bucket.id)"
|
||||
>
|
||||
{{ $t('misc.delete') }}
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
/>
|
||||
<draggable
|
||||
v-if="!collapsedBuckets[bucket.id]"
|
||||
:model-value="bucket.tasks"
|
||||
group="tasks"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: {
|
||||
tasks: true,
|
||||
'dragging-disabled': !canDragTasks || !isPositionSorting,
|
||||
},
|
||||
type: 'transition-group',
|
||||
}"
|
||||
:animation="100"
|
||||
:handle="dragHandle"
|
||||
:delay-on-touch-only="!isTouchDevice"
|
||||
:delay="isTouchDevice ? 0 : 1000"
|
||||
ghost-class="task-ghost"
|
||||
@start="handleDragStart"
|
||||
@end="(e) => saveTaskPositionInBucket(e, bucket)"
|
||||
>
|
||||
<template #item="{element: t}">
|
||||
<SingleTaskInProject
|
||||
:show-list-color="false"
|
||||
:can-mark-as-done="canWrite || isPseudoProject"
|
||||
:the-task="t"
|
||||
:all-tasks="getAllTasksFromBuckets()"
|
||||
@taskUpdated="updateTaskInBuckets"
|
||||
>
|
||||
<span
|
||||
v-if="canDragTasks && isPositionSorting"
|
||||
class="icon handle"
|
||||
>
|
||||
<Icon icon="grip-lines" />
|
||||
</span>
|
||||
</SingleTaskInProject>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- Load more -->
|
||||
<div
|
||||
v-if="!collapsedBuckets[bucket.id] && bucket.tasks.length < bucket.count"
|
||||
class="bucket-section__load-more"
|
||||
>
|
||||
<ButtonLink @click="loadMoreForBucket(bucket.id)">
|
||||
{{ $t('project.list.loadMore', {remaining: bucket.count - bucket.tasks.length}) }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add section button -->
|
||||
<div
|
||||
v-if="canWrite"
|
||||
class="bucket-section__add"
|
||||
>
|
||||
<div
|
||||
v-if="showNewSectionInput"
|
||||
class="field has-addons bucket-section__add-input"
|
||||
>
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
v-model="newSectionTitle"
|
||||
v-focus.always
|
||||
class="input"
|
||||
:placeholder="$t('project.list.addSectionPlaceholder')"
|
||||
type="text"
|
||||
@keyup.enter="createNewSection"
|
||||
@keyup.esc="showNewSectionInput = false"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<XButton
|
||||
:shadow="false"
|
||||
@click="createNewSection"
|
||||
>
|
||||
{{ $t('project.list.add') }}
|
||||
</XButton>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonLink
|
||||
v-else
|
||||
@click="showNewSectionInput = true"
|
||||
>
|
||||
<Icon icon="plus" />
|
||||
{{ $t('project.list.addSection') }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
|
||||
<!-- Delete section confirmation modal -->
|
||||
<Modal
|
||||
v-if="showSectionDeleteModal"
|
||||
@close="showSectionDeleteModal = false"
|
||||
@submit="deleteSection"
|
||||
>
|
||||
<template #header>
|
||||
{{ $t('project.list.deleteSection') }}
|
||||
</template>
|
||||
<template #text>
|
||||
<p>{{ $t('project.list.deleteSectionText1') }}</p>
|
||||
<p>{{ $t('project.list.deleteSectionText2') }}</p>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<!-- Flat mode (existing) -->
|
||||
<template v-else>
|
||||
<AddTask
|
||||
v-if="!project?.isArchived && canWrite"
|
||||
ref="addTaskRef"
|
||||
class="list-view__add-task d-print-none"
|
||||
:default-position="firstNewPosition"
|
||||
@taskAdded="updateTaskList"
|
||||
/>
|
||||
|
||||
<Nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
||||
{{ $t('project.list.empty') }}
|
||||
<ButtonLink
|
||||
v-if="project?.id > 0 && canWrite"
|
||||
@click="focusNewTaskInput()"
|
||||
>
|
||||
{{ $t('project.list.newTaskCta') }}
|
||||
</ButtonLink>
|
||||
</Nothing>
|
||||
|
||||
<draggable
|
||||
v-if="tasks && tasks.length > 0"
|
||||
v-model="tasks"
|
||||
:group="{name: 'tasks', put: false}"
|
||||
:disabled="!canDragTasks || !isPositionSorting"
|
||||
item-key="id"
|
||||
tag="ul"
|
||||
:component-data="{
|
||||
class: {
|
||||
tasks: true,
|
||||
'dragging-disabled': !canDragTasks || !isPositionSorting
|
||||
},
|
||||
type: 'transition-group'
|
||||
}"
|
||||
:animation="100"
|
||||
:handle="dragHandle"
|
||||
:delay-on-touch-only="!isTouchDevice"
|
||||
:delay="isTouchDevice ? 0 : 1000"
|
||||
ghost-class="task-ghost"
|
||||
@start="handleDragStart"
|
||||
@end="saveTaskPosition"
|
||||
>
|
||||
<template #item="{element: t, index}">
|
||||
<SingleTaskInProject
|
||||
:ref="(el) => setTaskRef(el, index)"
|
||||
:show-list-color="false"
|
||||
:can-mark-as-done="canWrite || isPseudoProject"
|
||||
:the-task="t"
|
||||
:all-tasks="allTasks"
|
||||
@taskUpdated="updateTasks"
|
||||
>
|
||||
<span
|
||||
v-if="canDragTasks && isPositionSorting"
|
||||
class="icon handle"
|
||||
>
|
||||
<Icon icon="grip-lines" />
|
||||
</span>
|
||||
</SingleTaskInProject>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<Pagination
|
||||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
/>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -102,6 +285,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, nextTick, onMounted, onBeforeUnmount, watch, toRef} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
|
||||
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
|
||||
|
|
@ -112,22 +296,39 @@ import FilterPopup from '@/components/project/partials/FilterPopup.vue'
|
|||
import Nothing from '@/components/misc/Nothing.vue'
|
||||
import Pagination from '@/components/misc/Pagination.vue'
|
||||
import SortPopup from '@/components/project/partials/SortPopup.vue'
|
||||
import Dropdown from '@/components/misc/Dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/DropdownItem.vue'
|
||||
import Modal from '@/components/misc/Modal.vue'
|
||||
|
||||
import {useTaskList} from '@/composables/useTaskList'
|
||||
import {useTaskDragToProject} from '@/composables/useTaskDragToProject'
|
||||
import {shouldShowTaskInListView} from '@/composables/useTaskListFiltering'
|
||||
import {PERMISSIONS as Permissions} from '@/constants/permissions'
|
||||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {
|
||||
type CollapsedBuckets,
|
||||
getCollapsedBucketState,
|
||||
saveCollapsedBucketState,
|
||||
} from '@/helpers/saveCollapsedBucketState'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IBucket} from '@/modelTypes/IBucket'
|
||||
import {isSavedFilter, useSavedFilter} from '@/services/savedFilter'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useBucketStore} from '@/stores/buckets'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import TaskPositionService from '@/services/taskPosition'
|
||||
import TaskPositionModel from '@/models/taskPosition'
|
||||
import TaskBucketService from '@/services/taskBucket'
|
||||
import TaskBucketModel from '@/models/taskBucket'
|
||||
import BucketModel from '@/models/bucket'
|
||||
import ProjectViewService from '@/services/projectViews'
|
||||
import ProjectViewModel from '@/models/projectView'
|
||||
|
||||
const props = defineProps<{
|
||||
isLoadingProject: boolean,
|
||||
|
|
@ -139,6 +340,8 @@ const projectId = toRef(props, 'projectId')
|
|||
|
||||
defineOptions({name: 'List'})
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const ctaVisible = ref(false)
|
||||
|
||||
const drag = ref(false)
|
||||
|
|
@ -177,6 +380,14 @@ watch(
|
|||
const isPositionSorting = computed(() => 'position' in sortByParam.value)
|
||||
|
||||
const firstNewPosition = computed(() => {
|
||||
if (hasBuckets.value) {
|
||||
const defaultBucket = buckets.value.find(b => b.id === currentView.value?.defaultBucketId) || buckets.value[0]
|
||||
if (defaultBucket?.tasks?.length > 0) {
|
||||
return calculateItemPosition(null, defaultBucket.tasks[0].position)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (tasks.value.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
|
@ -186,6 +397,8 @@ const firstNewPosition = computed(() => {
|
|||
|
||||
const baseStore = useBaseStore()
|
||||
const taskStore = useTaskStore()
|
||||
const bucketStore = useBucketStore()
|
||||
const projectStore = useProjectStore()
|
||||
const {handleTaskDropToProject} = useTaskDragToProject()
|
||||
const project = computed(() => baseStore.currentProject)
|
||||
|
||||
|
|
@ -214,7 +427,184 @@ function focusNewTaskInput() {
|
|||
addTaskRef.value?.focusTaskInput()
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Bucket/Section mode
|
||||
// ==========================================
|
||||
|
||||
const currentView = computed(() => {
|
||||
return project.value?.views?.find(v => v.id === props.viewId) as IProjectView || null
|
||||
})
|
||||
|
||||
const hasBuckets = computed(() => {
|
||||
return currentView.value?.bucketConfigurationMode !== 'none'
|
||||
&& currentView.value?.bucketConfigurationMode !== undefined
|
||||
})
|
||||
|
||||
const buckets = computed(() => bucketStore.buckets)
|
||||
|
||||
const collapsedBuckets = ref<CollapsedBuckets>({})
|
||||
|
||||
watch(
|
||||
[() => props.projectId, () => props.viewId, hasBuckets],
|
||||
async ([pId, vId, bucketed]) => {
|
||||
if (!bucketed) return
|
||||
await bucketStore.loadBucketsForProject(pId, vId, params.value)
|
||||
collapsedBuckets.value = getCollapsedBucketState(pId)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function toggleBucketCollapse(bucketId: IBucket['id']) {
|
||||
collapsedBuckets.value = {
|
||||
...collapsedBuckets.value,
|
||||
[bucketId]: !collapsedBuckets.value[bucketId],
|
||||
}
|
||||
saveCollapsedBucketState(props.projectId, collapsedBuckets.value)
|
||||
}
|
||||
|
||||
function getAllTasksFromBuckets(): ITask[] {
|
||||
return buckets.value.flatMap(b => b.tasks)
|
||||
}
|
||||
|
||||
function updateTaskInBuckets(updatedTask: ITask) {
|
||||
bucketStore.setTaskInBucket(updatedTask)
|
||||
}
|
||||
|
||||
async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
||||
const bucket = bucketStore.getBucketById(bucketId)
|
||||
if (bucket?.title === bucketTitle) {
|
||||
return
|
||||
}
|
||||
|
||||
await bucketStore.updateBucket({
|
||||
id: bucketId,
|
||||
title: bucketTitle,
|
||||
projectId: projectId.value,
|
||||
})
|
||||
success({message: t('project.list.sectionTitleSavedSuccess')})
|
||||
}
|
||||
|
||||
const sectionToDelete = ref<IBucket['id']>(0)
|
||||
const showSectionDeleteModal = ref(false)
|
||||
|
||||
function deleteBucketModal(bucketId: IBucket['id']) {
|
||||
if (buckets.value.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
sectionToDelete.value = bucketId
|
||||
showSectionDeleteModal.value = true
|
||||
}
|
||||
|
||||
async function deleteSection() {
|
||||
try {
|
||||
await bucketStore.deleteBucket({
|
||||
bucket: new BucketModel({
|
||||
id: sectionToDelete.value,
|
||||
projectId: projectId.value,
|
||||
projectViewId: props.viewId,
|
||||
}),
|
||||
params: params.value,
|
||||
})
|
||||
success({message: t('project.list.deleteSectionSuccess')})
|
||||
} finally {
|
||||
showSectionDeleteModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const newSectionTitle = ref('')
|
||||
const showNewSectionInput = ref(false)
|
||||
|
||||
async function createNewSection() {
|
||||
if (newSectionTitle.value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
await bucketStore.createBucket(new BucketModel({
|
||||
title: newSectionTitle.value,
|
||||
projectId: projectId.value,
|
||||
projectViewId: props.viewId,
|
||||
}))
|
||||
newSectionTitle.value = ''
|
||||
showNewSectionInput.value = false
|
||||
}
|
||||
|
||||
async function toggleDefaultBucket(bucket: IBucket) {
|
||||
const defaultBucketId = currentView.value?.defaultBucketId === bucket.id
|
||||
? 0
|
||||
: bucket.id
|
||||
|
||||
const projectViewService = new ProjectViewService()
|
||||
const updatedView = await projectViewService.update(new ProjectViewModel({
|
||||
...currentView.value,
|
||||
defaultBucketId,
|
||||
}))
|
||||
|
||||
const views = project.value.views.map(v => v.id === currentView.value?.id ? updatedView : v)
|
||||
const updatedProject = {
|
||||
...project.value,
|
||||
views,
|
||||
}
|
||||
|
||||
projectStore.setProject(updatedProject)
|
||||
|
||||
success({message: t('project.kanban.defaultBucketSavedSuccess')})
|
||||
}
|
||||
|
||||
async function loadMoreForBucket(bucketId: IBucket['id']) {
|
||||
await bucketStore.loadNextTasksForBucket(
|
||||
props.projectId,
|
||||
props.viewId,
|
||||
params.value,
|
||||
bucketId,
|
||||
)
|
||||
}
|
||||
|
||||
async function saveTaskPositionInBucket(
|
||||
e: {originalEvent?: MouseEvent, to: HTMLElement, from: HTMLElement, newIndex: number, oldIndex: number},
|
||||
targetBucket: IBucket,
|
||||
) {
|
||||
drag.value = false
|
||||
|
||||
const task = targetBucket.tasks[e.newIndex]
|
||||
if (!task) return
|
||||
|
||||
const taskBefore = targetBucket.tasks[e.newIndex - 1] ?? null
|
||||
const taskAfter = targetBucket.tasks[e.newIndex + 1] ?? null
|
||||
|
||||
const position = calculateItemPosition(
|
||||
taskBefore?.position ?? null,
|
||||
taskAfter?.position ?? null,
|
||||
)
|
||||
|
||||
await taskPositionService.value.update(new TaskPositionModel({
|
||||
position,
|
||||
projectViewId: props.viewId,
|
||||
taskId: task.id,
|
||||
}))
|
||||
|
||||
// If bucket changed, update bucket assignment
|
||||
if (e.to !== e.from) {
|
||||
const taskBucketService = new TaskBucketService()
|
||||
await taskBucketService.update(new TaskBucketModel({
|
||||
taskId: task.id,
|
||||
bucketId: targetBucket.id,
|
||||
projectViewId: props.viewId,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Flat mode (existing logic)
|
||||
// ==========================================
|
||||
|
||||
function updateTaskList(task: ITask) {
|
||||
if (hasBuckets.value) {
|
||||
bucketStore.addTaskToBucket(task)
|
||||
baseStore.setHasTasks(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isPositionSorting.value) {
|
||||
// reload tasks with current filter and sorting
|
||||
loadTasks()
|
||||
|
|
@ -246,7 +636,8 @@ function updateTasks(updatedTask: ITask) {
|
|||
function handleDragStart(e: { item: HTMLElement }) {
|
||||
drag.value = true
|
||||
const taskId = parseInt(e.item.dataset.taskId ?? '', 10)
|
||||
const task = tasks.value.find(t => t.id === taskId)
|
||||
const allAvailableTasks = hasBuckets.value ? getAllTasksFromBuckets() : tasks.value
|
||||
const task = allAvailableTasks.find(t => t.id === taskId)
|
||||
|
||||
if (task) {
|
||||
taskStore.setDraggedTask(task)
|
||||
|
|
@ -421,4 +812,84 @@ onBeforeUnmount(() => {
|
|||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Bucket section styles
|
||||
.bucket-section {
|
||||
&:not(:first-child) {
|
||||
border-block-start: 1px solid var(--grey-200);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: .75rem 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--grey-50);
|
||||
border-block-end: 1px solid var(--grey-100);
|
||||
|
||||
&:hover {
|
||||
background: var(--grey-100);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__collapse-icon {
|
||||
transition: transform 150ms ease;
|
||||
color: var(--grey-500);
|
||||
|
||||
&.is-collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__title {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: .125rem .25rem;
|
||||
border-radius: $radius;
|
||||
min-inline-size: 0;
|
||||
|
||||
&[contenteditable='true'] {
|
||||
cursor: text;
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__count {
|
||||
font-size: .85rem;
|
||||
color: var(--grey-500);
|
||||
font-weight: 500;
|
||||
|
||||
&.is-max {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
.bucket-section__options {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.bucket-section__load-more {
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
color: var(--grey-500);
|
||||
}
|
||||
|
||||
.bucket-section__add {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bucket-section__add-input {
|
||||
max-inline-size: 400px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue