Skip to content

Commit ac9b1fc

Browse files
sheikhcodersclaude
andcommitted
✨ Add ESLint config, additional UI components, and type definitions
- ESLint configuration for Next.js - Separator and label UI components - TypeScript declarations for fonts - Enhanced chat components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 3546b25 commit ac9b1fc

File tree

13 files changed

+441
-0
lines changed

13 files changed

+441
-0
lines changed

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
18

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
18

eslint.config.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { dirname } from "path";
2+
import { fileURLToPath } from "url";
3+
import { FlatCompat } from "@eslint/eslintrc";
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = dirname(__filename);
7+
8+
const compat = new FlatCompat({
9+
baseDirectory: __dirname,
10+
});
11+
12+
const eslintConfig = [
13+
...compat.extends("next/core-web-vitals", "next/typescript"),
14+
{
15+
rules: {
16+
// Accessibility rules
17+
"jsx-a11y/alt-text": "error",
18+
"jsx-a11y/anchor-has-content": "error",
19+
"jsx-a11y/aria-props": "error",
20+
"jsx-a11y/aria-role": "error",
21+
"jsx-a11y/role-has-required-aria-props": "error",
22+
// TypeScript rules
23+
"@typescript-eslint/no-unused-vars": [
24+
"warn",
25+
{ argsIgnorePattern: "^_" },
26+
],
27+
},
28+
},
29+
];
30+
31+
export default eslintConfig;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Skeleton } from "@/components/ui/skeleton";
2+
3+
/**
4+
* Message loading skeleton
5+
* MUST: Skeletons mirror final content to avoid layout shift
6+
*/
7+
export function MessageSkeleton() {
8+
return (
9+
<div className="flex gap-3" aria-busy="true" aria-label="Loading message">
10+
{/* Avatar skeleton */}
11+
<Skeleton className="h-8 w-8 shrink-0 rounded-full" />
12+
13+
{/* Message content skeleton */}
14+
<div className="flex max-w-[85%] flex-col gap-2">
15+
<Skeleton className="h-4 w-48" />
16+
<Skeleton className="h-4 w-64" />
17+
<Skeleton className="h-4 w-32" />
18+
</div>
19+
</div>
20+
);
21+
}

src/components/ui/card.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as React from "react";
2+
import { cn } from "@/lib/utils";
3+
4+
const Card = React.forwardRef<
5+
HTMLDivElement,
6+
React.HTMLAttributes<HTMLDivElement>
7+
>(({ className, ...props }, ref) => (
8+
<div
9+
ref={ref}
10+
className={cn(
11+
"rounded-xl border bg-card text-card-foreground shadow",
12+
className
13+
)}
14+
{...props}
15+
/>
16+
));
17+
Card.displayName = "Card";
18+
19+
const CardHeader = React.forwardRef<
20+
HTMLDivElement,
21+
React.HTMLAttributes<HTMLDivElement>
22+
>(({ className, ...props }, ref) => (
23+
<div
24+
ref={ref}
25+
className={cn("flex flex-col space-y-1.5 p-6", className)}
26+
{...props}
27+
/>
28+
));
29+
CardHeader.displayName = "CardHeader";
30+
31+
const CardTitle = React.forwardRef<
32+
HTMLDivElement,
33+
React.HTMLAttributes<HTMLDivElement>
34+
>(({ className, ...props }, ref) => (
35+
<div
36+
ref={ref}
37+
className={cn("font-semibold leading-none tracking-tight", className)}
38+
{...props}
39+
/>
40+
));
41+
CardTitle.displayName = "CardTitle";
42+
43+
const CardDescription = React.forwardRef<
44+
HTMLDivElement,
45+
React.HTMLAttributes<HTMLDivElement>
46+
>(({ className, ...props }, ref) => (
47+
<div
48+
ref={ref}
49+
className={cn("text-sm text-muted-foreground", className)}
50+
{...props}
51+
/>
52+
));
53+
CardDescription.displayName = "CardDescription";
54+
55+
const CardContent = React.forwardRef<
56+
HTMLDivElement,
57+
React.HTMLAttributes<HTMLDivElement>
58+
>(({ className, ...props }, ref) => (
59+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
60+
));
61+
CardContent.displayName = "CardContent";
62+
63+
const CardFooter = React.forwardRef<
64+
HTMLDivElement,
65+
React.HTMLAttributes<HTMLDivElement>
66+
>(({ className, ...props }, ref) => (
67+
<div
68+
ref={ref}
69+
className={cn("flex items-center p-6 pt-0", className)}
70+
{...props}
71+
/>
72+
));
73+
CardFooter.displayName = "CardFooter";
74+
75+
export {
76+
Card,
77+
CardHeader,
78+
CardFooter,
79+
CardTitle,
80+
CardDescription,
81+
CardContent,
82+
};

src/components/ui/label.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as LabelPrimitive from "@radix-ui/react-label";
5+
import { cva, type VariantProps } from "class-variance-authority";
6+
import { cn } from "@/lib/utils";
7+
8+
const labelVariants = cva(
9+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
10+
);
11+
12+
const Label = React.forwardRef<
13+
React.ElementRef<typeof LabelPrimitive.Root>,
14+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
15+
VariantProps<typeof labelVariants>
16+
>(({ className, ...props }, ref) => (
17+
<LabelPrimitive.Root
18+
ref={ref}
19+
className={cn(labelVariants(), className)}
20+
{...props}
21+
/>
22+
));
23+
Label.displayName = LabelPrimitive.Root.displayName;
24+
25+
export { Label };

src/components/ui/separator.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as SeparatorPrimitive from "@radix-ui/react-separator";
5+
import { cn } from "@/lib/utils";
6+
7+
const Separator = React.forwardRef<
8+
React.ElementRef<typeof SeparatorPrimitive.Root>,
9+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
10+
>((
11+
{ className, orientation = "horizontal", decorative = true, ...props },
12+
ref
13+
) => (
14+
<SeparatorPrimitive.Root
15+
ref={ref}
16+
decorative={decorative}
17+
orientation={orientation}
18+
className={cn(
19+
"shrink-0 bg-border",
20+
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
21+
className
22+
)}
23+
{...props}
24+
/>
25+
));
26+
Separator.displayName = SeparatorPrimitive.Root.displayName;
27+
28+
export { Separator };

src/components/ui/skeleton.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { cn } from "@/lib/utils";
2+
3+
/**
4+
* Skeleton loading component
5+
* MUST: Skeletons mirror final content to avoid layout shift
6+
*/
7+
function Skeleton({
8+
className,
9+
...props
10+
}: React.HTMLAttributes<HTMLDivElement>) {
11+
return (
12+
<div
13+
className={cn(
14+
"animate-pulse-subtle rounded-md bg-muted",
15+
// Reduced motion: no animation
16+
"motion-reduce:animate-none",
17+
className
18+
)}
19+
aria-hidden="true"
20+
{...props}
21+
/>
22+
);
23+
}
24+
25+
export { Skeleton };
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as VisuallyHiddenPrimitive from "@radix-ui/react-visually-hidden";
5+
6+
/**
7+
* Visually hidden component for screen reader only content
8+
* MUST: Don't ship the schema—visuals may omit labels but accessible names still exist
9+
*/
10+
const VisuallyHidden = React.forwardRef<
11+
React.ElementRef<typeof VisuallyHiddenPrimitive.Root>,
12+
React.ComponentPropsWithoutRef<typeof VisuallyHiddenPrimitive.Root>
13+
>(({ ...props }, ref) => (
14+
<VisuallyHiddenPrimitive.Root ref={ref} {...props} />
15+
));
16+
VisuallyHidden.displayName = "VisuallyHidden";
17+
18+
export { VisuallyHidden };

src/hooks/use-focus-trap.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"use client";
2+
3+
import { useEffect, useRef, useCallback } from "react";
4+
5+
/**
6+
* Hook for trapping focus within a container
7+
* MUST: Manage focus (trap, move, and return) per APG patterns
8+
*/
9+
export function useFocusTrap<T extends HTMLElement>(active: boolean = true) {
10+
const containerRef = useRef<T>(null);
11+
const previousActiveElement = useRef<HTMLElement | null>(null);
12+
13+
// Get all focusable elements within the container
14+
const getFocusableElements = useCallback(() => {
15+
if (!containerRef.current) return [];
16+
17+
const selector = [
18+
'a[href]',
19+
'button:not([disabled])',
20+
'input:not([disabled])',
21+
'select:not([disabled])',
22+
'textarea:not([disabled])',
23+
'[tabindex]:not([tabindex="-1"])',
24+
].join(',');
25+
26+
return Array.from(
27+
containerRef.current.querySelectorAll<HTMLElement>(selector)
28+
).filter((el) => {
29+
// Filter out elements that are not visible
30+
return el.offsetParent !== null;
31+
});
32+
}, []);
33+
34+
// Handle Tab key to trap focus
35+
const handleKeyDown = useCallback(
36+
(event: KeyboardEvent) => {
37+
if (!active || event.key !== 'Tab') return;
38+
39+
const focusableElements = getFocusableElements();
40+
if (focusableElements.length === 0) return;
41+
42+
const firstElement = focusableElements[0];
43+
const lastElement = focusableElements[focusableElements.length - 1];
44+
45+
if (event.shiftKey) {
46+
// Shift + Tab: go to last element if on first
47+
if (document.activeElement === firstElement) {
48+
event.preventDefault();
49+
lastElement.focus();
50+
}
51+
} else {
52+
// Tab: go to first element if on last
53+
if (document.activeElement === lastElement) {
54+
event.preventDefault();
55+
firstElement.focus();
56+
}
57+
}
58+
},
59+
[active, getFocusableElements]
60+
);
61+
62+
useEffect(() => {
63+
if (!active) return;
64+
65+
// Store the currently focused element
66+
previousActiveElement.current = document.activeElement as HTMLElement;
67+
68+
// Focus the first focusable element in the container
69+
const focusableElements = getFocusableElements();
70+
if (focusableElements.length > 0) {
71+
focusableElements[0].focus();
72+
}
73+
74+
// Add event listener for Tab key
75+
document.addEventListener('keydown', handleKeyDown);
76+
77+
return () => {
78+
document.removeEventListener('keydown', handleKeyDown);
79+
80+
// Return focus to the previously focused element
81+
if (previousActiveElement.current) {
82+
previousActiveElement.current.focus();
83+
}
84+
};
85+
}, [active, getFocusableElements, handleKeyDown]);
86+
87+
return containerRef;
88+
}

0 commit comments

Comments
 (0)