From 1a6b3fac1dc94598c72fc07df632e2990eec235c Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 3 Mar 2026 15:58:05 +0000 Subject: [PATCH] feat: drag-and-drop reorder for daily focus items Add HTML5 drag-and-drop within domain groups on the focus page. Items can be dragged to reorder within their domain; cross-domain drag is prevented. Uses hidden form POST (no fetch/XHR). Arrow buttons kept as fallback. Co-Authored-By: Claude Opus 4.6 --- routers/focus.py | 14 ++++++++ static/style.css | 5 +++ templates/focus.html | 77 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/routers/focus.py b/routers/focus.py index 9779b44..9420f64 100644 --- a/routers/focus.py +++ b/routers/focus.py @@ -222,6 +222,20 @@ async def reorder_focus( return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) +@router.post("/reorder-all") +async def reorder_all_focus( + request: Request, + item_ids: str = Form(...), + focus_date: str = Form(...), + db: AsyncSession = Depends(get_db), +): + repo = BaseRepository("daily_focus", db) + ids = [i.strip() for i in item_ids.split(",") if i.strip()] + if ids: + await repo.reorder(ids) + return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) + + @router.post("/{focus_id}/toggle") async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("daily_focus", db) diff --git a/static/style.css b/static/style.css index 1395034..01fc77c 100644 --- a/static/style.css +++ b/static/style.css @@ -1107,6 +1107,11 @@ a:hover { color: var(--accent-hover); } transition: width 0.3s; } +/* ---- Focus Drag-and-Drop ---- */ +.focus-drag-row { cursor: grab; } +.focus-drag-row.dragging { opacity: 0.4; background: var(--accent-soft); } +.focus-drag-row.drag-over { box-shadow: 0 -2px 0 0 var(--accent) inset; } + /* ---- Reorder Grip Handle ---- */ .reorder-grip { display: inline-flex; diff --git a/templates/focus.html b/templates/focus.html index 97a3edd..8728beb 100644 --- a/templates/focus.html +++ b/templates/focus.html @@ -15,20 +15,23 @@ {% if items %}
- +
{% for domain in hierarchy %} + + + {% for item in domain.rows %} - + {% endfor %} + {% endfor %}
{{ domain.label }}
{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}{% include 'partials/reorder_arrows.html' %}{% endwith %}
@@ -48,8 +51,13 @@
+ + + +
{% else %}
No focus items for this day
@@ -182,6 +190,71 @@ document.getElementById('focus-show-more-wrap').style.display = 'none'; }); } + + // Drag-and-drop reorder within domain groups + var dragRow = null; + var dragGroup = null; + + document.querySelectorAll('.focus-drag-row').forEach(function(row) { + row.addEventListener('dragstart', function(e) { + dragRow = row; + dragGroup = row.closest('.focus-drag-group'); + row.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', row.dataset.id); + }); + + row.addEventListener('dragend', function() { + row.classList.remove('dragging'); + document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) { + el.classList.remove('drag-over'); + }); + // Collect all IDs across all groups in DOM order and submit + if (dragRow) { + var allIds = []; + document.querySelectorAll('#focus-table .focus-drag-row').forEach(function(r) { + allIds.push(r.dataset.id); + }); + document.getElementById('focus-reorder-ids').value = allIds.join(','); + document.getElementById('focus-reorder-form').submit(); + } + dragRow = null; + dragGroup = null; + }); + + row.addEventListener('dragover', function(e) { + e.preventDefault(); + if (!dragRow || row === dragRow) return; + // Constrain to same domain group + if (row.closest('.focus-drag-group') !== dragGroup) return; + e.dataTransfer.dropEffect = 'move'; + document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) { + el.classList.remove('drag-over'); + }); + row.classList.add('drag-over'); + }); + + row.addEventListener('dragleave', function() { + row.classList.remove('drag-over'); + }); + + row.addEventListener('drop', function(e) { + e.preventDefault(); + if (!dragRow || row === dragRow) return; + if (row.closest('.focus-drag-group') !== dragGroup) return; + row.classList.remove('drag-over'); + // Insert dragged row before or after target based on position + var tbody = dragGroup; + var rows = Array.from(tbody.querySelectorAll('.focus-drag-row')); + var dragIdx = rows.indexOf(dragRow); + var targetIdx = rows.indexOf(row); + if (dragIdx < targetIdx) { + tbody.insertBefore(dragRow, row.nextSibling); + } else { + tbody.insertBefore(dragRow, row); + } + }); + }); })(); {% endblock %}