diff --git a/locales/de.json b/locales/de.json index 1aaa8393..3a0b60f9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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", diff --git a/locales/en.json b/locales/en.json index 19988d29..ca3d2ffd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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", diff --git a/locales/es.json b/locales/es.json index 30c11a2a..f53b282c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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", diff --git a/locales/fr.json b/locales/fr.json index e746e497..94166bfe 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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", diff --git a/locales/he.json b/locales/he.json index 36a106a1..c7a59c2d 100644 --- a/locales/he.json +++ b/locales/he.json @@ -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": "ביטול", diff --git a/locales/ja.json b/locales/ja.json index e76ab85b..13b1ea6c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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": "キャンセル", diff --git a/locales/ko.json b/locales/ko.json index 73acbb39..74830c2f 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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": "취소", diff --git a/locales/ru.json b/locales/ru.json index 279207a0..d7f6060d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Отмена", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index f9a631d6..4f2145be 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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": "取消", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 72d7b48b..079e9212 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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": "取消", diff --git a/static/css/components/shared/edit-metadata.css b/static/css/components/shared/edit-metadata.css index 0c2a578c..ad790d55 100644 --- a/static/css/components/shared/edit-metadata.css +++ b/static/css/components/shared/edit-metadata.css @@ -92,41 +92,126 @@ border-radius: var(--border-radius-xs); padding: 4px 8px; position: relative; - cursor: grab; transition: transform 0.18s ease; } -.metadata-item:active { +/* --- Shared chip reordering (tags + trigger words) ------------------------ */ +/* Chips in a list that is actually sortable advertise the grab gesture only + then, so lists that cannot be reordered never lie about it. */ + +.metadata-items.pointer-sort-enabled .metadata-item { + cursor: grab; +} + +.metadata-items.pointer-sort-enabled .metadata-item:active { cursor: grabbing; } -.metadata-item-dragging { +/* Grip handle: always in the DOM, revealed when the list is sortable */ +.reorder-handle { + display: none; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 0; + margin-left: -2px; + border: none; + background: transparent; + color: var(--text-color); + opacity: 0.4; + font-size: 0.8em; + line-height: 1; + cursor: grab; + /* Keep a touch drag on the handle from scrolling the surrounding panel */ + touch-action: none; + transition: opacity 0.2s ease, color 0.2s ease; +} + +.has-sortable-words .reorder-handle { + display: inline-flex; +} + +/* Tag chips have no flex gap (unlike trigger word tags), so the grip needs its + own spacing before the tag text */ +.metadata-item .reorder-handle { + margin-right: 4px; +} + +.reorder-handle:hover, +.reorder-handle:focus-visible { + opacity: 0.9; + color: var(--lora-accent); +} + +.reorder-handle:focus-visible { + outline: 2px solid var(--lora-accent); + outline-offset: 1px; + border-radius: 2px; +} + +.reorder-handle:active { + cursor: grabbing; +} + +/* Hint shown in the edit controls row while reordering is available */ +.reorder-hint { + display: none; + align-items: center; + gap: 4px; + margin-right: auto; + font-size: 0.75em; + color: var(--text-color); + opacity: 0.6; + white-space: nowrap; +} + +.has-sortable-words .reorder-hint { + display: inline-flex; +} + +/* Snapped-to-grid transition for the remaining chips while dragging */ +.reorder-sorting > * { + transition: transform 0.18s ease; +} + +/* The lifted chip that follows the pointer */ +.reorder-dragging { box-shadow: var(--shadow-dialog); cursor: grabbing; opacity: 0.95; transition: none; } -.metadata-item-placeholder { +/* Drop target left behind by the lifted chip */ +.reorder-placeholder { border: 1px dashed var(--lora-accent); border-radius: var(--border-radius-xs); background: rgba(255, 255, 255, 0.1); pointer-events: none; } -.metadata-items-sorting .metadata-item { - transition: transform 0.18s ease; -} - -body.metadata-drag-active { +body.reorder-drag-active { user-select: none; cursor: grabbing; } -body.metadata-drag-active * { +body.reorder-drag-active * { cursor: grabbing !important; } +/* Screen-reader-only live region announcing reorder moves */ +.reorder-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .metadata-item-content { color: var(--lora-accent) !important; font-size: 0.85em; diff --git a/static/js/components/shared/ModelTags.js b/static/js/components/shared/ModelTags.js index b6226626..4b6691ec 100644 --- a/static/js/components/shared/ModelTags.js +++ b/static/js/components/shared/ModelTags.js @@ -7,6 +7,12 @@ import { getModelApiClient } from '../../api/modelApiFactory.js'; import { translate } from '../../utils/i18nHelpers.js'; import { getPriorityTagSuggestions } from '../../utils/priorityTagHelpers.js'; import { state } from '../../state/index.js'; +import { enablePointerSort } from './pointerSort.js'; +import { + createReorderSupport, + renderReorderHandle, + renderReorderHint, +} from './reorderSupport.js'; const MODEL_TYPE_SUGGESTION_KEY_MAP = { loras: 'lora', @@ -18,16 +24,23 @@ const MODEL_TYPE_SUGGESTION_KEY_MAP = { }; const METADATA_ITEM_SELECTOR = '.metadata-item'; const METADATA_ITEMS_CONTAINER_SELECTOR = '.metadata-items'; -const METADATA_ITEM_DRAGGING_CLASS = 'metadata-item-dragging'; -const METADATA_ITEM_PLACEHOLDER_CLASS = 'metadata-item-placeholder'; -const METADATA_ITEMS_SORTING_CLASS = 'metadata-items-sorting'; -const BODY_DRAGGING_CLASS = 'metadata-drag-active'; +const METADATA_DRAG_HANDLE_SELECTOR = '.reorder-handle'; + +/** + * Tag items have no click action of their own, so the whole chip stays + * draggable (handleSelector is null); the small threshold keeps a click on the + * grip from starting a drag (it just focuses the grip). Touch users drag by the + * grip, which is the element that opts out of scrolling via touch-action. + */ +const TAG_SORT_CONFIG = { + itemSelector: METADATA_ITEM_SELECTOR, + dragThreshold: 5, +}; let activeModelTypeKey = ''; let priorityTagSuggestions = []; let priorityTagSuggestionsLoaded = false; let priorityTagSuggestionsPromise = null; -let activeTagDragState = null; // Configurable options for tag editing (set by setupTagEditMode) let tagEditOptions = { @@ -423,6 +436,7 @@ function createTagEditUI(currentTags, editBtnHTML = '') {