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>
57 lines
1.9 KiB
TypeScript
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];
|
|
}
|