diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 08d453c1..fe5dcbb3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,6 +11,7 @@ Before opening a pull request:
- Add regression coverage for changes to durable jobs, permissions, cancellation, or API behavior.
- Run the repository's formatter, lint, typecheck, tests, and production build.
- Exercise changed UI behavior and check narrow-screen layouts and keyboard access.
+- Browser layout regressions use Playwright's bundled Chromium, or installed Chrome when the bundled executable is missing. If neither is available, run `npx playwright install chromium --only-shell` before testing. Tests launch a fresh temporary browser profile.
- Update setup instructions if configuration or dependencies change.
- Keep credentials, local databases, generated test reports, and internal planning notes out of commits.
diff --git a/src/client/PageDocument.tsx b/src/client/PageDocument.tsx
index 164b3bf8..da3e1045 100644
--- a/src/client/PageDocument.tsx
+++ b/src/client/PageDocument.tsx
@@ -19,6 +19,7 @@ import { api } from './api';
import { usePageAutosave } from './editor/use-page-autosave';
import { inspectMarkdown } from './editor/markdown';
import { DocumentMenu } from './editor/DocumentMenu';
+import { PageTitleField } from './editor/PageTitleField';
import { PageConversation } from './PageConversation';
const RichEditor = lazy(() => import('./editor/RichEditor'));
export function PageDocument({
@@ -260,15 +261,9 @@ export function PageDocument({
)}
-
- controller.edit({ title: event.target.value })
- }
+ onChange={(title) => controller.edit({ title })}
/>
{sourceMode ? (
<>
diff --git a/src/client/editor.css b/src/client/editor.css
index 9c9c91af..1c630149 100644
--- a/src/client/editor.css
+++ b/src/client/editor.css
@@ -397,6 +397,11 @@
margin-bottom: 24px;
outline: none;
border-radius: 0;
+ display: block;
+ resize: none;
+ overflow: hidden;
+ font-family: inherit;
+ overflow-wrap: anywhere;
}
.document-title::placeholder {
color: #747c70;
diff --git a/src/client/editor/PageTitleField.tsx b/src/client/editor/PageTitleField.tsx
new file mode 100644
index 00000000..a4dfbd88
--- /dev/null
+++ b/src/client/editor/PageTitleField.tsx
@@ -0,0 +1,45 @@
+import { useLayoutEffect, useRef } from 'react';
+
+export function singleLineTitle(value: string): string {
+ return value.replace(/[\r\n]+/g, ' ');
+}
+
+export function PageTitleField({
+ value,
+ onChange,
+}: {
+ value: string;
+ onChange: (title: string) => void;
+}) {
+ const ref = useRef(null);
+ useLayoutEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const fitToContent = () => {
+ el.style.height = 'auto';
+ el.style.height = `${el.scrollHeight}px`;
+ };
+ fitToContent();
+ // Text wraps differently when the field's width changes (viewport or
+ // panel resize), so the fitted height must be recalculated then too,
+ // not only when the value changes.
+ const observer = new ResizeObserver(fitToContent);
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, [value]);
+ return (
+