style(layout): the whole row is the drag handle
A list whose rows can only be moved by a sixteen-pixel grip is a list most people conclude cannot be moved at all. The grip stays as the sign that it can, and the row itself now carries the gesture — plus a line on the edge the row would take, because the question a dragging hand asks is 'between which two', which a highlighted target does not answer.
This commit is contained in:
+2
-2
@@ -3,10 +3,10 @@
|
||||
"version": "0.27.10",
|
||||
"date": "",
|
||||
"en": [
|
||||
"Widget order (Settings → Appearance): the row to the right of the entry can be rearranged by dragging. QSO entry and the F1-F5 panel head the list, locked — they are not part of that row and nothing should be allowed to push what you type into behind a rotator dial. A widget you have switched off keeps its place and comes back where you left it, and the main view follows as you drag."
|
||||
"Widget order (Settings → Appearance): the row to the right of the entry can be rearranged by dragging — the whole row is the handle, and a line shows where it will land. QSO entry and the F1-F5 panel head the list, locked — they are not part of that row and nothing should be allowed to push what you type into behind a rotator dial. A widget you have switched off keeps its place and comes back where you left it, and the main view follows as you drag."
|
||||
],
|
||||
"fr": [
|
||||
"Ordre des widgets (Réglages → Apparence) : la rangée à droite de la saisie se réorganise par glisser-déposer. La saisie du QSO et le panneau F1-F5 ouvrent la liste, verrouillés — ils ne font pas partie de cette rangée, et rien ne doit pouvoir repousser ce dans quoi vous tapez derrière une boussole de rotor. Un widget désactivé garde sa place et revient là où vous l’aviez laissé, et la vue principale suit pendant que vous glissez."
|
||||
"Ordre des widgets (Réglages → Apparence) : la rangée à droite de la saisie se réorganise par glisser-déposer — toute la ligne se saisit, et un trait montre où elle atterrira. La saisie du QSO et le panneau F1-F5 ouvrent la liste, verrouillés — ils ne font pas partie de cette rangée, et rien ne doit pouvoir repousser ce dans quoi vous tapez derrière une boussole de rotor. Un widget désactivé garde sa place et revient là où vous l’aviez laissé, et la vue principale suit pendant que vous glissez."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -333,6 +333,10 @@ function WidgetOrderSection() {
|
||||
const [order, setOrder] = useState<string[]>(readWidgetOrder);
|
||||
const dragKey = useRef<string | null>(null);
|
||||
const [dragging, setDragging] = useState<string | null>(null);
|
||||
// Where the row would land. Drawn as a line above the target rather than by
|
||||
// colouring it: the question a dragging hand asks is "between which two", and
|
||||
// a highlighted row answers a different one.
|
||||
const [over, setOver] = useState<string | null>(null);
|
||||
|
||||
const commit = (keys: string[]) => {
|
||||
setOrder(keys);
|
||||
@@ -364,18 +368,32 @@ function WidgetOrderSection() {
|
||||
</div>
|
||||
))}
|
||||
{order.map((k) => (
|
||||
<div key={k}
|
||||
onDragOver={(e) => { if (dragKey.current) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } }}
|
||||
onDrop={(e) => { if (dragKey.current) { e.preventDefault(); moveTo(dragKey.current, k); } }}
|
||||
className={cn('flex items-center gap-2 rounded-md border border-border bg-card px-2 py-1.5 text-sm',
|
||||
dragging === k && 'opacity-50')}>
|
||||
<span draggable
|
||||
onDragStart={(e) => { dragKey.current = k; setDragging(k); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragEnd={() => { dragKey.current = null; setDragging(null); }}
|
||||
title={t('wo.drag')}
|
||||
className="cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-foreground">
|
||||
<GripVertical className="size-4" />
|
||||
</span>
|
||||
// The WHOLE row is the handle, not the grip alone: a list whose rows
|
||||
// can only be moved by a 16-pixel icon is a list most people conclude
|
||||
// cannot be moved. The grip stays as the sign that it can.
|
||||
<div key={k} draggable
|
||||
onDragStart={(e) => { dragKey.current = k; setDragging(k); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragEnd={() => { dragKey.current = null; setDragging(null); setOver(null); }}
|
||||
onDragOver={(e) => {
|
||||
if (!dragKey.current) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
if (over !== k) setOver(k);
|
||||
}}
|
||||
onDragLeave={() => { if (over === k) setOver(null); }}
|
||||
onDrop={(e) => {
|
||||
if (!dragKey.current) return;
|
||||
e.preventDefault();
|
||||
moveTo(dragKey.current, k);
|
||||
setOver(null);
|
||||
}}
|
||||
title={t('wo.drag')}
|
||||
className={cn('flex items-center gap-2 rounded-md border bg-card px-2 py-1.5 text-sm select-none',
|
||||
'cursor-grab active:cursor-grabbing transition-shadow',
|
||||
dragging === k ? 'opacity-50 border-primary shadow-lg' : 'border-border hover:border-foreground/30',
|
||||
// The landing line, on the edge the row would take.
|
||||
over === k && dragging !== k && 'shadow-[inset_0_3px_0_0_var(--primary)]')}>
|
||||
<GripVertical className="size-4 shrink-0 text-muted-foreground/50" />
|
||||
<span className="flex-1 min-w-0 truncate">{t(WIDGET_LABELS[k] ?? k)}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user