managarten/packages/shared-ui/src/dnd/drag-state.svelte.ts
Till JS 8409f8a8a2 feat(shared-ui, todo): add cross-type drag & drop system with tag enrichment
Implement a two-layer DnD system in @manacore/shared-ui/dnd that coexists
with svelte-dnd-action (same-type reordering):

- Layer 1 (Pointer Events): dragSource + dropTarget actions for cross-type
  drags (e.g. Tag → Task). Mobile-first with long-press (300ms) and haptic
  feedback.
- Layer 2 (Passive Overlay): passiveDropZone action detects when
  svelte-dnd-action drags hover over external targets (e.g. Task → Tag pill,
  Task → Trash zone).
- DragPreview: floating pill that follows the finger/cursor during Layer 1
  drags.
- ActionZone: auto-appearing drop zones (trash, archive) during any drag.

Integrate into Todo app:
- TagStrip pills: draggable (dragSource) + accept tasks (passiveDropZone)
- TaskList items: accept tags (dropTarget) + register drags for passive layer
- ViewColumn + FokusLayout: register svelte-dnd-action drags for passive layer
- Layout: DragPreview + ActionZone (trash) added, tasks enriched with resolved
  label objects from shared tags so tag badges actually render on TaskItem.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:00:25 +02:00

114 lines
3.1 KiB
TypeScript

/**
* Global reactive drag state.
*
* Shared between dragSource, dropTarget, passiveDropZone, and UI components
* (DragPreview, ActionZone) so they can coordinate visuals.
*/
import type { DragPayload, DragType } from './types';
// ── State ───────────────────────────────────────────────────
/** The item currently being dragged (Layer 1: pointer-events system). */
let activeDrag = $state<DragPayload | null>(null);
/** Current pointer position during drag (screen coordinates). */
let pointerX = $state(0);
let pointerY = $state(0);
/** The ID of the drop target currently being hovered. */
let hoveredTargetId = $state<string | null>(null);
/**
* Whether a svelte-dnd-action drag is in progress (Layer 2).
* Set by passiveDropZone when it detects aria-grabbed elements.
*/
let svelteActionDragActive = $state(false);
/**
* Payload inferred from svelte-dnd-action drag (for passiveDropZone).
* Set via registerSvelteActionDrag() from the app.
*/
let svelteActionPayload = $state<DragPayload | null>(null);
// ── Accessors ───────────────────────────────────────────────
export const dragState = {
get activeDrag() {
return activeDrag;
},
get pointerX() {
return pointerX;
},
get pointerY() {
return pointerY;
},
get hoveredTargetId() {
return hoveredTargetId;
},
get isDragging() {
return activeDrag !== null;
},
get svelteActionDragActive() {
return svelteActionDragActive;
},
get svelteActionPayload() {
return svelteActionPayload;
},
/** True if ANY drag is happening (Layer 1 or Layer 2). */
get anyDragActive() {
return activeDrag !== null || svelteActionDragActive;
},
};
// ── Mutations ───────────────────────────────────────────────
export function startDrag(payload: DragPayload) {
activeDrag = payload;
}
export function updatePointer(x: number, y: number) {
pointerX = x;
pointerY = y;
}
export function setHoveredTarget(id: string | null) {
hoveredTargetId = id;
}
export function endDrag() {
activeDrag = null;
hoveredTargetId = null;
}
/**
* Called by app code to inform the passive layer that a svelte-dnd-action
* drag has started, along with the payload of the dragged item.
*
* Usage in TaskList:
* onconsider={(e) => {
* registerSvelteActionDrag({ type: 'task', data: { id: e.detail.info.id } });
* }}
* onfinalize={(e) => {
* clearSvelteActionDrag();
* }}
*/
export function registerSvelteActionDrag(payload: DragPayload) {
svelteActionDragActive = true;
svelteActionPayload = payload;
}
export function clearSvelteActionDrag() {
svelteActionDragActive = false;
svelteActionPayload = null;
hoveredTargetId = null;
}
/**
* Check whether a given drag type is currently active (either layer).
*/
export function isTypeBeingDragged(type: DragType): boolean {
if (activeDrag?.type === type) return true;
if (svelteActionPayload?.type === type) return true;
return false;
}