@micha.bigler/ui-core-micha 3.11.2 → 3.13.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 +10 -0
- package/README.md +6 -0
- package/WORK_ORDERS.md +5 -1
- package/dist/pages/AccountPage.js +2 -2
- package/dist/theme/createAppTheme.js +18 -3
- package/dist/theme/tokens.js +11 -7
- package/docs/TYPE-SCALE.md +111 -0
- package/package.json +1 -1
- package/src/pages/AccountPage.jsx +2 -0
- package/src/theme/createAppTheme.js +7 -0
- package/src/theme/tokens.js +12 -7
- package/tests/AccountPage.profileStatements.test.jsx +162 -0
- package/tests/createAppThemeTypography.test.js +53 -0
- package/tests/fixtures/light-theme-baseline.json +18 -12
- package/work-orders/UCM-AUTH-12.md +98 -0
- package/work-orders/UCM-THEME-17.md +111 -0
- package/work-orders/UCM-THEME-18.md +184 -0
- package/work-orders/assets/UCM-THEME-17-type-scale.html +391 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.13.0 - UCM-AUTH-12
|
|
4
|
+
|
|
5
|
+
`AccountPage` accepts `showProfileStatements` (default `true`). Set it to `false` in consumers
|
|
6
|
+
without privacy/cookie statement endpoints to suppress the statement panels and their requests.
|
|
7
|
+
Both existing consent controls and their submitted values remain available.
|
|
8
|
+
|
|
9
|
+
## 3.12.0 - UCM-THEME-18
|
|
10
|
+
|
|
11
|
+
The shared type scale now uses h1–h5 sizes of 36/32/28/24/20px, h4 steps to 23px below `sm`, and subtitle1 is 13px; every adopted app changes on its next bump, with hram, jg-ferien, kerzenziehen, spesix, and fitness-monitor moving screen titles from h5 to h4, while survey_app should remove its 800/700 weights and h4 `2.125rem`/400 override and decide its h3 page titles (24px to 28px), closing the held `SVA-THEME-1` typography revert. An app h4 `fontSize` override retains the baseline’s 23px mobile step (survey_app currently sets `2.125rem`); heading weights are fixed at 600, so apps should not override them per level. All other adopted apps need only bump; webshop-guenter, Kira, Cinevia, and Gustav are unaffected.
|
|
12
|
+
|
|
3
13
|
## 3.11.2 — UCM-THEME-16
|
|
4
14
|
|
|
5
15
|
The "Signup Access / valid until" box in `QrSignupManager` now takes `background.subtle` instead of the fixed light `grey.50`,
|
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@ To add an entry, create a small component in `dev/entries.jsx` and append it to
|
|
|
10
10
|
|
|
11
11
|
Spread `uiCoreTranslations` into the default i18n namespace as the supported aggregate; per-feature translation exports remain available to selective apps that accept responsibility for pairing every adopted feature with its bundle.
|
|
12
12
|
|
|
13
|
+
## Account profile statements
|
|
14
|
+
|
|
15
|
+
`AccountPage` defaults `showProfileStatements` to `true`. Pass `false` when the consumer does not
|
|
16
|
+
offer privacy/cookie statement text endpoints; this hides their panels and prevents their requests.
|
|
17
|
+
The privacy and cookie consent controls and their submitted values remain available.
|
|
18
|
+
|
|
13
19
|
## Consumer integration check
|
|
14
20
|
|
|
15
21
|
```js
|
package/WORK_ORDERS.md
CHANGED
|
@@ -28,7 +28,11 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
28
28
|
|
|
29
29
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
30
30
|
|---|---|---|---|---|---|---|
|
|
31
|
-
| UCM-
|
|
31
|
+
| UCM-AUTH-12 | [Optional account profile statement text](work-orders/UCM-AUTH-12.md) | AccountPage showProfileStatements defaults true; false suppresses statement panels/requests while preserving both consent controls and their payload. Minor release 3.13.0, consumed by innoservice INNO-FIX-13. | 2026-10-13 | in-review | 97b7d83 | Tier 3 (shared-core account surface). Gate #1 explicitly approved in the innoservice Codex chat on 2026-10-13, including release and app pin bump. Separate configured Luna implementation and independent reviews; existing default and consent behavior are invariants. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests, ui, security · 0 raised · 0 accepted · worst accepted: none. Parent scoped tests: AccountPage.profileStatements.test.jsx + AccountPage.test.jsx, 6 passed, correct kit cwd, maxWorkers2. Native author attempt EPERM before collection; parent gate green. Reviewed additive API/default and package release metadata; main push triggers the existing release build/suite/publish. Publication and innoservice capture remain pending. |
|
|
32
|
+
| UCM-SHELL-8 | SectionNav lists contain direct button children | innoservice INNO-FIX-11 retained three serious axe list violations in account-admin-populated at 1280 px: ul[aria-labelledby="_r_b_-0"], ul[aria-labelledby="_r_b_-1"], ul[aria-labelledby="_r_b_-2"]. Installed ui-core-micha 3.12.0 AccountPage uses src/layout/SectionNav.jsx:132-159, where ListItemButton divs sit directly under List/ul. | 2026-10-13 | planned | | Source-attributed from CI38387014900, candidate9937adb, artifact e2e-capture-38387014900-1; full node evidence in innoservice/work-orders/assets/INNO-FIX-11-axe-nodes.json. Registered as expressly required by INNO-FIX-11 Part A; no kit source/token fix or implementation approval implied. Registration itself: Tier 1, self-checked identifiers/source/evidence; tests none (metadata only). Future shared-core implementation is Tier 3 and needs its own approved scope. |
|
|
33
|
+
| UCM-THEME-17 | Decide a less compressed baseline type scale | Design round (design-language process): inventory, instrument with 2-3 candidate scales next to today's baseline and MUI on real screens (hram, a survey site, jg-ferien, kerzenziehen), value rounds, frozen sheet. Measured 2026-10-04: page/card/section titles differ by 1-3 px, hierarchy rests on weight alone. Full order [`work-orders/UCM-THEME-17.md`](work-orders/UCM-THEME-17.md) | 2026-10-04 | done | `68dfc6a`, `998e36c`, `cb9dd89`, `c452224` | Tier 2 (document only) · operator decision 2026-10-04: baseline finding, not per-app overrides · implementation order follows the freeze · survey_app SVA-THEME-1 typography revert held until then · **frozen 2026-10-04** after two value rounds: candidate B, mobile h4 23, h1-h3 36/32/28; value source [`docs/TYPE-SCALE.md`](docs/TYPE-SCALE.md) · review: none, document order, the operator freeze is the gate · implementation UCM-THEME-18 |
|
|
34
|
+
| UCM-THEME-18 | Ship the frozen baseline type scale | `tokens.js` typography per `docs/TYPE-SCALE.md`: h1-h3 36/32/28, h4 24 (23 below `sm`), h5 20, subtitle1 13; light fixture typography entries; minor release 3.12.0. Full order [`work-orders/UCM-THEME-18.md`](work-orders/UCM-THEME-18.md) | 2026-10-04 | 5be83d5 | | Tier 3 (shared-core) · reviewer + ui_reviewer · downstream per-app orders (h5 page titles -> h4, per-level weight overrides) written after the release - 2026-10-04: Codex implemented (gpt-6-luna; probe of stale 2026-10-03 record succeeded, available) - orchestrator fixed review findings - review: codex/gpt-6-luna - lenses: envelope,regression,duplication,tests + ui_review - 3 raised - 2 accepted - worst accepted: defect (h4 mobile key now follows an app-overridden sm, test added; completeness test strengthened; rejected: breakpointDownQuery vs theme.breakpoints.down, no theme exists at token time) - light fixture diff limited to typography entries - no rendered side-by-side (no prototype; values asserted from the resolved theme in light and dark) - release 3.12.0 |
|
|
35
|
+
| UCM-I18N-5 | [User-list pagination range text is hard-coded English](work-orders/UCM-I18N-5.md) | `UserListComponent`'s `TablePagination` sets `labelRowsPerPage` but no `labelDisplayedRows`, so German shows "1–4 of 4". | 2026-10-01 | done | 6b9d557 | Found in jg-ferien's rendered check 2026-10-01. Follows `UCM-I18N-4`. Tier 3. Done = released AND jg-ferien pin bumped. Envelope written 2026-10-01; operator: hand to the jg Paket-4 session. Implementation map filled 2026-10-02 (`a732025`): the translation bundle carries FOUR locales (de/fr/en/sw), not three — confirmed by reading `authTranslations.ts` directly; proposed two keys (exact-count + "more than" form) with a best-effort, not native-verified, Swahili wording. Implemented by codex/gpt-5.6-luna exactly per the map: `labelDisplayedRows` resolves `UserList.DISPLAYED_ROWS`/`UserList.DISPLAYED_ROWS_MORE_THAN` through `t()`, both keys added in all four locales. review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui_reviewer (one concurrent batch, diff inline) · 4 raised · 3 accepted (deduplicated) · worst accepted: defect. Accepted: the rendered English test would still pass against a reverted implementation, since the EN wording was deliberately made identical to MUI's own hardcoded default — added a second test swapping in a distinctive marker string for just that key, proving the render genuinely resolves through i18next, not MUI's fallback; `authTranslations.test.js`'s new key-parity check only asserted non-empty strings, not that each locale actually carries the `{{from}}`/`{{to}}`/`{{count}}` placeholders the others do — strengthened with an explicit per-locale placeholder check; self-identified (corroborated by the `tests` lens's own closing remark) that the `count === -1` ("more than") branch can never be reached through a render of `UserListComponent` itself (its own `count` prop is always a real array length) — extracted the inline callback into an exported, directly unit-tested `formatDisplayedRowsLabel(t)` rather than leaving it permanently untested dead code. Rejected: `regression`'s claim that hard-coding `labelDisplayedRows` "overrides any consuming app's MUI theme customisation" — speculative, no actual consumer override exists (same explicit-prop design as `labelRowsPerPage` since `UCM-I18N-4`), and the reviewer's own repo search found zero consumer snapshots/assertions relying on the old English text. `envelope`'s "release + pin bump missing from the diff" was correct but not a code defect — addressed by the remaining steps below, not a fix to the reviewed diff. `duplication`: 0 findings. `ui_reviewer`: 0 blocking findings; confirmed placeholder consistency across all 8 strings; Swahili wording flagged as advisory-only ("structurally plausible to a non-native reviewer, native-speaker verification advisable" — not re-asked of a native speaker here, recorded as open). Tests: 37/37 green (`userScreenTranslations.test.jsx`, `authTranslations.test.js`, `UserListComponent.test.jsx`). Released as `3.11.1` (patch — continues `UCM-I18N-4`'s i18n-gap category, not a new capability area), commit `6b9d557`; npm Trusted Publishing confirmed via the Actions job log (not a registry re-query — provenance publishes can lag), tarball `@micha.bigler/ui-core-micha@3.11.1`. jg-ferien's pin bumped 3.7.0 -> 3.11.1 same day, closed as jg's own `JG-DEPS-3` (`2c1d6e3`) — see that repo's `WORK_ORDERS.md` for the empirical safety checks (status-colour palette diff, `UserListComponent`-unused confirmation, full-suite run) this multi-version jump required. |
|
|
32
36
|
| UCM-TEST-1 | A kit integration check every app runs in its own suite | Exported `checkKitIntegration({ i18n, wrapper, pages })` -> `{ findings }` (shape of `assertThemeComplete`): kit keys present in the app's i18n, one `i18next` instance, kit pages render in the app's providers without raw keys, the `invalid`-email case shows the field text. Replaces per-app hand-written wiring tests and manual auth walks. Full order [`work-orders/UCM-TEST-1.md`](work-orders/UCM-TEST-1.md) | 2026-10-01 | done | 2cde120 | Tier 3 (shared-core, auth surface) · operator decision 2026-10-01: central check, apps adopt with their next bump · companion: `workflow-templates` WFT-CI-31. Codex implemented (available today, no fallback). Orchestrator independently found + fixed before review: a false-positive in the raw-code render heuristic (a legitimate short lowercase kit-shipped value, e.g. German `"oder"` for `Auth.LOGIN_OR`, would otherwise be flagged as a leaked backend code — added a `KIT_VALUES` exemption + a pinning test), and independently proved Required Test 5 (resolver-bypassed -> error-text finding) via a transient mutation of `resolveErrorText.js`, restored clean. Independent `reviewer` (all 4 lenses) + `ui_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui · 9 raised · 6 accepted · worst accepted: blocker**. Accepted and fixed: catalogue check didn't force `fallbackLng: false` per lookup, so a consumer's own `fallbackLng` config (survey_app's is `"de"`, not `"false"`) could silently resolve a missing-language key through the fallback and mask it (tests lens) — fixed, plus a dedicated test using a real fallback-configured instance; Required Test 6 only checked adapter reference-identity, not that a real post-check request actually reaches the real adapter (envelope lens) — strengthened with a sentinel-adapter test; React 19's `act()` needs `IS_REACT_ACT_ENVIRONMENT`, undocumented for consumers (ui lens) — added to the README example; README's `pages` description implied it fully replaces the default set, when `LoginPage` always additionally renders for the error-text check regardless (ui lens, nit) — clarified. Accepted, documented rather than redesigned: mutating the shared `apiClient.defaults.adapter` for the check's duration is process-wide and could intercept a concurrent real request from other code in the same test file (regression lens, blocker) — this is the Envelope's own decided stub design (`src/utils/auth-errors.js`-style module-singleton stubbing, same pattern already used throughout this suite), not fixable without abandoning that decision; added a README caveat instead of restructuring. Rejected: `RAW_CODE` only matches lowercase-snake-case, missing uppercase/hyphenated shapes (envelope + tests lenses, same point from both) — every real backend error code in this codebase (`src/utils/auth-errors.js`, allauth headless responses) is lowercase snake_case; widening the regex matches no real shape here and raises false-positive risk for marginal benefit. `KIT_VALUES` could let a leaked code evade detection if it coincidentally equals a real kit translation value (tests lens) — true but reintroducing the "oder" false positive to close it is a worse trade; no real backend code collides with the kit's UI copy in practice. The correctly-wired/legitimate-word/resolver/adapter-restore tests would all pass against a hardcoded `{findings:[]}` stub in isolation (tests lens, nit) — true of any happy-path assertion; each is paired with a failure-case test proving the same code path actually detects the defect (Required Tests 2-5), which is where the real proof lives. `duplication`: no capability-level overlap with `assertThemeComplete` or any shared-core/app module. **Verification** (WO's own instruction): ran `checkKitIntegration` once against survey_app's real wiring — not by touching that repo, but from inside this repo's own resolved environment, replicating `survey_app/frontend/i18n/index.js`'s real merge + config exactly (`fallbackLng: "de"`, survey_app's actual `translations.json`) plus this repo's real `AuthProvider` — **result: `findings: []`**, including against the real fallback-configured instance the tests-lens finding above was about. Orchestrator's own scoped run: `tests/checkKitIntegration.test.jsx` + `tests/resolveErrorText.test.js` (12 tests) + 7 directly-dependent files (33 tests) = 45/45 green via `npx vitest run <files> --pool=threads --maxWorkers=1`. No rendered gate — library-only change, no running app to preview. **Released as `3.10.0`** (minor — new exported capability, per the WO's own instruction). Per-app adoption rows are added by the Expertenchat when each app's next bump is scoped — not part of this WO. |
|
|
33
37
|
| UCM-AUTH-11 | [`UserListComponent` offers an admin the delete action on their own row](work-orders/UCM-AUTH-11.md) | Without a `canDeleteUser` prop, `canDelete` falls back to `canEdit`, which is true for a superuser or role `admin` on EVERY row, their own included (`src/components/UserListComponent.jsx` `defaultCanEdit`/`canDelete`, ~l.128-156); the backend (dcm) still allows an admin's generic self-delete. Default should never offer deleting one's own row. | 2026-09-30 | released | 83bac37 | From django-core-micha `DCM-AUTH-4` (dropped there): operator decision 2026-09-30, UI-only, backend unchanged. Affected consumers measured: kerzenziehen and reimbursements render ucm's `AccountPage` without props (jg-ferien passes its own `userListCanDeleteUser` and already excludes itself). Both apps pin ucm **2.41.3**, so reaching them needs a major bump (3.0.0 removed chart layout props) — tracked as its own follow-up, not this WO. Gate #1 approved 2026-09-30 (operator handover). Codex implemented (available today, no fallback): new `isOwnRow(targetUser)` helper (`String(id) === String(id)`, `false` whenever either id is unresolved — covers the loading-state risk) gates the delete button's RENDER — `{showDeleteAction && !isOwnRow(row) && (...)}` — not its `disabled` state, so a consumer's `canDeleteUser` returning `true` for the own row cannot bring the button back; `canDelete`/`canEdit`/`defaultCanEdit` untouched. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 3 raised · 1 accepted · worst accepted: nit**. Accepted (nit, tests lens): the committed fixtures only ever used numeric ids for both `currentUser` and the own row, so a strict-equality implementation would have passed every committed test despite violating the Envelope's own `3`/`"3"` identity requirement — added a fifth case with a string-vs-number id mismatch. Rejected: envelope lens's claim that "Required Test 5 (mutation proof) is missing" — the WO's own Part B (mirroring `UCM-THEME-15`/`UCM-PRIM-2`'s established pattern in this repo) asks for a TRANSIENT demonstration during implementation, not a permanent fifth test case; the implementer's own `PROGRESS` narration confirms it removed the gate, watched tests 1 and 3 fail (2 delete buttons found instead of 1), then restored — exactly as asked. Envelope lens's claim that "package.json remains 3.9.0 so the release is incomplete" — true at the moment the reviewer read the diff (version bump + register are the Orchestrator's own finalize step, explicitly forbidden to the implementer), not a defect in the change itself; done now. `regression`/`duplication`/`ui`/`sec` lenses: no findings — `sec_reviewer` specifically confirmed the render-gate cannot be reopened via `canDeleteUser`/`canEditUser`/`extraRowActions`, and that the backend's own continued refusal of an admin's self-delete (`DCM-AUTH-4`) means this UI-only fix is not masking a still-open server-side gap. Orchestrator's own scoped run: `tests/UserListComponent.test.jsx` 7/7 green + `tests/userScreenTranslations.test.jsx` (directly-dependent, also renders this component) 4/4 green, via `npx vitest run <files> --pool=threads --maxWorkers=1` (default fork pool repeatedly failed to start workers in this sandbox — an environment flake, not a code issue). No rendered gate — library component, no running app to preview; unit-tested only. **Released as `3.9.1`** (patch — behaviour fix of an existing component, no API change, per the WO's own instruction). `npm view @micha.bigler/ui-core-micha version` will read `3.9.1` once the registry's version-list index catches up to the just-completed publish (provenance-signed publishes report success immediately but can take a few minutes to appear in `npm view`/the registry API — confirmed via the GitHub Actions job log on the prior `UCM-AUTH-10` release, not re-verified here to avoid the same registry-lag false alarm). **Not done in this session** (per the WO's own Part C): registering the kerzenziehen/reimbursements major-bump follow-up rows in their own `WORK_ORDERS.md` registers — flagged for whoever next works in those repos. |
|
|
34
38
|
| UCM-AUTH-10 | Error texts never show a raw backend code | One resolver for every kit error display: backend `i18nKey` > field key `Auth.field.<field>.<code>` > code key (AUTH-2) > backend message > caller default; `apiClient` sends the UI language as `Accept-Language`; `invalid`/`required` field keys for every submitted field; ~40 call sites migrated; reconstructs the missing 3.7.0 changelog entry. Full order [`work-orders/UCM-AUTH-10.md`](work-orders/UCM-AUTH-10.md) | 2026-09-30 | done | 9019d3d | Tier 3 (shared-core, auth screens, header on every request) · operator decision 2026-09-30: code-based UI, backend message as last specific fallback, UI language sent to the backend · found in `survey_app` SVA-DEP-1 acceptance (pre-existing, not a bump regression). Codex implemented (available today, no fallback): new `src/utils/resolveErrorText.js` (plain function, not a hook — called inside `catch` blocks) implementing steps 0-4 via `i18n.exists`/`i18n.t`; `extractErrorInfo`/`normaliseApiError` now carry `field` (`param` allauth / `field` dcm, normalised to one name), `i18nKey`, and `backendMessage` (never falls back to the code — the literal bug) as separate fields; `apiClient` gained a request interceptor sending `Accept-Language: i18next.language` only when `i18next.isInitialized` (never guesses); ~40 call sites across 22 files migrated to the uniform pattern (state now always holds resolved display text, never a bare key; render sites dropped their `t()` wrapper); 48 new `Auth.field.<field>.{invalid,required}` keys (24 fields, enumerated from `authApi.jsx`, all 4 locales + `de_informal` where wording differs); `CHANGELOG.md`'s missing `3.7.0` entry reconstructed. Independent `reviewer` (all 4 lenses) + `ui_reviewer` + `sec_reviewer` (Codex gpt-5.6-luna, concurrent background batch, full diff): **review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui,sec · 12 raised · 4 accepted · worst accepted: defect**. Accepted: (defect, envelope lens — a real guard blind spot) `MFAComponent.jsx`'s `setErrorKey('Auth.MFA_TOTP_ALREADY_ACTIVE')` was a plain literal-key setter the mechanical sweep missed — the call-site-guard test only matches the `.code` pattern, not "any literal setter needs `t()` now that render dropped its wrapper"; would have displayed the raw key on screen, exactly the bug this WO exists to fix. Fixed (wrapped in `t()`); no dedicated test exists for this component, fix verified by direct code inspection only. (nit, ui_reviewer) the new field-key catalogue omitted `RegistrationMethodsManager.jsx`'s 5 boolean policy-toggle fields (`allow_admin_invite`, `allow_self_signup_{access_code,open,email_domain,qr}`); added, all 4 locales. (nit, tests lens) `resolveErrorText.test.js`'s "dcm field shape" case was byte-identical to the allauth-shape case, testing nothing new (the param/field normalisation it claimed to cover already happens one layer down, in `extractErrorInfo`, and is genuinely tested there); removed the redundant duplicate. Rejected: 2 lenses (envelope, ui_reviewer) claimed `SupportRecoveryRequestsTab.jsx` calls `resolveErrorText(i18n, …)` without destructuring `i18n` — verified directly against the file twice, `const { t, i18n } = useTranslation()` is present at line 42 and every call site is a closure over it; a stale/misread finding from both lenses, not a real defect. 3 lenses (envelope, regression, tests) flagged step 3 (`backendMessage` rendered verbatim) as an exposure risk — this is the Envelope's own **decided** design (`Operator decision: … the backend message as the last specific fallback`), not an implementation defect; `sec_reviewer` — the lens specifically briefed with dcm's S4 audit-log rationale and tasked with exactly this question — found no HTML-injection path and confirmed React's text-node escaping covers every render site, which is what the Envelope's own risk-review request was actually asking. `sec_reviewer` additionally flagged `BulkInviteCsvTab.jsx:88`'s `err.message` and `PasswordInvitePage.jsx:117`'s `err.raw.messages` as chain bypasses — the former is a purely local CSV-file-read error with no backend response involved at all (confirmed no `apiClient` call in that `try` block), the latter is `UCM-AUTH-9`'s already-shipped, already-reviewed password-rejection-reason display, unchanged by this WO and out of scope per its own non-goal ("no change to which errors are shown, where, or what triggers one"). `envelope`+`tests` lenses' claim that `auth-errors.js`'s native `.message` still falls back to the code — deliberate, per this WO's own Part B ("you may leave `err.message` with its existing fallback-to-code behaviour untouched") and covered by the implementer's own test; the field the resolver actually reads (`backendMessage`) correctly never falls back, which is what the Envelope's separation requirement is about. Orchestrator's own scoped run: 14 files, 102 tests green (`resolveErrorText`/`loginPageErrorResolution`/`errorCallSiteGuard`/`apiClientLanguage`/`authErrors`/`AccountPage`/`PasswordInvitePage`/`SignUpPage`/`UserListComponent`/`QrSignupManager.registrationContext`/`i18nAggregate`/`authTranslations`/`themeCompleteness`/`i18nGermanVariant`), via `npx vitest run <files> --pool=threads --maxWorkers=1` (the default fork pool repeatedly failed to start workers in this sandbox — an environment flake, not a code issue; the threads pool is stable). **Verification (the operator's exact screenshot case, rendered end to end):** `en` → "Enter a valid email address."; `de` → "Bitte geben Sie eine gültige E-Mail-Adresse ein." — both resolve via step 1 (`Auth.field.email.invalid`), never the raw `invalid` code, confirmed by `tests/loginPageErrorResolution.test.jsx` (`en`) and by the locale-agnostic resolver design (same key, `de` value) for `de`. No rendered browser gate — verified via the real `LoginPage` component rendered against a real i18next instance in the test harness. **Released as `3.9.0`** (minor, per the Envelope's own instruction — `resolveErrorText` is internal-only, not part of the public `src/index.js` surface). `npm view @micha.bigler/ui-core-micha version` should read `3.9.0` once `publish.yml` runs on this push. No consumer bump in this session (non-goal). |
|
|
@@ -26,7 +26,7 @@ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
|
|
|
26
26
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
27
27
|
import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
|
|
28
28
|
import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
|
|
29
|
-
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowNewColumn = true, userListShowSuccessfulLoginColumn = true, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
|
|
29
|
+
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowNewColumn = true, userListShowSuccessfulLoginColumn = true, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], showProfileStatements = true, }) {
|
|
30
30
|
var _a;
|
|
31
31
|
const { t, i18n } = useTranslation();
|
|
32
32
|
const { user, login, loading } = useContext(AuthContext);
|
|
@@ -174,5 +174,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
174
174
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
175
175
|
? null
|
|
176
176
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
177
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
177
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true, showStatements: showProfileStatements }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
178
178
|
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
import './fonts';
|
|
2
13
|
import { createTheme, darken, decomposeColor, getContrastRatio, lighten } from '@mui/material/styles';
|
|
3
|
-
import { BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS, BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS_DARK, BASELINE_PALETTE, BASELINE_PALETTE_DARK, BASELINE_STATIC, DARK_OVERLAY_SHADOW, DARK_OVERLAY_SURFACE, DARK_SCRIM, MOTION, withMainShades, } from './tokens';
|
|
14
|
+
import { BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS, BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS_DARK, BASELINE_PALETTE, BASELINE_PALETTE_DARK, BASELINE_STATIC, BREAKPOINT_SM, DARK_OVERLAY_SHADOW, DARK_OVERLAY_SURFACE, DARK_SCRIM, MOTION, breakpointDownQuery, withMainShades, } from './tokens';
|
|
4
15
|
import { calculateContrastRatio } from './themeCompleteness';
|
|
5
16
|
function findFunction(value, path) {
|
|
6
17
|
if (typeof value === 'function')
|
|
@@ -255,7 +266,7 @@ function createPaletteAwareComponents(palette, isDark) {
|
|
|
255
266
|
* objects are created, so runtime app/site overrides cannot leave stale values.
|
|
256
267
|
*/
|
|
257
268
|
export function createAppTheme(appConfig = {}) {
|
|
258
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
269
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
|
|
259
270
|
if (!((_a = appConfig === null || appConfig === void 0 ? void 0 : appConfig.palette) === null || _a === void 0 ? void 0 : _a.primary)) {
|
|
260
271
|
throw new TypeError('createAppTheme: appConfig.palette.primary is required.');
|
|
261
272
|
}
|
|
@@ -322,7 +333,11 @@ export function createAppTheme(appConfig = {}) {
|
|
|
322
333
|
: { main: primaryMain })), { contrastText: primaryContrastText }), dataSeries: { categorical: derivedSeries } });
|
|
323
334
|
const resolvedPalette = Object.assign(Object.assign({}, finalPalette), computedPalette);
|
|
324
335
|
const fontFamily = ((_p = appConfig.typography) === null || _p === void 0 ? void 0 : _p.fontFamily) || BASELINE_STATIC.typography.fontFamily;
|
|
325
|
-
|
|
336
|
+
// The h4 step below `sm` follows the app's own `sm` if it overrides the breakpoint,
|
|
337
|
+
// so the step always ends where theme.breakpoints.down('sm') ends.
|
|
338
|
+
const _t = BASELINE_STATIC.typography.h4, _u = breakpointDownQuery(BREAKPOINT_SM), h4Mobile = _t[_u], h4Base = __rest(_t, [typeof _u === "symbol" ? _u : _u + ""]);
|
|
339
|
+
const smValue = (_s = (_r = (_q = appConfig.breakpoints) === null || _q === void 0 ? void 0 : _q.values) === null || _r === void 0 ? void 0 : _r.sm) !== null && _s !== void 0 ? _s : BREAKPOINT_SM;
|
|
340
|
+
return createTheme(Object.assign(Object.assign({}, BASELINE_STATIC), { typography: Object.assign(Object.assign({}, BASELINE_STATIC.typography), { fontFamily, h4: Object.assign(Object.assign({}, h4Base), { [breakpointDownQuery(smValue)]: h4Mobile }) }), fontLoading: Object.assign(Object.assign({}, BASELINE_STATIC.fontLoading), { weights: [...BASELINE_STATIC.fontLoading.weights] }), shadow: isDark ? Object.assign(Object.assign({}, BASELINE_STATIC.shadow), { overlay: DARK_OVERLAY_SHADOW }) : BASELINE_STATIC.shadow, palette: resolvedPalette, themeCompleteness: {
|
|
326
341
|
baseline: true,
|
|
327
342
|
exemptions: (isDark
|
|
328
343
|
? BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS_DARK
|
package/dist/theme/tokens.js
CHANGED
|
@@ -40,6 +40,8 @@ export const TYPOGRAPHY_VARIANTS = [
|
|
|
40
40
|
const typeVariant = (fontSize, fontWeight, lineHeight, extra = {}) => (Object.assign({ fontSize,
|
|
41
41
|
fontWeight,
|
|
42
42
|
lineHeight, letterSpacing: 0 }, extra));
|
|
43
|
+
export const BREAKPOINT_SM = 600;
|
|
44
|
+
export const breakpointDownQuery = (value) => `@media (max-width:${value - 0.05}px)`;
|
|
43
45
|
export const BASELINE_PALETTE = {
|
|
44
46
|
ink: {
|
|
45
47
|
primary: '#212529',
|
|
@@ -156,13 +158,15 @@ const autofill = {
|
|
|
156
158
|
export const BASELINE_STATIC = {
|
|
157
159
|
typography: {
|
|
158
160
|
fontFamily: "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
159
|
-
h1: typeVariant('
|
|
160
|
-
h2: typeVariant('
|
|
161
|
-
h3: typeVariant('
|
|
162
|
-
h4: typeVariant('
|
|
163
|
-
|
|
161
|
+
h1: typeVariant('36px', 600, 1.2),
|
|
162
|
+
h2: typeVariant('32px', 600, 1.22),
|
|
163
|
+
h3: typeVariant('28px', 600, 1.25),
|
|
164
|
+
h4: typeVariant('24px', 600, 1.3, {
|
|
165
|
+
[breakpointDownQuery(BREAKPOINT_SM)]: { fontSize: '23px' },
|
|
166
|
+
}),
|
|
167
|
+
h5: typeVariant('20px', 600, 1.3),
|
|
164
168
|
h6: typeVariant('16px', 600, 1.35),
|
|
165
|
-
subtitle1: typeVariant('
|
|
169
|
+
subtitle1: typeVariant('13px', 600, 1.4),
|
|
166
170
|
subtitle2: typeVariant('13px', 500, 1.4),
|
|
167
171
|
body1: typeVariant('14px', 400, 1.55),
|
|
168
172
|
body2: typeVariant('13px', 400, 1.55),
|
|
@@ -182,7 +186,7 @@ export const BASELINE_STATIC = {
|
|
|
182
186
|
},
|
|
183
187
|
spacing: 8,
|
|
184
188
|
breakpoints: {
|
|
185
|
-
values: { xs: 0, sm:
|
|
189
|
+
values: { xs: 0, sm: BREAKPOINT_SM, md: 900, lg: 1200, xl: 1536 },
|
|
186
190
|
},
|
|
187
191
|
density: {
|
|
188
192
|
controlHeight: 40,
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Baseline type scale
|
|
2
|
+
|
|
3
|
+
Reference sheet for `UCM-THEME-17`. **This file is the value source** for the baseline typography:
|
|
4
|
+
`UCM-THEME-18` takes every value from here by variant name, and if code and this sheet ever disagree, this
|
|
5
|
+
sheet is the one to fix first.
|
|
6
|
+
|
|
7
|
+
Instrument: [`work-orders/assets/UCM-THEME-17-type-scale.html`](../work-orders/assets/UCM-THEME-17-type-scale.html)
|
|
8
|
+
(the candidates beside today's baseline and the MUI default on four estate screens at 375 px and 1280 px; sizes,
|
|
9
|
+
ratios and frame heights are measured in the page).
|
|
10
|
+
|
|
11
|
+
## Frozen
|
|
12
|
+
|
|
13
|
+
**Frozen 2026-10-04 by the operator** ("Ja, einfrieren"), after two value rounds.
|
|
14
|
+
|
|
15
|
+
## Why the scale changed
|
|
16
|
+
|
|
17
|
+
The previous baseline stepped its headings by 1 to 3 px (h4 20, h5 18, h6 16, subtitle1 15), so hierarchy rested
|
|
18
|
+
on weight alone, and where apps raised every level to 700 or 800 the levels merged (survey_app sites, operator
|
|
19
|
+
screenshots 2026-10-04). The approved prototypes in the estate had used the larger title step all along: screen
|
|
20
|
+
title about 1.7x body at 1280 px, stepped down on mobile, section labels below body size.
|
|
21
|
+
|
|
22
|
+
## Structural decisions (operator, 2026-10-04)
|
|
23
|
+
|
|
24
|
+
1. **One scale** for every app and page type; no separate reading register. Body stays at the tool density
|
|
25
|
+
(`jg-ferien` `JG-DL-7` had rejected a larger body).
|
|
26
|
+
2. **The screen title (h4) steps down on narrow screens.** No other variant does.
|
|
27
|
+
3. **Size carries the hierarchy, weight is fixed**: every heading 600. App overrides are for font family, not
|
|
28
|
+
for per-level weight.
|
|
29
|
+
4. **The section label (subtitle1) sits below body size**, set apart by weight.
|
|
30
|
+
|
|
31
|
+
Role mapping, unchanged from the `THEME-1` sheet: h4 screen title, h5 panel or dialog, h6 card title,
|
|
32
|
+
subtitle1 section label.
|
|
33
|
+
|
|
34
|
+
## Token table
|
|
35
|
+
|
|
36
|
+
| Variant | Before | Frozen | Weight | Line height | Origin |
|
|
37
|
+
|---|---|---|---|---|---|
|
|
38
|
+
| h1 | 32 | **36** | 600 | 1.2 | derived: 4 px steps above h3 |
|
|
39
|
+
| h2 | 28 | **32** | 600 | 1.22 | derived: 4 px steps above h3 |
|
|
40
|
+
| h3 | 24 | **28** | 600 | 1.25 | derived: 4 px above h4 |
|
|
41
|
+
| h4, screen title | 20 | **24** | 600 | 1.3 | distilled: prototype median (KZ-UI-1, UX-10, JG-MEM-1) |
|
|
42
|
+
| h4 below `sm` (600 px) | 20 | **23** | 600 | 1.3 | decided round 2: the smallest size that keeps h4 : h5 at 1.12 or more on mobile. The breakpoint is the baseline's own `sm`, derived, not judged |
|
|
43
|
+
| h5, panel | 18 | **20** | 600 | 1.3 | distilled: prototypes 18 to 20 |
|
|
44
|
+
| h6, card title | 16 | 16 | 600 | 1.35 | unchanged |
|
|
45
|
+
| subtitle1, section label | 15/600 | **13** | 600 | 1.4 | distilled: prototypes 12 to 13 at 600 to 700 |
|
|
46
|
+
| subtitle2 | 13/500 | 13 | 500 | 1.4 | unchanged |
|
|
47
|
+
| body1 | 14 | 14 | 400 | 1.55 | unchanged (decision 1) |
|
|
48
|
+
| body2 | 13 | 13 | 400 | 1.55 | unchanged |
|
|
49
|
+
| button | 14/500 | 14 | 500 | 1.4 | unchanged |
|
|
50
|
+
| caption | 12 | 12 | 400 | 1.4 | unchanged |
|
|
51
|
+
| overline | 11/600 | 11 | 600 | 1.4 | unchanged |
|
|
52
|
+
|
|
53
|
+
Sizes in px. `letterSpacing` 0 throughout, overline 0.4px uppercase as before.
|
|
54
|
+
|
|
55
|
+
## Measurements (computed in the instrument, 2026-10-04)
|
|
56
|
+
|
|
57
|
+
| Ratio | Before | Frozen | MUI |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| h4 : h5 at 1280 | 1.11 | 1.20 | 1.42 |
|
|
60
|
+
| h4 : h5 at 375 | 1.11 | 1.15 | 1.42 |
|
|
61
|
+
| h5 : h6 | 1.13 | 1.25 | 1.20 |
|
|
62
|
+
| h6 : body1 | 1.14 | 1.14 | 1.25 |
|
|
63
|
+
| h4 : body1 at 1280 / 375 | 1.43 / 1.43 | 1.71 / 1.64 | 2.13 / 2.13 |
|
|
64
|
+
| subtitle1 : body1 | 1.07 | 0.93 | 1.00 |
|
|
65
|
+
|
|
66
|
+
The instrument works with a threshold of 1.12 between two heading levels at the same weight. That number is this
|
|
67
|
+
sheet's working assumption, not a standard.
|
|
68
|
+
|
|
69
|
+
Frame height against the previous baseline: hram Research -1.1 % / -1.0 % (375 / 1280), jg-ferien dashboard
|
|
70
|
+
+0.5 % / +0.5 %, kerzenziehen admin -1.4 % / -1.3 %, survey site +0.7 % / +0.8 %. The smaller section label offsets
|
|
71
|
+
the larger titles; the vertical-space risk named in the work order does not occur. MUI by comparison: +4 to +18 %.
|
|
72
|
+
|
|
73
|
+
## Rejected, and why
|
|
74
|
+
|
|
75
|
+
- **Candidate A** (h4 22, h5 18, h6 16, subtitle1 13): leaves h5 : h6 at 1.13, the panel and card stay as
|
|
76
|
+
close as before.
|
|
77
|
+
- **Candidate C** (h4 28, h5 22, h6 17, subtitle1 12): the counter-test; closest to MUI, largest height cost
|
|
78
|
+
(up to +2.3 %).
|
|
79
|
+
- **Mobile title at desktop minus 2** (round 1, B at 22): h4 : h5 on mobile falls to 1.10, as close as before.
|
|
80
|
+
- **h1 to h3 kept at 32 / 28 / 24**: h3 would equal the new h4 and drop out as a level.
|
|
81
|
+
- **The MUI scale**: body 16 is the size `jg-ferien` already rejected for tool screens.
|
|
82
|
+
|
|
83
|
+
## Known consequences outside this repo
|
|
84
|
+
|
|
85
|
+
These are inputs for the per-app follow-up after the release, not part of the baseline:
|
|
86
|
+
|
|
87
|
+
- **Screens that use another variant as their screen title** do not get the new title value or the mobile step.
|
|
88
|
+
Measured 2026-10-04 (first heading variant in each `*Page.jsx`, a heuristic): hram 5 pages h5, jg-ferien 3 pages
|
|
89
|
+
h5 (`DashboardPage.jsx:187`), kerzenziehen admin h5 (rendered measurement), spesix 1 page h5, fitness-monitor
|
|
90
|
+
1 page h5; survey_app uses h3 on 7 pages, which grows from 24 to 28.
|
|
91
|
+
- **Per-level weight overrides** contradict decision 3: survey_app (h1 to h6 800/700, site builder 800),
|
|
92
|
+
hram (500), spesix (weights). survey_app's held typography revert (`SVA-THEME-1`) is decided by this sheet.
|
|
93
|
+
- **subtitle1 also serves as a title, not only as a section label** (added 2026-10-04, after the freeze; the
|
|
94
|
+
inventory missed it, because it read subtitle1 only in its section-label role on hram Research). Uses in
|
|
95
|
+
`src/`: hram 72, jg-ferien 39, ui-core-micha itself 12 (`UserMenu` user name, `SectionNav` active item,
|
|
96
|
+
announcement titles), innoservice 9, survey_app 7, reimbursements 6, spesix 5. As item or panel titles they
|
|
97
|
+
fall below body size. **Decided at the jg-ferien pilot `JG-DEPS-4`, 2026-10-04: subtitle1 stays at 13 px.**
|
|
98
|
+
The operator looked at jg-ferien on the 3.12.0 build, including `FinanceSummaryPanel` with subtitle1 as a
|
|
99
|
+
panel section title, and accepted it without any remapping. Rollout orders therefore do not remap
|
|
100
|
+
subtitle1 by default; a remap happens only where an app's own look rejects a specific screen.
|
|
101
|
+
- **Exception to decision 3: spesix keeps its own heading weights** (operator, 2026-10-04, at `SPX-DEP-3`). Its
|
|
102
|
+
`DESIGN` block (h1 800, h2-h5 700, h6 600, button 700) belongs to a decided design (`SPX-THEME-1`, identity
|
|
103
|
+
bucket), and the new sizes separate the levels on their own. survey_app's 800/700 weights are not covered by
|
|
104
|
+
this exception: there the operator found the bold levels themselves to be the problem.
|
|
105
|
+
- webshop-guenter keeps its handoff scale (decided brand, identity bucket). Kira, Cinevia and Gustav use no MUI
|
|
106
|
+
heading variants and are unaffected.
|
|
107
|
+
|
|
108
|
+
## Unverified
|
|
109
|
+
|
|
110
|
+
Declared, not shown by the static instrument: long titles wrapping in French and German at 375 px, dialogs,
|
|
111
|
+
the h1 to h3 sizes on real public pages, and the step at exactly 600 px.
|
package/package.json
CHANGED
|
@@ -53,6 +53,7 @@ export function AccountPage({
|
|
|
53
53
|
showBulkInviteCsvTab = false,
|
|
54
54
|
bulkInviteCsvProps = {},
|
|
55
55
|
extraTabs = [],
|
|
56
|
+
showProfileStatements = true,
|
|
56
57
|
}) {
|
|
57
58
|
const { t, i18n } = useTranslation();
|
|
58
59
|
const { user, login, loading } = useContext(AuthContext);
|
|
@@ -253,6 +254,7 @@ export function AccountPage({
|
|
|
253
254
|
showName
|
|
254
255
|
showPrivacy
|
|
255
256
|
showCookies
|
|
257
|
+
showStatements={showProfileStatements}
|
|
256
258
|
/>
|
|
257
259
|
</Box>
|
|
258
260
|
)}
|
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
BASELINE_PALETTE,
|
|
9
9
|
BASELINE_PALETTE_DARK,
|
|
10
10
|
BASELINE_STATIC,
|
|
11
|
+
BREAKPOINT_SM,
|
|
11
12
|
DARK_OVERLAY_SHADOW,
|
|
12
13
|
DARK_OVERLAY_SURFACE,
|
|
13
14
|
DARK_SCRIM,
|
|
14
15
|
MOTION,
|
|
16
|
+
breakpointDownQuery,
|
|
15
17
|
withMainShades,
|
|
16
18
|
} from './tokens';
|
|
17
19
|
import { calculateContrastRatio } from './themeCompleteness';
|
|
@@ -377,6 +379,10 @@ export function createAppTheme(appConfig = {}) {
|
|
|
377
379
|
};
|
|
378
380
|
const resolvedPalette = { ...finalPalette, ...computedPalette };
|
|
379
381
|
const fontFamily = appConfig.typography?.fontFamily || BASELINE_STATIC.typography.fontFamily;
|
|
382
|
+
// The h4 step below `sm` follows the app's own `sm` if it overrides the breakpoint,
|
|
383
|
+
// so the step always ends where theme.breakpoints.down('sm') ends.
|
|
384
|
+
const { [breakpointDownQuery(BREAKPOINT_SM)]: h4Mobile, ...h4Base } = BASELINE_STATIC.typography.h4;
|
|
385
|
+
const smValue = appConfig.breakpoints?.values?.sm ?? BREAKPOINT_SM;
|
|
380
386
|
|
|
381
387
|
return createTheme(
|
|
382
388
|
{
|
|
@@ -384,6 +390,7 @@ export function createAppTheme(appConfig = {}) {
|
|
|
384
390
|
typography: {
|
|
385
391
|
...BASELINE_STATIC.typography,
|
|
386
392
|
fontFamily,
|
|
393
|
+
h4: { ...h4Base, [breakpointDownQuery(smValue)]: h4Mobile },
|
|
387
394
|
},
|
|
388
395
|
fontLoading: {
|
|
389
396
|
...BASELINE_STATIC.fontLoading,
|
package/src/theme/tokens.js
CHANGED
|
@@ -50,6 +50,9 @@ const typeVariant = (fontSize, fontWeight, lineHeight, extra = {}) => ({
|
|
|
50
50
|
...extra,
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
export const BREAKPOINT_SM = 600;
|
|
54
|
+
export const breakpointDownQuery = (value) => `@media (max-width:${value - 0.05}px)`;
|
|
55
|
+
|
|
53
56
|
export const BASELINE_PALETTE = {
|
|
54
57
|
ink: {
|
|
55
58
|
primary: '#212529',
|
|
@@ -222,13 +225,15 @@ const autofill = {
|
|
|
222
225
|
export const BASELINE_STATIC = {
|
|
223
226
|
typography: {
|
|
224
227
|
fontFamily: "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
|
225
|
-
h1: typeVariant('
|
|
226
|
-
h2: typeVariant('
|
|
227
|
-
h3: typeVariant('
|
|
228
|
-
h4: typeVariant('
|
|
229
|
-
|
|
228
|
+
h1: typeVariant('36px', 600, 1.2),
|
|
229
|
+
h2: typeVariant('32px', 600, 1.22),
|
|
230
|
+
h3: typeVariant('28px', 600, 1.25),
|
|
231
|
+
h4: typeVariant('24px', 600, 1.3, {
|
|
232
|
+
[breakpointDownQuery(BREAKPOINT_SM)]: { fontSize: '23px' },
|
|
233
|
+
}),
|
|
234
|
+
h5: typeVariant('20px', 600, 1.3),
|
|
230
235
|
h6: typeVariant('16px', 600, 1.35),
|
|
231
|
-
subtitle1: typeVariant('
|
|
236
|
+
subtitle1: typeVariant('13px', 600, 1.4),
|
|
232
237
|
subtitle2: typeVariant('13px', 500, 1.4),
|
|
233
238
|
body1: typeVariant('14px', 400, 1.55),
|
|
234
239
|
body2: typeVariant('13px', 400, 1.55),
|
|
@@ -248,7 +253,7 @@ export const BASELINE_STATIC = {
|
|
|
248
253
|
},
|
|
249
254
|
spacing: 8,
|
|
250
255
|
breakpoints: {
|
|
251
|
-
values: { xs: 0, sm:
|
|
256
|
+
values: { xs: 0, sm: BREAKPOINT_SM, md: 900, lg: 1200, xl: 1536 },
|
|
252
257
|
},
|
|
253
258
|
density: {
|
|
254
259
|
controlHeight: 40,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
6
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
7
|
+
|
|
8
|
+
vi.mock('react-i18next', () => {
|
|
9
|
+
const KEYS = {
|
|
10
|
+
'Account.TITLE': 'Account & Administration',
|
|
11
|
+
'Account.PAGE_TITLE': 'Account',
|
|
12
|
+
'Account.TAB_PROFILE': 'Profile',
|
|
13
|
+
'Account.TAB_SECURITY': 'Security',
|
|
14
|
+
'Account.GROUP_MY_ACCOUNT': 'My account',
|
|
15
|
+
'Profile.ACCEPT_PRIVACY_LABEL': 'Accept privacy statement',
|
|
16
|
+
'Profile.ACCEPT_COOKIES_LABEL': 'Accept convenience cookies',
|
|
17
|
+
'Profile.PRIVACY_STATEMENT_TITLE': 'Privacy statement',
|
|
18
|
+
'Profile.COOKIES_STATEMENT_TITLE': 'Cookie statement',
|
|
19
|
+
'Profile.SAVE_BUTTON': 'Save',
|
|
20
|
+
'Profile.PRIVACY_SECTION_TITLE': 'Privacy and cookies',
|
|
21
|
+
'Profile.USERNAME_LABEL': 'Username',
|
|
22
|
+
'Profile.EMAIL_LABEL': 'Email',
|
|
23
|
+
'Profile.FIRST_NAME_LABEL': 'First name',
|
|
24
|
+
'Profile.LAST_NAME_LABEL': 'Last name',
|
|
25
|
+
};
|
|
26
|
+
const t = (key, fallback) => KEYS[key] ?? fallback ?? key;
|
|
27
|
+
return {
|
|
28
|
+
useTranslation: () => ({
|
|
29
|
+
t,
|
|
30
|
+
i18n: { exists: (key) => Object.hasOwn(KEYS, key), t },
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock('../src/auth/authApi', () => ({
|
|
36
|
+
fetchAuthPolicy: vi.fn(),
|
|
37
|
+
fetchPrivacyStatement: vi.fn(),
|
|
38
|
+
fetchCookieStatement: vi.fn(),
|
|
39
|
+
updateUserProfile: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock('../src/components/SecurityComponent', () => ({ SecurityComponent: () => null }));
|
|
43
|
+
vi.mock('../src/components/UserListComponent', () => ({ UserListComponent: () => null }));
|
|
44
|
+
vi.mock('../src/components/UserInviteComponent', () => ({ UserInviteComponent: () => null }));
|
|
45
|
+
vi.mock('../src/components/AccessCodeManager', () => ({ AccessCodeManager: () => null }));
|
|
46
|
+
vi.mock('../src/components/AllowedEmailDomainsManager', () => ({ AllowedEmailDomainsManager: () => null }));
|
|
47
|
+
vi.mock('../src/components/RegistrationMethodsManager', () => ({ RegistrationMethodsManager: () => null }));
|
|
48
|
+
vi.mock('../src/components/AuthFactorRequirementCard', () => ({ AuthFactorRequirementCard: () => null }));
|
|
49
|
+
vi.mock('../src/components/AccessCodeSingleUseToggle', () => ({ AccessCodeSingleUseToggle: () => null }));
|
|
50
|
+
vi.mock('../src/components/QrSignupManager', () => ({ QrSignupManager: () => null }));
|
|
51
|
+
vi.mock('../src/components/QrSignupValidityManager', () => ({ QrSignupValidityManager: () => null }));
|
|
52
|
+
vi.mock('../src/components/SupportRecoveryRequestsTab', () => ({ SupportRecoveryRequestsTab: () => null }));
|
|
53
|
+
vi.mock('../src/components/BulkInviteCsvTab', () => ({ BulkInviteCsvTab: () => null }));
|
|
54
|
+
|
|
55
|
+
import { AuthContext } from '../src/auth/AuthContext';
|
|
56
|
+
import {
|
|
57
|
+
fetchCookieStatement,
|
|
58
|
+
fetchPrivacyStatement,
|
|
59
|
+
updateUserProfile,
|
|
60
|
+
} from '../src/auth/authApi';
|
|
61
|
+
import { AccountPage } from '../src/pages/AccountPage';
|
|
62
|
+
|
|
63
|
+
const theme = createTheme();
|
|
64
|
+
const user = {
|
|
65
|
+
email: 'person@example.com',
|
|
66
|
+
username: 'person',
|
|
67
|
+
first_name: 'Pat',
|
|
68
|
+
last_name: 'Example',
|
|
69
|
+
accepted_privacy_statement: false,
|
|
70
|
+
accepted_convenience_cookies: true,
|
|
71
|
+
is_superuser: false,
|
|
72
|
+
ui_permissions: {},
|
|
73
|
+
available_roles: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function setViewportWidth(width) {
|
|
77
|
+
window.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
78
|
+
matches: query.includes('max-width') ? width <= 900 : width > 900,
|
|
79
|
+
media: query,
|
|
80
|
+
onchange: null,
|
|
81
|
+
addEventListener: vi.fn(),
|
|
82
|
+
removeEventListener: vi.fn(),
|
|
83
|
+
addListener: vi.fn(),
|
|
84
|
+
removeListener: vi.fn(),
|
|
85
|
+
dispatchEvent: vi.fn(),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderAccount({ showProfileStatements } = {}) {
|
|
90
|
+
const login = vi.fn();
|
|
91
|
+
return {
|
|
92
|
+
login,
|
|
93
|
+
...render(
|
|
94
|
+
<ThemeProvider theme={theme}>
|
|
95
|
+
<AuthContext.Provider value={{ user, login, loading: false }}>
|
|
96
|
+
<MemoryRouter initialEntries={['/account']}>
|
|
97
|
+
<Routes>
|
|
98
|
+
<Route
|
|
99
|
+
path="/account"
|
|
100
|
+
element={<AccountPage showProfileStatements={showProfileStatements} />}
|
|
101
|
+
/>
|
|
102
|
+
</Routes>
|
|
103
|
+
</MemoryRouter>
|
|
104
|
+
</AuthContext.Provider>
|
|
105
|
+
</ThemeProvider>,
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('AccountPage profile statements', () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
setViewportWidth(1280);
|
|
113
|
+
vi.mocked(fetchPrivacyStatement).mockResolvedValue('Privacy text');
|
|
114
|
+
vi.mocked(fetchCookieStatement).mockResolvedValue('Cookie text');
|
|
115
|
+
vi.mocked(updateUserProfile).mockResolvedValue({ ...user, accepted_privacy_statement: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
cleanup();
|
|
120
|
+
vi.clearAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('fetches and presents both statement panels when the prop is omitted', async () => {
|
|
124
|
+
renderAccount();
|
|
125
|
+
|
|
126
|
+
expect(await screen.findByText('Privacy statement')).toBeTruthy();
|
|
127
|
+
expect(screen.getByText('Cookie statement')).toBeTruthy();
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(fetchPrivacyStatement).toHaveBeenCalledOnce();
|
|
130
|
+
expect(fetchCookieStatement).toHaveBeenCalledOnce();
|
|
131
|
+
});
|
|
132
|
+
expect(await screen.findByText('Privacy text')).toBeTruthy();
|
|
133
|
+
expect(screen.getByText('Cookie text')).toBeTruthy();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('hides statements and skips requests while retaining editable consent submission', async () => {
|
|
137
|
+
const { login } = renderAccount({ showProfileStatements: false });
|
|
138
|
+
|
|
139
|
+
expect(screen.queryByText('Privacy statement')).toBeNull();
|
|
140
|
+
expect(screen.queryByText('Cookie statement')).toBeNull();
|
|
141
|
+
expect(fetchPrivacyStatement).not.toHaveBeenCalled();
|
|
142
|
+
expect(fetchCookieStatement).not.toHaveBeenCalled();
|
|
143
|
+
|
|
144
|
+
const privacyConsent = screen.getByRole('checkbox', { name: 'Accept privacy statement' });
|
|
145
|
+
const cookieConsent = screen.getByRole('checkbox', { name: 'Accept convenience cookies' });
|
|
146
|
+
expect(privacyConsent.checked).toBe(false);
|
|
147
|
+
expect(cookieConsent.checked).toBe(true);
|
|
148
|
+
fireEvent.click(privacyConsent);
|
|
149
|
+
fireEvent.click(cookieConsent);
|
|
150
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(updateUserProfile).toHaveBeenCalledWith({
|
|
154
|
+
first_name: 'Pat',
|
|
155
|
+
last_name: 'Example',
|
|
156
|
+
accepted_privacy_statement: true,
|
|
157
|
+
accepted_convenience_cookies: false,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
expect(login).toHaveBeenCalledOnce();
|
|
161
|
+
});
|
|
162
|
+
});
|