Compare commits

...
3 Commits
Author SHA1 Message Date
vadim d5642909e0 dynamic width
Gitea Actions Demo / Deploy (push) Successful in 42s
2026-10-05 19:46:27 +03:00
vadim 37fa2b5966 fix big content 2026-10-05 19:36:46 +03:00
vadim 51debefee5 wip 2026-10-05 10:52:49 +03:00
15 changed files with 318 additions and 81 deletions

No files matched your search

+1 -1
View File
@@ -53,7 +53,7 @@ onMounted(async () => {
<template> <template>
<component :is="layout"> <component :is="layout">
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<div :key="route.name"> <div :key="route.name" class="h-100">
<component :is="Component" /> <component :is="Component" />
</div> </div>
</router-view> </router-view>
+6 -7
View File
@@ -160,7 +160,7 @@ function onEnter(e: KeyboardEvent) {
/> />
</div> </div>
<div class="chat__bubble"> <v-sheet class="chat__bubble px-4 py-2">
<div v-if="item.showHeader && !item.own" class="font-weight-medium text-body-small text-primary"> <div v-if="item.showHeader && !item.own" class="font-weight-medium text-body-small text-primary">
{{ authorName(item.comment) }} {{ authorName(item.comment) }}
</div> </div>
@@ -171,13 +171,13 @@ function onEnter(e: KeyboardEvent) {
<div class="chat__meta text-medium-emphasis"> <div class="chat__meta text-medium-emphasis">
<span v-if="item.comment.editedAt && !item.comment.deletedAt">edited · </span>{{ item.time }} <span v-if="item.comment.editedAt && !item.comment.deletedAt">edited · </span>{{ item.time }}
</div> </div>
</div> </v-sheet>
</div> </div>
</template> </template>
</div> </div>
<v-fade-transition> <v-fade-transition>
<v-btn v-if="!atBottom" class="chat__jump" icon size="small" elevation="2" @click="scrollToBottom('smooth')"> <v-btn v-if="!atBottom" density="compact" class="chat__jump" icon @click="scrollToBottom('smooth')">
<v-badge :model-value="unread > 0" :content="unread" color="primary" floating> <v-badge :model-value="unread > 0" :content="unread" color="primary" floating>
<v-icon icon="mdi-chevron-down" /> <v-icon icon="mdi-chevron-down" />
</v-badge> </v-badge>
@@ -220,7 +220,8 @@ function onEnter(e: KeyboardEvent) {
.chat { .chat {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: min(30vh, 560px); height: 100%;
min-height: 20rem;
&__viewport { &__viewport {
position: relative; position: relative;
@@ -271,12 +272,10 @@ function onEnter(e: KeyboardEvent) {
&__bubble { &__bubble {
max-width: min(75%, 640px); max-width: min(75%, 640px);
min-width: 0; min-width: 0;
padding: 6px 10px;
border-radius: 14px;
background: rgba(var(--v-theme-on-surface), 0.06); background: rgba(var(--v-theme-on-surface), 0.06);
.chat__row--own & { .chat__row--own & {
background: rgba(var(--v-theme-primary), 0.14); background: rgba(var(--v-theme-primary), 0.35);
} }
} }
+10 -2
View File
@@ -46,10 +46,18 @@ const open = () => {
<v-btn density="compact" size="small" :icon="icon" @click="open" /> <v-btn density="compact" size="small" :icon="icon" @click="open" />
<div @click="open" class="font-weight-bold text-capitalize text-title-medium cursor-pointer">{{ title }}</div> <div @click="open" class="font-weight-bold text-capitalize text-title-medium cursor-pointer">{{ title }}</div>
</div> </div>
<div v-if="isOpen" class="ml-6 mt-2"> <div v-if="isOpen" class="panel__body ml-6 mt-2">
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
// Lets the body fill the panel when the panel itself is stretched by its parent
.panel__body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
</style>
+11 -7
View File
@@ -33,17 +33,20 @@ const cancel = () => {
<v-hover #default="{ isHovering, props }"> <v-hover #default="{ isHovering, props }">
<v-sheet <v-sheet
v-bind="props" v-bind="props"
:class="isHovering ? 'px-4' : 'px-0'" :class="isHovering ? 'px-0' : 'px-0'"
class="py-1 cursor-pointer content-sheet" class="cursor-pointer d-flex ga-1"
@click="startEdit" @click="startEdit"
> >
{{ text ?? 'click to add content' }} <div class="content-sheet">
{{ text ?? 'click to add content' }}
</div>
<v-icon v-if="isHovering" icon="mdi-pencil" size="small" />
</v-sheet> </v-sheet>
</v-hover> </v-hover>
</template> </template>
<template v-else> <template v-else>
<v-textarea autofocus class="mb-1" hide-details v-model="draft" variant="outlined" auto-grow rows="4" /> <v-textarea autofocus class="mb-1" hide-details v-model="draft" variant="outlined" auto-grow rows="4" max-rows="15" />
<div class="d-flex justify-end ga-1"> <div class="d-flex justify-end ga-1">
<v-btn density="compact" prepend-icon="mdi-cancel" variant="text" text="Cancel" @click="cancel" /> <v-btn density="compact" prepend-icon="mdi-cancel" variant="text" text="Cancel" @click="cancel" />
@@ -59,9 +62,10 @@ const cancel = () => {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.content-sheet:hover { //.content-sheet:hover {
background-color: rgba(var(--v-theme-on-surface), 0.06); //background-color: rgba(var(--v-theme-on-surface), 0.06);
} //}
.v-sheet { .v-sheet {
transition: padding 0.2s ease-in-out; transition: padding 0.2s ease-in-out;
} }
-1
View File
@@ -49,7 +49,6 @@ const cancel = () => {
<v-date-input <v-date-input
input-format="dd-mm-yyyy" input-format="dd-mm-yyyy"
autofocus autofocus
class="mb-1"
hide-details hide-details
prepend-icon="" prepend-icon=""
v-model="draft" v-model="draft"
+97 -44
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watchEffect } from 'vue' import { computed, ref, useTemplateRef, watchEffect } from 'vue'
import { useIssueStore } from '@/stores/issue.js' import { useIssueStore } from '@/stores/issue.js'
import ExpansionPanel from '@/components/ExpansionPanel.vue' import ExpansionPanel from '@/components/ExpansionPanel.vue'
import IssueContent from '@/components/IssueContent.vue' import IssueContent from '@/components/IssueContent.vue'
@@ -11,6 +11,8 @@ import IssueTitle from '@/components/IssueTitle.vue'
import IssueDate from '@/components/IssueDate.vue' import IssueDate from '@/components/IssueDate.vue'
import CommentsList from '@/components/CommentsList.vue' import CommentsList from '@/components/CommentsList.vue'
import HistoryList from '@/components/HistoryList.vue' import HistoryList from '@/components/HistoryList.vue'
import SplitHandle from '@/components/SplitHandle.vue'
import { useSplitPane } from '@/composables/useSplitPane.ts'
interface Props { interface Props {
ckey: string ckey: string
@@ -29,11 +31,12 @@ const activity = ref()
const { ckey } = defineProps<Props>() const { ckey } = defineProps<Props>()
const splitContainer = useTemplateRef<HTMLElement>('splitContainer')
const { gridTemplate, handleProps } = useSplitPane(splitContainer, { storageKey: 'issue-details' })
const createdAtFormatted = computed(() => { const createdAtFormatted = computed(() => {
if (!issue.value?.createdAt) return '' if (!issue.value?.createdAt) return ''
return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'medium', timeStyle: 'short' }).format( return new Date(issue.value.createdAt).toLocaleString('ru-RU')
new Date(issue.value.createdAt),
)
}) })
async function fetch() { async function fetch() {
@@ -101,62 +104,112 @@ defineExpose({
</script> </script>
<template> <template>
<div v-if="issue" class="d-flex flex-column ga-4 pa-4"> <div v-if="issue" ref="splitContainer" class="pa-4 details">
<div>{{ ckey }}</div> <div class="d-flex flex-column ga-4 details__main">
<div>{{ ckey }}</div>
<issue-title :text="issue.title" @changed="onTitleChanged" /> <issue-title :text="issue.title" @changed="onTitleChanged" />
<v-card elevation="0" border> <v-card elevation="0" border>
<v-card-text class="u-grid"> <v-card-text class="u-grid">
<div class="text-medium-emphasis">Status:</div> <div class="text-medium-emphasis">Status:</div>
<status-menu :value="issue.status" @changed="onStatusChanged" /> <status-menu :value="issue.status" @changed="onStatusChanged" />
<div class="text-medium-emphasis">Reporter:</div> <div class="text-medium-emphasis">Reporter:</div>
<reporter-info :user="issue.reporter" /> <reporter-info :user="issue.reporter" />
<div class="text-medium-emphasis">Assignee:</div> <div class="text-medium-emphasis">Assignee:</div>
<assignee-menu :value="issue.assignee" :compact="false" @changed="onAssigneeChanged" /> <assignee-menu :value="issue.assignee" :compact="false" @changed="onAssigneeChanged" />
<div class="text-medium-emphasis">Created At:</div> <div class="text-medium-emphasis">Created At:</div>
<div>{{ createdAtFormatted }}</div> <div>{{ createdAtFormatted }}</div>
<div class="text-medium-emphasis">Due Date:</div> <div class="text-medium-emphasis">Due Date:</div>
<issue-date :date="issue.dueDate" @changed="onDueDateChanged" /> <issue-date :date="issue.dueDate" @changed="onDueDateChanged" />
</v-card-text> </v-card-text>
</v-card> </v-card>
<expansion-panel title="Details"> <expansion-panel title="Details" class="details__content">
<issue-content :text="issue.content" @changed="onContentChanged" /> <issue-content :text="issue.content" @changed="onContentChanged" />
</expansion-panel> </expansion-panel>
</div>
<split-handle v-bind="handleProps" />
<div class="d-flex flex-column">
<expansion-panel title="Activity" class="activity">
<v-sheet class="activity__sheet">
<v-btn-toggle class="align-self-start" border divided v-model="activity" mandatory="force" density="compact">
<v-btn rounded="0" prepend-icon="mdi-chat" value="comment">Comments</v-btn>
<v-btn rounded="0" prepend-icon="mdi-history" value="history">History</v-btn>
</v-btn-toggle>
<expansion-panel title="Activity"> <v-tabs-window v-model="activity" class="activity__window">
<v-sheet> <v-tabs-window-item value="comment" class="h-100">
<v-tabs v-model="activity" density="compact"> <comments-list :issue-id="issue.id" />
<v-tab prepend-icon="mdi-star" value="comment">Comments</v-tab> </v-tabs-window-item>
<v-tab prepend-icon="mdi-star" value="history">History</v-tab> <v-tabs-window-item value="history">
</v-tabs> <history-list />
</v-tabs-window-item>
<v-divider /> </v-tabs-window>
</v-sheet>
<v-tabs-window v-model="activity"> </expansion-panel>
<v-tabs-window-item value="comment"> </div>
<comments-list :issue-id="issue.id" />
</v-tabs-window-item>
<v-tabs-window-item value="history">
<history-list />
</v-tabs-window-item>
</v-tabs-window>
</v-sheet>
</expansion-panel>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.details {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: v-bind(gridTemplate);
column-gap: 0.5rem;
}
.details > * {
min-height: 0;
overflow-y: auto;
}
// Children keep their content height; only the Details panel shrinks to the space left and scrolls
.details__main > * {
flex-shrink: 0;
}
.details__content {
flex-shrink: 1;
min-height: 0;
:deep(.panel__body) {
overflow-y: auto;
}
}
.activity {
flex: 1;
min-height: 0;
&__sheet {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
&__window {
flex: 1;
min-height: 0;
:deep(.v-window__container) {
height: 100%;
}
}
}
.u-grid { .u-grid {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
align-items: center; align-items: center;
column-gap: 1rem; column-gap: 1rem;
row-gap: 0.75rem; row-gap: 1.5rem;
} }
</style> </style>
+9 -8
View File
@@ -33,13 +33,14 @@ const cancel = () => {
<v-hover #default="{ isHovering, props }"> <v-hover #default="{ isHovering, props }">
<v-sheet <v-sheet
v-bind="props" v-bind="props"
:class="isHovering ? 'px-3' : 'px-0'" :class="isHovering ? 'px-0' : 'px-0'"
class="py-1 cursor-pointer title-sheet" class="cursor-pointer d-flex align-center ga-1"
@click="startEdit" @click="startEdit"
> >
<div class="text-uppercase text-title-large"> <div class="text-uppercase text-title-large">
{{ text ?? 'click to write text' }} {{ text ?? 'click to write text' }}
</div> </div>
<v-icon v-if="isHovering" icon="mdi-pencil" size="small" />
</v-sheet> </v-sheet>
</v-hover> </v-hover>
</template> </template>
@@ -56,11 +57,11 @@ const cancel = () => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.title-sheet:hover { //.title-sheet:hover {
background-color: rgba(var(--v-theme-on-surface), 0.06); // background-color: rgba(var(--v-theme-on-surface), 0.06);
} //}
.v-sheet { //.v-sheet {
transition: padding 0.2s ease-in-out; // transition: padding 0.2s ease-in-out;
} //}
</style> </style>
+50
View File
@@ -0,0 +1,50 @@
<script setup lang="ts">
interface Props {
dragging?: boolean
}
const { dragging = false } = defineProps<Props>()
</script>
<template>
<div
class="split-handle"
:class="{ 'split-handle--active': dragging }"
role="separator"
aria-orientation="vertical"
tabindex="0"
title="Drag to resize, double-click to reset"
/>
</template>
<style scoped lang="scss">
.split-handle {
position: relative;
width: 9px;
cursor: col-resize;
touch-action: none;
outline: none;
// Thin visible line in the middle of a wider grab area
&::after {
content: '';
position: absolute;
inset: 0 4px;
background: rgba(var(--v-border-color), var(--v-border-opacity));
transition: background-color 0.15s;
}
&:hover::after,
&:focus-visible::after,
&--active::after {
inset: 0 3px;
background: rgb(var(--v-theme-primary));
}
}
:global(body.split-pane--dragging),
:global(body.split-pane--dragging *) {
cursor: col-resize !important;
user-select: none;
}
</style>
+88
View File
@@ -0,0 +1,88 @@
import { computed, onScopeDispose, ref, type Ref } from 'vue'
import { useLocalStorage } from '@vueuse/core'
interface Options {
/** localStorage key for the ratio; the ratio is not persisted without it */
storageKey?: string
/** Share of the container taken by the first pane, 0..1 */
initial?: number
min?: number
max?: number
/** Ratio change per arrow key press */
step?: number
}
/**
* Two panes in a CSS grid split by a draggable handle.
* Bind `gridTemplate` to `grid-template-columns` of the container and `handleProps` to the handle between the panes.
*/
export function useSplitPane(container: Ref<HTMLElement | null | undefined>, options: Options = {}) {
const { storageKey, initial = 0.5, min = 0.2, max = 0.8, step = 0.02 } = options
const clamp = (value: number) => Math.min(max, Math.max(min, value))
const ratio = storageKey ? useLocalStorage(`split-pane:${storageKey}`, initial) : ref(initial)
const dragging = ref(false)
// fr units share the space left after the auto-sized handle, so the ratio is exact
const gridTemplate = computed(() => {
const r = clamp(ratio.value)
return `minmax(0, ${r}fr) auto minmax(0, ${1 - r}fr)`
})
function onPointerMove(e: PointerEvent) {
const el = container.value
if (!el) return
// Measure against the content box, so the container padding does not shift the handle off the pointer
const rect = el.getBoundingClientRect()
const style = getComputedStyle(el)
const left = rect.left + parseFloat(style.paddingLeft)
const width = rect.width - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight)
if (width <= 0) return
ratio.value = clamp((e.clientX - left) / width)
}
function stopDrag() {
dragging.value = false
document.body.classList.remove('split-pane--dragging')
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', stopDrag)
window.removeEventListener('pointercancel', stopDrag)
}
function onPointerdown(e: PointerEvent) {
if (e.button !== 0) return
e.preventDefault()
dragging.value = true
// Disables text selection and keeps the resize cursor while the pointer is outside the handle
document.body.classList.add('split-pane--dragging')
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', stopDrag)
window.addEventListener('pointercancel', stopDrag)
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'ArrowLeft') ratio.value = clamp(ratio.value - step)
else if (e.key === 'ArrowRight') ratio.value = clamp(ratio.value + step)
else return
e.preventDefault()
}
function reset() {
ratio.value = initial
}
onScopeDispose(stopDrag)
const handleProps = computed(() => ({
dragging: dragging.value,
'aria-valuenow': Math.round(clamp(ratio.value) * 100),
'aria-valuemin': Math.round(min * 100),
'aria-valuemax': Math.round(max * 100),
onPointerdown,
onKeydown,
onDblclick: reset,
}))
return { ratio, gridTemplate, dragging, handleProps, reset }
}
+21 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { authClient } from '@/lib/auth-client.ts' import { authClient } from '@/lib/auth-client.ts'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import ProjectMenu from '@/components/ProjectMenu.vue' import ProjectMenu from '@/components/ProjectMenu.vue'
@@ -9,6 +9,7 @@ import { useThemeMode } from '@/composables/useThemeMode.ts'
import { THEME_ICONS } from '@/lib/theme.ts' import { THEME_ICONS } from '@/lib/theme.ts'
const router = useRouter() const router = useRouter()
const route = useRoute()
const session = authClient.useSession() const session = authClient.useSession()
const { mode: themeMode, cycleThemeMode } = useThemeMode() const { mode: themeMode, cycleThemeMode } = useThemeMode()
@@ -59,6 +60,10 @@ async function goToSignOut() {
const goToSettings = () => { const goToSettings = () => {
router.push({ name: 'settings' }) router.push({ name: 'settings' })
} }
const fluid = computed(() => {
return route.meta.fluid !== undefined ? !!route.meta.fluid : true
})
</script> </script>
<template> <template>
@@ -141,12 +146,14 @@ const goToSettings = () => {
</div> </div>
</v-app-bar> </v-app-bar>
<v-main> <v-main>
<v-container :fluid="false"> <v-container :fluid="fluid" class="layout-container">
<div class="nav-row d-flex align-center ga-2 mb-2"> <div class="nav-row d-flex align-center ga-2 mb-2">
<back-button /> <back-button />
<app-breadcrumbs /> <app-breadcrumbs />
</div> </div>
<slot></slot> <div class="layout-content">
<slot></slot>
</div>
</v-container> </v-container>
</v-main> </v-main>
</v-layout> </v-layout>
@@ -156,4 +163,15 @@ const goToSettings = () => {
.nav-row:empty { .nav-row:empty {
display: none; display: none;
} }
.layout-container {
height: calc(100dvh - var(--v-layout-top) - var(--v-layout-bottom));
display: flex;
flex-direction: column;
}
.layout-content {
flex: 1;
min-height: 0;
}
</style> </style>
+3
View File
@@ -75,6 +75,9 @@ const vuetify = createVuetify({
density: 'compact', density: 'compact',
hideDetails: true, hideDetails: true,
}, },
VCard: {
elevation: 0,
},
}, },
}) })
+2 -1
View File
@@ -16,6 +16,7 @@ declare module 'vue-router' {
breadcrumb?: string | ((route: RouteLocationNormalizedLoaded) => string) breadcrumb?: string | ((route: RouteLocationNormalizedLoaded) => string)
/** Named route the back button should navigate to, when deep-linking means history may be empty. */ /** Named route the back button should navigate to, when deep-linking means history may be empty. */
backTo?: string backTo?: string
fluid?: boolean
} }
} }
@@ -25,7 +26,7 @@ const router = createRouter({
{ {
path: '/', path: '/',
component: HomeView, component: HomeView,
meta: { layout: 'Default' }, meta: { layout: 'Default', fluid: false },
}, },
{ {
name: 'sign-in', name: 'sign-in',
+1
View File
@@ -20,6 +20,7 @@ export const issuesRoutes: RouteRecordRaw = {
meta: { meta: {
breadcrumb: (route) => route.params.ckey as string, breadcrumb: (route) => route.params.ckey as string,
backTo: 'issues', backTo: 'issues',
fluid: false,
}, },
}, },
], ],
+1 -1
View File
@@ -24,7 +24,7 @@ watchEffect(async () => fetch())
</script> </script>
<template> <template>
<v-sheet border> <v-sheet border class="h-100 d-flex flex-column">
<issue-details :ckey="ckey" /> <issue-details :ckey="ckey" />
</v-sheet> </v-sheet>
</template> </template>
+18 -6
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useIssueStore } from '@/stores/issue' import { useIssueStore } from '@/stores/issue'
import { useProjectStore } from '@/stores/project.ts' import { useProjectStore } from '@/stores/project.ts'
@@ -10,6 +10,8 @@ import type { Issue } from '@/api/issue.api.ts'
import IssuesListElement from '@/components/IssuesListElement.vue' import IssuesListElement from '@/components/IssuesListElement.vue'
import IssueStats from '@/components/IssueStats.vue' import IssueStats from '@/components/IssueStats.vue'
import IssueFilter from '@/components/IssueFilter.vue' import IssueFilter from '@/components/IssueFilter.vue'
import SplitHandle from '@/components/SplitHandle.vue'
import { useSplitPane } from '@/composables/useSplitPane.ts'
const issueStore = useIssueStore() const issueStore = useIssueStore()
const issueDetailsRef = ref<InstanceType<typeof IssueDetails>>() const issueDetailsRef = ref<InstanceType<typeof IssueDetails>>()
@@ -90,7 +92,9 @@ onMounted(async () => {
await load() await load()
await issueStore.getStat(currentProjectId.value) await issueStore.getStat(currentProjectId.value)
}) })
const gridTemplate = computed(() => (selected.value.length ? '1fr 1fr' : '1fr')) const splitContainer = useTemplateRef<HTMLElement>('splitContainer')
const split = useSplitPane(splitContainer, { storageKey: 'issues-list', min: 0.25, max: 0.75 })
const gridTemplate = computed(() => (selected.value.length ? split.gridTemplate.value : '1fr'))
const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`) const goToIssue = (key: string[]) => router.push(`/issues/${key[0]}`)
async function updatePage(val: number) { async function updatePage(val: number) {
@@ -183,8 +187,8 @@ async function onDeleteBatch() {
</script> </script>
<template> <template>
<div class="d-flex flex-column ga-2"> <div class="d-flex flex-column ga-2 h-100">
<div class="u-grid ga-1"> <div ref="splitContainer" class="u-grid">
<div class="u-height"> <div class="u-height">
<issue-filter <issue-filter
class="mb-2" class="mb-2"
@@ -274,7 +278,9 @@ async function onDeleteBatch() {
</div> </div>
</div> </div>
<v-sheet border v-if="selected.length" class="overflow-y-auto"> <split-handle v-if="selected.length" v-bind="split.handleProps.value" />
<v-sheet border v-if="selected.length" class="d-flex flex-column">
<div class="d-flex align-center ga-2 px-4 py-2"> <div class="d-flex align-center ga-2 px-4 py-2">
<div>Work item</div> <div>Work item</div>
<v-spacer /> <v-spacer />
@@ -297,9 +303,15 @@ async function onDeleteBatch() {
.u-grid { .u-grid {
display: grid; display: grid;
grid-template-columns: v-bind(gridTemplate); grid-template-columns: v-bind(gridTemplate);
flex: 1;
min-height: 0;
}
.u-grid > * {
min-height: 0;
} }
.u-height { .u-height {
height: calc(100vh - 10rem); overflow-y: auto;
} }
</style> </style>