mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
🤲 a11y: Sidebar Text Contrast (#3665)
* fix: use theme class for valid contrast for light/dark, conversation group names, fix type issues * fix: searchbar text contrast styling, closes #3469 * style: add theming for prompt cards, fix a11y contrast issues * a11y: address placeholder text contrast issues in LLM controls section * chore: Add conditional check for pull request repository in a11y workflow * style: Update text color contrast to WCAG standard for placeholder and focus states in AssistantPanel components
This commit is contained in:
parent
f8a5dad265
commit
91fc89076f
16 changed files with 62 additions and 51 deletions
12
.github/workflows/a11y.yml
vendored
12
.github/workflows/a11y.yml
vendored
|
@ -4,15 +4,23 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- 'client/src/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_workflow:
|
||||
description: 'Set to true to run this workflow'
|
||||
required: true
|
||||
default: 'false'
|
||||
|
||||
jobs:
|
||||
axe-linter:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.repo.full_name == 'danny-avila/LibreChat'
|
||||
if: >
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'danny-avila/LibreChat') ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_workflow == 'true')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dequelabs/axe-linter-action@v1
|
||||
with:
|
||||
api_key: ${{ secrets.AXE_LINTER_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -110,7 +110,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
placeholder={localize('com_files_filter')}
|
||||
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm dark:border-gray-500"
|
||||
className="max-w-sm border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
@ -175,7 +175,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
@ -184,7 +184,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
>
|
||||
{row.getVisibleCells().map((cell, index) => {
|
||||
const maxWidth =
|
||||
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>)?.meta?.size ??
|
||||
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>).meta.size ??
|
||||
'auto';
|
||||
|
||||
const style: Style = {};
|
||||
|
|
|
@ -10,7 +10,7 @@ const Conversations = ({
|
|||
moveToTop,
|
||||
toggleNav,
|
||||
}: {
|
||||
conversations: TConversation[];
|
||||
conversations: Array<TConversation | null>;
|
||||
moveToTop: () => void;
|
||||
toggleNav: () => void;
|
||||
}) => {
|
||||
|
@ -33,15 +33,15 @@ const Conversations = ({
|
|||
{groupedConversations.map(([groupName, convos]) => (
|
||||
<div key={groupName}>
|
||||
<div
|
||||
className="text-text-secondary"
|
||||
style={{
|
||||
color: '#535353',
|
||||
fontSize: '0.7rem',
|
||||
marginTop: '20px',
|
||||
marginBottom: '5px',
|
||||
paddingLeft: '10px',
|
||||
}}
|
||||
>
|
||||
{localize(groupName) || groupName}
|
||||
{localize(groupName) ?? groupName}
|
||||
</div>
|
||||
{convos.map((convo, i) => (
|
||||
<Convo
|
||||
|
|
|
@ -150,7 +150,7 @@ export default function DataTableFile<TData, TValue>({
|
|||
placeholder={localize('com_files_filter')}
|
||||
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
||||
className="max-w-sm dark:border-gray-500"
|
||||
className="max-w-sm border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
<UploadFileButton
|
||||
onClick={() => {
|
||||
|
@ -204,7 +204,7 @@ export default function DataTableFile<TData, TValue>({
|
|||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
@ -213,7 +213,7 @@ export default function DataTableFile<TData, TValue>({
|
|||
>
|
||||
{row.getVisibleCells().map((cell, index) => {
|
||||
const maxWidth =
|
||||
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>)?.meta?.size ??
|
||||
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>).meta.size ??
|
||||
'auto';
|
||||
|
||||
const style: Style = {};
|
||||
|
|
|
@ -164,7 +164,7 @@ export default function FileSidePanel() {
|
|||
onChange={() => {
|
||||
console.log('changed');
|
||||
}}
|
||||
className="max-w-sm dark:border-gray-500"
|
||||
className="max-w-sm border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
|
|
|
@ -232,7 +232,7 @@ export default function VectorStoreSidePanel() {
|
|||
onChange={() => {
|
||||
console.log('changed');
|
||||
}}
|
||||
className="max-w-sm dark:border-gray-500"
|
||||
className="max-w-sm border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
|
|
|
@ -60,14 +60,16 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover dark:focus-within:bg-surface-hover',
|
||||
'group relative mt-1 flex h-10 cursor-pointer items-center gap-3 rounded-lg border-border-medium px-3 py-2 text-text-primary transition-colors duration-200 focus-within:bg-surface-hover hover:bg-surface-hover',
|
||||
isSmallScreen === true ? 'h-16 rounded-2xl' : '',
|
||||
)}
|
||||
>
|
||||
{<Search 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" />
|
||||
}
|
||||
<input
|
||||
type="text"
|
||||
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 outline-none dark:placeholder-opacity-100"
|
||||
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
|
||||
value={text}
|
||||
onChange={onChange}
|
||||
onKeyDown={(e) => {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { useState, useMemo } from 'react';
|
|||
import { Menu as MenuIcon, Edit as EditIcon, EarthIcon, TextSearch } from 'lucide-react';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuGroup,
|
||||
|
@ -29,12 +28,12 @@ export default function ChatGroupItem({
|
|||
const [isVariableDialogOpen, setVariableDialogOpen] = useState(false);
|
||||
const onEditClick = useCustomLink<HTMLDivElement>(`/d/prompts/${group._id}`);
|
||||
const groupIsGlobal = useMemo(
|
||||
() => instanceProjectId && group.projectIds?.includes(instanceProjectId),
|
||||
() => instanceProjectId != null && group.projectIds?.includes(instanceProjectId),
|
||||
[group, instanceProjectId],
|
||||
);
|
||||
const isOwner = useMemo(() => user?.id === group.author, [user, group]);
|
||||
|
||||
const onCardClick = () => {
|
||||
const onCardClick: React.MouseEventHandler<HTMLButtonElement> = () => {
|
||||
const text = group.productionPrompt?.prompt ?? '';
|
||||
if (!text) {
|
||||
return;
|
||||
|
@ -53,23 +52,26 @@ export default function ChatGroupItem({
|
|||
name={group.name}
|
||||
category={group.category ?? ''}
|
||||
onClick={onCardClick}
|
||||
snippet={group.oneliner ? group.oneliner : group.productionPrompt?.prompt ?? ''}
|
||||
snippet={
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
: group.productionPrompt?.prompt ?? ''
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{groupIsGlobal && <EarthIcon className="icon-md text-green-400" />}
|
||||
{groupIsGlobal === true && <EarthIcon className="icon-md text-green-400" />}
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
id="promtps-menu-trigger"
|
||||
aria-label="promtps-menu-trigger"
|
||||
variant="outline"
|
||||
<button
|
||||
id="prompts-menu-trigger"
|
||||
aria-label="prompts-menu-trigger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="z-50 h-7 w-7 p-0 transition-all duration-300 ease-in-out hover:border-white dark:bg-gray-800 dark:hover:border-gray-400 dark:focus:border-gray-500"
|
||||
className="z-50 inline-flex h-7 w-7 items-center justify-center rounded-md border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-secondary focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<MenuIcon className="icon-md dark:text-gray-300" />
|
||||
</Button>
|
||||
<MenuIcon className="icon-md text-text-secondary" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="z-50 mt-2 w-36 rounded-lg"
|
||||
|
@ -81,7 +83,7 @@ export default function ChatGroupItem({
|
|||
e.stopPropagation();
|
||||
setPreviewDialogOpen(true);
|
||||
}}
|
||||
className="w-full cursor-pointer rounded-lg disabled:cursor-not-allowed dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700"
|
||||
className="w-full cursor-pointer rounded-lg text-text-secondary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
|
||||
>
|
||||
<TextSearch className="mr-2 h-4 w-4" />
|
||||
<span>{localize('com_ui_preview')}</span>
|
||||
|
@ -90,7 +92,7 @@ export default function ChatGroupItem({
|
|||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
disabled={!isOwner}
|
||||
className="cursor-pointer rounded-lg disabled:cursor-not-allowed dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:bg-gray-700"
|
||||
className="cursor-pointer rounded-lg text-text-secondary hover:bg-surface-hover focus:bg-surface-hover disabled:cursor-not-allowed"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEditClick(e);
|
||||
|
|
|
@ -152,7 +152,7 @@ export default function FilterPrompts({
|
|||
setDisplayName(e.target.value);
|
||||
setName(e.target.value);
|
||||
}}
|
||||
className="w-full border-border-light"
|
||||
className="w-full border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -10,27 +10,25 @@ export default function ListCard({
|
|||
category: string;
|
||||
name: string;
|
||||
snippet: string;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top
|
||||
text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-all duration-300 ease-in-out hover:bg-gray-100 dark:border-gray-700 dark:hover:bg-gray-700"
|
||||
className="relative my-2 flex w-full cursor-pointer flex-col gap-2 rounded-2xl border border-border-light px-3 pb-4 pt-3 text-start
|
||||
align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-all duration-300 ease-in-out hover:bg-surface-tertiary"
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex flex-row gap-2">
|
||||
<CategoryIcon category={category} className="icon-md" />
|
||||
<h3 className="break-word select-none text-balance text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<h3 className="break-word select-none text-balance text-sm font-semibold text-text-primary">
|
||||
{name}
|
||||
</h3>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
<div className="ellipsis select-none text-balance text-sm text-gray-600 dark:text-gray-400">
|
||||
{snippet}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ellipsis select-none text-balance text-sm text-text-secondary">{snippet}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ const BookmarkTable = () => {
|
|||
placeholder={localize('com_ui_bookmarks_filter')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full border-border-light"
|
||||
className="w-full border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
|
||||
|
|
|
@ -271,7 +271,7 @@ export default function AssistantPanel({
|
|||
name="id"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<p className="h-3 text-xs italic text-gray-600">{field.value ?? ''}</p>
|
||||
<p className="h-3 text-xs italic text-text-secondary">{field.value ?? ''}</p>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -310,7 +310,7 @@ export default function AssistantPanel({
|
|||
{...field}
|
||||
value={field.value ?? ''}
|
||||
{...{ max: 32768 }}
|
||||
className={cn(inputClass, 'min-h-[100px] resize-none resize-y')}
|
||||
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
||||
id="instructions"
|
||||
placeholder={localize('com_assistants_instructions_placeholder')}
|
||||
rows={3}
|
||||
|
|
|
@ -45,7 +45,7 @@ export default function CodeFiles({
|
|||
|
||||
const endpointFileConfig = fileConfig.endpoints[endpoint];
|
||||
|
||||
if (endpointFileConfig?.disabled) {
|
||||
if (endpointFileConfig.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default function CodeFiles({
|
|||
return (
|
||||
<div className="mb-2 w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-token-text-tertiary rounded-lg text-xs">
|
||||
<div className="rounded-lg text-xs text-text-secondary">
|
||||
{localize('com_assistants_code_interpreter_files')}
|
||||
</div>
|
||||
<FileRow
|
||||
|
|
|
@ -77,7 +77,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
placeholder={localize('com_files_filter')}
|
||||
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
|
||||
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
||||
className="w-full border-border-light"
|
||||
className="w-full border-border-light placeholder:text-text-secondary"
|
||||
/>
|
||||
</div>
|
||||
<div className="overflow-y-auto rounded-md border border-black/10 dark:border-white/10">
|
||||
|
@ -102,7 +102,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
|
|
@ -13,7 +13,6 @@ import { EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
|
|||
import type {
|
||||
TConversation,
|
||||
ConversationData,
|
||||
ConversationUpdater,
|
||||
GroupedConversations,
|
||||
ConversationListResponse,
|
||||
} from 'librechat-data-provider';
|
||||
|
@ -61,7 +60,9 @@ const getGroupName = (date: Date) => {
|
|||
return ' ' + getYear(date).toString();
|
||||
};
|
||||
|
||||
export const groupConversationsByDate = (conversations: TConversation[]): GroupedConversations => {
|
||||
export const groupConversationsByDate = (
|
||||
conversations: Array<TConversation | null>,
|
||||
): GroupedConversations => {
|
||||
if (!Array.isArray(conversations)) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -60,13 +60,13 @@ export const cardStyle =
|
|||
'transition-colors rounded-md min-w-[75px] border font-normal bg-white hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700 dark:bg-gray-800 text-black dark:text-gray-600 focus:outline-none data-[state=open]:bg-gray-50 dark:data-[state=open]:bg-gray-700';
|
||||
|
||||
export const defaultTextProps =
|
||||
'rounded-md border border-gray-200 focus:border-gray-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:focus:bg-gray-600 dark:focus:border-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:outline-none';
|
||||
'rounded-md border border-gray-200 focus:border-gray-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none focus-within:placeholder:text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-700 dark:border-gray-600 dark:focus:bg-gray-600 dark:focus:border-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:outline-none';
|
||||
|
||||
export const optionText =
|
||||
'p-0 shadow-none text-right pr-1 h-8 border-transparent hover:bg-gray-800/10 dark:hover:bg-white/10 dark:focus:bg-white/10 transition-colors';
|
||||
|
||||
export const defaultTextPropsLabel =
|
||||
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-600 dark:focus:outline-none';
|
||||
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none focus-within:placeholder:text-text-primary placeholder:text-text-secondary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-600 dark:focus:outline-none';
|
||||
|
||||
export function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue