Art: drag pieces into a new order

Each gallery tile gets a small drag handle (top-left, ✱). Grab it and
drop it anywhere among the others - the tile moves live as you drag
over neighbours, then the new order is saved automatically on drop.

- Artwork gets a `position` column; the gallery lists by position (was
  upload date). New uploads land after everything else.
- New POST /art/reorder endpoint takes an ordered list of ids (one
  shared CSRF token, "reorder-art") and rewrites positions 1..n.
- Migration backfills position from the existing upload-date order via
  ROW_NUMBER(), then drops the bootstrap default so it matches the
  Doctrine mapping.
- New Stimulus controller (art-reorder): native HTML5 drag/drop, no
  library. dragover live-reorders the DOM by comparing cursor x to the
  hovered tile's midpoint; dragend posts the resulting id order and
  shows a small "Saving order… / Order saved" status pill.

Verified: schema validate clean: a throwaway script exercised
persist+reload of a reordered set against the real DB, and a live
browser check dispatched real DragEvents through the actual Stimulus
controller and confirmed the exact POST payload it sent. phpunit 9/9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-15 17:18:02 +02:00
co-authored by Claude Sonnet 5
parent bc0433e356
commit 8b8f67c1d0
8 changed files with 291 additions and 15 deletions
+41
View File
@@ -113,6 +113,47 @@ table.dataTable {
height: auto;
}
/* Drag-to-reorder handle, top-left of each tile */
.art-drag-handle {
position: absolute;
top: 0.35rem;
left: 0.35rem;
z-index: 2;
width: 1.9rem;
height: 1.9rem;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--bs-border-color);
border-radius: 0.35rem;
background: var(--bs-body-bg);
color: var(--bs-secondary-color);
opacity: 0.8;
cursor: grab;
line-height: 1;
}
.art-drag-handle:hover { opacity: 1; color: var(--bs-body-color); }
.art-drag-handle:active { cursor: grabbing; }
.art-tile.is-dragging { opacity: 0.4; }
.art-tile.is-drag-over .card { outline: 2px dashed var(--bs-primary); outline-offset: 2px; }
.art-reorder-status {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 1080;
padding: 0.4rem 0.85rem;
border-radius: 2rem;
background: var(--bs-body-color);
color: var(--bs-body-bg);
font-size: 0.85rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.art-reorder-status.is-visible { opacity: 0.92; }
.art-reorder-status.is-error { background: var(--bs-danger); color: #fff; }
/* --- Tracker: unfoldable stat blocks --------------------------------- */
[data-bs-toggle="collapse"] .sb-caret {
display: inline-block;