Skip to content
Back to skills

Elm Add Drag Reorder

ASecurity

Add touch/mouse drag-to-reorder (on top of the existing ▲/▼ or ◀/▶ FLIP reorder arrows) to a reorderable list in the Elm SPA (frontends/elm-spa), or debug/extend the shared UI.Drag module. Use when a list already uses UI.Flip.reorderButtons/reorderButtonPair and should also drag, or when drag feels laggy/wrong.

  • 65 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
developmentgonodetestingfrontend

Works with

  • cli

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add JonLatane/jonline --skill elm-add-drag-reorder --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Elm Add Drag Reorder?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Elm Add Drag Reorder
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/jonlatane-elm-add-drag-reorder-rellm/badge)](https://www.skillsdirectory.com/skills/jonlatane-elm-add-drag-reorder-rellm)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: elm-add-drag-reorder
description: Add touch/mouse drag-to-reorder (on top of the existing ▲/▼ or ◀/▶ FLIP reorder arrows) to a reorderable list in the Elm SPA (frontends/elm-spa), or debug/extend the shared UI.Drag module. Use when a list already uses UI.Flip.reorderButtons/reorderButtonPair and should also drag, or when drag feels laggy/wrong.
---

Drag-to-reorder lives in one shared module, `frontends/elm-spa/src/UI/Drag.elm` (read its module doc first), layered on `UI.Flip`'s FLIP animations. Lists already using it: Starred panel (`Shared/StarredPanel.elm`), Accounts + Servers strip (`Shared/AccountsPanel.elm`, rendered in `UI.elm`), custom nav tabs (`ThemeTab/CustomTabsConfiguration.elm`), Federated + Mastodon server editors (`FederationTab.elm`), My Media selected-media strip (`Shared/MyMediaPanel.elm`). Copy one of them — `CustomTabsConfiguration.elm` is the smallest horizontal example, `StarredPanel.elm` the smallest vertical one.

The list must already have: a keyed `.flip-animated-row`/`-column` container, a stable string key per item, a DOM `id` per item (the element carrying `UI.Flip.moveAttributes`), and a per-item `UI.Flip.MoveState` dict + settle msg (what the tap-an-arrow path uses).

## Wiring (7 places)

1. **Model**: `drag : UI.Drag.State` (= `UI.Drag.init`), next to the list's `moveAnimations`. Msg: `DragMsg UI.Drag.Msg`.
2. **Subscription**: `Sub.map DragMsg (UI.Drag.subscriptions model.drag)` (only live while a measurement is in flight).
3. **Config** (rebuild from the model on every call): `{ axis, owner (unique string), domId, keys (current order, as strings), groupOf }`. `groupOf = \_ -> ""` for a free list; give a pinned item its own group id, or split groups where items can't cross (AccountsPanel: main-server accounts vs. rest; pinned main server).
4. **Update branch** `DragMsg m`: call `UI.Drag.update config m model.drag`, then fold the `Output`s in order:
   - `Reordered newKeys` → write the order back where it really lives (`UI.Drag.reorderByKeys keyOf newKeys list`, or for `sortOrder`-backed lists AccountsPanel's `applyDraggedOrder`) **and persist** exactly like the tap path does.
   - `Slide slides` → `moveAnimations = UI.Drag.applySlides SettledMsg slides moveAnimations`.
   - `Cmd.map DragMsg dragCmd`.
5. **Arrows**: pass `dragAttrs = UI.Drag.handleAttrs DragMsg axis key model.drag` to `UI.Flip.reorderButtons`/`reorderButtonPair`, and wrap each arrow's click with `UI.Drag.onClick drag msg` (or `onClickStoppingPropagation` where the arrows sit inside another clickable) so the click after a drag is swallowed. Skip `handleAttrs` (pass `[]`) wherever the arrows are hidden.
6. **Overlay**: add `UI.Drag.overlay DragMsg axis model.drag` once in the list's view — as an extra *keyed child* (`|> List.map (Tuple.pair "drag-overlay")`) of the keyed container, so no wrapper `div` disturbs the flex layout (it's `position: fixed`).
7. Optional: add class `reorder-dragging` to the item while `UI.Drag.isDragging drag key`.

CSS is in `public/style/ui/drag.css` (already linked from `index.html`); nothing per-list needed.

## How it works / gotchas (all learned the hard way)

- **Pointer events, not mouse/touch.** `touch-action: none` on the handle (drag.css) stops touch scrolling; touch events keep targeting the starting element, mouse drags escape the small arrow, so `overlay` (full-screen, only mounted once the drag passes a 4px threshold) catches those.
- **Handlers are attached from the start**, filtered by `buttons > 0`. Attaching them only after the press re-renders loses a fast first move that leaves a 20px arrow before the frame lands (found by a headless drag test that did nothing).
- **Multi-slot jumps**: each pointer move measures *all* items in one `Ports.measureElements` round trip, picks the slot (`targetOrder`: pointer vs neighbors' midpoints along the axis), emits `Reordered`, waits one frame (`Dom.getViewport`), measures again, emits `Slide`s. Same measure/reorder/measure recipe as `UI.Flip.measureElementsCmd` documents — one-frame wait is mandatory or the second measure sees the old layout.
- **Interrupting running slides** works because the port also reports `layoutX/layoutY` (position with the in-flight translate removed, via `DOMMatrixReadOnly`): targets are judged against layout positions, and slide deltas are old *visible* position minus new *layout* position.
- **Linger/lag gotcha**: never re-measure after a "nothing to change" round trip — that looped a full measurement every frame while holding and janked the slide (and scaled with item count). `State.slot` caches the pointer range `( low, high ]` where the target can't change (`slotRange`); inside it the pointermove decoder fails (no Elm message at all) and `checkPointer` is a no-op. If drag still feels eager, the next lever is hysteresis around midpoints.
- **Keys vs. DOM ids**: `keys` are the list's own keys; `domId key` must give the id of the element the measurement should read (the one that carries the move transform).
- **Horizontal lists assume a single row** (only x is compared); no auto-scroll while dragging a scrolling strip (Servers strip clips at the panel edge).
- A `.flip-moving` item turns `display: grid` into `display: flex`, which shrinks short-content rows (Bluesky account) while sliding — `flip.css` has a `.flip-animated-column ... > * { flex: 1 1 auto }` fix; keep it in mind for any new vertical FLIP list.
- **Port replies must be async.** Elm registers a subscription (here `UI.Drag.subscriptions`, live only while a measurement is in flight) *after* running the same update step's commands, so a `measureElements` reply sent synchronously can arrive before it and be silently dropped -- the drag measured once, then never reordered anything (taps were fine; mouse and touch both). `index.html` now answers via `setTimeout(..., 0)`; keep it that way.
- `Ports.elementsMeasured` is one shared untargeted port: `UI.Drag` tags its requests `"drag:" ++ owner` and ignores everyone else's results, so `owner` must be unique per list.

## Testing

- Pure logic: `tests/DragTests.elm` (`targetOrder`, `slotRange`, `reorderByKeys`) — extend when changing it. `make test` = elm-review + elm-test-rs.
- Real behavior needs a browser; use the `run-elm` driver / a throwaway Playwright script (import from `.claude/skills/run-elm/node_modules`): seed `localStorage.rellmStarredPosts` with fake keys (`["a@h1.test", ...]`), open the Starred panel (narrow viewport shows the ⭐ toggle in the nav), `mouse.down` on an arrow, step `mouse.move` in ~15px steps with 16ms waits, assert row order/`localStorage` after `mouse.up`. Count `getBoundingClientRect` calls (monkeypatch) over a few seconds of jiggling inside one slot to check the linger fix: should be ~one measurement pass, not hundreds.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.