Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import { ChevronDown } from 'lucide-react';

import { cn } from '../../lib/utils';

const Accordion = AccordionPrimitive.Root;

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn('border-b', className)} {...props} />
));
AccordionItem.displayName = 'AccordionItem';

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

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

The AccordionContent component references animation classes 'animate-accordion-up' and 'animate-accordion-down' that are not defined anywhere in the codebase. These Tailwind CSS animations need to be configured in a tailwind.config file or the animations won't work, resulting in broken accordion expand/collapse effects.

Suggested change
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
className="overflow-hidden text-sm transition-all"

Copilot uses AI. Check for mistakes.
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));

AccordionContent.displayName = AccordionPrimitive.Content.displayName;

Comment on lines +17 to +51
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

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

The AccordionContent component's displayName is set after the component definition but the component is not exported as a named constant before setting displayName. While this works, it's inconsistent with AccordionTrigger which sets displayName using the Primitive's displayName. Consider following the same pattern for consistency.

Suggested change
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
const AccordionTrigger = Object.assign(
React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)),
{ displayName: AccordionPrimitive.Trigger.displayName }
);
const AccordionContent = Object.assign(
React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn('pb-4 pt-0', className)}>{children}</div>
</AccordionPrimitive.Content>
)),
{ displayName: AccordionPrimitive.Content.displayName }
);

Copilot uses AI. Check for mistakes.
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
42 changes: 42 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export type ClassValue = string | number | null | false | undefined | ClassDictionary | ClassArray;

interface ClassDictionary {
[key: string]: any;
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

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

The ClassDictionary interface uses 'any' as the value type, which defeats TypeScript's type safety. The actual logic only checks for truthiness, so the type should be 'any' could be replaced with 'boolean | string | number | null | undefined' or simply 'unknown' to maintain type safety while allowing the truthiness check.

Suggested change
[key: string]: any;
[key: string]: boolean | string | number | null | undefined;

Copilot uses AI. Check for mistakes.
}

interface ClassArray extends Array<ClassValue> {}

function toVal(mix: ClassValue): string {
let str = '';
if (typeof mix === 'string' || typeof mix === 'number') {
str += mix;
} else if (Array.isArray(mix)) {
for (const item of mix) {
const val = toVal(item);
if (val) {
if (str) str += ' ';
str += val;
}
}
} else if (mix && typeof mix === 'object') {
for (const key in mix as ClassDictionary) {
if ((mix as ClassDictionary)[key]) {
if (str) str += ' ';
str += key;
}
}
}
return str;
}

export function cn(...inputs: ClassValue[]): string {
let str = '';
for (const input of inputs) {
const val = toVal(input);
if (val) {
if (str) str += ' ';
str += val;
}
}
return str;
}
Comment on lines +32 to +42

Choose a reason for hiding this comment

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

high

This cn implementation is a good start for combining class names. However, it doesn't handle merging of conflicting Tailwind CSS classes. For example, cn('p-4', 'p-2') will produce 'p-4 p-2', which can lead to unpredictable styling because both classes are applied. In most Tailwind projects, cn utilities use tailwind-merge to resolve these conflicts, so the output would be just 'p-2'. Since the goal is to avoid external dependencies, be aware of this limitation. You might need to be careful not to pass conflicting utilities, or consider implementing a basic merging logic for common cases (like padding, margin, colors).

Loading
Loading