Skip to content

Commit f3b2f62

Browse files
committed
feat: add simple React-based alternatives to Radix UI components
- Added SimpleDialog, SimpleTooltip, and SimpleLabel components as pure React alternatives - Created a compatibility layer in radix-compatibility.tsx to minimize code changes - Updated UserDataModal and App to use the new components - This is a first step toward removing Radix UI dependency completely
1 parent 061d403 commit f3b2f62

File tree

6 files changed

+424
-6
lines changed

6 files changed

+424
-6
lines changed

src/App.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
'use client';
22

3-
// Import reactive shim first to ensure global React is available
4-
import './lib/react-shim';
5-
63
import React, { useEffect } from 'react';
7-
import { TooltipProvider } from '@radix-ui/react-tooltip';
4+
import { TooltipProvider } from './components/ui/radix-compatibility';
85

96
// Providers
107
import { AuthProvider } from './features/auth/AuthProvider';

src/components/modals/UserDataModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import React, { useState, useEffect } from 'react';
44
import { useEntries } from '../../features/statements/hooks/useEntries';
55
import { useAuth } from '../../features/auth/api/hooks';
6-
import { DialogContent, DialogTitle, DialogDescription, DialogClose } from '../ui/dialog';
6+
// Use non-Radix components
7+
import { DialogContent, DialogTitle, DialogDescription, DialogClose } from '../ui/radix-compatibility';
78
import { Button } from '../ui/button';
89
import { Input } from '../ui/input';
910
import { Save, X, User, Mail, Award, Edit2, LogOut } from 'lucide-react';
10-
import { Tooltip, TooltipTrigger, TooltipContent } from '../ui/tooltip';
11+
import { Tooltip, TooltipTrigger, TooltipContent } from '../ui/radix-compatibility';
1112
import { validateEmail } from '../../lib/utils/validateEmail';
1213
import QuestionCounter from '../ui/questionCounter/QuestionCounter';
1314
import ProgressWithFeedback from '../ui/progress/ProgressWithFeedback';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React, { useState } from 'react';
2+
3+
// Import our simple components
4+
import {
5+
SimpleDialog,
6+
SimpleDialogTrigger,
7+
SimpleDialogContent,
8+
SimpleDialogHeader,
9+
SimpleDialogFooter,
10+
SimpleDialogTitle,
11+
SimpleDialogDescription,
12+
SimpleDialogClose,
13+
SimpleDialogOverlay,
14+
SimpleDialogPortal
15+
} from './simple-dialog';
16+
17+
import {
18+
SimpleTooltipProvider,
19+
} from './simple-tooltip';
20+
21+
import { SimpleLabel } from './simple-label';
22+
23+
// Create compatibility layer
24+
interface DialogRootProps {
25+
children: React.ReactNode;
26+
open?: boolean;
27+
onOpenChange?: (open: boolean) => void;
28+
}
29+
30+
// Create a Dialog Root that has the same API as Radix's
31+
const Dialog: React.FC<DialogRootProps> = ({ children, open, onOpenChange }) => {
32+
const [isOpen, setIsOpen] = useState(open || false);
33+
34+
const handleOpenChange = (newOpen: boolean) => {
35+
setIsOpen(newOpen);
36+
if (onOpenChange) onOpenChange(newOpen);
37+
};
38+
39+
return (
40+
<SimpleDialog isOpen={isOpen} onOpenChange={handleOpenChange} className="">
41+
{children}
42+
</SimpleDialog>
43+
);
44+
};
45+
46+
// Re-export with Radix-compatible names
47+
// Custom Tooltip components compatible with Radix API
48+
const Tooltip: React.FC<{children: React.ReactNode}> = ({ children }) => {
49+
return <>{children}</>;
50+
};
51+
52+
const TooltipTrigger: React.FC<{children: React.ReactNode, asChild?: boolean}> = ({ children }) => {
53+
return <>{children}</>;
54+
};
55+
56+
const TooltipContent: React.FC<{children: React.ReactNode, className?: string}> = ({ children, className }) => {
57+
return <div className={className}>{children}</div>;
58+
};
59+
60+
export {
61+
Dialog,
62+
SimpleDialogTrigger as DialogTrigger,
63+
SimpleDialogContent as DialogContent,
64+
SimpleDialogHeader as DialogHeader,
65+
SimpleDialogFooter as DialogFooter,
66+
SimpleDialogTitle as DialogTitle,
67+
SimpleDialogDescription as DialogDescription,
68+
SimpleDialogClose as DialogClose,
69+
SimpleDialogOverlay as DialogOverlay,
70+
SimpleDialogPortal as DialogPortal,
71+
72+
SimpleTooltipProvider as TooltipProvider,
73+
Tooltip,
74+
TooltipTrigger,
75+
TooltipContent,
76+
77+
SimpleLabel as Label,
78+
};
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import React, { useEffect } from 'react';
2+
import { cn } from '@/lib/utils';
3+
4+
interface SimpleDialogProps {
5+
isOpen: boolean;
6+
onOpenChange: (isOpen: boolean) => void;
7+
children: React.ReactNode;
8+
className?: string;
9+
}
10+
11+
interface SimpleDialogContentProps {
12+
children: React.ReactNode;
13+
className?: string;
14+
headerTitle?: string;
15+
onOpenAutoFocus?: (e: any) => void;
16+
onEscapeKeyDown?: () => void;
17+
onPointerDownOutside?: () => void;
18+
}
19+
20+
interface SimpleDialogTriggerProps {
21+
children: React.ReactNode;
22+
asChild?: boolean;
23+
}
24+
25+
interface SimpleDialogTitleProps {
26+
children: React.ReactNode;
27+
className?: string;
28+
}
29+
30+
interface SimpleDialogDescriptionProps {
31+
children: React.ReactNode;
32+
className?: string;
33+
}
34+
35+
const SimpleDialogPortal: React.FC<{ children: React.ReactNode }> = ({ children }) => {
36+
return <>{children}</>;
37+
};
38+
39+
const SimpleDialogOverlay: React.FC<{ className?: string }> = ({ className }) => {
40+
return (
41+
<div
42+
className={cn(
43+
'fixed inset-0 z-50 bg-black/80',
44+
className
45+
)}
46+
/>
47+
);
48+
};
49+
50+
const SimpleDialogContent = React.forwardRef<HTMLDivElement, SimpleDialogContentProps>(
51+
({ className, children, headerTitle, onOpenAutoFocus, ...props }, ref) => {
52+
if (onOpenAutoFocus) {
53+
// Prevent auto focus
54+
useEffect(() => {
55+
const handler = (e: FocusEvent) => {
56+
if (onOpenAutoFocus) onOpenAutoFocus(e);
57+
};
58+
document.addEventListener('focus', handler, { once: true });
59+
return () => document.removeEventListener('focus', handler);
60+
}, [onOpenAutoFocus]);
61+
}
62+
63+
return (
64+
<div
65+
ref={ref}
66+
className={cn(
67+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border-0 bg-background p-0 shadow-lg duration-200',
68+
className
69+
)}
70+
{...props}
71+
>
72+
{children}
73+
</div>
74+
);
75+
}
76+
);
77+
SimpleDialogContent.displayName = 'SimpleDialogContent';
78+
79+
const SimpleDialogClose: React.FC<{ children: React.ReactNode; className?: string; asChild?: boolean }> = ({
80+
children,
81+
className,
82+
}) => {
83+
return (
84+
<button className={cn('', className)}>
85+
{children}
86+
</button>
87+
);
88+
};
89+
90+
const SimpleDialogTitle: React.FC<SimpleDialogTitleProps> = ({ children, className }) => {
91+
return (
92+
<h2 className={cn('text-lg font-semibold leading-none tracking-tight', className)}>
93+
{children}
94+
</h2>
95+
);
96+
};
97+
98+
const SimpleDialogDescription: React.FC<SimpleDialogDescriptionProps> = ({ children, className }) => {
99+
return (
100+
<p className={cn('text-sm text-muted-foreground', className)}>
101+
{children}
102+
</p>
103+
);
104+
};
105+
106+
const SimpleDialogTrigger: React.FC<SimpleDialogTriggerProps> = ({ children }) => {
107+
return <>{children}</>;
108+
};
109+
110+
const SimpleDialog: React.FC<SimpleDialogProps> = ({
111+
isOpen,
112+
onOpenChange,
113+
children,
114+
className
115+
}) => {
116+
// Handle ESC key press
117+
useEffect(() => {
118+
if (!isOpen) return;
119+
120+
const onKeyDown = (e: KeyboardEvent) => {
121+
if (e.key === 'Escape') {
122+
onOpenChange(false);
123+
}
124+
};
125+
126+
window.addEventListener('keydown', onKeyDown);
127+
return () => window.removeEventListener('keydown', onKeyDown);
128+
}, [isOpen, onOpenChange]);
129+
130+
if (!isOpen) return null;
131+
132+
return (
133+
<div className={cn('', className)}>
134+
{children}
135+
</div>
136+
);
137+
};
138+
139+
// For compatibility with existing code
140+
const SimpleDialogHeader: React.FC<{ className?: string; children: React.ReactNode }> = ({
141+
className,
142+
children
143+
}) => (
144+
<div
145+
className={cn(
146+
'flex flex-col space-y-1.5 text-center sm:text-left',
147+
className
148+
)}
149+
>
150+
{children}
151+
</div>
152+
);
153+
154+
const SimpleDialogFooter: React.FC<{ className?: string; children: React.ReactNode }> = ({
155+
className,
156+
children
157+
}) => (
158+
<div
159+
className={cn(
160+
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
161+
className
162+
)}
163+
>
164+
{children}
165+
</div>
166+
);
167+
168+
export {
169+
SimpleDialog,
170+
SimpleDialogTrigger,
171+
SimpleDialogContent,
172+
SimpleDialogHeader,
173+
SimpleDialogFooter,
174+
SimpleDialogTitle,
175+
SimpleDialogDescription,
176+
SimpleDialogClose,
177+
SimpleDialogOverlay,
178+
SimpleDialogPortal
179+
};

src/components/ui/simple-label.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import { cn } from '@/lib/utils';
3+
import { cva, type VariantProps } from 'class-variance-authority';
4+
5+
const labelVariants = cva(
6+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
7+
);
8+
9+
interface SimpleLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement>, VariantProps<typeof labelVariants> {
10+
children: React.ReactNode;
11+
}
12+
13+
const SimpleLabel = React.forwardRef<HTMLLabelElement, SimpleLabelProps>(
14+
({ className, children, ...props }, ref) => (
15+
<label
16+
ref={ref}
17+
className={cn(labelVariants(), className)}
18+
{...props}
19+
>
20+
{children}
21+
</label>
22+
)
23+
);
24+
25+
SimpleLabel.displayName = 'SimpleLabel';
26+
27+
export { SimpleLabel };

0 commit comments

Comments
 (0)