Skip to content

refactor: replace framer motion with css animations for hero-ui v3 migration#430

Merged
domhhv merged 2 commits intomainfrom
refactor/replace-framer-with-css-animations
Apr 1, 2026
Merged

refactor: replace framer motion with css animations for hero-ui v3 migration#430
domhhv merged 2 commits intomainfrom
refactor/replace-framer-with-css-animations

Conversation

@domhhv
Copy link
Copy Markdown
Owner

@domhhv domhhv commented Mar 31, 2026

Description

Remove framer-motion (motion/AnimatePresence) usage across several UI components and replace animated wrappers with CSS utility classes and simple layout toggles. Updated components: CalendarFilters, DayCalendar, MonthCalendarCell, MonthCalendarGrid, WeekCalendar, NotePeriodPicker and common/SwipeableContainer — imports and motion variants/props (e.g. direction, swipeKey, slideVariants) were removed and replaced with class-based transitions (animate-in, fade-in, transition-all, grid row toggles, overflow-hidden). Also added cn imports where needed for class composition. Additionally updated .claude/settings.json to include the new "mcp__heroui-migration__get_migration_workflow" hook.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring
  • Performance improvement
  • Test addition or update
  • Build process or tooling change
  • Code style or formatting change
  • Other (please describe)

Checklist

  • My code follows the project's style guidelines
  • I've tested my changes locally
  • I've updated documentation if needed

Summary by CodeRabbit

  • Refactor

    • Removed lifecycle enter/exit animations across calendar views, period picker, and swipe container for more stable, higher-performance rendering and consistent show/hide behavior
    • Adjusted header quick-action buttons for improved responsive sizing and simplified keyboard hint display
  • Chores

    • Extended system permissions configuration

@domhhv domhhv self-assigned this Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Replaced framer-motion AnimatePresence/motion usage with plain DOM rendering and class-based show/hide (Tailwind) across calendar, note, header, and common components; removed related props from SwipeableContainer; added one MCP permission in .claude/settings.json.

Changes

Cohort / File(s) Summary
Settings & Permissions
.claude/settings.json
Added mcp__heroui-migration__get_migration_workflow to the MCP permissions allowlist.
Calendar components
src/components/calendar/CalendarFilters.tsx, src/components/calendar/DayCalendar.tsx, src/components/calendar/MonthCalendarCell.tsx, src/components/calendar/MonthCalendarGrid.tsx, src/components/calendar/WeekCalendar.tsx
Removed framer-motion (AnimatePresence/motion.div) wrappers; switched to plain div renderings and class-based visibility toggles (e.g., hidden/flex). Keyed animation lifecycle and motion props were removed.
Note component
src/components/note/NotePeriodPicker.tsx
Replaced AnimatePresence/motion height+opacity animations with a static container controlled via Tailwind class toggles (hidden/flex).
Common component API
src/components/common/SwipeableContainer.tsx
Removed framer-motion variants and wrapper; eliminated direction and swipeKey props from the component API and render children directly.
Header UI tweaks
src/components/header/Header.tsx
Refactored Note/Log quick-action buttons: removed extra wrapper divs, adjusted button props (color, fullWidth, radius) responsive to screen width, and simplified Kbd rendering (key-only).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibble at frames that used to glide,

I tuck animations gently inside.
Plain divs now hop where motion once played,
Permissions added, the migration’s laid.
Hooray — a tidy trail my paws have made! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: removing framer-motion and replacing it with CSS animations as part of a HeroUI v3 migration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/replace-framer-with-css-animations

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.

@relativeci
Copy link
Copy Markdown

relativeci bot commented Mar 31, 2026

#317 Bundle Size — 2.08MiB (-0.07%).

b705687(current) vs 43e577c main#315(baseline)

Warning

Bundle contains 2 duplicate packages – View duplicate packages

Bundle metrics  Change 2 changes Improvement 1 improvement
                 Current
#317
     Baseline
#315
Improvement  Initial JS 1.67MiB(-0.08%) 1.68MiB
No change  Initial CSS 0B 0B
Change  Cache Invalidation 7.45% 36.98%
No change  Chunks 11 11
No change  Assets 12 12
No change  Modules 6752 6752
No change  Duplicate Modules 0 0
No change  Duplicate Code 0% 0%
No change  Packages 228 228
No change  Duplicate Packages 2 2
Bundle size by type  Change 1 change Improvement 1 improvement
                 Current
#317
     Baseline
#315
Improvement  JS 1.84MiB (-0.07%) 1.84MiB
No change  CSS 248.48KiB 248.48KiB

Bundle analysis reportBranch refactor/replace-framer-with-css...Project dashboard


Generated by RelativeCIDocumentationReport issue

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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/components/common/SwipeableContainer.tsx (1)

4-18: ⚠️ Potential issue | 🔴 Critical

Props direction and swipeKey are declared but never used.

The type definition (lines 7-8) still includes direction and swipeKey as required props, but the component destructuring (lines 13-17) omits them entirely. This creates:

  1. Dead code in the type definition
  2. A misleading API where callers must provide values that are silently ignored
  3. Orphaned state in call sites (ImageCarousel.tsx and DayCalendar.tsx) that maintain direction state no longer consumed

Remove these props from the type to complete the refactor:

,

Proposed fix
 type SwipeableContainerProps = {
   children: React.ReactNode;
   className?: string;
-  direction: number;
-  swipeKey: string;
   onSwipeLeft?: () => void;
   onSwipeRight?: () => void;
 };

After this change, TypeScript will flag call sites that still pass these props, allowing you to clean up the orphaned direction/swipeKey state in ImageCarousel.tsx and DayCalendar.tsx.

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

In `@src/components/common/SwipeableContainer.tsx` around lines 4 - 18, Remove the
unused required props from the SwipeableContainer type: delete direction and
swipeKey from SwipeableContainerProps and from any related prop documentation so
the component signature matches the destructuring in SwipeableContainer; then
update call sites (e.g., ImageCarousel.tsx and DayCalendar.tsx) that still pass
direction or swipeKey to stop providing them and remove the now-orphaned
state/vars there. Ensure no runtime logic depended on those props before
deleting, and run TypeScript to catch remaining references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/calendar/WeekCalendar.tsx`:
- Around line 431-438: The Tailwind animation classes used in WeekCalendar.tsx
("animate-in", "fade-in", "duration-500") require the tailwindcss-animate plugin
(or equivalent for Tailwind v4); install the plugin as a dependency, register it
in the Tailwind config plugins array, and add/import its CSS utilities into your
main stylesheet (e.g., src/index.css) so the classes applied in WeekCalendar.tsx
(the div wrapping OccurrenceChip) produce the intended animation; after changes,
rebuild/start the dev server and verify the animations appear on the
OccurrenceChip elements.

---

Outside diff comments:
In `@src/components/common/SwipeableContainer.tsx`:
- Around line 4-18: Remove the unused required props from the SwipeableContainer
type: delete direction and swipeKey from SwipeableContainerProps and from any
related prop documentation so the component signature matches the destructuring
in SwipeableContainer; then update call sites (e.g., ImageCarousel.tsx and
DayCalendar.tsx) that still pass direction or swipeKey to stop providing them
and remove the now-orphaned state/vars there. Ensure no runtime logic depended
on those props before deleting, and run TypeScript to catch remaining
references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5c244a4-549f-4960-9d12-f379961b475d

📥 Commits

Reviewing files that changed from the base of the PR and between 43e577c and db928fa.

📒 Files selected for processing (8)
  • .claude/settings.json
  • src/components/calendar/CalendarFilters.tsx
  • src/components/calendar/DayCalendar.tsx
  • src/components/calendar/MonthCalendarCell.tsx
  • src/components/calendar/MonthCalendarGrid.tsx
  • src/components/calendar/WeekCalendar.tsx
  • src/components/common/SwipeableContainer.tsx
  • src/components/note/NotePeriodPicker.tsx

Comment on lines +431 to +438
<div
key={habitId}
exit={{ scale: 0 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="animate-in fade-in duration-500"
>
<OccurrenceChip
occurrences={habitOccurrences}
/>
</motion.div>
</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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if tailwindcss-animate is installed
echo "=== Checking package.json for tailwindcss-animate ==="
cat package.json | jq '.dependencies["tailwindcss-animate"] // .devDependencies["tailwindcss-animate"] // "NOT FOUND"'

# Check if the plugin is configured anywhere
echo -e "\n=== Searching for tailwindcss-animate configuration ==="
rg -l "tailwindcss-animate" --type-add 'config:*.{js,ts,mjs,cjs,json,css}' --type config

# Check CSS files for animate plugin import
echo -e "\n=== Checking CSS files for plugin imports ==="
fd -e css --exec grep -l "animate" {} \; 2>/dev/null || echo "No matches"

Repository: domhhv/habitrack

Length of output: 240


🏁 Script executed:

# Verify the animation classes in the specified file and context
echo "=== Checking WeekCalendar.tsx for animation classes ==="
sed -n '430,440p' src/components/calendar/WeekCalendar.tsx

# Search for all uses of animate-in or fade-in classes in the codebase
echo -e "\n=== Searching for animate-in or fade-in class usage ==="
rg "animate-in|fade-in" --type ts --type tsx -n

# Check tailwind config to see what plugins are configured
echo -e "\n=== Checking tailwind.config file ==="
fd "tailwind.config" -t f --exec cat {} \;

Repository: domhhv/habitrack

Length of output: 770


🏁 Script executed:

# Search more broadly for tailwind config files
echo "=== Finding tailwind config files ==="
fd -i "tailwind" -t f

# Check if Tailwind v4 has animate-in natively by checking CSS output
echo -e "\n=== Checking for Tailwind config in tsconfig or other locations ==="
cat tsconfig.json 2>/dev/null | grep -A 5 -B 5 "tailwind\|path" || echo "No tailwind in tsconfig"

# Look for any CSS imports that might load animations
echo -e "\n=== Checking all CSS files for animation-related imports ==="
fd -e css -exec grep -H "animation\|animate" {} \;

Repository: domhhv/habitrack

Length of output: 736


🏁 Script executed:

# Check Tailwind version in package.json
echo "=== Checking Tailwind version ==="
cat package.json | jq '.dependencies.tailwindcss, .devDependencies.tailwindcss'

# Correctly search CSS files
echo -e "\n=== Checking CSS files for animation configuration ==="
fd -e css -exec cat {} \;

# Check if Tailwind v4 includes animate-in natively
echo -e "\n=== Searching for animate-in in node_modules (if available) ==="
find node_modules/tailwindcss -name "*.js" -o -name "*.ts" 2>/dev/null | head -5 || echo "node_modules not fully available in sandbox"

Repository: domhhv/habitrack

Length of output: 383


🌐 Web query:

Tailwind CSS v4 animate-in fade-in animation classes native support

💡 Result:

No, Tailwind CSS v4 does not have native support for "animate-in fade-in" animation classes. These classes come from the popular community plugin tailwindcss-animate (or its v4-compatible replacements like tw-animate-css), which provides utilities such as animate-in, fade-in, slide-in-from-top, etc., for entrance/exit animations commonly used in UI libraries like shadcn/ui. Tailwind CSS v4 core provides built-in animation utilities like animate-spin, animate-ping, animate-pulse, and animate-bounce, plus support for custom animations via @theme { --animate-*: ... @keyframes ... }. For fade-in effects, use transition-opacity with opacity-0 to opacity-100, or the new starting: variant with @starting-style for entry transitions without JS (e.g., starting:opacity-0 opacity-100 transition-opacity). To add animate-in/fade-in support in v4: - Install tw-animate-css: npm i -D tw-animate-css - Import in CSS: @import "tw-animate-css"; - Usage:

Content
Official docs confirm no such native classes; they are plugin-provided.

Citations:


Install and configure animation plugin for animate-in and fade-in classes.

The code uses animate-in fade-in duration-500 classes, which require the tailwindcss-animate plugin (or tw-animate-css for Tailwind v4). These classes are not native to Tailwind CSS v4 and are not currently configured in the project—the plugin is not installed in package.json and no imports exist in the CSS files.

Without the plugin, these animation classes will be silently ignored, resulting in no animation effect. Install the plugin, import it in src/index.css, and verify the animations render correctly.

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

In `@src/components/calendar/WeekCalendar.tsx` around lines 431 - 438, The
Tailwind animation classes used in WeekCalendar.tsx ("animate-in", "fade-in",
"duration-500") require the tailwindcss-animate plugin (or equivalent for
Tailwind v4); install the plugin as a dependency, register it in the Tailwind
config plugins array, and add/import its CSS utilities into your main stylesheet
(e.g., src/index.css) so the classes applied in WeekCalendar.tsx (the div
wrapping OccurrenceChip) produce the intended animation; after changes,
rebuild/start the dev server and verify the animations appear on the
OccurrenceChip elements.

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 (1)
src/components/header/Header.tsx (1)

115-123: Consider extracting the mobile breakpoint check into a local constant.

screenWidth < 768 is repeated across both button props and icon sizes. Pulling it into a single isMobileActions flag will keep the two buttons in sync and reduce future drift.

Refactor sketch
 const Header = () => {
   const { isDesktop, screenWidth } = useScreenWidth();
+  const isMobileActions = screenWidth < 768;
@@
-              fullWidth={screenWidth < 768}
+              fullWidth={isMobileActions}
               onPress={dispatchNoteDrawerOpen}
-              radius={screenWidth < 768 ? 'full' : 'sm'}
+              radius={isMobileActions ? 'full' : 'sm'}
@@
-                size={screenWidth < 768 ? 12 : 16}
+                size={isMobileActions ? 12 : 16}
@@
-              fullWidth={screenWidth < 768}
+              fullWidth={isMobileActions}
               onPress={dispatchOccurrenceDrawerOpen}
-              radius={screenWidth < 768 ? 'full' : 'sm'}
+              radius={isMobileActions ? 'full' : 'sm'}
@@
-                size={screenWidth < 768 ? 12 : 16}
+                size={isMobileActions ? 12 : 16}

Also applies to: 136-144

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

In `@src/components/header/Header.tsx` around lines 115 - 123, Extract the
repeated breakpoint check into a local boolean (e.g., isMobileActions) at the
top of the Header component and replace all occurrences of `screenWidth < 768`
with that flag so the button props and icon sizes stay in sync; update usages in
the block around the Note button (fullWidth, radius, icon size, className) and
the similar block around lines 136-144 to reference isMobileActions instead of
repeating the literal expression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/calendar/CalendarFilters.tsx`:
- Around line 125-132: The wrapper currently toggles visibility via CSS using
the isVisible variable in CalendarFilters, which leaves mounted Select
components (controlled by useDisclosure) running and can orphan HeroUI popovers;
change the component to return null early when !isVisible so the entire tree
(including the Selects and their popovers) unmounts instead of merely hiding via
the 'hidden' class—remove the conditional 'hidden ...' approach and add an early
guard like "if (!isVisible) return null" at the start of the CalendarFilters
render to ensure cleanup of useDisclosure-managed popovers.

In `@src/components/note/NotePeriodPicker.tsx`:
- Around line 139-159: The icon-only period navigation buttons in
NotePeriodPicker are missing accessible names; update the Button components (the
one wrapping CaretLeftIcon and the matching right-arrow button) to include
aria-label props that reflect the current period (use periodKind and periodDate
to build labels like "Previous day", "Next week", or include the formatted date
if desired) so screen readers announce their purpose; locate the buttons used
inside the onPress handlers that call handleChange and setPeriodDate and add
aria-label attributes accordingly, following the React Aria/HeroUI accessibility
pattern.

---

Nitpick comments:
In `@src/components/header/Header.tsx`:
- Around line 115-123: Extract the repeated breakpoint check into a local
boolean (e.g., isMobileActions) at the top of the Header component and replace
all occurrences of `screenWidth < 768` with that flag so the button props and
icon sizes stay in sync; update usages in the block around the Note button
(fullWidth, radius, icon size, className) and the similar block around lines
136-144 to reference isMobileActions instead of repeating the literal
expression.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c387ac27-f1f9-49d5-86bd-76eec7ec0dca

📥 Commits

Reviewing files that changed from the base of the PR and between db928fa and b705687.

📒 Files selected for processing (6)
  • src/components/calendar/CalendarFilters.tsx
  • src/components/calendar/DayCalendar.tsx
  • src/components/calendar/MonthCalendarCell.tsx
  • src/components/calendar/WeekCalendar.tsx
  • src/components/header/Header.tsx
  • src/components/note/NotePeriodPicker.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/components/calendar/DayCalendar.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/calendar/WeekCalendar.tsx
  • src/components/calendar/MonthCalendarCell.tsx

Comment on lines +125 to +132
const isVisible = (isDesktop || filters.isShownOnMobile) && !!user;

return (
<AnimatePresence mode="wait">
{(isDesktop || filters.isShownOnMobile) && user && (
<motion.div
initial={{
height: 0,
opacity: 0,
}}
className="flex max-w-full flex-col items-stretch justify-end gap-2 min-[450px]:flex-row lg:justify-between"
exit={{
height: 0,
opacity: 0,
transition: {
height: {
duration: 0.4,
},
opacity: {
duration: 0.25,
},
},
}}
animate={{
height: 'auto',
opacity: 1,
transition: {
height: {
duration: 0.4,
},
opacity: {
delay: 0.15,
duration: 0.25,
},
},
}}
>
<Select
size="sm"
radius="sm"
color="secondary"
variant="bordered"
selectionMode="multiple"
className="w-full md:w-50"
placeholder="Filter by habits"
selectedKeys={filters.habitIds}
isOpen={isHabitsFilterSelectOpen}
onChange={handleHabitsFilterChange}
onOpenChange={onHabitsFilterSelectOpenChange}
scrollShadowProps={{
visibility: 'bottom',
}}
renderValue={(selectedHabits: SelectedItems<Habit>) => {
return (
<CrossPlatformHorizontalScroll className="flex space-x-2">
{selectedHabits.map(({ key }) => {
if (typeof key !== 'string' || !habits[key]) {
return null;
}
<div
className={cn(
'hidden max-w-full flex-col items-stretch justify-end gap-2 min-[450px]:flex-row lg:justify-between',
isVisible && 'flex'
)}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the CalendarFilters.tsx file
find . -name "CalendarFilters.tsx" -type f

Repository: domhhv/habitrack

Length of output: 105


🏁 Script executed:

# Get the file size first to see if we can read it fully
wc -l src/components/calendar/CalendarFilters.tsx

Repository: domhhv/habitrack

Length of output: 107


🏁 Script executed:

# Read the entire file to understand the implementation
cat -n src/components/calendar/CalendarFilters.tsx

Repository: domhhv/habitrack

Length of output: 10440


🏁 Script executed:

# Check if there are any useEffect hooks or other mechanisms that handle visibility changes
grep -n "useEffect\|isVisible\|isOpen" src/components/calendar/CalendarFilters.tsx

Repository: domhhv/habitrack

Length of output: 339


🏁 Script executed:

# Check the imports to understand what components are being used
head -n 50 src/components/calendar/CalendarFilters.tsx

Repository: domhhv/habitrack

Length of output: 1536


CSS-hiding the filter bar leaves open Select popovers orphaned in the DOM.

Both Select components maintain controlled isOpen state via useDisclosure. Currently, the wrapper becomes display: none when isVisible is false, but the Select components remain mounted. If either Select is open when the wrapper becomes hidden, HeroUI will leave the popover/backdrop mounted in the DOM (since it renders via a portal), creating an orphaned UI element.

Return null when !isVisible instead of using CSS to hide the wrapper, ensuring the entire component tree unmounts and cleans up any open popovers.

🩹 Suggested change
   const isVisible = (isDesktop || filters.isShownOnMobile) && !!user;

+  if (!isVisible) {
+    return null;
+  }
+
   return (
-    <div
-      className={cn(
-        'hidden max-w-full flex-col items-stretch justify-end gap-2 min-[450px]:flex-row lg:justify-between',
-        isVisible && 'flex'
-      )}
-    >
+    <div className="flex max-w-full flex-col items-stretch justify-end gap-2 min-[450px]:flex-row lg:justify-between">

Also applies to: 143-145, 237-239

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

In `@src/components/calendar/CalendarFilters.tsx` around lines 125 - 132, The
wrapper currently toggles visibility via CSS using the isVisible variable in
CalendarFilters, which leaves mounted Select components (controlled by
useDisclosure) running and can orphan HeroUI popovers; change the component to
return null early when !isVisible so the entire tree (including the Selects and
their popovers) unmounts instead of merely hiding via the 'hidden' class—remove
the conditional 'hidden ...' approach and add an early guard like "if
(!isVisible) return null" at the start of the CalendarFilters render to ensure
cleanup of useDisclosure-managed popovers.

Comment on lines +139 to +159
<Button
size="sm"
isIconOnly
variant="flat"
className="h-full w-5! min-w-auto"
onPress={() => {
void handleChange(() => {
switch (periodKind) {
case 'day':
return setPeriodDate(periodDate.subtract({ days: 1 }));

case 'week':
return setPeriodDate(periodDate.subtract({ weeks: 1 }));

case 'month':
return setPeriodDate(periodDate.subtract({ months: 1 }));
}

return '';
}}
renderThumb={(props) => {
return (
<div
{...props}
className="bg-primary-600 top-1/2 left-1/2 mx-auto flex h-8 w-8 items-center justify-center rounded-full text-white"
>
{periodKind === 'day' && <SunIcon size={20} />}
{periodKind === 'week' && <NumberSevenIcon size={20} />}
{periodKind === 'month' && <CalendarDotsIcon size={20} />}
</div>
);
}}
onChange={(value) => {
const nextValue = Array.isArray(value) ? value[0] : value;

void handleChange(() => {
switch (nextValue) {
case 1:
setPeriodDate(periodDate);
setPeriodKind('day');
break;

case 2: {
const weekStart = startOfWeek(
periodDate,
locale,
firstDayOfWeek
);
setPeriodDate(weekStart);
setPeriodKind('week');
break;
}

case 3: {
const monthStart = startOfMonth(periodDate);
setPeriodDate(monthStart);
setPeriodKind('month');
break;
}
}
});
}}
/>
<div className="flex gap-1">
<Button
size="sm"
isIconOnly
variant="flat"
className="h-full w-5! min-w-auto"
onPress={() => {
void handleChange(() => {
switch (periodKind) {
case 'day':
return setPeriodDate(periodDate.subtract({ days: 1 }));

case 'week':
return setPeriodDate(periodDate.subtract({ weeks: 1 }));

case 'month':
return setPeriodDate(periodDate.subtract({ months: 1 }));
}
});
}}
>
<CaretLeftIcon />
</Button>
<RangeCalendar
isReadOnly
focusedValue={periodDate}
firstDayOfWeek={firstDayOfWeek}
value={{
end: endRange,
start: periodDate,
}}
classNames={{
cell: '[&_span]:cursor-default! cursor-default',
nextButton: 'hidden',
prevButton: 'hidden',
}}
/>
<Button
size="sm"
isIconOnly
variant="flat"
className="h-full w-5! min-w-auto"
onPress={() => {
void handleChange(() => {
switch (periodKind) {
case 'day':
return setPeriodDate(periodDate.add({ days: 1 }));

case 'week':
return setPeriodDate(periodDate.add({ weeks: 1 }));

case 'month':
return setPeriodDate(periodDate.add({ months: 1 }));
}
});
}}
>
<CaretRightIcon />
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
});
}}
>
<CaretLeftIcon />
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 | 🟠 Major

Label the icon-only period buttons.

Both isIconOnly buttons are missing an accessible name, so screen readers will only announce a generic “button”. Add aria-labels that reflect the current period.

♿ Proposed fix
         <Button
+          aria-label={`Go to previous ${periodKind}`}
           size="sm"
           isIconOnly
           variant="flat"
           className="h-full w-5! min-w-auto"
@@
         <Button
+          aria-label={`Go to next ${periodKind}`}
           size="sm"
           isIconOnly
           variant="flat"
           className="h-full w-5! min-w-auto"
As per coding guidelines, "Use React Aria foundation for accessibility in HeroUI components".

Also applies to: 175-195

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

In `@src/components/note/NotePeriodPicker.tsx` around lines 139 - 159, The
icon-only period navigation buttons in NotePeriodPicker are missing accessible
names; update the Button components (the one wrapping CaretLeftIcon and the
matching right-arrow button) to include aria-label props that reflect the
current period (use periodKind and periodDate to build labels like "Previous
day", "Next week", or include the formatted date if desired) so screen readers
announce their purpose; locate the buttons used inside the onPress handlers that
call handleChange and setPeriodDate and add aria-label attributes accordingly,
following the React Aria/HeroUI accessibility pattern.

@domhhv domhhv merged commit ce3b6f3 into main Apr 1, 2026
11 checks passed
@domhhv domhhv deleted the refactor/replace-framer-with-css-animations branch April 1, 2026 19:44
@coderabbitai coderabbitai bot mentioned this pull request Apr 3, 2026
12 tasks
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