@micha.bigler/ui-core-micha 2.23.0 → 2.25.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,271 @@
1
+ # WORK ORDER MSG-6i (ui-core-micha) — attachment tile size, poll spacing/denominator, wider emoji picker
2
+
3
+ **EXECUTION DIRECTIVE.** If you are the implementer reading this as your own spec, this section is not
4
+ addressed to you — it tells the Orchestrator how to invoke you; you ARE that invocation, do not shell
5
+ out to `codex exec`. Orchestrator: implement through `codex exec` in the background, invoked **directly
6
+ via Bash** (never the `debugger`/`*_coder` Agent wrappers) with **both** flags `--skip-git-repo-check`
7
+ and `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from this
8
+ file. Fall back to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.
9
+
10
+ Feature-level WO (Expertenchat envelope). Operator polish requests 2026-08-03, gathered in a
11
+ requirements round after MSG-6h shipped; four independent, small scopes bundled the same way
12
+ MSG-6f bundled A/B/C.
13
+
14
+ ## TIER
15
+ Tier 2 — shared-core UI consumed by every host app. Independent `reviewer` + `ui_reviewer` mandatory
16
+ (scope C changes poll math, not just styling).
17
+
18
+ ## SCOPE
19
+
20
+ **A. Attachment tile: 64px → 120px.**
21
+
22
+ `AttachmentList.jsx`'s tile `sx={{ width: 64, height: 64, ... }}` (both the `ButtonBase` and its
23
+ internal thumbnail `Box component="img"`, which is `width: '100%', height: '100%'` and inherits).
24
+ Change both dimensions to `120`. Check the non-image tile's icon/caption layout still reads well at
25
+ the larger size (currently sized for 64px — `InsertDriveFileOutlinedIcon fontSize="small"` and a
26
+ `caption` typography may look sparse/small inside a 120px box; consider `fontSize="medium"` or larger
27
+ if it looks disproportionate — operator did not specify, use judgement, flag if genuinely unsure).
28
+ `Stack direction="row" flexWrap="wrap"` spacing stays as-is unless the larger tiles visibly break
29
+ wrapping at typical bubble widths.
30
+
31
+ **B. Poll card: more breathing room, not a wider bubble.**
32
+
33
+ Operator, verbatim (after seeing a screenshot of the current cramped layout — checkbox, option text,
34
+ and percentage squeezed onto one tight line, thin bars close to the text, minimal gap between options):
35
+ *"Nicht generell breiter, aber luftiger."* (Not generally wider, but airier.) This is **not** scope A
36
+ of a width change — the poll's outer bubble width stays exactly as-is (still shares
37
+ `MessageBubble.jsx`'s `maxWidth: min(75%, 680px)` with every other message type). Increase internal
38
+ spacing in `PollCard.jsx`: the `Stack spacing={0.75}` wrapping the whole card, the per-option
39
+ `Stack direction="row" spacing={0.5}` (control + label), and the `LinearProgress`'s `height: 6` are
40
+ the likely levers — raise spacing values and/or bar height/thickness so the card reads as roomier.
41
+ No prescribed exact values; operator did not send further numeric guidance beyond the "airier, not
42
+ wider" direction — use reasonable judgement (e.g. spacing bumped by ~50%, bar height 8-10px) and
43
+ **flag this specific change for a live screenshot check before considering it closed** — same
44
+ disclosed-limitation pattern as MSG-6h's reaction-spacing fix (no staging credentials this round).
45
+
46
+ **C. Poll percentage denominator: respondents of this poll, not sum of votes cast.**
47
+
48
+ **This reverses an explicit non-goal from `MSG-6g`'s own envelope**, confirmed deliberately by the
49
+ operator in this round (not an oversight): MSG-6g stated *"Denominator: total votes cast, not
50
+ participant count. A poll's turnout relative to group size is a different question and is not in
51
+ scope."* — operator's correction: for a **multi-select** poll, summing `vote_count` across options can
52
+ exceed the number of people who actually answered (one person can vote for several options), so a
53
+ per-option percentage computed against that inflated sum reads as misleadingly low and the bars
54
+ don't sum to a sensible 100%. The new denominator: **the number of distinct people who cast at least
55
+ one vote in this poll** — not total conversation participants (an un-started, never-voting group
56
+ member should not silently deflate everyone else's percentages further).
57
+
58
+ Compute this **client-side, no backend change**: `serialize_poll`'s existing `option.voters` arrays
59
+ (already present, already used for the viewer's-own-vote marking) already carry every voter's id per
60
+ option — the union of all options' `voters` arrays, deduplicated, is exactly "everyone who answered
61
+ this poll." Replace `PollCard.jsx`'s current
62
+ `totalVotes = options.reduce((sum, option) => sum + (option.vote_count ?? option.voters?.length ?? 0), 0)`
63
+ with a respondent count derived from the union of `option.voters` across all options (a `Set` of voter
64
+ ids). **Zero-vote rendering must stay `NaN`-free** (existing guard, do not regress it) — zero
65
+ respondents still means 0% for every option, not a division error.
66
+
67
+ **D. Composer emoji picker: its own, wider curated list, decoupled from reaction quick-picks.**
68
+
69
+ `Composer.jsx:144` currently reuses `QUICK_EMOJIS` from `ReactionBar.jsx` (`['👍', '❤️', '😂', '🎉',
70
+ '👀']`, 5 entries) — the same tiny set meant for quick message reactions, not for composing free-form
71
+ text. Operator confirmed direction after discussion: mobile users already have a full native emoji
72
+ keyboard (no in-app work needed there); desktop users lack an obvious equivalent, so a **wider curated
73
+ grid (~20-30 common emojis)** in the Composer's own picker adds real value. **Not** a full
74
+ search-based picker (out of scope — meaningfully more engineering for a need mobile already covers).
75
+
76
+ Define a new, separate constant (e.g. `COMPOSER_EMOJIS` in `Composer.jsx` itself, or a shared
77
+ `emojiSets.js` if that reads cleaner) with ~20-30 common emojis, grouped loosely by theme (smileys,
78
+ gestures, hearts, objects/travel — operator did not specify an exact list; use a reasonable, common
79
+ default and flag it as easily adjustable). Render as a grid inside the existing `Menu` (currently a
80
+ plain vertical `MenuItem` list — at 25-30 entries that would be a very long scroll; switch to a
81
+ wrapping grid layout, e.g. a `Box` with `display: grid` or `flexWrap`, inside the `Menu`). Keep
82
+ `ReactionBar.jsx`'s own `QUICK_EMOJIS` untouched (5, curated, unchanged) — reactions stay small and
83
+ fast, this is a Composer-only change.
84
+
85
+ ## NON-GOALS / DO NOT TOUCH
86
+ - Do not touch `ReactionBar.jsx`'s own reaction UI or its 5-emoji `QUICK_EMOJIS` — scope D adds a
87
+ second, separate, larger set for the Composer only.
88
+ - Do not widen the message bubble's `maxWidth` (`MessageBubble.jsx`) — scope B is internal spacing
89
+ only, confirmed explicitly by the operator.
90
+ - Do not touch `MessageBubble.jsx`'s reaction-overlap fix from MSG-6h, or the lightbox/context-menu
91
+ logic from MSG-6h — this WO only resizes the tile (scope A), doesn't change its interaction model.
92
+ - No backend change for scope C — the data needed already exists in the current API response.
93
+ - Do not build a full emoji search/picker component (scope D) — a curated grid only.
94
+
95
+ ## RISKS
96
+ - Scope C is a **behaviour change visible to every host app** the moment this version is consumed —
97
+ a poll's displayed percentages will differ from before for any multi-select poll where the same
98
+ person voted for multiple options. State this plainly; it is the operator's own explicit correction,
99
+ not a silent reinterpretation.
100
+ - Scope A's larger tile changes vertical rhythm inside a message bubble containing attachments —
101
+ check it doesn't visually crowd the meta row (timestamp/read-ticks) directly below it.
102
+ - Scope B and D have no prescribed exact values — implement a reasonable default, disclose the
103
+ specific choices made (spacing multipliers, the exact emoji list) in the completion note so the
104
+ operator can request a numeric tweak without re-deriving what was chosen.
105
+
106
+ ## REQUIRED TESTS TO WRITE
107
+ Narrow and behavioural. Do NOT run the full suite.
108
+
109
+ 1. Attachment tile renders at 120×120 (image and non-image cases).
110
+ 2. Poll respondent-count denominator: a multi-select poll where one voter voted for two options must
111
+ show percentages based on the **respondent count** (e.g. 1 respondent → both their chosen options
112
+ show 100%, not summed-and-diluted), not `sum(vote_count)`. This must **fail against the current
113
+ `sum(vote_count)` code** — prove it with a case where the two differ (multi-select, one voter,
114
+ two options voted).
115
+ 3. Poll respondent-count denominator: the existing single-choice zero-vote case still shows 0%, no
116
+ `NaN`, no regression (reuse/extend the existing MSG-6g zero-vote test's assertion shape).
117
+ 4. Composer emoji picker: opening it shows more than 5 emoji options (the expanded set, not
118
+ `ReactionBar`'s `QUICK_EMOJIS`); selecting one still inserts it at the cursor position (existing
119
+ `insertEmoji` behaviour, unchanged — just fed from the new list).
120
+ 5. `ReactionBar`'s own picker is unaffected — still exactly its original 5 `QUICK_EMOJIS` (regression
121
+ guard that scope D didn't leak into scope-unrelated reaction UI).
122
+
123
+ ## TEST SCOPE FOR THE GATE (orchestrator)
124
+ Attachment-, poll-, and Composer-adjacent messaging test files. Not the full suite.
125
+
126
+ ## TARGET REPO
127
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha`. Branch `main`. Publish per the repo's release flow;
128
+ the jg-ferien pin bump is part of THIS WO's completion, not a deferred step (standing correction this
129
+ session: publish alone is not delivery).
130
+
131
+ ## MINI-HANDOVER (pastable)
132
+
133
+ > Repo: `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`). Work order:
134
+ > `work-orders/MSG-6i.md` — read it fully, then follow the `orchestrate-codex` skill.
135
+ >
136
+ > Four independent scopes: (A) attachment tile 64px→120px in `AttachmentList.jsx`. (B) `PollCard.jsx`
137
+ > internal spacing increased ("airier, not wider" — bubble maxWidth unchanged) — no exact numbers
138
+ > given, use judgement, flag for a live screenshot check. (C) poll percentage denominator changes from
139
+ > `sum(vote_count)` to the count of distinct respondents (union of all options' `voters` arrays,
140
+ > client-side, no backend change) — **this deliberately reverses an explicit MSG-6g non-goal**, operator
141
+ > re-confirmed. (D) Composer's emoji picker gets its own ~20-30-entry curated grid, decoupled from
142
+ > `ReactionBar`'s 5-entry `QUICK_EMOJIS` (untouched) — no full search picker, mobile already has a
143
+ > native one.
144
+
145
+ ## PROGRESS CONTRACT
146
+ Emit `PLAN: <steps>` up front, then a single-line `PROGRESS: [<n>/<total>] <action>` before every
147
+ relevant action and `PROGRESS: [<n>/<total>] done` on completion, spaced so no gap exceeds ~2 min,
148
+ stdout unbuffered, and exactly one final `RESULT: DONE|BLOCKED <reason>`.
149
+
150
+ ---
151
+
152
+ ## Part B — Implementation map (Orchestrator) / as-built record
153
+
154
+ **Codex was unavailable (out of credits) — implemented directly by the Orchestrator.**
155
+
156
+ ### Scope A — `AttachmentList.jsx`
157
+ Tile `sx={{ width: 64, height: 64, ... }}` → `120, 120`. Non-image tile's icon bumped
158
+ `fontSize="small"` → `"large"` and its `Stack` spacing `0.25` → `0.75` — the original
159
+ small icon+caption pairing read sparse against the much larger tile.
160
+
161
+ ### Scope B — `PollCard.jsx` spacing
162
+ Outer `Paper` padding `p: 1` → `1.5`; outer `Stack` (question/options/close-row) spacing
163
+ `0.75` → `1.5`; per-option `richLabel` `Stack` spacing `0.25` → `0.75`; `LinearProgress`
164
+ `height: 6` → `8`. No exact values were specified by the operator beyond "airier, not
165
+ wider" (confirmed via a screenshot of the cramped original) — **not independently
166
+ visually re-verified against a fresh screenshot before closing** (no staging/live
167
+ credentials for this specific check this round); flagging per the same disclosed-
168
+ limitation pattern as MSG-6h's reaction-spacing fix.
169
+
170
+ ### Scope C — `PollCard.jsx` percentage denominator
171
+ `totalVotes = sum(option.vote_count)` replaced with
172
+ `respondentCount = new Set(options.flatMap(option => option.voters || [])).size` — the
173
+ count of distinct people who voted for *any* option, computed client-side from data
174
+ already present in the response (`voters` arrays, already used for the viewer's-own-vote
175
+ marking). `percent` now divides by `respondentCount`.
176
+
177
+ ### Scope D — `Composer.jsx` emoji picker
178
+ New `COMPOSER_EMOJIS` (28 entries) defined locally in `Composer.jsx`, rendered as a
179
+ 7-column CSS grid of `IconButton`s inside the existing `Menu` (replacing the old
180
+ `QUICK_EMOJIS.map(... <MenuItem>...)` vertical list — `MenuItem` import removed, now
181
+ unused). `ReactionBar.jsx`'s own `QUICK_EMOJIS` (5, curated) untouched.
182
+
183
+ ### Companion fix found and fixed — `Thread.jsx` (not in this WO's original scope)
184
+ Discovered while live-verifying jg-ferien's `MSG-18` (mobile Composer hidden behind the
185
+ fixed bottom nav) in the *same* browser session as this WO's own work: after fixing every
186
+ jg-side layout offset, the Composer was **still** rendering behind the nav. Root cause
187
+ traced into this package: `Thread.jsx`'s root `<Stack>` had no `flex`/`minHeight`, and its
188
+ scroll `Box` used a **hardcoded `maxHeight: 560`** — neither participates in a host's
189
+ flex-column layout at all, so on a short mobile viewport the thread never shrinks to fit,
190
+ regardless of how correctly the *host* sizes its own containers. Fixed: both the root
191
+ `Stack` and the scroll `Box` gained `flex: 1, minHeight: 0` (the classic flex-item
192
+ `min-height: auto` trap — a flex item won't shrink below its content's natural size
193
+ without this). `maxHeight: 560` kept as an upper-bound fallback, so a host that does
194
+ **not** embed `Thread` in a flex-column context sees **zero behaviour change** (both new
195
+ properties are no-ops outside a flex context). Live-verified end-to-end (see MSG-18's own
196
+ as-built record in jg-ferien for the full trace): the composer moved from `top: 742`
197
+ (inside the fixed nav's own `740–812` range) to `top: 629.5` (a clean 64px gap above the
198
+ nav at `740`), by locally building this package's `dist/` and patching it into jg's
199
+ `node_modules` for verification — no publish needed to prove the fix, but IS needed to
200
+ actually ship it (bundled into this same release, not deferred).
201
+
202
+ ### Tests
203
+ `tests/messagingMsg6i.test.jsx` (new) — 7 tests: all 5 WO-required cases, one extra
204
+ (emoji-insertion behaviour still works from the new grid), plus a 6th added post-review
205
+ (see R3 below) asserting `Thread.jsx`'s `flex`/`minHeight` are actually applied.
206
+
207
+ ### Verification (pre-review)
208
+ Full suite 249 → 255 (6 new), `tsc` clean.
209
+
210
+ ### Independent review — `reviewer` + `ui_reviewer`, both run in parallel, findings + fixes
211
+
212
+ Both reviewers independently converged on the same P2 keyboard-navigation regression
213
+ (scope D) — treated as one finding below (R1/U1).
214
+
215
+ - **R1/U1 (P2, confirmed by both reviewers) — Composer's emoji grid broke MUI `Menu`'s
216
+ arrow-key roving-focus.** Nesting the 28 `IconButton`s inside an intervening
217
+ `<Box role="group">` meant `MenuList`'s `moveFocus` (which only traverses **direct**
218
+ children) saw a single non-tabbable child, so ArrowDown/Up/Home/End all became dead —
219
+ none of the 28 buttons reachable via the menu's own keyboard model, and nesting
220
+ `role="button"` descendants under `role="menu"` isn't a conformant ARIA pattern either.
221
+ **Fixed**: swapped `<Menu>` for `<Popover>` (`Composer.jsx`) — a grid of plain buttons
222
+ was never actually a linear menu, so this stops advertising semantics it can't deliver
223
+ and leaves keyboard traversal to the browser's native Tab order, which does reach every
224
+ button. Replicated Menu's default `anchorOrigin`/`transformOrigin`
225
+ (`{vertical:'bottom',horizontal:'left'}` / `{vertical:'top',horizontal:'left'}`) so the
226
+ picker's on-screen position is unchanged.
227
+ - **R2 (P2, `reviewer`) — optimistic vote could desync `vote_count` from `voters`.**
228
+ `MessagingProvider.applyOptimisticVote` bumped `vote_count` unconditionally but only
229
+ updated `voters` when `userId` was known (a documented "brief window" before
230
+ `AuthContext` populates `user` — in practice also the default state for any host/test
231
+ that doesn't wrap `MessagingProvider` in `AuthContext` at all, confirmed while fixing
232
+ this). Scope C's new `respondentCount` denominator reads only from `voters`, so that
233
+ window could show a transiently inconsistent/>-100% percentage. **Fixed**: added a
234
+ sentinel (`OPTIMISTIC_ANONYMOUS_VOTER`, a space-prefixed string no real UUID can match)
235
+ used in place of `userId` in `voters` when it's unknown, so `vote_count` and `voters`
236
+ move together in every case — the real vote still overwrites this on
237
+ reconcile/revert. Re-ran every existing poll-vote test
238
+ (`messagingMsg6g/Interactions/PollRendering/CompactBubble/Api`) to confirm the
239
+ already-tested optimistic-count behaviour (which relies on the no-`AuthContext` test
240
+ shape) still holds — it does.
241
+ - **R3 (P2, `reviewer`) — `Thread.jsx`'s flex/minHeight companion fix had zero regression
242
+ coverage**, and the "not meaningfully assertable via jsdom" justification didn't hold up
243
+ (this WO's own tile-size test already proves resolved `sx` values ARE readable via
244
+ `getComputedStyle` without real layout). **Fixed**: added test 6 to
245
+ `tests/messagingMsg6i.test.jsx`, asserting `flexGrow`/`minHeight` on both `Thread`'s
246
+ root `Stack` and scroll `Box`, plus that `maxHeight: 560` is still present as the
247
+ fallback cap.
248
+ - **U2 (P3, `ui_reviewer`) — hardcoded `fontSize: '1.15rem'`** on the per-emoji
249
+ `IconButton`. No existing theme/typography token covers emoji-glyph sizing (checked
250
+ `ReactionBar.jsx`'s own emoji rendering for precedent — none exists there either).
251
+ **Not fixed** — P3, no clear correct token to switch to; noted here rather than
252
+ silently dropped.
253
+ - **U3 (P3, `ui_reviewer`) — the emoji grid reused `MessagingComposer.ADD_EMOJI`** as its
254
+ `role="group"` `aria-label`, identical to the trigger button's own label, so a screen
255
+ reader announces "Add emoji" twice back-to-back. **Fixed**: added a dedicated
256
+ `MessagingComposer.EMOJI_PICKER` key ("Choose an emoji" / "Emoji auswählen" / "Choisir
257
+ un emoji") for the grid.
258
+ - **R4 (P3, `reviewer`) — test 4b's assertion (`input.value.length > 2`) didn't prove
259
+ cursor-position insertion**, only that something got appended. **Fixed**: rewrote it to
260
+ match the exact expected string at a mid-string cursor position (same shape as the
261
+ pre-existing `messagingComposerChunk5.test.jsx` assertion).
262
+ - **R5 (P3, `reviewer`) — residual, not fixed**: the 120px tile stretches whatever
263
+ `getAttachmentThumbnail` returns via `objectFit: cover`; if that thumbnail is
264
+ server-sized for the old 64px target, the larger tile could show blur. Not verifiable
265
+ from this repo (backend-controlled, untouched by this WO) — flagged as disclosed
266
+ residual risk, consistent with scope B's own "no live screenshot check this round" note.
267
+
268
+ ### Verification (post-review)
269
+ Full suite (single-forked to avoid an unrelated vitest worker-pool timeout seen under the
270
+ default parallel pool): **45 test files, 256/256 passed** (255 + 1 new). `tsc -p
271
+ tsconfig.build.json --noEmit` clean.