diff --git a/app/components/@settings/core/ControlPanel.tsx b/app/components/@settings/core/ControlPanel.tsx index 5043e862..e359c058 100644 --- a/app/components/@settings/core/ControlPanel.tsx +++ b/app/components/@settings/core/ControlPanel.tsx @@ -5,13 +5,14 @@ import * as RadixDialog from '@radix-ui/react-dialog'; import { classNames } from '~/utils/classNames'; import { closeSettingsPanel, + resetControlPanelHeader, resetTabConfiguration, settingsPanelStore, tabConfigurationStore, } from '~/lib/stores/settings'; import type { TabType, TabVisibilityConfig } from './types'; import { DEFAULT_TAB_CONFIG, TAB_ICONS, TAB_LABELS } from './constants'; -import { CloseCircle } from 'iconsax-reactjs'; +import { ControlPanelHeader } from './ControlPanelHeader'; import { Settings } from 'lucide-react'; // Import all tab components @@ -22,6 +23,7 @@ import { useUserStore } from '~/lib/stores/user'; import { DeprecatedRole } from '@prisma/client'; import OrganizationTab from '~/components/@settings/tabs/organization/OrganizationTab'; import MembersTab from '~/components/@settings/tabs/members/MembersTab'; +import RolesTab from '~/components/@settings/tabs/roles/RolesTab'; const LAST_ACCESSED_TAB_KEY = 'control-panel-last-tab'; @@ -148,8 +150,15 @@ export const ControlPanel = () => { }; const handleTabClick = (tabId: TabType) => { + if (activeTab === tabId) { + return; + } + setActiveTab(tabId); + // Reset the header when switching tabs + resetControlPanelHeader(); + // Store the selected tab localStorage.setItem(LAST_ACCESSED_TAB_KEY, tabId); }; @@ -166,6 +175,8 @@ export const ControlPanel = () => { return ; case 'members': return ; + case 'roles': + return ; default: return null; } @@ -221,21 +232,16 @@ export const ControlPanel = () => { {/* Main Content */}
+ +
-
- -
{activeTab ? ( getTabComponent(activeTab) diff --git a/app/components/@settings/core/ControlPanelHeader.tsx b/app/components/@settings/core/ControlPanelHeader.tsx new file mode 100644 index 00000000..d0f3f012 --- /dev/null +++ b/app/components/@settings/core/ControlPanelHeader.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { useStore } from '@nanostores/react'; +import { classNames } from '~/utils/classNames'; +import { ArrowLeft } from 'lucide-react'; +import { CloseCircle } from 'iconsax-reactjs'; +import { closeSettingsPanel, settingsPanelStore } from '~/lib/stores/settings'; + +export const ControlPanelHeader = () => { + const { header } = useStore(settingsPanelStore); + + return ( +
+
+
+ {header.onBack && ( + + )} + {header.title && ( +

{header.title}

+ )} +
+
+
+ +
+
+ ); +}; diff --git a/app/components/@settings/core/constants.ts b/app/components/@settings/core/constants.ts index 850fa2c4..8cdb7ce3 100644 --- a/app/components/@settings/core/constants.ts +++ b/app/components/@settings/core/constants.ts @@ -1,5 +1,5 @@ import type { TabType, TabVisibilityConfig } from './types'; -import { Building, Database, GitBranch, type LucideIcon, Rocket, Users } from 'lucide-react'; +import { Building, Database, GitBranch, type LucideIcon, Rocket, Users, ShieldUser } from 'lucide-react'; export const TAB_ICONS: Record = { data: Database, @@ -7,6 +7,7 @@ export const TAB_ICONS: Record = { 'deployed-apps': Rocket, organization: Building, members: Users, + roles: ShieldUser, }; export const TAB_LABELS: Record = { @@ -15,6 +16,7 @@ export const TAB_LABELS: Record = { 'deployed-apps': 'Deployed Apps', organization: 'Organization', members: 'Members', + roles: 'Roles', }; export const TAB_DESCRIPTIONS: Record = { @@ -23,6 +25,7 @@ export const TAB_DESCRIPTIONS: Record = { 'deployed-apps': 'View and manage your deployed applications', organization: 'Manage your organization', members: 'Manage your organization members', + roles: 'Manage roles and permissions for organization members', }; export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [ @@ -32,4 +35,5 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [ { id: 'organization', visible: true, window: 'admin', order: 0 }, { id: 'members', visible: true, window: 'admin', order: 1 }, + { id: 'roles', visible: true, window: 'admin', order: 2 }, ]; diff --git a/app/components/@settings/core/types.ts b/app/components/@settings/core/types.ts index 1faf5b5c..43729c0a 100644 --- a/app/components/@settings/core/types.ts +++ b/app/components/@settings/core/types.ts @@ -1,4 +1,4 @@ -export type TabType = 'data' | 'github' | 'deployed-apps' | 'organization' | 'members'; +export type TabType = 'data' | 'github' | 'deployed-apps' | 'organization' | 'members' | 'roles'; export type WindowType = 'user' | 'admin'; @@ -30,4 +30,5 @@ export const TAB_LABELS: Record = { 'deployed-apps': 'Deployed Apps', organization: 'Organization', members: 'Members', + roles: 'Roles', }; diff --git a/app/components/@settings/tabs/roles/AssignRoleMembers.tsx b/app/components/@settings/tabs/roles/AssignRoleMembers.tsx new file mode 100644 index 00000000..bf9b829e --- /dev/null +++ b/app/components/@settings/tabs/roles/AssignRoleMembers.tsx @@ -0,0 +1,181 @@ +import { useState, useEffect, useMemo } from 'react'; +import { setControlPanelHeader } from '~/lib/stores/settings'; +import { Circle, CircleCheck, Search } from 'lucide-react'; +import { toast } from 'sonner'; +import type { Role, User } from './types'; + +type AssignRoleMembersProps = { + role: Role; + onRoleUpdate: (updatedRole: Role) => void; + closeAssignMembers: () => void; +}; + +export default function AssignRoleMembers({ role, onRoleUpdate, closeAssignMembers }: AssignRoleMembersProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [membersNotInRole, setMembersNotInRole] = useState([]); + const [selectedUsers, setSelectedUsers] = useState>(new Map()); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + setControlPanelHeader({ + title: `Assign members to "${role.name}" role`, + onBack: closeAssignMembers, + }); + + const fetchMembersNotInRole = async () => { + try { + setIsLoading(true); + + const response = await fetch('/api/organization/member'); + const data: { members: User[] } = await response.json(); + + if (data.members) { + const roleUserIds = new Set(role.users?.map((user) => user.id)); + const membersNotInRoleList: User[] = data.members + .filter((member) => !roleUserIds.has(member.id)) + .map((member) => ({ id: member.id, name: member.name, email: member.email })); + + setMembersNotInRole(membersNotInRoleList); + } + } catch (error) { + console.error('Error fetching organization members:', error); + toast.error('Failed to fetch organization members'); + } finally { + setIsLoading(false); + } + }; + + fetchMembersNotInRole(); + }, []); + + const handleAssignMembers = async () => { + if (selectedUsers.size === 0) { + return; + } + + setIsSaving(true); + + const newRoleUsers = new Map(selectedUsers); + + try { + await Promise.all( + Array.from(selectedUsers.values()).map((user) => + fetch(`/api/roles/${role.id}/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId: user.id }), + }).then((res) => { + if (!res.ok) { + const failedUser = membersNotInRole.find((m) => m.id === user.id); + toast.error(`Failed to assign member "${failedUser?.email || 'Unknown'}"`); + newRoleUsers.delete(user.id); + } + }), + ), + ); + + const updatedRole = { + ...role, + users: [...(role.users || []), ...Array.from(newRoleUsers.values())], + }; + onRoleUpdate(updatedRole); + closeAssignMembers(); + } finally { + setIsSaving(false); + } + }; + + const toggleSelectUser = (user: User) => { + if (!user?.id) { + return; + } + + setSelectedUsers((prev) => { + const newSelected = new Map(prev); + + if (newSelected.has(user.id)) { + newSelected.delete(user.id); + } else { + newSelected.set(user.id, user); + } + + return newSelected; + }); + }; + + const filteredMembers = useMemo( + () => + membersNotInRole.filter( + (member) => + member.name.toLowerCase().includes(searchQuery.toLowerCase()) || + member.email.toLowerCase().includes(searchQuery.toLowerCase()), + ), + [membersNotInRole, searchQuery], + ); + + if (isLoading) { + return ( +
+
Loading members...
+
+ ); + } + + return ( +
+
+
+ +
+ setSearchQuery(e.target.value)} + className="w-full h-9 pl-7 pr-2.5 py-2 rounded-[50px] bg-gray-600/50 text-sm text-white placeholder-gray-400 focus:outline-none" + placeholder="Search members..." + /> +
+ +
Assign members
+ +
+ {filteredMembers.map((member) => ( +
toggleSelectUser(member)} + > + {selectedUsers.has(member.id) ? ( + + ) : ( + + )} +
+
{member.name}
+
{member.email}
+
+
+ ))} + + {filteredMembers.length === 0 && !isLoading && ( +
No members found
+ )} +
+ +
+
+ +
+
+
+ ); +} diff --git a/app/components/@settings/tabs/roles/RoleDetails.tsx b/app/components/@settings/tabs/roles/RoleDetails.tsx new file mode 100644 index 00000000..51240251 --- /dev/null +++ b/app/components/@settings/tabs/roles/RoleDetails.tsx @@ -0,0 +1,273 @@ +import { useEffect, useState } from 'react'; +import * as ToggleGroup from '@radix-ui/react-toggle-group'; +import { Trash2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { resetControlPanelHeader, setControlPanelHeader } from '~/lib/stores/settings'; +import { Dialog, DialogClose, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; +import type { Role } from './types'; +import RoleMembers from './RoleMembers'; +import AssignRoleMembers from './AssignRoleMembers'; +import { classNames } from '~/utils/classNames'; + +const ROLE_TABS = ['Members', 'Environments', 'Data Sources', 'Apps']; +interface RoleDetailsProps { + role: Role; + onBack(): void; + onRoleUpdate: (updatedRole: Role) => void; + onRoleDelete: (roleId: string) => void; +} + +export default function RoleDetails({ role, onBack, onRoleUpdate, onRoleDelete }: RoleDetailsProps) { + const [roleName, setRoleName] = useState(role.name); + const [roleDescription, setRoleDescription] = useState(role.description); + const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [activeTab, setActiveTab] = useState('Members'); + const [showAssignedMembers, setShowAssignedMembers] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + + useEffect(() => { + setControlPanelHeader({ + title: `Edit "${role.name}"`, + onBack, + }); + + // clear the header when the component unmounts + return () => { + resetControlPanelHeader(); + }; + }, [role.name]); + + const getTabComponent = (tab: string) => { + switch (tab) { + case 'Members': + return ( + setShowAssignedMembers(true)} /> + ); + case 'Environments': + case 'Data Sources': + case 'Apps': + return
This feature is coming soon!
; + default: + return null; + } + }; + + const handleSave = async () => { + try { + setIsSaving(true); + + const response = await fetch(`/api/roles/${role.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name: roleName, description: roleDescription || null }), + }); + + const data: { success: boolean; role: Role; error?: string } = await response.json(); + + if (data.success) { + onRoleUpdate({ ...role, name: data.role.name, description: data.role.description }); + toast.success('Role updated successfully'); + } else { + toast.error(data.error || 'Failed to update role'); + } + } catch (error) { + console.error('Error updating role:', error); + toast.error('Failed to update role'); + } finally { + setIsSaving(false); + } + }; + + const handleDelete = async () => { + try { + setIsDeleting(true); + + const response = await fetch(`/api/roles/${role.id}`, { + method: 'DELETE', + }); + + const data: { success: boolean; error?: string } = await response.json(); + + if (data.success) { + toast.success('Role deleted successfully'); + onRoleDelete(role.id); + } else { + toast.error(data.error || 'Failed to delete role'); + } + } catch (error) { + console.error('Error deleting role:', error); + toast.error('Failed to delete role'); + } finally { + setIsDeleting(false); + } + }; + + const isSaveDisabled = + isSaving || roleName.trim() === '' || (role.name === roleName && role.description === roleDescription); + + if (showAssignedMembers) { + return ( + { + setShowAssignedMembers(false); + setControlPanelHeader({ + title: `Edit "${role.name}"`, + onBack, + }); + }} + /> + ); + } + + return ( +
+
+ + setRoleName(e.target.value)} + className="w-full px-2 py-1 rounded-lg bg-gray-700 border border-gray-600 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Enter role name" + /> +
+
+ + setRoleDescription(e.target.value)} + className="w-full px-2 py-1 rounded-lg bg-gray-700 border border-gray-600 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Enter role description" + /> +
+
+ { + if (value) { + setActiveTab(value); + } + }} + aria-label="Role tabs" + > + {ROLE_TABS.map((tab) => ( + + {tab} + + ))} + +
+
{getTabComponent(activeTab)}
+ +
+
+ + + +
+
+ + + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+

+ This action cannot be undone. All permissions associated with this role will be removed from the + members. +

+ {role.users.length > 0 && ( +

+ This role is currently assigned to {role.users.length} member(s). +

+ )} +

Are you sure you want to delete the role "{role.name}"?

+
+ +
+ + + + +
+
+
+
+
+
+ ); +} diff --git a/app/components/@settings/tabs/roles/RoleMembers.tsx b/app/components/@settings/tabs/roles/RoleMembers.tsx new file mode 100644 index 00000000..b8a216bb --- /dev/null +++ b/app/components/@settings/tabs/roles/RoleMembers.tsx @@ -0,0 +1,182 @@ +import React, { useState, useMemo } from 'react'; +import { CircleMinus, Plus, Search, Trash2 } from 'lucide-react'; +import { Dialog, DialogClose, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; +import { classNames } from '~/utils/classNames'; +import { toast } from 'sonner'; +import { useUserStore } from '~/lib/stores/user'; +import type { Role, User } from './types'; + +type RoleMembersProps = { + role: Role; + onRoleUpdate: (updatedRole: Role) => void; + onAssignMembers: () => void; +}; + +export default function RoleMembers({ role, onRoleUpdate, onAssignMembers }: RoleMembersProps) { + const { user } = useUserStore(); + const currentUserId = user?.id; + const [searchQuery, setSearchQuery] = useState(''); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [selectedMember, setSelectedMember] = useState(null); + + const members = role.users || []; + + const handleDeleteMember = async (roleId: string, memberId: string | undefined) => { + if (!memberId) { + return; + } + + setIsDeleting(true); + + try { + const response = await fetch(`/api/roles/${roleId}/users/${memberId}`, { + method: 'DELETE', + }); + const data: { success: boolean; error?: string } = await response.json(); + + if (data.success) { + onRoleUpdate({ + ...role, + users: members.filter((member) => member.id !== memberId), + }); + } else { + toast.error(data.error || 'Failed to remove member from role'); + } + } catch (error) { + console.error('Error removing member from role:', error); + toast.error('Failed to remove member from role'); + } finally { + setShowDeleteConfirm(false); + setSelectedMember(null); + setIsDeleting(false); + } + }; + + const filteredMembers = useMemo( + () => + members.filter( + (member) => + member.name.toLowerCase().includes(searchQuery.toLowerCase()) || + member.email.toLowerCase().includes(searchQuery.toLowerCase()), + ), + [members, searchQuery], + ); + + return ( +
+
+
+
+ +
+ setSearchQuery(e.target.value)} + className="w-full h-9 pl-7 pr-2.5 py-2 rounded-[50px] bg-gray-600/50 text-sm text-white placeholder-gray-400 focus:outline-none" + placeholder="Search members..." + /> +
+ +
+ +
+
+ Assigned Members +
+ +
+ {filteredMembers.map((member, index) => ( + +
+
+
{member.name}
+
{member.email}
+
+
+ {member.id !== currentUserId && ( + + )} +
+
+ {index < filteredMembers.length - 1 &&
} +
+ ))} +
+
+ + + +
+
+
+
+
+ +
+
+ +
+
+
+ +
+

+ They will lose access to all resources associated with this role. Are you sure you want to remove + them? +

+
+ +
+ + + + +
+
+
+
+
+
+ ); +} diff --git a/app/components/@settings/tabs/roles/RolesTab.tsx b/app/components/@settings/tabs/roles/RolesTab.tsx new file mode 100644 index 00000000..453c14ec --- /dev/null +++ b/app/components/@settings/tabs/roles/RolesTab.tsx @@ -0,0 +1,131 @@ +import { useState, useEffect, useMemo } from 'react'; +import { ChevronRight, Search } from 'lucide-react'; +import { toast } from 'sonner'; +import RoleDetails from './RoleDetails'; +import type { Role } from './types'; + +type LoaderData = { + roles: Role[]; +}; + +export default function RolesTab() { + const [searchQuery, setSearchQuery] = useState(''); + const [roles, setRoles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + + useEffect(() => { + fetchRoles(); + }, []); + + const fetchRoles = async () => { + try { + setIsLoading(true); + + const response = await fetch('/api/roles'); + const data: LoaderData = await response.json(); + + if (data.roles) { + setRoles(data.roles); + } + } catch (error) { + console.error('Error fetching roles:', error); + toast.error('Failed to fetch roles'); + } finally { + setIsLoading(false); + } + }; + + const handleRoleUpdate = (updatedRole: Role) => { + setRoles((prevRoles) => prevRoles.map((role) => (role.id === updatedRole.id ? { ...role, ...updatedRole } : role))); + + if (selectedRole && selectedRole.id === updatedRole.id) { + setSelectedRole(updatedRole); + } + }; + + const filteredRoles = useMemo( + () => roles.filter((role) => role.name.toLowerCase().includes(searchQuery.toLowerCase())), + [roles, searchQuery], + ); + + if (isLoading) { + return ( +
+
+

Roles

+ Loading... +
+
+
Loading roles...
+
+
+ ); + } + + if (selectedRole) { + return ( + setSelectedRole(null)} + onRoleUpdate={handleRoleUpdate} + onRoleDelete={(roleId: string) => { + setRoles((prevRoles) => prevRoles.filter((role) => role.id !== roleId)); + setSelectedRole(null); + }} + /> + ); + } + + return ( +
+
+

Roles

+ {filteredRoles.length} +
+ +
+
+ +
+ setSearchQuery(e.target.value)} + className="w-[566px] h-9 pl-10 pr-2.5 py-2 rounded-[50px] bg-gray-600/50 text-white placeholder-gray-400 focus:outline-none" + placeholder="Search..." + /> +
+ + + + + + + + + + + {filteredRoles.map((role) => ( + setSelectedRole(role)} + > + + + + + ))} + +
RoleMembers
+
{role.name}
+ {role.description &&
{role.description}
} +
{role.users.length} +
+ +
+
+
+ ); +} diff --git a/app/components/@settings/tabs/roles/types.ts b/app/components/@settings/tabs/roles/types.ts new file mode 100644 index 00000000..f00479d0 --- /dev/null +++ b/app/components/@settings/tabs/roles/types.ts @@ -0,0 +1,24 @@ +export interface Role { + id: string; + name: string; + description: string; + organizationId: string; + permissions: Permission[]; + users: User[]; +} + +export interface Permission { + id: string; + roleId: string; + action: string; + resource: string; + environmentId: string | null; + dataSourceId: string | null; + websiteId: string | null; +} + +export interface User { + id: string; + name: string; + email: string; +} diff --git a/app/lib/services/roleService.ts b/app/lib/services/roleService.ts index aa44b32e..0e63a329 100644 --- a/app/lib/services/roleService.ts +++ b/app/lib/services/roleService.ts @@ -11,11 +11,25 @@ export async function getRole(id: string): Promise { } export async function getRoles(): Promise { - return prisma.role.findMany({ + const roles = await prisma.role.findMany({ include: { permissions: true, + users: { + include: { + user: true, + }, + }, }, }); + + return roles.map((role) => ({ + ...role, + users: role.users.map((user) => ({ + id: user.user.id, + name: user.user.name, + email: user.user.email, + })), + })); } export async function createRole( diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index fd614e1a..0d407373 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -221,9 +221,7 @@ export const useSettingsStore = create((set) => ({ openSettings: () => { set({ isOpen: true, - selectedTab: 'user', - - // Always open to user tab + selectedTab: 'user', // Always open to user tab }); }, @@ -239,11 +237,20 @@ export const useSettingsStore = create((set) => ({ }, })); +export interface ControlPanelHeader { + title: string | null; + onBack: (() => void) | null; +} + export const settingsPanelStore = atom({ isOpen: false, selectedTab: 'data' as string | null, showAddForm: false, isHomepageSource: false, + header: { + title: null, + onBack: null, + } as ControlPanelHeader, }); export const openSettingsPanel = ( @@ -251,9 +258,41 @@ export const openSettingsPanel = ( showAddForm: boolean = false, isHomepageSource = false, ) => { - settingsPanelStore.set({ isOpen: true, selectedTab: tab, showAddForm, isHomepageSource }); + settingsPanelStore.set({ + isOpen: true, + selectedTab: tab, + showAddForm, + isHomepageSource, + header: { title: null, onBack: null }, + }); }; export const closeSettingsPanel = () => { - settingsPanelStore.set({ isOpen: false, selectedTab: null, showAddForm: false, isHomepageSource: false }); + settingsPanelStore.set({ + isOpen: false, + selectedTab: null, + showAddForm: false, + isHomepageSource: false, + header: { title: null, onBack: null }, + }); +}; + +export const setControlPanelHeader = (newHeader: Partial) => { + const currentStore = settingsPanelStore.get(); + settingsPanelStore.set({ + ...currentStore, + header: { ...currentStore.header, ...newHeader }, + }); +}; + +export const resetControlPanelHeader = () => { + const currentStore = settingsPanelStore.get(); + + // Only update if it's not already in its default state + if (currentStore.header.title !== null || currentStore.header.onBack !== null) { + settingsPanelStore.set({ + ...currentStore, + header: { title: null, onBack: null }, + }); + } }; diff --git a/package.json b/package.json index 6247b4d5..23ee5c7f 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.1.4", "@t3-oss/env-nextjs": "^0.13.8", "@tailwindcss/postcss": "^4.1.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ec65c73..dc743935 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,6 +165,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.1.2 version: 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': + specifier: ^1.1.11 + version: 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-tooltip': specifier: ^1.1.4 version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2937,6 +2940,9 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -3203,6 +3209,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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-scroll-area@1.2.9': resolution: {integrity: sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==} peerDependencies: @@ -3277,6 +3296,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + 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-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + 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-tooltip@1.2.7': resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} peerDependencies: @@ -14152,6 +14197,8 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14427,6 +14474,23 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-scroll-area@1.2.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.1 @@ -14520,6 +14584,32 @@ snapshots: '@types/react': 18.3.23 '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2