One dialog per thing, and rows dragged into a chapter
A separator had three menu entries — rename, colour, line — plus an input that appeared in the row itself: three shapes of interaction for one small thing, and none of them showed the result. Choosing "dotted" from a list of words is a guess. It is one Edit now, and the dialog draws the line as it will look with every shape drawn beside its name, from the very rules the list draws with: a sample that can disagree with the result is worse than none. A chapter goes the same way — name, mark and colour in one dialog, the same one whether it is being made or changed, with the row drawn at the top. Asked from three entries, getting a new chapter to look right took three visits. The name dialog it used is gone. A row is put into a chapter by dragging it onto it: the chapter lights up and takes what is dropped rather than growing a line above it, since a chapter is a place and not a neighbour. Notes and separators both, and the "+ Separator" inside a chapter goes with it — a second way to make the same thing in a second place is a second thing to keep straight. A link now wears the mark of the note it points at, in the text and in the @ menu. It travels when the note's mark changes, as the label already did. Two alignment faults: a task list's checkbox was nudged down by a fixed 0.35em measured once against one font, so it floated above the line it belonged to — it is centred in a box one line tall now. And the title bar's mark was spaced like the buttons at the far end and grew with its contents, so picking an emoji shifted the title sideways. Fourteen suites green: 374 browser checks, 241 unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59844046c4
commit
d68b0775c2
20 files changed
+1185
-594
No files matched your search
@@ -23,31 +23,50 @@ const MOUSE_THRESHOLD = 5;
|
||||
export interface ReorderDrag {
|
||||
/** The row the pointer is over, for the drop indicator. */
|
||||
overId: string | null;
|
||||
/** A row that would *take* what is being carried, rather than sit next to it. */
|
||||
intoId: string | null;
|
||||
/** The row being carried, so it can be shown as lifted. */
|
||||
draggingId: string | null;
|
||||
/** Spread onto each row; `id` is what gets reordered. */
|
||||
rowProps: (id: string) => { "data-reorder-id": string; onPointerDown: (e: React.PointerEvent) => void };
|
||||
/**
|
||||
* Spread onto each row; `id` is what gets reordered.
|
||||
*
|
||||
* `into` marks a row that is a place rather than a neighbour — a chapter.
|
||||
* Dropping on one files what is carried into it instead of putting it above.
|
||||
*/
|
||||
rowProps: (
|
||||
id: string,
|
||||
into?: boolean
|
||||
) => {
|
||||
"data-reorder-id": string;
|
||||
"data-reorder-into"?: string;
|
||||
onPointerDown: (e: React.PointerEvent) => void;
|
||||
};
|
||||
/** Spread onto the grip inside a row. */
|
||||
gripProps: { "data-reorder-grip": string };
|
||||
}
|
||||
|
||||
export function useReorderDrag(
|
||||
enabled: boolean,
|
||||
reorder: (dragId: string, targetId: string) => void
|
||||
reorder: (dragId: string, targetId: string) => void,
|
||||
fileInto?: (dragId: string, targetId: string) => void
|
||||
): ReorderDrag {
|
||||
const [overId, setOverId] = useState<string | null>(null);
|
||||
const [intoId, setIntoId] = useState<string | null>(null);
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
|
||||
// Everything the pointer handlers need, kept out of state: they run far more
|
||||
// often than anything needs to be redrawn.
|
||||
const drag = useRef<{ id: string; x: number; y: number; active: boolean; fromGrip: boolean } | null>(null);
|
||||
const overRef = useRef<string | null>(null);
|
||||
const intoRef = useRef<string | null>(null);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
drag.current = null;
|
||||
overRef.current = null;
|
||||
intoRef.current = null;
|
||||
setDraggingId(null);
|
||||
setOverId(null);
|
||||
setIntoId(null);
|
||||
document.body.style.removeProperty("user-select");
|
||||
}, []);
|
||||
|
||||
@@ -87,9 +106,18 @@ export function useReorderDrag(
|
||||
const row = el?.closest?.("[data-reorder-id]") as HTMLElement | null;
|
||||
const target = row?.dataset.reorderId ?? null;
|
||||
const next = target && target !== d.id ? target : null;
|
||||
if (next !== overRef.current) {
|
||||
overRef.current = next;
|
||||
setOverId(next);
|
||||
/* A chapter takes what is dropped on it rather than standing beside it,
|
||||
* which is what "drag it into the chapter" means. Filing is only offered
|
||||
* when there is somewhere to file *to*. */
|
||||
const into = next && row?.dataset.reorderInto !== undefined && fileInto ? next : null;
|
||||
if (into !== intoRef.current) {
|
||||
intoRef.current = into;
|
||||
setIntoId(into);
|
||||
}
|
||||
const beside = into ? null : next;
|
||||
if (beside !== overRef.current) {
|
||||
overRef.current = beside;
|
||||
setOverId(beside);
|
||||
}
|
||||
if (e.cancelable) e.preventDefault();
|
||||
};
|
||||
@@ -97,7 +125,9 @@ export function useReorderDrag(
|
||||
const up = () => {
|
||||
const d = drag.current;
|
||||
const target = overRef.current;
|
||||
if (d?.active && target && target !== d.id) reorder(d.id, target);
|
||||
const into = intoRef.current;
|
||||
if (d?.active && into && into !== d.id) fileInto?.(d.id, into);
|
||||
else if (d?.active && target && target !== d.id) reorder(d.id, target);
|
||||
stop();
|
||||
};
|
||||
|
||||
@@ -109,7 +139,7 @@ export function useReorderDrag(
|
||||
window.removeEventListener("pointerup", up);
|
||||
window.removeEventListener("pointercancel", stop);
|
||||
};
|
||||
}, [enabled, reorder, stop]);
|
||||
}, [enabled, reorder, fileInto, stop]);
|
||||
|
||||
// A drag that outlives its list would leave the page unselectable.
|
||||
useEffect(
|
||||
@@ -121,8 +151,13 @@ export function useReorderDrag(
|
||||
|
||||
return {
|
||||
overId,
|
||||
intoId,
|
||||
draggingId,
|
||||
rowProps: (id: string) => ({ "data-reorder-id": id, onPointerDown: onPointerDown(id) }),
|
||||
rowProps: (id: string, into = false) => ({
|
||||
"data-reorder-id": id,
|
||||
...(into ? { "data-reorder-into": "" } : {}),
|
||||
onPointerDown: onPointerDown(id),
|
||||
}),
|
||||
gripProps: { "data-reorder-grip": "" },
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user