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>
|
||
|---|---|---|
| .. | ||
| ActionZone.svelte | ||
| drag-source.ts | ||
| drag-state.svelte.ts | ||
| DragPreview.svelte | ||
| drop-target.ts | ||
| index.ts | ||
| passive-drop.ts | ||
| README.md | ||
| types.ts | ||
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
dragSourceto become draggable, TaskItems usedropTargetto 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,
passiveDropZonedetects 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 |