diff --git a/package.json b/package.json index f37bc7f4..d7a707c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "data-monorepo", - "version": "0.10.11", + "version": "0.10.12", "private": true, "engines": { "node": ">=24" diff --git a/packages/data-ai/.claude-plugin/plugin.json b/packages/data-ai/.claude-plugin/plugin.json index 474eeba1..162a262f 100644 --- a/packages/data-ai/.claude-plugin/plugin.json +++ b/packages/data-ai/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "adobe-data-ai", - "version": "0.10.11", + "version": "0.10.12", "description": "Architecture skills for @adobe/data — data-oriented modelling, archetype iteration, hot-path performance, and related conventions.", "author": { "name": "Adobe" diff --git a/packages/data-ai/package.json b/packages/data-ai/package.json index 8ea3f9b5..000ea6ba 100644 --- a/packages/data-ai/package.json +++ b/packages/data-ai/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-ai", - "version": "0.10.11", + "version": "0.10.12", "description": "Cross-agent architecture skills for @adobe/data — installable as a Claude Code plugin or copied into any Agent-Skills-compatible agent (Cursor, Codex).", "type": "module", "private": false, diff --git a/packages/data-gpu/package.json b/packages/data-gpu/package.json index 9200669b..68f415c0 100644 --- a/packages/data-gpu/package.json +++ b/packages/data-gpu/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-gpu", - "version": "0.10.11", + "version": "0.10.12", "description": "Adobe data WebGPU plugins and types for graphics and compute", "type": "module", "private": false, diff --git a/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/drag-todo.test.ts b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/drag-todo.test.ts new file mode 100644 index 00000000..3283cf36 --- /dev/null +++ b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/drag-todo.test.ts @@ -0,0 +1,65 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. +import { describe, it, expect } from "vitest"; +import { Database } from "@adobe/data/ecs"; +import type { DragState } from "@adobe/data-lit"; +import { ServiceDatabase } from "../../service-database/service-database.js"; +import { dragTodo } from "./drag-todo.js"; + +// `dragTodo` is the "drag UI op" — conformance intentionally skips it (see +// `transactions.md`), so this is its only coverage. It is the one place the +// `useDragGenerator` → action → transaction mapping (including the cancel +// reset) actually lives. + +// A stand-in row height; `services/` must not import the real UI constant. +const ROW_HEIGHT = 48; + +async function* dragStates(...states: DragState[]): AsyncGenerator { + for (const state of states) yield state; +} + +describe("dragTodo action", () => { + it("streams a live offset on move, then reorders on the final drop", async () => { + const db = Database.create(ServiceDatabase.plugin); + const entity = db.transactions.createTodo({ name: "a" }); + const other = db.transactions.createTodo({ name: "b" }); + + await dragTodo(db, { + entity, + index: 0, + rowHeight: ROW_HEIGHT, + drag: dragStates( + { type: "move", delta: [0, 10], position: [0, 10] }, + { + type: "end", + delta: [0, ROW_HEIGHT], + position: [0, ROW_HEIGHT], + }, + ), + }); + + const after = db.read(entity); + expect(after?.dragPosition).toBeNull(); + // dragged from index 0 to index 1 — now orders after "b". + expect(after?.order).toBeGreaterThan(db.read(other)!.order!); + }); + + it("abandons the drag with no reorder on cancel", async () => { + const db = Database.create(ServiceDatabase.plugin); + const entity = db.transactions.createTodo({ name: "a" }); + const before = db.read(entity)!; + + await dragTodo(db, { + entity, + index: 0, + rowHeight: ROW_HEIGHT, + drag: dragStates( + { type: "move", delta: [0, 10], position: [0, 10] }, + { type: "cancel" }, + ), + }); + + const after = db.read(entity); + expect(after?.dragPosition).toBeNull(); + expect(after?.order).toBe(before.order); + }); +}); diff --git a/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/drag-todo.ts b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/drag-todo.ts new file mode 100644 index 00000000..0d47ab5d --- /dev/null +++ b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/drag-todo.ts @@ -0,0 +1,41 @@ +// © 2026 Adobe. MIT License. See /LICENSE for details. +import type { Entity } from "@adobe/data/ecs"; +import type { DragState } from "@adobe/data-lit"; +import type { ServiceDatabase } from "../../service-database/service-database.js"; + +// `useDragGenerator` hands us the raw pointer stream; this action maps each +// yielded `DragState` frame to the `dragTodo` transaction's args and drives +// the transaction directly with that mapped async generator, so every frame — +// including the final drop — commits as one coalesced, undoable step. A +// cancelled gesture yields a `dragPosition: null` frame with no `finalIndex`, +// which abandons the drag without reordering. +// +// `rowHeight` is a UI layout measure passed in by the caller (never imported — +// `services/` must not depend on `ui/`), used to convert the final pixel offset +// into a list-index delta. +export const dragTodo = ( + db: ServiceDatabase, + args: { + readonly entity: Entity; + readonly index: number; + readonly rowHeight: number; + readonly drag: AsyncGenerator; + }, +) => { + const { entity, index, rowHeight, drag } = args; + return db.transactions.dragTodo(async function* () { + for await (const state of drag) { + if (state.type === "move") { + yield { entity, dragPosition: state.delta[1] }; + } else if (state.type === "end") { + yield { + entity, + dragPosition: state.delta[1], + finalIndex: index + Math.round(state.delta[1] / rowHeight), + }; + } else if (state.type === "cancel") { + yield { entity, dragPosition: null }; + } + } + }); +}; diff --git a/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/index.ts b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/index.ts index b9746cb3..e4787154 100644 --- a/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/index.ts +++ b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/index.ts @@ -7,3 +7,4 @@ export * from "./delete-todo.js"; export * from "./delete-all-todos.js"; export * from "./toggle-display-completed.js"; export * from "./reorder-todo.js"; +export * from "./drag-todo.js"; diff --git a/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/reorder-todo.ts b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/reorder-todo.ts index c236d909..19fcb878 100644 --- a/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/reorder-todo.ts +++ b/packages/data-lit-todo/src/features/main/services/main-service/action-database/actions/reorder-todo.ts @@ -3,9 +3,10 @@ import type { Entity } from "@adobe/data/ecs"; import type { ServiceDatabase } from "../../service-database/service-database.js"; // Move a todo to `toIndex` — the programmatic counterpart of the drag UI (which -// dispatches the richer `dragTodo` transaction directly). Reproduces -// `State.reorderTodo` as a single final-drop `dragTodo` commit, so it is the -// same-named action that conforms the `reorderTodo` transition. +// drives the same `dragTodo` transaction through the richer `dragTodo` action, +// streamed frame-by-frame from `useDragGenerator`). Reproduces `State.reorderTodo` +// as a single final-drop `dragTodo` commit, so it is the same-named action that +// conforms the `reorderTodo` transition. export const reorderTodo = ( db: ServiceDatabase, { id, toIndex }: { id: Entity; toIndex: number }, diff --git a/packages/data-lit-todo/src/features/main/services/main-service/transaction-database/transactions/drag-todo.ts b/packages/data-lit-todo/src/features/main/services/main-service/transaction-database/transactions/drag-todo.ts index 6e53e2fb..f2c4aafa 100644 --- a/packages/data-lit-todo/src/features/main/services/main-service/transaction-database/transactions/drag-todo.ts +++ b/packages/data-lit-todo/src/features/main/services/main-service/transaction-database/transactions/drag-todo.ts @@ -8,11 +8,16 @@ import { normalizeOrder, selectOrderedTodos } from "./order/index.js"; // `type DragTodoInput = Parameters[1]`. type DragTodoInput = { readonly entity: Entity; - /** Live vertical pixel offset from the todo's resting position. */ - readonly dragPosition: number; + /** + * Live vertical pixel offset from the todo's resting position. `null` + * abandons the drag with no reorder — the same shape a cancelled gesture + * yields as its final frame. + */ + readonly dragPosition: number | null; /** * Target index within the currently visible list. Present only on the final - * frame of a drag; while omitted the drag is still in progress. + * frame of a drag; while omitted the drag is still in progress (or was + * cancelled). */ readonly finalIndex?: number; }; @@ -24,8 +29,10 @@ type DragTodoInput = { * a fractional `order` between its new visible neighbours, clears * `dragPosition`, then normalizes every todo back to contiguous integers. * - * Intended to be driven by `useDragTransaction`, which invokes it with an - * `AsyncArgsProvider` so all frames commit as one undoable step. + * Intended to be driven by the `dragTodo` action, which maps a + * `useDragGenerator` stream into this shape and drives this transaction with + * the resulting async generator so every frame — including the final drop — + * commits as one undoable step. */ export const dragTodo = (t: CoreDatabase.Store, input: DragTodoInput): void => { t.undoable = { coalesce: false }; diff --git a/packages/data-lit-todo/src/features/main/ui/todo-row/todo-row-element.ts b/packages/data-lit-todo/src/features/main/ui/todo-row/todo-row-element.ts index fa60cc1f..df080ce3 100644 --- a/packages/data-lit-todo/src/features/main/ui/todo-row/todo-row-element.ts +++ b/packages/data-lit-todo/src/features/main/ui/todo-row/todo-row-element.ts @@ -1,17 +1,12 @@ // © 2026 Adobe. MIT License. See /LICENSE for details. import { customElement, property } from "lit/decorators.js"; import type { Entity } from "@adobe/data/ecs"; -import { useObservableValues, useState, useDragTransaction } from "@adobe/data-lit"; +import { useObservableValues, useState, useDragGenerator } from "@adobe/data-lit"; import { TodoElement } from "../todo-element.js"; import { styles } from "./todo-row.css.js"; import { TODO_ROW_HEIGHT } from "./todo-row.constants.js"; -import type { dragTodo } from "../../services/main-service/transaction-database/transactions/drag-todo.js"; import * as presentation from "./todo-row-presentation.js"; -// The transaction owns this shape and doesn't export it; infer it from the -// function's second parameter rather than importing a type. -type DragTodoInput = Parameters[1]; - const tagName = "todo-row"; declare global { @@ -40,29 +35,24 @@ export class TodoRowElement extends TodoElement { ); const todo = values?.todo; - // Dragging is a lifecycle/pointer concern, so it lives in the element (not - // the pure presentation). A single coalesced transaction spans the whole - // gesture: `move` frames record the live pixel offset, `end` commits the - // reorder. Drag is a continuous manipulation, so it calls the transaction - // directly rather than an analytics-wrapped action. + // Dragging is a lifecycle/pointer concern, so the pointer stream is + // captured here — but mapping it into transaction args is logic, and + // belongs to the `dragTodo` action, not this element (`element.md`: no + // shape-building in a callback). `useDragGenerator` hands the action a + // raw `DragState` generator; the action drives the `dragTodo` transaction + // with it directly, so the whole gesture — every live frame plus the + // final drop — commits as one coalesced, undoable step. const { entity, index } = this; - useDragTransaction( - { - transaction: this.service.transactions.dragTodo, - update: (value) => { - if (value.type === "move") { - return { entity, dragPosition: value.delta[1] }; - } - if (value.type === "end") { - return { - entity, - dragPosition: value.delta[1], - finalIndex: index + Math.round(value.delta[1] / TODO_ROW_HEIGHT), - }; - } - }, - }, - [this.service.transactions.dragTodo, entity, index], + useDragGenerator( + {}, + [this.service.actions.dragTodo, entity, index], + (drag) => + this.service.actions.dragTodo({ + entity, + index, + rowHeight: TODO_ROW_HEIGHT, + drag, + }), ); return presentation.render({ diff --git a/packages/data-lit/package.json b/packages/data-lit/package.json index 7ff4755f..0835d003 100644 --- a/packages/data-lit/package.json +++ b/packages/data-lit/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-lit", - "version": "0.10.11", + "version": "0.10.12", "description": "Adobe data Lit bindings - hooks, elements, decorators", "type": "module", "private": false, diff --git a/packages/data-persistence/package.json b/packages/data-persistence/package.json index 5ef892b3..340525e4 100644 --- a/packages/data-persistence/package.json +++ b/packages/data-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-persistence", - "version": "0.10.11", + "version": "0.10.12", "description": "Worker-based incremental persistence layer for @adobe/data ECS over OPFS (browser) and node:fs (server).", "type": "module", "sideEffects": false, diff --git a/packages/data-react/package.json b/packages/data-react/package.json index 9549cfa3..a3861eed 100644 --- a/packages/data-react/package.json +++ b/packages/data-react/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-react", - "version": "0.10.11", + "version": "0.10.12", "description": "Adobe data React bindings — hooks and context for ECS database", "type": "module", "private": false, diff --git a/packages/data-rpc/package.json b/packages/data-rpc/package.json index f1fd1019..6977f610 100644 --- a/packages/data-rpc/package.json +++ b/packages/data-rpc/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-rpc", - "version": "0.10.11", + "version": "0.10.12", "description": "Schema-driven, bidirectional projection of @adobe/data async data services across a boundary (iframe / MessagePort / Worker). Only Data crosses the wire.", "type": "module", "sideEffects": false, diff --git a/packages/data-solid/package.json b/packages/data-solid/package.json index 7732ee0a..14ded1bf 100644 --- a/packages/data-solid/package.json +++ b/packages/data-solid/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-solid", - "version": "0.10.11", + "version": "0.10.12", "description": "Adobe data SolidJS bindings — context and provider for ECS database", "type": "module", "private": false, diff --git a/packages/data-sync/package.json b/packages/data-sync/package.json index cd64ff15..51427f4e 100644 --- a/packages/data-sync/package.json +++ b/packages/data-sync/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-sync", - "version": "0.10.11", + "version": "0.10.12", "description": "Multi-user real-time synchronisation for @adobe/data ECS — server, client, and in-process loopback.", "type": "module", "sideEffects": false, diff --git a/packages/data-testing/package.json b/packages/data-testing/package.json index fca8c8d0..a884cd8a 100644 --- a/packages/data-testing/package.json +++ b/packages/data-testing/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data-testing", - "version": "0.10.11", + "version": "0.10.12", "description": "Conformance-testing utilities (Match + Conformance runners) for @adobe/data ECS features", "type": "module", "sideEffects": false, diff --git a/packages/data/package.json b/packages/data/package.json index 9f78831f..41a08568 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@adobe/data", - "version": "0.10.11", + "version": "0.10.12", "description": "Adobe data oriented programming library", "type": "module", "sideEffects": false,