Skip to content

Commit bb4b933

Browse files
committed
feat: improve parity
1 parent 6b97f94 commit bb4b933

9 files changed

Lines changed: 686 additions & 199 deletions

File tree

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
5050
### 6. Immersive page layout and state screens
5151
| Item | Status | Notes |
5252
|------|--------|-------|
53-
| Fullscreen `PageEditorView.vue` shell | **Not started** | Still a `max-w-3xl` card stack in `DefaultLayout.vue` |
54-
| `MainToolbar` (shadcn) | **Not started** | No page-specific toolbar exists |
55-
| `LeftSidebar` (shadcn) — Recent, Favorites, Selected, Current path | **Not started** | Zero sidebar infrastructure on page route |
56-
| `RightSidebar` (shadcn) — Note/Page/Arrow properties | **Not started** | No property panels exist |
53+
| Fullscreen `PageEditorView.vue` shell | **Done** | `PageLayout.vue` replaces `DefaultLayout.vue` for `/pages/:pageId` via route meta |
54+
| `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 |
5757
| `TableContextMenu` (shadcn) — right-click on canvas | **Not started** | No context menu on canvas |
5858
| `LoadingOverlay` during page bootstrap | **Partial** | Inline loading text in cards only |
5959
| Global CSS for spatial routes (`user-select: none`, `overflow: hidden`, `touch-action: none`) | **Not started** | `DefaultLayout.vue` does the opposite |
@@ -65,12 +65,12 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
6565
|------|--------|-------|
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` |
68-
| Drag opacity (`0.7`) | **Not started** | No opacity change during drag |
68+
| Drag opacity (`0.7`) | **Done** | `isDragging` ref toggles `opacity-70` during drag/resize |
6969
| `Teleport` to global overlay during drag/resize | **Not started** | Notes drag inside parent; no overlay portal |
7070
| `NoteDropZones` | **Not started** | No invisible drop zones on container notes |
7171
| `NoteArrowHandles` — 4 directional arrow handles | **Partial** | 4 small dots exist, but not full arrow-creation flow |
7272
| `ArrowLinkZones` | **Not started** | No edge zones for arrow reconnection |
73-
| `NoteLinkIcon` (external link indicator) | **Not started** | No link icon when `link.url` set |
73+
| `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 |
@@ -80,13 +80,13 @@ Achieve parity with the legacy `/pages/:pageId` immersive spatial canvas experie
8080
### 8. Arrow visual parity (legacy style, no Quasar)
8181
| Item | Status | Notes |
8282
|------|--------|-------|
83-
| Curve body (`CurveArrow.vue`) | **Not started** | Only straight SVG `<line>` exists |
84-
| Line body (`LineArrow.vue`) | **Partial** | Straight line is default, but no `bodyType` switching |
85-
| Arrow heads (`OpenHead.vue`) | **Not started** | No arrowheads rendered |
83+
| Curve body (`CurveArrow.vue`) | **Done** | Quadratic bezier with perpendicular offset; `bodyType === 'curve'` |
84+
| Line body (`LineArrow.vue`) | **Done** | Straight line when `bodyType === 'line'` |
85+
| Arrow heads (`OpenHead.vue`) | **Done** | SVG `<marker>` chevron heads; `sourceHead`/`targetHead` supported |
8686
| Arrow label (editable `Y.XmlFragment`) | **Not started** | No label support |
87-
| Hitbox (thick invisible stroke) | **Not started** | Thin `cursor-pointer` line only |
87+
| Hitbox (thick invisible stroke) | **Done** | `stroke="transparent" stroke-width="20"` pointer-events-auto hitbox |
8888
| Drag-to-reconnect | **Not started** | No endpoint grabbing |
89-
| Color matching note color logic | **Not started** | Stroke uses `currentColor` or `var(--primary)` |
89+
| Color matching note color logic | **Done** | Arrow color mapped via same 10-color map as notes |
9090

9191
### 9. Find and replace
9292
| Item | Status | Notes |

new-deepnotes/apps/web/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { RouterView, useRoute } from "vue-router";
44
55
import AuthLayout from "@/layouts/AuthLayout.vue";
66
import DefaultLayout from "@/layouts/DefaultLayout.vue";
7+
import PageLayout from "@/layouts/PageLayout.vue";
78
89
import { useSession } from "./features/auth/useSession";
910
import {
@@ -17,6 +18,7 @@ const route = useRoute();
1718
1819
const layout = computed(() => {
1920
if (route.meta.layout === "auth") return AuthLayout;
21+
if (route.meta.layout === "page") return PageLayout;
2022
return DefaultLayout;
2123
});
2224

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

Lines changed: 167 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import { computed, onMounted, ref } from "vue";
33
import { RouterLink, useRoute, useRouter } from "vue-router";
44
55
import { Button } from "@/components/ui/button";
6+
import {
7+
Card,
8+
CardContent,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card";
612
713
import { useSession } from "../auth/useSession";
814
import SpatialPageView from "../spatial/SpatialPageView.vue";
915
import { useUserTemplates } from "../spatial/useUserTemplates";
1016
import PageEditorBacklinksCard from "./PageEditorBacklinksCard.vue";
1117
import PageEditorCollabStatusCard from "./PageEditorCollabStatusCard.vue";
1218
import PageEditorManagementCard from "./PageEditorManagementCard.vue";
13-
import PageEditorPathCard from "./PageEditorPathCard.vue";
1419
import PageEditorSnapshotsCard from "./PageEditorSnapshotsCard.vue";
15-
import PageEditorTiptapCard from "./PageEditorTiptapCard.vue";
16-
import { Y_FRAG_PROSEMIRROR, Y_TEXT_DEFAULT } from "./page-editor-constants";
1720
import { createPageCollabDoc } from "./page-yjs-doc";
1821
import { usePageCollabEditor } from "./usePageCollabEditor";
1922
import { usePageManagement } from "./usePageManagement";
@@ -23,6 +26,15 @@ import { usePageSnapshots } from "./usePageSnapshots";
2326
2427
import type { SnapshotRow } from "./page-snapshot-list";
2528
29+
function pagePathLabel(
30+
pid: string,
31+
labels: Record<string, string>,
32+
): string {
33+
const label = labels[pid];
34+
if (label != null && label.length > 0) return label;
35+
return `[Page ${pid}]`;
36+
}
37+
2638
const route = useRoute();
2739
const router = useRouter();
2840
const { client, isAuthenticated, user, bootstrapped } = useSession();
@@ -159,96 +171,164 @@ onMounted(() => {
159171

160172
<template>
161173
<div v-if="!isAuthenticated" class="text-muted-foreground text-sm" />
162-
<div v-else class="space-y-4">
163-
<div class="flex flex-wrap items-center justify-between gap-2">
164-
<div>
165-
<h1 class="text-lg font-semibold">Spatial page</h1>
166-
<p class="text-muted-foreground text-xs">
167-
Pages are spatial worlds; this editor focuses the shared rich-text note
168-
for collab (multi-note canvas parity follows legacy).
169-
</p>
170-
<p
171-
v-if="user"
172-
class="text-muted-foreground mt-1 font-mono text-xs break-all"
173-
>
174-
{{ pageId }} · group {{ collabGroupId ?? "—" }} · personal
175-
{{ user.personalGroupId }}
176-
</p>
177-
</div>
178-
<Button as-child size="sm" variant="outline">
179-
<RouterLink to="/">Home</RouterLink>
180-
</Button>
181-
</div>
182-
183-
<PageEditorPathCard
184-
:path-loading="pathLoading"
185-
:path-error="pathError"
186-
:path-page-ids="pathPageIds"
187-
:path-page-labels="pathPageLabels"
188-
:page-prefs-loading="pagePrefsLoading"
189-
:is-favorite="isFavorite"
190-
:bump-message="bumpMessage"
191-
:favorite-message="favoriteMessage"
192-
@bump-as-starting="bumpAsStarting()"
193-
@toggle-favorite="toggleFavorite()"
194-
@remove-from-recent="removeThisFromRecent()"
195-
/>
196-
197-
<p
198-
v-if="pageOpsMessage"
199-
class="text-muted-foreground border-border rounded-md border px-3 py-2 text-sm"
200-
>
201-
{{ pageOpsMessage }}
202-
</p>
203-
204-
<PageEditorSnapshotsCard
205-
:snapshot-loading="snapshotsLoading"
206-
:snapshots="snapshotList"
207-
:collab-loading="collabLoading"
208-
:load-error="loadError"
209-
:crypto-error="cryptoError"
210-
@restore="restoreFromSnapshot($event)"
211-
@remove="deleteSnapshot($event)"
212-
@save-manual="saveSnapshotManual()"
213-
/>
214-
215-
<PageEditorManagementCard
216-
v-model:move-dest-group-id="moveDestGroupId"
217-
:collab-loading="collabLoading"
218-
:load-error="loadError"
219-
:crypto-error="cryptoError"
220-
:collab-group-id="collabGroupId"
221-
@move-to-group="management.movePageToOtherGroup()"
222-
@set-as-main-page="management.setAsGroupMainPage()"
223-
@soft-delete="management.softDeleteThisPage()"
224-
@purge="management.purgeThisPagePermanently()"
225-
/>
226-
227-
<PageEditorBacklinksCard :page-id="pageId" />
228174

175+
<template v-else>
176+
<!-- === Main canvas slot === -->
229177
<SpatialPageView
230178
:ydoc="ydoc"
231179
:default-note-template="noteTemplate"
232180
:default-arrow-template="arrowTemplate"
233181
/>
234182

235-
<PageEditorCollabStatusCard
236-
:collab-loading="collabLoading"
237-
:load-error="loadError"
238-
:crypto-error="cryptoError"
239-
:collab-ws-live="collabWsLive"
240-
:collab-ws-error="collabWsError"
241-
:update-count="updateCount"
242-
:collab-last-index="collabLastIndex"
243-
:push-error="pushError"
244-
@unlock-with-password="onUnlockWithPassword($event)"
245-
/>
183+
<!-- === Toolbar center: breadcrumb path === -->
184+
<template #toolbar-center>
185+
<nav
186+
v-if="!pathLoading && !pathError && pathPageIds.length > 0"
187+
class="text-muted-foreground flex flex-wrap items-center justify-center gap-1 text-xs"
188+
>
189+
<template v-for="(pid, i) in pathPageIds" :key="pid">
190+
<span v-if="i > 0" aria-hidden="true">/</span>
191+
<RouterLink
192+
v-if="i < pathPageIds.length - 1"
193+
class="text-primary hover:underline"
194+
:to="`/pages/${pid}`"
195+
>
196+
{{ pagePathLabel(pid, pathPageLabels) }}
197+
</RouterLink>
198+
<span v-else class="text-foreground font-medium">
199+
{{ pagePathLabel(pid, pathPageLabels) }}
200+
</span>
201+
</template>
202+
</nav>
203+
<span v-else-if="pathLoading" class="text-muted-foreground text-xs">
204+
Loading path…
205+
</span>
206+
<span v-else-if="pathError" class="text-destructive text-xs">
207+
{{ pathError }}
208+
</span>
209+
</template>
246210

247-
<PageEditorTiptapCard
248-
:editor="editor"
249-
:y-state-bytes="yStateBytes"
250-
:y-frag-prosemirror="Y_FRAG_PROSEMIRROR"
251-
:y-text-default="Y_TEXT_DEFAULT"
252-
/>
253-
</div>
211+
<!-- === Left sidebar === -->
212+
<template #left-sidebar>
213+
<div class="space-y-3">
214+
<!-- Current path -->
215+
<Card>
216+
<CardHeader class="pb-2">
217+
<CardTitle class="text-sm">Path</CardTitle>
218+
</CardHeader>
219+
<CardContent class="space-y-2 text-xs">
220+
<p v-if="pathLoading" class="text-muted-foreground">Loading…</p>
221+
<p v-else-if="pathError" class="text-destructive">{{ pathError }}</p>
222+
<nav v-else class="text-muted-foreground flex flex-wrap items-center gap-1">
223+
<template v-for="(pid, i) in pathPageIds" :key="pid">
224+
<span v-if="i > 0">/</span>
225+
<RouterLink
226+
v-if="i < pathPageIds.length - 1"
227+
class="text-primary hover:underline"
228+
:to="`/pages/${pid}`"
229+
>
230+
{{ pagePathLabel(pid, pathPageLabels) }}
231+
</RouterLink>
232+
<span v-else class="text-foreground font-medium">
233+
{{ pagePathLabel(pid, pathPageLabels) }}
234+
</span>
235+
</template>
236+
</nav>
237+
<div class="flex flex-wrap gap-1">
238+
<Button
239+
size="xs"
240+
variant="secondary"
241+
class="h-6 text-[10px]"
242+
:disabled="pagePrefsLoading"
243+
@click="bumpAsStarting()"
244+
>
245+
Make starting
246+
</Button>
247+
<Button
248+
size="xs"
249+
variant="outline"
250+
class="h-6 text-[10px]"
251+
:disabled="pagePrefsLoading"
252+
@click="toggleFavorite()"
253+
>
254+
{{ isFavorite ? "Unfavorite" : "Favorite" }}
255+
</Button>
256+
</div>
257+
<p v-if="bumpMessage" class="text-muted-foreground text-[10px]">
258+
{{ bumpMessage }}
259+
</p>
260+
<p v-if="favoriteMessage" class="text-amber-700 dark:text-amber-300 text-[10px]">
261+
{{ favoriteMessage }}
262+
</p>
263+
</CardContent>
264+
</Card>
265+
266+
<!-- Collab status mini -->
267+
<PageEditorCollabStatusCard
268+
:collab-loading="collabLoading"
269+
:load-error="loadError"
270+
:crypto-error="cryptoError"
271+
:collab-ws-live="collabWsLive"
272+
:collab-ws-error="collabWsError"
273+
:update-count="updateCount"
274+
:collab-last-index="collabLastIndex"
275+
:push-error="pushError"
276+
@unlock-with-password="onUnlockWithPassword($event)"
277+
/>
278+
</div>
279+
</template>
280+
281+
<!-- === Right sidebar === -->
282+
<template #right-sidebar>
283+
<div class="space-y-3">
284+
<PageEditorSnapshotsCard
285+
:snapshot-loading="snapshotsLoading"
286+
:snapshots="snapshotList"
287+
:collab-loading="collabLoading"
288+
:load-error="loadError"
289+
:crypto-error="cryptoError"
290+
@restore="restoreFromSnapshot($event)"
291+
@remove="deleteSnapshot($event)"
292+
@save-manual="saveSnapshotManual()"
293+
/>
294+
295+
<PageEditorManagementCard
296+
v-model:move-dest-group-id="moveDestGroupId"
297+
:collab-loading="collabLoading"
298+
:load-error="loadError"
299+
:crypto-error="cryptoError"
300+
:collab-group-id="collabGroupId"
301+
@move-to-group="management.movePageToOtherGroup()"
302+
@set-as-main-page="management.setAsGroupMainPage()"
303+
@soft-delete="management.softDeleteThisPage()"
304+
@purge="management.purgeThisPagePermanently()"
305+
/>
306+
307+
<PageEditorBacklinksCard :page-id="pageId" />
308+
</div>
309+
</template>
310+
311+
<!-- === Floating overlay === -->
312+
<template #floating-overlay>
313+
<!-- Bottom-right info -->
314+
<div
315+
class="pointer-events-none absolute bottom-3 right-3 z-20 text-right text-xs"
316+
>
317+
<div class="text-primary font-medium">
318+
{{ pageId }}
319+
</div>
320+
<div v-if="collabGroupId" class="text-muted-foreground">
321+
group {{ collabGroupId }}
322+
</div>
323+
</div>
324+
325+
<!-- Page ops message toast -->
326+
<div
327+
v-if="pageOpsMessage"
328+
class="bg-card border-border pointer-events-auto absolute top-3 left-1/2 z-20 max-w-md -translate-x-1/2 rounded-md border px-3 py-2 text-xs shadow-lg"
329+
>
330+
{{ pageOpsMessage }}
331+
</div>
332+
</template>
333+
</template>
254334
</template>

new-deepnotes/apps/web/src/features/pages/pages-routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const pagesRoutes: RouteRecordRaw[] = [
99
{
1010
path: "/pages/:pageId",
1111
name: "page",
12+
meta: { layout: "page" },
1213
component: () => import("./PageEditorView.vue"),
1314
},
1415
{

0 commit comments

Comments
 (0)