Skip to content

Commit 621bf6b

Browse files
committed
docs: update spatial parity checklist with test coverage for useCanvasActions and useCanvasContextMenu composables
1 parent 375f5aa commit 621bf6b

9 files changed

Lines changed: 471 additions & 176 deletions

File tree

docs/SPATIAL_PARITY_CHECKLIST.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
| # | Feature | Legacy reference | New file(s) | Status | Test file |
2323
|---|---------|------------------|-------------|--------|-----------|
24-
| 1.1 | Create note (double-click canvas) | `DisplayPage/DisplayWorld/DisplayNote/CreateNote.ts` | `SpatialPageView.vue:onCanvasDoubleClick` | **Done** | |
24+
| 1.1 | Create note (double-click canvas) | `DisplayPage/DisplayWorld/DisplayNote/CreateNote.ts` | `useCanvasActions.ts:onCanvasDoubleClick` | **Done** | `useCanvasActions.test.ts` |
2525
| 1.2 | Delete note (Delete key) | `DisplayPage/DisplayWorld/DisplayNote/DeleteNote.ts` | `SpatialPageView.vue:onKeyDown` | **Done** ||
2626
| 1.3 | Move note (drag) | `space/pos.ts`, `DisplayNote/DragNote.ts` | `DisplayNote.vue:onPointerDown/Move/Up` | **Done** | `DisplayNote.test.ts` |
2727
| 1.4 | Resize note (8 handles) | `DisplayNote/ResizeNote.ts` | `DisplayNote.vue:8-handle resize` | **Done** | `DisplayNote.test.ts` |
@@ -188,7 +188,7 @@
188188
| 12.14 | Right sidebar — Snapshots | `RightSidebar/Snapshots.vue` | `PageEditorSnapshotsCard.vue` | **Done** ||
189189
| 12.15 | Right sidebar — Management | `RightSidebar/Management.vue` | `PageEditorManagementCard.vue` | **Done** ||
190190
| 12.16 | Right sidebar — Backlinks | `RightSidebar/Backlinks.vue` | `PageEditorBacklinksCard.vue` | **Done** ||
191-
| 12.17 | Canvas context menu | `TableContextMenu.vue` | `CanvasContextMenu.vue` | **Partial** | |
191+
| 12.17 | Canvas context menu | `TableContextMenu.vue` | `CanvasContextMenu.vue` + `useCanvasContextMenu.ts` | **Partial** | `useCanvasContextMenu.test.ts` |
192192
| 12.18 | Per-note context menu | `NoteContextMenu.vue` || **Not started** ||
193193
| 12.19 | Page state screens (8 total) | `DisplayScreens/*.vue` | `PageStateScreens.vue` + 8 components | **Done** ||
194194
| 12.20 | Loading overlay | `LoadingOverlay.vue` | `PageStateScreens.vue` (inline) | **Partial** ||
@@ -276,7 +276,7 @@ Every field from legacy `INoteCollab` and `IArrowCollab` is present in the new Y
276276

277277
- [x] Checklist contains ≥ 60 rows. (Current count: **82+ rows**)
278278
- [x] Schema diff table covers every legacy `INoteCollab` and `IArrowCollab` field.
279-
- [ ] Every "Done" item has a passing automated test. **VIOLATED.** ~30+ UI/interaction rows marked "Done" still have "—" in the Test file column. Progress since last evaluation: `note-geometry.test.ts` (8 tests), `useBoxSelection.test.ts` (6 tests), `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests) added. Remaining gaps: `DisplayNote.vue` (basic render tests only), `DisplayArrow.vue` (no component tests), `SpatialPageView.vue` (no component/integration tests), drag/resize interaction, arrow creation/reconnection, sidebar/toolbar integration.
279+
- [ ] Every "Done" item has a passing automated test. **VIOLATED.** ~30+ UI/interaction rows marked "Done" still have "—" in the Test file column. Progress since last evaluation: `useCanvasActions.test.ts` (3 tests), `useCanvasContextMenu.test.ts` (6 tests), `note-geometry.test.ts` (8 tests), `useBoxSelection.test.ts` (6 tests), `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests) added. `DisplayArrow.test.ts` (12 tests) and `DisplayNote.test.ts` (19 tests) also added. Remaining gaps: `SpatialPageView.vue` (no component/integration tests), drag/resize interaction, arrow creation/reconnection, sidebar/toolbar integration.
280280
- [ ] Phase 6 is not declared done until ≥ 80% of rows are **Done**. **NOT MET.** Strict enforcement of the test rule would drop the true "Done" count well below 80%.
281281

282282
---

docs/restart-plan/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ A criterion is **not met** until the verification command or check passes in CI.
7777

7878
### Phase 6 — Spatial canvas polish (in progress)
7979

80-
- **`docs/SPATIAL_PARITY_CHECKLIST.md` created.** 82+ rows covering notes, arrows, camera, selection, clipboard, editing, collab, templates, UI, backlinks, group access. Schema diff table complete. **Test coverage improved:** `DisplayArrow.test.ts` (12 tests) and expanded `DisplayNote.test.ts` (19 tests) added. `selection.test.ts` now covers `bringToTop`. Many rows still lack tests.
80+
- **`docs/SPATIAL_PARITY_CHECKLIST.md` created.** 82+ rows covering notes, arrows, camera, selection, clipboard, editing, collab, templates, UI, backlinks, group access. Schema diff table complete. **Test coverage improved:** `useCanvasActions.test.ts` (3 tests), `useCanvasContextMenu.test.ts` (6 tests), `DisplayArrow.test.ts` (12 tests), `DisplayNote.test.ts` (19 tests) added. `selection.test.ts` covers `bringToTop`. Many rows still lack tests.
8181
- **Left sidebar panels now load real data.** `useUserPageLists` composable wires `GET /api/users/me/pages/recent` and `GET /api/users/me/pages/favorites` into `RecentPagesCard` and `FavoritePagesCard`. Clear handlers call API-backed `clearRecent`/`clearFavorites`.
8282
- **Right sidebar properties panels exist but lack depth.** `NotePropertiesCard.vue`, `ArrowPropertiesCard.vue`, `PagePropertiesCard.vue` are wired and visible, but many legacy properties (wrap, anchor, z-index, timestamps) are not exposed.
8383
- **`MainToolbar.vue` extracted as standalone component.** `PageLayout.vue` now delegates to `MainToolbar.vue` for the header shell. Still missing: page action buttons (insert note/arrow, alignment, formatting), zoom controls other than reset, fit-to-screen, screenshot.
@@ -90,14 +90,14 @@ A criterion is **not met** until the verification command or check passes in CI.
9090
- **No `PageElem` abstraction.** Legacy notes and arrows inherit from `PageElem`, sharing selected/active/editing/visible/region state. New code treats them as completely separate types.
9191
- **`editing` state management implemented.** `useSpatialEditing.ts` tracks which note/arrow is being edited. Escape stops editing; canvas click stops editing; Delete/Backspace is suppressed while editing to avoid deleting selected elements.
9292
- **Container rendering lacks legacy depth.** `stretchChildren`, `wrapChildren`, `originOffset`, and overflow detection are in the model but not enforced in rendering. Spatial vs non-spatial container distinction is not fully implemented.
93-
- **`SpatialPageView.vue` partially refactored.** Keyboard shortcuts extracted to `useSpatialKeyboard.ts`. Box selection, arrow drag, arrow reconnect, and note drag extracted to dedicated composables (`useBoxSelection.ts`, `useArrowDrag.ts`, `useArrowReconnect.ts`, `useNoteDrag.ts`). Note geometry utilities extracted to `note-geometry.ts`. Component reduced from ~740 lines to ~365 lines. Remaining inline logic: context menu handlers, canvas double-click, fit-to-screen.
93+
- **`SpatialPageView.vue` refactored.** Keyboard shortcuts extracted to `useSpatialKeyboard.ts`. Box selection, arrow drag, arrow reconnect, and note drag extracted to dedicated composables. Note geometry utilities extracted to `note-geometry.ts`. Canvas actions (double-click, fit-to-screen) extracted to `useCanvasActions.ts`. Context menu handlers extracted to `useCanvasContextMenu.ts`. Component reduced from ~740 lines to ~260 lines.
9494
- **Selection partially improved.** `bringToTop` zIndex bump on selection is now implemented and tested. Formatting integration across selected editors, active element/region keyboard navigation, and `selectAll` including descendant arrows remain missing.
9595
- **Missing floating UI:** back/forward nav, screenshot, user avatars on canvas.
9696

9797
### Other gaps
9898

9999
- **Realtime notification toast** — only `/notifications` page exists, no badge/toast.
100-
- **Composable size**`useGroupMembersDetail.ts` (103 lines), `usePageCollabEditor.ts` (238 lines), and `useSpatialPage.ts` (195 lines) are all under the 300-line limit. Container logic extracted to `container-ops.ts`. `SpatialPageView.vue` script section reduced from ~740 lines to ~365 lines after extracting `useSpatialKeyboard.ts`, `useBoxSelection.ts`, `useArrowDrag.ts`, `useArrowReconnect.ts`, `useNoteDrag.ts`, and `note-geometry.ts`.
100+
- **Composable size**`useGroupMembersDetail.ts` (103 lines), `usePageCollabEditor.ts` (238 lines), and `useSpatialPage.ts` (195 lines) are all under the 300-line limit. Container logic extracted to `container-ops.ts`. `SpatialPageView.vue` script section reduced from ~740 lines to ~260 lines after extracting keyboard, box selection, arrow drag, arrow reconnect, note drag, note geometry, canvas actions, and context menu handlers into dedicated composables.
101101
- **Auth: `rememberDevice` UI missing in login**`LoginView.vue` has no "Remember this device" checkbox for 2FA login; users are re-prompted every time. API schema already supports it.
102102
- **Auth: no distributed locking** — Legacy used Redlock (`user-lock:${userId}`) around password change, email change, and 2FA mutations. New code relies on DB transactions only.
103103

docs/restart-plan/phase-6-spatial-polish.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Phase 6: Spatial canvas polish
22

33
> **Prerequisites:** Phase 5 done.
4-
> **Status:** In progress (2026-05-31 — `SpatialPageView.vue` refactored. Box selection, arrow drag, arrow reconnect, and note drag extracted into dedicated composables. `note-geometry.ts` and `useBoxSelection.test.ts` added. `useSpatialEditing`, `bringToTop`, line-body arrow geometry, and `useSpatialKeyboard` were extracted in prior work. **New:** `DisplayArrow.test.ts` (12 tests) and expanded `DisplayNote.test.ts` (19 tests) added. `selection.test.ts` now covers `bringToTop` explicitly.)
4+
> **Status:** In progress (2026-05-31 — `SpatialPageView.vue` further refactored. Canvas actions (double-click, fit-to-screen) extracted to `useCanvasActions.ts`; context menu handlers extracted to `useCanvasContextMenu.ts`. Box selection, arrow drag, arrow reconnect, and note drag previously extracted. `useCanvasActions.test.ts` (3 tests) and `useCanvasContextMenu.test.ts` (6 tests) added. `note-geometry.ts` and `useBoxSelection.test.ts` added in prior work. `DisplayArrow.test.ts` (12 tests) and `DisplayNote.test.ts` (19 tests) added. `selection.test.ts` covers `bringToTop`.)
55
66
---
77

@@ -110,7 +110,7 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
110110

111111
## Verification
112112

113-
- [ ] Each deliverable has a test (unit, component, or integration). **Partially improved.** New tests: `note-geometry.test.ts` (8 tests), `useBoxSelection.test.ts` (6 tests), `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests), `DisplayArrow.test.ts` (12 tests), `DisplayNote.test.ts` (19 tests). Major gaps remain: `SpatialPageView.vue` (no component/integration tests), drag/resize end-to-end interaction tests, arrow creation flow tests, sidebar/toolbar integration tests.
113+
- [ ] Each deliverable has a test (unit, component, or integration). **Partially improved.** New tests: `useCanvasActions.test.ts` (3 tests), `useCanvasContextMenu.test.ts` (6 tests), `note-geometry.test.ts` (8 tests), `useBoxSelection.test.ts` (6 tests), `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests), `DisplayArrow.test.ts` (12 tests), `DisplayNote.test.ts` (19 tests). Major gaps remain: `SpatialPageView.vue` (no component/integration tests), drag/resize end-to-end interaction tests, arrow creation flow tests, sidebar/toolbar integration tests.
114114
- [ ] Phase 1 checklist is >80% marked done. **NOT MET.** Strict enforcement of the checklist's "Done = implemented + passing test" rule drops the true completion rate well below 80%.
115115

116116
---
@@ -127,7 +127,7 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
127127
- [x] `MainToolbar`, `LeftSidebar`, `RightSidebar`, and `TableContextMenu` are implemented as standalone shadcn components and visible on `/pages/:pageId`.
128128
- [x] Sidebar panels (`RecentPages`, `FavoritePages`) display real data from API.
129129
- [x] Arrow geometry reads actual note heights instead of hardcoding `80px`.
130-
- [ ] `SpatialPageView.vue` is refactored to avoid god-component anti-pattern. **Partial.** Keyboard shortcuts extracted to `useSpatialKeyboard.ts`; box selection extracted to `useBoxSelection.ts`; arrow drag extracted to `useArrowDrag.ts`; arrow reconnect extracted to `useArrowReconnect.ts`; note drag extracted to `useNoteDrag.ts`; note geometry extracted to `note-geometry.ts`. Component reduced from ~740 to ~365 lines. Remaining inline: context menu handlers, fit-to-screen, canvas double-click.
130+
- [x] `SpatialPageView.vue` is refactored to avoid god-component anti-pattern. Keyboard shortcuts extracted to `useSpatialKeyboard.ts`; box selection extracted to `useBoxSelection.ts`; arrow drag extracted to `useArrowDrag.ts`; arrow reconnect extracted to `useArrowReconnect.ts`; note drag extracted to `useNoteDrag.ts`; note geometry extracted to `note-geometry.ts`; canvas actions extracted to `useCanvasActions.ts`; context menu handlers extracted to `useCanvasContextMenu.ts`. Component reduced from ~740 to ~260 lines.
131131
- [x] Selection implements `bringToTop`. Formatting integration and active element/region navigation remain missing.
132132
- [ ] Container rendering enforces `stretchChildren`, `wrapChildren`, and spatial vs non-spatial layout modes.
133133
- [ ] Manual QA session with 3+ users finds no blocking usability issues.

new-deepnotes/apps/web/src/features/spatial/SpatialPageView.vue

Lines changed: 28 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import { useSpatialSelection } from "./selection";
1414
import { useSpatialEditing } from "./useSpatialEditing";
1515
import { useSpatialUndoRedo } from "./undo-redo";
1616
import { useSpatialKeyboard } from "./useSpatialKeyboard";
17-
import { screenToWorld } from "./spatial-viewport-math";
1817
import { provideNoteHeights } from "./useNoteHeights";
1918
import { useBoxSelection } from "./useBoxSelection";
2019
import { useArrowDrag } from "./useArrowDrag";
2120
import { useArrowReconnect } from "./useArrowReconnect";
2221
import { useNoteDrag } from "./useNoteDrag";
23-
import { copySelection, pastePayload } from "./clipboard";
22+
import { useCanvasActions } from "./useCanvasActions";
23+
import { useCanvasContextMenu } from "./useCanvasContextMenu";
2424
import type { ClipboardNote, ClipboardArrow } from "./clipboard";
2525
2626
const props = defineProps<{
@@ -112,13 +112,6 @@ const pasteCount = ref(0);
112112
// --- find/replace dialog state ---
113113
const findReplaceOpen = ref(false);
114114
115-
// --- context menu state ---
116-
const contextMenu = ref<{
117-
open: boolean;
118-
x: number;
119-
y: number;
120-
}>({ open: false, x: 0, y: 0 });
121-
122115
const noteById = computed(() => {
123116
const map = new Map<string, (typeof noteList.value)[0]["model"]>();
124117
for (const n of noteList.value) {
@@ -174,165 +167,33 @@ const {
174167
moveNoteOutOfContainer,
175168
});
176169
177-
function onCanvasDoubleClick(e: MouseEvent) {
178-
const canvas = canvasRef.value;
179-
if (!canvas || !canvas.rootEl) return;
180-
181-
const rect = canvas.rootEl.getBoundingClientRect();
182-
const cx = rect.left + rect.width / 2;
183-
const cy = rect.top + rect.height / 2;
184-
185-
const world = screenToWorld(
186-
e.clientX,
187-
e.clientY,
188-
cx,
189-
cy,
190-
canvas.camX,
191-
canvas.camY,
192-
canvas.zoom,
193-
);
194-
195-
createNoteAt(world.x, world.y, props.defaultNoteTemplate);
196-
}
197-
198-
function fitToScreen() {
199-
const canvas = canvasRef.value;
200-
if (!canvas) return;
201-
202-
// Calculate bounding box of all root notes
203-
if (rootNoteList.value.length === 0) {
204-
canvas.resetView();
205-
return;
206-
}
207-
208-
let minX = Infinity;
209-
let minY = Infinity;
210-
let maxX = -Infinity;
211-
let maxY = -Infinity;
212-
213-
for (const note of rootNoteList.value) {
214-
const wStr = note.model.width.value.expanded;
215-
const w = wStr === "Auto" ? 160 : parseFloat(wStr);
216-
const h = 80; // Default height estimate
217-
const x = note.model.pos.value.x;
218-
const y = note.model.pos.value.y;
219-
220-
minX = Math.min(minX, x);
221-
minY = Math.min(minY, y);
222-
maxX = Math.max(maxX, x + w);
223-
maxY = Math.max(maxY, y + h);
224-
}
225-
226-
canvas.fitToScreen({ minX, minY, maxX, maxY }, 40);
227-
}
228-
229-
function onCanvasContextMenu(e: MouseEvent) {
230-
e.preventDefault();
231-
const canvas = canvasRef.value;
232-
if (!canvas || !canvas.rootEl) return;
233-
234-
const rect = canvas.rootEl.getBoundingClientRect();
235-
const cx = rect.left + rect.width / 2;
236-
const cy = rect.top + rect.height / 2;
237-
238-
const world = screenToWorld(
239-
e.clientX,
240-
e.clientY,
241-
cx,
242-
cy,
243-
canvas.camX,
244-
canvas.camY,
245-
canvas.zoom,
246-
);
247-
248-
contextMenu.value = {
249-
open: true,
250-
x: e.clientX,
251-
y: e.clientY,
252-
};
253-
}
254-
255-
function handleContextMenuCreateNote(x: number, y: number) {
256-
const canvas = canvasRef.value;
257-
if (!canvas || !canvas.rootEl) return;
258-
259-
const rect = canvas.rootEl.getBoundingClientRect();
260-
const cx = rect.left + rect.width / 2;
261-
const cy = rect.top + rect.height / 2;
262-
263-
const world = screenToWorld(
264-
x,
265-
y,
266-
cx,
267-
cy,
268-
canvas.camX,
269-
canvas.camY,
270-
canvas.zoom,
271-
);
272-
273-
createNoteAt(world.x, world.y, props.defaultNoteTemplate);
274-
}
275-
276-
async function handleContextMenuPaste(payload: { notes: ClipboardNote[]; arrows: ClipboardArrow[] }) {
277-
const canvas = canvasRef.value;
278-
const centerX = canvas?.camX ?? 0;
279-
const centerY = canvas?.camY ?? 0;
280-
const offset = pasteCount.value * 32;
281-
pasteCount.value += 1;
282-
283-
const result = pastePayload(payload, {
284-
createNote: createNoteAt,
285-
createArrow: createArrow,
286-
offsetX: centerX + offset,
287-
offsetY: centerY + offset,
288-
});
289-
290-
selection.clear();
291-
for (const id of result.noteIds) {
292-
selection.select(id, "note", true);
293-
}
294-
}
295-
296-
function handleContextMenuDeleteSelected() {
297-
for (const id of selection.selectedOfKind("note")) {
298-
deleteNote(id);
299-
}
300-
for (const id of selection.selectedOfKind("arrow")) {
301-
deleteArrow(id);
302-
}
303-
selection.clear();
304-
}
305-
306-
async function handleContextMenuCopySelected() {
307-
const selectedNotes = noteList.value.filter((n) =>
308-
selection.isSelected(n.id),
309-
);
310-
const selectedArrows = arrowList.value.filter((a) =>
311-
selection.isSelected(a.id),
312-
);
313-
if (selectedNotes.length > 0) {
314-
await copySelection(selectedNotes, selectedArrows);
315-
}
316-
}
170+
const { onCanvasDoubleClick, fitToScreen } = useCanvasActions({
171+
canvasRef,
172+
rootNoteList,
173+
createNoteAt,
174+
defaultNoteTemplate: props.defaultNoteTemplate,
175+
});
317176
318-
async function handleContextMenuCutSelected() {
319-
const selectedNotes = noteList.value.filter((n) =>
320-
selection.isSelected(n.id),
321-
);
322-
const selectedArrows = arrowList.value.filter((a) =>
323-
selection.isSelected(a.id),
324-
);
325-
if (selectedNotes.length > 0) {
326-
await copySelection(selectedNotes, selectedArrows);
327-
for (const id of selection.selectedOfKind("note")) {
328-
deleteNote(id);
329-
}
330-
for (const id of selection.selectedOfKind("arrow")) {
331-
deleteArrow(id);
332-
}
333-
selection.clear();
334-
}
335-
}
177+
const {
178+
contextMenu,
179+
onCanvasContextMenu,
180+
handleContextMenuCreateNote,
181+
handleContextMenuPaste,
182+
handleContextMenuDeleteSelected,
183+
handleContextMenuCopySelected,
184+
handleContextMenuCutSelected,
185+
} = useCanvasContextMenu({
186+
canvasRef,
187+
selection,
188+
noteList,
189+
arrowList,
190+
createNoteAt,
191+
createArrow,
192+
deleteNote,
193+
deleteArrow,
194+
defaultNoteTemplate: props.defaultNoteTemplate,
195+
pasteCount,
196+
});
336197
337198
// --- keyboard shortcuts ---
338199
const { onKeyDown } = useSpatialKeyboard({

0 commit comments

Comments
 (0)