@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.
- package/CHANGELOG.md +7 -0
- package/DESIGN.md +9 -1
- package/README.md +2 -0
- package/WORK_ORDERS.md +8 -0
- package/dist/auth/UserMenu.js +23 -1
- package/dist/components/EmptyState.js +17 -0
- package/dist/components/UserListComponent.js +2 -1
- package/dist/i18n/authTranslations.js +196 -0
- package/dist/i18n/uiCoreTranslations.js +19 -0
- package/dist/index.js +2 -1
- package/dist/messaging/Composer.js +4 -2
- package/dist/messaging/MessageBubble.js +2 -1
- package/dist/messaging/ReadTicks.js +5 -3
- package/dist/messaging/linkifyText.js +39 -0
- package/dist/pages/PasswordInvitePage.js +23 -8
- package/dist/pages/SignUpPage.js +4 -4
- package/dist/theme/createAppTheme.js +62 -9
- package/dist/theme/kitSxRegistry.js +5 -1
- package/dist/theme/tokens.js +1 -1
- package/package.json +1 -1
- package/src/auth/UserMenu.jsx +26 -1
- package/src/components/EmptyState.jsx +38 -0
- package/src/components/UserListComponent.jsx +2 -0
- package/src/i18n/authTranslations.ts +197 -0
- package/src/i18n/uiCoreTranslations.js +10 -0
- package/src/index.js +2 -1
- package/src/messaging/Composer.jsx +7 -3
- package/src/messaging/MessageBubble.jsx +2 -1
- package/src/messaging/ReadTicks.jsx +4 -3
- package/src/messaging/linkifyText.jsx +37 -0
- package/src/pages/PasswordInvitePage.jsx +36 -7
- package/src/pages/SignUpPage.jsx +79 -68
- package/src/theme/createAppTheme.js +68 -4
- package/src/theme/kitSxRegistry.js +9 -1
- package/src/theme/tokens.js +1 -1
- package/tests/EmptyState.test.jsx +62 -0
- package/tests/PasswordInvitePage.test.jsx +135 -0
- package/tests/SignUpPage.test.jsx +15 -2
- package/tests/UserListComponent.test.jsx +85 -0
- package/tests/UserMenu.test.jsx +40 -4
- package/tests/createAppThemePageContrast.test.js +108 -0
- package/tests/i18nGermanVariant.test.js +65 -0
- package/tests/messagingComposer.test.jsx +25 -0
- package/tests/messagingLinkify.test.jsx +51 -0
- package/tests/messagingReadTicksTooltip.test.jsx +36 -0
- package/tests/userScreenTranslations.test.jsx +165 -0
- package/work-orders/UCM-AUTH-9.md +207 -0
- package/work-orders/UCM-I18N-4.md +226 -0
- package/work-orders/UCM-MSG-20.md +192 -0
- package/work-orders/UCM-PRIM-2.md +114 -0
- package/work-orders/UCM-SHELL-7.md +134 -0
- package/work-orders/UCM-THEME-13.md +69 -0
- package/work-orders/UCM-THEME-14.md +142 -0
- 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`).)*
|