feat(frontend): one grip reorder affordance for tags and trigger words

Model tags could already be reordered by dragging a chip, but the only
hint was a `cursor: grab` on `.metadata-item` — a hover-only, mouse-only
signal that also leaked into the bulk add-tags modal, where the chips are
not sortable at all. Trigger words could not be reordered, and their
order matters: "Copy Trigger Words" and the insert-into-node action join
the array as-is to build a prompt.

Both editors now share one vocabulary: a `⠿` grip that appears whenever
the list has something to order, plus `Alt + arrow` keyboard moves with
an aria-live announcement. Whether the chip body is draggable is a
property of the item rather than of the feature:

- tags have no click action of their own, so the whole chip stays
  draggable (`handleSelector: null`), with a 5px threshold so a click on
  the grip only focuses it
- trigger words keep click-to-edit on the body, so a drag starts from the
  grip only
- the grip is the element that opts out of touch scrolling
  (`touch-action: none`), so touch users drag by the grip in both editors
- reordering is offered only while editing: trigger words reveal the grip
  from `.edit-mode`, tags from the edit container, which stays hidden
  outside edit mode

The drag engine moves out of ModelTags.js into shared/pointerSort.js,
which now marks sortable containers with `pointer-sort-enabled` so only
lists that really sort show the grab cursor. Labels, the sortable flag,
the keyboard handler and the live region live in
shared/reorderSupport.js, and both editors render the same three
`common.reorder.*` keys (translations follow in the next commit).

Two inherited engine bugs are fixed on the way: the drop position was
only settled when an animation frame was still pending, so a fast drag
(or one that started by crossing the threshold) fell back into its
original slot; and the guard that stops a drop from triggering the chip's
own click handler was removed on a timer, swallowing unrelated clicks
until the next task.
This commit is contained in:
Will Miao
2026-09-16 08:22:46 +08:00
parent c6c44b741a
commit 01137eed88
18 changed files with 1552 additions and 210 deletions
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "Speichern",
"cancel": "Abbrechen",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "Cancel",
"confirm": "Confirm",
"reorder": {
"dragHandle": "Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "Reorder {item}, position {position} of {total}",
"announcement": "Moved to position {position} of {total}"
},
"actions": {
"save": "Save",
"cancel": "Cancel",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "Cancelar",
"confirm": "Confirmar",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "Guardar",
"cancel": "Cancelar",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "Annuler",
"confirm": "Confirmer",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "Enregistrer",
"cancel": "Annuler",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "ביטול",
"confirm": "אישור",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "שמירה",
"cancel": "ביטול",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "キャンセル",
"confirm": "確認",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "保存",
"cancel": "キャンセル",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "취소",
"confirm": "확인",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "저장",
"cancel": "취소",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "Отмена",
"confirm": "Подтвердить",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "Сохранить",
"cancel": "Отмена",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "取消",
"confirm": "确认",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "保存",
"cancel": "取消",
+5
View File
@@ -2,6 +2,11 @@
"common": {
"cancel": "取消",
"confirm": "確認",
"reorder": {
"dragHandle": "[TODO: Translate] Drag to reorder (Alt + ↑/↓)",
"ariaLabel": "[TODO: Translate] Reorder {item}, position {position} of {total}",
"announcement": "[TODO: Translate] Moved to position {position} of {total}"
},
"actions": {
"save": "儲存",
"cancel": "取消",