From a62c0d3133ccd3d20d24790318af7937a8dfe79c Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Sat, 28 Mar 2026 22:41:11 -0400 Subject: [PATCH 1/2] feat(penpal): match file view right margin to left margin Removes the max-width: 900px constraint on .file-main-scroll so content expands to fill available space with symmetric 40px padding on both sides, ensuring consistent margins between content and both sidebar boundaries. Closes PENPAL-11 Co-Authored-By: Claude Opus 4.6 --- apps/penpal/ERD.md | 2 ++ apps/penpal/PRODUCT.md | 2 ++ apps/penpal/TESTING.md | 1 + apps/penpal/frontend/src/index.css | 3 +- .../src/test/file-view-layout.test.ts | 36 +++++++++++++++++++ 5 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 apps/penpal/frontend/src/test/file-view-layout.test.ts diff --git a/apps/penpal/ERD.md b/apps/penpal/ERD.md index 3b8f96366..3b3677d8f 100644 --- a/apps/penpal/ERD.md +++ b/apps/penpal/ERD.md @@ -309,6 +309,8 @@ see-also: - **E-PENPAL-SIDEBAR-RESIZE**: The left sidebar width is managed in `Layout.tsx` React state (`sidebarWidth`), initialized from `localStorage.getItem('sidebarWidth')` (default 240px). A 4px drag handle (`.sidebar-resize-handle`) on the right edge of the sidebar initiates mouse-tracking: `mousedown` captures `startX` and `startWidth`, `mousemove` computes `newWidth = startWidth + (clientX - startX)` clamped to [200, 700], `mouseup` persists to `localStorage`. The `.app` grid template uses an inline style `gridTemplateColumns: \`${sidebarWidth}px 1fr\`` to override the CSS default. The drag handle uses the same visual styling as the existing `.chat-resize-handle` (2px accent-colored indicator on hover). ← [P-PENPAL-SIDEBAR-RESIZE](PRODUCT.md#P-PENPAL-SIDEBAR-RESIZE) +- **E-PENPAL-VIEW-MARGINS**: The `.file-main-scroll` container uses symmetric left and right padding (40px) and no `max-width` cap so that content expands to fill the available column width. The file-page-layout CSS grid separates the content column from the comments panel column with a resize handle; the visual margin between content text and comments panel equals the margin between the left sidebar border and the content text. + ← [P-PENPAL-VIEW-MARGINS](PRODUCT.md#P-PENPAL-VIEW-MARGINS) - **E-PENPAL-FRONTMATTER-STRIP**: `markdown.StripFrontmatter()` checks for `---` prefix, finds the next `\n---` occurrence, and returns content after it with leading newlines trimmed. Applied in `handleRawFile` before serving content and in `publish/render.go` during publishing. The frontend renders the already-stripped content. ← [P-PENPAL-FRONTMATTER](PRODUCT.md#P-PENPAL-FRONTMATTER) diff --git a/apps/penpal/PRODUCT.md b/apps/penpal/PRODUCT.md index 72169c44a..b57dd314c 100644 --- a/apps/penpal/PRODUCT.md +++ b/apps/penpal/PRODUCT.md @@ -200,6 +200,8 @@ Global views aggregate content across all projects. They appear as top-level ite - **P-PENPAL-RENDER**: Clicking a file opens a two-pane view: rendered markdown on the left, comments panel on the right, with a draggable divider. +- **P-PENPAL-VIEW-MARGINS**: The file view maintains equal margins between the content and each side boundary (left sidebar and right comments panel). Content expands to fill available space between the margins. + - **P-PENPAL-GFM**: Markdown rendering supports GitHub Flavored Markdown: tables, task lists, strikethrough, autolinks, and syntax-highlighted code blocks with a dark color scheme (for fenced blocks with a language specifier). - **P-PENPAL-FRONTMATTER**: YAML/TOML frontmatter is stripped from the rendered output so users see only the document content. diff --git a/apps/penpal/TESTING.md b/apps/penpal/TESTING.md index 4dabf4b60..a077af67b 100644 --- a/apps/penpal/TESTING.md +++ b/apps/penpal/TESTING.md @@ -100,6 +100,7 @@ see-also: | File Handler (P-PENPAL-FILE-HANDLER) | — | — | api_manage_test.go | — | | Source Disambiguation (P-PENPAL-SRC-DISAMBIG) | — | Layout.test.tsx | — | — | | Home Label (P-PENPAL-HOME-LABEL) | — | Layout.test.tsx | — | — | +| File View Margins (P-PENPAL-VIEW-MARGINS) | — | file-view-layout.test.ts | — | — | ## Known Coverage Gaps diff --git a/apps/penpal/frontend/src/index.css b/apps/penpal/frontend/src/index.css index 74d402b5a..d08fddc11 100644 --- a/apps/penpal/frontend/src/index.css +++ b/apps/penpal/frontend/src/index.css @@ -888,7 +888,8 @@ a:hover { text-decoration: underline; } opacity: 1; } .file-main { display: flex; flex-direction: column; overflow: hidden; } -.file-main-scroll { padding: 0 40px 24px 40px; max-width: 900px; overflow-y: auto; flex: 1; } +/* E-PENPAL-VIEW-MARGINS: symmetric padding, no max-width cap — content expands to fill column */ +.file-main-scroll { padding: 0 40px 24px 40px; overflow-y: auto; flex: 1; } /* Sidebar floating card (TOC) */ .sidebar-card { diff --git a/apps/penpal/frontend/src/test/file-view-layout.test.ts b/apps/penpal/frontend/src/test/file-view-layout.test.ts new file mode 100644 index 000000000..12cd07393 --- /dev/null +++ b/apps/penpal/frontend/src/test/file-view-layout.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +// E-PENPAL-VIEW-MARGINS: verify file view layout CSS enforces symmetric margins +// and no max-width cap on the scroll container. +describe('file view layout margins', () => { + const css = readFileSync(resolve(__dirname, '../../src/index.css'), 'utf-8'); + + it('file-main-scroll has symmetric left and right padding', () => { + // Extract the .file-main-scroll rule + const match = css.match(/\.file-main-scroll\s*\{([^}]+)\}/); + expect(match).not.toBeNull(); + const rule = match![1]; + + // Padding should include equal left and right values + const paddingMatch = rule.match(/padding:\s*([^;]+)/); + expect(paddingMatch).not.toBeNull(); + const padding = paddingMatch![1].trim(); + + // Parse the padding shorthand — expect "0 Xpx Ypx Xpx" form where left === right + const parts = padding.split(/\s+/); + // 4-value shorthand: top right bottom left + expect(parts.length).toBe(4); + const rightPadding = parts[1]; + const leftPadding = parts[3]; + expect(rightPadding).toBe(leftPadding); + }); + + it('file-main-scroll does not have a max-width constraint', () => { + const match = css.match(/\.file-main-scroll\s*\{([^}]+)\}/); + expect(match).not.toBeNull(); + const rule = match![1]; + expect(rule).not.toMatch(/max-width/); + }); +}); From f98f48084b2bc5e83b972464488f49995c67809b Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Sat, 28 Mar 2026 23:38:16 -0400 Subject: [PATCH 2/2] fix(penpal): add @types/node and use ESM imports in layout test The file-view-layout test used CJS-style Node.js imports (fs, path, __dirname) without @types/node, causing tsc --noEmit to fail in CI. Co-Authored-By: Claude Opus 4.6 --- apps/penpal/frontend/package.json | 1 + apps/penpal/frontend/src/test/file-view-layout.test.ts | 8 +++++--- pnpm-lock.yaml | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/penpal/frontend/package.json b/apps/penpal/frontend/package.json index ad9f4f13e..9be94a45a 100644 --- a/apps/penpal/frontend/package.json +++ b/apps/penpal/frontend/package.json @@ -35,6 +35,7 @@ "@testing-library/react": "^16.0.0", "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", + "@types/node": "^24.11.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/unist": "^3.0.3", diff --git a/apps/penpal/frontend/src/test/file-view-layout.test.ts b/apps/penpal/frontend/src/test/file-view-layout.test.ts index 12cd07393..bf7e53484 100644 --- a/apps/penpal/frontend/src/test/file-view-layout.test.ts +++ b/apps/penpal/frontend/src/test/file-view-layout.test.ts @@ -1,11 +1,13 @@ import { describe, it, expect } from 'vitest'; -import { readFileSync } from 'fs'; -import { resolve } from 'path'; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; // E-PENPAL-VIEW-MARGINS: verify file view layout CSS enforces symmetric margins // and no max-width cap on the scroll container. describe('file view layout margins', () => { - const css = readFileSync(resolve(__dirname, '../../src/index.css'), 'utf-8'); + const here = dirname(fileURLToPath(import.meta.url)); + const css = readFileSync(resolve(here, '../index.css'), 'utf-8'); it('file-main-scroll has symmetric left and right padding', () => { // Extract the .file-main-scroll rule diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a80b79c93..99ef4b462 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ importers: '@types/mdast': specifier: ^4.0.4 version: 4.0.4 + '@types/node': + specifier: ^24.11.0 + version: 24.11.0 '@types/react': specifier: ^19.0.0 version: 19.2.14