feat: add install prompt
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m52s

This commit is contained in:
Nathan Lamy 2025-08-19 21:59:56 +02:00
parent 6428126514
commit d4f1bb5dcd
7 changed files with 557 additions and 1 deletions

View file

@ -5,6 +5,7 @@ import Preferences from "./preferences";
import BottomNavigation from "~/components/bottom-nav";
import Profile from "./profile";
import type { User } from "~/lib/api";
import NotificationSettings from "./notifications";
export default function SettingsPage({ user }: { user: User }) {
const tabs = [
@ -24,7 +25,7 @@ export default function SettingsPage({ user }: { user: User }) {
value: "notifications",
label: "Notifications",
icon: <Bell className="h-4 w-4" />,
content: <div>WIP</div>,
content: <NotificationSettings user={user} />,
},
];

View file

@ -0,0 +1,120 @@
import {
ChevronDown,
Chrome,
Info,
MoreVertical,
Share,
Smartphone,
} from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { Button } from "../ui/button";
import { CardDescription } from "../ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@radix-ui/react-accordion";
import { Separator } from "../ui/separator";
import { useEffect, useState } from "react";
export default function InstallApp() {
const [supportsPWA, setSupportsPWA] = useState(false);
const [promptInstall, setPromptInstall] = useState<any>(null);
useEffect(() => {
const handler = (e: any) => {
e.preventDefault();
console.log("we are being triggered :D");
setSupportsPWA(true);
setPromptInstall(e);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("transitionend", handler);
}, []);
const onClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
evt.preventDefault();
if (!promptInstall) {
return;
}
promptInstall.prompt();
};
return (
<>
<Alert className="bg-blue-50 border border-blue-200 dark:bg-blue-900 dark:border-blue-700">
<Info className="text-blue-600! dark:text-blue-400! h-5 w-5" />
<AlertTitle className="text-blue-900 dark:text-white">
Informations importantes
</AlertTitle>
<AlertDescription>
<ul className="space-y-1 text-blue-800 dark:text-blue-300">
<li>
Pour recevoir des notifications, vous devez installer notre
application sur votre appareil.
</li>
<li>
Vous devrez autoriser les notifications lorsque l'application
vous le demandera.
</li>
</ul>
</AlertDescription>
</Alert>
{/* Install PWA button */}
<Button
className="w-full"
onClick={onClick}
disabled={!supportsPWA}
>
<Smartphone className="h-4 w-4 sm:h-5 sm:w-5 mr-2" />
Installer l'application
</Button>
<Separator />
<CardDescription>
Si le bouton ci-dessus ne fonctionne pas, suivez ces étapes :
</CardDescription>
<Accordion type="multiple" className="w-full space-y-4">
<AccordionItem value="android">
<AccordionTrigger className="flex items-center gap-1 group">
<h4>Instructions Android (Chrome)</h4>
<ChevronDown className="w-4 h-4 text-primary transition-transform group-data-[state=open]:rotate-180" />
</AccordionTrigger>
<AccordionContent>
<ol className="space-y-2 list-decimal list-inside pl-6 text-muted-foreground text-sm">
<li>Ouvrez ce site avec Google Chrome</li>
<li>Appuyez sur les trois points en haut à droite</li>
<li>Sélectionnez "Ajouter à l'écran d'accueil"</li>
<li>Confirmez pour installer l'application</li>
</ol>
</AccordionContent>
</AccordionItem>
<AccordionItem value="ios">
<AccordionTrigger className="flex items-center gap-1 group">
<h4>Instructions iOS (Safari)</h4>
<ChevronDown className="w-4 h-4 text-primary transition-transform group-data-[state=open]:rotate-180" />
</AccordionTrigger>
<AccordionContent>
<ol className="space-y-2 list-decimal list-inside pl-6 text-muted-foreground text-sm">
<li>Ouvrez ce site avec Safari</li>
<li>
Appuyez sur l'icône de partage en bas (le carré avec une flèche)
</li>
<li>
Faites défiler vers le bas et sélectionnez "Sur l'écran
d'accueil"
</li>
<li>Confirmez pour installer l'application</li>
</ol>
</AccordionContent>
</AccordionItem>
</Accordion>
</>
);
}

View file

@ -0,0 +1,247 @@
import { useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Switch } from "~/components/ui/switch";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import {
Bell,
Smartphone,
Monitor,
Trash2,
Send,
AlertTriangle,
Info,
} from "lucide-react";
import InstallApp from "./install-app";
export default function NotificationSettings() {
// TODO: Replace with actual user data
const [pushEnabled, setPushEnabled] = useState(false);
const [events, setEvents] = useState([
{ id: "new-message", label: "New Message", enabled: true },
{ id: "comment-reply", label: "Comment Reply", enabled: true },
{ id: "app-update", label: "App Update", enabled: false },
{ id: "weekly-digest", label: "Weekly Digest", enabled: true },
{ id: "security-alert", label: "Security Alert", enabled: true },
{ id: "friend-request", label: "Friend Request", enabled: false },
]);
const [subscriptions] = useState([
{
id: "1",
name: "iPhone 15 Pro",
type: "mobile",
status: "active",
lastSeen: "2 hours ago",
},
{
id: "2",
name: "MacBook Pro",
type: "desktop",
status: "active",
lastSeen: "5 minutes ago",
},
{
id: "3",
name: "Chrome on Windows",
type: "desktop",
status: "revoked",
lastSeen: "3 days ago",
},
]);
const toggleEvent = (eventId: string) => {
setEvents(
events.map((event) =>
event.id === eventId ? { ...event, enabled: !event.enabled } : event
)
);
};
return (
<div className="max-w-4xl mx-auto space-y-6 sm:space-y-8">
<div className="space-y-2">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
Paramètres de notifications
</h1>
<p className="text-sm sm:text-base text-muted-foreground">
Gérez vos préférences de notification et vos appareils abonnés.
</p>
</div>
{/* Enable Push Notifications Section */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Bell className="h-5 w-5" />
<CardTitle>Activer les notifications</CardTitle>
</div>
<CardDescription>
Autorisez cette application à vous envoyer des notifications push.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* <div className="flex items-start sm:items-center justify-between gap-4">
<div className="space-y-1 flex-1">
<p className="font-medium text-sm sm:text-base">
Notifications
</p>
<p className="text-xs sm:text-sm text-muted-foreground">
Recevez des notifications même lorsque lapplication est fermée.
</p>
</div>
<Switch
checked={pushEnabled}
onCheckedChange={setPushEnabled}
aria-label="Enable push notifications"
className="flex-shrink-0"
/>
</div> */}
<InstallApp />
</CardContent>
</Card>
{/* Select Notification Events Section */}
<Card>
<CardHeader>
<CardTitle>Select Notification Events</CardTitle>
<CardDescription>
Choose which events you want to receive notifications for
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3 sm:space-y-4">
{events.map((event, index) => (
<div key={event.id}>
<div className="flex items-start sm:items-center justify-between py-2 gap-4">
<div className="space-y-1 flex-1 min-w-0">
<p className="font-medium text-sm sm:text-base">
{event.label}
</p>
<p className="text-xs sm:text-sm text-muted-foreground leading-relaxed">
{event.id === "new-message" &&
"Get notified when you receive a new message"}
{event.id === "comment-reply" &&
"Get notified when someone replies to your comment"}
{event.id === "app-update" &&
"Get notified when a new app version is available"}
{event.id === "weekly-digest" &&
"Receive a weekly summary of your activity"}
{event.id === "security-alert" &&
"Important security notifications and alerts"}
{event.id === "friend-request" &&
"Get notified when someone sends you a friend request"}
</p>
</div>
<Switch
checked={event.enabled}
onCheckedChange={() => toggleEvent(event.id)}
aria-label={`Toggle ${event.label} notifications`}
className="flex-shrink-0"
/>
</div>
{index < events.length - 1 && (
<Separator className="mt-3 sm:mt-4" />
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* Manage Subscriptions Section */}
<Card>
<CardHeader>
<CardTitle>Manage Subscriptions</CardTitle>
<CardDescription>
View and manage devices that can receive notifications
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3 sm:space-y-4">
{subscriptions.map((subscription, index) => (
<div key={subscription.id}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex-shrink-0">
{subscription.type === "mobile" ? (
<Smartphone className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" />
) : (
<Monitor className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" />
)}
</div>
<div className="space-y-1 flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<p className="font-medium text-sm sm:text-base truncate">
{subscription.name}
</p>
{subscription.status === "revoked" && (
<Badge
variant="destructive"
className="flex items-center gap-1 text-xs"
>
<AlertTriangle className="h-3 w-3" />
Revoked
</Badge>
)}
{subscription.status === "active" && (
<Badge variant="secondary" className="text-xs">
Active
</Badge>
)}
</div>
<p className="text-xs sm:text-sm text-muted-foreground">
Last seen {subscription.lastSeen}
</p>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-2 self-start sm:self-center">
<Button
variant="outline"
size="sm"
disabled={subscription.status === "revoked"}
className="flex items-center gap-1 bg-transparent text-xs sm:text-sm px-2 sm:px-3"
>
<Send className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden xs:inline">Test</span>
</Button>
<Button
variant="outline"
size="sm"
className="flex items-center gap-1 text-destructive hover:text-destructive bg-transparent text-xs sm:text-sm px-2 sm:px-3"
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden xs:inline">Delete</span>
</Button>
</div>
</div>
{index < subscriptions.length - 1 && (
<Separator className="mt-3 sm:mt-4" />
)}
</div>
))}
{subscriptions.length === 0 && (
<div className="text-center py-6 sm:py-8 text-muted-foreground">
<Bell className="h-10 w-10 sm:h-12 sm:w-12 mx-auto mb-3 sm:mb-4 opacity-50" />
<p className="text-sm sm:text-base">
No subscribed devices found
</p>
<p className="text-xs sm:text-sm">
Enable push notifications to add this device
</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,64 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "~/lib/utils"
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View file

@ -0,0 +1,29 @@
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "~/lib/utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };

View file

@ -11,6 +11,7 @@
},
"dependencies": {
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.14",
@ -21,6 +22,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.12",
"@react-router/node": "^7.5.3",
"@react-router/serve": "^7.5.3",

93
pnpm-lock.yaml generated
View file

@ -11,6 +11,9 @@ importers:
'@marsidev/react-turnstile':
specifier: ^1.1.0
version: 1.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-accordion':
specifier: ^1.2.12
version: 1.2.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-avatar':
specifier: ^1.1.10
version: 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -41,6 +44,9 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.3(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-tabs':
specifier: ^1.1.12
version: 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -1645,6 +1651,19 @@ packages:
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-accordion@1.2.12':
resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-arrow@1.1.7':
resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
peerDependencies:
@ -1684,6 +1703,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-collapsible@1.1.12':
resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@ -1972,6 +2004,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-switch@1.2.6':
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tabs@1.1.12':
resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==}
peerDependencies:
@ -8166,6 +8211,23 @@ snapshots:
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-accordion@1.2.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-direction': 1.1.1(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1)
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
@ -8204,6 +8266,22 @@ snapshots:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.1)
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1)
@ -8520,6 +8598,21 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.8
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.1)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.1)
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
optionalDependencies:
'@types/react': 19.1.8
'@types/react-dom': 19.1.6(@types/react@19.1.8)
'@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies:
'@radix-ui/primitive': 1.1.2