docs(specs): blueprint overlay picker — drag-list + add-dropdown
Replace per-row checkbox + numeric Order inputs with a drag-to-reorder list of selected overlays plus a native <select> for adding more. Native HTML5 DnD; no library, no JS-disabled fallback. Server contract unchanged (overlay_ids in DOM order; existing fallback_position branch absorbs the omitted overlay_position_<id> fields). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
01760a31f5
commit
dec4fed809
1 changed files with 118 additions and 0 deletions
|
|
@ -0,0 +1,118 @@
|
|||
# L4D2 Blueprint Overlay Picker Design
|
||||
|
||||
**Goal:** Replace the checkbox + numeric-Order table on the blueprint detail page with a drag-to-reorder list and a single dropdown to add overlays. Drag-and-drop is the primary reorder mechanic; per-row Order text inputs are removed.
|
||||
|
||||
**Approval status:** User-approved. No companion implementation plan — small surface, implemented directly.
|
||||
|
||||
## Context
|
||||
|
||||
`templates/blueprint_detail.html:14-28` currently renders one HTML table for the blueprint's overlays. Each row carries a `Use` checkbox, a numeric `Order` text input, and the overlay name. To enable an overlay you check it; to reorder you type integers into per-row text fields. Adding a new overlay between existing ones means renumbering everything below it by hand.
|
||||
|
||||
This spec replaces that table with a single ordered list of *selected* overlays plus a `<select>` dropdown for adding more. Drag-to-reorder is the only reorder interaction. A ✕ button on each row removes it (returning it to the dropdown). Picking an entry from the dropdown appends it to the list (and removes it from the dropdown).
|
||||
|
||||
The change is intentionally scoped small: no two-panel layout, no filter widget, no touch / keyboard reorder support, no JS-disabled fallback. The native `<select>` element supplies typeahead-by-letter and keyboard navigation for free, which covers the no-drag path. The page is desktop-primary.
|
||||
|
||||
## Locked Decisions
|
||||
|
||||
1. **Single ordered list of selected overlays only.** No second pane. The "available" set lives in the `<select>`. Adding via dropdown is one click; removing via ✕ is one click; reordering is one drag.
|
||||
2. **Native HTML5 drag-and-drop.** No vendored library, no polyfill. Touch-screen drag is unsupported on Android and rough on iOS — accepted because the page is desktop-primary. Add and remove still work on touch via the `<select>` and the `<button>`.
|
||||
3. **JS-required UI.** If JS does not load, the page is unusable. No degradation to the old checkbox table.
|
||||
4. **Server contract unchanged.** Each list row owns one `<input type="hidden" name="overlay_ids" value="{id}">`. Form-submission order = DOM order. The existing `ordered_overlay_ids_from_form` handler in `routes/blueprint_routes.py` already falls back to enumerate index when no `overlay_position_<id>` field is present, so it accepts the new shape with no Python edit.
|
||||
5. **Dropdown re-sorted alphabetically on remove.** When ✕ removes a row, the corresponding `<option>` is sorted-inserted back into the `<select>` (case-insensitive name compare). The dropdown stays predictable.
|
||||
6. **Drop-indicator visual.** A 2px focus-color bar drawn via `box-shadow … inset` on the row under the cursor: top-bar = "drop will land before this row", bottom-bar = "drop will land after this row". The hover side is computed by comparing `event.clientY` to the row's vertical midpoint.
|
||||
7. **Drop on empty space inside the list = append.** Drop directly on the dragged row (or with no movement) = no-op. Escape during drag triggers `dragend`, which clears all visual classes.
|
||||
8. **Out of scope:** keyboard reorder, ARIA live announcements, touch DnD polyfill, server-side cleanup of the now-unused `overlay_position_<id>` form-field path.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
GET /blueprints/<id>
|
||||
page_routes.blueprint_page
|
||||
├─▶ selected_overlays (ordered by BlueprintOverlay.position)
|
||||
└─▶ available_overlays = all_overlays \ selected_overlays
|
||||
(alphabetical)
|
||||
|
||||
templates/blueprint_detail.html
|
||||
<ol data-overlay-list> ← drag target, hidden inputs
|
||||
<li data-overlay-id draggable> × ⋮⋮ name </li>
|
||||
…
|
||||
</ol>
|
||||
<select data-overlay-add> ← add path
|
||||
<option>Pick a name…</option>
|
||||
<option value=overlay.id>name</option> ← available_overlays
|
||||
…
|
||||
</select>
|
||||
|
||||
static/js/blueprint-overlay-picker.js
|
||||
├─ dragstart/over/leave/drop/end → reorder DOM under [data-overlay-list]
|
||||
├─ click [data-action="remove"] → remove row + sorted-insert <option>
|
||||
├─ change [data-overlay-add] → append <li>, remove <option>
|
||||
└─ refreshEmpty() → toggle [data-overlay-empty][hidden]
|
||||
|
||||
POST /blueprints/<id>
|
||||
form-encoded body: overlay_ids=<id>&overlay_ids=<id>&… (in DOM order)
|
||||
blueprint_routes.update_blueprint_form
|
||||
→ ordered_overlay_ids_from_form (existing; fallback_position branch)
|
||||
→ replace_blueprint_overlays (existing)
|
||||
```
|
||||
|
||||
## Form-contract details
|
||||
|
||||
The new template emits one hidden input per selected row, colocated as a child of the `<li>`:
|
||||
|
||||
```html
|
||||
<li data-overlay-id="3" draggable="true">
|
||||
<span class="overlay-picker-handle">⋮⋮</span>
|
||||
<span class="overlay-picker-name">workshop_maps</span>
|
||||
<button type="button" data-action="remove">×</button>
|
||||
<input type="hidden" name="overlay_ids" value="3">
|
||||
</li>
|
||||
```
|
||||
|
||||
Browser form serialization preserves DOM order across multiple inputs that share a `name`. Werkzeug's `request.form.getlist("overlay_ids")` returns them in submission order. `ordered_overlay_ids_from_form` then assigns each id its enumerate-index position via the `fallback_position` branch (lines 19-31 of `routes/blueprint_routes.py`) and feeds the result to `replace_blueprint_overlays`.
|
||||
|
||||
The JSON path (`POST /blueprints` with `application/json`) already takes `overlay_ids` list order at line 64 of the same file — this spec does not affect it.
|
||||
|
||||
## UI / UX details
|
||||
|
||||
- **Empty state.** When no overlays are selected, a `[data-overlay-empty]` paragraph reads "No overlays selected. Pick one below to add." JS toggles its `hidden` attribute on every list mutation.
|
||||
- **Drag handle.** Visual only (`⋮⋮` glyph). The whole row is `draggable="true"`; the user does not have to grab the handle specifically.
|
||||
- **Drop indicator math.** During `dragover`, compute `event.clientY < rect.top + rect.height/2`; that boolean picks `drop-before` (bar at top) vs `drop-after` (bar at bottom). On `drop`, read which class is set and `insertBefore` or `insertBefore(…, target.nextSibling)` accordingly.
|
||||
- **Sorted insert on remove.** Walk `<select>` children comparing `option.dataset.overlayName` (lowercased) against the removed name; `insertBefore` the new option ahead of the first option whose name sorts later, or append if none.
|
||||
- **Reset select after add.** Set `select.value = ""` so the placeholder reappears after each add.
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Change |
|
||||
|---|---|
|
||||
| `l4d2web/routes/page_routes.py` | Compute `available_overlays`; pass to template. |
|
||||
| `l4d2web/templates/blueprint_detail.html` | Replace overlay table with `<ol>` + `<select>`; add `<script defer>`. |
|
||||
| `l4d2web/static/css/components.css` | Append `.overlay-picker-*` rules. Reuse existing tokens. |
|
||||
| `l4d2web/static/js/blueprint-overlay-picker.js` | New IIFE. ~150 LOC. |
|
||||
| `l4d2web/tests/test_blueprints.py` | Two new GET-page assertions. |
|
||||
| `l4d2web/tests/test_pages.py` | Update `test_blueprint_detail_has_ordered_overlay_form` to match new shape. |
|
||||
|
||||
## Verification
|
||||
|
||||
Manual browser flow (`/blueprints/<id>`):
|
||||
|
||||
1. Initial render shows the saved selection in saved order; dropdown holds the rest. No console errors.
|
||||
2. Drag a row up/down. Focus-colored bar appears at the top or bottom of the hover-target row (depending on which half is hovered). On drop, the row moves; hidden inputs reflect the new order.
|
||||
3. Click ✕ on a row. Row vanishes; the same name appears in the dropdown in alphabetical position.
|
||||
4. Pick from the dropdown. New row appears at the end of the list; the option leaves the dropdown; the placeholder is reselected.
|
||||
5. Save the blueprint, reload. Order survives the round-trip.
|
||||
6. Press Escape mid-drag. Drop indicators clear; source row regains opacity; nothing moved.
|
||||
|
||||
Test commands:
|
||||
|
||||
```
|
||||
pytest l4d2web/tests/test_blueprints.py -q
|
||||
pytest l4d2web/tests -q
|
||||
```
|
||||
|
||||
## Out of scope / future follow-ups
|
||||
|
||||
- **Drop the `overlay_position_<id>` server-side path.** Once no client emits those fields, `ordered_overlay_ids_from_form` collapses to `[int(v) for v in request.form.getlist("overlay_ids")]`. Test `test_form_update_preserves_ordered_overlays_and_multiline_fields` (`l4d2web/tests/test_blueprints.py:220`) gets simplified accordingly.
|
||||
- **Touch-friendly DnD.** Vendor a polyfill (`drag-drop-touch`) or rewrite the picker on pointer events if mobile editing becomes a real use case.
|
||||
- **Keyboard reorder.** Space-to-grab + arrow-keys + ARIA live announcements. Currently only add/remove are keyboard-accessible.
|
||||
- **Filter on the selected list.** Not needed at v1's overlay counts; revisit if blueprints commonly carry 20+ overlays.
|
||||
Loading…
Reference in a new issue