Skip to content

Commit 22230e7

Browse files
fix(web): make desktop editor fill canvas
1 parent 876fd2e commit 22230e7

7 files changed

Lines changed: 96 additions & 28 deletions

File tree

.beads/export-state.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"last_dolt_commit":"f0a33golatv3m0uq2ec7uirkanleb8p6","timestamp":"2026-07-11T13:43:05.306808-06:00","issues":304,"memories":13}
1+
{"last_dolt_commit":"vo1fusalr1a6768d9vnh19pncshf6d5d","timestamp":"2026-07-11T13:57:06.239003-06:00","issues":305,"memories":13}

.beads/interactions.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,4 @@
363363
{"id":"int-79bef5f3","kind":"field_change","created_at":"2026-07-11T19:12:43.305859Z","actor":"James Lal","issue_id":"attn-7xl.7.7","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Desktop hosted authoring now consumes the shared native workspace frame, Sidebar, PathBreadcrumb, Editor, ReviewBar, and review rail; mobile remains rendered reader-first with dock/sheets. Native/hosted builds, 77 unit files, 57 routed E2E tests, 22 Chromium/WebKit reader tests, route bundle gates, axe, offline, and visual captures pass. Follow-up icon-registry chunk optimization tracked in attn-7xl.7.8."}}
364364
{"id":"int-5975de26","kind":"field_change","created_at":"2026-07-11T19:31:46.157162Z","actor":"James Lal","issue_id":"attn-7xl.7.9","extra":{"field":"status","new_value":"in_progress","old_value":"open"}}
365365
{"id":"int-04a9ff04","kind":"field_change","created_at":"2026-07-11T19:42:57.164512Z","actor":"James Lal","issue_id":"attn-7xl.7.9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed false competing-tab leases with a reload-stable per-tab identity plus BroadcastChannel duplicate detection; mobile readers now defer authority until Edit. Added rename/reload/navigation/duplicated-tab/mobile regressions. Svelte check, browser build, 77 unit files, 59 routed E2E, and 30 Chromium/WebKit storage-reader tests pass."}}
366+
{"id":"int-88217ca4","kind":"field_change","created_at":"2026-07-11T19:56:58.067545Z","actor":"James Lal","issue_id":"attn-7xl.7.11","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Desktop ProseMirror now flex-fills the document viewport and accepts clicks in blank canvas space. Normal desktop Edit/Done chrome was removed; only genuine lease denial exposes Retry in its banner. Mobile remains reader-first and desktop editing restores after responsive transitions. Svelte check, browser build, 77 unit files, 61 routed E2E, and 30 Chromium/WebKit storage-reader tests pass."}}

.beads/issues.jsonl

Lines changed: 9 additions & 8 deletions
Large diffs are not rendered by default.

web/e2e/hosted-authoring.spec.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,49 @@ test('landing hash intent creates without any dialog', async ({ page }) => {
5858
);
5959
});
6060

61+
test('desktop editor fills the canvas and has no edit mode toggle', async ({ page }) => {
62+
await page.goto('/app#new');
63+
const editor = documentEditor(page);
64+
await expect(editor).toHaveAttribute('contenteditable', 'true');
65+
await expect(page.locator('[data-action="edit"]')).toHaveCount(0);
66+
await expect(page.getByRole('button', { name: 'Done', exact: true })).toHaveCount(0);
67+
await expect(page.getByRole('button', { name: 'Edit', exact: true })).toHaveCount(0);
68+
69+
const geometry = await page.evaluate(() => {
70+
const viewport = document.querySelector<HTMLElement>('.hosted-content-viewport');
71+
const editable = document.querySelector<HTMLElement>('.hosted-native-document .ProseMirror');
72+
if (!viewport || !editable) throw new Error('desktop editor geometry is unavailable');
73+
const viewportRect = viewport.getBoundingClientRect();
74+
const editorRect = editable.getBoundingClientRect();
75+
return {
76+
viewportHeight: viewportRect.height,
77+
editorHeight: editorRect.height,
78+
bottomGap: viewportRect.bottom - editorRect.bottom,
79+
clickX: editorRect.left + editorRect.width / 2,
80+
clickY: editorRect.bottom - 48,
81+
};
82+
});
83+
expect(geometry.editorHeight).toBeGreaterThan(geometry.viewportHeight * 0.9);
84+
expect(Math.abs(geometry.bottomGap)).toBeLessThanOrEqual(1);
85+
86+
await page.mouse.click(geometry.clickX, geometry.clickY);
87+
await page.keyboard.type('Typed from the blank canvas.');
88+
await expect(editor).toContainText('Typed from the blank canvas.');
89+
});
90+
91+
test('returning from mobile reader mode restores desktop editing', async ({ page }) => {
92+
await page.goto('/app#new');
93+
await expect(documentEditor(page)).toHaveAttribute('contenteditable', 'true');
94+
95+
await page.setViewportSize({ width: 390, height: 844 });
96+
await page.locator('.thumb-dock').getByRole('button', { name: 'Done' }).click();
97+
await expect(documentEditor(page)).toHaveAttribute('contenteditable', 'false');
98+
99+
await page.setViewportSize({ width: 1280, height: 800 });
100+
await expect(documentEditor(page)).toHaveAttribute('contenteditable', 'true');
101+
await expect(page.getByRole('button', { name: 'Done', exact: true })).toHaveCount(0);
102+
});
103+
61104
test('import creates a real multi-file workspace preserving paths', async ({ page }) => {
62105
await page.goto('/app');
63106
const chooser = page.waitForEvent('filechooser');
@@ -287,8 +330,6 @@ test('phase gate: create → type → reload → edit → export → reimport wi
287330
await expect(page.locator('.save-state[data-commits]')).not.toHaveAttribute('data-commits', '0', {
288331
timeout: 15_000,
289332
});
290-
await page.getByRole('button', { name: 'Done' }).click();
291-
292333
// Reload: the committed head recovers.
293334
await page.reload();
294335
await expect(page.locator('[data-body-text]')).toContainText('Journey body survives everything.');

web/e2e/hosted-shells.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ test('desktop editor reuses the native sidebar, editor, and review rail frame',
4444
await expect(page.getByRole('button', { name: 'desk.png' })).toBeVisible();
4545
await expect(page.getByRole('button', { name: 'notes.json' })).toBeVisible();
4646
await expect(page.locator('.hosted-native-document .ProseMirror')).toBeVisible();
47+
await expect(page.locator('[data-action="edit"]')).toHaveCount(0);
48+
await expect(page.getByRole('button', { name: 'Done', exact: true })).toHaveCount(0);
4749
await expect(page.locator('[data-slot="right-rail"]')).toHaveCount(1);
4850
await expect(page.locator('.file-rail, .review-rail')).toHaveCount(0);
4951
await expectNoHorizontalScroll(page);

web/src/hosted/app/EditorShell.svelte

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
let desktopLayout = $state(
4848
typeof window !== 'undefined' && window.matchMedia('(min-width: 901px)').matches,
4949
);
50-
let desktopEditRequested = false;
50+
let desktopEditRequested = $state(false);
5151
let HostedDesktopWorkspaceFrame = $state<typeof HostedDesktopWorkspaceFrameType | null>(null);
5252
5353
$effect(() => {
@@ -543,9 +543,12 @@
543543
// posture as native attn. Mobile intentionally remains reader-first and
544544
// enters editing only from its thumb dock.
545545
$effect(() => {
546+
if (!desktopLayout) {
547+
desktopEditRequested = false;
548+
return;
549+
}
546550
if (
547-
!desktopLayout
548-
|| desktopEditRequested
551+
desktopEditRequested
549552
|| editing
550553
|| editorLoading
551554
|| !canEdit
@@ -712,7 +715,11 @@
712715
</script>
713716

714717
{#snippet documentSurface()}
715-
<div class:hosted-native-document={desktopLayout} class:writing-sheet={!desktopLayout}>
718+
<div
719+
class:hosted-native-document={desktopLayout}
720+
class:hosted-native-editor-document={desktopLayout && activeEntry?.presentation === 'editable'}
721+
class:writing-sheet={!desktopLayout}
722+
>
716723
{#if health.mode !== 'persistent' && health.mode !== 'best-effort'}
717724
<div class="hosted-document-banner">
718725
<DegradedBanner mode={health.mode} />
@@ -724,6 +731,14 @@
724731
<strong>Another tab is editing this workspace.</strong>
725732
<p>This tab stays read-only until the other tab finishes or closes.</p>
726733
</div>
734+
{#if desktopLayout}
735+
<button
736+
class="hosted-header-button"
737+
type="button"
738+
disabled={editorLoading}
739+
onclick={() => void enterEdit()}
740+
>Retry edit</button>
741+
{/if}
727742
</div>
728743
{/if}
729744
{#if ownerState?.roomId && !ownerState.liveEditingAvailable}
@@ -837,17 +852,6 @@
837852
<span class="save-state hosted-save-state" data-save-state={saveState} data-commits={commitCount}>
838853
{ownerRoomStatus ?? saveState}
839854
</span>
840-
{#if canEdit}
841-
<button
842-
class="hosted-header-button"
843-
type="button"
844-
data-action="edit"
845-
disabled={editorLoading}
846-
onclick={() => editing ? void exitEdit() : void enterEdit()}
847-
>
848-
{editing ? 'Done' : editorLoading ? 'Opening…' : editDenied ? 'Retry edit' : 'Edit'}
849-
</button>
850-
{/if}
851855
</div>
852856
{/snippet}
853857

web/src/hosted/app/app-shell.css

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,27 @@ a.workspace-row:hover {
525525
min-height: 100%;
526526
}
527527

528-
.hosted-native-document > .hosted-editor-surface {
529-
display: contents;
528+
.hosted-native-editor-document {
529+
display: flex;
530+
min-height: 100%;
531+
flex-direction: column;
532+
padding-bottom: 0;
533+
}
534+
535+
.hosted-native-editor-document > .hosted-editor-surface,
536+
.hosted-native-editor-document .editor-container,
537+
.hosted-native-editor-document .prosemirror-mount {
538+
display: flex;
539+
min-height: 0;
540+
flex: 1;
541+
flex-direction: column;
542+
}
543+
544+
.hosted-native-editor-document .ProseMirror {
545+
width: 100%;
546+
min-height: 100%;
547+
flex: 1;
548+
padding-bottom: 4rem;
530549
}
531550

532551
.hosted-document-banner {

0 commit comments

Comments
 (0)