Implement Fluid Emerald app redesign
linter / quality (pull_request) Successful in 1m18s
tests / ci (8.4) (pull_request) Successful in 1m54s

This commit is contained in:
2026-07-18 16:45:54 +02:00
parent 59817ad30f
commit a46695c2eb
62 changed files with 2165 additions and 1434 deletions
+85 -40
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { Form, Head, Link } from '@inertiajs/vue3';
import { ArrowRight, CircleUserRound } from 'lucide-vue-next';
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
import Heading from '@/components/Heading.vue';
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@@ -16,7 +18,10 @@ import { index as backlogIndex } from '@/routes/projects/backlog';
import { index as projectIndex, show } from '@/routes/projects/index';
import { board as boardSettings } from '@/routes/projects/settings';
import { index as boardIndex } from '@/routes/projects/board';
import { create as createTicket, show as showTicket } from '@/routes/projects/tickets';
import {
create as createTicket,
show as showTicket,
} from '@/routes/projects/tickets';
import type { BreadcrumbItem, ProjectBoardPageProps } from '@/types';
import type { TicketModel } from '@/types/models/ticket-model';
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
@@ -65,10 +70,10 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbs">
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
<Head :title="`${project.name} board`" />
<div class="flex flex-1 flex-col gap-6 p-4">
<div class="app-page max-w-none">
<div
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
>
@@ -108,10 +113,7 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
</div>
</div>
<div
v-if="status"
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
>
<div v-if="status" class="status-success">
{{ status }}
</div>
@@ -131,13 +133,18 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
</CardContent>
</Card>
<div v-else class="grid gap-4 xl:grid-cols-3">
<div
v-else
class="-mx-4 flex snap-x snap-proximity items-start gap-4 overflow-x-auto px-4 pb-5 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"
>
<Card
v-for="boardColumn in boardColumns"
:key="boardColumn.id"
class="flex h-full flex-col gap-0 overflow-hidden"
class="bg-surface-container-low w-[min(21rem,calc(100vw-3rem))] shrink-0 snap-start gap-0 overflow-hidden border-0 py-0 shadow-none hover:shadow-none sm:w-[21rem]"
>
<CardHeader class="bg-muted/20 border-b">
<CardHeader
class="border-outline-variant/60 sticky top-0 z-10 border-b px-4 py-4"
>
<div class="flex items-start justify-between gap-3">
<div>
<CardTitle>{{ boardColumn.name }}</CardTitle>
@@ -163,33 +170,38 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
</div>
<Badge variant="secondary">
#{{ boardColumn.position }}
{{ boardColumn.tickets?.length ?? 0 }}
</Badge>
</div>
</CardHeader>
<CardContent class="flex flex-1 flex-col gap-4 p-4">
<CardContent
class="flex min-h-48 flex-1 flex-col gap-3 p-3"
>
<div
v-if="(boardColumn.tickets?.length ?? 0) === 0"
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
class="empty-state bg-transparent"
>
No tickets are currently in this column.
</div>
<div v-else class="flex flex-col gap-4">
<div v-else class="flex flex-col gap-3">
<Card
v-for="ticket in boardColumn.tickets"
:key="ticket.id"
:class="[
'gap-0',
'shadow-xs gap-0 py-0 hover:-translate-y-0.5',
ticket.is_blocked
? 'border-amber-300 bg-amber-50/70 dark:border-amber-700/60 dark:bg-amber-950/30'
: '',
]"
>
<CardHeader class="pb-4">
<CardHeader class="gap-3 px-4 pb-3 pt-4">
<div class="flex flex-wrap gap-2">
<Badge variant="outline">
<Badge
variant="outline"
class="text-primary"
>
{{ ticket.ticket_key }}
</Badge>
<Badge
@@ -201,16 +213,6 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
<Badge variant="secondary">
{{ ticket.ticket_type.name }}
</Badge>
<Badge variant="secondary">
{{ ticket.ticket_priority.name }}
</Badge>
<Badge variant="outline">
{{
ticketStatusLabel(
ticket.ticket_status,
)
}}
</Badge>
</div>
<CardTitle class="text-base">
<Link
@@ -240,9 +242,56 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
>
{{ ticket.blocked_comment }}
</CardDescription>
<div
class="border-outline-variant/60 flex items-center justify-between gap-3 border-t pt-3"
>
<div
class="flex min-w-0 items-center gap-2"
>
<UserAvatar
v-if="ticket.assignee"
:user="ticket.assignee"
class="size-7"
/>
<span
v-else
class="border-outline-variant text-muted-foreground flex size-7 items-center justify-center rounded-full border border-dashed"
>
<CircleUserRound
class="size-4"
/>
</span>
<span
class="text-muted-foreground truncate text-xs"
>
{{
ticket.assignee?.name ??
'Unassigned'
}}
</span>
</div>
<div
class="flex shrink-0 items-center gap-1.5"
>
<span
class="bg-primary size-1.5 rounded-full"
/>
<span
class="text-muted-foreground text-xs font-medium"
>
{{
ticket.ticket_priority.name
}}
</span>
</div>
</div>
</CardHeader>
<CardContent v-if="canEditTickets" class="pt-0">
<CardContent
v-if="canEditTickets"
class="border-outline-variant/60 border-t px-4 py-3"
>
<Form
v-bind="
ProjectBoardController.move.form({
@@ -258,15 +307,7 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
name="redirect_to"
value="board"
/>
<label
:for="`ticket-status-${ticket.id}`"
class="text-muted-foreground text-xs font-medium uppercase tracking-wide"
>
Move ticket
</label>
<div
class="flex flex-col gap-2 sm:flex-row"
>
<div class="flex items-center gap-2">
<select
:id="`ticket-status-${ticket.id}`"
name="ticket_status_id"
@@ -275,7 +316,7 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
ticket,
).length === 0 || processing
"
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-w-44 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-60"
class="min-w-0 flex-1 text-xs"
>
<option
v-if="
@@ -303,7 +344,8 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
</select>
<Button
type="submit"
size="sm"
size="icon-sm"
variant="ghost"
:disabled="
processing ||
allowedTargetStatuses(
@@ -311,7 +353,10 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
).length === 0
"
>
Update status
<ArrowRight class="size-4" />
<span class="sr-only"
>Update status</span
>
</Button>
</div>
<p