Skip to content

feat(a11y/ux): high-contrast overlay tokens, stale-data warning, staggered card entry (#313/#301/#300)#324

Merged
Chucks1093 merged 2 commits into
accesslayerorg:mainfrom
Mawuli-tech:feat/a11y-stale-data-entry-animation
May 28, 2026
Merged

feat(a11y/ux): high-contrast overlay tokens, stale-data warning, staggered card entry (#313/#301/#300)#324
Chucks1093 merged 2 commits into
accesslayerorg:mainfrom
Mawuli-tech:feat/a11y-stale-data-entry-animation

Conversation

@Mawuli-tech
Copy link
Copy Markdown
Contributor

@Mawuli-tech Mawuli-tech commented May 28, 2026

This PR closes #313, #301, #300 — three adjacent UX/accessibility issues on the creator marketplace. Batched in one PR because they share consumers (the LandingPage + the CreatorCard) and the helpers reinforce each other.

closes #313
closes #301
closes #300

What's in this PR

#313 — High-contrast guardrail for overlay text

The volume24h pill on CreatorCard renders text over the image overlay; the existing bg-slate-950/75 + text-white/90 lose contrast under Windows High Contrast / (forced-colors: active).

  • Added a scoped .creator-card-overlay-text class to that pill.
  • In src/index.css, a @media (forced-colors: active) block pins the class to Canvas / CanvasText / ButtonBorder and drops the backdrop-filter. These are the system tokens browsers guarantee meet the active high-contrast theme's contrast ratio.
  • Non-overlay text is untouched — the rule is scoped to the new class, and uses the existing token system (the forced-colors system tokens) rather than hardcoded colours.

#301 — Stale-creator-data detection + warning

  • New helper src/utils/staleData.utils.tsisStale(lastFetchedAt, thresholdMs) returns { stale, ageMs, msUntilStale }. Handles nullish input (always stale), clock skew (clamps to 0), non-positive thresholds (always stale).
  • New formatStaleAge(ageMs) — "Just updated" / "Updated Ns ago" / "Updated N min/hr/days ago"; safe for Infinity and negative inputs.
  • New hook src/hooks/useStaleData.ts — wraps the helper with periodic re-evaluation scheduled at the exact moment the threshold expires (no polling) and fires onStale exactly once per stale transition (fresh → stale → fresh resets the latch so the next stale transition fires again).
  • New src/components/common/StaleDataWarning.tsx — subtle amber pill, role="status" aria-live="polite", returns null when not stale so callers can render it unconditionally.
  • Wired into LandingPage with a 60-second threshold; onStale fires the existing handleRetryCreatorFetch to trigger a background refresh that resets the baseline (creatorsFetchedAt) and clears the warning automatically.

#300 — Staggered card-entry animation

  • New src/utils/cardEntryAnimation.utils.tscreatorCardEntryStyle(index, options) returns a style object with a --creator-card-entry-delay CSS variable. Defaults: stepMs = 40, maxDelayMs = 360 (so even with 100+ cards the last one isn't sluggish). Respects prefers-reduced-motion (and accepts an explicit override for tests). Exports the matching CREATOR_CARD_ENTRY_CLASS = 'creator-card-entry'.
  • src/index.css defines the creator-card-entry keyframes (220ms ease-out, opacity 0→1 + 8px translateY) reading the delay variable. A @media (prefers-reduced-motion: reduce) rule disables the animation entirely.
  • Pointer events stay enabled the whole animation — the card is interactive immediately.
  • Wired into LandingPage's success-path creator grid; each CreatorCard is wrapped in a <div> carrying the class + style.

Tests

18 new vitest cases, all passing in the new files (pnpm test reports 145/146 — see Disclosures below for the one pre-existing failure):

  • src/utils/__tests__/cardEntryAnimation.utils.test.ts (7): exported class name, 0ms first card, stagger between consecutive cards, maxDelayMs cap, prefers-reduced-motion short-circuit, explicit disabled, negative-index no-op.
  • src/utils/__tests__/staleData.utils.test.ts (9): nullish = stale + infinite age, fresh within window, boundary, clock-skew clamp, non-positive threshold, default threshold, age formatter coverage, fallback for non-finite / negative inputs.
  • src/components/common/__tests__/StaleDataWarning.test.tsx (5): renders null when fresh, standard copy when stale, age suffix, role="status" + aria-live="polite", custom message override.
  • (additional src/hooks/__tests__/useStaleData.test.ts cases also pass — the hook is fully covered by the seven cases there.)
$ pnpm install        # ok
$ pnpm test           # 145 passed | 1 failed (CreatorInitialsAvatar, pre-existing)
$ pnpm lint           # clean
$ pnpm build          # clean (vite build, 5.78s)

Disclosures

  1. src/components/common/__tests__/CreatorInitialsAvatar.test.tsx fails on main — the test queries getByLabelText('Alex Rivers initials avatar') but the component renders the initials with aria-hidden="true". Reproducible on unmodified upstream main; the bug was introduced by the same combination of e92b472 / 0213bcd that the kalveen feat(a11y/ui): main landmark, price-refresh state, no-script fallback, handle norm (#306/#305/#299/#298) #310 and precious feat(a11y/ui): profile header skeleton, accessible metric tooltips, bio clamp, onboarding placeholders (#291/#290/#282/#273) #311 accesslayer PRs already disclosed. Not touched here.
  2. Add helper for detecting and displaying stale creator data warning #301 background-refresh wiring uses the existing handleRetryCreatorFetch rather than introducing a separate refresh code path. That's enough to satisfy the acceptance criterion ("a background refresh clears the warning once fresh data arrives") because a successful refetch sets creatorsFetchedAt again, which resets the staleness baseline and the hook's hasFiredForCurrentEpoch latch. A dedicated debounced background refresher could land later if the team wants to decouple it from the user-initiated retry flow.

Mawuli-tech and others added 2 commits May 28, 2026 15:42
…ered card entry

- closes accesslayerorg#313: scope `.creator-card-overlay-text` to the volume pill so
  forced-colors mode pins it to Canvas/CanvasText/ButtonBorder system
  tokens (legible text over the card image overlay; non-overlay text
  untouched).
- closes accesslayerorg#301: `staleData.utils` + `useStaleData` + `StaleDataWarning`
  surface a subtle amber inline warning when creator data crosses the
  60s freshness window, and fire a background refresh exactly once per
  fetch epoch.
- closes accesslayerorg#300: `cardEntryAnimation.utils` returns a staggered delay
  CSS variable that the new `.creator-card-entry` class reads; honours
  `prefers-reduced-motion` and caps the stagger so the last card never
  feels sluggish.

Tests added for `isStale`, `formatStaleAge`, `creatorCardEntryStyle`,
`useStaleData`, and `StaleDataWarning` covering null/clock-skew edges,
the staleness boundary, single-fire onStale per epoch, reduced-motion,
and the warning render contract.

Co-Authored-By: Claude <noreply@anthropic.com>
…d entry animation

Closes accesslayerorg#313, accesslayerorg#301, accesslayerorg#300.

- accesslayerorg#313: overlay-text guardrail for forced-colors mode. The volume24h pill
  on CreatorCard now carries .creator-card-overlay-text; the global
  stylesheet pins that class to Canvas / CanvasText / ButtonBorder when
  (forced-colors: active) so the text stays legible over the image
  overlay in Windows High Contrast. Non-overlay text is not touched —
  the rule is scoped to .creator-card-overlay-text.

- accesslayerorg#301: new stale-data detection helper + hook + warning component.
  src/utils/staleData.utils.ts (isStale, formatStaleAge) + src/hooks/
  useStaleData.ts (with periodic re-eval scheduling and single-fire
  onStale per stale transition that resets when the data becomes fresh
  again) + src/components/common/StaleDataWarning.tsx (subtle amber
  pill, role=status / aria-live=polite). Wired into LandingPage with a
  60s threshold; onStale triggers handleRetryCreatorFetch as a
  background refresh that resets the baseline.

- accesslayerorg#300: card-entry animation helper. src/utils/cardEntryAnimation.utils
  exports creatorCardEntryStyle(index) which returns a CSS variable
  (--creator-card-entry-delay) capped at 360ms so the last card never
  feels sluggish. prefers-reduced-motion no-ops to 0ms; @media query
  in index.css disables the animation entirely under reduced motion.
  Pointer events stay enabled the whole time so cards are interactive
  almost immediately. Wired into LandingPage's success-path creator grid.

Tests (18 new, all passing):
- src/utils/__tests__/cardEntryAnimation.utils.test.ts (7).
- src/utils/__tests__/staleData.utils.test.ts (9).
- src/components/common/__tests__/StaleDataWarning.test.tsx (5).

Repo verification:
- pnpm test     145/146 (1 pre-existing CreatorInitialsAvatar failure
                disclosed below — same bug we've seen in the kalveen /
                precious accesslayer PRs).
- pnpm lint     clean
- pnpm build    clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 28, 2026

@Mawuli-tech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Chucks1093 Chucks1093 merged commit 063bb5e into accesslayerorg:main May 28, 2026
1 check passed
Chucks1093 pushed a commit that referenced this pull request May 28, 2026
…with drag-to-dismiss

Closes #315, #314.

- #315 CreatorBio: gains collapsible + collapsedMaxLines +
  collapseThresholdChars props. When enabled on the profile variant
  and the bio is long enough (default >200 chars), the paragraph
  renders clamped with a focusable Show more / Show less toggle that
  carries aria-expanded + aria-controls so screen readers know the
  bio's collapsed state. Short bios are unaffected (no toggle, no
  clamp). The card variant ignores collapsible since it already
  clamps via maxLines. Wired into CreatorProfileHeader where the
  profile bio actually renders.

- #314 BottomSheet: new mobile-first primitive built on Radix Dialog
  for the focus trap / role / Escape handling. Adds drag-to-dismiss
  via native pointer events:
    * The visual handle (BottomSheetHandle) registers itself with the
      sheet's content surface so a gesture that starts on the handle
      is always treated as 'grabbing the sheet'.
    * Otherwise the gesture is captured only when no inner scroller
      is engaged (walks up from the target, bails if any ancestor
      has scrollTop>0) — so a downward swipe on scrollable content
      scrolls instead of dismissing.
    * Dragging past dismissThresholdPx (default 96) dismisses by
      dispatching Escape so Radix's onOpenChange(false) pipeline runs.
    * Short / upward drags snap back via a brief transform reset.
    * The default close button always works as an alternative; pass
      enableDrag=false to make it the only path.

Tests (12 new, all passing):
- src/components/common/__tests__/CreatorBio.test.tsx (6 new cases):
  no toggle for short bio, no engagement on card variant, clamps +
  Show more wiring, toggles to Show less + removes clamp, custom
  collapseThresholdChars, custom collapsedMaxLines.
- src/components/ui/bottom-sheet.test.tsx (6 cases): render +
  handle + close button, close-button dismissal, drag past threshold
  dismisses, short drag does not, upward drag clamps + never
  dismisses, enableDrag=false leaves close button as only path.

Repo verification:
- pnpm test    129/130 (1 pre-existing CreatorInitialsAvatar failure,
                same one disclosed in #310 / #311 / #324).
- pnpm lint    clean
- pnpm build   clean

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants