--- name: fantasia-drag-drop description: >- List and table drag-and-drop policy for Fantasia Archive: vue-draggable-plus for QList and flat lists; quasar-ui-q-draggable-table (v-draggable-table) for QTable row reorder; @he-tree/vue Draggable for trees. Use when adding reorder UX to settings lists, worlds, tables, or documenting DnD dependencies. --- # Fantasia Archive — drag-and-drop (lists and tables) Full policy: [fa-drag-drop-lists.mdc](../../rules/fa-drag-drop-lists.mdc). ## Policy summary | Surface | Library | Notes | | --- | --- | --- | | **Flat lists** (`QList`, `QItem`, vertical rows) | **`vue-draggable-plus`** | Default for any list reorder | | **`QTable` row reorder** | **`quasar-ui-q-draggable-table`** | **`v-draggable-table`**, boot **`q-draggable-table`** | | **Hierarchical trees** | **`@he-tree/vue`** **`Draggable`** | Built-in DnD — **do not** use vue-draggable-plus on trees | | **Floating / spatial move** | Custom pointer sessions | **`src/scripts/floatingWindows/`** — not this skill | Deps in root **`package.json`**: **`vue-draggable-plus`**, **`quasar-ui-q-draggable-table`**. ## `vue-draggable-plus` (default lists) Docs: [vue-draggable-plus.pages.dev](https://vue-draggable-plus.pages.dev/en/). ### Component example (`QList` body) ```vue ``` ### Composable / target container Sortable root not component root → **`useDraggable(el, list, options)`** or **`v-draggable`** with selector — see upstream **target container** docs. ### Persistence - Reorder updates in-memory array first; on **`@update`** / **`onEnd`**, write domain order (e.g. **`sort_order`**) via **`runFaActionAwait`** or store → **`projectContent`** IPC. - Drag wiring in feature **`scripts/`**; thin **`.vue`** imports composable or **`VueDraggable`** only. ### Vertical tab strips (**`DialogProjectSettings`** worlds list) Vertical category tabs reuse **`faDragDrop_manager`** (**`faVerticalDraggableTabsSortableDragOptions`**, document drag cursor from **`faDragDropDocumentDragCursorWiring.ts`**, **`hideNativeSortableDragGhost`**, **`resolveFaVerticalDraggableTabIdUnderPoint`**) + **`faVerticalDraggableTabListPointerHoverWiring.ts`** + global SCSS **`src/css/theme/custom-components/faVerticalDraggableTabs.scss`**. Reusable element **`FaVerticalDraggableTabList`**. **`DialogProjectSettingsWorldsTabList`**: **`vue-draggable-plus`** + movement threshold so short clicks select tab without drag. Palette swatch reorder: same library in **`DialogProjectSettingsWorldColorPaletteEditor`**. ### Horizontal open document tabs (**`ProjectAppControlBarOpenedTabs`**) Workspace header strip: **`VueDraggable`** horizontal over **`q-route-tab`** children. Reuse Sortable forceFallback + **`hideNativeSortableDragGhost`** / document drag cursor from **`faDragDrop_manager`**. Feature wiring: **`projectAppControlBarTabsSortableDragOptionsWiring`**, edge scroll **`projectAppControlBarTabsDragEdgeScroll*`** (Quasar tabs content **`overflow: hidden`**), wheel **`projectAppControlBarTabsWheelScroll*`**. Domain: **`openedDocumentTabReorder`** → store **`reorderDocumentTabs`**. Native link drag off: **`draggable="false"`** on route tabs. **Pointer hover (not CSS `:hover`):** Sortable sticky `:hover` wrong after reorder → **`FaVerticalDraggableTabList`** tracks **`pointerHoverItemId`** via **`pointermove`** / **`pointerleave`**; tab slot gets **`isPointerHovered`** → **`faVerticalDraggableTabs__tab--pointerHover`**. After drag **`end`**, clear hover then resync from last client point after **`FA_VERTICAL_DRAGGABLE_TABS_SORTABLE_ANIMATION_MS`**. Flat skin: Quasar-like wash; fantasy: gold edge glow on pointer class. #### Vertical draggable tab strips (reusable column) Shared pattern: **left column** draggable tabs + optional **Add** row (master–detail settings dialogs). Not horizontal **`QTabs`** replacement — vertical reorderable list chrome only. **Reference implementations** | SFC | Role | | --- | --- | | **`DialogProjectSettingsWorldsTabList.vue`** | Worlds column; default width **240px** | | **`DialogProjectSettingsDocumentTemplatesTabList.vue`** | Document templates column; **`tab-list-width-px="360"`** | | **`DialogProjectSettingsWorldsTabItem.vue`** / **`DialogProjectSettingsDocumentTemplatesTabItem.vue`** | Single tab row (`faVerticalDraggableTabs__tab` BEM) | **DOM skeleton** (outer host must include class **`faVerticalDraggableTabs`**): ```text .faVerticalDraggableTabs ← :style from buildFaVerticalDraggableTabsRootStyle .faVerticalDraggableTabs__scroll.hasScrollbar VueDraggable.faVerticalDraggableTabs__draggable *TabItem × N ← role=button, class faVerticalDraggableTabs__tab .faVerticalDraggableTabs__divider ← q-separator .faVerticalDraggableTabs__addButtonRow q-btn.faVerticalDraggableTabs__addButton ``` Import global styles once per feature via **`@use`** / **`src=`** on colocated unscoped SCSS (see **`DialogProjectSettings.worldsTabList.unscoped.scss`**). Base tokens: **`src/css/theme/custom-components/_faVerticalDraggableTabs.variables.scss`**. **Layout props** (TabList host SFC; all optional with defaults): | Prop | Default | Maps to CSS variable | | --- | --- | --- | | **`tabListWidthPx`** | **`240`** | **`--fa-vertical-draggable-tabs-column-width`** | | **`tabPadding`** | **`'4px 40px 4px 60px'`** | **`--fa-vertical-draggable-tabs-tab-padding`** | | **`tabTextAlign`** | **`'left'`** | **`--fa-vertical-draggable-tabs-tab-text-align`** | | **`tabJustifyContent`** | **`'flex-start'`** | **`--fa-vertical-draggable-tabs-tab-justify-content`** | | **`tabLabelTextTransform`** | **`'none'`** | **`--fa-vertical-draggable-tabs-tab-label-text-transform`** | | **`tabLabelFontSize`** | **`'14px'`** | **`--fa-vertical-draggable-tabs-tab-label-font-size`** | | **`dense`** | **`false`** (document templates TabList default **`true`**) | **`--fa-vertical-draggable-tabs-tab-min-height`** (**`36px`** when dense; SCSS fallback **`48px`**) | Types: **`types/I_faVerticalDraggableTabs.ts`**. Defaults + **`buildFaVerticalDraggableTabsRootStyle`**: import from **`faDragDrop_manager`**. **TabList script pattern** ```ts import { FA_VERTICAL_DRAGGABLE_TABS_TAB_JUSTIFY_CONTENT_DEFAULT, FA_VERTICAL_DRAGGABLE_TABS_TAB_LABEL_FONT_SIZE_DEFAULT, FA_VERTICAL_DRAGGABLE_TABS_TAB_LABEL_TEXT_TRANSFORM_DEFAULT, FA_VERTICAL_DRAGGABLE_TABS_TAB_PADDING_DEFAULT, FA_VERTICAL_DRAGGABLE_TABS_TAB_TEXT_ALIGN_DEFAULT, buildFaVerticalDraggableTabsRootStyle } from 'app/src/scripts/faDragDrop/faDragDrop_manager' const tabListRootStyle = computed(() => buildFaVerticalDraggableTabsRootStyle({ columnWidthPx: props.tabListWidthPx, tabDense: props.dense, tabJustifyContent: props.tabJustifyContent, tabLabelFontSize: props.tabLabelFontSize, tabLabelTextTransform: props.tabLabelTextTransform, tabPadding: props.tabPadding, tabTextAlign: props.tabTextAlign })) ``` **Drag wiring** (both reference TabLists): - **`faVerticalDraggableTabsSortableDragOptions`** on **`VueDraggable`** - **`touch-start-threshold="5"`** — tap-to-select without drag - **`@start`** → **`applyFaVerticalDraggableTabsDocumentDragCursor`** (from **`faDragDrop_manager`**) - **`@end`** → **`clearFaVerticalDraggableTabsDocumentDragCursor`** + emit reordered array - Root **`computed`** class **`faVerticalDraggableTabs--listDragging`** while dragging - Tab item modifiers: **`--active`**, **`--dragging`**, **`--error`** **Tests**: **`src/scripts/faDragDrop/_tests/buildFaVerticalDraggableTabsRootStyle.vitest.test.ts`**; TabList Vitest stubs **`VueDraggable`**; Playwright uses **`data-test-locator`** on list, tabs, add button. ## `quasar-ui-q-draggable-table` (`QTable` rows) Registered globally via **`src/boot/q-draggable-table.ts`** (**`quasar.config.ts`** **`boot`**). ```vue ``` ```ts function onDrop (from: number, to: number): void { const next = [...rows.value] next.splice(to, 0, next.splice(from, 1)[0]) rows.value = next // Persist row order } ``` - Import **`quasar-ui-q-draggable-table/dist/index.css`** from boot manager only (no per-SFC duplicate). - Avoid **`virtual-scroll`** on same **`QTable`** when using row drag. - Upstream: [github.com/bd2051/q-draggable-table](https://github.com/bd2051/q-draggable-table) ## Trees — use he-tree, not vue-draggable-plus Nested hierarchies → [fantasia-he-tree](../fantasia-he-tree/SKILL.md): **`Draggable`** from **`@he-tree/vue`** owns reorder semantics. ### World template layout tree **Project Settings** world template layout uses he-tree **`Draggable`** — see [fa-drag-drop-lists.mdc](../../rules/fa-drag-drop-lists.mdc) **World template layout tree** for component map, commit policy, wiring, validation, rename menu, tests. Do not duplicate that narrative here. ## Tests - **Vitest**: mock **`vue-draggable-plus`** when mount-only smoke; test reorder helpers in **`scripts/_tests`** with pure array moves. - **Playwright**: **`data-test-locator`** on handles; pointer APIs per [playwright-tests.mdc](../../rules/playwright-tests.mdc). ## Related - [fa-drag-drop-lists.mdc](../../rules/fa-drag-drop-lists.mdc) - [fa-he-tree.mdc](../../rules/fa-he-tree.mdc) - [fantasia-floating-windows](../fantasia-floating-windows/SKILL.md) — spatial window drag