Skip to content

feat(editor): add document header with rich text editing and per-page display#3

Open
ranjan-codewalnut wants to merge 2 commits intomainfrom
feat/document-header
Open

feat(editor): add document header with rich text editing and per-page display#3
ranjan-codewalnut wants to merge 2 commits intomainfrom
feat/document-header

Conversation

@ranjan-codewalnut
Copy link
Copy Markdown
Collaborator

PR Template & Definition of Done

What does this PR do?

  • Adds a persistent document header area that appears on every page, similar to headers in word processors. The header is editable inline on the first page using a dedicated TipTap editor instance, and rendered as a read-only copy on subsequent pages for print-like pagination.
  • Introduces a toolbar toggle button (PanelTop icon) to show/hide the header. When hidden, header content is preserved in state and restored when re-shown.
  • Header supports rich text formatting (bold, italic, underline, font family/size, text color, highlight, text alignment) by reusing the existing toolbar — unsupported features (lists, headings, images, horizontal rule) are gracefully disabled via schema capability checks when the header editor is focused.
  • Fixes a TipTap crash (There is no node type named 'listItem') caused by calling editor.can().liftListItem("listItem") during render when the header editor's schema doesn't include list nodes.
  • Fixes duplicate TipTap extension warning by using .configure() to create separate extension instances for each editor.
  • Updates page break calculation in PageView to account for the dynamic document header height using a ResizeObserver.

What steps does your reviewer have to take to test this PR manually?

  1. Run bun dev and open the editor in the browser.
  2. Click the PanelTop icon (last toolbar button) to toggle the header — a header area with placeholder text "Click here to add a header" should appear above the document body.
  3. Click the header area and type text. Apply formatting (bold, italic, underline, font changes, text color) — all should work.
  4. Verify that list, heading, image, and horizontal rule toolbar buttons are disabled (grayed out) when the header is focused.
  5. Click back into the body area — all toolbar buttons should re-enable.
  6. Add enough body content to create multiple pages — the header should repeat (read-only) on every page with correct pagination.
  7. Toggle the header off and back on — content should be preserved.
  8. Open the browser console and verify no errors (no duplicate extension warning, no listItem crash).

Pull Request standards checklist - Please check off

  • This Branch will be carrying one single responsibility - feature/bugfix/style/refactor...
  • I have followed conventional commit messages and descriptive Branch naming.
  • My PR has descriptive folder/file names that I have worked on.

Testing checklist - Please check off

  • I have performed manual testing on my local to validate all changes.

If you have not followed and completed any of the above, please explain why below.
...

Definition of Done - Please check off

  • My code is well tested and I have confidence my code works as I expect in a variety of situations.
  • I have lint and format code is enabled when the file is saved and I have fixed all errors highlighted by lint.
  • I have deleted all non-descriptive comments and dead code from the files I've touched.
  • I have rebased my branch with the base branch I want to merge into and all the commits in this PR are my own.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
doc-editor Ready Ready Preview, Comment Apr 8, 2026 6:06am

Request Review

- Add persistent header area that appears on every page with visual pagination
- Support rich text editing (bold, italic, underline, font, color) in header
- Show read-only header copy on subsequent pages for print-like appearance
- Add toolbar toggle button to show/hide the header section
- Store header content separately from main document body
- Add visual separator between header and document content
- Disable unsupported toolbar features (lists, headings, images, HR) when
  header editor is focused to prevent crashes
- Fix duplicate TipTap extension warning by creating separate instances

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/components/Editor.tsx
Comment on lines +96 to +100
documentHeaderSlot={
headerEditor ? (
<EditorContent editor={headerEditor} className="header-editor" />
) : null
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use ternary operator.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5ce6dc9 — replaced the ternary with a logical AND (headerEditor && <EditorContent ... />).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ranjan-codewalnut
Copy link
Copy Markdown
Collaborator Author

@mergemitra review

@mergemitra
Copy link
Copy Markdown

mergemitra Bot commented Apr 10, 2026

PR Review

Scores

PR Quality Correctness Code Quality Test Quality Enterprise Quality
PR Quality Notes

PR Description & Scope

  • ✅ Title follows conventional format with a clear editor scope and feature summary.
  • ✅ The body accurately covers the header toggle, per-page copy, and ResizeObserver pagination.

Commit Messages & History

  • ✅ Both commits use conventional format and separate the feature from the small refactor cleanly.

PR Size & Focus

  • ✅ The feature stays focused in 4 files and 277 changed lines, which is a tight single-scope PR.

Self-Review & Polish

  • ✅ The earlier ternary in Editor.tsx is now a logical AND, matching the prior review request.
  • ✅ No debug logs, conflict markers, or accidental files are present in the current diff.

TL;DR

This PR adds an editable document header, a toolbar toggle, and repeated per-page header rendering with schema-aware toolbar gating for the header editor. The main risks are in page geometry: pagination still treats the header as extra chrome instead of reducing body capacity, and stale page-break margins can survive header toggles or resizes. Accessibility and verification also lag behind the new UI because the header toggle is mouse-only, repeated header clones stay in the accessibility tree, and the repo still has no automated test infrastructure.

Merge Recommendation: Approve with suggestions — the feature is close, but pagination correctness and accessibility regressions should be addressed before relying on the header in real multi-page documents.

Focus Areas for Architect Review

  • PageView now has two competing page-size models: a fixed PAGE_CONTENT_HEIGHT and a measured header height added outside it. That split is what drives the pagination drift above, and it will keep making print/export behavior brittle until the page geometry is defined in one place.
  • PageView is starting to combine three concerns: layout chrome rendering, header measurement, and pagination/block-break logic. A dedicated pagination hook plus a small header-frame component would make future print-layout changes much easier to reason about.
  • The editor now has capability-aware toolbar behavior and multiple schemas. This is a good point to standardize a shared “editor capabilities/extensions” abstraction so new editor variants do not keep adding one-off schema checks and duplicated extension lists.
  • dangerouslySetInnerHTML is now part of the render path for header pagination. That is acceptable for local editor state, but if this app later loads persisted/imported/collaborative documents, the header HTML should be sanitized or rendered from TipTap JSON rather than trusted as raw HTML.
  • The header is hidden by default, but the app still eagerly creates a second TipTap instance on every load (src/components/Editor.tsx#L46). If long documents or lower-end devices matter, lazy-mounting that editor would reduce startup and memory cost.
  • The current head’s README/docs still describe pagination and toolbar behavior without the new document-header feature, so this ships a user-visible workflow change without corresponding operational/user documentation.
  • This repo has no committed test files/directories and no test script, so the new header, pagination, and toolbar behavior in src/components/Editor.tsx, src/components/PageView.tsx, and src/components/Toolbar.tsx is currently only verifiable by manual testing.
  • The feature now depends on DOM measurement and ResizeObserver; if automated tests are introduced later, they will need a deterministic mock/polyfill to avoid layout-driven flakiness.

Based on 5b0ad11...5ce6dc9

const PAGE_CONTENT_HEIGHT = 1056 - HEADER_HEIGHT - FOOTER_HEIGHT;
const PAGE_HORIZONTAL_PADDING = 96;
const OVERLAY_HEIGHT = HEADER_HEIGHT + FOOTER_HEIGHT + GAP_HEIGHT;
const BASE_OVERLAY_HEIGHT = HEADER_HEIGHT + FOOTER_HEIGHT + GAP_HEIGHT;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctness] 🟠 In src/components/PageView.tsx:13-15, src/components/PageView.tsx:103-105, src/components/PageView.tsx:174-205, and src/components/PageView.tsx:229-233, the new document header is added on top of the existing 960px body area instead of reducing the per-page body capacity. Once the header has any height, each page becomes 1056 + headerHeight tall, but the break logic still allows the full 960px of body content on that page. In practice, any document that was near a page boundary before turning the header on will paginate too late, which contradicts the PR’s “print-like pagination” goal.

if (adjustedTop < pageBoundary && adjustedBottom > pageBoundary) {
// Add margin to push it to the next page's content area
const spaceNeeded = pageBoundary - adjustedTop + OVERLAY_HEIGHT;
const spaceNeeded = pageBoundary - adjustedTop + overlayHeight;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctness] 🟠 src/components/PageView.tsx:47-52 clears marginBottom, but the page-break spacer is written to marginTop at src/components/PageView.tsx:72-74. That means the new header toggle / header height observer can never fully recompute pagination: if a block needed a forced break before the header was hidden or resized, its old top margin stays in the layout even when that break is no longer needed, so content remains pushed onto later pages and totalPages can stay inflated after toggling the header.

if (block.dataset.pageMargin) {
  block.style.marginTop = "";
  delete block.dataset.pageMargin;
}

ℹ️ Original location: src/components/PageView.tsx:52 — moved to nearest diff line for GitHub compatibility.

Comment thread src/components/Editor.tsx
const [activeEditorType, setActiveEditorType] = useState<"body" | "header">(
"body"
);
const [headerHtml, setHeaderHtml] = useState("");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctness] 🟠 src/components/Editor.tsx:24,68-70 initializes headerHtml as an empty string and only populates it after the header editor emits an update, while src/components/PageView.tsx:274-291 only renders repeated headers when documentHeaderHtml is truthy. If the user turns the header on and immediately starts writing body content, page 1 shows the editable header but page 2+ render no repeated header at all, even though the pagination math is already reserving that vertical space. That breaks the advertised “header appears on every page” behavior for untouched headers.

Related: src/components/PageView.tsx:274-291

<ToolbarDivider />

{/* Toggle Document Header */}
<ToolbarButton
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Enterprise Quality] 🟠 The new header toggle is not keyboard-operable. ToolbarButton only dispatches actions from onMouseDown, and the new PanelTop control uses that component, so Tab + Enter / Space never shows or hides the header. That makes the feature inaccessible to keyboard-only users.

<button
  type="button"
  title={title}
  disabled={disabled}
  onMouseDown={(event) => {
    event.preventDefault();
    onClick();
  }}
  onKeyDown={(event) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      onClick();
    }
  }}
>

ℹ️ Original location: src/components/Toolbar.tsx:79 — moved to nearest diff line for GitHub compatibility.


{/* Document header (read-only copy for subsequent pages) */}
{showHeader && documentHeaderHtml && (
<div
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Enterprise Quality] 🟠 The per-page header clones are visual-only, but they are still exposed to assistive technology. On a multi-page document, screen readers will encounter the same header content once for the editable first-page header and then again for every subsequent page clone, which makes navigation noisy and misleading. These repeated copies should be hidden from the accessibility tree.

Suggested change
<div
<div
aria-hidden="true"

clipPath: "inset(0 -10px)",
}}
>
<div className="document-header bg-[#f8f9fc] rounded-sm px-3 py-2">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Code Quality] 🟠 The document-header shell is duplicated almost verbatim in the first-page branch at src/components/PageView.tsx:186 and the repeated-page branch at src/components/PageView.tsx:284, with the same divider duplicated again at src/components/PageView.tsx:189 and src/components/PageView.tsx:290. Styling or spacing changes now require touching both branches, which makes page-1 and page-N behavior easy to let drift apart. Extracting a small wrapper component/helper would remove that duplication.


[Code Quality] 💬 Minor [nitpick] The new header styling introduces raw color literals in multiple places: src/components/PageView.tsx:186, src/components/PageView.tsx:189, src/index.css:70, and src/index.css:76. Those values now form a separate mini-theme for the header, which makes later restyling harder than using shared CSS variables or named design tokens.

Related: src/index.css:70-76

Comment thread src/components/Editor.tsx
<PageView
documentHeaderSlot={
headerEditor && (
<EditorContent editor={headerEditor} className="header-editor" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Enterprise Quality] 💬 Minor [nitpick] The new header editor adds a second editable region without a programmatic name, so assistive-tech users get another generic editable surface with no cue that it is the document header rather than the body. Adding an explicit label would make the two editors distinguishable.

Suggested change
<EditorContent editor={headerEditor} className="header-editor" />
<EditorContent
editor={headerEditor}
className="header-editor"
aria-label="Document header"
/>

useState(0);
const isAdjustingRef = useRef(false);

const showHeader = isDocumentHeaderVisible && !!documentHeaderSlot;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Code Quality] 💬 Minor [nitpick] showHeader in src/components/PageView.tsx:102 is a boolean without an is/has/should prefix. A name like isHeaderShown or shouldShowHeader reads more clearly in conditions and stays aligned with the project’s boolean naming rule.

Comment thread src/components/Editor.tsx
function Editor() {
const editor = useEditor({
const [isHeaderVisible, setIsHeaderVisible] = useState(false);
const [activeEditorType, setActiveEditorType] = useState<"body" | "header">(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Code Quality] 💬 Minor [nitpick] src/components/Editor.tsx:21, src/components/Editor.tsx:43, src/components/Editor.tsx:67, and src/components/Editor.tsx:78 repeat the editor-mode literals "body" and "header" across state, focus handlers, and toggle logic. Centralizing those values behind a small constant or enum would make this state machine less brittle if another editor mode is added later.

Comment thread src/index.css
}

/* Document header editor styles */
.document-header .tiptap {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Code Quality] 💬 Minor [nitpick] src/index.css:65 and src/index.css:73 re-declare the base .tiptap and placeholder selectors almost line-for-line just to tweak the header variant. That duplicates editor typography behavior in two places, so future changes to placeholder styling or base text rules will need manual syncing. A modifier class or CSS custom properties layered on top of .tiptap would be easier to maintain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant