Skip to content

feat: add animated tabs to ui#115

Merged
tonyantony300 merged 2 commits intotonyantony300:mainfrom
blackmouse572:feat/animate-tabs
Feb 5, 2026
Merged

feat: add animated tabs to ui#115
tonyantony300 merged 2 commits intotonyantony300:mainfrom
blackmouse572:feat/animate-tabs

Conversation

@blackmouse572
Copy link
Contributor

Description

Add animated tabs for smooth butter UX
result

Checklist

  • I have run npm run lint before raising this PR
  • I have run npm run format before raising this PR

Copilot AI review requested due to automatic review settings February 5, 2026 11:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds animated tabs to the UI by integrating components from the animate-ui library. The changes introduce smooth animations and transitions to the tab interface for improved user experience.

Changes:

  • Added new dependencies: motion (v12.31.1) and radix-ui (v1.4.3) for animation and UI primitives
  • Implemented new utility hooks (use-controlled-state, use-auto-height, get-strict-context) to support animation features
  • Created animated tab components with highlight effects and auto-height transitions
  • Updated Sender and Receiver components to use consistent text color styling with utility classes

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
web-app/src/routes/index.tsx Integrates new animated tabs components, replacing previous tab implementation
web-app/src/components/animate-ui/primitives/radix/tabs.tsx Core animated tabs primitive components wrapping Radix UI
web-app/src/components/animate-ui/components/tabs.tsx Styled tabs components with animation support
web-app/src/components/animate-ui/primitives/effects/highlight.tsx Highlight animation effect for active tab indicators
web-app/src/components/animate-ui/primitives/effects/auto-height.tsx Auto-height animation wrapper for smooth content transitions
web-app/src/components/animate-ui/primitives/animate/slot.tsx Slot component for flexible rendering with motion support
web-app/src/hooks/use-controlled-state.tsx Hook for managing controlled/uncontrolled state patterns
web-app/src/hooks/use-auto-height.tsx Hook for automatic height measurement with ResizeObserver
web-app/src/lib/get-strict-context.tsx Utility for creating type-safe React contexts
web-app/src/components/sender/Sender.tsx Updated text styling to use utility classes
web-app/src/components/receiver/Receiver.tsx Updated text styling to use utility classes
package.json Added motion and radix-ui dependencies
components.json Added animate-ui registry and changed icon library

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'use client'

import * as React from 'react'
import { Tabs as TabsPrimitive } from 'radix-ui'
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The import statement import { Tabs as TabsPrimitive } from 'radix-ui' is incorrect. The radix-ui package (v1.4.3) is a meta-package that re-exports individual Radix UI components, but looking at its exports structure, the Tabs component should be imported from @radix-ui/react-tabs instead. This import will likely fail at runtime as the Tabs export may not be available from the main radix-ui package, or if it is, it should be accessed as Tabs from the package's exports structure, not as a nested TabsPrimitive.Root object.

The correct import should be:

import * as TabsPrimitive from '@radix-ui/react-tabs'

This ensures proper access to TabsPrimitive.Root, TabsPrimitive.List, TabsPrimitive.Trigger, and TabsPrimitive.Content components.

Suggested change
import { Tabs as TabsPrimitive } from 'radix-ui'
import * as TabsPrimitive from '@radix-ui/react-tabs'

Copilot uses AI. Check for mistakes.
import { SingleLayoutPage } from '@/components/common/SingleLayoutPage'
import { invoke } from '@tauri-apps/api/core'
import { useEffect, useRef, useState } from 'react'
import * as SingleLayoutPage from '@/components/common/SingleLayoutPage'
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The import statement for SingleLayoutPage has been changed incorrectly. The original file exports SingleLayoutPage as a named export (not a default export or namespace), so the import should be:

import { SingleLayoutPage } from '@/components/common/SingleLayoutPage'

Then it should be used directly as <SingleLayoutPage> rather than <SingleLayoutPage.SingleLayoutPage>. The current implementation will fail at runtime because importing as a namespace (import * as SingleLayoutPage) and then accessing it as SingleLayoutPage.SingleLayoutPage is unnecessary and inconsistent with the actual export.

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +89
<SingleLayoutPage.SingleLayoutPage>
<div className="max-w-2xl mx-auto w-full">
<Frame>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as 'send' | 'receive')}
>
<FrameHeader>
<TabsList className="w-full">
<TabsTab disabled={isReceiving} value="send">
<TabsTrigger disabled={isReceiving} value="send">
{t('common:send')}
</TabsTab>
<TabsTab disabled={isSharing} value="receive">
</TabsTrigger>
<TabsTrigger disabled={isSharing} value="receive">
{t('common:receive')}
</TabsTab>
</TabsTrigger>
</TabsList>
</FrameHeader>
<FramePanel>
<TabsPanel value="send">
<TabsContent value="send">
<Sender onTransferStateChange={setIsSharing} />
</TabsPanel>
<TabsPanel value="receive">
</TabsContent>
<TabsContent value="receive">
<Receiver onTransferStateChange={setIsReceiving} />
</TabsPanel>
</TabsContent>
</FramePanel>
</Tabs>
</Frame>
</div>
</SingleLayoutPage>
</SingleLayoutPage.SingleLayoutPage>
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The usage of SingleLayoutPage.SingleLayoutPage is incorrect. Since the import on line 3 should be a named import (import { SingleLayoutPage }), this should simply be:

<SingleLayoutPage>

and the closing tag on line 89 should be:

</SingleLayoutPage>

Copilot uses AI. Check for mistakes.
"prefix": ""
},
"iconLibrary": "lucide",
"iconLibrary": "phosphor-react",
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The icon library has been changed from "lucide" to "phosphor-react", but there's no corresponding package update visible in the package.json changes. If this is an intentional change, ensure that the "phosphor-react" package is added to dependencies. However, this change appears unrelated to the stated PR purpose of "add animated tabs to ui" and may have been included accidentally.

Suggested change
"iconLibrary": "phosphor-react",
"iconLibrary": "lucide",

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +79
const isAlreadyMotion =
typeof children.type === 'object' &&
children.type !== null &&
isMotionComponent(children.type)

const Base = React.useMemo(
() =>
isAlreadyMotion
? (children.type as React.ElementType)
: motion.create(children.type as React.ElementType),
[isAlreadyMotion, children.type]
)

if (!React.isValidElement(children)) return null
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The Slot component accesses children.type on line 67 before checking if children is a valid React element on line 79. This could cause a runtime error if children is null, undefined, or not a React element. The validation should occur before accessing any properties of children.

Move the if (!React.isValidElement(children)) return null check to the beginning of the function, before the isAlreadyMotion check.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +45
function Tabs(props: TabsProps) {
const [value, setValue] = useControlledState({
value: props.value,
defaultValue: props.defaultValue,
onChange: props.onValueChange,
})

return (
<TabsProvider value={{ value, setValue }}>
<TabsPrimitive.Root
data-slot="tabs"
{...props}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The Tabs component passes all props (including value and defaultValue) to both useControlledState and TabsPrimitive.Root. This creates a conflict where the state is managed twice - once internally by useControlledState and once by the Radix Tabs primitive. When passing {...props} to TabsPrimitive.Root, the original value prop is included, which will cause the Radix component to compete with the controlled state management.

The component should either:

  1. Remove value and defaultValue from the props spread to TabsPrimitive.Root, and only pass the controlled value state
  2. Or let TabsPrimitive.Root handle the state entirely

The current implementation will lead to unpredictable behavior when both the parent component and Radix try to control the value.

Suggested change
function Tabs(props: TabsProps) {
const [value, setValue] = useControlledState({
value: props.value,
defaultValue: props.defaultValue,
onChange: props.onValueChange,
})
return (
<TabsProvider value={{ value, setValue }}>
<TabsPrimitive.Root
data-slot="tabs"
{...props}
function Tabs({
value: valueProp,
defaultValue: defaultValueProp,
onValueChange: onValueChangeProp,
...restProps
}: TabsProps) {
const [value, setValue] = useControlledState({
value: valueProp,
defaultValue: defaultValueProp,
onChange: onValueChangeProp,
})
return (
<TabsProvider value={{ value, setValue }}>
<TabsPrimitive.Root
data-slot="tabs"
{...restProps}
value={value}

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +89
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to control deps
}, deps)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The useLayoutEffect on line 61 depends on measure and options.includeParentBox, but the dependency array only includes deps (which are user-provided dependencies). The measure function is a dependency because it's called within the effect, and options.includeParentBox is used within the effect. While the biome-ignore comment indicates this is intentional, omitting measure from the dependencies could lead to stale closures where an old version of the measure function is used.

If the intention is to have controlled dependencies, consider either:

  1. Adding measure and options to the dependencies alongside deps
  2. Or restructuring to avoid the dependency on measure by inlining the measurement logic
Suggested change
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to control deps
}, deps)
}, [measure, options.includeParentBox, ...deps])

Copilot uses AI. Check for mistakes.
@tonyantony300 tonyantony300 merged commit 0e7f714 into tonyantony300:main Feb 5, 2026
2 checks passed
@tonyantony300
Copy link
Owner

Thanks @blackmouse572 for this cool addition to UI. Merged!

@blackmouse572 blackmouse572 deleted the feat/animate-tabs branch February 6, 2026 04:50
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.

3 participants