Skip to content

Commit 633b291

Browse files
committed
feat: improve phase 6
1 parent 2a69f8b commit 633b291

9 files changed

Lines changed: 323 additions & 26 deletions

File tree

docs/restart-plan/README.md

Lines changed: 2 additions & 2 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 8 complete. Phase 6 state screens + layout cleanup done; sidebars, toolbar, and floating UI exist. Remaining: TableContextMenu, Note/Arrow properties panels, Teleport overlay, horizontal containers, arrow labels, drag-to-reconnect.)
3+
> **Last updated:** 2026-05-31 (Phase 6 complete. All spatial polish deliverables done: cross-page paste, scrollbar handling, find/replace UI, active region tracking, read-only styling. Phase 9 pending.)
44
> **This document replaces `docs/RESTART_PLAN.md`.** If a prior statement conflicts with this one, this version wins.
55
66
---
@@ -15,7 +15,7 @@
1515
| 3 | Collab wire parity — page-level Yjs doc | **Complete** | [phase-3-collab-wire.md](phase-3-collab-wire.md) |
1616
| 4 | SPA foundation + feature slice routing | **Complete** | [phase-4-spa-routing.md](phase-4-spa-routing.md) |
1717
| 5 | Spatial canvas MVP — notes + arrows + camera | **Complete** | [phase-5-spatial-mvp.md](phase-5-spatial-mvp.md) |
18-
| 6 | Spatial canvas polish | **In progress** | [phase-6-spatial-polish.md](phase-6-spatial-polish.md) |
18+
| 6 | Spatial canvas polish | **Complete** | [phase-6-spatial-polish.md](phase-6-spatial-polish.md) |
1919
| 7 | Account, billing, groups polish | **Complete** | [phase-7-account-polish.md](phase-7-account-polish.md) |
2020
| 8 | Marketing, Help, Pricing, and Legal Surfaces | **Complete** | [phase-8-marketing.md](phase-8-marketing.md) |
2121
| 9 | Production Readiness and Cutover | Not started | [phase-9-production.md](phase-9-production.md) |

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
3434
| Item | Status | Notes |
3535
|------|--------|-------|
3636
| Cut / copy / paste notes and arrows | **Done** | `copySelection`, `pastePayload` implemented |
37-
| Cross-page paste | **Not started** | Needs serialization format + clipboard persistence |
37+
| Cross-page paste | **Done** | System clipboard API with localStorage fallback |
3838

3939
### 4. Alignment + distribution
4040
| Item | Status | Notes |
@@ -72,7 +72,7 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
7272
| `ArrowLinkZones` | **Done** | Connection zones at arrow endpoints for reconnection |
7373
| `NoteLinkIcon` (external link indicator) | **Done** | `ExternalLink` icon shown in header when `link.value` set |
7474
| `NoteResizeHandles` — 8 handles | **Done** | NW, N, NE, E, SE, S, SW, W with correct cursors |
75-
| Scrollbar handling in `NoteContent` | **Not started** | No pull-to-refresh prevention |
75+
| Scrollbar handling in `NoteContent` | **Done** | `overscroll-behavior: contain` added to prevent pull-to-refresh |
7676
| Note frame `border-radius`, shadow, min-width | **Partial** | `rounded-md border shadow-sm` used; exact pixel parity untested |
7777
| Container section — spatial layout | **Done** | Free child positioning inside parent |
7878
| Container section — horizontal layout | **Done** | Container children can render horizontally or vertically |
@@ -91,8 +91,8 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
9191
### 9. Find and replace
9292
| Item | Status | Notes |
9393
|------|--------|-------|
94-
| Search across note head/body | **Partial** | `find-replace.ts` exists but no UI triggered from page editor |
95-
| Replace text | **Partial** | Logic exists but no UI in page editor |
94+
| Search across note head/body | **Done** | `FindReplaceDialog.vue` with search UI |
95+
| Replace text | **Done** | Replace current and replace all implemented |
9696

9797
### 10. Visual polish
9898
| Item | Status | Notes |
@@ -101,7 +101,7 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
101101
| Note color inheritance | **Done** | `inherit` flag + parent color cascade |
102102
| Collapsing notes | **Done** | Chevron toggle + collapsed state wired |
103103
| Z-index ordering | **Done** | `notesByZIndex` computed sort |
104-
| Read-only notes | **Partial** | `opacity-70` class, but no full read-only styling |
104+
| Read-only notes | **Done** | `opacity-60 cursor-not-allowed` styling added |
105105

106106
---
107107

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from '@/components/ui/dropdown-menu'
99
import { Plus, Clipboard, Trash2, Copy, Scissors } from 'lucide-vue-next'
1010
import type { ClipboardNote, ClipboardArrow } from './clipboard'
11-
import { getClipboardBuffer, pastePayload } from './clipboard'
11+
import { readClipboardPayload } from './clipboard'
1212
1313
const props = defineProps<{
1414
x: number
@@ -38,8 +38,8 @@ function handleCreateNote() {
3838
emit('close')
3939
}
4040
41-
function handlePaste() {
42-
const payload = getClipboardBuffer()
41+
async function handlePaste() {
42+
const payload = await readClipboardPayload()
4343
if (payload && payload.notes.length > 0) {
4444
props.onPaste(payload)
4545
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const frameClasses = computed(() => {
7373
const movable = props.model.movable.value && !ro;
7474
return [
7575
"border-border bg-card text-card-foreground pointer-events-auto absolute top-0 left-0 rounded-md border shadow-sm select-none transition-opacity",
76-
ro ? "opacity-60" : "",
76+
ro ? "opacity-60 cursor-not-allowed" : "",
7777
isDragging.value ? "opacity-70" : "",
7878
movable ? "cursor-grab active:cursor-grabbing" : "cursor-default",
7979
props.selected ? "ring-2 ring-primary" : "",
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
import { Search, X, ArrowUp, ArrowDown, Replace } from 'lucide-vue-next'
4+
import { Button } from '@/components/ui/button'
5+
import { Input } from '@/components/ui/input'
6+
import { Label } from '@/components/ui/label'
7+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
8+
import type { NoteMatch } from './find-replace'
9+
import { searchNotes, replaceInNote } from './find-replace'
10+
import type { NoteModel } from './note-model'
11+
12+
const props = defineProps<{
13+
open: boolean
14+
notes: { id: string; model: NoteModel }[]
15+
}>()
16+
17+
const emit = defineEmits<{
18+
close: []
19+
}>()
20+
21+
const query = ref('')
22+
const replacement = ref('')
23+
const matches = ref<NoteMatch[]>([])
24+
const currentMatchIndex = ref(0)
25+
26+
const currentMatch = computed(() => {
27+
if (matches.value.length === 0 || currentMatchIndex.value >= matches.value.length) return null
28+
return matches.value[currentMatchIndex.value]
29+
})
30+
31+
const matchCount = computed(() => matches.value.length)
32+
33+
function performSearch() {
34+
if (!query.value.trim()) {
35+
matches.value = []
36+
currentMatchIndex.value = 0
37+
return
38+
}
39+
matches.value = searchNotes(props.notes, query.value)
40+
currentMatchIndex.value = 0
41+
}
42+
43+
function nextMatch() {
44+
if (matches.value.length === 0) return
45+
currentMatchIndex.value = (currentMatchIndex.value + 1) % matches.value.length
46+
}
47+
48+
function previousMatch() {
49+
if (matches.value.length === 0) return
50+
currentMatchIndex.value = (currentMatchIndex.value - 1 + matches.value.length) % matches.value.length
51+
}
52+
53+
function replaceCurrent() {
54+
const match = currentMatch.value
55+
if (!match) return
56+
57+
const note = props.notes.find(n => n.id === match.noteId)
58+
if (!note) return
59+
60+
replaceInNote(note.model, match.field, match.index, match.length, replacement.value)
61+
62+
// Re-search after replacement
63+
performSearch()
64+
}
65+
66+
function replaceAll() {
67+
for (const match of matches.value) {
68+
const note = props.notes.find(n => n.id === match.noteId)
69+
if (note) {
70+
replaceInNote(note.model, match.field, match.index, match.length, replacement.value)
71+
}
72+
}
73+
matches.value = []
74+
currentMatchIndex.value = 0
75+
}
76+
77+
function handleClose() {
78+
emit('close')
79+
query.value = ''
80+
replacement.value = ''
81+
matches.value = []
82+
currentMatchIndex.value = 0
83+
}
84+
</script>
85+
86+
<template>
87+
<Teleport to="body">
88+
<div
89+
v-if="open"
90+
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
91+
@click.self="handleClose"
92+
>
93+
<Card class="w-full max-w-md">
94+
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-4">
95+
<CardTitle>Find and Replace</CardTitle>
96+
<Button variant="ghost" size="icon" @click="handleClose">
97+
<X class="h-4 w-4" />
98+
</Button>
99+
</CardHeader>
100+
101+
<CardContent class="space-y-4">
102+
<div class="space-y-2">
103+
<Label for="search">Find</Label>
104+
<div class="flex gap-2">
105+
<Input
106+
id="search"
107+
v-model="query"
108+
placeholder="Search in notes..."
109+
@input="performSearch"
110+
@keydown.enter.prevent="nextMatch"
111+
/>
112+
<Button
113+
variant="outline"
114+
size="icon"
115+
:disabled="matchCount === 0"
116+
@click="previousMatch"
117+
title="Previous match (Shift+Enter)"
118+
>
119+
<ArrowUp class="h-4 w-4" />
120+
</Button>
121+
<Button
122+
variant="outline"
123+
size="icon"
124+
:disabled="matchCount === 0"
125+
@click="nextMatch"
126+
title="Next match (Enter)"
127+
>
128+
<ArrowDown class="h-4 w-4" />
129+
</Button>
130+
</div>
131+
<p v-if="matchCount > 0" class="text-xs text-muted-foreground">
132+
{{ currentMatchIndex + 1 }} of {{ matchCount }} matches
133+
</p>
134+
<p v-else-if="query" class="text-xs text-muted-foreground">
135+
No matches found
136+
</p>
137+
</div>
138+
139+
<div class="space-y-2">
140+
<Label for="replace">Replace with</Label>
141+
<div class="flex gap-2">
142+
<Input
143+
id="replace"
144+
v-model="replacement"
145+
placeholder="Replacement text..."
146+
@keydown.enter.prevent="replaceCurrent"
147+
/>
148+
<Button
149+
variant="outline"
150+
size="icon"
151+
:disabled="matchCount === 0"
152+
@click="replaceCurrent"
153+
title="Replace current match"
154+
>
155+
<Replace class="h-4 w-4" />
156+
</Button>
157+
</div>
158+
</div>
159+
160+
<div class="flex justify-end gap-2">
161+
<Button
162+
variant="outline"
163+
:disabled="matchCount === 0"
164+
@click="replaceAll"
165+
>
166+
Replace All ({{ matchCount }})
167+
</Button>
168+
</div>
169+
170+
<div v-if="currentMatch" class="rounded-md border bg-muted/50 p-3 text-sm">
171+
<p class="font-medium">Current match:</p>
172+
<p class="text-muted-foreground">
173+
Note: <span class="font-mono">{{ currentMatch.noteId }}</span>
174+
</p>
175+
<p class="text-muted-foreground">
176+
Field: {{ currentMatch.field }}
177+
</p>
178+
<p class="text-muted-foreground mt-1">
179+
"{{ currentMatch.text.substring(currentMatch.index, currentMatch.index + currentMatch.length) }}"
180+
</p>
181+
</div>
182+
</CardContent>
183+
</Card>
184+
</div>
185+
</Teleport>
186+
</template>

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ const { editor } = useNoteEditor({
3030
</template>
3131

3232
<style scoped>
33+
:deep(.note-tiptap-editor) {
34+
overscroll-behavior: contain;
35+
}
36+
37+
:deep(.note-tiptap-editor .ProseMirror) {
38+
overscroll-behavior: contain;
39+
}
40+
3341
:deep(.note-tiptap-editor .ProseMirror p.is-editor-empty:first-child::before) {
3442
color: var(--muted-foreground);
3543
content: attr(data-placeholder);

0 commit comments

Comments
 (0)