Skip to content

Commit 4a57c05

Browse files
committed
feat: phase 6
1 parent 437d90a commit 4a57c05

11 files changed

Lines changed: 577 additions & 194 deletions

File tree

docs/restart-plan/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DeepNotes Restart Plan — Index
22

3-
> **Last updated:** 2026-05-31 (Phase 6 re-evaluated. Status corrected from "Complete" to "In Progress". See `phase-6-spatial-polish.md` and `SPATIAL_PARITY_CHECKLIST.md` for details. Phase 9 pending.)
3+
> **Last updated:** 2026-05-31 (Phase 6 in progress. `useSpatialEditing`, `bringToTop`, line-body arrow geometry, and `useSpatialKeyboard` extracted. See `phase-6-spatial-polish.md` and `SPATIAL_PARITY_CHECKLIST.md` for details. Phase 9 pending.)
44
> **This document replaces `docs/RESTART_PLAN.md`.** If a prior statement conflicts with this one, this version wins.
55
66
---
@@ -86,12 +86,12 @@ A criterion is **not met** until the verification command or check passes in CI.
8686
- **Note drag `Teleport` overlay fixed.** Overlay now applies `scale(zoom)` and uses `posOverride` so the preview tracks the cursor correctly at all zoom levels.
8787
- **Page state screens exist but 4 states are indistinguishable.** `page-deleted`, `group-deleted`, `invited`, `rejected` all map to the same generic error UI because the API does not return distinct error codes.
8888
- **Context menu exists for canvas but not for individual notes.** `CanvasContextMenu.vue` (right-click on empty canvas) is implemented. No per-note context menu exists.
89-
- **Arrow geometry is oversimplified.** New `DisplayArrow.vue` uses center-point math. Legacy had rectangle-edge intersection for `bodyType === 'line'`, interregional coordinate transforms, and `fakePos`/`looseEndpoint` rendering.
89+
- **Arrow geometry partially fixed.** `DisplayArrow.vue` now uses rectangle-edge intersection for `bodyType === 'line'` via `arrow-geometry.ts`. Interregional coordinate transforms and `fakePos`/`looseEndpoint` rendering remain missing.
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.
91-
- **No `editing` state management.** Legacy tracks which element is being edited, stopping editing when clicking elsewhere. New relies on Tiptap's internal focus, which can lead to conflicting edits.
91+
- **`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` is a 1,070-line god component.** Legacy distributed responsibility across `Page`, `PageNotes`, `PageArrows`, `PageSelection`, `PageCamera`, `NoteDragging`, `NoteResizing`, etc. The monolithic component violates the spirit of the "No composable > 300 lines" success criterion.
94-
- **Selection lacks legacy depth.** No `bringToTop` on selection, no formatting integration across selected editors, no active element/region meaningful UI or keyboard navigation. `selectAll` only selects root notes, not descendant arrows.
93+
- **`SpatialPageView.vue` partially refactored.** Keyboard shortcuts (~150 lines) extracted to `useSpatialKeyboard.ts`. Still a large component; further extraction needed for drag, resize, box-select, and arrow-reconnection logic.
94+
- **Selection partially improved.** `bringToTop` zIndex bump on selection is now implemented. 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

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

Lines changed: 9 additions & 7 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 — status corrected after evaluation. Multiple "Done" items were over-reported; see notes below.)
4+
> **Status:** In progress (2026-05-31 — `useSpatialEditing`, `bringToTop`, line-body arrow geometry, and `useSpatialKeyboard` extracted since last evaluation. Multiple "Done" items were over-reported; see notes below.)
55
66
---
77

@@ -20,6 +20,8 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
2020
| Box selection (drag on empty canvas) | **Done** | Threshold-based drag-to-box-select implemented |
2121
| Select all (`Ctrl+A`) | **Done** | `onKeyDown` in `SpatialPageView.vue` |
2222
| Active element / active region tracking | **Partial** | `useSpatialSelection` has `activeId` and `activeRegionId` ref but no real active-region UI or keyboard navigation |
23+
| `bringToTop` on selection | **Done** | `useSpatialSelection` bumps selected note `zIndex` above other selected notes |
24+
| `editing` state management | **Done** | `useSpatialEditing.ts` tracks editing note/arrow; Escape and canvas click stop editing; Delete suppressed while editing |
2325

2426
### 2. Containers
2527
| Item | Status | Notes |
@@ -81,12 +83,12 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
8183
| Item | Status | Notes |
8284
|------|--------|-------|
8385
| Curve body (`CurveArrow.vue`) | **Done** | Quadratic bezier with perpendicular offset; `bodyType === 'curve'` |
84-
| Line body (`LineArrow.vue`) | **Done** | Straight line when `bodyType === 'line'` |
86+
| Line body (`LineArrow.vue`) | **Done** | Straight line when `bodyType === 'line'`; rectangle-edge intersection via `arrow-geometry.ts` |
8587
| Arrow heads (`OpenHead.vue`) | **Done** | SVG `<marker>` chevron heads; `sourceHead`/`targetHead` supported |
8688
| Arrow label (editable `Y.XmlFragment`) | **Done** | `NoteTiptapEditor` at midpoint. Proper collaborative rich-text editing on `Y.XmlFragment` |
8789
| Hitbox (thick invisible stroke) | **Done** | `stroke="transparent" stroke-width="20"` pointer-events-auto hitbox |
8890
| Drag-to-reconnect | **Done** | Connection zones + `onReconnectPointerMove/Up` in `SpatialPageView.vue` wired |
89-
| Arrow source/target anchor positioning | **Done** | `DisplayArrow.vue` geometry now uses `sourceAnchor`/`targetAnchor` when provided |
91+
| Arrow source/target anchor positioning | **Done** | `DisplayArrow.vue` geometry uses `sourceAnchor`/`targetAnchor` when provided; line body falls back to rectangle-edge intersection |
9092
| Color matching note color logic | **Partial** | Same hardcoded 10-color map used, but `inherit` logic may not cascade correctly for arrows |
9193

9294
### 9. Find and replace
@@ -108,7 +110,7 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
108110

109111
## Verification
110112

111-
- [ ] Each deliverable has a test (unit, component, or integration). **Major gaps remain:** `DisplayNote.vue` (only basic render tests), `DisplayArrow.vue` (no tests), `SpatialPageView.vue` (no component/integration tests), drag/resize interaction tests, box selection tests, arrow creation/reconnection tests, sidebar/toolbar integration tests.
113+
- [ ] Each deliverable has a test (unit, component, or integration). **Partially improved.** New tests: `arrow-geometry.test.ts` (5 tests), `useSpatialEditing.test.ts` (4 tests). Major gaps remain: `DisplayNote.vue` (only basic render tests), `DisplayArrow.vue` (no component tests), `SpatialPageView.vue` (no component/integration tests), drag/resize interaction tests, box selection tests, arrow creation/reconnection tests, sidebar/toolbar integration tests.
112114
- [ ] 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%.
113115

114116
---
@@ -121,11 +123,11 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
121123
- [x] `PageEditorView.vue` renders as a full-screen immersive shell (no scrolling card page).
122124
- [x] All 8 dedicated page-state screens exist and are reachable. (`page-deleted`/`group-deleted`/`invited`/`rejected` are indistinguishable without richer API error codes.)
123125
- [ ] `DisplayNote.vue` matches legacy note visuals. **PARTIAL.** Colors use hardcoded 10-color map instead of legacy `colorNameToColorHex` with `lightenByRatio`. Drop zones, arrow handles, and frame styling are simplified. No custom scrollbar handling.
124-
- [ ] `DisplayArrow.vue` supports full legacy arrow behavior. **PARTIAL.** Curve/line bodies and heads work, but line body lacks rectangle-edge intersection, interregional arrows don't transform coordinate spaces, and `fakePos`/`looseEndpoint` are not rendered.
126+
- [ ] `DisplayArrow.vue` supports full legacy arrow behavior. **PARTIAL.** Curve/line bodies and heads work; line body now has rectangle-edge intersection. Interregional arrows don't transform coordinate spaces; `fakePos`/`looseEndpoint` are not rendered.
125127
- [x] `MainToolbar`, `LeftSidebar`, `RightSidebar`, and `TableContextMenu` are implemented as standalone shadcn components and visible on `/pages/:pageId`.
126128
- [x] Sidebar panels (`RecentPages`, `FavoritePages`) display real data from API.
127129
- [x] Arrow geometry reads actual note heights instead of hardcoding `80px`.
128-
- [ ] `SpatialPageView.vue` is refactored to avoid god-component anti-pattern. Currently 1,070 lines.
129-
- [ ] Selection implements `bringToTop`, formatting integration, and active element/region navigation.
130+
- [ ] `SpatialPageView.vue` is refactored to avoid god-component anti-pattern. Keyboard shortcuts extracted to `useSpatialKeyboard.ts`; drag, resize, box-select, and arrow-reconnection logic still inline.
131+
- [x] Selection implements `bringToTop`. Formatting integration and active element/region navigation remain missing.
130132
- [ ] Container rendering enforces `stretchChildren`, `wrapChildren`, and spatial vs non-spatial layout modes.
131133
- [ ] Manual QA session with 3+ users finds no blocking usability issues.

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

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ArrowModel } from "./arrow-model";
44
import type { NoteModel } from "./note-model";
55
import NoteTiptapEditor from "./NoteTiptapEditor.vue";
66
import { useNoteHeights } from "./useNoteHeights";
7+
import { computeArrowEndpoints } from "./arrow-geometry";
78
89
const props = defineProps<{
910
id: string;
@@ -17,6 +18,7 @@ const emit = defineEmits<{
1718
select: [];
1819
toggle: [];
1920
reconnectStart: [arrowId: string, from: 'source' | 'target']
21+
"edit-start": [];
2022
}>();
2123
2224
const labelFragment = computed(() => props.model.label.value);
@@ -46,30 +48,50 @@ const geometry = computed(() => {
4648
const t = props.targetModel;
4749
if (!s || !t) return null;
4850
49-
// Use anchor positions if provided, otherwise use note centers
5051
const w1 = s.width.value.expanded;
5152
const nw1 = w1 === "Auto" ? 160 : parseFloat(w1);
5253
const h1 = noteHeights.value.get(props.model.source.value) ?? 80;
53-
54-
const sourceAnchor = props.model.sourceAnchor.value;
55-
const x1 = sourceAnchor
56-
? s.pos.value.x + sourceAnchor.x
57-
: s.pos.value.x + nw1 / 2;
58-
const y1 = sourceAnchor
59-
? s.pos.value.y + sourceAnchor.y
60-
: s.pos.value.y + h1 / 2;
6154
6255
const w2 = t.width.value.expanded;
6356
const nw2 = w2 === "Auto" ? 160 : parseFloat(w2);
6457
const h2 = noteHeights.value.get(props.model.target.value) ?? 80;
65-
58+
59+
const sourceAnchor = props.model.sourceAnchor.value;
6660
const targetAnchor = props.model.targetAnchor.value;
67-
const x2 = targetAnchor
68-
? t.pos.value.x + targetAnchor.x
69-
: t.pos.value.x + nw2 / 2;
70-
const y2 = targetAnchor
71-
? t.pos.value.y + targetAnchor.y
72-
: t.pos.value.y + h2 / 2;
61+
62+
let x1 = s.pos.value.x + nw1 / 2;
63+
let y1 = s.pos.value.y + h1 / 2;
64+
let x2 = t.pos.value.x + nw2 / 2;
65+
let y2 = t.pos.value.y + h2 / 2;
66+
67+
if (sourceAnchor) {
68+
x1 = s.pos.value.x + sourceAnchor.x;
69+
y1 = s.pos.value.y + sourceAnchor.y;
70+
}
71+
72+
if (targetAnchor) {
73+
x2 = t.pos.value.x + targetAnchor.x;
74+
y2 = t.pos.value.y + targetAnchor.y;
75+
} else if (props.model.bodyType.value === "line") {
76+
// For line body, use rectangle-edge intersection
77+
const endpoints = computeArrowEndpoints(
78+
s.pos.value,
79+
t.pos.value,
80+
nw1,
81+
h1,
82+
nw2,
83+
h2,
84+
true,
85+
);
86+
if (!sourceAnchor) {
87+
x1 = endpoints.x1;
88+
y1 = endpoints.y1;
89+
}
90+
if (!targetAnchor) {
91+
x2 = endpoints.x2;
92+
y2 = endpoints.y2;
93+
}
94+
}
7395
7496
const minX = Math.min(x1, x2);
7597
const minY = Math.min(y1, y2);
@@ -223,7 +245,7 @@ function onPointerDown(e: PointerEvent) {
223245
height="32"
224246
class="pointer-events-auto"
225247
>
226-
<div class="h-full w-full">
248+
<div class="h-full w-full" @focusin="emit('edit-start')">
227249
<NoteTiptapEditor
228250
:fragment="labelFragment"
229251
:editable="!props.model.readOnly.value"

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const emit = defineEmits<{
2323
dragstart: [id: string];
2424
dragend: [id: string];
2525
arrowDragStart: [payload: { noteId: string }];
26+
"edit-start": [];
2627
}>();
2728
2829
const rootRef = ref<HTMLElement | null>(null);
@@ -279,6 +280,7 @@ function toggleCollapsed() {
279280
v-if="model.head.enabled.value && !model.collapsing.collapsed.value"
280281
class="px-2 pt-1"
281282
@pointerdown.stop
283+
@focusin="emit('edit-start')"
282284
>
283285
<NoteTiptapEditor
284286
:fragment="headFrag!"
@@ -292,6 +294,7 @@ function toggleCollapsed() {
292294
v-if="model.body.enabled.value && !model.collapsing.collapsed.value"
293295
class="px-2 pb-1"
294296
@pointerdown.stop
297+
@focusin="emit('edit-start')"
295298
>
296299
<NoteTiptapEditor
297300
:fragment="bodyFrag!"

0 commit comments

Comments
 (0)