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 @@ +