diff --git a/web/src/App.svelte b/web/src/App.svelte
index 87425e0..2d5b5b9 100644
--- a/web/src/App.svelte
+++ b/web/src/App.svelte
@@ -53,6 +53,15 @@
let editTimeEnd = $state('')
let tokenName = $state('tui')
let tokenScope = $state('write')
+ let dragTicketId = $state('')
+ let dragOverStatus = $state('')
+ let suppressTicketClick = false
+
+ const boardStatuses = [
+ ['todo', 'OPEN'],
+ ['in_progress', 'WIP'],
+ ['done', 'DONE'],
+ ]
function desiredHash() {
return buildHash({
@@ -591,6 +600,63 @@
return tickets.filter((t) => t.status === status)
}
+ async function setTicketStatus(ticket, status) {
+ if (!ticket || ticket.status === status) return
+ error = ''
+ const prev = ticket.status
+ tickets = tickets.map((t) => (t.id === ticket.id ? { ...t, status } : t))
+ try {
+ await api.patchTicket(projectId, ticket.id, { status })
+ if (selected?.id === ticket.id) {
+ selected = { ...selected, status }
+ editStatus = status
+ }
+ } catch (e) {
+ tickets = tickets.map((t) => (t.id === ticket.id ? { ...t, status: prev } : t))
+ error = e.message
+ }
+ }
+
+ function onTicketDragStart(e, ticket) {
+ dragTicketId = ticket.id
+ e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.setData('text/plain', ticket.id)
+ }
+
+ function onTicketDragEnd() {
+ dragTicketId = ''
+ dragOverStatus = ''
+ // Click fires after dragend on some browsers; suppress open-once.
+ suppressTicketClick = true
+ setTimeout(() => {
+ suppressTicketClick = false
+ }, 0)
+ }
+
+ function onColumnDragOver(e, status) {
+ e.preventDefault()
+ e.dataTransfer.dropEffect = 'move'
+ dragOverStatus = status
+ }
+
+ function onColumnDragLeave(e, status) {
+ if (dragOverStatus === status) dragOverStatus = ''
+ }
+
+ async function onColumnDrop(e, status) {
+ e.preventDefault()
+ const id = e.dataTransfer.getData('text/plain') || dragTicketId
+ dragTicketId = ''
+ dragOverStatus = ''
+ const ticket = tickets.find((t) => t.id === id)
+ if (ticket) await setTicketStatus(ticket, status)
+ }
+
+ function onBoardTicketClick(t) {
+ if (suppressTicketClick) return
+ openTicket(t)
+ }
+
async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
@@ -775,15 +841,34 @@
- {#each [['todo', 'OPEN'], ['in_progress', 'WIP'], ['done', 'DONE']] as [st, label]}
-
+ {#each boardStatuses as [st, label]}
+
onColumnDragOver(e, st)}
+ ondragleave={(e) => onColumnDragLeave(e, st)}
+ ondrop={(e) => onColumnDrop(e, st)}
+ role="list"
+ aria-label={label}
+ >
{label}
{#each byStatus(st) as t}
-
+
onTicketDragStart(e, t)}
+ ondragend={onTicketDragEnd}
+ >
+
+
⠿
+
+ {:else}
+
Drop hier
{/each}
{/each}
diff --git a/web/src/app.css b/web/src/app.css
index 10c1433..cc40c47 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -350,20 +350,38 @@ code {
.error { color: var(--danger); margin: 0.5rem 0; }
.row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.stack { display: grid; gap: 0.6rem; }
+.board-col {
+ min-height: 8rem;
+ padding: 0.55rem;
+ border-radius: 8px;
+ border: 1px dashed transparent;
+ transition: border-color 0.15s ease, background 0.15s ease;
+}
+.board-col.drag-over {
+ border-color: color-mix(in srgb, var(--accent) 70%, var(--line));
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+}
+.board-empty {
+ padding: 0.75rem 0.4rem;
+ font-size: 0.85rem;
+ text-align: center;
+ border: 1px dashed var(--line);
+ border-radius: 6px;
+}
.ticket {
border: none;
- border-radius: 0;
- padding: 0.65rem 0.85rem;
+ border-radius: 6px;
+ padding: 0.55rem 0.7rem 0.4rem;
background: color-mix(in srgb, var(--panel-2) 55%, transparent);
- cursor: pointer;
+ cursor: grab;
text-align: left;
width: 100%;
color: var(--ink);
font-weight: 500;
+ display: grid;
+ gap: 0.25rem;
}
.ticket:hover {
- border: none;
- color: var(--ink);
background: linear-gradient(
to right,
color-mix(in srgb, var(--accent) 35%, transparent) 0%,
@@ -371,6 +389,32 @@ code {
color-mix(in srgb, var(--panel-2) 70%, transparent) 100%
);
}
+.ticket.dragging {
+ opacity: 0.45;
+ cursor: grabbing;
+}
+.ticket-open {
+ display: grid;
+ gap: 0.15rem;
+ width: 100%;
+ padding: 0.05rem 0;
+ text-align: left;
+ color: inherit;
+ font-weight: inherit;
+ background: transparent;
+}
+.ticket-open:hover {
+ color: inherit;
+ background: transparent;
+}
+.drag-handle {
+ justify-self: end;
+ color: var(--muted);
+ font-size: 0.95rem;
+ letter-spacing: -0.08em;
+ line-height: 1;
+ user-select: none;
+}
.badge {
display: inline-block;
font-size: 0.75rem;