diff --git a/packages/ui/README.md b/packages/ui/README.md
index 0d52dd5..3a72f3b 100644
--- a/packages/ui/README.md
+++ b/packages/ui/README.md
@@ -208,6 +208,23 @@ opens an editor; until it lands, and if it never does, the field *is* a textarea
string, and the swap carries the text, the caret and the focus. Escape and ⌘↵ are explicit keymaps,
because a contenteditable inherits neither from the browser.
+The browser's **writing aids** are the same fact and are handled the same way. A textarea spellchecks,
+autocorrects, capitalizes sentences and offers writing suggestions by default; CodeMirror turns all
+four off on its contenteditable. So the set is named once — `WRITING_AIDS` in `editor.ts` — and both
+halves carry it: the fallback as attributes, the editor through `EditorView.contentAttributes`. The
+facet rather than a `setAttribute` on `contentDOM`, because CodeMirror recomputes the whole content
+attribute set on every view update against its own cached values, and a hand-written attribute lives
+only until something makes that comparison unequal. The pair matters as a pair on a touch keyboard,
+where capitalization is the visible half of what a person calls autocorrect — enable one and not the
+other and a draft changes behaviour mid-sentence, the moment the chunk lands.
+
+Prose in **single-line** fields is opted in one at a time, since Firefox's `layout.spellcheckDefault`
+checks multi-line fields only: a goal title, an imported goal's title, a space name, a private
+space's public label, an artifact type's one-line description. Identifiers stay off and say so where
+they are — a handle, a project name (it is also the URL), a git URL, a path, a check command, a
+ticket. Labels and saved-view names are left alone too: they are tags people re-use, not sentences,
+and squiggling a project's own vocabulary back at them teaches nothing.
+
Image upload is on for goal bodies, messages and replies, request briefs, and review findings — the
surfaces whose records people read as prose. A finding is nested inside a review record rather than
being a record of its own, and it needs no attachment field for that: the **body owns the
diff --git a/packages/ui/src/lib/components/JoinPrivate.svelte b/packages/ui/src/lib/components/JoinPrivate.svelte
index 3b28b9e..ad0d4e3 100644
--- a/packages/ui/src/lib/components/JoinPrivate.svelte
+++ b/packages/ui/src/lib/components/JoinPrivate.svelte
@@ -99,7 +99,14 @@
-
+
diff --git a/packages/ui/src/lib/components/MarkdownEditor.svelte b/packages/ui/src/lib/components/MarkdownEditor.svelte
index 26ff84e..0f8ce79 100644
--- a/packages/ui/src/lib/components/MarkdownEditor.svelte
+++ b/packages/ui/src/lib/components/MarkdownEditor.svelte
@@ -2,7 +2,7 @@
import { onMount, tick } from 'svelte'
import type { Compartment, Extension } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
- import { loadEditor, type EditorModules } from '$lib/editor.js'
+ import { loadEditor, WRITING_AIDS, type EditorModules } from '$lib/editor.js'
import { grow } from '$lib/grow.js'
import { IMAGE_ACCEPT, ImageRejected, type PickedFile } from '$lib/images.js'
import { afterContainingImage, remap } from '$lib/insertion.js'
@@ -13,7 +13,7 @@
// Titles, paths, line numbers and one-line descriptions stay native controls — they are not
// markdown, and highlighting them would be the app claiming they are.
//
- // Three things it is careful about, because a replacement editor is the classic way a form breaks:
+ // Four things it is careful about, because a replacement editor is the classic way a form breaks:
//
// - **The bound value is still a string.** Everything above this — `$state`, draft-holding,
// `trim()`, the disabled Post button, the command's `--body` argument — is unchanged, and the
@@ -25,6 +25,10 @@
// - **Escape leaves the field, ⌘↵ submits.** Both were the browser's or the page's when this was
// a textarea and are explicit keymaps now (`editor.ts`), because a contenteditable inherits
// neither.
+ // - **The browser's writing aids stay on.** Spellcheck, autocorrect, sentence capitalization and
+ // writing suggestions are a textarea's defaults and are ones CodeMirror switches off, so the
+ // set is stated once (`WRITING_AIDS` in `editor.ts`) and applied to both halves. Otherwise a
+ // phone loses its capitalization mid-draft the moment the chunk lands.
//
// Images are optional and off by default. They are enabled on the surfaces whose records are read
// as prose by people (goal bodies, messages, briefs, review findings) and left off where what
@@ -82,10 +86,6 @@
let editable: Compartment | undefined
let modules: EditorModules | undefined
- // Safari's `autocorrect` is useful but absent from Svelte's standard HTML attribute typings, so
- // keep the pair in a spread while still emitting ordinary HTML attributes.
- const writingAids: Record = { spellcheck: 'true', autocorrect: 'on' }
-
// The fallback grows with what is in it, the way the composer's textarea did. Once CodeMirror is
// up the same thing happens from the CSS below, on the element that is actually visible.
$effect(() => {
@@ -149,10 +149,6 @@
})
if (ariaLabel) view.contentDOM.setAttribute('aria-label', ariaLabel)
if (id) view.contentDOM.id = id
- // CodeMirror replaces the native textarea with a contenteditable, so browser writing aids
- // have to be opted into again on the element that actually receives input.
- view.contentDOM.setAttribute('spellcheck', 'true')
- view.contentDOM.setAttribute('autocorrect', 'on')
ready = true
if (hadFocus) {
view.focus()
@@ -440,6 +436,10 @@
+