From e5177f590514c9914334ac82bb2fabb6ee75b45c Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 18 Mar 2026 21:32:03 +0100 Subject: [PATCH] fix: align TitleField with editor by sharing scroll container Move title-section inside a shared .editor-scroll-area wrapper so both title and editor content center within the same scrollable context, fixing alignment drift at wide widths caused by scrollbar offset. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/Editor.css | 28 ++++++++-- src/components/EditorContent.tsx | 64 ++++++++--------------- tests/smoke/title-field-alignment.spec.ts | 57 ++++++++++++++++++++ 3 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 tests/smoke/title-field-alignment.spec.ts diff --git a/src/components/Editor.css b/src/components/Editor.css index b6ef6e76..97722af9 100644 --- a/src/components/Editor.css +++ b/src/components/Editor.css @@ -23,11 +23,19 @@ opacity: 0.55; } +/* Scroll area wrapping title + editor — single scroll context for alignment */ +.editor-scroll-area { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; +} + /* BlockNote container */ .editor__blocknote-container { flex: 1; min-height: 0; - overflow-y: auto; display: flex; justify-content: center; position: relative; @@ -162,9 +170,23 @@ opacity: 1 !important; } +/* --- Title Section: wraps icon + title + separator, aligned to editor --- */ +.title-section { + width: 100%; + max-width: var(--editor-max-width, 760px); + margin: 0 auto; + padding: 0 var(--editor-padding-horizontal, 40px); + flex-shrink: 0; +} + +.title-section__separator { + border-bottom: 1px solid var(--border-primary, rgba(0, 0, 0, 0.08)); + margin-top: 12px; +} + /* --- Note Icon Area --- */ .note-icon-area { - padding: 16px 54px 0; + padding: 16px 0 0; flex-shrink: 0; } @@ -250,7 +272,7 @@ /* --- Title Field --- */ .title-field { - padding: 4px 54px 0; + padding: 4px 0 0; flex-shrink: 0; } diff --git a/src/components/EditorContent.tsx b/src/components/EditorContent.tsx index c73b6a23..07a1cdd7 100644 --- a/src/components/EditorContent.tsx +++ b/src/components/EditorContent.tsx @@ -144,30 +144,6 @@ function ActiveTabBreadcrumb({ activeTab, props }: { ) } -function EditorBody({ activeTab, isLoadingNewTab, entries, editor, diffMode, diffContent, onToggleDiff, rawMode, onRawContentChange, onSave, onNavigateWikilink, onEditorChange, vaultPath, isDarkTheme, isTrashed, rawLatestContentRef }: { - activeTab: Tab | null; isLoadingNewTab: boolean; entries: VaultEntry[] - editor: ReturnType - diffMode: boolean; diffContent: string | null; onToggleDiff: () => void - rawMode: boolean; onRawContentChange?: (path: string, content: string) => void; onSave?: () => void - onNavigateWikilink: (target: string) => void; onEditorChange?: () => void - vaultPath?: string; isDarkTheme?: boolean; isTrashed: boolean - rawLatestContentRef?: React.MutableRefObject -}) { - const showEditor = !diffMode && !rawMode - return ( - <> - {diffMode && } - - {showEditor && activeTab && ( -
- -
- )} - {isLoadingNewTab && showEditor && } - - ) -} - export function EditorContent({ activeTab, isLoadingNewTab, entries, editor, diffMode, diffContent, onToggleDiff, @@ -178,7 +154,7 @@ export function EditorContent({ ...breadcrumbProps }: EditorContentProps) { const isTrashed = activeTab?.entry.trashed ?? false - const showTitleField = activeTab && !diffMode && !rawMode + const showEditor = !diffMode && !rawMode const entryIcon = activeTab?.entry.icon ?? null const emojiIcon = entryIcon && isEmoji(entryIcon) ? entryIcon : null @@ -207,23 +183,29 @@ export function EditorContent({ {activeTab?.entry.archived && breadcrumbProps.onUnarchiveNote && ( breadcrumbProps.onUnarchiveNote!(activeTab.entry.path)} /> )} - {showTitleField && ( - + {diffMode && } + + {showEditor && activeTab && ( +
+
+ + onTitleChange?.(activeTab.entry.path, newTitle)} + /> +
+
+ +
)} - {showTitleField && ( - onTitleChange?.(activeTab.entry.path, newTitle)} - /> - )} - + {isLoadingNewTab && showEditor && }
) } diff --git a/tests/smoke/title-field-alignment.spec.ts b/tests/smoke/title-field-alignment.spec.ts new file mode 100644 index 00000000..8129d913 --- /dev/null +++ b/tests/smoke/title-field-alignment.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test' + +test.describe('TitleField alignment and separator', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForTimeout(500) + + // Open a note so TitleField is visible + const noteItem = page.locator('.app__note-list .cursor-pointer').first() + await noteItem.click() + await page.waitForTimeout(500) + }) + + test('title-section and editor body share the same scroll container', async ({ page }) => { + // The .editor-scroll-area should contain both .title-section and .editor__blocknote-container + const scrollArea = page.locator('.editor-scroll-area') + await expect(scrollArea).toBeVisible({ timeout: 3000 }) + + const titleSection = scrollArea.locator('.title-section') + await expect(titleSection).toBeVisible() + + const editorContainer = scrollArea.locator('.editor__blocknote-container') + await expect(editorContainer).toBeVisible() + }) + + test('separator line is visible between title and editor', async ({ page }) => { + const separator = page.locator('.title-section__separator') + await expect(separator).toBeVisible({ timeout: 3000 }) + + // Separator must have a visible border-bottom + const borderBottom = await separator.evaluate( + el => getComputedStyle(el).borderBottomStyle + ) + expect(borderBottom).toBe('solid') + }) + + test('title-section and editor content are horizontally aligned', async ({ page }) => { + // Resize viewport to a wide width to test alignment + await page.setViewportSize({ width: 1400, height: 800 }) + await page.waitForTimeout(300) + + const titleSection = page.locator('.title-section') + const editorBlock = page.locator('.editor__blocknote-container .bn-editor') + + await expect(titleSection).toBeVisible({ timeout: 3000 }) + await expect(editorBlock).toBeVisible({ timeout: 3000 }) + + const titleBox = await titleSection.boundingBox() + const editorBox = await editorBlock.boundingBox() + + expect(titleBox).not.toBeNull() + expect(editorBox).not.toBeNull() + + // Both should have the same left edge (within 2px tolerance) + expect(Math.abs(titleBox!.x - editorBox!.x)).toBeLessThanOrEqual(2) + }) +})