Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.20.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
Expand Down
6 changes: 4 additions & 2 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@
--statusbar-height: 28px;
--comment-panel-width: 260px;
--page-max-width: 816px;
--page-padding-x: 96px;
/* 816 − 2×64 = 688px measure (~85 chars). Trim padding, not width, so the
page footprint is unchanged and can't force horizontal scroll against the
fixed comment column. Print block flattens this, so PDF stays US-Letter. */
--page-padding-x: 64px;
--page-padding-y: 72px;
--editor-scroll-gap: 40px;

Expand Down Expand Up @@ -1898,7 +1901,6 @@ del.track-delete {
}
.ProseMirror {
min-height: 0 !important;
background-image: none !important;
color: #000 !important;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/AddCommentButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default function AddCommentButton({
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add a comment… (Cmd+Enter to post)"
placeholder="Add a comment… (@claude to get an AI response)"
rows={3}
autoFocus
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/CommentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default function CommentCard({
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Reply… (type @claude to ask Claude)"
placeholder="Reply… (@claude to get an AI response)"
rows={2}
autoFocus
/>
Expand Down
38 changes: 38 additions & 0 deletions src/test/components/composerPlaceholders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

// Guard test (spec 11): both comment composers must advertise the @claude
// affordance in their placeholder. We read the component sources and match
// EVERY placeholder="…" so a future inserted composer can't silently drop the
// hint. Paired with a negative control proving the check can fail.
// Paths resolve from the repo root (vitest runs with cwd = repo root).

function source(rel: string): string {
return readFileSync(join(process.cwd(), rel), 'utf8');
}

/** All static `placeholder="…"` literals in a source file. */
function placeholders(src: string): string[] {
return [...src.matchAll(/placeholder="([^"]*)"/g)].map((m) => m[1]);
}

describe('composer placeholders mention @claude', () => {
it('the new-comment composer placeholder mentions @claude', () => {
const found = placeholders(source('src/components/AddCommentButton.tsx'));
expect(found.length).toBeGreaterThan(0);
for (const p of found) expect(p).toContain('@claude');
});

it('the reply composer placeholder mentions @claude', () => {
const found = placeholders(source('src/components/CommentCard.tsx'));
expect(found.length).toBeGreaterThan(0);
for (const p of found) expect(p).toContain('@claude');
});

it('negative control: a placeholder lacking @claude fails the check', () => {
const found = placeholders('<textarea placeholder="Add a comment…" />');
expect(found).toEqual(['Add a comment…']);
expect(found.every((p) => p.includes('@claude'))).toBe(false);
});
});
83 changes: 83 additions & 0 deletions src/test/styles/pageSurface.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

// Guard tests that read App.css directly. They lock in two deliberate end-states
// that a later refactor could silently revert:
// 1. No painted page surface (no faux page-break gradient) — spec 10.
// 2. A widened reading measure (trimmed side padding) — spec 12.
// Each assertion is paired with a negative control proving it can fail.
// Read via fs (not Vite's `?raw`, which returns empty for `.css` under vitest —
// the CSS plugin intercepts the extension first). Paths resolve from the repo
// root (vitest runs with cwd = repo root).
const css = readFileSync(join(process.cwd(), 'src/App.css'), 'utf8');

/**
* Concatenate the bodies of every CSS rule that paints the page surface itself —
* a selector list entry that is `selector` standing alone (optionally with a
* pseudo-class/element or its own combinator target), e.g. `.ProseMirror`,
* `.ProseMirror:focus`, `.ProseMirror::before`. We deliberately EXCLUDE descendant
* rules like `.ProseMirror .comment-thread-line`, whose gradients belong to nested
* indicators (track-changes thread lines), not the page background. The faux
* page-break gradient we're guarding against was painted directly on the surface,
* so this is the scope that matters — and a gradient added to any surface variant
* (`.ProseMirror::before`) is still caught.
*/
function ruleBody(source: string, selector: string): string {
const esc = selector.replace('.', '\\.');
// The whole selector-list entry must be `selector` plus only pseudo suffixes —
// no whitespace/combinator that would make it a descendant/child rule.
const surface = new RegExp(`^${esc}(?![\\w-])(?:::?[\\w-]+(?:\\([^)]*\\))?)*$`);
const bodies: string[] = [];
for (const m of source.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
const selectors = m[1].split(',').map((s) => s.trim());
if (selectors.some((s) => surface.test(s))) bodies.push(m[2]);
}
return bodies.join('\n');
}

describe('page surface (spec 10 — no faux page-break indicator)', () => {
it('.ProseMirror paints no background image / gradient', () => {
const body = ruleBody(css, '.ProseMirror');
expect(body).not.toContain('background-image');
expect(body).not.toContain('linear-gradient');
});

it('.editor-page paints no background image / gradient', () => {
const body = ruleBody(css, '.editor-page');
expect(body).not.toContain('background-image');
expect(body).not.toContain('linear-gradient');
});

it('keeps a page-sized min-height so a near-empty doc still fills the card', () => {
// The layout minimum stays even though the page lines are gone.
expect(ruleBody(css, '.ProseMirror')).toMatch(/min-height:\s*912px/);
});

it('negative control: the assertion catches a painted gradient', () => {
// Prove the guard would fail if the gradient came back.
const withGradient =
'.ProseMirror { background-image: repeating-linear-gradient(#000 0, #000 1px); }';
expect(ruleBody(withGradient, '.ProseMirror')).toContain('linear-gradient');
});
});

describe('reading measure (spec 12 — wider surface)', () => {
function readToken(name: string): number {
const m = css.match(new RegExp(`${name}:\\s*(\\d+)px`));
if (!m) throw new Error(`token ${name} not found in App.css`);
return Number(m[1]);
}

it('keeps the widened side padding (≤ 72px)', () => {
expect(readToken('--page-padding-x')).toBeLessThanOrEqual(72);
});

it('keeps at least US-Letter page width (≥ 816px)', () => {
expect(readToken('--page-max-width')).toBeGreaterThanOrEqual(816);
});

it('negative control: the old cramped 96px padding would fail the guard', () => {
expect(96).not.toBeLessThanOrEqual(72);
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"types": ["vitest/globals"]
"types": ["vitest/globals", "node"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
Expand Down
Loading