fix: polish project workspace interactions
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { ProjectPageHeaderProps } from '@/types/project-navigation';
|
||||||
|
|
||||||
|
defineProps<ProjectPageHeaderProps>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" class="rounded px-2.5 py-1">
|
||||||
|
{{ project.key }}
|
||||||
|
</Badge>
|
||||||
|
<span class="text-muted-foreground text-xs font-medium">
|
||||||
|
{{ context }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="mt-2 truncate text-2xl font-bold tracking-tight">
|
||||||
|
{{ project.name }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted-foreground mt-1 max-w-3xl text-sm leading-6">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex shrink-0 flex-wrap gap-2 self-start sm:self-auto">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@@ -14,24 +14,6 @@ defineProps<ProjectTabsProps>();
|
|||||||
aria-label="Project sections"
|
aria-label="Project sections"
|
||||||
class="border-outline-variant/70 flex min-w-max items-center gap-1 border-b"
|
class="border-outline-variant/70 flex min-w-max items-center gap-1 border-b"
|
||||||
>
|
>
|
||||||
<Link
|
|
||||||
:href="boardIndex(project.id)"
|
|
||||||
prefetch
|
|
||||||
:class="[
|
|
||||||
'group relative inline-flex h-11 items-center gap-2 px-3 text-sm font-semibold transition-colors',
|
|
||||||
active === 'board'
|
|
||||||
? 'text-primary'
|
|
||||||
: 'text-muted-foreground hover:text-foreground',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<KanbanSquare class="size-4" />
|
|
||||||
Board
|
|
||||||
<span
|
|
||||||
v-if="active === 'board'"
|
|
||||||
class="bg-primary absolute inset-x-2 -bottom-px h-0.5"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
:href="backlogIndex(project.id)"
|
:href="backlogIndex(project.id)"
|
||||||
prefetch
|
prefetch
|
||||||
@@ -50,6 +32,24 @@ defineProps<ProjectTabsProps>();
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
:href="boardIndex(project.id)"
|
||||||
|
prefetch
|
||||||
|
:class="[
|
||||||
|
'group relative inline-flex h-11 items-center gap-2 px-3 text-sm font-semibold transition-colors',
|
||||||
|
active === 'board'
|
||||||
|
? 'text-primary'
|
||||||
|
: 'text-muted-foreground hover:text-foreground',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<KanbanSquare class="size-4" />
|
||||||
|
Board
|
||||||
|
<span
|
||||||
|
v-if="active === 'board'"
|
||||||
|
class="bg-primary absolute inset-x-2 -bottom-px h-0.5"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
v-if="canManageProject"
|
v-if="canManageProject"
|
||||||
:href="settingsIndex(project.id)"
|
:href="settingsIndex(project.id)"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form, Head, Link } from '@inertiajs/vue3';
|
import { Form, Head, Link } from '@inertiajs/vue3';
|
||||||
|
import { Plus } from 'lucide-vue-next';
|
||||||
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
|
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
|
||||||
import Heading from '@/components/Heading.vue';
|
import ProjectPageHeader from '@/components/projects/ProjectPageHeader.vue';
|
||||||
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -72,34 +73,25 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
|||||||
<Head :title="`${project.name} backlog`" />
|
<Head :title="`${project.name} backlog`" />
|
||||||
|
|
||||||
<div class="app-page max-w-none gap-5">
|
<div class="app-page max-w-none gap-5">
|
||||||
<div
|
<ProjectPageHeader
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
:project="project"
|
||||||
|
context="Project backlog"
|
||||||
|
description="Plan and refine work before moving it onto the board."
|
||||||
>
|
>
|
||||||
<div class="space-y-2">
|
<Button v-if="canCreateTickets" as-child size="sm">
|
||||||
<Badge variant="outline" class="rounded px-2.5 py-1">
|
<Link
|
||||||
{{ project.key }}
|
:href="
|
||||||
</Badge>
|
createTicket(
|
||||||
<Heading
|
{ project: project.id },
|
||||||
:title="project.name"
|
{ query: { source: 'backlog' } },
|
||||||
description="Plan and refine work before moving it onto the board."
|
)
|
||||||
/>
|
"
|
||||||
</div>
|
>
|
||||||
|
<Plus class="size-4" />
|
||||||
<div class="flex flex-wrap gap-3">
|
Create ticket
|
||||||
<Button v-if="canCreateTickets" as-child>
|
</Link>
|
||||||
<Link
|
</Button>
|
||||||
:href="
|
</ProjectPageHeader>
|
||||||
createTicket(
|
|
||||||
{ project: project.id },
|
|
||||||
{ query: { source: 'backlog' } },
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Create ticket
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<ProjectTabs
|
<ProjectTabs
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link, router } from '@inertiajs/vue3';
|
import { Head, Link, router } from '@inertiajs/vue3';
|
||||||
import { CircleUserRound, GripVertical, Plus } from 'lucide-vue-next';
|
import { CircleUserRound, GripVertical, Plus } from 'lucide-vue-next';
|
||||||
import { onBeforeUnmount, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
|
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
|
||||||
|
import ProjectPageHeader from '@/components/projects/ProjectPageHeader.vue';
|
||||||
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
||||||
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
|
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { index as projectIndex } from '@/routes/projects';
|
import { index as projectIndex } from '@/routes/projects';
|
||||||
@@ -53,6 +53,7 @@ const pointerX = ref(0);
|
|||||||
const pointerY = ref(0);
|
const pointerY = ref(0);
|
||||||
|
|
||||||
let autoScrollFrame: number | undefined;
|
let autoScrollFrame: number | undefined;
|
||||||
|
let initialScrollFrame: number | undefined;
|
||||||
let longPressTimer: number | undefined;
|
let longPressTimer: number | undefined;
|
||||||
let suppressClickTicketId: number | null = null;
|
let suppressClickTicketId: number | null = null;
|
||||||
|
|
||||||
@@ -96,6 +97,9 @@ const isActiveDropTarget = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dropZoneRows = (ticket: TicketModel, boardColumnId: number): string =>
|
||||||
|
`repeat(${allowedStatusesForColumn(ticket, boardColumnId).length}, minmax(0, 1fr))`;
|
||||||
|
|
||||||
const clearLongPressTimer = (): void => {
|
const clearLongPressTimer = (): void => {
|
||||||
if (longPressTimer !== undefined) {
|
if (longPressTimer !== undefined) {
|
||||||
window.clearTimeout(longPressTimer);
|
window.clearTimeout(longPressTimer);
|
||||||
@@ -221,7 +225,11 @@ const handlePointerDown = (
|
|||||||
startY: event.clientY,
|
startY: event.clientY,
|
||||||
};
|
};
|
||||||
|
|
||||||
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
window.addEventListener('pointermove', handlePointerMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
window.addEventListener('pointerup', finishPointer);
|
||||||
|
window.addEventListener('pointercancel', cancelPointer);
|
||||||
|
|
||||||
if (event.pointerType === 'touch') {
|
if (event.pointerType === 'touch') {
|
||||||
longPressTimer = window.setTimeout(startDragging, 240);
|
longPressTimer = window.setTimeout(startDragging, 240);
|
||||||
@@ -268,6 +276,9 @@ const handlePointerMove = (event: PointerEvent): void => {
|
|||||||
const resetDragState = (): void => {
|
const resetDragState = (): void => {
|
||||||
clearLongPressTimer();
|
clearLongPressTimer();
|
||||||
stopAutoScroll();
|
stopAutoScroll();
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
window.removeEventListener('pointerup', finishPointer);
|
||||||
|
window.removeEventListener('pointercancel', cancelPointer);
|
||||||
pendingDragTicket.value = null;
|
pendingDragTicket.value = null;
|
||||||
draggedTicket.value = null;
|
draggedTicket.value = null;
|
||||||
dragPointer.value = null;
|
dragPointer.value = null;
|
||||||
@@ -311,11 +322,6 @@ const finishPointer = (event: PointerEvent): void => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetElement = event.currentTarget as HTMLElement;
|
|
||||||
if (targetElement.hasPointerCapture(event.pointerId)) {
|
|
||||||
targetElement.releasePointerCapture(event.pointerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDragState();
|
resetDragState();
|
||||||
|
|
||||||
if (drag !== null) {
|
if (drag !== null) {
|
||||||
@@ -347,7 +353,21 @@ const handleTicketClick = (event: MouseEvent, ticketId: number): void => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initialScrollFrame = window.requestAnimationFrame(() => {
|
||||||
|
if (boardScroller.value !== null) {
|
||||||
|
boardScroller.value.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialScrollFrame = undefined;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (initialScrollFrame !== undefined) {
|
||||||
|
window.cancelAnimationFrame(initialScrollFrame);
|
||||||
|
}
|
||||||
|
|
||||||
resetDragState();
|
resetDragState();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -357,23 +377,13 @@ onBeforeUnmount(() => {
|
|||||||
<Head :title="`${project.name} board`" />
|
<Head :title="`${project.name} board`" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="app-page min-h-[calc(100dvh-3.5rem)] max-w-none gap-4 overflow-hidden"
|
class="app-page min-h-[calc(100dvh-3.5rem)] max-w-none gap-5 overflow-hidden"
|
||||||
>
|
>
|
||||||
<header class="flex items-end justify-between gap-4">
|
<ProjectPageHeader
|
||||||
<div class="min-w-0">
|
:project="project"
|
||||||
<div class="flex items-center gap-2">
|
context="Project board"
|
||||||
<Badge variant="outline" class="rounded px-2.5 py-1">
|
description="Move tickets through the project's active workflow."
|
||||||
{{ project.key }}
|
>
|
||||||
</Badge>
|
|
||||||
<span class="text-muted-foreground text-xs font-medium">
|
|
||||||
Project board
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="mt-2 truncate text-2xl font-bold tracking-tight">
|
|
||||||
{{ project.name }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button v-if="canCreateTickets" as-child size="sm">
|
<Button v-if="canCreateTickets" as-child size="sm">
|
||||||
<Link
|
<Link
|
||||||
:href="
|
:href="
|
||||||
@@ -387,7 +397,7 @@ onBeforeUnmount(() => {
|
|||||||
Create ticket
|
Create ticket
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</ProjectPageHeader>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<ProjectTabs
|
<ProjectTabs
|
||||||
@@ -429,20 +439,20 @@ onBeforeUnmount(() => {
|
|||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
ref="boardScroller"
|
ref="boardScroller"
|
||||||
class="-mx-4 flex min-h-[max(28rem,calc(100dvh-14rem))] flex-1 snap-x snap-proximity items-stretch gap-3 overflow-x-auto px-4 pb-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"
|
class="flex min-h-[max(28rem,calc(100dvh-14rem))] min-w-0 flex-1 snap-x snap-proximity items-stretch gap-3 overflow-x-auto overscroll-x-contain pb-3"
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
v-for="boardColumn in boardColumns"
|
v-for="boardColumn in boardColumns"
|
||||||
:key="boardColumn.id"
|
:key="boardColumn.id"
|
||||||
:data-board-column-id="boardColumn.id"
|
:data-board-column-id="boardColumn.id"
|
||||||
:class="[
|
:class="[
|
||||||
'bg-surface-container-lowest flex min-h-full w-[min(21rem,calc(100vw-2.5rem))] shrink-0 snap-start flex-col rounded border transition-[border-color,background-color,filter,opacity,box-shadow] duration-150 sm:w-[21rem]',
|
'bg-surface-container-lowest relative flex min-h-full w-[min(21rem,calc(100vw-2.5rem))] shrink-0 snap-start flex-col rounded border transition-[border-color,background-color,filter,opacity,box-shadow] duration-150 sm:w-[21rem]',
|
||||||
draggedTicket === null
|
draggedTicket === null
|
||||||
? 'border-outline-variant/70'
|
? 'border-outline-variant/70'
|
||||||
: isAllowedColumn(boardColumn.id)
|
: isAllowedColumn(boardColumn.id)
|
||||||
? isActiveDropTarget(boardColumn.id)
|
? isActiveDropTarget(boardColumn.id)
|
||||||
? 'border-primary bg-primary/8 ring-primary/25 ring-2'
|
? 'border-tertiary bg-tertiary/5 ring-tertiary/30 shadow-lg ring-2'
|
||||||
: 'border-primary/70 bg-primary/5 ring-primary/10 ring-1'
|
: 'border-primary/80 bg-primary/3 ring-primary/15 ring-1'
|
||||||
: 'border-outline-variant/40 bg-muted/40 opacity-45 grayscale',
|
: 'border-outline-variant/40 bg-muted/40 opacity-45 grayscale',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -463,16 +473,24 @@ onBeforeUnmount(() => {
|
|||||||
{{ boardColumn.tickets?.length ?? 0 }}
|
{{ boardColumn.tickets?.length ?? 0 }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="relative flex flex-1 flex-col gap-2.5 p-2.5">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
draggedTicket !== null &&
|
draggedTicket !== null &&
|
||||||
allowedStatusesForColumn(
|
allowedStatusesForColumn(
|
||||||
draggedTicket.ticket,
|
draggedTicket.ticket,
|
||||||
boardColumn.id,
|
boardColumn.id,
|
||||||
).length > 1
|
).length > 0
|
||||||
"
|
"
|
||||||
class="mt-3 grid gap-1.5"
|
class="absolute inset-2.5 z-20 grid gap-2"
|
||||||
|
:style="{
|
||||||
|
gridTemplateRows: dropZoneRows(
|
||||||
|
draggedTicket.ticket,
|
||||||
|
boardColumn.id,
|
||||||
|
),
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="ticketStatus in allowedStatusesForColumn(
|
v-for="ticketStatus in allowedStatusesForColumn(
|
||||||
@@ -482,21 +500,19 @@ onBeforeUnmount(() => {
|
|||||||
:key="ticketStatus.id"
|
:key="ticketStatus.id"
|
||||||
:data-ticket-status-id="ticketStatus.id"
|
:data-ticket-status-id="ticketStatus.id"
|
||||||
:class="[
|
:class="[
|
||||||
'rounded border border-dashed px-3 py-2 text-center text-xs font-semibold transition-colors',
|
'board-drop-zone flex min-h-0 items-center justify-center rounded border-2 border-dashed p-4 text-center text-xs font-bold transition-[border-color,background-color,box-shadow,color] duration-150',
|
||||||
isActiveDropTarget(
|
isActiveDropTarget(
|
||||||
boardColumn.id,
|
boardColumn.id,
|
||||||
ticketStatus.id,
|
ticketStatus.id,
|
||||||
)
|
)
|
||||||
? 'border-primary bg-primary text-primary-foreground'
|
? 'board-drop-zone-active border-tertiary text-on-tertiary-container ring-tertiary/30 dark:text-tertiary-fixed ring-2'
|
||||||
: 'border-primary/60 bg-primary/8 text-primary',
|
: 'border-primary/70 text-on-primary-container dark:text-primary-fixed',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
Drop as {{ ticketStatus.name }}
|
Drop as {{ ticketStatus.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-2.5 p-2.5">
|
|
||||||
<div
|
<div
|
||||||
v-if="(boardColumn.tickets?.length ?? 0) === 0"
|
v-if="(boardColumn.tickets?.length ?? 0) === 0"
|
||||||
class="text-muted-foreground flex min-h-28 flex-1 items-center justify-center rounded border border-dashed p-4 text-center text-xs"
|
class="text-muted-foreground flex min-h-28 flex-1 items-center justify-center rounded border border-dashed p-4 text-center text-xs"
|
||||||
@@ -525,16 +541,6 @@ onBeforeUnmount(() => {
|
|||||||
? 'pointer-events-none animate-pulse opacity-55'
|
? 'pointer-events-none animate-pulse opacity-55'
|
||||||
: '',
|
: '',
|
||||||
]"
|
]"
|
||||||
@pointerdown="
|
|
||||||
handlePointerDown(
|
|
||||||
$event,
|
|
||||||
ticket,
|
|
||||||
boardColumn,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@pointermove="handlePointerMove"
|
|
||||||
@pointerup="finishPointer"
|
|
||||||
@pointercancel="cancelPointer"
|
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
:href="
|
:href="
|
||||||
@@ -545,6 +551,13 @@ onBeforeUnmount(() => {
|
|||||||
"
|
"
|
||||||
class="block touch-pan-y select-none p-3.5"
|
class="block touch-pan-y select-none p-3.5"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
@pointerdown="
|
||||||
|
handlePointerDown(
|
||||||
|
$event,
|
||||||
|
ticket,
|
||||||
|
boardColumn,
|
||||||
|
)
|
||||||
|
"
|
||||||
@click="
|
@click="
|
||||||
handleTicketClick($event, ticket.id)
|
handleTicketClick($event, ticket.id)
|
||||||
"
|
"
|
||||||
@@ -656,3 +669,44 @@ onBeforeUnmount(() => {
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.board-drop-zone {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--primary) 8%,
|
||||||
|
var(--surface-container-lowest)
|
||||||
|
);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
color-mix(in srgb, var(--primary) 12%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
color-mix(in srgb, var(--primary) 12%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-drop-zone-active {
|
||||||
|
background-color: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--tertiary) 14%,
|
||||||
|
var(--surface-container-lowest)
|
||||||
|
);
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
color-mix(in srgb, var(--tertiary) 20%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
color-mix(in srgb, var(--tertiary) 20%, transparent) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
|
box-shadow: inset 0 0 0 1px
|
||||||
|
color-mix(in srgb, var(--tertiary) 22%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
Tags,
|
Tags,
|
||||||
} from 'lucide-vue-next';
|
} from 'lucide-vue-next';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import ProjectPageHeader from '@/components/projects/ProjectPageHeader.vue';
|
||||||
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
import ProjectTabs from '@/components/projects/ProjectTabs.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AppLayout from '@/layouts/AppLayout.vue';
|
import AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { index as projectsIndex } from '@/routes/projects';
|
import { index as projectsIndex } from '@/routes/projects';
|
||||||
@@ -134,29 +134,18 @@ const sectionHref = (section: ProjectSettingsSection) =>
|
|||||||
<Head :title="`${project.name} settings`" />
|
<Head :title="`${project.name} settings`" />
|
||||||
|
|
||||||
<div class="app-page max-w-none gap-5">
|
<div class="app-page max-w-none gap-5">
|
||||||
<header
|
<ProjectPageHeader
|
||||||
class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"
|
:project="project"
|
||||||
|
context="Project settings"
|
||||||
|
description="Project configuration and access controls."
|
||||||
>
|
>
|
||||||
<div class="space-y-2">
|
|
||||||
<Badge variant="outline" class="rounded px-2.5 py-1">
|
|
||||||
{{ project.key }}
|
|
||||||
</Badge>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold tracking-tight">
|
|
||||||
{{ project.name }}
|
|
||||||
</h1>
|
|
||||||
<p class="text-muted-foreground mt-1 text-sm">
|
|
||||||
Project configuration and access controls.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
v-if="
|
v-if="
|
||||||
activeSection === 'general' &&
|
activeSection === 'general' &&
|
||||||
$page.props.auth.permissions.projectCreate
|
$page.props.auth.permissions.projectCreate
|
||||||
"
|
"
|
||||||
as-child
|
as-child
|
||||||
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<Link :href="projectTemplateCreate(project.id)">
|
<Link :href="projectTemplateCreate(project.id)">
|
||||||
@@ -164,7 +153,7 @@ const sectionHref = (section: ProjectSettingsSection) =>
|
|||||||
Save as template
|
Save as template
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</ProjectPageHeader>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<ProjectTabs
|
<ProjectTabs
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ export type ProjectTabsProps = {
|
|||||||
project: ProjectModel;
|
project: ProjectModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProjectPageHeaderProps = {
|
||||||
|
context: string;
|
||||||
|
description: string;
|
||||||
|
project: ProjectModel;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmbeddedProjectSettingsSectionProps<T> = Omit<
|
export type EmbeddedProjectSettingsSectionProps<T> = Omit<
|
||||||
T,
|
T,
|
||||||
'embedded' | 'project' | 'status'
|
'embedded' | 'project' | 'status'
|
||||||
|
|||||||
Reference in New Issue
Block a user