(SP: 2) [Frontend] Q&A Empty State UX Refresh#352
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughReplace 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorShared mutable
qaStatewithoutbeforeEachreset 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), causingscreen.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-300class names conflict with Tailwind'stransition-delayutilities.Tailwind generates
.delay-150 { transition-delay: 150ms }and.delay-300 { transition-delay: 300ms }. Your custom rules setanimation-delayusing 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 inQaSection.tsxaccordingly (though see the separate comment onmotion-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 forborder-color(Line 183) and again forbackground-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-upis 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 setsopacity: 0as its initial state — any future direct use without themotion-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:hexToRgbasilently falls back to black for 3-char shorthand hex
#FFF→normalized = "FFF", length 3 ≠ 6, returnsrgba(0,0,0,α). All currentcategoryTabStylesaccent 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
frontend/components/q&a/AccordionList.tsx (1)
48-55:hexToRgbahas no NaN guard afterparseInt.If
normalizedis 6 characters but contains non-hex characters (e.g. a malformed accent string),parseIntreturnsNaNand the output isrgba(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-transformon the chevron is not guarded by motion-reduce.The
[&[data-state=open]>svg]:rotate-180rotation (Line 38) is driven bytransition-transform duration-200on 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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
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.
Closes #351
Summary by CodeRabbit
New Features
Improvements
Tests