diff --git a/frontend/src/components/ActivityRow.svelte b/frontend/src/components/ActivityRow.svelte
index 480fda5..6ea08f4 100644
--- a/frontend/src/components/ActivityRow.svelte
+++ b/frontend/src/components/ActivityRow.svelte
@@ -18,16 +18,10 @@
/** Called when the row updates its own activity (e.g. after a heart
* toggle). The parent should patch its list so the change persists. */
onChanged?: (a: Activity) => void;
- /** Render a drag handle on this row. The parent owns the actual drag
- * wiring via svelte-dnd-action; the handle's pointerdown/touchstart
- * just flips the parent's dragDisabled to false (one drag at a time). */
- draggable?: boolean;
- onDragHandleStart?: () => void;
}
let {
activity, privateCleartext = null,
onDeleted, onEdit, onChanged,
- draggable = false, onDragHandleStart,
}: Props = $props();
// Fallback decrypt for the case where Home hasn't pre-computed yet (e.g.
@@ -201,22 +195,7 @@
-
diff --git a/frontend/src/components/Home.svelte b/frontend/src/components/Home.svelte
index d9ce8a0..583865e 100644
--- a/frontend/src/components/Home.svelte
+++ b/frontend/src/components/Home.svelte
@@ -89,16 +89,22 @@
].some((s) => s.toLowerCase().includes(needle));
}
- // The list is now unified — one column, sorted by sort_position. The
- // server's COALESCE(custom, -created_at) handles both manually-positioned
- // and not-yet-touched rows. Bookmark/friends/private/semi/public are all
- // intermingled; their vis-badges tell the viewer which is which.
+ // The list is unified — one column. On the public landing ("/", which
+ // anonymous and logged-in users both see), the order is strictly
+ // newest-first regardless of any personal sort the viewer may have
+ // applied on /home. On the authenticated dashboard, sort by the
+ // viewer's effective sort_position (custom if dragged, else
+ // -created_at, both surfaced by the server).
const filtered = $derived(
activities
.filter((a) => !publicOnly || a.visibility !== 'private')
.filter((a) => matchesQuery(a, query))
.slice()
- .sort((a, b) => a.sort_position - b.sort_position),
+ .sort((a, b) =>
+ publicOnly
+ ? b.created_at - a.created_at
+ : a.sort_position - b.sort_position,
+ ),
);
// --- Drag-and-drop reordering --------------------------------------------
@@ -106,33 +112,34 @@
// We hand it the `filtered` array and let it dispatch `consider`/`finalize`
// CustomEvents as the user drags.
//
- // Drag-handle-only behaviour: the library doesn't have a built-in "handle"
- // option, so we use the standard recipe — keep dragDisabled=true by
- // default, flip to false on pointerdown/touchstart on the handle, flip
- // back after finalize. Clicks on the title/buttons inside a card don't
- // initiate drag because dragDisabled is true everywhere else.
+ // dragDisabled is a pure function of auth state — NOT a handle toggle.
+ // An earlier version gated it on handle pointerdown, which had two bugs:
+ // 1. Desktop required two gestures (handle to "enable", then card to
+ // drag) because pointerdown flipped the flag too late for the
+ // already-in-progress mouse gesture.
+ // 2. Keyboard reorder relies on dragDisabled being false at focus time
+ // — gating it broke Space/arrow-key lifting.
+ // The library's built-in distance threshold prevents accidental drags
+ // from clicks. Form controls and links inside cards aren't draggable.
+ // The drag-handle icon stays as a visual affordance only.
- /** Items the library is currently working with — a copy of `filtered` it
- * can mutate during the drag for the visual transition. Re-derived from
- * `filtered` between drags. */
+ /** Items the library mutates during the drag. Re-synced from `filtered`
+ * between drags via the $effect below. */
let dndItems: Activity[] = $state([]);
- /** Disable drag globally unless the user has just tapped a drag handle. */
- let dragDisabled = $state(true);
+ /** Drag is disabled for anonymous viewers and on the public landing. */
+ const dragDisabled = $derived(!session.user || publicOnly);
+
+ /** Set to true while a drag is in progress, so the resync effect doesn't
+ * fight the library by overwriting its in-flight item ordering. */
+ let dragActive = $state(false);
$effect(() => {
- // Keep dndItems in sync with the source-of-truth `filtered` array
- // whenever it changes outside of an active drag (e.g. after a new
- // activity is created, or the search filter shrinks the list).
- if (dragDisabled) dndItems = filtered.slice();
+ if (!dragActive) dndItems = filtered.slice();
});
- function activateDrag() {
- if (!session.user) return;
- dragDisabled = false;
- }
-
function handleConsider(e: CustomEvent<{ items: Activity[] }>) {
// Library asks us to render this transient ordering during the drag.
+ dragActive = true;
dndItems = e.detail.items;
}
@@ -140,7 +147,7 @@
const next = e.detail.items;
const movedId = e.detail.info.id;
dndItems = next;
- dragDisabled = true;
+ dragActive = false;
// Find where the moved item landed and compute a midpoint position
// between its (new) neighbours' sort_positions.
@@ -267,8 +274,6 @@
onDeleted={onDeleted}
onEdit={(act) => (editing = act)}
onChanged={onChanged}
- draggable={!!session.user && !publicOnly}
- onDragHandleStart={activateDrag}
/>
{/each}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 4f8781f..6aebe7b 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -294,30 +294,14 @@ nav.top h1::after {
override the ::before-emoji default that .vis-badge applies otherwise. */
.vis-badge.bookmarked::before { content: none; }
-/* Drag handle on each activity row in the unified list view. The button
- itself is what `draggable="true"` is bound to so users have a clear
- target; the rest of the card stays selectable. */
-.drag-handle {
- min-height: 24px !important;
- min-width: 28px !important;
- padding: 0 0.3rem !important;
- cursor: grab;
- background: transparent !important;
- border: 1px solid transparent !important;
- color: var(--muted);
- font-size: 1.1rem;
- line-height: 1;
- user-select: none;
- align-self: stretch;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-.drag-handle:hover {
- background: var(--accent-soft) !important;
- border-color: var(--border) !important;
-}
-.drag-handle:active { cursor: grabbing; }
+/* Reorder affordance for the unified activity list. svelte-dnd-action
+ wraps each item in a div with role="listitem"; the card becomes
+ grabbable on the empty regions while buttons and links inside keep
+ their native cursor. */
+[role="listitem"] article.card { cursor: grab; }
+[role="listitem"] article.card:active { cursor: grabbing; }
+[role="listitem"] article.card button,
+[role="listitem"] article.card a { cursor: pointer; }
.error { color: var(--danger); margin-top: 0.5rem; }