Skip to content

(SP: 2) [Frontend] Q&A Empty State UX Refresh#352

Merged
ViktorSvertoka merged 2 commits into
developfrom
feat/qa-empty-state-polish
Feb 22, 2026
Merged

(SP: 2) [Frontend] Q&A Empty State UX Refresh#352
ViktorSvertoka merged 2 commits into
developfrom
feat/qa-empty-state-polish

Conversation

@ViktorSvertoka
Copy link
Copy Markdown
Member

@ViktorSvertoka ViktorSvertoka commented Feb 22, 2026

  • Updated the Q&A empty state message to a clearer 3-line format.
  • Added localized copy for all supported languages (en, uk, pl).
  • Implemented staggered fade-up animation for empty-state lines with Tailwind/CSS utility classes.
  • Added motion-safe/motion-reduce behavior for accessibility.
  • Kept styling lightweight and consistent with the current Next.js + Tailwind visual language.
  • Preserved and validated related behavior with targeted Q&A component tests.

Closes #351

Summary by CodeRabbit

  • New Features

    • Multi-line empty state messaging for Q&A with localized text.
    • Fade-up animation utility with configurable delays.
  • Improvements

    • Gradient backgrounds and consistent accent fallbacks for accordion items.
    • Better handling of reduced-motion preferences to disable animations.
    • Accessibility tweaks: explicit button type and improved icon semantics.
  • Tests

    • Test setup adjusted to ensure non-empty Q&A state during one spec.

@ViktorSvertoka ViktorSvertoka self-assigned this Feb 22, 2026
@ViktorSvertoka ViktorSvertoka added the enhancement New feature or request label Feb 22, 2026
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devlovers-net Ready Ready Preview, Comment Feb 22, 2026 1:03am

@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 22, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit a2c9702
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/699a55664acc9e0008791978
😎 Deploy Preview https://deploy-preview-352--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Replace single-line Q&A empty state with localized multi-line copy and staggered fade-up animations; add CSS variables for per-item accents, motion-reduce accessibility classes, small accessibility fixes, and helper utilities for color/term normalization.

Changes

Cohort / File(s) Summary
CSS & Animations
frontend/app/globals.css
Added .qa-accordion-item base styles and gradient background (with fallback), introduced @keyframes fade-up, .animate-fade-up, delay utilities (.delay-150, .delay-300), removed gradient on hover/focus/open, and respected reduced-motion preferences.
Accordion & Button Components
frontend/components/q&a/AccordionList.tsx, frontend/components/ui/accordion.tsx, frontend/components/q&a/FloatingExplainButton.tsx
Always expose --qa-accent and --qa-accent-soft CSS vars (computed via new hexToRgba), add normalizeCachedTerm, apply motion-reduce utility classes, add aria-hidden="true" to chevron, and set type="button" on floating button.
Empty State & Tests
frontend/components/q&a/QaSection.tsx, frontend/components/tests/q&a/qa-section.test.tsx
Replace single-line "no questions" text with memoized emptyStateLines rendering up to three animated lines (staggered); test updated to set a non-empty items array for the affected test.
Localization
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Replace qa.noResults single-line messages with multi-line localized strings and ensure trailing newlines at EOF.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

UI

Suggested reviewers

  • AM1007

Poem

🐰✨ The empty state now hops into view,
Three soft lines revealed with a fade-up cue.
Colors settled in vars, motion kept kind,
Small accessible tweaks for humankind.
Hooray — the Q&A looks fresh and new!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main change: refreshing the Q&A empty state UX, which aligns with the file modifications across CSS, TypeScript components, and localization files.
Linked Issues check ✅ Passed All objectives from issue #351 are met: generic message replaced with localized 3-line copy (en, uk, pl), staggered fade-up animation implemented, motion-reduce preferences respected, and changes validated with tests.
Out of Scope Changes check ✅ Passed All changes directly support the Q&A empty state UX refresh objective. CSS animations, component styling, accessibility improvements, and test updates are all scoped to the linked issue #351.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/qa-empty-state-polish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/components/tests/q&a/qa-section.test.tsx (1)

10-19: ⚠️ Potential issue | 🟡 Minor

Shared mutable qaState without beforeEach reset causes fragile test isolation.

After test 2 sets qaState.items = [{ id: 'q1' }], test 1 ("renders empty state when no questions") will see a non-empty items array if test order ever changes (e.g., shuffle mode, CI parallelism, future reordering), causing screen.getAllByText('noQuestions') to find nothing and the assertion to throw.

🛡️ Proposed fix — reset shared state in `beforeEach`
+import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { describe, expect, it, vi } from 'vitest';

 describe('QaSection', () => {
+  beforeEach(() => {
+    qaState.items = [];
+    qaState.totalPages = 0;
+  });
+
   it('renders empty state when no questions', () => {
-    qaState.totalPages = 0;
     render(<QaSection />);
     expect(screen.getAllByText('noQuestions').length).toBeGreaterThan(0);
   });

   it('renders category tabs and pagination', () => {
     qaState.totalPages = 3;
     qaState.items = [{ id: 'q1' }];

Also applies to: 63-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/tests/q`&a/qa-section.test.tsx around lines 10 - 19, The
shared mutable qaState object in qa-section.test.tsx causes test-order-dependent
failures; update the test file to reset or recreate qaState before each test
(e.g., in a beforeEach block) so each test gets a fresh object instead of
mutating the shared qaState; specifically reference the qaState constant used by
the tests and ensure functions like handleCategoryChange and handlePageChange
remain mocked (vi.fn()) while items, active, currentPage, isLoading, localeKey,
and totalPages are reinitialized for every test to restore isolation.
🧹 Nitpick comments (4)
frontend/app/globals.css (3)

232-238: .delay-150 / .delay-300 class names conflict with Tailwind's transition-delay utilities.

Tailwind generates .delay-150 { transition-delay: 150ms } and .delay-300 { transition-delay: 300ms }. Your custom rules set animation-delay using the same class names, so whichever stylesheet wins the cascade determines the property applied — this is ambiguous and fragile.

Consider renaming the custom animation-delay classes to avoid the collision, e.g. .anim-delay-150 / .anim-delay-300, and update their usage in QaSection.tsx accordingly (though see the separate comment on motion-safe:delay-* — the recommended fix there uses Tailwind arbitrary values and avoids these classes entirely).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/globals.css` around lines 232 - 238, The custom CSS classes
.delay-150 and .delay-300 conflict with Tailwind's transition-delay utilities;
rename them to avoid the collision (e.g. .anim-delay-150 and .anim-delay-300) in
globals.css and update all usages in components such as QaSection.tsx to the new
class names, or alternatively replace those usages in QaSection.tsx with
Tailwind arbitrary animation-delay utilities (e.g.
motion-safe:animate-[animation-name] and motion-safe:delay-[150ms]) to eliminate
the need for custom classes.

183-204: Merge the two interaction-state rule blocks into one.

.qa-accordion-item:hover, .qa-accordion-item:focus-within, .qa-accordion-item[data-state='open'] is declared twice — once for border-color (Line 183) and again for background-image: none (Line 200). These should be a single rule block.

♻️ Proposed refactor
-.qa-accordion-item:hover,
-.qa-accordion-item:focus-within,
-.qa-accordion-item[data-state='open'] {
-  border-color: var(--qa-accent, var(--accent-primary));
-}
-
 .qa-accordion-item {
   position: relative;
   overflow: hidden;
   background-image: linear-gradient(
     90deg,
     transparent 0%,
     transparent 54%,
     var(--qa-accent-soft, rgba(161, 161, 170, 0.22)) 100%
   );
 }
 
 .qa-accordion-item:hover,
 .qa-accordion-item:focus-within,
 .qa-accordion-item[data-state='open'] {
+  border-color: var(--qa-accent, var(--accent-primary));
   background-image: none;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/globals.css` around lines 183 - 204, The two identical selector
groups ".qa-accordion-item:hover, .qa-accordion-item:focus-within,
.qa-accordion-item[data-state='open']" are duplicated; merge them into a single
rule that sets both border-color and background-image: none (keeping the
existing values: border-color: var(--qa-accent, var(--accent-primary)) and
background-image: none) and remove the redundant block so the interaction-state
styles for .qa-accordion-item are defined once.

215-238: .animate-fade-up is not guarded in the existing @media (prefers-reduced-motion: reduce) block.

The existing block at Line 572 suppresses animate-float, animate-spin-slow, etc., but not .animate-fade-up. This class sets opacity: 0 as its initial state — any future direct use without the motion-safe: variant would leave content invisible for reduced-motion users (animation doesn't run, opacity stays 0).

🛡️ Recommended addition to existing reduced-motion block
 `@media` (prefers-reduced-motion: reduce) {
   .animate-float,
   .animate-spin-slow,
   .animate-spin-slower,
-  .animate-dash-flow {
+  .animate-dash-flow,
+  .animate-fade-up {
     animation: none !important;
   }
+
+  .animate-fade-up {
+    opacity: 1;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/globals.css` around lines 215 - 238, The .animate-fade-up class
currently sets opacity: 0 and its animation isn't disabled inside the existing
`@media` (prefers-reduced-motion: reduce) block, so add a rule in that media block
to neutralize it (e.g., remove animation and set opacity: 1) so reduced-motion
users won't remain invisible; specifically target .animate-fade-up (and
optionally .delay-150/.delay-300 if you want to clear animation-delay) within
the `@media` (prefers-reduced-motion: reduce) block to set animation: none
!important and opacity: 1 !important.
frontend/components/q&a/AccordionList.tsx (1)

43-50: hexToRgba silently falls back to black for 3-char shorthand hex

#FFFnormalized = "FFF", length 3 ≠ 6, returns rgba(0,0,0,α). All current categoryTabStyles accent values are 6-char, so this is benign today. If the accent source ever includes shorthand values, the fallback is visually surprising (solid black).

♻️ Proposed optional guard
 function hexToRgba(hex: string, alpha: number): string {
-  const normalized = hex.replace('#', '');
-  if (normalized.length !== 6) return `rgba(0, 0, 0, ${alpha})`;
+  let normalized = hex.replace('#', '');
+  if (normalized.length === 3) {
+    normalized = normalized.split('').map(c => c + c).join('');
+  }
+  if (normalized.length !== 6) return `rgba(0, 0, 0, ${alpha})`;
   const r = parseInt(normalized.slice(0, 2), 16);
   const g = parseInt(normalized.slice(2, 4), 16);
   const b = parseInt(normalized.slice(4, 6), 16);
+  if (isNaN(r) || isNaN(g) || isNaN(b)) return `rgba(0, 0, 0, ${alpha})`;
   return `rgba(${r}, ${g}, ${b}, ${alpha})`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/q`&a/AccordionList.tsx around lines 43 - 50, The
hexToRgba function currently treats any non-6-char hex as invalid and silently
returns black; update hexToRgba to recognize 3-char shorthand hex (when
normalized.length === 3) by expanding each nibble to two characters (e.g., "abc"
→ "aabbcc") before parsing, keep the existing fallback for other invalid inputs,
and ensure you operate on the existing normalized variable inside hexToRgba so
categoryTabStyles accent values using shorthand will render correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/components/q`&a/AccordionList.tsx:
- Line 410: The class string on the accordion item (className in
AccordionList.tsx for the "qa-accordion-item" element) currently includes
"motion-reduce:transition-none" which disables all transitions including the
functional "transition-colors"; remove the "motion-reduce:transition-none" token
and instead opt into the entrance animation only when motion is allowed by using
the motion-safe/ motion-reduce pattern (e.g., use motion-safe:animate-in /
motion-reduce:animate-none or similar) so that "transition-colors" remains
active for reduced-motion users while the entrance animation is suppressed for
them.
- Around line 400-405: The inline style object itemStyle in AccordionList.tsx
uses CSS custom properties like '--qa-accent' which TypeScript's
React.CSSProperties doesn't accept in strict mode; fix it by changing the object
to use a type assertion (matching Pagination.tsx) e.g. create the object with
animationDelay, animationFillMode, '--qa-accent' and '--qa-accent-soft' and
append "as React.CSSProperties" to the itemStyle declaration (or alternatively
add the module augmentation for CSS custom properties in a global css.d.ts if
you prefer a project-wide fix).

In `@frontend/components/q`&a/QaSection.tsx:
- Around line 127-134: The stagger is broken because the classes
motion-safe:delay-150 and motion-safe:delay-300 set transition-delay instead of
animation-delay; update the two paragraph elements that render
emptyStateLines[1] and emptyStateLines[2] in QaSection.tsx to use Tailwind
arbitrary animation-delay (e.g., motion-safe:[animation-delay:0.15s] and
motion-safe:[animation-delay:0.3s]) alongside motion-safe:animate-fade-up so the
CSS rule targets animation-delay instead of transition-delay and preserves the
staggered fade-up timing.
- Around line 127-134: The second and third empty-state <p> elements (rendering
emptyStateLines[1] and emptyStateLines[2] inside the QaSection component) lack
dark-mode color variants; update those elements to include matching dark: text
color classes (e.g., add dark:text-gray-300 or dark:text-white to the className
for the emptyStateLines[1] and emptyStateLines[2] <p> nodes) to ensure
consistent and accessible contrast in dark theme.

In `@frontend/components/ui/accordion.tsx`:
- Line 61: The motion-reduce utility is losing to the data-state animation
selectors due to specificity; update the class string on the Accordion content
element (the one using data-[state=open]:animate-accordion-down and
data-[state=closed]:animate-accordion-up) to use Tailwind's important modifier
so reduced-motion sets animation:none with higher priority—replace
motion-reduce:animate-none with motion-reduce:!animate-none so the media-query
rule becomes animation: none !important and suppresses the accordion animations
for reduced-motion users.

---

Outside diff comments:
In `@frontend/components/tests/q`&a/qa-section.test.tsx:
- Around line 10-19: The shared mutable qaState object in qa-section.test.tsx
causes test-order-dependent failures; update the test file to reset or recreate
qaState before each test (e.g., in a beforeEach block) so each test gets a fresh
object instead of mutating the shared qaState; specifically reference the
qaState constant used by the tests and ensure functions like
handleCategoryChange and handlePageChange remain mocked (vi.fn()) while items,
active, currentPage, isLoading, localeKey, and totalPages are reinitialized for
every test to restore isolation.

---

Nitpick comments:
In `@frontend/app/globals.css`:
- Around line 232-238: The custom CSS classes .delay-150 and .delay-300 conflict
with Tailwind's transition-delay utilities; rename them to avoid the collision
(e.g. .anim-delay-150 and .anim-delay-300) in globals.css and update all usages
in components such as QaSection.tsx to the new class names, or alternatively
replace those usages in QaSection.tsx with Tailwind arbitrary animation-delay
utilities (e.g. motion-safe:animate-[animation-name] and
motion-safe:delay-[150ms]) to eliminate the need for custom classes.
- Around line 183-204: The two identical selector groups
".qa-accordion-item:hover, .qa-accordion-item:focus-within,
.qa-accordion-item[data-state='open']" are duplicated; merge them into a single
rule that sets both border-color and background-image: none (keeping the
existing values: border-color: var(--qa-accent, var(--accent-primary)) and
background-image: none) and remove the redundant block so the interaction-state
styles for .qa-accordion-item are defined once.
- Around line 215-238: The .animate-fade-up class currently sets opacity: 0 and
its animation isn't disabled inside the existing `@media` (prefers-reduced-motion:
reduce) block, so add a rule in that media block to neutralize it (e.g., remove
animation and set opacity: 1) so reduced-motion users won't remain invisible;
specifically target .animate-fade-up (and optionally .delay-150/.delay-300 if
you want to clear animation-delay) within the `@media` (prefers-reduced-motion:
reduce) block to set animation: none !important and opacity: 1 !important.

In `@frontend/components/q`&a/AccordionList.tsx:
- Around line 43-50: The hexToRgba function currently treats any non-6-char hex
as invalid and silently returns black; update hexToRgba to recognize 3-char
shorthand hex (when normalized.length === 3) by expanding each nibble to two
characters (e.g., "abc" → "aabbcc") before parsing, keep the existing fallback
for other invalid inputs, and ensure you operate on the existing normalized
variable inside hexToRgba so categoryTabStyles accent values using shorthand
will render correctly.

Comment thread frontend/components/q&amp;a/AccordionList.tsx Outdated
Comment thread frontend/components/q&amp;a/AccordionList.tsx Outdated
Comment thread frontend/components/q&amp;a/QaSection.tsx Outdated
Comment thread frontend/components/ui/accordion.tsx Outdated
@ViktorSvertoka ViktorSvertoka merged commit c4bd6f8 into develop Feb 22, 2026
10 of 11 checks passed
@ViktorSvertoka ViktorSvertoka deleted the feat/qa-empty-state-polish branch February 22, 2026 01:06
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
frontend/components/q&a/AccordionList.tsx (1)

48-55: hexToRgba has no NaN guard after parseInt.

If normalized is 6 characters but contains non-hex characters (e.g. a malformed accent string), parseInt returns NaN and the output is rgba(NaN, NaN, NaN, 0.22) — an invalid CSS value that browsers silently ignore, causing the soft accent to disappear. The current callers always pass valid 6-digit hex, so this is theoretical, but the function silently produces broken output instead of falling back.

♻️ Proposed fix
 function hexToRgba(hex: string, alpha: number): string {
   const normalized = hex.replace('#', '');
   if (normalized.length !== 6) return `rgba(0, 0, 0, ${alpha})`;
   const r = parseInt(normalized.slice(0, 2), 16);
   const g = parseInt(normalized.slice(2, 4), 16);
   const b = parseInt(normalized.slice(4, 6), 16);
+  if (isNaN(r) || isNaN(g) || isNaN(b)) return `rgba(0, 0, 0, ${alpha})`;
   return `rgba(${r}, ${g}, ${b}, ${alpha})`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/q`&a/AccordionList.tsx around lines 48 - 55, The
hexToRgba function can produce NaN if normalized contains non-hex chars; update
hexToRgba to validate the parsed r, g, b (use Number.isNaN or isFinite) after
parseInt and, if any channel is invalid, return a safe fallback like `rgba(0, 0,
0, ${alpha})` (or another defined default) instead of composing an invalid CSS
string; adjust the logic inside hexToRgba so it still checks normalized length
and then validates r, g, b and returns the fallback on any parse failure.
frontend/components/ui/accordion.tsx (1)

46-47: Optional: transition-transform on the chevron is not guarded by motion-reduce.

The [&[data-state=open]>svg]:rotate-180 rotation (Line 38) is driven by transition-transform duration-200 on the icon. Reduced-motion users will still see the animated flip. Since the motion-reduce work elsewhere in this PR is thorough, consider scoping the transition:

♻️ Proposed fix
-          className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
+          className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 motion-safe:transition-transform motion-safe:duration-200"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/ui/accordion.tsx` around lines 46 - 47, The chevron SVG
in accordion.tsx currently has "transition-transform duration-200" which
animates for reduced-motion users; update the chevron's className (the SVG
element with "text-muted-foreground pointer-events-none size-4 ...") to include
the motion-reduce utility so transitions are disabled when users prefer reduced
motion (e.g., add the motion-reduce:transition-none or prefer-reduced-motion
equivalent alongside the existing transition classes) so the
[&[data-state=open]>svg]:rotate-180 still applies but the flip animation is
suppressed for reduced-motion users.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/components/q`&a/QaSection.tsx:
- Around line 120-136: The empty-state container in QaSection (the div rendering
emptyStateLines) is not announced to screen readers; add role="status" to that
div (which implies aria-live="polite") so dynamic empty-state text
(emptyStateLines[0..2]) inside the QaSection component is announced when it
appears; update the div that contains the three <p> elements to include
role="status" and ensure no other conflicting ARIA live attributes are present.
- Around line 120-136: The empty-state container with className "py-20
text-center" reuses the same DOM node so the staggered animate-fade-up sequence
(applied to the three paragraphs rendered from emptyStateLines) only runs on
initial mount and not on subsequent tab/page changes; add a key prop (e.g.
key={animationKey}) to that outer div and derive animationKey from the thing
that changes between views (selected tab id, page id, or a combination with
isLoading) so the container remounts whenever the tab/page changes and the
animation/stagger restarts.

---

Duplicate comments:
In `@frontend/components/ui/accordion.tsx`:
- Line 61: The CSS cascade for the Accordion's className was losing the
motion-reduce override because Tailwind's motion-reduce:animate-none lacked the
important modifier; update the Accordion component's className that contains
data-[state=closed]:animate-accordion-up and
data-[state=open]:animate-accordion-down to use motion-reduce:!animate-none
(i.e., add the `!` important modifier) so the animation: none rule from
motion-reduce always overrides the data-state animation rules.

---

Nitpick comments:
In `@frontend/components/q`&a/AccordionList.tsx:
- Around line 48-55: The hexToRgba function can produce NaN if normalized
contains non-hex chars; update hexToRgba to validate the parsed r, g, b (use
Number.isNaN or isFinite) after parseInt and, if any channel is invalid, return
a safe fallback like `rgba(0, 0, 0, ${alpha})` (or another defined default)
instead of composing an invalid CSS string; adjust the logic inside hexToRgba so
it still checks normalized length and then validates r, g, b and returns the
fallback on any parse failure.

In `@frontend/components/ui/accordion.tsx`:
- Around line 46-47: The chevron SVG in accordion.tsx currently has
"transition-transform duration-200" which animates for reduced-motion users;
update the chevron's className (the SVG element with "text-muted-foreground
pointer-events-none size-4 ...") to include the motion-reduce utility so
transitions are disabled when users prefer reduced motion (e.g., add the
motion-reduce:transition-none or prefer-reduced-motion equivalent alongside the
existing transition classes) so the [&[data-state=open]>svg]:rotate-180 still
applies but the flip animation is suppressed for reduced-motion users.

Comment on lines +120 to +136
<div className="py-20 text-center">
{emptyStateLines[0] && (
<p className="text-lg font-semibold text-gray-900 motion-safe:animate-fade-up motion-reduce:opacity-100 dark:text-white">
{emptyStateLines[0]}
</p>
)}
{emptyStateLines[1] && (
<p className="mt-2 text-gray-400 motion-safe:animate-fade-up motion-safe:[animation-delay:150ms] motion-reduce:opacity-100 dark:text-gray-300">
{emptyStateLines[1]}
</p>
)}
{emptyStateLines[2] && (
<p className="mt-1 text-gray-500 motion-safe:animate-fade-up motion-safe:[animation-delay:300ms] motion-reduce:opacity-100 dark:text-gray-400">
{emptyStateLines[2]}
</p>
)}
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing role="status" — empty-state appearance is not announced to screen readers.

The empty state appears dynamically inside a tab panel when results are absent. Without a live region, screen readers will not announce the content change. Adding role="status" (implicit aria-live="polite") is the minimal, non-disruptive fix.

♿ Proposed fix
-              <div className="py-20 text-center">
+              <div role="status" className="py-20 text-center">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="py-20 text-center">
{emptyStateLines[0] && (
<p className="text-lg font-semibold text-gray-900 motion-safe:animate-fade-up motion-reduce:opacity-100 dark:text-white">
{emptyStateLines[0]}
</p>
)}
{emptyStateLines[1] && (
<p className="mt-2 text-gray-400 motion-safe:animate-fade-up motion-safe:[animation-delay:150ms] motion-reduce:opacity-100 dark:text-gray-300">
{emptyStateLines[1]}
</p>
)}
{emptyStateLines[2] && (
<p className="mt-1 text-gray-500 motion-safe:animate-fade-up motion-safe:[animation-delay:300ms] motion-reduce:opacity-100 dark:text-gray-400">
{emptyStateLines[2]}
</p>
)}
</div>
<div role="status" className="py-20 text-center">
{emptyStateLines[0] && (
<p className="text-lg font-semibold text-gray-900 motion-safe:animate-fade-up motion-reduce:opacity-100 dark:text-white">
{emptyStateLines[0]}
</p>
)}
{emptyStateLines[1] && (
<p className="mt-2 text-gray-400 motion-safe:animate-fade-up motion-safe:[animation-delay:150ms] motion-reduce:opacity-100 dark:text-gray-300">
{emptyStateLines[1]}
</p>
)}
{emptyStateLines[2] && (
<p className="mt-1 text-gray-500 motion-safe:animate-fade-up motion-safe:[animation-delay:300ms] motion-reduce:opacity-100 dark:text-gray-400">
{emptyStateLines[2]}
</p>
)}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/q`&a/QaSection.tsx around lines 120 - 136, The
empty-state container in QaSection (the div rendering emptyStateLines) is not
announced to screen readers; add role="status" to that div (which implies
aria-live="polite") so dynamic empty-state text (emptyStateLines[0..2]) inside
the QaSection component is announced when it appears; update the div that
contains the three <p> elements to include role="status" and ensure no other
conflicting ARIA live attributes are present.

⚠️ Potential issue | 🟡 Minor

Previous animation-delay and dark-mode issues are resolved; stagger will fire silently during loading.

The motion-safe:[animation-delay:150ms/300ms] arbitrary-property syntax is valid Tailwind v3/v4 and correctly targets animation-delay inside the prefers-reduced-motion: no-preference media query. dark:text-gray-300 / dark:text-gray-400 are also in place — both previous review items are closed.

However, the empty state <div> has no key. When the user switches between tabs that all return empty results, React reuses the same DOM node and never re-mounts it. The three animate-fade-up animations fire once at initial mount — while the outer container is still opacity-0 (isLoading === true). By the time loading completes and the container fades back in, the stagger sequence has already expired and the user sees all three lines appear at once at full opacity.

Adding key={animationKey} forces a re-mount whenever the tab or page changes, restarting the stagger on every transition to an empty state.

🐛 Proposed fix — add key to the empty state container
-              <div className="py-20 text-center">
+              <div key={animationKey} className="py-20 text-center">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/components/q`&a/QaSection.tsx around lines 120 - 136, The
empty-state container with className "py-20 text-center" reuses the same DOM
node so the staggered animate-fade-up sequence (applied to the three paragraphs
rendered from emptyStateLines) only runs on initial mount and not on subsequent
tab/page changes; add a key prop (e.g. key={animationKey}) to that outer div and
derive animationKey from the thing that changes between views (selected tab id,
page id, or a combination with isLoading) so the container remounts whenever the
tab/page changes and the animation/stagger restarts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant