diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 278021657..a182138ac 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -978,7 +978,7 @@ "Search pages and spaces...": "Search pages and spaces...", "No results found": "No results found", "You don't have permission to create pages here": "You don't have permission to create pages here", - "Chat menu": "Chat menu", + "Chat menu for {{title}}": "Chat menu for {{title}}", "API key menu": "API key menu", "Jump to comment selection": "Jump to comment selection", "Slash commands": "Slash commands", @@ -1064,7 +1064,7 @@ "Filter": "Filter", "Page title": "Page title", "Page content": "Page content", - "Member actions": "Member actions", + "Member actions for {{name}}": "Member actions for {{name}}", "Toggle password visibility": "Toggle password visibility", "Send comment": "Send comment", "Token actions": "Token actions", diff --git a/apps/client/src/components/layouts/global/global-sidebar.tsx b/apps/client/src/components/layouts/global/global-sidebar.tsx index 4670dae40..5ec322a58 100644 --- a/apps/client/src/components/layouts/global/global-sidebar.tsx +++ b/apps/client/src/components/layouts/global/global-sidebar.tsx @@ -105,7 +105,7 @@ export default function GlobalSidebar() {
- {t("Favorite spaces")} + {t("Favorite spaces")} {!isFavoritesPending && sortedFavoriteSpaces.length === 0 ? ( {t("Favorite spaces appear here")} diff --git a/apps/client/src/components/ui/custom-avatar.tsx b/apps/client/src/components/ui/custom-avatar.tsx index 0cf20a51b..c708b1769 100644 --- a/apps/client/src/components/ui/custom-avatar.tsx +++ b/apps/client/src/components/ui/custom-avatar.tsx @@ -16,13 +16,10 @@ interface CustomAvatarProps { mt?: string | number; } -// `color.shade` pairs whose contrast meets WCAG AA (4.5:1) in BOTH variants: -// - filled: white text on the shade as bg -// - light: shade as text on the color's light-bg (10% color.6 over white) -// Avoids lime/yellow/green/orange — even their dark shades have weak -// contrast. grape and indigo were bumped from .7 to darker shades because -// the original picks failed: grape.7 was 4.02/3.61 (both fail) and -// indigo.7 was 4.98/4.39 (light fails by a hair). +// color.shade picks whose FILLED variant (white text on the shade) meets WCAG AA 4.5:1. +// Avoids lime/yellow/green/orange, too light even at dark shades. +// For non-filled variants, initials text is forced to the .9 shade at render time: +// Mantine otherwise caps light-variant placeholder text at .6, dropping contrast to ~3:1. const SAFE_INITIALS_COLORS: MantineColor[] = [ "blue.8", "cyan.9", @@ -54,12 +51,21 @@ function sanitizeInitialsSource(name: string) { export const CustomAvatar = React.forwardRef< HTMLInputElement, CustomAvatarProps ->(({ avatarUrl, name, type, color, ...props }: CustomAvatarProps, ref) => { +>(({ avatarUrl, name, type, color, variant, ...props }: CustomAvatarProps, ref) => { const avatarLink = getAvatarUrl(avatarUrl, type); - const resolvedColor = - !color || color === "initials" ? pickInitialsColor(name ?? "") : color; + const isInitials = !color || color === "initials"; + const resolvedColor = isInitials ? pickInitialsColor(name ?? "") : color; const initialsSource = sanitizeInitialsSource(name ?? ""); + const placeholderStyles = + isInitials && variant !== "filled" + ? { + placeholder: { + color: `var(--mantine-color-${resolvedColor.split(".")[0]}-9)`, + }, + } + : undefined; + return ( ); diff --git a/apps/client/src/components/ui/radio-menu-item.tsx b/apps/client/src/components/ui/radio-menu-item.tsx new file mode 100644 index 000000000..3f0ae7c8f --- /dev/null +++ b/apps/client/src/components/ui/radio-menu-item.tsx @@ -0,0 +1,12 @@ +import { UnstyledButton } from "@mantine/core"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; + +// Menu.Item hard-codes role="menuitem"; use as its `component` to restore role="menuitemradio" so aria-checked works. +export const RadioMenuItem = forwardRef< + HTMLButtonElement, + ComponentPropsWithoutRef<"button"> +>((props, ref) => ( + +)); + +RadioMenuItem.displayName = "RadioMenuItem"; diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx index e2bd553c8..4f3d32af3 100644 --- a/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx +++ b/apps/client/src/ee/ai-chat/components/ai-chat-sidebar-item.tsx @@ -66,6 +66,8 @@ export default function AiChatSidebarItem({ [chat.updatedAt, i18n.language], ); + const chatTitle = chat.title || t("Untitled chat"); + useEffect(() => { if (renaming) { // Wait for the input to be mounted before selecting. @@ -120,9 +122,7 @@ export default function AiChatSidebarItem({ className={classes.chatItem} data-active={isActive || undefined} > - - {chat.title || t("Untitled chat")} - + {chatTitle} {formattedDate}
@@ -132,7 +132,7 @@ export default function AiChatSidebarItem({ size="xs" color="gray" onClick={(e) => e.preventDefault()} - aria-label={t("Chat menu")} + aria-label={t("Chat menu for {{title}}", { title: chatTitle })} > diff --git a/apps/client/src/ee/ai-chat/components/chat-input.tsx b/apps/client/src/ee/ai-chat/components/chat-input.tsx index e56d1fe06..88c270542 100644 --- a/apps/client/src/ee/ai-chat/components/chat-input.tsx +++ b/apps/client/src/ee/ai-chat/components/chat-input.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useEffect, useState } from "react"; +import { useCallback, useId, useRef, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react"; import { Popover } from "@mantine/core"; @@ -107,6 +107,7 @@ export default function ChatInput({ const [isEmpty, setIsEmpty] = useState(true); const [pendingAttachments, setPendingAttachments] = useState([]); const [plusMenuOpen, setPlusMenuOpen] = useState(false); + const plusMenuId = useId(); const fileInputRef = useRef(null); const onSendRef = useRef(onSend); onSendRef.current = onSend; @@ -342,6 +343,7 @@ export default function ChatInput({ position="top-start" width={220} shadow="md" + withRoles={false} trapFocus returnFocus > @@ -351,13 +353,17 @@ export default function ChatInput({ className={classes.plusButton} onClick={() => setPlusMenuOpen((o) => !o)} aria-label="Add content" + aria-haspopup="menu" + aria-expanded={plusMenuOpen} + aria-controls={plusMenuOpen ? plusMenuId : undefined} > - + diff --git a/apps/client/src/ee/ai-chat/styles/ai-chat.module.css b/apps/client/src/ee/ai-chat/styles/ai-chat.module.css index 67f97fbb2..2daa0a2b8 100644 --- a/apps/client/src/ee/ai-chat/styles/ai-chat.module.css +++ b/apps/client/src/ee/ai-chat/styles/ai-chat.module.css @@ -76,7 +76,6 @@ padding: var(--mantine-spacing-xs) var(--mantine-spacing-lg) var(--mantine-spacing-lg); } -/* Empty state - Notion AI style centered layout */ .emptyState { flex: 1; display: flex; diff --git a/apps/client/src/features/group/components/group-members.tsx b/apps/client/src/features/group/components/group-members.tsx index 14c5903a7..3bf04b5ac 100644 --- a/apps/client/src/features/group/components/group-members.tsx +++ b/apps/client/src/features/group/components/group-members.tsx @@ -91,7 +91,9 @@ export default function GroupMembersList() { diff --git a/apps/client/src/features/notification/components/notification-popover.tsx b/apps/client/src/features/notification/components/notification-popover.tsx index 3c5286c48..5a5068de8 100644 --- a/apps/client/src/features/notification/components/notification-popover.tsx +++ b/apps/client/src/features/notification/components/notification-popover.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useId, useState } from "react"; import { ActionIcon, Group, @@ -31,6 +31,7 @@ import classes from "../notification.module.css"; export function NotificationPopover() { const { t } = useTranslation(); + const titleId = useId(); const [opened, setOpened] = useState(false); const [tab, setTab] = useState("direct"); const [filter, setFilter] = useState("all"); @@ -83,10 +84,11 @@ export function NotificationPopover() { - + <Title id={titleId} order={2} fz="sm" fw={600}> {t("Notifications")} diff --git a/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx b/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx index 27b0ce210..d65a9c418 100644 --- a/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx +++ b/apps/client/src/features/page/tree/components/space-tree-node-menu.tsx @@ -34,6 +34,7 @@ import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts"; import { treeModel } from "@/features/page/tree/model/tree-model"; import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts"; import type { SpaceTreeNode } from "@/features/page/tree/types.ts"; +import classes from "@/features/page/tree/styles/tree.module.css"; export interface NodeMenuProps { node: SpaceTreeNode; @@ -123,8 +124,9 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) { { diff --git a/apps/client/src/features/page/tree/components/space-tree-row.tsx b/apps/client/src/features/page/tree/components/space-tree-row.tsx index 3da690066..e55373c25 100644 --- a/apps/client/src/features/page/tree/components/space-tree-row.tsx +++ b/apps/client/src/features/page/tree/components/space-tree-row.tsx @@ -201,13 +201,13 @@ function PageArrow({ isOpen, hasChildren, onToggle }: PageArrowProps) { return ( @@ -220,7 +220,8 @@ function PageArrow({ isOpen, hasChildren, onToggle }: PageArrowProps) { { diff --git a/apps/client/src/features/page/tree/styles/tree.module.css b/apps/client/src/features/page/tree/styles/tree.module.css index e116a1352..6ed758e64 100644 --- a/apps/client/src/features/page/tree/styles/tree.module.css +++ b/apps/client/src/features/page/tree/styles/tree.module.css @@ -57,6 +57,10 @@ flex-shrink: 0; } +.actionIcon { + color: light-dark(var(--mantine-color-dark-3), var(--mantine-color-gray-4)); +} + .text { flex: 1; /* min-width: 0 lets a flex child shrink below its content size — required diff --git a/apps/client/src/features/search/components/search-spotlight-filters.tsx b/apps/client/src/features/search/components/search-spotlight-filters.tsx index d41bef7e7..0b2bcc48c 100644 --- a/apps/client/src/features/search/components/search-spotlight-filters.tsx +++ b/apps/client/src/features/search/components/search-spotlight-filters.tsx @@ -17,6 +17,7 @@ import { import { useTranslation } from "react-i18next"; import { useGetSpacesQuery } from "@/features/space/queries/space-query"; import { SpaceFilterMenu } from "@/features/space/components/space-filter-menu"; +import { RadioMenuItem } from "@/components/ui/radio-menu-item"; import { useHasFeature } from "@/ee/hooks/use-feature"; import { Feature } from "@/ee/features"; import classes from "./search-spotlight-filters.module.css"; @@ -175,7 +176,7 @@ export function SearchSpotlightFilters({ {contentTypeOptions.map((option) => ( !option.disabled && diff --git a/apps/client/src/features/space/components/space-filter-menu.tsx b/apps/client/src/features/space/components/space-filter-menu.tsx index 00a9f38bc..785ac791e 100644 --- a/apps/client/src/features/space/components/space-filter-menu.tsx +++ b/apps/client/src/features/space/components/space-filter-menu.tsx @@ -13,6 +13,7 @@ import { useDebouncedValue } from "@mantine/hooks"; import { IconCheck, IconSearch } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useGetSpacesQuery } from "@/features/space/queries/space-query"; +import { RadioMenuItem } from "@/components/ui/radio-menu-item"; type SpaceFilterMenuProps = { value: string | null; @@ -75,7 +76,7 @@ export function SpaceFilterMenu({ onChange(null)} > @@ -103,7 +104,7 @@ export function SpaceFilterMenu({ {orderedSpaces.map((space) => ( onChange(space.id)} > diff --git a/apps/client/src/features/space/components/space-members.tsx b/apps/client/src/features/space/components/space-members.tsx index 8f1502574..cf431d049 100644 --- a/apps/client/src/features/space/components/space-members.tsx +++ b/apps/client/src/features/space/components/space-members.tsx @@ -210,7 +210,9 @@ export default function SpaceMembersList({ diff --git a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx index ce1c588a0..f8fd035fb 100644 --- a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx @@ -12,9 +12,14 @@ import useUserRole from "@/hooks/use-user-role.tsx"; interface Props { userId: string; + name: string; deactivatedAt: Date | null; } -export default function MemberActionMenu({ userId, deactivatedAt }: Props) { +export default function MemberActionMenu({ + userId, + name, + deactivatedAt, +}: Props) { const { t } = useTranslation(); const deleteWorkspaceMemberMutation = useDeleteWorkspaceMemberMutation(); const deactivateMutation = useDeactivateWorkspaceMemberMutation(); @@ -86,7 +91,7 @@ export default function MemberActionMenu({ userId, deactivatedAt }: Props) { diff --git a/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx b/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx index 76a32ffb1..6423ebddc 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-members-table.tsx @@ -111,6 +111,7 @@ export default function WorkspaceMembersTable() { {isAdmin && ( )}