Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ jobs:
- run: npm ci
- run: npm run validate:packages
- run: npm run check:snapsort-api
- run: npm run test:core-ut
- run: npm run test:snapsort-ut
- run: npm run test:snapline-ut
- run: npm run typecheck
- run: npm run lint
- run: npm run check:adapters
- run: npm ci --prefix website
- run: npm run check:tests
- run: npm run check:website
- run: npm run build
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,20 @@ npm run dev:react # React demo

**Run tests:**
```bash
npm test
npm test # every unit suite (tests/ut)
npm run test:e2e # every e2e project on Chromium
npm run test:snapsort # one project: core, asset-base, snapline,
# snapsort, or layout
```

End-to-end suites live in `tests/e2e/<project>/<feature>/`, one Playwright
config per project (`tests/e2e/<project>/playwright.config.ts`). Servers and
ports come from `tests/e2e/shared/servers.ts`. Tests that never open a page
belong in `tests/ut/`.

Tests cover the engine and asset packages, driven through the `demo/` apps.
The website is not tested: do not add specs for its pages or gallery.

## Release Workflow

Publishing is triggered by pushing version tags, not by pushing `main`.
Expand Down
5 changes: 0 additions & 5 deletions SNAPSORT_SIMPLIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,6 @@ npm run validate:packages
npm run check:adapters
npm run check:website
npm run test:snapsort
npx playwright test -c tests/e2e/docs-framework-code.playwright.config.ts --grep "SnapSort callback docs"
```

**Commit:** `refactor(snapsort): trim the 0.5 compatibility API`
Expand Down Expand Up @@ -701,10 +700,8 @@ or colliding application ID fails.
npm run test:snapsort
npm run test:snapsort-react
npm run test:snapsort-svelte
npm run test:snapsort-examples
npm run check:adapters
npm run check:website
npx playwright test -c tests/e2e/docs-framework-code.playwright.config.ts
```

**Commit:** `refactor(snapsort): unify ghost transactions`
Expand Down Expand Up @@ -786,8 +783,6 @@ After all approved phases:
npm run ci
npm run test:snapsort
npm run test:layout
npm run test:snapsort-examples
npx playwright test -c tests/e2e/docs-framework-code.playwright.config.ts
```

`npm run ci` does not run the Playwright suites, so it is necessary but not
Expand Down
5 changes: 4 additions & 1 deletion assets/snapline/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,10 @@ class NodeMirror extends ElementObject {
/* Reconciles state with what the browser actually rendered */
#syncMeasuredGeometry(stage: "READ_1" | "READ_2"): void {
if (!this.element) return;
const property = this.readDom({ unapplyTransform: false }, stage);
// The collision box is a child of this node and inherits its scale, so
// it takes the untransformed size: without this, a scaled node's scale
// would apply twice.
const property = this.readDom({ unapplyTransform: true }, stage);
// During resize, setSizeState sets the correct dimensions.
if (!this.#isResizing) {
this.#hitBox.width = property.width;
Expand Down
5 changes: 4 additions & 1 deletion assets/snapsort/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ no pre-mutation FLIP rectangle and must be ignored safely by animation code.
virtual rectangle retains the initial pointer-to-item offset. There is no
implicit deepest-container preference; an application may opt into
`prioritizeTreeDepth` through `getDropPriority` when it wants explicit
virtual-X/pointer-Y depth policy.
virtual-X/pointer-Y depth policy. Because the cross axis never enters the
score, side-by-side destinations (columns of a board) opt into
`prioritizePointerContainer` so the column under the pointer wins over a
neighbor whose gap merely lines up on the main axis.
- Wrapped rows and columns use the selected visual line's measured cross-axis
band. A boundary that wraps uses the next line's leading edge and band;
append uses the previous line's trailing edge and band.
Expand Down
11 changes: 1 addition & 10 deletions assets/snapsort/src/drag/pointer-controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { freezePoint } from "@snap-engine/core/geometry";
import type {
dragEndProp,
dragProp,
Expand All @@ -6,16 +7,6 @@ import type {
import type { ResolvedDropTarget } from "../algorithm";
import type { DragSessionController } from "./session";

function freezePoint(point: {
x: number;
y: number;
}): Readonly<{ x: number; y: number }> {
return Object.freeze({
x: point.x,
y: point.y,
});
}

export class PointerDragController {
readonly inputType = "pointer" as const;
readonly pointerId: number;
Expand Down
11 changes: 1 addition & 10 deletions assets/snapsort/src/drag/session.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AnimationObject } from "@snap-engine/core/animation";
import type { ElementBox, dragStartProp } from "@snap-engine/core";
import {
freezePoint,
freezeRect,
type BoxModel,
type Rect,
Expand Down Expand Up @@ -102,16 +103,6 @@ interface DropTargetUpdate {
readonly shouldClear: boolean;
}

function freezePoint(point: {
x: number;
y: number;
}): Readonly<{ x: number; y: number }> {
return Object.freeze({
x: point.x,
y: point.y,
});
}

function freezeLocation(location: DragLocation): DragLocation {
return Object.freeze({ ...location });
}
Expand Down
18 changes: 17 additions & 1 deletion assets/snapsort/src/keyboard-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@ export class KeyboardDragController {
prop.event.preventDefault();
};

#isMovementKey(key: string): boolean {
const { previous, next } = this.#bindings;
return (
previous.column.has(key) ||
previous.row.has(key) ||
next.column.has(key) ||
next.row.has(key)
);
}

#handleActiveDirectSession(
event: KeyboardEvent,
input: DirectDragController,
Expand All @@ -204,7 +214,13 @@ export class KeyboardDragController {
}

const direction = input.currentTarget?.container.direction;
if (!direction) return;
if (!direction) {
// The lift is still activating, so there is no target to move from
// yet. Consume movement keys anyway: otherwise an arrow pressed right
// after lifting falls through to the browser and scrolls the page.
if (this.#isMovementKey(event.key)) event.preventDefault();
return;
}

if (this.#bindings.previous[direction].has(event.key)) {
input.movePrevious();
Expand Down
25 changes: 24 additions & 1 deletion demo/react/src/snapsort/SnapSortFixtures.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { defaultAnimations, Item as CoreItem } from "@snap-engine/snapsort";
import {
prioritizeIntersectingContainer,
prioritizePointerContainer,
rejectDrop,
} from "@snap-engine/snapsort/callbacks";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
Expand Down Expand Up @@ -1399,12 +1400,30 @@ export function SnapSortComponentsDemo() {
[columns],
);

// Programmatic removal through SnapSort (animated by the owning Container)
// versus a direct state deletion (not animated), for the e2e suite.
const removeItemThroughSnapSort = useCallback(
(itemId) => {
const column = columns.find((candidate) =>
candidate.items.some((item) => item.id === itemId),
);
return column
? (containerRefs.current.get(column.id)?.removeItem(itemId) ?? false)
: false;
},
[columns],
);

useEffect(() => {
window.__snapsortMoveComponentItem = moveItemAcrossColumns;
window.__snapsortRemoveComponentItem = removeItemThroughSnapSort;
window.__snapsortDeleteComponentItem = deleteItem;
return () => {
delete window.__snapsortMoveComponentItem;
delete window.__snapsortRemoveComponentItem;
delete window.__snapsortDeleteComponentItem;
};
}, [moveItemAcrossColumns]);
}, [moveItemAcrossColumns, removeItemThroughSnapSort, deleteItem]);

return (
<div className="snapsort-fixture components-demo">
Expand Down Expand Up @@ -2224,6 +2243,10 @@ export function SnapSortInsertionDemo() {
direction: "column",
mode: "insertion",
name: `insertion-${column.id}`,
// Insertion ranks gaps by main-axis distance alone, so a
// side-by-side column needs pointer-container priority to
// keep the marker in the column under the pointer.
callbacks: { getDropPriority: prioritizePointerContainer },
}}
itemId={column.id}
key={column.id}
Expand Down
30 changes: 15 additions & 15 deletions demo/svelte/src/demo/drop_snap_nested/DropSnapNestedDemo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -766,33 +766,33 @@
color: #000;
}

:global(.snapsort-container) {
.snapsort-demo :global(.snapsort-container) {
gap: var(--size-8);
min-height: 40px;
}

:global(.snapsort-container .snapsort-container) {
.snapsort-demo :global(.snapsort-container .snapsort-container) {
border: 2px solid #000;
padding: var(--size-8);
}

:global(.demo-item),
:global(.layer-item),
:global(.snapsort-item) {
.snapsort-demo :global(.demo-item),
.snapsort-demo :global(.layer-item),
.snapsort-demo :global(.snapsort-item) {
margin: var(--size-4);
border: 2px solid #000;
background: #fff;
cursor: grab;
box-sizing: border-box;
}

:global(.demo-item:active),
:global(.layer-item:active),
:global(.snapsort-item:active) {
.snapsort-demo :global(.demo-item:active),
.snapsort-demo :global(.layer-item:active),
.snapsort-demo :global(.snapsort-item:active) {
cursor: grabbing;
}

:global(.demo-item p) {
.snapsort-demo :global(.demo-item p) {
margin: 0;
padding: var(--size-8) var(--size-12);
font-size: 1rem;
Expand All @@ -804,20 +804,20 @@
min-height: 240px;
}

:global(.demo-item.size-item p) {
.snapsort-demo :global(.demo-item.size-item p) {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
text-align: center;
}

:global(.demo-item.row-item) {
.snapsort-demo :global(.demo-item.row-item) {
min-width: 50px;
text-align: center;
}

:global(.demo-item.selected) {
.snapsort-demo :global(.demo-item.selected) {
border-color: #6366f1;
box-shadow: inset 0 0 0 2px #6366f1;
background: #eef2ff;
Expand All @@ -829,7 +829,7 @@
color: #888;
}

:global(.demo-item.sub-item) {
.snapsort-demo :global(.demo-item.sub-item) {
opacity: 0.6;
}

Expand Down Expand Up @@ -880,14 +880,14 @@
user-select: none;
}

:global(.ghost) {
.snapsort-demo :global(.ghost) {
background: #e5e5e5;
border: 2px solid #9a9a9a;
box-sizing: border-box;
opacity: 1;
}

:global(.double-row-ghost-label) {
.snapsort-demo :global(.double-row-ghost-label) {
display: flex;
align-items: center;
justify-content: center;
Expand Down
Loading
Loading