managarten/packages/shared-ui/src/dnd
Till JS 6bb9d77be9 feat(sync): F3 — drop updatedAt as a synced data field
Removes `updatedAt` from the wire protocol and from every Local-prefixed
record type. Replaced by two orthogonal mechanisms — deriveUpdatedAt()
for read-side public-facing values, _updatedAtIndex shadow for indexed
sorts.

Local-side:
- New `_updatedAtIndex` shadow column. Stamped by the Dexie creating /
  updating hook on every write. Stripped from the pending-change payload
  so it never travels to mana-sync. Indexed in Dexie v53 on the 22 tables
  that previously indexed `updatedAt`.
- `deriveUpdatedAt(record)` in sync.ts returns max(__fieldMeta[*].at) so
  the public-facing Task / Note / etc. shape keeps an `updatedAt: string`
  property without holding it as data.
- Type-converters across ~60 module/queries.ts and types.ts files now
  call `deriveUpdatedAt(local)` instead of reading `local.updatedAt`.

Module-store sweep:
- Regex codemod removed `updatedAt: new Date().toISOString()` /
  `: now` / `: now()` / `: nowIso()` stamping from 121 store files
  (~382 call sites total). Single-property update calls
  (`{ updatedAt: now }`) collapsed to `{}`; touch-only patterns
  (writing/drafts, writing/generations) kept the call as a no-op
  because the hook now stamps `_updatedAtIndex` automatically on
  any Dexie modification.
- Local* interfaces stripped of `updatedAt: string` (43 types.ts files).
  Public-facing types (Task, Note, Mission, Agent, …) keep
  `updatedAt: string` as a computed read-side property.
- Companion's chat conversation now sorts on a real
  `lastMessageAt` data field instead of touching `updatedAt`.
- Session-only stores (times/session-alarms, session-countdown-timers)
  stamp `updatedAt: now` directly because they're not in Dexie and
  have no field-meta layer to derive from.

Sync engine:
- applyServerChanges sets `_updatedAtIndex` itself when applying
  server changes (max of server-field times for updates, recordTime
  for inserts) so server-replays land orderable.
- Dropped the legacy `localUpdatedAt` fallback — every record now has
  `__fieldMeta`, the per-field at is the canonical source.
- Soft-delete tombstone path stops stamping `updatedAt: serverTime`,
  uses `_updatedAtIndex` instead.

Server-side:
- mana-ai iteration-writer no longer emits `updatedAt` in
  sync_changes.data; receivers derive it from the field-meta map.
- mana-sync types: no change (the wire format already uses
  `field_meta` / `at` from F1).

Out of scope: backend Drizzle schemas (mana-credits, mana-events, …)
keep their `updated_at` columns. Those are pure server-internal — not
part of the sync_changes / __fieldMeta mechanism F3 cleans up.

Tests + checks:
- 0 svelte-check errors over 7652 files.
- 29/29 sync.test.ts (vitest).
- 61 mana-ai bun tests.
- mana-sync go test ./... cached green.

Plan: docs/plans/sync-field-meta-overhaul.md F3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:12:22 +02:00
..
ActionZone.svelte fix(mana/web+packages): clear all 270 warnings to zero 2026-04-10 17:34:49 +02:00
drag-source.ts fix(shared-ui): block click event after drag to prevent detail view opening 2026-04-03 13:15:53 +02:00
drag-state.svelte.ts feat(shared-ui, todo): add cross-type drag & drop system with tag enrichment 2026-04-01 21:00:25 +02:00
DragPreview.svelte fix(manacore/web): fix getTagsByIds missing allTags param in zitare, fix TagDragData cast 2026-04-03 14:20:34 +02:00
drop-target.ts feat(shared-ui, todo): add cross-type drag & drop system with tag enrichment 2026-04-01 21:00:25 +02:00
index.ts feat(shared-ui, todo): add cross-type drag & drop system with tag enrichment 2026-04-01 21:00:25 +02:00
passive-drop.ts feat(shared-ui, todo): add cross-type drag & drop system with tag enrichment 2026-04-01 21:00:25 +02:00
README.md chore: complete ManaCore → Mana rename (docs, go modules, plists, images) 2026-04-07 12:26:10 +02:00
types.ts feat(sync): F3 — drop updatedAt as a synced data field 2026-04-26 23:12:22 +02:00

Cross-Type Drag & Drop System

Shared DnD system for Mana apps. Enables dragging items between different component types (e.g. Tag onto Task, Task onto Trash zone).

Designed to coexist with svelte-dnd-action which handles same-type reordering.

Architecture

Two layers:

  • Layer 1 (Pointer Events): For items NOT managed by svelte-dnd-action. Tag pills in the TagStrip use dragSource to become draggable, TaskItems use dropTarget to accept tags. Works on mouse, touch, and pen via Pointer Events.

  • Layer 2 (Passive Overlay): For items already managed by svelte-dnd-action. When a Task is being reordered via svelte-dnd-action, passiveDropZone detects if the pointer hovers over a Tag pill or ActionZone and fires the appropriate action on drop. No conflict with existing DnD.

Usage

Make an element draggable (Layer 1)

<script>
  import { dragSource } from '@mana/shared-ui/dnd';
</script>

<button use:dragSource={{
  type: 'tag',
  data: () => ({ id: tag.id, name: tag.name, color: tag.color }),
}}>
  {tag.name}
</button>
  • Desktop: drag starts after 5px mouse movement
  • Mobile: drag starts after 300ms long-press (with haptic feedback)

Make an element a drop target (Layer 1)

<script>
  import { dropTarget } from '@mana/shared-ui/dnd';
</script>

<div use:dropTarget={{
  accepts: ['tag'],
  onDrop: (payload) => assignTag(item.id, payload.data.id),
  canDrop: (payload) => !item.tagIds.includes(payload.data.id),
}}>
  {item.title}
</div>

CSS class mana-drop-target-hover is added during hover, mana-drop-target-success briefly after a successful drop.

React to svelte-dnd-action drags (Layer 2)

<script>
  import { passiveDropZone, registerSvelteActionDrag, clearSvelteActionDrag } from '@mana/shared-ui/dnd';
</script>

<!-- In your svelte-dnd-action handlers: -->
<div
  use:dndzone={{ items, type: 'task-dnd' }}
  onconsider={(e) => {
    items = e.detail.items;
    registerSvelteActionDrag({ type: 'task', data: { id: e.detail.info.id } });
  }}
  onfinalize={(e) => {
    // ... normal handling ...
    clearSvelteActionDrag();
  }}
>

<!-- On external targets (e.g. tag pills): -->
<button use:passiveDropZone={{
  accepts: ['task'],
  onDrop: (payload) => assignTag(tag.id, payload.data.id),
  highlightClass: 'my-highlight-class',
}}>

Floating preview + action zones

Place once in your app layout:

<script>
  import { DragPreview, ActionZone } from '@mana/shared-ui/dnd';
</script>

<DragPreview />
<ActionZone
  accepts={['task']}
  onDrop={(payload) => deleteItem(payload.data.id)}
  variant="danger"
  label="Delete"
/>

ActionZone auto-shows/hides when any drag is active. Variants: danger, warning, info, success.

Drag Types

Type Used by
tag Tag pills (TagStrip, PillTagSelector)
task Todo tasks (TaskList, Kanban)
card Cards app
photo Photos app
file Storage app
event Calendar events
link uLoad links
contact Contacts

CSS Classes

Class When
mana-drag-source-active On the source element during drag
mana-drop-target-hover On drop target while valid item hovers
mana-drop-target-success Brief flash after successful drop
mana-passive-zone-hover On passive zone while item hovers
mana-passive-zone-success Brief flash after successful passive drop

Files

File Purpose
types.ts DragType, payload interfaces, option types
drag-state.svelte.ts Global reactive state (Svelte 5 runes)
drag-source.ts use:dragSource action (Pointer Events)
drop-target.ts use:dropTarget action
passive-drop.ts use:passiveDropZone action (Layer 2)
DragPreview.svelte Floating drag ghost
ActionZone.svelte Trash/archive drop zone