Files
ai-agent/frontend/src/lib/listOrder.ts
Gabriel Vidal 0bfcd1ac37 feat(ui): add HoldEditable — hold an item to pick it up and rearrange the list
Press and hold any item of a group for 1.4s and it lifts out of the flow and
follows the pointer while the rest of the group jumps in place (each with its
own random phase). Dragging past a neighbour's midpoint hands over that slot;
releasing drops the item there, ends edit mode and commits the new order.

Pointer-events throughout (one code path for mouse and touch), a body-level
portal for the lifted item so no `overflow: hidden` ancestor can clip it, and
FLIP for the shuffle. Layout-agnostic: the group's flex/grid classes come from
the caller, so the same component drives a horizontal navbar and a vertical
panel of cards.

`useListOrder(name, items, getKey)` is the companion for lists hard-coded in the
app: it persists only the order, as ids, under `settings.listOrders[name]`, and
reconciles it against the code list on read — so adding, renaming or removing an
entry in a later release can never strand or duplicate an item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:29:08 +02:00

57 lines
1.9 KiB
TypeScript

import { useCallback, useMemo } from "react";
import { useSettings } from "@/settings";
/**
* Make a hard-coded list user-arrangeable.
*
* The canonical list stays in code (that's where the labels, icons and routes
* belong); only the *order* is persisted, as ids, under `settings.listOrders`.
* On read the saved order is reconciled against the code list: ids the app no
* longer ships are dropped and entries the saved order never heard of are
* appended in their code order. So adding, renaming or removing a list entry in
* a later release can neither strand a saved order nor duplicate an item.
*
* Pair it with {@link HoldEditable}, whose `onReorder` takes exactly the setter
* this returns:
*
* ```tsx
* const [tabs, setTabs] = useListOrder("nav", TABS, (t) => t.id);
* <HoldEditable items={tabs} getKey={(t) => t.id} onReorder={setTabs}>…</HoldEditable>
* ```
*
* @param name Stable key for this list inside `settings.listOrders`.
* @param items The canonical list, in its default order.
* @param getKey Stable id of an item.
*/
export function useListOrder<T>(
name: string,
items: T[],
getKey: (item: T) => string,
): [T[], (next: T[]) => void] {
const saved = useSettings((s) => s.listOrders?.[name]);
const setListOrder = useSettings((s) => s.setListOrder);
const ordered = useMemo(() => {
if (!saved?.length) return items;
const byId = new Map(items.map((it) => [getKey(it), it]));
const out: T[] = [];
const seen = new Set<string>();
for (const id of saved) {
const it = byId.get(id);
if (it !== undefined && !seen.has(id)) {
out.push(it);
seen.add(id);
}
}
for (const it of items) if (!seen.has(getKey(it))) out.push(it);
return out;
}, [saved, items, getKey]);
const commit = useCallback(
(next: T[]) => setListOrder(name, next.map(getKey)),
[name, getKey, setListOrder],
);
return [ordered, commit];
}