Let the composer own its own frame #244

Merged
robbertbos merged 1 commit from refactor/composer-owns-its-box into main 2026-08-06 18:17:50 +00:00
Owner

Step 3 of making the composer one reusable element. #240 made the three composers look the same by fixing the same numbers in three places; this removes the reason they could differ.

What moved

The frame around the editor was declared by each consumer:

Frame came from
ReplyComposer .wrapper (padding) + shared .field-box
ComposeOverlay Field.vue with its own default
NotesEditor hand-rolled border, radius, background, focus-within

MessageEditor's root is now the field box itself: the padding, the shared .field-box look, and the click-to-focus that makes the whole box behave like one input. A new #above slot takes what used to sit above the editor inside the frame - for the reply composer that is the drop overlay and the scheduled strip.

What each consumer loses

ReplyComposer: .wrapper stays but only as behaviour - the drop target, the paste hook and data-state. Its box declarations and handleWrapperClick are gone. So is min-height: 96px, which never bound: the box measures 113 to 131px.

ComposeOverlay: no longer wraps the composer in Field. It renders its own label above it, which is the two lines Field contributed beyond the frame.

Field.vue is deleted. It had exactly one consumer and its whole job was the frame.

Net 83 lines removed, 45 added.

What this does not do yet

NotesEditor still builds its own Tiptap editor and its own frame. Bringing it onto MessageEditor is the next step, and it is the only one with visible content consequences - notes headings render at 36px against 24px in the composers, and the mark set differs (dialect: 'notes' allows underline and highlight). That deserves its own review.

Worth a look by eye

No gate asserts on any of this. Specifically:

  • Reply composer: drag a file onto it (the drop overlay must still cover the box), paste an image, and click on the padding around the editor - the cursor should land in the text.
  • Scheduled strip: open a card with a scheduled reply; the strip must still sit inside the frame, above the editor.
  • New message: the "Bericht" label above the box, and the same box padding as the reply.
  • Sending state: while a reply is in flight the editor dims ([data-state='sending']).

Verification

vue-tsc, eslint (0 errors), vitest 692 in 95 files, vite build. Box padding measured at 4px in Chromium on the built bundle.

Step 3 of making the composer one reusable element. #240 made the three composers *look* the same by fixing the same numbers in three places; this removes the reason they could differ. ## What moved The frame around the editor was declared by each consumer: | | Frame came from | |---|---| | `ReplyComposer` | `.wrapper` (padding) + shared `.field-box` | | `ComposeOverlay` | `Field.vue` with its own default | | `NotesEditor` | hand-rolled border, radius, background, focus-within | `MessageEditor`'s root is now the field box itself: the padding, the shared `.field-box` look, and the click-to-focus that makes the whole box behave like one input. A new `#above` slot takes what used to sit above the editor *inside* the frame - for the reply composer that is the drop overlay and the scheduled strip. ## What each consumer loses **`ReplyComposer`**: `.wrapper` stays but only as behaviour - the drop target, the paste hook and `data-state`. Its box declarations and `handleWrapperClick` are gone. So is `min-height: 96px`, which never bound: the box measures 113 to 131px. **`ComposeOverlay`**: no longer wraps the composer in `Field`. It renders its own label above it, which is the two lines `Field` contributed beyond the frame. **`Field.vue` is deleted.** It had exactly one consumer and its whole job was the frame. Net 83 lines removed, 45 added. ## What this does not do yet `NotesEditor` still builds its own Tiptap editor and its own frame. Bringing it onto `MessageEditor` is the next step, and it is the only one with visible content consequences - notes headings render at 36px against 24px in the composers, and the mark set differs (`dialect: 'notes'` allows underline and highlight). That deserves its own review. ## Worth a look by eye No gate asserts on any of this. Specifically: - **Reply composer**: drag a file onto it (the drop overlay must still cover the box), paste an image, and click on the padding around the editor - the cursor should land in the text. - **Scheduled strip**: open a card with a scheduled reply; the strip must still sit inside the frame, above the editor. - **New message**: the "Bericht" label above the box, and the same box padding as the reply. - **Sending state**: while a reply is in flight the editor dims (`[data-state='sending']`). ## Verification `vue-tsc`, eslint (0 errors), vitest 692 in 95 files, `vite build`. Box padding measured at 4px in Chromium on the built bundle.
Let the composer own its own frame
All checks were successful
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
CI / pre-commit (pull_request) Successful in 28s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 40s
test-build / build (frontend) (pull_request) Successful in 54s
test-build / build (backend) (pull_request) Successful in 56s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m4s
CI / backend-test (pull_request) Successful in 1m35s
CI / e2e (pull_request) Successful in 3m53s
CI / release-scripts (push) Successful in 7s
security-scan / SBOM (trivy) (push) Successful in 10s
security-scan / Filesystem scan (trivy fs) (push) Successful in 13s
security-scan / JS SCA (npm audit) (push) Successful in 15s
security-scan / Python SAST (bandit) (push) Successful in 21s
CI / pre-commit (push) Successful in 25s
security-scan / Python SCA (pip-audit) (push) Successful in 40s
publish-main / build (frontend) (push) Successful in 59s
publish-main / build (backend) (push) Successful in 1m2s
publish-main / build (push) Successful in 0s
CI / frontend-test (push) Successful in 1m6s
CI / backend-test (push) Successful in 1m37s
CI / e2e (push) Successful in 4m19s
53f08cf037
The box around the editor was declared by each consumer: ReplyComposer had
.wrapper with the padding and the shared .field-box, ComposeOverlay used the
Field component with its own default, NotesEditor hand-rolled border, radius,
background and focus-within. That is the only reason the three could drift, and
they did - the previous change had to fix the same numbers in three places.

MessageEditor's root is now the field box: padding, the shared .field-box look,
and the click-to-focus that makes the whole box behave like one input. A new
#above slot takes what used to sit above the editor inside the frame, which for
the reply composer is the drop overlay and the scheduled strip.

ReplyComposer's .wrapper stays, but only as behaviour: the drop target, the
paste hook and data-state. Its box declarations and handleWrapperClick are gone;
so is min-height: 96px, which never bound (the box measures 113 to 131px).

Field.vue is deleted. It had one consumer and its whole job was the frame.
ComposeOverlay now renders its own label above the composer, which is the two
lines Field contributed beyond the frame.
robbertbos deleted branch refactor/composer-owns-its-box 2026-08-06 18:17:50 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!244
No description provided.