Skip to content

fix(uxcat): bias descriptions, modal scrolling, Apple Calendar .ics, pill offset - #165

Merged
MaryWylde merged 4 commits into
devfrom
fix/uxcat-bugs
Aug 3, 2026
Merged

fix(uxcat): bias descriptions, modal scrolling, Apple Calendar .ics, pill offset#165
MaryWylde merged 4 commits into
devfrom
fix/uxcat-bugs

Conversation

@MaryWylde

Copy link
Copy Markdown
Contributor

Four user-reported bugs on UXCAT / UX Core. One commit per fix, each reviewable on its own.

1. Bias explanation never loads during the test (d4171fa)

Reported as a geo/VPN issue (tested from RU, and via NL and DE VPNs) — it wasn't. uxCoreData was hardcoded to null in the UX Core context value, so every consumer of the shared bias list got an empty array, for everyone, on every network.

On the ongoing test the bias name comes from the test API and rendered fine; the description comes from context and sat on a skeleton forever, which is what made it look like a slow/blocked fetch.

Populated client-side from a slim Strapi query gated to /uxcat routes. The full bias payload is ~1.4 MB and these screens read only number, title, description, slug and mentionedQuestionsIds, so we select just those (~38 KB/page) and cache per module. mentionedQuestionsIds is not optional — test-result JSON.parses it to pick recommended reading and throws on null.

Same fix repairs QuestionAnalyse, which built its list in an effect with an empty dependency array and so never saw the data land.

2. "Our projects" modal doesn't scroll (1166ea1)

Reported on /ru; it's the shared UX Core modal, so en and hy had it too. The wrapper had no height ceiling, so on short viewports it grew past the screen and the centred overlay clipped it at both ends — header and footer buttons unreachable.

Capped the wrapper, stopped the header shrinking, and gave the body min-height: 0 — without that a flex child can't shrink below its content height and overflow: auto never engages. The mobile fullHeightMobile override still wins by source order.

3. Apple Calendar rejects the .ics (8b2cf72)

The file was missing PRODID, UID and DTSTAMP (all required by RFC 5545), used LF instead of CRLF, and wrote NaN for both dates — nextTestTime arrives as an epoch string and new Date('1758…') is an Invalid Date. Apple Calendar rejects it outright; more lenient clients were masking it.

Replaced react-icalendar-link with a small local builder: required properties, TEXT escaping per §3.3.11, URL left unescaped as a URI value, and folding at 75 octets rather than characters (Cyrillic copy overruns a character-based fold). UID derives from the start time, so re-downloading updates the event instead of stacking duplicates.

4. Copilot pill overlaps UX Core page chrome (1873535)

Lifted 70px on /uxcore (incl. bias pages), /uxcg, /uxcp, /uxcat and /uxcore-api, via a --ks-aux-lift custom property driven off the uxcorePage body class _app.tsx already sets for exactly those routes.

The margin goes on the widget root, not the pill: the chat panel is positioned against the root, so raising the pill alone would leave the panel anchored low and overlapping it. On mobile the panel height comes from --ks-aux-panel-h, and a JS-set custom property overrides a CSS fallback entirely — so the lift is subtracted there too, otherwise the panel keeps full height and runs off the top of short screens (measured panelTop: -39 at 360×640 before the fix).

Test plan

  • tsc --noEmit clean; ESLint clean on touched files (remaining warnings pre-date this branch)
  • Bias data — verified against staging: 105 items each for en/ru, zero index misalignment between locales (mergeBiasesLocalization pairs en[i] with ru[i], so this matters), no null descriptions
  • Modal — Playwright at 1280×620 across ru/en/hy. Before: wrapper spanned −110→731, 0 scroll, footer off-screen. After: 20→600, body scrolls, footer reachable
  • ICS — generated output asserted: all-CRLF, required properties present, no NaN, every line ≤75 octets, commas/semicolons/newlines escaped, URL unescaped; parseEventDate correct for epoch number, numeric string and ISO string, null for garbage
  • Pill — all 8 routes at 1280×900: 24px gap off-UX-Core, exactly 94px on. Panel rides along (identical 54px pill↔panel gap, unchanged 622px height). Mobile 390×844 and 360×640 both fully on screen after the fix
  • Confirm the 70px clearance is the right number against the real overlapping element

Note for the reviewer

react-icalendar-link is now unused but still in package.json. Left in deliberately: removing it needs npx --yes yarn@1.22.22 remove, because the system yarn is Berry 4 and rewrites the v1 lockfile into Berry format. Happy to drop it here or in a follow-up.

🤖 Generated with Claude Code

MaryWylde and others added 4 commits August 3, 2026 16:38
uxCoreData was hardcoded to null in the UX Core context value, so every
consumer of the shared bias list saw an empty array. On the ongoing test
the bias name comes from the test API and rendered fine, but the
description came from context and sat on a skeleton forever. Reported as
a geo/VPN issue; it affected everyone.

Populate it client-side from a slim Strapi query gated to /uxcat routes.
The full bias payload is ~1.4 MB and those screens only read number,
title, description, slug and mentionedQuestionsIds, so select just those
and cache per module. mentionedQuestionsIds is required: test-result
JSON.parses it to pick recommended reading and would throw on null.

Also fixes QuestionAnalyse, which built its list in an effect with an
empty dependency array and so never saw the data arrive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The wrapper had no height ceiling, so on short viewports it grew past
the screen and the centred overlay clipped it at both ends — the header
and the footer buttons were unreachable. Reported against the "Our
projects" modal on /ru, but it is the shared UX Core modal, so every
consumer had it.

Cap the wrapper, stop the header shrinking, and give the body
min-height:0 — without that the flex child cannot shrink below its
content height and overflow:auto never engages. The mobile
fullHeightMobile override still wins by source order.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The downloaded file was missing PRODID, UID and DTSTAMP — all required
by RFC 5545 — used LF instead of CRLF, and wrote NaN for the dates:
nextTestTime arrives as an epoch string and new Date('1758...') is an
Invalid Date, so the timestamps never rendered. Apple Calendar rejects
the file outright; more lenient clients had been hiding it.

Replace react-icalendar-link with a small local builder that emits the
required properties, escapes TEXT per 3.3.11, leaves URL unescaped since
it is a URI value, and folds lines at 75 octets rather than characters
(Cyrillic copy overruns a character-based fold). The UID is derived from
the start time so re-downloading updates the event instead of stacking
duplicates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Those pages keep their own controls in the bottom-right corner, where
the pill sits. Add clearance via a --ks-aux-lift custom property, set
from the uxcorePage body class that _app.tsx already applies to
/uxcore, /uxcg, /uxcp, /uxcat and /uxcore-api.

The margin goes on the root rather than the pill: the panel is
positioned against the root, so raising the pill alone would leave the
panel anchored low and overlapping it. On mobile the panel height comes
from --ks-aux-panel-h, and a JS-set custom property overrides the CSS
fallback entirely, so the lift has to be subtracted there too —
otherwise the panel keeps its full height and runs off the top of short
screens.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @MaryWylde's task in 4m 21s —— View job


Review complete

Todo list

  • Gather context (PR body, diff, changed files)
  • Review src/pages/_app.tsx — uxCoreData fetch, uxcorePage class, hydration safety
  • Review src/uxcore/api/biases.ts — slim Strapi query
  • Review src/uxcore/components/AddToCalendar + CalendarItems — date handling
  • Review src/uxcore/lib/ics.ts — new ICS builder (RFC 5545, escaping, folding)
  • Review src/uxcore/components/Modal/Modal.module.scss — scroll fix
  • Review src/uxcore/components/QuestionAnalyse/QuestionAnalyse.tsx — effect deps fix
  • Review widget/src/AskUxCore.tsx + styles.css — pill lift
  • Post inline comments for concrete issues
  • Summarize findings in tracking comment

Assessment

Well-diagnosed set of fixes — each commit correctly traces the bug to its root cause rather than papering over symptoms, and the PR description's reasoning checks out against the code. Highlights:

  • Bias data (d4171fa): confirmed uxCoreData: null was hardcoded in _app.tsx's context value pre-fix; the new fetch is properly gated to isUxcatRoute (/uxcat*), cancels on unmount, and only populates fields consumers actually read (mentionedQuestionsIds included, matching test-result.tsx's JSON.parse requirement). QuestionAnalyse's effect dependency array ([][uxCoreData]) is now correct — verified no stale-closure issues remain. All current consumers of uxCoreData (QuestionAnalyse, OngoingLayout, test-result.tsx) live under /uxcat, so the route gating doesn't strand anyone. (UXCoreModalMobile also reads a uxCoreData prop but it's dead code predating this branch — never rendered, not something this PR touches.)
  • Modal scroll (1166ea1): correct flexbox fix — max-height ceiling on the column wrapper, flex-shrink: 0 on the header, min-height: 0 + overflow: auto on the body. This is the right combination; min-height: 0 is exactly what's needed for a flex child to shrink below content size.
  • ICS builder (8b2cf72): solid RFC 5545 implementation — required properties present, CRLF line endings, backslash-escaped before comma/semicolon/newline (correct order), URL left unescaped as a URI value, byte-based (not character-based) folding for Cyrillic text, deterministic UID from start time. Two minor nits left inline (non-blocking): the fold threshold is 74 octets vs. the documented 75, and the pagination loop in getSlimBiases has no upper-bound guard the way getStrapiBiases does.
  • Pill lift (1873535): the CSS math checks out — margin-bottom on a position: fixed element with bottom set does shift the box up as intended, and the --ks-aux-lift custom property is correctly composed into both the JS-computed panel height (mobile, gated behind the existing isMobile/open check) and the CSS fallback calc() (desktop). Reuses the pre-existing uxcorePage body class rather than duplicating route-matching logic.

No violations of AGENTS.md conventions found — import order, barrel files, SCSS modules, no App Router/Tailwind/new state libs, no hydration hazards (all window/document access in AskUxCore.tsx is inside effects with typeof window === 'undefined' guards). No changes to UX Core bias data itself (names/slugs/citations) — this only changes what fields are fetched, so no approval gate applies there.

Two minor, non-blocking suggestions left as inline comments (a defensive bound on getSlimBiases's pagination loop, and a comment/constant mismatch in the ICS folding logic). Did not run tsc/eslint/Playwright myself (no node_modules in this checkout and installing was out of scope for a review pass) — relying on the PR's own stated test plan for that coverage, which looks thorough (per-locale index alignment, ICS byte-length assertions, Playwright viewport checks).

Comment thread src/uxcore/api/biases.ts

for (const locale of SLIM_LOCALES) {
let page = 1;
while (true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor robustness nit: this while (true) only terminates on an empty page or a short page (json.data.length < PAGE_SIZE). getStrapiBiases below guards the same kind of loop with fetched < TOTAL_ITEMS_EXPECTED so a Strapi pagination bug (e.g. always returning exactly 100 items) can't spin forever. Worth adding an equivalent upper bound here too, since this runs client-side on every /uxcat page load.

Comment thread src/uxcore/lib/ics.ts
const CRLF = '\r\n';
const PRODID = '-//KeepSimple//UXCAT//EN';
const DEFAULT_DURATION_MS = 30 * 60 * 1000;
const MAX_LINE_OCTETS = 74;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small mismatch: the comment on foldLine (and the PR description) says folding happens "at 75 octets," but MAX_LINE_OCTETS is 74. Not a bug — it's on the conservative side (first physical line could actually hold 75 per RFC 5545 §3.1, only continuation lines need the -1 for the leading space) — but worth a one-line comment explaining the off-by-one is intentional, or bumping the first line to 75, so a future reader doesn't "fix" it into a real bug.

@MaryWylde
MaryWylde merged commit c94b426 into dev Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant