@micha.bigler/ui-core-micha 3.6.0 → 3.8.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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/DESIGN.md +9 -1
  3. package/README.md +2 -0
  4. package/WORK_ORDERS.md +8 -0
  5. package/dist/auth/UserMenu.js +23 -1
  6. package/dist/components/EmptyState.js +17 -0
  7. package/dist/components/UserListComponent.js +2 -1
  8. package/dist/i18n/authTranslations.js +196 -0
  9. package/dist/i18n/uiCoreTranslations.js +19 -0
  10. package/dist/index.js +2 -1
  11. package/dist/messaging/Composer.js +4 -2
  12. package/dist/messaging/MessageBubble.js +2 -1
  13. package/dist/messaging/ReadTicks.js +5 -3
  14. package/dist/messaging/linkifyText.js +39 -0
  15. package/dist/pages/PasswordInvitePage.js +23 -8
  16. package/dist/pages/SignUpPage.js +4 -4
  17. package/dist/theme/createAppTheme.js +62 -9
  18. package/dist/theme/kitSxRegistry.js +5 -1
  19. package/dist/theme/tokens.js +1 -1
  20. package/package.json +1 -1
  21. package/src/auth/UserMenu.jsx +26 -1
  22. package/src/components/EmptyState.jsx +38 -0
  23. package/src/components/UserListComponent.jsx +2 -0
  24. package/src/i18n/authTranslations.ts +197 -0
  25. package/src/i18n/uiCoreTranslations.js +10 -0
  26. package/src/index.js +2 -1
  27. package/src/messaging/Composer.jsx +7 -3
  28. package/src/messaging/MessageBubble.jsx +2 -1
  29. package/src/messaging/ReadTicks.jsx +4 -3
  30. package/src/messaging/linkifyText.jsx +37 -0
  31. package/src/pages/PasswordInvitePage.jsx +36 -7
  32. package/src/pages/SignUpPage.jsx +79 -68
  33. package/src/theme/createAppTheme.js +68 -4
  34. package/src/theme/kitSxRegistry.js +9 -1
  35. package/src/theme/tokens.js +1 -1
  36. package/tests/EmptyState.test.jsx +62 -0
  37. package/tests/PasswordInvitePage.test.jsx +135 -0
  38. package/tests/SignUpPage.test.jsx +15 -2
  39. package/tests/UserListComponent.test.jsx +85 -0
  40. package/tests/UserMenu.test.jsx +40 -4
  41. package/tests/createAppThemePageContrast.test.js +108 -0
  42. package/tests/i18nGermanVariant.test.js +65 -0
  43. package/tests/messagingComposer.test.jsx +25 -0
  44. package/tests/messagingLinkify.test.jsx +51 -0
  45. package/tests/messagingReadTicksTooltip.test.jsx +36 -0
  46. package/tests/userScreenTranslations.test.jsx +165 -0
  47. package/work-orders/UCM-AUTH-9.md +207 -0
  48. package/work-orders/UCM-I18N-4.md +226 -0
  49. package/work-orders/UCM-MSG-20.md +192 -0
  50. package/work-orders/UCM-PRIM-2.md +114 -0
  51. package/work-orders/UCM-SHELL-7.md +134 -0
  52. package/work-orders/UCM-THEME-13.md +69 -0
  53. package/work-orders/UCM-THEME-14.md +142 -0
  54. package/work-orders/UCM-THEME-15.md +356 -0
@@ -0,0 +1,207 @@
1
+ # UCM-AUTH-9 — Auth flows that dead-end: a rejected password removes the invite form, a closed signup looks open
2
+
3
+ # A. Envelope — authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ - Goal: every auth page leaves the person a way forward and says why something failed.
8
+ - Expected outcome:
9
+ - Invite/reset page: a server-side rejection of the password keeps the form, shows the reason
10
+ (e.g. "too common") at the field, and lets the person retry. After success the person sees a
11
+ confirmation, not a silent jump to the login page.
12
+ - Signup page: when self-registration is off (`signup: false`, `signup_modes: []`), the page says
13
+ so instead of rendering a form whose submit can never enable.
14
+ - User list: a role change confirms that it was saved, or shows why not.
15
+
16
+ ## Context — measured 2026-09-29 in jg-ferien's local browser test (ucm 3.5.0 installed there)
17
+
18
+ - `src/pages/PasswordInvitePage.jsx`: the form renders only when `!successKey && !errorKey`
19
+ (~l.142); any error — including the server's `Auth.RESET_PASSWORD_INVALID` with
20
+ `messages: ["This password is too common."]` — removes both password fields and the button;
21
+ only a reload recovers. The reason is not shown. Client-side validation keeps the form.
22
+ - After a successful set the page navigates to login (~l.102) with no confirmation.
23
+ - `src/pages/SignUpPage.jsx`: submit is `disabled: submitting || signupModes.length === 0 ||
24
+ (turnstileRequired && !turnstileToken)`; with no signup modes the page still shows e-mail and
25
+ access-code fields and a permanently grey button, no explanation.
26
+ - `src/components/UserListComponent.jsx`: role selects save without any feedback.
27
+
28
+ ## Scope + non-goals
29
+
30
+ - In scope: the three behaviours above.
31
+ - Explicit non-goals / do-not-touch:
32
+ - Password rules and the backend responses (dcm) stay as they are.
33
+ - Strings and the formal/informal choice are `UCM-I18N-4`.
34
+
35
+ ## Tier · gates
36
+
37
+ - **Tier 3** — shared-core auth screens.
38
+ - Reviewer: `reviewer` (all lenses) · `sec_reviewer` (auth flow) · `ui_reviewer`.
39
+ - **Done means released and jg-ferien's pin bumped.**
40
+
41
+ ## Risks
42
+
43
+ - An invalid/expired token must still end in the "link invalid" state without a form — only a
44
+ password rejection keeps the form. The two error classes must be told apart by code, not by text.
45
+
46
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the orchestrator's run is the gate)
47
+
48
+ 1. Server rejects the password → form still present, reason shown, retry possible.
49
+ 2. Invalid token → invalid-link state, no form.
50
+ 3. Success → confirmation visible.
51
+ 4. Signup with no modes → explanatory message, no form.
52
+ 5. Role change → success or error feedback.
53
+
54
+ ---
55
+
56
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
57
+
58
+ ## Context package — three independent fixes
59
+
60
+ ### 1. `src/pages/PasswordInvitePage.jsx` — the dead-end form + silent navigate
61
+
62
+ - **Root cause of both bugs: ONE shared `errorKey` state variable covers two structurally
63
+ different failures**, and the render condition `{!successKey && !errorKey && <PasswordSetForm
64
+ .../>}` (line 142) hides the form for EITHER. The link-validity check (`verifyResetToken`, the
65
+ `useEffect` at line 36) and the password submission (`setNewPassword`, `handleSubmit` at line 56)
66
+ are two separate API calls that already fail independently — the fix is to stop collapsing their
67
+ results into one state variable, not to inspect error codes at render time.
68
+ - **Fix — split the state:**
69
+ 1. Keep `errorKey` (rename mentally to "link check result") for the `useEffect` at line 36–54
70
+ only — this is the existing "invalid/expired link" path and must keep hiding the form (the
71
+ Risk note: "an invalid/expired token must still end in the 'link invalid' state without a
72
+ form"). No change needed to this effect's logic.
73
+ 2. Add a new `submitErrorKey` state, set ONLY inside `handleSubmit`'s `catch` block (line
74
+ 103–105) instead of reusing `errorKey`. Read the human-readable reason from the response body
75
+ the backend actually sends: `normaliseApiError` (see `src/utils/auth-errors.js`) puts the
76
+ whole response body on `err.raw`; a password-policy rejection carries
77
+ `err.raw?.messages` (an array of strings, e.g. `["This password is too common."]"`) alongside
78
+ `err.code`. Prefer joining `err.raw.messages` when it's a non-empty array; otherwise fall back
79
+ to `t(err.code || 'Auth.RESET_PASSWORD_FAILED')`, same as today.
80
+ 3. Change the render condition so the form shows whenever the LINK itself is valid, regardless
81
+ of `submitErrorKey`: `{!errorKey && !successKey && <PasswordSetForm .../>}` (drop the old
82
+ shared `errorKey` check that also hid it on submit errors — `errorKey` from here on only ever
83
+ means "link invalid", never "password rejected"). Display `submitErrorKey` in its own
84
+ `<Typography color="error">` above or below the form, distinct from the existing `errorKey`
85
+ alert.
86
+ 4. Pass `submitErrorKey` down or keep it page-local and render it directly — `PasswordSetForm`
87
+ itself does not need to change (it already renders its OWN `localErrorKey` for client-side
88
+ validation; the server-rejection message is a page-level concern rendered alongside the form,
89
+ not inside `PasswordSetForm`).
90
+ - **Fix — success confirmation instead of a silent jump:** in `handleSubmit`'s try block (line
91
+ 66–102), the code currently computes `target` and calls `navigate(target)` immediately after
92
+ `setSuccessKey(...)` — the navigation happens before the success message can ever be seen. Stop
93
+ calling `navigate` automatically. Instead:
94
+ 1. Keep the existing `successKey` set exactly as today (line 68–72) and the existing
95
+ `safeNextPath`/`target` computation (line 73–101) — that same-origin validation logic is
96
+ correct and must not change.
97
+ 2. Store the computed `target` in a new state variable (e.g. `continueTarget`) instead of
98
+ navigating with it immediately.
99
+ 3. Where `{successKey && <Typography color="primary" ...>}` already renders (line 136–140), add
100
+ a `<Button variant="contained" onClick={() => navigate(continueTarget)}>` reading
101
+ `t('Auth.SIGNUP_GO_TO_LOGIN')` (existing key, reused — this button always leads to the login
102
+ page, same destination the automatic navigate used to go to) right below the confirmation
103
+ text. The person now sees the confirmation and clicks through themselves.
104
+ - Test file: extend whatever test already covers `PasswordInvitePage` (grep `tests/` for
105
+ `PasswordInvitePage` — if none exists, create `tests/PasswordInvitePage.test.jsx` following
106
+ `tests/AccountPage.test.jsx`'s page-level render pattern: mock `../src/auth/authApi`'s
107
+ `verifyResetToken`/`setNewPassword` with `vi.mock`, wrap in a router since the page uses
108
+ `useNavigate`/`useParams`/`useLocation`).
109
+ 1. `verifyResetToken` resolves (valid link), `setNewPassword` rejects with
110
+ `{ response: { data: { code: 'Auth.RESET_PASSWORD_INVALID', messages: ['This password is too
111
+ common.'] } } }` (the shape `normaliseApiError` reads) → after submit, the `PasswordSetForm`
112
+ (its password fields) is still present, "This password is too common." is visible, and the
113
+ user can submit again.
114
+ 2. `verifyResetToken` rejects → the invalid-link message is shown and `PasswordSetForm` is
115
+ ABSENT (`screen.queryByLabelText`/`queryByRole` for a password field returns null).
116
+ 3. `verifyResetToken` resolves, `setNewPassword` resolves → after submit, the success message is
117
+ visible AND a "go to login" button is visible (and the test can assert `navigate` was NOT
118
+ called automatically — only after clicking the button, if you choose to test the click too).
119
+
120
+ ### 2. `src/pages/SignUpPage.jsx` — signup closed but form still renders
121
+
122
+ - `signupModes` (line 51–59) is already computed as an empty array when self-signup is off. The
123
+ page currently still renders the mode-picker (only if `signupModes.length > 1`, so with zero
124
+ modes that block is already skipped) AND the full email/access-code form regardless, with the
125
+ submit button just permanently disabled (line 234: `disabled={submitting || signupModes.length
126
+ === 0 || ...}`) and no explanation anywhere.
127
+ - **Fix:** immediately after the `NarrowPage`'s `<Helmet>` block (after line 165), add:
128
+ `{signupModes.length === 0 ? (<Alert severity="info" sx={SIGN_UP_PAGE_ALERT_SX}>{t('Auth.SIGNUP_CLOSED', 'Sign-up is currently closed for this project.')}</Alert>) : (<>...the existing success/error
129
+ alerts + mode-picker + form + "already have an account" block, unchanged...</>)}` — i.e. wrap
130
+ everything from the existing `successKey` alert (line 166) through the "already have an account"
131
+ block (line 254) in this conditional, so a zero-modes project shows ONLY the explanatory message,
132
+ no form at all. New translation key `Auth.SIGNUP_CLOSED` needs an entry in `authTranslations.ts`
133
+ (all locales the file already carries: de/fr/en/sw) — do not leave it English-only via the
134
+ `t(key, fallback)` default-only pattern, add the real bundle entry.
135
+ - Test file: extend whatever exists for `SignUpPage` (grep `tests/` for `SignUpPage`).
136
+ 1. `authMethods.signup_modes = []` and `authMethods.signup = false` → the explanatory message is
137
+ shown, no email/access-code `TextField` and no submit button are present.
138
+ 2. Existing signup-open behaviour (at least one mode) is unchanged — re-run/extend whatever
139
+ assertions already cover the working case so this fix can't be a silent regression.
140
+
141
+ ### 3. `src/components/UserListComponent.jsx` — role change has no feedback
142
+
143
+ - `handleChangeRole` (line 106–124) already has a `catch` that shows `alert(...)` on failure — that
144
+ half already satisfies "or shows why not". The missing half is SUCCESS feedback: today a
145
+ successful role change just silently re-fetches (`await loadUsers()`, line 120) with nothing
146
+ telling the person it worked.
147
+ - **Fix:** add a lightweight success signal after the `await loadUsers()` call inside the `try`
148
+ block (line 106–120) — the simplest, lowest-risk option consistent with this component's existing
149
+ style (it already uses the browser's own `alert()`/`window.confirm()` for feedback elsewhere in
150
+ this same file, e.g. line 93, 102, 122 — match that existing convention rather than introducing a
151
+ new Snackbar/Toast pattern this component doesn't otherwise use):
152
+ `alert(t('UserList.ROLE_UPDATE_SUCCESS', 'Role updated.'));` right after `await loadUsers();`.
153
+ New key `UserList.ROLE_UPDATE_SUCCESS` needs a real bundle entry (all locales) in
154
+ `authTranslations.ts` — this key is new to `I18N-4`'s hardcoded-English-string list too; if
155
+ `I18N-4` lands first in the register it may already exist, check before adding a duplicate.
156
+ - Test file: extend the existing `UserListComponent` test (grep `tests/` for
157
+ `UserListComponent`) — assert that a successful `onChangeRole`/`updateUserRole` call triggers the
158
+ new success signal (mock `window.alert` with `vi.spyOn(window, 'alert')` and assert it was called
159
+ with the translated success text), and that the existing failure path is unaffected (still calls
160
+ `alert` with the error path, exactly as today — do not weaken that assertion).
161
+
162
+ ## Do-not-touch / invariants
163
+
164
+ - Password rules and the backend responses (dcm) stay as they are — this WO only changes how the
165
+ frontend reads/displays what the backend already returns.
166
+ - Strings and the formal/informal address choice belong to `UCM-I18N-4`, not here — new keys added
167
+ here (`Auth.SIGNUP_CLOSED`, `UserList.ROLE_UPDATE_SUCCESS`) get plain, correct de/fr/en/sw values,
168
+ not the informal-German variant mechanism (that mechanism is `I18N-4`'s own scope).
169
+ - Do not touch `SignupConfirmPage.jsx` — out of scope (not named in the Envelope's Context), even
170
+ though it has a structurally similar immediate-navigate-after-success pattern; note it in the
171
+ register Notiz as a possible follow-up, do not fix it here.
172
+
173
+ ## Target repo working directory (absolute)
174
+
175
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`)
176
+
177
+ ## Preamble — a REQUIRED block IN this file, not something appended at invocation
178
+
179
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
180
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
181
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
182
+ > unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the register
183
+ > row and the review verdicts are the orchestrator's alone.** **Your tools are for editing source
184
+ > and test files and for running the tests you wrote — nothing else.** Do NOT install dependencies,
185
+ > touch a lockfile, run a package manager, or tidy up stray files; if something in the repo state
186
+ > blocks you, stop and report it as `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT
187
+ > `git add`/`commit`/`push` — leave every
188
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
189
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
190
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
191
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
192
+ > those are the gate; your own run does not count as the gate.
193
+ >
194
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
195
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
196
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
197
+ > `RESULT: DONE|BLOCKED <reason>`.
198
+
199
+ ---
200
+
201
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
202
+
203
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
204
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
205
+ > reviewers, no verification run, no register edit, no commit.**
206
+
207
+ *(to be filled by the Orchestrator: execution directive; `reviewer` (all lenses) + `sec_reviewer` + `ui_reviewer`; release; jg-ferien pin bump; register rows here and in jg (`JG-UX-19`).)*
@@ -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`).)*