diff --git a/.changeset/clean-moons-invite.md b/.changeset/clean-moons-invite.md new file mode 100644 index 00000000..9a87be42 --- /dev/null +++ b/.changeset/clean-moons-invite.md @@ -0,0 +1,11 @@ +--- +"@tailor-platform/app-shell": patch +--- + +Rebuild the `form/composer` pattern on `Form` + `Field`. It was the only `form/*` pattern with neither — a bare `Card` of controls with an `onClick` submit — which left it inconsistent with its four siblings and short of three things a composer actually needs. + +The body now sits in a `Field.Root` with a `sr-only` `Field.Label` (a real label instead of `aria-label`, so error and description wiring stays available), Send is `type="submit"` so validation gates it, and server rejections — moderation, rate limit, thread closed — route through `Form`'s `errors` prop into `Field.Error` rather than a toast, landing next to the text the user still has. + +The body stays **controlled**: it is read during render to gate Send and to swap the placeholder, which a submit-time handler cannot do. The pattern doc now says so explicitly, because this is the one `form/*` pattern where field state is load-bearing rather than redundant — elsewhere `onFormSubmit` reads registered `Field.Root`s and mirroring values into state is the anti-pattern. + +Also fixes a data-loss path in the reference implementation: it cleared the body unconditionally, losing the user's text when a submit failed. It now clears only on success. diff --git a/catalogue/src/pattern/form/composer/PATTERN.md b/catalogue/src/pattern/form/composer/PATTERN.md index 3833da99..76c7a399 100644 --- a/catalogue/src/pattern/form/composer/PATTERN.md +++ b/catalogue/src/pattern/form/composer/PATTERN.md @@ -4,7 +4,7 @@ name: Composer category: pattern subcategory: form description: Free-text composer — a multi-line input above a trailing action row, for notes, replies, and comments submitted in place -requiredImports: [Card, Textarea, Button, Checkbox] +requiredImports: [Card, Form, Field, Textarea, Button, Checkbox] tags: [composer, textarea, note, reply, comment, actions, discard, send] do: - A free-text box the user fills and submits in place — add a note, reply on a thread, leave a comment, give a rejection reason @@ -66,10 +66,13 @@ Leave it empty and the buttons still sit correctly at the right; `justify-betwee - The body control is `Textarea`, never `Input` — `Input` is locked to `h-9` and clips prose to one 36px line. - Size the body with `rows` (3–5 is the usual range). Do not override its height with classes: `astw:` is internal to AppShell, and `Textarea`'s supported sizing API is `rows`. -- The `Textarea` needs an accessible name. There is usually no visible label in a composer, so pass `aria-label`; a placeholder is not a label. +- The `Textarea` needs an accessible name. There is usually no visible label in a composer, so use a `sr-only` `Field.Label`; a placeholder is not a label. Prefer this over `aria-label` — the `Field.Root` is already there, and a real label keeps the error and description wiring available. - One primary `Button` in the action row. Discard is `ghost`, and any left-side action is `ghost` too. - The action row is one row at every width — the left side shrinks, the buttons do not wrap under the body. - Disable the submit on empty or whitespace-only input, and disable both buttons while `submitting`. +- The composer is a `Form`, not a bare widget. The body sits in a `Field.Root name="…"`, Send is `type="submit"`, and Discard is `type="button"` — inside a `
` an untyped `