Skip to content

Commit 697e9f9

Browse files
committed
docs: update spatial parity checklist to 223 tests passing and implement dynamic container originOffset tracking - useNoteHeights now tracks originOffsets map, DisplayNote.vue measures actual container content area offset and publishes it, getNoteEffectiveWorldPos/getNoteRect accept originOffsets parameter (defaults to 48px), all geometry callers (useBoxSelection, useNoteDrag, useArrowReconnect, useCanvasActions) use dynamic offsets instead of hardcoded 48px
1 parent aa077f1 commit 697e9f9

12 files changed

Lines changed: 75 additions & 26 deletions

docs/SPATIAL_PARITY_CHECKLIST.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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-
- [x] Every "Done" item has a passing automated test. **222 tests passing across 30 test files in `features/spatial/` as of 2026-06-01.** Test gaps filled: `SpatialPageView.test.ts` now covers delete note (1.2), teleport overlay (1.15), z-index ordering (1.21), zoom reset (4.6), zoom indicator (4.8), selection count (5.7), undo/redo buttons (8.3). `CollabAvatars.test.ts` (4 tests) added for user avatars on canvas (12.27). `PagePropertiesCard.test.ts` (5 tests) added for page properties (12.13). Partial items promoted to Done: note drop zones (1.16), arrow handles (1.17), note frame styling (1.19), scrollbar handling (1.23), arrow color matching (3.12), active element tracking (5.5), search across note head/body (9.2), awareness / remote user avatars (10.4), note properties (12.11), arrow properties (12.12), page properties (12.13), canvas context menu (12.17). **Quality issues fixed (2026-06-01):** `note-geometry.ts:getNoteRect` now accepts `heights` parameter from `useNoteHeights` instead of hardcoding `80px`; `useArrowDrag.ts` uses actual note heights for source origin; `useCanvasActions.fitToScreen` considers selection bounds first; `color-utils.ts` adds `lightenColor` / `resolveNoteColorVariants` for legacy color parity; `note-editor-registry.ts` enables cross-selection formatting (`Ctrl+B/I/U`); `Tab`/`Shift+Tab`/`Enter` adds active element keyboard navigation; `DisplayArrow.vue` renders interregional arrows with `fakePos`/`looseEndpoint` fallback. **Remaining quality issues:** container layout lacks legacy `originOffset`/`overflow`/`islandRect` computations.
279+
- [x] Every "Done" item has a passing automated test. **223 tests passing across 30 test files in `features/spatial/` as of 2026-06-01.** Test gaps filled: `SpatialPageView.test.ts` now covers delete note (1.2), teleport overlay (1.15), z-index ordering (1.21), zoom reset (4.6), zoom indicator (4.8), selection count (5.7), undo/redo buttons (8.3). `CollabAvatars.test.ts` (4 tests) added for user avatars on canvas (12.27). `PagePropertiesCard.test.ts` (5 tests) added for page properties (12.13). Partial items promoted to Done: note drop zones (1.16), arrow handles (1.17), note frame styling (1.19), scrollbar handling (1.23), arrow color matching (3.12), active element tracking (5.5), search across note head/body (9.2), awareness / remote user avatars (10.4), note properties (12.11), arrow properties (12.12), page properties (12.13), canvas context menu (12.17). **Quality issues fixed (2026-06-01):** `note-geometry.ts:getNoteRect` now accepts `heights` parameter from `useNoteHeights` instead of hardcoding `80px`; `useArrowDrag.ts` uses actual note heights for source origin; `useCanvasActions.fitToScreen` considers selection bounds first; `color-utils.ts` adds `lightenColor` / `resolveNoteColorVariants` for legacy color parity; `note-editor-registry.ts` enables cross-selection formatting (`Ctrl+B/I/U`); `Tab`/`Shift+Tab`/`Enter` adds active element keyboard navigation; `DisplayArrow.vue` renders interregional arrows with `fakePos`/`looseEndpoint` fallback; `useNoteHeights` now tracks dynamic `originOffsets` — `getNoteEffectiveWorldPos` and all callers use actual container content area offset instead of hardcoded 48px. **Remaining quality issues:** container `overflow` boolean tracking and `islandRect`/`relativeRect` computations are not implemented.
280280
- [x] Phase 6 is not declared done until ≥ 80% of rows are **Done**. **MET.** 72 of 82 rows (88%) are now Done.
281281

282282
---

docs/restart-plan/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ All major deliverables implemented and tested. 88% of `docs/SPATIAL_PARITY_CHECK
9090

9191
**Fixed (2026-06-01):**
9292
6. ~~**Interregional arrows are schema-only.**~~ `DisplayArrow.vue` now renders arrows with `fakePos` fallback when `sourceModel`/`targetModel` is missing. `looseEndpoint` indicators shown as endpoint circles.
93+
7. ~~**Container layout is functional but simplified.**~~ `useNoteHeights` now tracks dynamic `originOffsets` per note. `DisplayNote.vue` measures actual container content area offset; `getNoteEffectiveWorldPos` uses it instead of hardcoded 48px.
9394

9495
**Remaining (non-blocking):**
95-
7. **Container layout is functional but simplified.** Missing legacy `originOffset`/`overflow`/`islandRect` computations.
96-
8. **Loading overlay polish (12.20)** remains partial.
96+
8. **Container overflow and island computations.** `overflow` boolean tracking and `islandRect`/`relativeRect` are not implemented.
97+
9. **Loading overlay polish (12.20)** remains partial.
9798

9899
### Phase 9 — Production Readiness (in progress)
99100

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,13 @@ An independent codebase audit compared legacy (`apps/client/src/code/pages/page/
131131
4. ~~**Color system is simplified.**~~ **FIXED (2026-06-01).** Added `color-utils.ts` with `lightenColor` and `resolveNoteColorVariants` functions that replicate legacy `lightenByRatio` behavior. `DisplayNote.vue` and `DisplayArrow.vue` now use `base`/`light`/`highlight` variants instead of flat 10-color map.
132132
5. ~~**Selection formatting integration is missing.**~~ **FIXED (2026-06-01).** Created `note-editor-registry.ts` for tracking Tiptap editors per note. `Ctrl+B/I/U` now applies bold/italic/underline across all selected note editors (head + body). `NoteTiptapEditor.vue` registers its editor on mount.
133133
6. ~~**Active region tracking is partial.**~~ **FIXED (2026-06-01).** `Tab`/`Shift+Tab` now cycles through selected notes as the active element. `Enter` starts editing the active note. Basic keyboard navigation wired in `useSpatialKeyboard.ts`.
134-
7. **Container layout is functional but simplified.** Legacy containers have `originOffset`, `overflow`, island region tracking, and complex `relativeRect`/`islandRect` computations. New containers handle `spatial` vs `horizontal`/`flex` + `wrap`/`stretch` but lack the recursive spatial origin system.
134+
7. ~~**Container layout is functional but simplified.**~~ **PARTIALLY FIXED (2026-06-01).** `useNoteHeights` now tracks dynamic `originOffsets` per note. `DisplayNote.vue` measures and publishes the actual container content area offset instead of hardcoding 48px. `getNoteEffectiveWorldPos` and all geometry callers (`useBoxSelection`, `useNoteDrag`, `useArrowReconnect`) use the dynamic offset. Remaining: `overflow` boolean tracking and `islandRect`/`relativeRect` computations are not implemented.
135135

136136
---
137137

138138
## Verification
139139

140-
- [x] Each deliverable has a test (unit, component, or integration). **Met.** 222 tests passing across 30 test files in `features/spatial/`.
140+
- [x] Each deliverable has a test (unit, component, or integration). **Met.** 223 tests passing across 30 test files in `features/spatial/`.
141141
- [x] Phase 1 checklist is >80% marked done. **MET.** 72 of 82 rows (88%) are Done.
142142

143143
---

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ const emit = defineEmits<{
3131
}>();
3232
3333
const rootRef = ref<HTMLElement | null>(null);
34-
const { heights: noteHeights } = useNoteHeights();
34+
const containerChildrenRef = ref<HTMLElement | null>(null);
35+
const { heights: noteHeights, originOffsets: noteOriginOffsets } = useNoteHeights();
3536
3637
function publishHeight() {
3738
const el = rootRef.value;
@@ -40,8 +41,23 @@ function publishHeight() {
4041
}
4142
}
4243
43-
onMounted(publishHeight);
44-
onUpdated(publishHeight);
44+
function publishOriginOffset() {
45+
const root = rootRef.value;
46+
const children = containerChildrenRef.value;
47+
if (root && children && props.model.container.enabled.value) {
48+
const offset = children.offsetTop - root.offsetTop;
49+
noteOriginOffsets.value.set(props.id, offset);
50+
}
51+
}
52+
53+
onMounted(() => {
54+
publishHeight();
55+
publishOriginOffset();
56+
});
57+
onUpdated(() => {
58+
publishHeight();
59+
publishOriginOffset();
60+
});
4561
4662
const colorVariants = computed(() => {
4763
const c = props.model.color.value;
@@ -354,6 +370,7 @@ function onContextMenu(e: MouseEvent) {
354370
<!-- container children -->
355371
<template v-if="model.container.enabled.value && childModels?.length && !model.collapsing.collapsed.value">
356372
<div
373+
ref="containerChildrenRef"
357374
data-testid="container-children"
358375
class="absolute inset-x-0 bottom-0 overflow-visible"
359376
:class="[

new-deepnotes/apps/web/src/features/spatial/note-geometry.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("note-geometry", () => {
3535
expect(getNoteEffectiveWorldPos("n1", noteList, parentOf)).toEqual({ x: 10, y: 20 });
3636
});
3737

38-
it("returns offset position when note is inside a container", () => {
38+
it("returns offset position when note is inside a container (default 48)", () => {
3939
const noteList = [
4040
{
4141
id: "parent",
@@ -57,6 +57,29 @@ describe("note-geometry", () => {
5757
});
5858
});
5959

60+
it("uses custom originOffset when provided", () => {
61+
const noteList = [
62+
{
63+
id: "parent",
64+
model: {
65+
pos: { value: { x: 100, y: 200 } },
66+
} as unknown as NoteModel,
67+
},
68+
{
69+
id: "child",
70+
model: {
71+
pos: { value: { x: 10, y: 20 } },
72+
} as unknown as NoteModel,
73+
},
74+
];
75+
const parentOf = new Map<string, string>([["child", "parent"]]);
76+
const originOffsets = new Map([["parent", 72]]);
77+
expect(getNoteEffectiveWorldPos("child", noteList, parentOf, originOffsets)).toEqual({
78+
x: 110,
79+
y: 200 + 20 + 72,
80+
});
81+
});
82+
6083
it("returns null for missing note", () => {
6184
expect(getNoteEffectiveWorldPos("missing", [], new Map())).toBeNull();
6285
});

new-deepnotes/apps/web/src/features/spatial/note-geometry.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function getNoteEffectiveWorldPos(
2424
noteId: string,
2525
noteList: { id: string; model: NoteModel }[],
2626
parentOf: Map<string, string>,
27+
originOffsets?: Map<string, number>,
2728
): { x: number; y: number } | null {
2829
const entry = noteList.find((n) => n.id === noteId);
2930
if (!entry) return null;
@@ -33,12 +34,10 @@ export function getNoteEffectiveWorldPos(
3334
}
3435
const parent = noteList.find((n) => n.id === parentId);
3536
if (!parent) return { x: entry.model.pos.value.x, y: entry.model.pos.value.y };
37+
const offset = originOffsets?.get(parentId) ?? 48;
3638
return {
3739
x: parent.model.pos.value.x + entry.model.pos.value.x,
38-
y:
39-
parent.model.pos.value.y +
40-
entry.model.pos.value.y +
41-
48 /* container content offset */,
40+
y: parent.model.pos.value.y + entry.model.pos.value.y + offset,
4241
};
4342
}
4443

@@ -47,10 +46,11 @@ export function getNoteRect(
4746
noteList: { id: string; model: NoteModel }[],
4847
parentOf: Map<string, string>,
4948
heights?: Map<string, number>,
49+
originOffsets?: Map<string, number>,
5050
): NoteRect | null {
5151
const entry = noteList.find((n) => n.id === noteId);
5252
if (!entry) return null;
53-
const pos = getNoteEffectiveWorldPos(noteId, noteList, parentOf);
53+
const pos = getNoteEffectiveWorldPos(noteId, noteList, parentOf, originOffsets);
5454
if (!pos) return null;
5555
const wStr = entry.model.width.value.expanded;
5656
const w = wStr === "Auto" ? 160 : parseFloat(wStr);

new-deepnotes/apps/web/src/features/spatial/useArrowReconnect.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface UseArrowReconnectInput {
1919
}
2020

2121
export function useArrowReconnect(input: UseArrowReconnectInput) {
22-
const { heights: noteHeights } = useNoteHeights();
22+
const { heights: noteHeights, originOffsets: noteOriginOffsets } = useNoteHeights();
2323
const reconnectingArrowId = ref<string | null>(null);
2424
const reconnectingFrom = ref<'source' | 'target' | null>(null);
2525
const hoveredNoteId = ref<string | null>(null);
@@ -57,7 +57,7 @@ export function useArrowReconnect(input: UseArrowReconnectInput) {
5757
let bestNoteId: string | null = null;
5858

5959
for (const note of input.noteList.value) {
60-
const noteRect = getNoteRect(note.id, input.noteList.value, input.parentOf.value, noteHeights.value);
60+
const noteRect = getNoteRect(note.id, input.noteList.value, input.parentOf.value, noteHeights.value, noteOriginOffsets.value);
6161
if (!noteRect) continue;
6262

6363
if (

new-deepnotes/apps/web/src/features/spatial/useBoxSelection.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe("useBoxSelection", () => {
1414
beforeEach(() => {
1515
(useNoteHeights as ReturnType<typeof vi.fn>).mockReturnValue({
1616
heights: ref(new Map<string, number>()),
17+
originOffsets: ref(new Map<string, number>()),
1718
});
1819
});
1920

new-deepnotes/apps/web/src/features/spatial/useBoxSelection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface UseBoxSelectionInput {
3030
}
3131

3232
export function useBoxSelection(input: UseBoxSelectionInput) {
33-
const { heights: noteHeights } = useNoteHeights();
33+
const { heights: noteHeights, originOffsets: noteOriginOffsets } = useNoteHeights();
3434
let boxState: BoxSelectionState | null = null;
3535

3636
function onCanvasPointerDown(e: PointerEvent) {
@@ -98,7 +98,7 @@ export function useBoxSelection(input: UseBoxSelectionInput) {
9898
const boxH = Math.max(w1.y, w2.y) - boxY;
9999

100100
for (const note of input.rootNoteList.value) {
101-
const noteRect = getNoteRect(note.id, input.noteList.value, input.parentOf.value, noteHeights.value);
101+
const noteRect = getNoteRect(note.id, input.noteList.value, input.parentOf.value, noteHeights.value, noteOriginOffsets.value);
102102
if (!noteRect) continue;
103103

104104
if (

new-deepnotes/apps/web/src/features/spatial/useCanvasActions.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ vi.mock("./useNoteHeights", () => ({
88
}));
99

1010
describe("useCanvasActions", () => {
11-
function mockNoteHeights(heights: Map<string, number>) {
11+
function mockNoteHeights(heights: Map<string, number>, originOffsets?: Map<string, number>) {
1212
(useNoteHeights as ReturnType<typeof vi.fn>).mockReturnValue({
1313
heights: ref(heights),
14+
originOffsets: ref(originOffsets ?? new Map<string, number>()),
1415
});
1516
}
1617

0 commit comments

Comments
 (0)