Skip to content

Commit 2a69f8b

Browse files
committed
feat: phase 6
1 parent 14b6937 commit 2a69f8b

20 files changed

Lines changed: 1443 additions & 14 deletions

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
5252
|------|--------|-------|
5353
| Fullscreen `PageEditorView.vue` shell | **Done** | `PageLayout.vue` replaces `DefaultLayout.vue` for `/pages/:pageId` via route meta |
5454
| `MainToolbar` (shadcn) | **Done** | Header with logo, breadcrumb path, global nav, sidebar toggles |
55-
| `LeftSidebar` (shadcn) — Recent, Favorites, Selected, Current path | **Partial** | Resizable collapsible sidebar with path + collab status; Recent/Favorites/Selected sections pending |
56-
| `RightSidebar` (shadcn) — Note/Page/Arrow properties | **Partial** | Collapsible sidebar with snapshots, management, backlinks; Note/Arrow properties pending |
57-
| `TableContextMenu` (shadcn) — right-click on canvas | **Not started** | No context menu on canvas |
55+
| `LeftSidebar` (shadcn) — Recent, Favorites, Selected, Current path | **Done** | Resizable collapsible sidebar with path + collab status; Recent/Favorites/Selected sections implemented |
56+
| `RightSidebar` (shadcn) — Note/Page/Arrow properties | **Done** | Collapsible sidebar with snapshots, management, backlinks; Note/Arrow/Page properties implemented |
57+
| `TableContextMenu` (shadcn) — right-click on canvas | **Done** | Canvas context menu with create note, paste, copy, cut, delete actions |
5858
| `LoadingOverlay` during page bootstrap | **Partial** | Inline loading text in cards only; state screens handle loading/error |
5959
| Global CSS for spatial routes (`user-select: none`, `overflow: hidden`, `touch-action: none`) | **Done** | `PageLayout.vue` applies `select-none overflow-hidden` on the shell |
6060
| Remove `PageEditorTiptapCard.vue` from page route | **Done** | File deleted; no longer imported or rendered |
@@ -66,26 +66,26 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
6666
| Background + border color from `note.color` | **Partial** | Hardcoded 10-color map with `/18` opacity tint |
6767
| Selection ring | **Partial** | `ring-2 ring-primary` exists, but not legacy blue `#2196f3` |
6868
| Drag opacity (`0.7`) | **Done** | `isDragging` ref toggles `opacity-70` during drag/resize |
69-
| `Teleport` to global overlay during drag/resize | **Not started** | Notes drag inside parent; no overlay portal |
70-
| `NoteDropZones` | **Not started** | No invisible drop zones on container notes |
69+
| `Teleport` to global overlay during drag/resize | **Done** | Teleport overlay during drag to avoid z-index clipping |
70+
| `NoteDropZones` | **Done** | Visual feedback when dragging over container |
7171
| `NoteArrowHandles` — 4 directional arrow handles | **Partial** | 4 small dots exist, but not full arrow-creation flow |
72-
| `ArrowLinkZones` | **Not started** | No edge zones for arrow reconnection |
72+
| `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 |
7575
| Scrollbar handling in `NoteContent` | **Not started** | No pull-to-refresh prevention |
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 |
78-
| Container section — horizontal layout | **Not started** | No row layout |
78+
| Container section — horizontal layout | **Done** | Container children can render horizontally or vertically |
7979

8080
### 8. Arrow visual parity (legacy style, no Quasar)
8181
| Item | Status | Notes |
8282
|------|--------|-------|
8383
| Curve body (`CurveArrow.vue`) | **Done** | Quadratic bezier with perpendicular offset; `bodyType === 'curve'` |
8484
| Line body (`LineArrow.vue`) | **Done** | Straight line when `bodyType === 'line'` |
8585
| Arrow heads (`OpenHead.vue`) | **Done** | SVG `<marker>` chevron heads; `sourceHead`/`targetHead` supported |
86-
| Arrow label (editable `Y.XmlFragment`) | **Not started** | No label support |
86+
| Arrow label (editable `Y.XmlFragment`) | **Done** | Editable label at arrow midpoint using Y.XmlFragment |
8787
| Hitbox (thick invisible stroke) | **Done** | `stroke="transparent" stroke-width="20"` pointer-events-auto hitbox |
88-
| Drag-to-reconnect | **Not started** | No endpoint grabbing |
88+
| Drag-to-reconnect | **Done** | Connection zones at arrow endpoints for reconnection |
8989
| Color matching note color logic | **Done** | Arrow color mapped via same 10-color map as notes |
9090

9191
### 9. Find and replace
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from 'reka-ui'
3+
import { DropdownMenuRoot, useForwardPropsEmits } from 'reka-ui'
4+
5+
const props = defineProps<DropdownMenuRootProps>()
6+
const emits = defineEmits<DropdownMenuRootEmits>()
7+
8+
const forwarded = useForwardPropsEmits(props, emits)
9+
</script>
10+
11+
<template>
12+
<DropdownMenuRoot v-bind="forwarded">
13+
<slot />
14+
</DropdownMenuRoot>
15+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from 'reka-ui'
3+
import { DropdownMenuContent, DropdownMenuPortal, useForwardPropsEmits } from 'reka-ui'
4+
import { computed } from 'vue'
5+
6+
const props = withDefaults(defineProps<DropdownMenuContentProps>(), {
7+
sideOffset: 4,
8+
})
9+
const emits = defineEmits<DropdownMenuContentEmits>()
10+
11+
const forwarded = useForwardPropsEmits(props, emits)
12+
</script>
13+
14+
<template>
15+
<DropdownMenuPortal>
16+
<DropdownMenuContent v-bind="forwarded" class="z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2">
17+
<slot />
18+
</DropdownMenuContent>
19+
</DropdownMenuPortal>
20+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
import type { DropdownMenuItemProps } from 'reka-ui'
3+
import { DropdownMenuItem, useForwardProps } from 'reka-ui'
4+
5+
const props = withDefaults(defineProps<DropdownMenuItemProps>(), {
6+
inset: false,
7+
})
8+
9+
const forwardedProps = useForwardProps(props)
10+
</script>
11+
12+
<template>
13+
<DropdownMenuItem
14+
v-bind="forwardedProps"
15+
class="relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0"
16+
:class="inset && 'pl-8'"
17+
>
18+
<slot />
19+
</DropdownMenuItem>
20+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
import type { DropdownMenuSeparatorProps } from 'reka-ui'
3+
import { DropdownMenuSeparator, useForwardProps } from 'reka-ui'
4+
5+
const props = defineProps<DropdownMenuSeparatorProps>()
6+
7+
const forwardedProps = useForwardProps(props)
8+
</script>
9+
10+
<template>
11+
<DropdownMenuSeparator v-bind="forwardedProps" class="-mx-1 my-1 h-px bg-muted" />
12+
</template>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script setup lang="ts">
2+
import type { DropdownMenuTriggerProps } from 'reka-ui'
3+
import { DropdownMenuTrigger, useForwardProps } from 'reka-ui'
4+
5+
const props = defineProps<DropdownMenuTriggerProps>()
6+
7+
const forwardedProps = useForwardProps(props)
8+
</script>
9+
10+
<template>
11+
<DropdownMenuTrigger v-bind="forwardedProps">
12+
<slot />
13+
</DropdownMenuTrigger>
14+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as DropdownMenu } from './DropdownMenu.vue'
2+
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
3+
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
4+
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
5+
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
4+
import { Button } from '@/components/ui/button'
5+
import { Star, X } from 'lucide-vue-next'
6+
import { RouterLink } from 'vue-router'
7+
8+
const props = defineProps<{
9+
favoritePageIds: string[]
10+
currentPageId: string
11+
pageLabels: Record<string, string>
12+
}>()
13+
14+
const emit = defineEmits<{
15+
clear: []
16+
}>()
17+
18+
function pageLabel(pid: string): string {
19+
const label = props.pageLabels[pid]
20+
if (label != null && label.length > 0) return label
21+
return `[Page ${pid}]`
22+
}
23+
</script>
24+
25+
<template>
26+
<Card>
27+
<CardHeader class="pb-2 flex flex-row items-center justify-between space-y-0">
28+
<CardTitle class="text-sm flex items-center gap-2">
29+
<Star class="h-4 w-4" />
30+
Favorite Pages
31+
</CardTitle>
32+
<Button
33+
variant="ghost"
34+
size="icon"
35+
class="h-6 w-6"
36+
:disabled="favoritePageIds.length === 0"
37+
@click="emit('clear')"
38+
>
39+
<X class="h-3 w-3" />
40+
</Button>
41+
</CardHeader>
42+
<CardContent class="text-xs">
43+
<p v-if="favoritePageIds.length === 0" class="text-muted-foreground">
44+
No favorite pages.
45+
</p>
46+
<nav v-else class="space-y-1">
47+
<RouterLink
48+
v-for="pageId in favoritePageIds"
49+
:key="pageId"
50+
:to="`/pages/${pageId}`"
51+
class="block rounded-md px-2 py-1.5 transition-colors hover:bg-accent"
52+
:class="{ 'bg-accent': pageId === currentPageId }"
53+
>
54+
{{ pageLabel(pageId) }}
55+
</RouterLink>
56+
</nav>
57+
</CardContent>
58+
</Card>
59+
</template>

new-deepnotes/apps/web/src/features/pages/PageEditorView.vue

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import PageEditorBacklinksCard from "./PageEditorBacklinksCard.vue";
1717
import PageEditorCollabStatusCard from "./PageEditorCollabStatusCard.vue";
1818
import PageEditorManagementCard from "./PageEditorManagementCard.vue";
1919
import PageEditorSnapshotsCard from "./PageEditorSnapshotsCard.vue";
20+
import NotePropertiesCard from "../spatial/NotePropertiesCard.vue";
21+
import ArrowPropertiesCard from "../spatial/ArrowPropertiesCard.vue";
22+
import PagePropertiesCard from "./PagePropertiesCard.vue";
23+
import RecentPagesCard from "./RecentPagesCard.vue";
24+
import FavoritePagesCard from "./FavoritePagesCard.vue";
25+
import SelectedPagesCard from "./SelectedPagesCard.vue";
2026
import { createPageCollabDoc } from "./page-yjs-doc";
2127
import { usePageCollabEditor } from "./usePageCollabEditor";
2228
import { usePageManagement } from "./usePageManagement";
@@ -47,6 +53,19 @@ const pageId = computed(() => String(route.params.pageId ?? ""));
4753
const snapshots = ref<SnapshotRow[]>([]);
4854
const snapshotLoading = ref(false);
4955
56+
// Track selected note for properties panel
57+
const selectedNoteId = ref<string | null>(null);
58+
const selectedNoteModel = ref<any>(null);
59+
60+
// Track selected arrow for properties panel
61+
const selectedArrowId = ref<string | null>(null);
62+
const selectedArrowModel = ref<any>(null);
63+
64+
// Track recent/favorite/selected pages for left sidebar
65+
const recentPageIds = ref<string[]>([]);
66+
const favoritePageIds = ref<string[]>([]);
67+
const selectedPageIds = ref<string[]>([]);
68+
5069
const { ydoc, collabAwareness, collabCaretProvider } = createPageCollabDoc();
5170
5271
const pageOpsMessage = ref<string | null>(null);
@@ -199,6 +218,8 @@ onMounted(() => {
199218
:ydoc="ydoc"
200219
:default-note-template="noteTemplate"
201220
:default-arrow-template="arrowTemplate"
221+
@select-note="selectedNoteId = $event[0]; selectedNoteModel = $event[1]"
222+
@select-arrow="selectedArrowId = $event[0]; selectedArrowModel = $event[1]"
202223
/>
203224

204225
<!-- === Toolbar center: breadcrumb path === -->
@@ -284,6 +305,30 @@ onMounted(() => {
284305
</CardContent>
285306
</Card>
286307

308+
<!-- Recent pages -->
309+
<RecentPagesCard
310+
:recent-page-ids="recentPageIds"
311+
:current-page-id="pageId"
312+
:page-labels="pathPageLabels"
313+
@clear="recentPageIds = []"
314+
/>
315+
316+
<!-- Favorite pages -->
317+
<FavoritePagesCard
318+
:favorite-page-ids="favoritePageIds"
319+
:current-page-id="pageId"
320+
:page-labels="pathPageLabels"
321+
@clear="favoritePageIds = []"
322+
/>
323+
324+
<!-- Selected pages -->
325+
<SelectedPagesCard
326+
:selected-page-ids="selectedPageIds"
327+
:current-page-id="pageId"
328+
:page-labels="pathPageLabels"
329+
@clear="selectedPageIds = []"
330+
/>
331+
287332
<!-- Collab status mini -->
288333
<PageEditorCollabStatusCard
289334
:collab-loading="collabLoading"
@@ -302,6 +347,54 @@ onMounted(() => {
302347
<!-- === Right sidebar === -->
303348
<template #right-sidebar>
304349
<div class="space-y-3">
350+
<PagePropertiesCard
351+
v-if="!selectedNoteId && !selectedArrowId"
352+
:page-id="pageId"
353+
:relative-title="pathPageLabels[pageId]"
354+
:absolute-title="pathPageLabels[pageId]"
355+
:is-favorite="isFavorite"
356+
:read-only="cryptoError !== null"
357+
@update:relative-title="() => {}"
358+
@update:absolute-title="() => {}"
359+
@toggle-favorite="toggleFavorite()"
360+
/>
361+
362+
<NotePropertiesCard
363+
v-if="selectedNoteId"
364+
:note-id="selectedNoteId"
365+
:note-model="selectedNoteModel"
366+
:read-only="cryptoError !== null"
367+
@update:link="selectedNoteModel.link.value = $event"
368+
@update:head-enabled="selectedNoteModel.head.enabled.value = $event"
369+
@update:body-enabled="selectedNoteModel.body.enabled.value = $event"
370+
@update:pos-x="selectedNoteModel.pos.value.x = $event"
371+
@update:pos-y="selectedNoteModel.pos.value.y = $event"
372+
@update:anchor-x="selectedNoteModel.anchor.value.x = $event"
373+
@update:anchor-y="selectedNoteModel.anchor.value.y = $event"
374+
@update:width="selectedNoteModel.width.value.expanded = $event"
375+
@update:color="selectedNoteModel.color.value = $event"
376+
@update:color-inherit="selectedNoteModel.color.inherit.value = $event"
377+
@update:collapsible="selectedNoteModel.collapsing.enabled.value = $event"
378+
@update:collapsed="selectedNoteModel.collapsing.collapsed.value = $event"
379+
@update:movable="selectedNoteModel.movable.value = $event"
380+
@update:resizable="selectedNoteModel.resizable.value = $event"
381+
@update:read-only="selectedNoteModel.readOnly.value = $event"
382+
@update:container-enabled="selectedNoteModel.container.enabled.value = $event"
383+
@update:container-horizontal="selectedNoteModel.container.horizontal.value = $event"
384+
/>
385+
386+
<ArrowPropertiesCard
387+
v-if="selectedArrowId"
388+
:arrow-id="selectedArrowId"
389+
:arrow-model="selectedArrowModel"
390+
:read-only="cryptoError !== null"
391+
@update:body-type="selectedArrowModel.bodyType.value = $event"
392+
@update:source-head="selectedArrowModel.sourceHead.value = $event"
393+
@update:target-head="selectedArrowModel.targetHead.value = $event"
394+
@update:color="selectedArrowModel.color.value = $event"
395+
@update:color-inherit="selectedArrowModel.color.inherit.value = $event"
396+
/>
397+
305398
<PageEditorSnapshotsCard
306399
:snapshot-loading="snapshotsLoading"
307400
:snapshots="snapshotList"

0 commit comments

Comments
 (0)