Skip to content

Commit ec36cbe

Browse files
committed
refactor: add editing state control, improve arrow curve geometry, enhance grid rendering, and add divider resize handles
- Add editingId prop to DisplayArrow and DisplayNote to control editor editability per-element - Compute isEditing flag and pass to NoteTiptapEditor :editable binding - Watch editable prop in useNoteEditor and call setEditable on changes - Replace arrow curve control point calculation: use normals from note centers instead of perpendicular offset - Reduce curve offset from 0.25
1 parent 1b0ef09 commit ec36cbe

6 files changed

Lines changed: 168 additions & 29 deletions

File tree

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ const props = defineProps<{
1414
sourceModel?: NoteModel;
1515
targetModel?: NoteModel;
1616
selected?: boolean;
17+
editingId?: string | null;
1718
}>();
1819
20+
const isEditing = computed(() => props.editingId === props.id);
21+
1922
const emit = defineEmits<{
2023
select: [];
2124
toggle: [];
@@ -122,6 +125,32 @@ const geometry = computed(() => {
122125
y2 = endpoints.y2;
123126
}
124127
128+
// Compute normals (direction from note center to edge point)
129+
let sourceNx = 0;
130+
let sourceNy = 0;
131+
let targetNx = 0;
132+
let targetNy = 0;
133+
134+
if (s) {
135+
const scx = s.pos.value.x + nw1 / 2;
136+
const scy = s.pos.value.y + h1 / 2;
137+
sourceNx = x1 - scx;
138+
sourceNy = y1 - scy;
139+
const sourceNlen = Math.hypot(sourceNx, sourceNy) || 1;
140+
sourceNx /= sourceNlen;
141+
sourceNy /= sourceNlen;
142+
}
143+
144+
if (t) {
145+
const tcx = t.pos.value.x + nw2 / 2;
146+
const tcy = t.pos.value.y + h2 / 2;
147+
targetNx = x2 - tcx;
148+
targetNy = y2 - tcy;
149+
const targetNlen = Math.hypot(targetNx, targetNy) || 1;
150+
targetNx /= targetNlen;
151+
targetNy /= targetNlen;
152+
}
153+
125154
const minX = Math.min(x1, x2);
126155
const minY = Math.min(y1, y2);
127156
@@ -141,19 +170,18 @@ const geometry = computed(() => {
141170
let centerY: number;
142171
143172
if (props.model.bodyType.value === "curve") {
144-
const perpX = dy / (dist || 1);
145-
const perpY = -dx / (dist || 1);
146-
const offset = dist * 0.25;
173+
// Control points along normals, similar to legacy
174+
const offset = Math.min(dist * 0.4, 150);
147175
148-
const c1x = localX1 + dx * 0.5 + perpX * offset;
149-
const c1y = localY1 + dy * 0.5 + perpY * offset;
150-
const c2x = localX2 - dx * 0.5 + perpX * offset;
151-
const c2y = localY2 - dy * 0.5 + perpY * offset;
176+
const c1x = localX1 + sourceNx * offset;
177+
const c1y = localY1 + sourceNy * offset;
178+
const c2x = localX2 + targetNx * offset;
179+
const c2y = localY2 + targetNy * offset;
152180
153181
pathD = `M ${localX1} ${localY1} C ${c1x} ${c1y}, ${c2x} ${c2y}, ${localX2} ${localY2}`;
154182
155-
sourceAngle = Math.atan2(c1y - localY1, c1x - localX1);
156-
targetAngle = Math.atan2(localY2 - c2y, localX2 - c2x);
183+
sourceAngle = Math.atan2(sourceNy, sourceNx);
184+
targetAngle = Math.atan2(targetNy, targetNx);
157185
158186
// Cubic bezier midpoint at t=0.5
159187
centerX = 0.125 * localX1 + 0.375 * c1x + 0.375 * c2x + 0.125 * localX2;
@@ -309,7 +337,7 @@ function onPointerDown(e: PointerEvent) {
309337
>
310338
<NoteTiptapEditor
311339
:fragment="labelFragment"
312-
:editable="!props.model.readOnly.value"
340+
:editable="isEditing && !props.model.readOnly.value"
313341
placeholder="Label…"
314342
:note-id="id"
315343
section="label"

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

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ const props = defineProps<{
2323
parentColor?: string | null;
2424
posOverride?: { x: number; y: number };
2525
isFlexChild?: boolean;
26+
editingId?: string | null;
2627
}>();
2728
29+
const isEditing = computed(() => props.editingId === props.id);
30+
2831
const emit = defineEmits<{
2932
select: [];
3033
toggle: [];
@@ -339,7 +342,7 @@ function onContextMenu(e: MouseEvent) {
339342
<div class="flex-1" @pointerdown.stop @focusin="emit('edit-start')">
340343
<NoteTiptapEditor
341344
:fragment="headFrag!"
342-
:editable="!model.readOnly.value"
345+
:editable="isEditing && !model.readOnly.value"
343346
placeholder="Head…"
344347
:note-id="id"
345348
section="head"
@@ -364,8 +367,19 @@ function onContextMenu(e: MouseEvent) {
364367
model.head.enabled.value &&
365368
(model.body.enabled.value || model.container.enabled.value)
366369
"
370+
class="relative"
367371
:style="{ height: '1px', backgroundColor: dividerColor }"
368-
/>
372+
>
373+
<div
374+
v-if="model.resizable.value && !model.readOnly.value && props.selected"
375+
class="absolute z-[2147483646] cursor-ns-resize"
376+
style="top: -3px; left: 0; right: 0; height: 7px;"
377+
@pointerdown="(e: PointerEvent) => onResizePointerDown(e, 's')"
378+
@pointermove="onResizePointerMove"
379+
@pointerup="onResizePointerUp"
380+
@pointercancel="onResizePointerUp"
381+
/>
382+
</div>
369383

370384
<!-- body section -->
371385
<div
@@ -376,7 +390,7 @@ function onContextMenu(e: MouseEvent) {
376390
<div class="flex-1" @pointerdown.stop @focusin="emit('edit-start')">
377391
<NoteTiptapEditor
378392
:fragment="bodyFrag!"
379-
:editable="!model.readOnly.value"
393+
:editable="isEditing && !model.readOnly.value"
380394
placeholder="Body…"
381395
:note-id="id"
382396
section="body"
@@ -401,8 +415,19 @@ function onContextMenu(e: MouseEvent) {
401415
model.body.enabled.value &&
402416
model.container.enabled.value
403417
"
418+
class="relative"
404419
:style="{ height: '1px', backgroundColor: dividerColor }"
405-
/>
420+
>
421+
<div
422+
v-if="model.resizable.value && !model.readOnly.value && props.selected"
423+
class="absolute z-[2147483646] cursor-ns-resize"
424+
style="top: -3px; left: 0; right: 0; height: 7px;"
425+
@pointerdown="(e: PointerEvent) => onResizePointerDown(e, 's')"
426+
@pointermove="onResizePointerMove"
427+
@pointerup="onResizePointerUp"
428+
@pointercancel="onResizePointerUp"
429+
/>
430+
</div>
406431

407432
<!-- container section -->
408433
<div
@@ -451,10 +476,10 @@ function onContextMenu(e: MouseEvent) {
451476
<template v-if="model.resizable.value && !model.readOnly.value && props.selected">
452477
<div
453478
v-for="h in ([
454-
{ key: 'nw', top: '0', left: '0', cursor: 'nwse-resize' },
455-
{ key: 'ne', top: '0', right: '0', cursor: 'nesw-resize' },
456-
{ key: 'sw', bottom: '0', left: '0', cursor: 'nesw-resize' },
457-
{ key: 'se', bottom: '0', right: '0', cursor: 'nwse-resize' },
479+
{ key: 'nw', top: '0%', left: '0%', cursor: 'nwse-resize' },
480+
{ key: 'ne', top: '0%', left: '100%', cursor: 'nesw-resize' },
481+
{ key: 'sw', top: '100%', left: '0%', cursor: 'nesw-resize' },
482+
{ key: 'se', top: '100%', left: '100%', cursor: 'nwse-resize' },
458483
] as const)"
459484
:key="h.key"
460485
class="absolute z-[2147483647] h-2.5 w-2.5 rounded-full"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script setup lang="ts">
2+
import { Button } from "@/components/ui/button";
3+
import { Camera, Search } from "@lucide/vue";
4+
5+
const emit = defineEmits<{
6+
screenshot: [];
7+
"find-replace": [];
8+
}>();
9+
</script>
10+
11+
<template>
12+
<div class="pointer-events-auto flex flex-col items-start gap-1.5">
13+
<Button
14+
variant="ghost"
15+
size="icon"
16+
class="bg-card border-border h-8 w-8 rounded-md border shadow-sm"
17+
title="Screenshot"
18+
@click="emit('screenshot')"
19+
>
20+
<Camera class="h-3.5 w-3.5" />
21+
</Button>
22+
23+
<Button
24+
variant="ghost"
25+
size="icon"
26+
class="bg-card border-border h-8 w-8 rounded-md border shadow-sm"
27+
title="Find and replace"
28+
@click="emit('find-replace')"
29+
>
30+
<Search class="h-3.5 w-3.5" />
31+
</Button>
32+
</div>
33+
</template>

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

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ScreenshotDialog from "./ScreenshotDialog.vue";
1111
import CollabAvatars from "./CollabAvatars.vue";
1212
import CanvasToolbar from "./CanvasToolbar.vue";
1313
import FloatingCameraButtons from "./FloatingCameraButtons.vue";
14+
import FloatingLeftButtons from "./FloatingLeftButtons.vue";
1415
import { useSpatialPage } from "./useSpatialPage";
1516
import { useSpatialSelection } from "./selection";
1617
import { useSpatialEditing } from "./useSpatialEditing";
@@ -138,6 +139,18 @@ const notesByZIndex = computed(() => {
138139
);
139140
});
140141
142+
const boxOverlayStyle = computed(() => {
143+
const br = selection.boxRect.value;
144+
if (!selection.boxSelecting.value || !br || !canvasRef.value?.rootEl) return null;
145+
const rect = canvasRef.value.rootEl.getBoundingClientRect();
146+
return {
147+
left: `${br.x - rect.left}px`,
148+
top: `${br.y - rect.top}px`,
149+
width: `${br.width}px`,
150+
height: `${br.height}px`,
151+
};
152+
});
153+
141154
// --- extracted composables ---
142155
const { onCanvasPointerDown, onCanvasPointerMove, onCanvasPointerUp } = useBoxSelection({
143156
canvasRef,
@@ -390,6 +403,7 @@ onUnmounted(() => {
390403
:source-model="noteById.get(arrow.model.source.value)"
391404
:target-model="noteById.get(arrow.model.target.value)"
392405
:selected="selection.isSelected(arrow.id)"
406+
:editing-id="editing.editingId.value"
393407
@select="selection.select(arrow.id, 'arrow')"
394408
@toggle="selection.toggle(arrow.id, 'arrow')"
395409
@reconnect-start="onArrowReconnectStart"
@@ -402,6 +416,7 @@ onUnmounted(() => {
402416
:model="note.model"
403417
:zoom="canvasRef?.zoom ?? 1"
404418
:selected="selection.isSelected(note.id)"
419+
:editing-id="editing.editingId.value"
405420
:is-drop-target="hoveredContainerId === note.id"
406421
:child-models="
407422
note.model.container.children.value
@@ -426,6 +441,13 @@ onUnmounted(() => {
426441
/>
427442
</SpatialWorldCanvas>
428443

444+
<!-- Left-side floating buttons -->
445+
<FloatingLeftButtons
446+
class="absolute top-14 left-3 z-20"
447+
@screenshot="screenshotOpen = true"
448+
@find-replace="findReplaceOpen = true"
449+
/>
450+
429451
<!-- Right-side floating camera buttons -->
430452
<FloatingCameraButtons
431453
class="absolute top-14 right-3 z-20"
@@ -440,14 +462,9 @@ onUnmounted(() => {
440462

441463
<!-- box selection overlay -->
442464
<div
443-
v-if="selection.boxSelecting.value && selection.boxRect.value"
465+
v-if="boxOverlayStyle"
444466
class="pointer-events-none absolute z-50 border border-primary bg-primary/10"
445-
:style="{
446-
left: `${selection.boxRect.value.x}px`,
447-
top: `${selection.boxRect.value.y}px`,
448-
width: `${selection.boxRect.value.width}px`,
449-
height: `${selection.boxRect.value.height}px`,
450-
}"
467+
:style="boxOverlayStyle"
451468
/>
452469

453470
<!-- arrow drag preview line -->

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { computed, ref } from "vue";
33
44
import { useSpatialViewport } from "./useSpatialViewport";
5+
import { isDark } from "@/features/theme/useThemePreference";
56
67
const rootRef = ref<HTMLElement | null>(null);
78
const {
@@ -38,6 +39,8 @@ const worldTransform = computed(() => {
3839
};
3940
});
4041
42+
const gridStroke = computed(() => (isDark.value ? "#5a5a5a" : "#484848"));
43+
4144
const rootCursorClass = computed(() =>
4245
spaceDown.value ? "cursor-grab active:cursor-grabbing" : "",
4346
);
@@ -66,12 +69,36 @@ function onAuxClick(e: MouseEvent) {
6669
@pointercancel="onPointerCancel"
6770
@auxclick="onAuxClick"
6871
>
69-
<div
70-
class="bg-size-[24px_24px] pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,rgba(0,0,0,0.06)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.06)_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,rgba(255,255,255,0.07)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.07)_1px,transparent_1px)]"
71-
aria-hidden="true"
72-
/>
7372
<div class="absolute top-1/2 left-1/2 h-0 w-0">
7473
<div class="will-change-transform" :style="worldTransform">
74+
<svg
75+
class="pointer-events-none absolute"
76+
style="top: -5000000px; left: -5000000px; width: 10000000px; height: 10000000px"
77+
aria-hidden="true"
78+
>
79+
<defs>
80+
<pattern
81+
id="spatial-grid"
82+
width="100"
83+
height="100"
84+
patternUnits="userSpaceOnUse"
85+
>
86+
<path
87+
d="M 100 0 L 0 0 0 100"
88+
fill="none"
89+
:stroke="gridStroke"
90+
stroke-width="1"
91+
/>
92+
</pattern>
93+
</defs>
94+
<rect
95+
x="0"
96+
y="0"
97+
width="10000000"
98+
height="10000000"
99+
fill="url(#spatial-grid)"
100+
/>
101+
</svg>
75102
<slot />
76103
</div>
77104
</div>

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ export function useNoteEditor(opts: {
4040
{ immediate: true },
4141
);
4242

43+
watch(
44+
() => opts.editable,
45+
(isEditable) => {
46+
if (editor.value && !editor.value.isDestroyed) {
47+
editor.value.setEditable(isEditable ?? true);
48+
}
49+
},
50+
);
51+
4352
onBeforeUnmount(() => {
4453
unregister?.();
4554
const ed = editor.value;

0 commit comments

Comments
 (0)