@micha.bigler/ui-core-micha 3.6.0 → 3.7.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.
Files changed (47) hide show
  1. package/DESIGN.md +1 -1
  2. package/README.md +2 -0
  3. package/WORK_ORDERS.md +7 -0
  4. package/dist/auth/UserMenu.js +23 -1
  5. package/dist/components/EmptyState.js +17 -0
  6. package/dist/components/UserListComponent.js +2 -1
  7. package/dist/i18n/authTranslations.js +196 -0
  8. package/dist/i18n/uiCoreTranslations.js +19 -0
  9. package/dist/index.js +2 -1
  10. package/dist/messaging/Composer.js +4 -2
  11. package/dist/messaging/MessageBubble.js +2 -1
  12. package/dist/messaging/ReadTicks.js +5 -3
  13. package/dist/messaging/linkifyText.js +39 -0
  14. package/dist/pages/PasswordInvitePage.js +23 -8
  15. package/dist/pages/SignUpPage.js +4 -4
  16. package/dist/theme/kitSxRegistry.js +5 -1
  17. package/package.json +1 -1
  18. package/src/auth/UserMenu.jsx +26 -1
  19. package/src/components/EmptyState.jsx +38 -0
  20. package/src/components/UserListComponent.jsx +2 -0
  21. package/src/i18n/authTranslations.ts +197 -0
  22. package/src/i18n/uiCoreTranslations.js +10 -0
  23. package/src/index.js +2 -1
  24. package/src/messaging/Composer.jsx +7 -3
  25. package/src/messaging/MessageBubble.jsx +2 -1
  26. package/src/messaging/ReadTicks.jsx +4 -3
  27. package/src/messaging/linkifyText.jsx +37 -0
  28. package/src/pages/PasswordInvitePage.jsx +36 -7
  29. package/src/pages/SignUpPage.jsx +79 -68
  30. package/src/theme/kitSxRegistry.js +9 -1
  31. package/tests/EmptyState.test.jsx +62 -0
  32. package/tests/PasswordInvitePage.test.jsx +135 -0
  33. package/tests/SignUpPage.test.jsx +15 -2
  34. package/tests/UserListComponent.test.jsx +85 -0
  35. package/tests/UserMenu.test.jsx +40 -4
  36. package/tests/i18nGermanVariant.test.js +65 -0
  37. package/tests/messagingComposer.test.jsx +25 -0
  38. package/tests/messagingLinkify.test.jsx +51 -0
  39. package/tests/messagingReadTicksTooltip.test.jsx +36 -0
  40. package/tests/userScreenTranslations.test.jsx +165 -0
  41. package/work-orders/UCM-AUTH-9.md +207 -0
  42. package/work-orders/UCM-I18N-4.md +226 -0
  43. package/work-orders/UCM-MSG-20.md +192 -0
  44. package/work-orders/UCM-PRIM-2.md +114 -0
  45. package/work-orders/UCM-SHELL-7.md +134 -0
  46. package/work-orders/UCM-THEME-13.md +69 -0
  47. package/work-orders/UCM-THEME-14.md +140 -0
@@ -0,0 +1,226 @@
1
+ # UCM-I18N-4 — Let a consumer choose informal German, and translate the hard-coded English in the auth and user screens
2
+
3
+ # A. Envelope — authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ - Goal: an app that addresses its users with "du" gets "du" in every ucm screen, and no ucm screen
8
+ shows hard-coded English in a German UI.
9
+ - Expected outcome:
10
+ - The kit's German strings exist in a formal ("Sie", today's default) and an informal ("du")
11
+ variant; a consumer picks one when it registers the kit's strings (the single export from
12
+ `I18N-1`). Apps that do nothing keep today's formal German.
13
+ - `UserListComponent`, the invite panel (`UserInviteComponent`, `BulkInviteCsvTab`) and their
14
+ messages use translation keys for every visible string, in all configured locales.
15
+
16
+ ## Context — measured 2026-09-29 in jg-ferien's local browser test (ucm 3.5.0 installed there)
17
+
18
+ - jg-ferien addresses its users with "du" throughout; ucm's auth screens use "Sie" — login, signup,
19
+ reset request, invite and reset pages (`src/i18n/authTranslations.ts`), so the break runs through
20
+ one flow (`/signup` "Sie", jg's `/signup/confirm` "du"), and one page mixes both (invite page
21
+ heading "Sie", validation "du").
22
+ - Hard-coded English in German UI: `UserListComponent.jsx` — "All Users", "Search",
23
+ "Search users...", "Rows per page:", "Successful Login", "Are you sure you want to delete this
24
+ user?"; invite panel — "Invite a new user", "Bulk Invite via CSV", "Upload a CSV file containing
25
+ email addresses. Header "email" is supported.", "Upload CSV", "Send Invites", success "Invitation
26
+ sent.".
27
+ - **Operator decision 2026-09-29: generic fixes go to ucm, app-specific ones to the app.**
28
+
29
+ ## Scope + non-goals
30
+
31
+ - In scope: the variant mechanism at registration time; the informal German variant of the auth
32
+ bundle; keys for the hard-coded strings above (de/en/fr at least, plus every locale the bundles
33
+ already carry).
34
+ - Explicit non-goals / do-not-touch:
35
+ - Default behaviour for existing consumers stays formal German — no consumer changes wording
36
+ without opting in.
37
+ - Behavioural fixes of those screens are `UCM-AUTH-9`.
38
+
39
+ ## Tier · gates
40
+
41
+ - **Tier 3** — change inside shared-core, a new public option on the registration export.
42
+ - Reviewer: `reviewer` (all lenses) · `ui_reviewer`.
43
+ - **Done means released and jg-ferien's pin bumped with the option set** (separate jg step).
44
+
45
+ ## Risks
46
+
47
+ - A variant that only covers some keys silently falls back to "Sie" for the rest; a test must prove
48
+ key parity between the two German variants.
49
+ - Consumers that already override ucm keys locally must keep working (override order unchanged).
50
+
51
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the orchestrator's run is the gate)
52
+
53
+ 1. Registering with the informal option yields "du" strings for the auth pages; without it, today's
54
+ strings.
55
+ 2. Key parity: formal and informal German cover the same key set.
56
+ 3. `UserListComponent` and the invite panel render no hard-coded English (render in `de`, assert the
57
+ listed strings are absent and translated keys present).
58
+ 4. Four-locale shape test (`I18N-3`) still passes for the new keys.
59
+
60
+ ---
61
+
62
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
63
+
64
+ ## Context package — two independent parts
65
+
66
+ ### 1. The variant mechanism + the informal auth bundle
67
+
68
+ - **What "registers the kit's strings" means today, concretely** (so the mechanism plugs into the
69
+ real thing, not a hypothetical): every consuming app's own `i18n/index.js` imports the named
70
+ bundle exports (`authTranslations`, `messagingTranslations`, …) or the single aggregate
71
+ `uiCoreTranslations` (`src/i18n/uiCoreTranslations.js`, built by `I18N-1`) and spreads them into
72
+ its own i18next `resources` object — see `jg-ferien/frontend/i18n/index.js` for the exact pattern
73
+ (not this repo; read it for reference only, do not edit it — jg's own pin-bump/adoption is a
74
+ separate step, out of this WO). **No app currently imports `uiCoreTranslations` itself** (checked
75
+ — only per-feature bundles are imported today), so changing how the aggregate is BUILT carries no
76
+ live migration risk.
77
+ - **Do not change `uiCoreTranslations`'s existing shape or behaviour** — `tests/i18nAggregate.test.js`
78
+ (`I18N-1`/`I18N-3`) asserts it is byte-identical to the plain merge of the 7 per-feature bundles;
79
+ breaking that assertion is not this WO's job. Add a NEW function alongside it instead:
80
+ ```js
81
+ // src/i18n/uiCoreTranslations.js — ADD this export, keep the existing `uiCoreTranslations` export unchanged
82
+ export function createUiCoreTranslations({ germanVariant = 'formal' } = {}) {
83
+ return Object.fromEntries(
84
+ Object.entries(uiCoreTranslations).map(([key, value]) => {
85
+ const { de_informal, ...rest } = value;
86
+ if (germanVariant === 'informal' && de_informal) return [key, { ...rest, de: de_informal }];
87
+ return [key, rest];
88
+ }),
89
+ );
90
+ }
91
+ ```
92
+ This is "the single export a consumer picks a variant with" (the Envelope's phrase) — a future jg
93
+ adoption step calls `createUiCoreTranslations({ germanVariant: 'informal' })` instead of spreading
94
+ `uiCoreTranslations` directly; every OTHER existing consumer is completely unaffected (they don't
95
+ call this function at all, and `uiCoreTranslations` itself is untouched) — this is what "no
96
+ consumer changes wording without opting in" means mechanically.
97
+ - **The informal German variant itself** — add a sibling `de_informal` field to the `de` value of
98
+ every key in `src/i18n/authTranslations.ts` where the FORMAL wording actually depends on
99
+ Sie/Ihr-form address. Do NOT add `de_informal` to a key whose German text has no such dependency
100
+ (e.g. "E-Mail-Adresse", "Passwort", "QR-Token" read identically either way) — the function above
101
+ already falls back to the formal `de` value when `de_informal` is absent, so omitting it there is
102
+ correct, not incomplete.
103
+ - Find every Sie-dependent key with a word-boundary scan of each entry's `de` value against
104
+ `\bSie\b|\bIhr\b|\bIhre\b|\bIhrem\b|\bIhren\b|\bIhrer\b|\bIhres\b` (covers "Sie", "Ihr(e/em/en/er/es)"
105
+ in all inflected forms) — this repo already has one page that MIXES both forms today
106
+ (`Auth.MFA_RECOVERY_TITLE`'s description at `authTranslations.ts` uses "du" while
107
+ `Auth.PAGE_INVITE_SUBTITLE` uses "Sie" — measured, not hypothetical), so a plain "does the file
108
+ already use du sometimes" check is not sufficient; use the regex scan.
109
+ - For each matched key, write a natural informal ("du"/"dein"/"dir"/imperative-du) rewording as
110
+ `de_informal`, keeping the same meaning, placeholders (`{{email}}` etc. must appear identically),
111
+ and punctuation style as the formal version. This is a real translation task — this is a
112
+ shared-core kit that already ships a disclosed machine-translated Swahili bundle (`I18N-3`); do
113
+ the same here: **disclose in your final `RESULT:` line that the informal wording was machine-
114
+ translated by you (the implementing model), not reviewed by a fluent German speaker**, exactly
115
+ as `I18N-3` did for Swahili. Do not silently imply human review.
116
+ - **Test file:** extend `tests/i18nAggregate.test.js` (it already imports `uiCoreTranslations` and
117
+ the per-bundle exports — read it first) or add a new `tests/i18nGermanVariant.test.js`, covering:
118
+ 1. For a representative key (e.g. `Auth.PAGE_INVITE_SUBTITLE`),
119
+ `createUiCoreTranslations({ germanVariant: 'informal' }).['Auth.PAGE_INVITE_SUBTITLE'].de`
120
+ is the informal wording and does NOT match the Sie-form regex; the default call
121
+ (`createUiCoreTranslations()`, no argument) returns the SAME `de` value as plain
122
+ `uiCoreTranslations['Auth.PAGE_INVITE_SUBTITLE'].de` (today's formal string, unopted-in
123
+ consumers see no change).
124
+ 2. **The real parity guard the Risk note asks for:** scan `authTranslations` for every key whose
125
+ `de` matches the Sie-form regex above, and assert EACH such key has a non-empty `de_informal`
126
+ that does NOT itself match the Sie-form regex (proves it was actually rewritten, not copied).
127
+ This is the test that would fail if a future key were added with formal wording only and
128
+ forgotten — the actual failure mode the Envelope's Risk names, not a generic "same key set"
129
+ check (which would be trivially true by construction here and prove nothing).
130
+ 3. Four-locale shape test (`I18N-3`, `tests/i18nAggregate.test.js`'s existing
131
+ `REQUIRED_LOCALES = ['de','en','fr','sw']` check) still passes — it iterates `uiCoreTranslations`
132
+ itself, which is unchanged; run it to confirm, do not edit it.
133
+ 4. Existing consumers unaffected: `uiCoreTranslations['Auth.PAGE_INVITE_SUBTITLE']` (the plain,
134
+ unchanged export) still equals exactly what it did before this WO — read from
135
+ `authTranslations.ts` directly, minus the new `de_informal` field which existing code never
136
+ reads.
137
+
138
+ ### 2. Hard-coded English in `UserListComponent`, `UserInviteComponent`, `BulkInviteCsvTab`
139
+
140
+ - **Every string named in the Envelope's Context already uses `t(key, englishDefault)`** — the
141
+ translation KEYS are already referenced in the JSX, they are simply missing from every bundle, so
142
+ `t()` silently falls through to the English default regardless of locale. **In almost every case
143
+ the fix is adding the bundle entry, not touching the component.** Add these keys to
144
+ `src/i18n/authTranslations.ts` (all four locales it already carries: de/en/fr/sw — plain, correct
145
+ translations, NOT the informal-German mechanism from part 1, this is unrelated hard-coded-string
146
+ work):
147
+ - From `src/components/UserListComponent.jsx` (verified against the file, line refs current):
148
+ `UserList.DELETE_CONFIRM` (`:93`), `Common.OPERATION_FAILED` (`:278`), `UserList.NEW` (`:313`),
149
+ `UserList.SUCCESSFUL_LOGIN` (`:331`), `UserList.ROLE` (`:349`), `Common.ACTIONS` (`:398`),
150
+ `UserList.TITLE` (`:503`), `Common.SEARCH` (`:509`), `UserList.SEARCH_PLACEHOLDER` (`:510`),
151
+ `UserList.NO_USERS` (`:560`). (`Common.YES`/`Common.NO`/`Common.DELETE`/`Auth.EMAIL_LABEL`/
152
+ `Profile.NAME_LABEL` already exist — do not re-add them.)
153
+ - **One genuine code change, not just a bundle entry:** the `<TablePagination>` at `:581` has no
154
+ localisation props at all — MUI supplies its own English default "Rows per page:" directly, it
155
+ is not reachable via `t()` today. Add `labelRowsPerPage={t('UserList.ROWS_PER_PAGE', 'Rows per
156
+ page:')}` to that element, plus the new `UserList.ROWS_PER_PAGE` bundle entry.
157
+ - From `src/components/UserInviteComponent.jsx`: `Auth.INVITE_SENT_SUCCESS` (`:29`, `:104` in
158
+ `BulkInviteCsvTab.jsx` too — shared key), `Auth.INVITE_TITLE` (`:43`). (`Auth.EMAIL_LABEL`,
159
+ `Auth.INVITE_BUTTON` already exist.)
160
+ - From `src/components/BulkInviteCsvTab.jsx`: `Account.BULK_INVITE_NO_EMAILS` (`:82`),
161
+ `Account.BULK_INVITE_PARSE_FAILED` (`:87`), `Account.BULK_INVITE_DONE` (`:117`, has `{{ok}}`/
162
+ `{{total}}` interpolation — keep both placeholders in every locale), `Account.BULK_INVITE_TITLE`
163
+ (`:129`), `Account.BULK_INVITE_HINT` (`:132-135`, multi-line `t()` call — easy to miss with a
164
+ single-line grep, it IS in scope), `Account.BULK_INVITE_UPLOAD` (`:143`),
165
+ `Account.BULK_INVITE_SEND` (`:147`), `Account.BULK_INVITE_COUNT` (`:150`, `{{count}}`),
166
+ `Common.STATUS` (`:166`), `Common.DETAILS` (`:167`), `Account.BULK_INVITE_PROGRESS` (`:190`,
167
+ `{{done}}`/`{{total}}`), `Account.BULK_INVITE_SUCCESS_COUNT` (`:192`, `{{count}}`).
168
+ (`Auth.EMAIL_LABEL`, `Common.SUCCESS`, `Common.ERROR` already exist.)
169
+ - **`UCM-AUTH-9` already landed** (earlier in this release's sequence) and touched this SAME
170
+ file: it added `UserList.ROLE_UPDATE_SUCCESS` (already present, all four locales — do not
171
+ re-add it) and, after its own review, reused the pre-existing `Auth.SELF_SIGNUP_DISABLED` key
172
+ for the closed-signup message rather than adding a new `Auth.SIGNUP_CLOSED` key (that key does
173
+ NOT exist — do not look for it or add it).
174
+ - Test file: extend `tests/UserListComponent`/`UserInviteComponent`/`BulkInviteCsvTab` tests if they
175
+ exist (grep `tests/`), or add focused new ones:
176
+ 1. Render each of the three components with `i18n.language = 'de'` (or however this repo's test
177
+ harness sets locale — check an existing test that asserts translated text, e.g.
178
+ `tests/authTranslations.test.js`, for the pattern) and assert NONE of the literal English
179
+ default strings listed above are present, and the translated (German) text IS present.
180
+ 2. Four-locale shape test (`i18nAggregate.test.js`) still passes for every new key.
181
+
182
+ ## Do-not-touch / invariants
183
+
184
+ - Default behaviour for existing consumers stays formal German — `uiCoreTranslations` itself must
185
+ not change; only the new `createUiCoreTranslations` function is variant-aware.
186
+ - Behavioural fixes of the auth screens are `UCM-AUTH-9`'s scope, not this one — this WO only adds
187
+ strings/keys and the variant mechanism, it does not change component logic (aside from the one
188
+ named `labelRowsPerPage` prop addition, which is not a behaviour change).
189
+ - No secrets, no dependency changes — everything here is data (translation bundles) plus one small
190
+ pure function.
191
+
192
+ ## Target repo working directory (absolute)
193
+
194
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`)
195
+
196
+ ## Preamble — a REQUIRED block IN this file, not something appended at invocation
197
+
198
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
199
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
200
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
201
+ > unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the register
202
+ > row and the review verdicts are the orchestrator's alone.** **Your tools are for editing source
203
+ > and test files and for running the tests you wrote — nothing else.** Do NOT install dependencies,
204
+ > touch a lockfile, run a package manager, or tidy up stray files; if something in the repo state
205
+ > blocks you, stop and report it as `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT
206
+ > `git add`/`commit`/`push` — leave every
207
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
208
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
209
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
210
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
211
+ > those are the gate; your own run does not count as the gate.
212
+ >
213
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
214
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
215
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
216
+ > `RESULT: DONE|BLOCKED <reason>`.
217
+
218
+ ---
219
+
220
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
221
+
222
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
223
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
224
+ > reviewers, no verification run, no register edit, no commit.**
225
+
226
+ *(to be filled by the Orchestrator: execution directive; `reviewer` (all lenses) + `ui_reviewer`; release; jg-ferien pin bump with the informal option; register rows here and in jg (`JG-UX-10`).)*
@@ -0,0 +1,192 @@
1
+ # UCM-MSG-20 — Message composer collapses on narrow screens
2
+
3
+ # A. Envelope — authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ - Goal: on a 375 px phone a person can type and send a message in every conversation type.
8
+ - Expected outcome:
9
+ - The text field keeps a usable width (at least most of the row) next to the action icons; the send
10
+ button is fully visible.
11
+ - URLs in messages are clickable links.
12
+ - The read marker next to the time explains itself (label or tooltip), or is not shown where it
13
+ carries no information.
14
+
15
+ ## Context — measured 2026-09-29 in jg-ferien's local browser test (ucm 3.5.0 installed there)
16
+
17
+ - jg-ferien, event announcement conversation, 375 px: next to four icon buttons (attach, emoji, poll,
18
+ announce) the textarea is **16 px** wide (its field 44 px), "Senden" is clipped (44 px), the row
19
+ overflows (315 of 295 px).
20
+ - Component: `src/messaging/Composer.jsx` (row layout incl. the four icon buttons,
21
+ `allowAnnouncement` prop); `src/messaging/MessageBubble.jsx` shows a bare "0/0" next to the time.
22
+ - Long words and URLs wrap correctly inside the bubble; URLs are plain text.
23
+ - The dcm register is canonical for `MSG-*`; this is the ucm-side row.
24
+
25
+ ## Scope + non-goals
26
+
27
+ - In scope: composer layout at narrow widths (e.g. actions collapse into one menu, or wrap to their
28
+ own row), linkified URLs, an explained or hidden read marker.
29
+ - Explicit non-goals / do-not-touch:
30
+ - No change to message sending, polls or announcements behaviour.
31
+
32
+ ## Tier · gates
33
+
34
+ - **Tier 3** — shared-core component.
35
+ - Reviewer: `reviewer` (all lenses; linkify is new logic) · `ui_reviewer` · `sec_reviewer` (linkify
36
+ must not create script or `javascript:` links).
37
+ - **Done means released and jg-ferien's pin bumped.**
38
+
39
+ ## Risks
40
+
41
+ - Linkify is an injection surface: only `http(s)` URLs, `rel="noopener noreferrer"`, no HTML from
42
+ message text.
43
+
44
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the orchestrator's run is the gate)
45
+
46
+ 1. At 375 px width the textarea has at least a defined minimum width and the send button is not
47
+ clipped, with all four actions enabled.
48
+ 2. `https://…` in a message renders as a link; `javascript:…` does not.
49
+ 3. The read marker has an accessible label or is absent.
50
+
51
+ ---
52
+
53
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
54
+
55
+ ## Context package — three independent fixes, same file family
56
+
57
+ ### 1. Composer overflow at narrow widths — `src/messaging/Composer.jsx`
58
+
59
+ - The single overflowing row is at line 139:
60
+ `<Stack direction="row" spacing={1} alignItems="flex-end">` — it holds, in order: the hidden file
61
+ `<input>`, 4 `IconButton`s (attach `:141`, emoji `:142`, poll `:143`, announce `:144` conditional
62
+ on `canAnnounce`), the `TextField` (`:145`, `fullWidth multiline minRows={2}`), and the Send
63
+ `Button` (`:146`).
64
+ - **Fix: split into two stacked rows, not one collapsing menu** (the WO's "or wrap to their own
65
+ row" option — simplest, lowest-risk, no new interaction pattern to test):
66
+ 1. First row: a `<Stack direction="row" spacing={1}>` containing only the icon buttons (attach,
67
+ emoji, poll, and the conditional announce button) — 3–4 icon buttons at ~40px each comfortably
68
+ fit 375px on their own.
69
+ 2. Second row: `<Stack direction="row" spacing={1} alignItems="flex-end">` containing only the
70
+ `TextField` and the Send `Button`.
71
+ 3. Give the `TextField` `sx={{ flex: 1, minWidth: 120 }}` (so it never collapses to near-zero
72
+ width when squeezed) and the Send `Button` `sx={{ flexShrink: 0 }}` (so it is never the thing
73
+ that gets clipped — it should shrink last, if at all).
74
+ 4. Everything else in the file (state, handlers, the poll/announcement dialogs, the emoji
75
+ popover) is unchanged — this is a pure JSX layout restructure of one `Stack`.
76
+ - Test file: extend `tests/messagingComposer.test.jsx` (existing — read it first for the render
77
+ harness/mock-provider pattern already in use, follow it exactly rather than inventing a new one).
78
+ Add: render the `Composer`, resize the test's container is not meaningful in jsdom (no real
79
+ layout), so instead assert via computed style: the `TextField`'s wrapping element has
80
+ `minWidth: '120px'` (or your chosen value) and the Send button's element has `flexShrink: '0'` —
81
+ both readable via `window.getComputedStyle`, same technique as
82
+ `tests/StatTile.test.jsx`'s `accent` test. Also assert all four action buttons
83
+ (`MessagingComposer.ADD_ATTACHMENT`/`ADD_EMOJI`/`MessagingPoll.CREATE`/
84
+ `MessagingAnnouncement.CREATE` — pass `allowAnnouncement` + a non-`direct` `conversation` prop to
85
+ exercise the fourth) are present and not `disabled` (aside from the poll button's existing
86
+ `pollOpen || files.some(isImage)` guard, unrelated to this WO).
87
+
88
+ ### 2. Linkify `http(s)` URLs — new file `src/messaging/linkifyText.jsx`
89
+
90
+ - No linkify library exists in this repo's dependencies today (checked — do not add one; a
91
+ dependency addition needs separate approval this WO does not have, and the requirement is narrow
92
+ enough not to need one).
93
+ - Export a pure function `linkifyText(text)` returning an array of strings and `<a>` React elements
94
+ (a `key`ed fragment-safe array, the same shape `.map()` output must have). URL matching: a strict
95
+ **`http://` or `https://` prefix only** — e.g.
96
+ `/(https?:\/\/[^\s<>"']+)/g` — so a `javascript:`, `data:`, or any other scheme can never match
97
+ the pattern in the first place (that is the actual security property the sec_reviewer will check
98
+ for — not an escaping step, an inclusion-list pattern that structurally cannot match anything
99
+ else). Split `text` on that pattern; for each URL match render
100
+ `<a href={url} target="_blank" rel="noopener noreferrer">{url}</a>`; non-matching segments pass
101
+ through as plain strings (React renders an array of strings/elements fine as children).
102
+ - Trim likely trailing punctuation that is prose, not part of the URL (a trailing `.`, `,`, `)`,
103
+ `!`, `?` immediately followed by a word boundary or end of string) so `"see https://x.com."` does
104
+ not link the sentence-ending period — check `tests/` for whether any existing kit code already
105
+ does this trimming (grep `messagingContractConformance` / `messagingApi` tests for a URL-trimming
106
+ precedent) before inventing your own rule; if none exists, a simple trailing-punctuation strip is
107
+ sufficient, name it in your `PROGRESS` narration.
108
+ - Wire it into `src/messaging/MessageBubble.jsx:149` — the `<Typography sx={{ whiteSpace:
109
+ 'pre-wrap', overflowWrap: 'anywhere' }}>{...}</Typography>` that renders the message body: replace
110
+ the raw string child with `linkifyText(<same expression>)` for the non-deleted, non-empty case
111
+ (deleted messages keep rendering `t('MessagingThread.DELETED')` as plain text, unlinkified).
112
+ - Test file: new `tests/messagingLinkify.test.jsx` (or extend an existing `MessageBubble` test if
113
+ one already covers body rendering — check `tests/messagingChunk6.test.jsx` and
114
+ `tests/messagingCompactBubble.test.jsx` first).
115
+ 1. A message body containing `https://example.com/path` renders an `<a>` with that exact `href`,
116
+ `target="_blank"`, and `rel="noopener noreferrer"`.
117
+ 2. A message body containing the literal text `javascript:alert(1)` renders it as plain text —
118
+ assert `screen.queryByRole('link')` is `null` and the literal string is present as text.
119
+ 3. A message body containing both a URL and surrounding prose renders the prose as plain text
120
+ and only the URL portion as a link (i.e. the split doesn't eat the surrounding words).
121
+
122
+ ### 3. Read marker explains itself — `src/messaging/ReadTicks.jsx`
123
+
124
+ - Both existing branches already set `aria-label` (line 74's `<span role="img">`/`<ButtonBase>` for
125
+ the counted case, line 80's `<StatusIcon role="img">` for the DM case) — that covers screen
126
+ readers, but `aria-label` is invisible to a SIGHTED user looking at a bare "0/0", which is the
127
+ Envelope's actual complaint ("explains itself (**label or tooltip**)" — the current label is
128
+ ARIA-only, not a visible one).
129
+ - Fix, at the `hasCounts` branch (line 56 onward): wrap the returned ratio element in MUI's
130
+ `Tooltip` (already imported patterns exist elsewhere in the kit, e.g. `UserListComponent.jsx`'s
131
+ `<Tooltip title={...}>`) using the same `label` text as the `title`, so hovering explains it
132
+ visually too. Import `Tooltip` from `@mui/material` at the top of the file (currently only
133
+ imports `List, ListItem, Popover, Typography, ButtonBase`).
134
+ - Additionally: when `status.recipient_count === 0` (a message with literally nobody to read it —
135
+ "0/0" carries no information per the Envelope's alternative: "or is not shown where it carries no
136
+ information"), return `null` before rendering the ratio, same early-return style already used at
137
+ line 41 (`if (!status) return null;`) and line 50 (`if (!hasCounts && conversation?.kind !==
138
+ 'direct') return null;`).
139
+ - Do NOT change the DM branch (line 78–80) — it already carries a real icon-shape + colour +
140
+ aria-label signal (not a bare number), out of this fix's actual complaint.
141
+ - Test file: extend `tests/messagingReadTicks*.test.*` if one exists (grep `tests/` for
142
+ `ReadTicks`); otherwise create `tests/messagingReadTicksTooltip.test.jsx`.
143
+ 1. With `read_count: 2, recipient_count: 5`, assert a `title` attribute (MUI `Tooltip` renders
144
+ one on the wrapped element before hover, or use `@testing-library/user-event` to hover and
145
+ assert the tooltip text appears — follow whatever pattern this repo's other `Tooltip` tests
146
+ already use, grep for one) explaining the ratio is present.
147
+ 2. With `recipient_count: 0`, assert the component renders nothing (e.g. `container.textContent`
148
+ is empty, or the specific test id/role used elsewhere for "renders null" assertions in this
149
+ test suite).
150
+
151
+ ## Do-not-touch / invariants
152
+
153
+ - No change to message sending, polls, or announcements behaviour (Envelope non-goal) — items 1–3
154
+ above are the complete scope; do not touch `submit`/`submitPoll`/`submitAnnouncement`.
155
+ - `sec_reviewer` will specifically check the linkify regex cannot match a non-`http(s)` scheme —
156
+ keep the pattern an explicit `https?://` prefix, never a generic "looks like a URL" heuristic.
157
+
158
+ ## Target repo working directory (absolute)
159
+
160
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`)
161
+
162
+ ## Preamble — a REQUIRED block IN this file, not something appended at invocation
163
+
164
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
165
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
166
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
167
+ > unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the register
168
+ > row and the review verdicts are the orchestrator's alone.** **Your tools are for editing source
169
+ > and test files and for running the tests you wrote — nothing else.** Do NOT install dependencies,
170
+ > touch a lockfile, run a package manager, or tidy up stray files; if something in the repo state
171
+ > blocks you, stop and report it as `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT
172
+ > `git add`/`commit`/`push` — leave every
173
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
174
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
175
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
176
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
177
+ > those are the gate; your own run does not count as the gate.
178
+ >
179
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
180
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
181
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
182
+ > `RESULT: DONE|BLOCKED <reason>`.
183
+
184
+ ---
185
+
186
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
187
+
188
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
189
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
190
+ > reviewers, no verification run, no register edit, no commit.**
191
+
192
+ *(to be filled by the Orchestrator: execution directive; `reviewer` (all lenses) + `ui_reviewer` + `sec_reviewer`; release; jg-ferien pin bump and 375 px check in an event conversation; register rows here, in dcm (MSG canonical) and in jg (`JG-UX-14`).)*
@@ -0,0 +1,114 @@
1
+ # UCM-PRIM-2 — A shared empty-state primitive
2
+
3
+ # A. Envelope — authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ - Goal: every "nothing here yet" in a consuming app looks and reads the same.
8
+ - Expected outcome: `EmptyState` in the kit — an optional icon, one line of text, an optional secondary
9
+ line and an optional action — built from theme tokens, accessible (text reachable by screen
10
+ readers, action a real button).
11
+
12
+ ## Context — measured 2026-09-29 in jg-ferien's local browser test (ucm 3.5.0 installed there)
13
+
14
+ - jg-ferien's 15 event sections show "empty" in **seven** ways (plain text, card, blue info alert,
15
+ card with title, filled green card, alert inside a green card, nothing at all). No shared component
16
+ exists in jg or ucm; jg has only a song-search special case.
17
+ - **Operator decision 2026-09-29: generic goes to ucm.** jg adopts it in `JG-UX-9`.
18
+ - `PRIM-1` promoted `StatTile`/`SoftChip` along the same lines.
19
+
20
+ ## Scope + non-goals
21
+
22
+ - In scope: the component, its export, a story or docs entry, tests.
23
+ - Explicit non-goals / do-not-touch: no adoption in consumers here; no change to `Alert` styling.
24
+
25
+ ## Tier · gates
26
+
27
+ - **Tier 3** — new public export in shared-core (additive).
28
+ - Reviewer: `reviewer` (all lenses) · `ui_reviewer`.
29
+ - **Done means released; jg adoption is `JG-UX-9`.**
30
+
31
+ ## Risks
32
+
33
+ - A too-flexible API reintroduces the seven variants; keep the props minimal.
34
+
35
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the orchestrator's run is the gate)
36
+
37
+ 1. Renders text only; with secondary line; with action (button with accessible name).
38
+ 2. Uses theme tokens (no literal colours; the kit's literal guard passes).
39
+
40
+ ---
41
+
42
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
43
+
44
+ ## Context package
45
+
46
+ - New file `src/components/EmptyState.jsx`. Model it on the existing primitives
47
+ `src/components/StatTile.jsx` and `src/components/SoftChip.jsx` — a small functional component,
48
+ default export, built from `Box`/`Typography`/`Button` (MUI), theme tokens only (no literal
49
+ colours — `reportOffPaletteColours` in `src/theme/themeCompleteness.js` scans every source file
50
+ for off-palette hex/rgb/named colours and is part of the kit's literal guard the Required Tests
51
+ reference).
52
+ - Props (keep minimal per the Risk note — do not over-generalise):
53
+ `icon` (optional, a rendered MUI icon element, e.g. `<InboxOutlinedIcon />`), `title` (string,
54
+ required — the one line of text), `description` (optional secondary line), `action` (optional
55
+ `{ label, onClick }` — rendered as a real `<Button variant="outlined">`, not a link, so it is
56
+ reachable and operable like `StatTile`'s children slot), `sx` (optional passthrough, MUI
57
+ convention used throughout the kit, e.g. `Composer.jsx`'s `COMPOSER_EMOJI_BUTTON_SX` pattern).
58
+ - Export it from `src/index.js` next to the other components section (`--- 5. Components ---`,
59
+ after `SoftChip` at line 65): `export { default as EmptyState } from './components/EmptyState';`.
60
+ - Accessibility: `title` must be real text content (not just an `aria-label`), so a screen reader
61
+ reads it as part of document flow; if `icon` is passed, it must not need its own label (decorative
62
+ — rely on the adjacent text, matching how `StatTile` treats its icon-less design).
63
+ - Test file: `tests/EmptyState.test.jsx`, following `tests/StatTile.test.jsx`'s pattern exactly —
64
+ `@vitest-environment jsdom`, wrap in `<ThemeProvider theme={createAppTheme(...)}>`, `cleanup()`
65
+ in `afterEach`.
66
+ - Render text-only, with `description`, with `action` (assert the button has an accessible name
67
+ matching the label and that clicking it calls the passed `onClick`).
68
+ - The "theme tokens only" requirement: either call `reportOffPaletteColours([{ path: 'src/components/EmptyState.jsx', source: fs.readFileSync(...) }])` (see how `themeCompleteness.js` self-tests, or grep existing callers of `reportOffPaletteColours` in `tests/` for the established pattern) and assert `findings` is empty, or (simpler, sufficient) just don't use any hex/rgb/named-colour literal in the component at all and note in a comment that the guard is structural, not a runtime assertion in this test. Prefer calling `reportOffPaletteColours` directly if an existing test already does — grep `tests/*.test.*` for `reportOffPaletteColours` to find the precedent and match its call shape exactly.
69
+
70
+ ## Do-not-touch / invariants
71
+
72
+ - Do not add `EmptyState` to `kitSxRegistry.js` unless you export a top-level `sx` object constant
73
+ from the component (only needed if the root element gets kit-owned baked-in styling beyond plain
74
+ `sx` passthrough — for this minimal component you likely don't need one; only add a registry entry
75
+ if you do add such a constant).
76
+ - No adoption anywhere in this repo's own consumers (`AccountPage`, etc.) — out of scope per the
77
+ Envelope's non-goals.
78
+ - Do not touch `Alert` styling anywhere.
79
+
80
+ ## Target repo working directory (absolute)
81
+
82
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`)
83
+
84
+ ## Preamble — a REQUIRED block IN this file, not something appended at invocation
85
+
86
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
87
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
88
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
89
+ > unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the register
90
+ > row and the review verdicts are the orchestrator's alone.** **Your tools are for editing source
91
+ > and test files and for running the tests you wrote — nothing else.** Do NOT install dependencies,
92
+ > touch a lockfile, run a package manager, or tidy up stray files; if something in the repo state
93
+ > blocks you, stop and report it as `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT
94
+ > `git add`/`commit`/`push` — leave every
95
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
96
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
97
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
98
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
99
+ > those are the gate; your own run does not count as the gate.
100
+ >
101
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
102
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
103
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
104
+ > `RESULT: DONE|BLOCKED <reason>`.
105
+
106
+ ---
107
+
108
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
109
+
110
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
111
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
112
+ > reviewers, no verification run, no register edit, no commit.**
113
+
114
+ *(to be filled by the Orchestrator: execution directive; `reviewer` (all lenses) + `ui_reviewer`; release; register row here and cross-reference in jg (`JG-UX-9`).)*