@micha.bigler/ui-core-micha 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ # MSG-3f — compact chat-bubble message layout
2
+
3
+ Status: planned · Tier 2 · Target repo: `ui-core-micha` (main)
4
+
5
+ ---
6
+
7
+ ## Part A — Envelope (Expertenchat, 2026-08-02)
8
+
9
+ ### Goal
10
+
11
+ Make a message occupy the space a message deserves. Today a one-word message renders as **six stacked
12
+ rows**; it must become **one**.
13
+
14
+ ### The problem
15
+
16
+ `MessageBubble` (`src/messaging/MessageBubble.jsx`) wraps everything in a vertical
17
+ `Stack spacing={0.75}`, so every element gets its own full-width row: the meta line, the body, the
18
+ `ReactionBar`, the reply control (a full MUI `Button`, which is why "Antworten" sits centred on a line of
19
+ its own), the overflow-menu `IconButton`, and finally `children` — where the host's `ReadTicks` lands as
20
+ a "Zugestellt an 0" text line. For the word "Hallo" that is roughly 260px of vertical space.
21
+
22
+ Operator direction: much more compact, icon-based rather than text-labelled, and modelled on WhatsApp.
23
+
24
+ ### "Like WhatsApp" — the actual specification
25
+
26
+ "Like WhatsApp" is not a spec, so here is what it means for this WO. Each point is a requirement.
27
+
28
+ **1. Bubbles.** Own messages right-aligned on a tinted surface; incoming messages left-aligned on a
29
+ neutral surface. The bubble shrinks to its content and is capped at roughly three quarters of the thread
30
+ width — not the current full-width outlined `Paper`. Exact radius, tint, and elevation are design-system
31
+ choices, not fixed here; what is fixed is the alignment split and the shrink-to-content behaviour.
32
+
33
+ **2. Meta moves inside the bubble — this is the line-count fix.** Timestamp and status icons sit
34
+ **inside** the bubble, bottom right, instead of being their own full-width sibling rows. Whether they
35
+ share the last text line or occupy their own narrow trailing line is left to the implementation:
36
+ riding the last line is nicer and welcome if it falls out naturally, but it is **explicitly not
37
+ required** (operator decision, 2026-08-02 — "muss nicht perfekt sein"). Do not spend the WO fighting
38
+ that CSS. What matters is that the meta is inside the bubble and right-aligned, not that it is inline.
39
+
40
+ **3. Sender name.** Only for non-direct conversations, small, above the text, inside the bubble. Direct
41
+ conversations show no sender at all (already the case today, keep it).
42
+
43
+ **4. Status as icons.** Replace the "Zugestellt an N" text with tick icons — sent / delivered /
44
+ all-read, distinguished by tick count and emphasis. Own messages only; the existing
45
+ `canShowReadTicks` gate is correct and stays.
46
+
47
+ **5. Actions collapse into one affordance.** No standalone reply button, no permanently rendered
48
+ reaction row. One overflow affordance at the bubble's top edge opens a single menu carrying Reply,
49
+ React, and the existing Edit / Delete / Copy entries.
50
+
51
+ How it is reached (operator decision, 2026-08-02 — **no long-press**):
52
+ - **Desktop:** the affordance appears on hover, and on keyboard focus. The existing right-click
53
+ `onContextMenu` path stays.
54
+ - **Touch:** a plain tap on the bubble reveals the affordance. No long-press, no swipe, no custom
55
+ pointer-event handling — the gesture edge cases (scroll conflict, text selection, iOS's own
56
+ context menu) are not worth their cost here.
57
+ - **React** is a menu entry that expands the existing `ReactionBar` emoji row. Do **not** try to nest an
58
+ emoji picker inside the MUI `Menu` — MUI does not nest menus gracefully, and the existing expandable
59
+ row already works.
60
+
61
+ **5b. The menu must open for messages the user cannot edit.** Today the whole menu is gated on `canAct`
62
+ (own message, or moderator), because it only contained Edit / Delete / Copy. Once Reply lives there, an
63
+ **incoming** message needs a menu too — you reply to other people's messages. So there are two variants:
64
+ any message offers Reply / React / Copy; a message the user may act on additionally offers Edit /
65
+ Delete. Getting this wrong makes replying to anyone else impossible, and it will not show up in a test
66
+ fixture that only contains own messages.
67
+
68
+ **6. Reactions.** Existing reaction chips render as a compact row overlapping the bubble's bottom edge,
69
+ not as their own stacked row. Adding a reaction moves into the action menu.
70
+
71
+ **7. Reply quote.** Stays inside the bubble as a tinted block with a coloured leading border, replacing
72
+ today's full-width text `Button`. Clicking it must still jump to the quoted message.
73
+
74
+ ### Confirmed against operator-supplied reference screenshots (2026-08-02)
75
+
76
+ The operator supplied two WhatsApp screenshots as the reference. What they pin down beyond the points
77
+ above:
78
+
79
+ - In the reference, a two-line message has its timestamp at the right end of the *second* line — so
80
+ WhatsApp's own rule is "meta rides the final line". Per point 2 this is **not** binding here; it is
81
+ recorded so the implementation knows what the ideal looks like if it comes cheaply.
82
+ - **Incoming messages carry no status icon at all** — not even a "sent" tick. Only own messages show
83
+ status.
84
+ - **At rest there is no visible action affordance whatsoever.** No reply control, no reaction button, no
85
+ overflow glyph. The bubble is text plus meta and nothing else until the user hovers or long-presses.
86
+ - **The meta is visually recessed**: noticeably smaller than the body and in a lower-contrast grey; the
87
+ read state is carried by colour on the ticks, not by the timestamp.
88
+ - Bubbles shrink to content — the short incoming message's bubble is markedly narrower than the long
89
+ one's, confirming that the width cap is a maximum and not a fixed width.
90
+
91
+ A bubble tail/notch is visible in the reference. Treat it as optional design-system garnish, not a
92
+ requirement of this WO.
93
+
94
+ ### Acceptance criterion
95
+
96
+ A message with short text, no reactions, no attachments and no reply quote occupies **at most two visual
97
+ lines** — the text and, at most, a narrow right-aligned meta line inside the bubble. One line if the meta
98
+ lands inline without a fight. Today's six rows for that case is what this WO exists to kill; anything
99
+ still costing a third line has not met it.
100
+
101
+ ### Non-negotiable: the icons must keep their accessible names
102
+
103
+ Replacing text with icons is exactly how a UI becomes unusable with a screen reader. Every icon
104
+ introduced by this WO carries the text it replaced as its accessible name — the status icons keep the
105
+ existing `MessagingReadTicks.DELIVERED` / `ALL_READ` strings as `aria-label`, the action affordance keeps
106
+ a label, and no i18n key is deleted because its text stopped being displayed. A visually compact UI that
107
+ announces "button" to a screen reader is a regression, not a redesign.
108
+
109
+ Second, related trap: today's overflow menu is hover-only (`'& .message-actions': { opacity: 0 }`), which
110
+ makes it unreachable on touch **and** invisible to keyboard users. This WO must not carry that forward.
111
+ The affordance must be focusable and operable by keyboard. Note that `opacity: 0` does **not** remove an
112
+ element from the tab order — it is only invisible — so a `:focus-within` (or `:focus-visible`) rule that
113
+ reveals it on keyboard focus closes the keyboard half cheaply. The touch half is point 5's tap-to-reveal.
114
+ That this is a pre-existing defect does not make it acceptable to reproduce.
115
+
116
+ ### Scope
117
+
118
+ `src/messaging/MessageBubble.jsx` primarily; `src/messaging/ReadTicks.jsx` (text → icon),
119
+ `src/messaging/ReactionBar.jsx` (compact placement, add-reaction moves into the menu), and the small part
120
+ of `src/messaging/Thread.jsx` that positions bubbles and passes `children`. Their tests, the messaging
121
+ i18n table if new labels are genuinely needed, and the `dev/` harness entry so the result can actually be
122
+ looked at.
123
+
124
+ ### Non-goals / do-not-touch
125
+
126
+ - **No custom touch gestures at all** — no swipe-to-reply and, per point 5, **no long-press**. Both need
127
+ hand-rolled pointer handling whose real cost is the edge cases (scroll conflict, text selection, iOS's
128
+ own context menu), and neither is needed for "compact". Operator decision 2026-08-02. If either is
129
+ wanted later it is its own WO.
130
+ - **No Composer change.** The screenshot that triggered this also shows a tall composer, but that is not
131
+ what was asked. Raise it separately rather than folding it in.
132
+ - **No change to `Thread`'s data flow**, pagination, reply-thread expansion, unread-reply marker
133
+ (MSG-3d), or the read-status contract.
134
+ - **No dcm change.** See the read-status note below.
135
+ - **No change to what `MessageBubble` accepts or renders as `children`** — hosts pass `ReadTicks` through
136
+ it today and must keep working.
137
+ - No new dependency. MUI plus the existing icon set is enough.
138
+
139
+ ### Read status — an observation, deliberately NOT a precondition
140
+
141
+ `ReadTicks` fetches per message (`getMessageReadStatus` in a `useEffect`), and dcm's `serialize_message`
142
+ carries no read state, so a thread fires one REST call per own message. Tick icons make that fan-out more
143
+ visible but do **not** worsen it: `canShowReadTicks` already bounds it to own messages, and the code
144
+ comments that bound as deliberate. Putting the delivered/read aggregate onto `serialize_message` — the
145
+ way `reply_count`/`last_reply_at` already ride it since MSG-3d, and viewer-independent in the same way —
146
+ would remove the fan-out entirely. That is a worthwhile dcm follow-up and should be raised as one, but
147
+ gating a UI fix behind a dcm release is the wrong trade. **Do not implement it here; do report it.**
148
+
149
+ ### Tier
150
+
151
+ **Tier 2.** Shared-core, and this is an opinionated visual decision imposed on every future consumer of
152
+ ucm's messaging surface — see the cross-consumer note. Codex-first, mandatory independent review, plus a
153
+ `ui_reviewer` pass: this WO is entirely about how something looks and the harness exists to look at it.
154
+
155
+ ### Cross-consumer note
156
+
157
+ ucm is consumer-agnostic by design, and this WO commits it to a specific chat aesthetic. That is
158
+ acceptable **now** because jg-ferien is currently the only messaging consumer (spesix is deferred per the
159
+ messaging-centralization course correction), so the design lands with one adopter rather than being
160
+ retrofitted onto several. Say so in the register row rather than letting a future consumer discover it.
161
+ What must **not** happen is jg-specific vocabulary or layout assumptions leaking into the package — the
162
+ same line MSG-3d drew for `external_key` applies here.
163
+
164
+ ### Risks
165
+
166
+ - **Do not get drawn into the inline-meta CSS.** Placing timestamp and ticks on the last text line, and
167
+ wrapping cleanly when the text ends near the edge, is a known-awkward problem needing a reserved inline
168
+ spacer or a floated meta element. Point 2 deliberately does not require it. If a first attempt does not
169
+ fall out cleanly, take the trailing meta line and move on — this risk is listed to be avoided, not
170
+ managed.
171
+ - **Long text, long words, RTL and zoom.** A shrink-to-content bubble with a right-aligned meta
172
+ breaks in interesting ways with a single unbroken 60-character token, at 200% zoom, and in narrow
173
+ mobile widths. Check those three explicitly.
174
+ - **Losing an action.** Reply, React, Edit, Delete and Copy all currently exist. Collapsing them into one
175
+ menu must not quietly drop one — particularly React, which is moving from a permanent control into the
176
+ menu and is the easiest to forget.
177
+ - **Announcements and polls.** `MessageBubble` also renders announcement titles, announcement link
178
+ buttons, poll cards and attachment lists. A bubble redesign that only considers plain text will make
179
+ those look broken. Each needs a look in the harness.
180
+
181
+ ### Tests to WRITE (scoped — run only these)
182
+
183
+ - a short own message renders its timestamp and status **inside** the bubble, not as a sibling row (the
184
+ acceptance criterion, asserted structurally rather than by pixel — do not assert that the meta shares
185
+ the text's line, which point 2 leaves open);
186
+ - the action menu opens for an **incoming** message and offers Reply, and offers Edit/Delete only where
187
+ `canAct` holds (point 5b — a fixture containing only own messages will not catch this);
188
+ - the affordance is reachable on touch without a long press;
189
+ - no standalone reply button and no permanently rendered reaction row exist in the DOM for a plain
190
+ message;
191
+ - own and incoming messages are distinguishable (alignment/styling hook), and the sender name renders for
192
+ a group conversation but not a direct one;
193
+ - the status icon exposes the same accessible name the text label used, and appears only on own messages;
194
+ - the action affordance is reachable and operable by keyboard alone — no hover, no long press;
195
+ - the action menu contains Reply, React, Edit, Delete and Copy for a message the user may act on, and the
196
+ reply quote still triggers `onJumpToMessage`;
197
+ - an announcement, a poll and an attachment message each still render their existing content inside the
198
+ new bubble.
199
+
200
+ Command: `pnpm vitest run tests/Thread.test.jsx tests/messagingMessageActions.test.jsx tests/messagingInteractions.test.jsx tests/messagingPollRendering.test.jsx` plus any new file. The existing messaging tests are the regression net — if several of them need rewriting, that is a signal the change went wider than the envelope, and it should be reported before proceeding.
201
+
202
+ ### Release
203
+
204
+ One version bump + npm publish at WO end. jg-ferien bumps its pin afterwards to pick the design up; that
205
+ pin bump is not part of this WO.
206
+
207
+ ### Preconditions
208
+
209
+ None. Independent of `MSG-3e` (per-candidate scope in `DirectMessageLauncher`) — different files, either
210
+ order, but if both are in flight coordinate the version bumps so two releases don't collide.
211
+
212
+ ### Approval Gate #1
213
+
214
+ Granted by the operator 2026-08-02: compact, icon-based, chat bubbles, WhatsApp as the model, across all
215
+ three design questions asked.
216
+
217
+ ### Mini-handover (pastable)
218
+
219
+ Orchestrator: implement `work-orders/MSG-3f.md` in `ui-core-micha` (main). `git pull` first, read the WO,
220
+ then follow `orchestrate-codex` (write Part B yourself, Codex-first, own independent review plus a
221
+ `ui_reviewer` pass on the harness, publish at WO end). **The acceptance criterion is one visual line for a
222
+ short message — check it before declaring done.**
223
+
224
+ ---
225
+
226
+ ## Part B — Implementation map (Orchestrator)
227
+
228
+ **Target repo working directory:** `C:\Users\Micha Bigler\Documents\webapps\ui-core-micha` (repo root; main
229
+ branch). Work from this package; open only the named files to verify.
230
+
231
+ ### Current shape (verified while scoping — do not re-derive)
232
+
233
+ **`src/messaging/MessageBubble.jsx`** (105 lines) — one `Paper` (`component="article"`, `:66`) wrapping a
234
+ single `Stack spacing={0.75}` (`:67`) with, in row order: reply-quote `Button` (`:68-73`, only if `replyTo`),
235
+ meta row — sender name (`conversation?.kind !== 'direct'`, `:75`) + `created_at` + "edited" caption
236
+ (`:74-78`), announcement title/link (`:79-80`), error `Alert` (`:81`), body/edit-`TextField` (`:82`),
237
+ `!deleted` wrapper with `AttachmentList` + `<ReactionBar message={message} />` + `PollCard` (`:83-87`),
238
+ standalone "Antworten" reply `Button` (`:88`, **not** inside the existing `Menu` — reply is currently
239
+ ungated by `canAct`, unlike edit/delete/copy), overflow `IconButton` (`:89`, `className="message-actions"`,
240
+ `canAct`-gated), `{children}` (`:90` — this is where `Thread.jsx` injects `<ReadTicks>`, currently its own
241
+ trailing row, not "inside the bubble"), then the edit/delete/copy `Menu` (`:92-96`) and delete-confirm
242
+ `Dialog` (`:97-101`).
243
+
244
+ - `canAct` (`:35-37`): `!deleted && message.kind !== 'poll' && ((Boolean(currentUser?.id) &&
245
+ message.sender?.id === currentUser?.id) || canModerateMessages)` — gates both `onContextMenu` (`:66`) and
246
+ the overflow `IconButton`. **No existing `isOwn`/alignment boolean** — the own-message check is inlined
247
+ into `canAct` only; introduce a named `isOwn` for the alignment split (same condition, without the
248
+ `!deleted`/`kind !== 'poll'` parts that are specific to action-gating, not alignment).
249
+ - Hover-only CSS trap (`:66`, inline `sx` on the `Paper`): `'& .message-actions': { opacity: 0 },
250
+ '&:hover .message-actions': { opacity: 1 }` — pure `:hover`, no `:focus-within`/`:focus-visible` variant,
251
+ no touch/tap path. This is the exact defect the WO's "Non-negotiable" section calls out — fix it here
252
+ (add a focus-visible rule; touch is a tap-to-reveal state, not CSS-only).
253
+ - `Thread.jsx:103` and `:113` are the two `<MessageBubble>` mount sites (root messages and nested replies
254
+ respectively), both passing `children={canShowReadTicks(message, user) && <ReadTicks .../>}`.
255
+ `canShowReadTicks` (`Thread.jsx:25-27`) duplicates (more strictly — adds a pending-optimistic-row guard)
256
+ the same own-message check as `canAct` — a third near-duplicate inline check. Consider factoring these
257
+ three (MessageBubble's `canAct`, Thread's `canShowReadTicks`, the new alignment `isOwn`) toward one
258
+ shared own-message predicate if it falls out cleanly; not required if it adds risk late in the WO.
259
+ - Direct-vs-group signal: `conversation?.kind !== 'direct'`, exactly this string comparison, reused
260
+ identically at `MessageBubble.jsx:75` and `ReadTicks.jsx:21` — no enum, just keep using it.
261
+
262
+ **`src/messaging/ReadTicks.jsx`** (25 lines) — fetch-per-message `useEffect` (`:13-17`,
263
+ `getMessageReadStatus(messageId)` from `useMessaging()`). Only **two** states exist in the data today:
264
+ `status.all_read` (bool) vs `status.delivered_count` (number) — **there is no third "sent, not yet
265
+ delivered" state** in the current contract; do not invent one that the backend can't back. Map the two
266
+ existing states to icon variants (e.g. a single check for "delivered", a double/colored check for
267
+ "all read") — do not add a third tick state without a data source. `status.recipient_detail` (only present
268
+ for non-direct conversations, `:21` comment already documents the DM carve-out) drives the existing
269
+ clickable popover of names (`:24`) — keep that behavior, just change what triggers it from a text caption to
270
+ an icon (`aria-haspopup="dialog"` must move with it). i18n keys already exist and are reusable as
271
+ `aria-label`s: `MessagingReadTicks.ALL_READ`, `.DELIVERED` (`{{count}}`), `.RECIPIENTS`.
272
+
273
+ **`src/messaging/ReactionBar.jsx`** (47 lines) — currently its own sibling row (`Stack`, `:32`), mounted
274
+ inside MessageBubble's `!deleted` wrapper (`MessageBubble.jsx:85`). **Add-reaction already exists and is
275
+ reusable as-is**: `:35`, an `IconButton` (`AddReactionOutlinedIcon`, `aria-label={t('MessagingReactions.ADD')}`)
276
+ toggling local `expanded` state; when expanded, `:37-43` renders `QUICK_EMOJIS`
277
+ (`['👍','❤️','😂','🎉','👀']`, `:8`) as `Chip`s — this is "the existing expandable row" the WO says already
278
+ works; reuse it verbatim inside the action menu's React entry, don't rebuild it. `ReactionBar` takes only
279
+ `message` as a prop today — no position/z-index hook back to the parent; if overlapping the bubble's bottom
280
+ edge needs coordination (e.g. negative margin colliding with bubble padding), that's a MessageBubble-side
281
+ CSS concern, only touch `ReactionBar`'s own layout if the compact-row placement itself needs to change (it
282
+ likely does — from a full sibling row to a smaller overlapping one).
283
+
284
+ ### The new bubble shape — map the seams, not the CSS
285
+
286
+ This WO is explicitly NOT asking for the inline-meta-shares-last-line CSS to be solved (Risks section, Part
287
+ A) — implement the straightforward version (meta as its own narrow trailing line inside the bubble,
288
+ right-aligned) and stop; do not spend cycles chasing the inline variant.
289
+
290
+ 1. **Alignment split**: introduce `isOwn` (own-message boolean, computed once, see above). Own → bubble
291
+ right-aligned on a tinted `Paper`; incoming → left-aligned, neutral surface. Shrink-to-content, capped at
292
+ ~75% of thread width (today's `maxWidth: 'min(100%, 680px)'`, `:66`, is a fixed cap regardless of
293
+ alignment — replace with a percentage-based cap and keep `min(...)` against a sane absolute ceiling for
294
+ very wide threads).
295
+ 2. **Meta moves inside**: move the timestamp + status-tick block from its own row into a small trailing
296
+ `Stack` inside the bubble's content area, right-aligned, "recessed" styling (smaller, lower-contrast —
297
+ Part A's confirmed-against-screenshots section). Sender name stays where it is conceptually (above text,
298
+ inside the bubble) but only for `conversation?.kind !== 'direct'` (unchanged condition).
299
+ 3. **Status as icons**: `ReadTicks.jsx` swaps its two `Typography` text branches for icon components
300
+ (`Done`/`DoneAll` from `@mui/icons-material`, following the repo's `Outlined`-variant convention where one
301
+ exists — check availability before picking exact icon names) with the existing i18n strings as
302
+ `aria-label`, own-messages-only (unchanged `canShowReadTicks` gate in `Thread.jsx`, do not touch it).
303
+ 4. **Actions collapse into one menu**: move the standalone reply `Button` (`:88`) INTO the existing `Menu`
304
+ (`:92-96`) as a new first entry, alongside React (opens `ReactionBar`'s existing `expanded` row — do not
305
+ nest an emoji picker inside the MUI `Menu`, per Part A) and the existing Edit/Delete/Copy entries. The
306
+ overflow `IconButton` (`:89`) becomes the single affordance — reuse the existing `MoreVertIcon` (already
307
+ the established overflow icon in this repo, `ConversationList.jsx` too), don't introduce a different one.
308
+ 5. **Two menu variants (5b — the easy-to-miss one)**: any message (own or incoming) gets Reply + React +
309
+ Copy; only a message where `canAct`-minus-the-reply-specific-parts holds (i.e. own message or moderator,
310
+ the existing author/moderator check) additionally gets Edit + Delete. This means the menu's visibility
311
+ condition changes from "`canAct` gates the whole menu" to "the menu is always offered when there's
312
+ anything to put in it (any message), but its CONTENTS vary" — restructure the render condition
313
+ accordingly; a fixture with only own messages will not catch a regression here (Part A's own warning).
314
+ 6. **Desktop hover+focus, touch tap**: fix the CSS trap directly — add a `&:focus-within .message-actions`
315
+ (or `:focus-visible`) rule alongside the existing `:hover` rule so keyboard Tab-focus reveals the
316
+ affordance (the element is already in the tab order, per Part A's analysis — `opacity:0` doesn't remove
317
+ it). For touch, add a tap-to-reveal state (e.g. local `actionsVisible` state toggled by a tap handler on
318
+ the bubble, independent of hover/focus) — no long-press, no swipe, no custom pointer-event tracking.
319
+ 7. **Reactions compact row**: `ReactionBar`'s existing-reactions row (not the add-reaction affordance, which
320
+ moves into the menu per point 4) becomes a compact row overlapping the bubble's bottom edge rather than a
321
+ full sibling Stack row — likely a negative top margin pulling it up into the bubble's bottom padding, or
322
+ an absolutely-positioned row anchored to the bubble. `ReactionBar`'s own `expanded`/`QUICK_EMOJIS` block
323
+ stays as-is; only its trigger changes (moves from `ReactionBar`'s own `AddReactionOutlinedIcon` button —
324
+ now redundant, since React lives in the menu — but the `expanded` row itself is unchanged).
325
+ 8. **Reply quote**: keep inside the bubble but restyle from a full-width `Button` (`:68-73`) to a tinted
326
+ block with a coloured leading border (e.g. `borderLeft`) — must still call `onJumpToMessage` on click
327
+ (existing prop, keep the click handler, only restyle the container).
328
+ 9. **Announcements/polls/attachments** (`:79-80`, `:83-87`): keep rendering inside the new bubble container;
329
+ check each in the harness (Part A's Risks section) since none of the four scoped test files assert on
330
+ their visual placement, only their data/behavior.
331
+
332
+ ### Verification harness — extend, don't just eyeball the diff
333
+
334
+ `dev/entries.jsx:140-157` already has a `messaging-message-bubble` entry, but it mounts a single bubble with
335
+ no `conversation` prop, no `currentUser` (so `canAct` is always false — no own-message demo), and no
336
+ `replyTo`/`children`/ReadTicks wired in — **insufficient to visually verify this WO's acceptance criterion**.
337
+ Either extend this entry with a currentUser matching one message's `sender.id` (for an own-message demo)
338
+ plus a second incoming-message instance, a `replyTo` prop, and mock `ReadTicks` children — or, likely
339
+ cleaner, extend the existing `messaging-thread` entry (`dev/entries.jsx:156`, `ThreadEntry` against
340
+ `messagingHarnessApi`) which already exercises the real Thread→MessageBubble wiring with realistic
341
+ `sender`/`reply_count`/replies data. Add fixture messages covering: a short own message (acceptance
342
+ criterion — one/two lines), a short incoming message, a long-single-unbroken-token message, a message at
343
+ 200% zoom (use the harness's viewport toggle), a group-conversation message (sender name visible) vs a
344
+ direct one (no sender name), an announcement, a poll, and an attachment. This is the actual verification
345
+ step — visually confirm the acceptance criterion before reporting `RESULT: DONE`.
346
+
347
+ ### Invariants / do-not-touch
348
+
349
+ - No Composer change (a different component, out of scope even though the triggering screenshot showed one).
350
+ - No change to `Thread.jsx`'s data flow, pagination, reply-thread expansion, unread-reply marker (MSG-3d),
351
+ or the read-status REST contract (`getMessageReadStatus` shape stays exactly as-is — two states, no new
352
+ backend call).
353
+ - No dcm change — the fan-out observation (one `getMessageReadStatus` call per own message) is a known,
354
+ pre-existing, and deliberately bounded cost; report the read-aggregate-on-`serialize_message` idea as a
355
+ dcm follow-up suggestion in the WO's own docs, do not implement it.
356
+ - `MessageBubble`'s `children` contract stays: hosts (jg-ferien today) pass `ReadTicks` through it and must
357
+ keep working unchanged from the outside.
358
+ - No new dependency — MUI plus `@mui/icons-material` is enough (both already used throughout `src/messaging/`).
359
+ - Every icon this WO introduces keeps its text as an `aria-label` — no i18n key gets deleted because its
360
+ text stopped being visually displayed (Part A's "Non-negotiable" section).
361
+
362
+ ### Tests — see Part A's "Tests to WRITE" list verbatim
363
+
364
+ Run: `pnpm vitest run tests/Thread.test.jsx tests/messagingMessageActions.test.jsx
365
+ tests/messagingInteractions.test.jsx tests/messagingPollRendering.test.jsx` plus any new file for the new
366
+ assertions (own/incoming alignment, menu-for-incoming-message, keyboard/touch affordance reachability,
367
+ icon aria-labels). These four existing files assert on data/behavior (menu gating, reaction round-trips,
368
+ poll rendering, ReadTicks own-message-only mounting), not DOM layout — good news for a redesign, but per
369
+ Part A: **if several existing tests need rewriting (not just relocating an assertion), that's a signal the
370
+ change went wider than the envelope — stop and report before proceeding**, don't just patch tests to match
371
+ whatever the implementation produced.
372
+
373
+ ### Progress contract
374
+
375
+ Emit a `PLAN: …` line first, then a single-line `PROGRESS: [n/total] <action>` before every relevant action
376
+ and `… done` on completion, no gap > ~2 min, unbuffered stdout, and exactly one final
377
+ `RESULT: DONE|BLOCKED <reason>`.
378
+
379
+ ### Constraints for the implementer
380
+
381
+ The WO text (Part A + this Part B) is the COMPLETE spec. Read `AGENTS.md` and
382
+ `.codex/skills/frontend-engineering` only for conventions. Stay in scope: `MessageBubble.jsx`,
383
+ `ReadTicks.jsx`, `ReactionBar.jsx`, the small `Thread.jsx` slice that mounts them, their tests, the messaging
384
+ i18n file if new labels are genuinely needed, and the `dev/` harness entry. Do not touch `Composer.jsx`,
385
+ `ConversationList.jsx`, poll/attachment components' internals, or anything outside this list. Do not update
386
+ `MEMORY.md`. **Do NOT `git add`/`commit`/`push`** — leave the diff for the Orchestrator's review; a prior WO
387
+ in this campaign (`ui-core-micha` MSG-3e) had its implementer self-commit and self-publish against this same
388
+ instruction — that was a governance breach, not a template to follow. **Do NOT bump the version or
389
+ publish** — the Orchestrator owns the release. WRITE the tests specified in Part A and RUN ONLY the listed
390
+ files plus any new ones — no full suite, no self-review; the Orchestrator owns the authoritative run, the
391
+ independent review, AND the `ui_reviewer` harness pass, and those are the gate.