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