fix: resolve UI accessibility, hydration, and semantic HTML issues (#520)

Co-authored-by: Mahesh Sanikommu <mahesh.svmr@gmail.com>
This commit is contained in:
Aman pandit 2025-12-08 11:01:30 +05:30 committed by GitHub
parent 1028c33e58
commit 08267ffa21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 322 additions and 314 deletions

View file

@ -1,23 +1,27 @@
"use client";
"use client"
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
import * as Sentry from "@sentry/nextjs"
import NextError from "next/error"
import { useEffect } from "react"
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
export default function GlobalError({
error,
}: {
error: Error & { digest?: string }
}) {
useEffect(() => {
Sentry.captureException(error)
}, [error])
return (
<html>
<body>
{/* `NextError` is the default Next.js error page component. Its type
return (
<html lang="en">
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}
<NextError statusCode={0} />
</body>
</html>
)
}

View file

@ -33,7 +33,10 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${font.variable} antialiased overflow-x-hidden`}>
<body
className={`${font.variable} antialiased overflow-x-hidden`}
suppressHydrationWarning
>
<ThemeProvider
attribute="class"
defaultTheme="system"

View file

@ -7,7 +7,7 @@ import { useInfiniteQuery } from "@tanstack/react-query"
import { useCallback, useEffect, useMemo, useState } from "react"
import type { z } from "zod"
import { MemoryGraph } from "@repo/ui/memory-graph"
import { Dialog, DialogContent } from "@repo/ui/components/dialog"
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import { ConnectAIModal } from "@/components/connect-ai-modal"
import { AddMemoryView } from "@/components/views/add-memory"
import { useChatOpen, useProject, useGraphModal } from "@/stores"
@ -173,6 +173,7 @@ export function GraphDialog() {
className="w-[95vw] h-[95vh] p-0 max-w-6xl sm:max-w-6xl"
showCloseButton={true}
>
<DialogTitle className="sr-only">Memory Graph</DialogTitle>
<div className="w-full h-full">
<MemoryGraph
documents={allDocuments}

View file

@ -1,294 +1,290 @@
'use client';
import { cn } from '@lib/utils';
import {
AnimatePresence,
motion
} from 'motion/react';
"use client"
import { cn } from "@lib/utils"
import { AnimatePresence, motion } from "motion/react"
import type {
TargetAndTransition,
Transition,
Variant,
Variants,
} from 'motion/react'
import React from 'react';
TargetAndTransition,
Transition,
Variant,
Variants,
} from "motion/react"
import React from "react"
export type PresetType = 'blur' | 'fade-in-blur' | 'scale' | 'fade' | 'slide';
export type PresetType = "blur" | "fade-in-blur" | "scale" | "fade" | "slide"
export type PerType = 'word' | 'char' | 'line';
export type PerType = "word" | "char" | "line"
export type TextEffectProps = {
children: string;
per?: PerType;
as?: keyof React.JSX.IntrinsicElements;
variants?: {
container?: Variants;
item?: Variants;
};
className?: string;
preset?: PresetType;
delay?: number;
speedReveal?: number;
speedSegment?: number;
trigger?: boolean;
onAnimationComplete?: () => void;
onAnimationStart?: () => void;
segmentWrapperClassName?: string;
containerTransition?: Transition;
segmentTransition?: Transition;
style?: React.CSSProperties;
};
children: string
per?: PerType
as?: keyof React.JSX.IntrinsicElements
variants?: {
container?: Variants
item?: Variants
}
className?: string
preset?: PresetType
delay?: number
speedReveal?: number
speedSegment?: number
trigger?: boolean
onAnimationComplete?: () => void
onAnimationStart?: () => void
segmentWrapperClassName?: string
containerTransition?: Transition
segmentTransition?: Transition
style?: React.CSSProperties
}
const defaultStaggerTimes: Record<PerType, number> = {
char: 0.03,
word: 0.05,
line: 0.1,
};
char: 0.03,
word: 0.05,
line: 0.1,
}
const defaultContainerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
},
},
exit: {
transition: { staggerChildren: 0.05, staggerDirection: -1 },
},
};
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.05,
},
},
exit: {
transition: { staggerChildren: 0.05, staggerDirection: -1 },
},
}
const defaultItemVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
},
exit: { opacity: 0 },
};
hidden: { opacity: 0 },
visible: {
opacity: 1,
},
exit: { opacity: 0 },
}
const presetVariants: Record<
PresetType,
{ container: Variants; item: Variants }
PresetType,
{ container: Variants; item: Variants }
> = {
blur: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: 'blur(12px)' },
visible: { opacity: 1, filter: 'blur(0px)' },
exit: { opacity: 0, filter: 'blur(12px)' },
},
},
'fade-in-blur': {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, y: 20, filter: 'blur(12px)' },
visible: { opacity: 1, y: 0, filter: 'blur(0px)' },
exit: { opacity: 0, y: 20, filter: 'blur(12px)' },
},
},
scale: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, scale: 0 },
visible: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0 },
},
},
fade: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0 },
visible: { opacity: 1 },
exit: { opacity: 0 },
},
},
slide: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 },
},
},
};
blur: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, filter: "blur(12px)" },
visible: { opacity: 1, filter: "blur(0px)" },
exit: { opacity: 0, filter: "blur(12px)" },
},
},
"fade-in-blur": {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, y: 20, filter: "blur(12px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
exit: { opacity: 0, y: 20, filter: "blur(12px)" },
},
},
scale: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, scale: 0 },
visible: { opacity: 1, scale: 1 },
exit: { opacity: 0, scale: 0 },
},
},
fade: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0 },
visible: { opacity: 1 },
exit: { opacity: 0 },
},
},
slide: {
container: defaultContainerVariants,
item: {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: 20 },
},
},
}
const AnimationComponent: React.FC<{
segment: string;
variants: Variants;
per: 'line' | 'word' | 'char';
segmentWrapperClassName?: string;
segment: string
variants: Variants
per: "line" | "word" | "char"
segmentWrapperClassName?: string
}> = React.memo(({ segment, variants, per, segmentWrapperClassName }) => {
const content =
per === 'line' ? (
<motion.span variants={variants} className='block'>
{segment}
</motion.span>
) : per === 'word' ? (
<motion.span
aria-hidden='true'
variants={variants}
className='inline-block whitespace-pre'
>
{segment}
</motion.span>
) : (
<motion.span className='inline-block whitespace-pre'>
{segment.split('').map((char, charIndex) => (
<motion.span
key={`char-${charIndex}`}
aria-hidden='true'
variants={variants}
className='inline-block whitespace-pre'
>
{char}
</motion.span>
))}
</motion.span>
);
const content =
per === "line" ? (
<motion.span variants={variants} className="block">
{segment}
</motion.span>
) : per === "word" ? (
<motion.span
aria-hidden="true"
variants={variants}
className="inline-block whitespace-pre"
>
{segment}
</motion.span>
) : (
<motion.span className="inline-block whitespace-pre">
{segment.split("").map((char, charIndex) => (
<motion.span
// biome-ignore lint/suspicious/noArrayIndexKey: Character position is stable within animation
key={`char-${charIndex}`}
aria-hidden="true"
variants={variants}
className="inline-block whitespace-pre"
>
{char}
</motion.span>
))}
</motion.span>
)
if (!segmentWrapperClassName) {
return content;
}
if (!segmentWrapperClassName) {
return content
}
const defaultWrapperClassName = per === 'line' ? 'block' : 'inline-block';
const defaultWrapperClassName = per === "line" ? "block" : "inline-block"
return (
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
{content}
</span>
);
});
return (
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
{content}
</span>
)
})
AnimationComponent.displayName = 'AnimationComponent';
AnimationComponent.displayName = "AnimationComponent"
const splitText = (text: string, per: PerType) => {
if (per === 'line') return text.split('\n');
return text.split(/(\s+)/);
};
if (per === "line") return text.split("\n")
return text.split(/(\s+)/)
}
const hasTransition = (
variant?: Variant
variant?: Variant,
): variant is TargetAndTransition & { transition?: Transition } => {
if (!variant) return false;
return (
typeof variant === 'object' && 'transition' in variant
);
};
if (!variant) return false
return typeof variant === "object" && "transition" in variant
}
const createVariantsWithTransition = (
baseVariants: Variants,
transition?: Transition & { exit?: Transition }
baseVariants: Variants,
transition?: Transition & { exit?: Transition },
): Variants => {
if (!transition) return baseVariants;
if (!transition) return baseVariants
const { exit: _, ...mainTransition } = transition;
const { exit: _, ...mainTransition } = transition
return {
...baseVariants,
visible: {
...baseVariants.visible,
transition: {
...(hasTransition(baseVariants.visible)
? baseVariants.visible.transition
: {}),
...mainTransition,
},
},
exit: {
...baseVariants.exit,
transition: {
...(hasTransition(baseVariants.exit)
? baseVariants.exit.transition
: {}),
...mainTransition,
staggerDirection: -1,
},
},
};
};
return {
...baseVariants,
visible: {
...baseVariants.visible,
transition: {
...(hasTransition(baseVariants.visible)
? baseVariants.visible.transition
: {}),
...mainTransition,
},
},
exit: {
...baseVariants.exit,
transition: {
...(hasTransition(baseVariants.exit)
? baseVariants.exit.transition
: {}),
...mainTransition,
staggerDirection: -1,
},
},
}
}
export function TextEffect({
children,
per = 'word',
as = 'p',
variants,
className,
preset = 'fade',
delay = 0,
speedReveal = 1,
speedSegment = 1,
trigger = true,
onAnimationComplete,
onAnimationStart,
segmentWrapperClassName,
containerTransition,
segmentTransition,
style,
children,
per = "word",
as = "p",
variants,
className,
preset = "fade",
delay = 0,
speedReveal = 1,
speedSegment = 1,
trigger = true,
onAnimationComplete,
onAnimationStart,
segmentWrapperClassName,
containerTransition,
segmentTransition,
style,
}: TextEffectProps) {
const segments = splitText(children, per);
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div;
const segments = splitText(children, per)
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div
const baseVariants = preset
? presetVariants[preset]
: { container: defaultContainerVariants, item: defaultItemVariants };
const baseVariants = preset
? presetVariants[preset]
: { container: defaultContainerVariants, item: defaultItemVariants }
const stagger = defaultStaggerTimes[per] / speedReveal;
const stagger = defaultStaggerTimes[per] / speedReveal
const baseDuration = 0.3 / speedSegment;
const baseDuration = 0.3 / speedSegment
const customStagger = hasTransition(variants?.container?.visible ?? {})
? (variants?.container?.visible as TargetAndTransition).transition
?.staggerChildren
: undefined;
const customStagger = hasTransition(variants?.container?.visible ?? {})
? (variants?.container?.visible as TargetAndTransition).transition
?.staggerChildren
: undefined
const customDelay = hasTransition(variants?.container?.visible ?? {})
? (variants?.container?.visible as TargetAndTransition).transition
?.delayChildren
: undefined;
const customDelay = hasTransition(variants?.container?.visible ?? {})
? (variants?.container?.visible as TargetAndTransition).transition
?.delayChildren
: undefined
const computedVariants = {
container: createVariantsWithTransition(
variants?.container || baseVariants.container,
{
staggerChildren: customStagger ?? stagger,
delayChildren: customDelay ?? delay,
...containerTransition,
exit: {
staggerChildren: customStagger ?? stagger,
staggerDirection: -1,
},
}
),
item: createVariantsWithTransition(variants?.item || baseVariants.item, {
duration: baseDuration,
...segmentTransition,
}),
};
const computedVariants = {
container: createVariantsWithTransition(
variants?.container || baseVariants.container,
{
staggerChildren: customStagger ?? stagger,
delayChildren: customDelay ?? delay,
...containerTransition,
exit: {
staggerChildren: customStagger ?? stagger,
staggerDirection: -1,
},
},
),
item: createVariantsWithTransition(variants?.item || baseVariants.item, {
duration: baseDuration,
...segmentTransition,
}),
}
return (
<AnimatePresence mode='popLayout'>
{trigger && (
<MotionTag
initial='hidden'
animate='visible'
exit='exit'
variants={computedVariants.container}
className={className}
onAnimationComplete={onAnimationComplete}
onAnimationStart={onAnimationStart}
style={style}
>
{per !== 'line' ? <span className='sr-only'>{children}</span> : null}
{segments.map((segment, index) => (
<AnimationComponent
key={`${per}-${index}-${segment}`}
segment={segment}
variants={computedVariants.item}
per={per}
segmentWrapperClassName={segmentWrapperClassName}
/>
))}
</MotionTag>
)}
</AnimatePresence>
);
return (
<AnimatePresence mode="popLayout">
{trigger && (
<MotionTag
initial="hidden"
animate="visible"
exit="exit"
variants={computedVariants.container}
className={className}
onAnimationComplete={onAnimationComplete}
onAnimationStart={onAnimationStart}
style={style}
>
{per !== "line" ? <span className="sr-only">{children}</span> : null}
{segments.map((segment, index) => (
<AnimationComponent
key={`${per}-${index}-${segment}`}
segment={segment}
variants={computedVariants.item}
per={per}
segmentWrapperClassName={segmentWrapperClassName}
/>
))}
</MotionTag>
)}
</AnimatePresence>
)
}

View file

@ -361,7 +361,7 @@ export function AddMemoryView({
return { previousMemories, optimisticId: optimisticMemory.id }
},
// If the mutation fails, roll back to the previous value
onError: (error, variables, context) => {
onError: (_error, variables, context) => {
if (context?.previousMemories) {
queryClient.setQueryData(
["documents-with-memories", variables.project],
@ -551,6 +551,7 @@ export function AddMemoryView({
className="w-[100vw] max-w-4xl sm:max-w-4xl backdrop-blur-xl border-white/10 z-[80] h-[52vh] overflow-y-auto p-4"
showCloseButton={false}
>
<DialogTitle className="sr-only">Add Memory</DialogTitle>
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as typeof activeTab)}
@ -638,9 +639,13 @@ export function AddMemoryView({
>
{({ state, handleChange, handleBlur }) => (
<>
<div className={addContentMutation.isPending ? "opacity-50" : ""}>
<TextEditor
containerClassName="bg-white/5 border-white/10 rounded-md"
<div
className={
addContentMutation.isPending ? "opacity-50" : ""
}
>
<TextEditor
containerClassName="bg-white/5 border-white/10 rounded-md"
disabled={addContentMutation.isPending}
onBlur={handleBlur}
onChange={handleChange}
@ -843,14 +848,14 @@ export function AddMemoryView({
</TabsContent>
<TabsContent value="file" className="space-y-4">
<form
<form
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
fileUploadForm.handleSubmit()
}}
className="h-full flex flex-col"
>
className="h-full flex flex-col"
>
<div className="grid gap-4">
<motion.div
animate={{ opacity: 1, y: 0 }}
@ -938,7 +943,7 @@ export function AddMemoryView({
</fileUploadForm.Field>
</motion.div>
</div>
<div className="mt-6 flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4 mt-auto">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-end w-full gap-4 mt-auto">
<div className="flex items-end gap-4">
{/* Left side - Project Selection */}
<motion.div

View file

@ -1,7 +1,7 @@
"use client";
"use client"
import { cn } from "@lib/utils";
import { Button } from "@ui/components/button";
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import {
Dialog,
DialogContent,
@ -9,38 +9,38 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@ui/components/dialog";
import { ScrollArea } from "@ui/components/scroll-area";
import { formatDistanceToNow } from "date-fns";
import { HistoryIcon, Plus, Trash2, X } from "lucide-react";
import { useMemo, useState } from "react";
import { analytics } from "@/lib/analytics";
import { useChatOpen, usePersistentChat, useProject } from "@/stores";
import { ChatMessages } from "./chat-messages";
} from "@ui/components/dialog"
import { ScrollArea } from "@ui/components/scroll-area"
import { formatDistanceToNow } from "date-fns"
import { HistoryIcon, Plus, Trash2, X } from "lucide-react"
import { useMemo, useState } from "react"
import { analytics } from "@/lib/analytics"
import { useChatOpen, usePersistentChat, useProject } from "@/stores"
import { ChatMessages } from "./chat-messages"
export function ChatRewrite() {
const { setIsOpen } = useChatOpen();
const { selectedProject } = useProject();
const { setIsOpen } = useChatOpen()
const { selectedProject } = useProject()
const { conversations, currentChatId, setCurrentChatId, getCurrentChat } =
usePersistentChat();
usePersistentChat()
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false)
const sorted = useMemo(() => {
return [...conversations].sort((a, b) =>
a.lastUpdated < b.lastUpdated ? 1 : -1,
);
}, [conversations]);
)
}, [conversations])
function handleNewChat() {
analytics.newChatStarted();
const newId = crypto.randomUUID();
setCurrentChatId(newId);
setIsDialogOpen(false);
analytics.newChatStarted()
const newId = crypto.randomUUID()
setCurrentChatId(newId)
setIsDialogOpen(false)
}
function formatRelativeTime(isoString: string): string {
return formatDistanceToNow(new Date(isoString), { addSuffix: true });
return formatDistanceToNow(new Date(isoString), { addSuffix: true })
}
return (
@ -74,18 +74,17 @@ export function ChatRewrite() {
<ScrollArea className="max-h-96">
<div className="flex flex-col gap-1">
{sorted.map((c) => {
const isActive = c.id === currentChatId;
const isActive = c.id === currentChatId
return (
<div
<button
type="button"
key={c.id}
role="button"
tabIndex={0}
onClick={() => {
setCurrentChatId(c.id);
setIsDialogOpen(false);
setCurrentChatId(c.id)
setIsDialogOpen(false)
}}
className={cn(
"flex items-center justify-between rounded-md px-3 py-2 outline-none",
"flex items-center justify-between rounded-md px-3 py-2 outline-none w-full text-left",
"transition-colors",
isActive ? "bg-primary/10" : "hover:bg-muted",
)}
@ -111,15 +110,15 @@ export function ChatRewrite() {
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
analytics.chatDeleted();
e.stopPropagation()
analytics.chatDeleted()
}}
aria-label="Delete conversation"
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
);
</button>
)
})}
{sorted.length === 0 && (
<div className="text-xs text-muted-foreground px-3 py-2">
@ -152,5 +151,5 @@ export function ChatRewrite() {
</div>
<ChatMessages />
</div>
);
)
}

View file

@ -1,7 +1,6 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
.sm-tweet-theme .react-tweet-theme {
--tweet-container-margin: 0px;
}

View file

@ -4,6 +4,7 @@ import { NextResponse } from "next/server"
export default async function proxy(request: Request) {
console.debug("[PROXY] === PROXY START ===")
const url = new URL(request.url)
console.debug("[PROXY] Path:", url.pathname)
console.debug("[PROXY] Method:", request.method)