feat: Refactor UI components for improved styling and accessibility in sharing dialogs

This commit is contained in:
Marco Beretta 2025-08-05 03:18:05 +02:00
parent ee33084848
commit a9b19fa956
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
11 changed files with 47 additions and 88 deletions

View file

@ -79,8 +79,7 @@ export default function AccessRolesPicker({
<Ariakit.MenuButton
aria-label={selectedRoleInfo?.description || 'Select role'}
className={cn(
'flex items-center justify-between gap-2 rounded-lg border border-border-light bg-surface-primary px-3 py-2 text-sm transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-ring-primary',
'min-w-[200px]',
'flex items-center justify-between gap-2 rounded-xl border border-border-light bg-transparent px-3 py-2 text-sm transition-colors hover:bg-surface-tertiary',
)}
>
<span className="font-medium">

View file

@ -1,8 +1,10 @@
import React, { useState } from 'react';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react';
import { Share2Icon, Users, Shield, Link, CopyCheck } from 'lucide-react';
import {
Label,
Button,
Spinner,
OGDialog,
OGDialogTitle,
OGDialogClose,
@ -20,8 +22,8 @@ import {
import GenericManagePermissionsDialog from './GenericManagePermissionsDialog';
import PublicSharingToggle from './PublicSharingToggle';
import AccessRolesPicker from './AccessRolesPicker';
import { cn, removeFocusOutlines } from '~/utils';
import { PeoplePicker } from './PeoplePicker';
import { cn } from '~/utils';
export default function GenericGrantAccessDialog({
resourceName,
@ -109,7 +111,6 @@ export default function GenericGrantAccessDialog({
setDefaultPermissionId(config?.defaultViewerRoleId);
setIsPublic(false);
setPublicRole(config?.defaultViewerRoleId);
setIsModalOpen(false);
} catch (error) {
console.error('Error granting access:', error);
showToast({
@ -134,26 +135,24 @@ export default function GenericGrantAccessDialog({
const TriggerComponent = children ? (
children
) : (
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
<Button
size="sm"
variant="outline"
aria-label={localize('com_ui_share_var', {
0: config?.getShareMessage(resourceName),
})}
type="button"
disabled={disabled}
>
<div className="flex items-center justify-center gap-2 text-blue-500">
<Share2Icon className="icon-md h-4 w-4" />
<div className="flex min-w-[32px] items-center justify-center gap-2 text-blue-500">
<span className="flex h-6 w-6 items-center justify-center">
<Share2Icon className="icon-md h-4 w-4" />
</span>
{totalCurrentShares > 0 && (
<span className="rounded-full bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-300">
{totalCurrentShares}
</span>
<Label className="text-sm font-medium text-text-secondary">{totalCurrentShares}</Label>
)}
</div>
</button>
</Button>
);
return (
@ -203,7 +202,7 @@ export default function GenericGrantAccessDialog({
onPublicRoleChange={setPublicRole}
resourceType={resourceType}
/>
<div className="flex justify-between border-t pt-4">
<div className="flex justify-between pt-4">
<div className="flex gap-2">
{hasPeoplePickerAccess && (
<GenericManagePermissionsDialog
@ -215,7 +214,6 @@ export default function GenericGrantAccessDialog({
{resourceId && resourceUrl && (
<Button
variant="outline"
size="sm"
onClick={() => {
if (isCopying) return;
copyResourceUrl(setIsCopying);
@ -250,7 +248,7 @@ export default function GenericGrantAccessDialog({
>
{updatePermissionsMutation.isLoading ? (
<div className="flex items-center gap-2">
<Loader className="h-4 w-4 animate-spin" />
<Spinner className="h-4 w-4" />
{localize('com_ui_granting')}
</div>
) : (

View file

@ -14,7 +14,6 @@ import type { TPrincipal, ResourceType, AccessRoleIds } from 'librechat-data-pro
import { useResourcePermissionState } from '~/hooks/Sharing';
import PublicSharingToggle from './PublicSharingToggle';
import { SelectedPrincipalsList } from './PeoplePicker';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
export default function GenericManagePermissionsDialog({
@ -190,20 +189,11 @@ export default function GenericManagePermissionsDialog({
const TriggerComponent = children ? (
children
) : (
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
aria-label={buttonAriaLabel}
type="button"
>
<div className="flex items-center justify-center gap-2 text-blue-500">
<Settings className="icon-md h-4 w-4" />
<span className="hidden sm:inline">{localize('com_ui_manage')}</span>
{originalTotalShares > 0 && `(${originalTotalShares})`}
</div>
</button>
<Button variant="outline" aria-label={buttonAriaLabel} type="button">
<Settings className="icon-md h-4 w-4" />
<span className="hidden sm:inline">{localize('com_ui_manage')}</span>
{originalTotalShares > 0 && `( ${originalTotalShares} )`}
</Button>
);
return (

View file

@ -232,7 +232,7 @@ export default function GrantAccessDialog({
onPublicRoleChange={setPublicRole}
resourceType={resourceType}
/>
<div className="flex justify-between border-t pt-4">
<div className="flex justify-between pt-4">
<div className="flex gap-2">
{hasPeoplePickerAccess && (
<ManagePermissionsDialog

View file

@ -83,7 +83,6 @@ export default function PeoplePicker({
});
setSearchQuery('');
}}
label={localize('com_ui_search_users_groups')}
isLoading={isLoading}
/>
</div>

View file

@ -69,15 +69,7 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
inputRef.current.focus();
}
};
const showClearIcon = localQuery.trim().length > 0;
const clearText = () => {
setLocalQuery('');
onQueryChange('');
debouncedOnQueryChange.cancel();
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<Ariakit.ComboboxProvider store={combobox}>
<Ariakit.ComboboxLabel className="text-token-text-primary mb-2 block font-medium">
@ -91,7 +83,7 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
)}
>
{isLoading ? (
<Spinner className="absolute left-3 h-4 w-4 text-text-primary" />
<Spinner className="absolute left-3 h-4 w-4" />
) : (
<Search className="absolute left-3 h-4 w-4 text-text-secondary group-focus-within:text-text-primary group-hover:text-text-primary" />
)}
@ -120,26 +112,12 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
placeholder={placeholder || localize('com_ui_select_options')}
className="m-0 mr-0 w-full rounded-md border-none bg-transparent p-0 py-2 pl-9 pr-3 text-sm leading-tight text-text-primary placeholder-text-secondary placeholder-opacity-100 focus:outline-none focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
/>
<button
type="button"
aria-label={`${localize('com_ui_clear')} ${localize('com_ui_search')}`}
className={cn(
'absolute right-[7px] flex h-5 w-5 items-center justify-center rounded-full border-none bg-transparent p-0 transition-opacity duration-200',
showClearIcon ? 'opacity-100' : 'opacity-0',
isSmallScreen === true ? 'right-[16px]' : '',
)}
onClick={clearText}
tabIndex={showClearIcon ? 0 : -1}
disabled={!showClearIcon}
>
<X className="h-5 w-5 cursor-pointer" />
</button>
</div>
</div>
<Ariakit.ComboboxPopover
portal={false} //todo fix focus when set to true
gutter={10}
// sameWidth
sameWidth
open={
isLoading ||
options.length > 0 ||

View file

@ -1,5 +1,5 @@
import React, { useState, useId } from 'react';
import * as Menu from '@ariakit/react/menu';
import * as Ariakit from '@ariakit/react/menu';
import { Button, DropdownPopup } from '@librechat/client';
import { Users, X, ExternalLink, ChevronDown } from 'lucide-react';
import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider';
@ -34,7 +34,7 @@ export default function SelectedPrincipalsList({
if (principles.length === 0) {
return (
<div className={`space-y-3 ${className}`}>
<div className="rounded-lg border border-dashed border-border py-8 text-center text-muted-foreground">
<div className="rounded-lg border border-dashed border-border-medium py-8 text-center text-muted-foreground">
<Users className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="mt-1 text-xs">{localize('com_ui_search_above_to_add')}</p>
</div>
@ -80,10 +80,10 @@ export default function SelectedPrincipalsList({
/>
)}
<Button
variant="ghost"
variant="outline"
size="sm"
onClick={() => onRemoveHandler(share.idOnTheSource!)}
className="h-8 w-8 p-0 hover:bg-destructive/10 hover:text-destructive"
className="h-8 w-8 p-0 hover:border-destructive/10 hover:bg-destructive/10 hover:text-destructive"
aria-label={localize('com_ui_remove_user', { 0: displayName })}
>
<X className="h-4 w-4" />
@ -122,10 +122,10 @@ function RoleSelector({ currentRole, onRoleChange, availableRoles }: RoleSelecto
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
trigger={
<Menu.MenuButton className="flex h-8 items-center gap-2 rounded-md border border-border-medium bg-surface-secondary px-2 py-1 text-sm font-medium transition-colors duration-200 hover:bg-surface-tertiary">
<Ariakit.MenuButton className="flex items-center justify-between gap-2 rounded-xl border border-border-light bg-transparent px-3 py-2 text-sm transition-colors hover:bg-surface-tertiary">
<span className="hidden sm:inline">{getLocalizedRoleName(currentRole)}</span>
<ChevronDown className="h-3 w-3" />
</Menu.MenuButton>
<ChevronDown className="h-4 w-4 text-text-secondary" />
</Ariakit.MenuButton>
}
items={availableRoles?.map((role) => ({
id: role.accessRoleId,

View file

@ -23,7 +23,7 @@ export default function PublicSharingToggle({
return (
<div className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="flex items-start gap-3">
<Globe className="mt-0.5 h-5 w-5 text-blue-500" />
<div>

View file

@ -1,15 +1,16 @@
import { useFormContext } from 'react-hook-form';
import {
OGDialog,
OGDialogTrigger,
Label,
OGDialogTemplate,
Button,
OGDialog,
TrashIcon,
useToastContext,
OGDialogTrigger,
OGDialogTemplate,
} from '@librechat/client';
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import { cn, logger, removeFocusOutlines, getDefaultAgentFormValues } from '~/utils';
import { logger, getDefaultAgentFormValues } from '~/utils';
import { useLocalize, useSetIndexOptions } from '~/hooks';
import { useDeleteAgentMutation } from '~/data-provider';
import { useChatContext } from '~/Providers';
@ -82,18 +83,16 @@ export default function DeleteButton({
return (
<OGDialog>
<OGDialogTrigger asChild>
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
<Button
size="sm"
variant="outline"
aria-label={localize('com_ui_delete') + ' ' + localize('com_ui_agent')}
type="button"
>
<div className="flex w-full items-center justify-center gap-2 text-red-500">
<TrashIcon />
</div>
</button>
</Button>
</OGDialogTrigger>
<OGDialogTemplate
title={localize('com_ui_delete') + ' ' + localize('com_ui_agent')}

View file

@ -1,7 +1,6 @@
import { CopyIcon } from 'lucide-react';
import { useToastContext } from '@librechat/client';
import { useToastContext, Button } from '@librechat/client';
import { useDuplicateAgentMutation } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks';
export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
@ -33,11 +32,9 @@ export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
};
return (
<button
className={cn(
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
removeFocusOutlines,
)}
<Button
size="sm"
variant="outline"
aria-label={localize('com_ui_duplicate') + ' ' + localize('com_ui_agent')}
type="button"
onClick={handleDuplicate}
@ -45,6 +42,6 @@ export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
<div className="flex w-full items-center justify-center gap-2 text-primary">
<CopyIcon className="size-4" />
</div>
</button>
</Button>
);
}

View file

@ -693,7 +693,6 @@
"com_ui_permission_level": "Permission Level",
"com_ui_grant_access": "Grant Access",
"com_ui_granting": "Granting...",
"com_ui_search_users_groups": "Search Users and Groups",
"com_ui_search_default_placeholder": "Search by name or email (min 2 chars)",
"com_ui_user": "User",
"com_ui_group": "Group",