From 6191acfa1401e5133b2c0ffc29721b5a4bb8abda Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:51:55 +0100 Subject: [PATCH 1/3] fix: a11y (#2275) --- .../public/locales/en-US/translation.json | 4 +-- .../layouts/global/global-sidebar.tsx | 2 +- .../src/components/ui/custom-avatar.tsx | 28 ++++++++++++------- .../src/components/ui/radio-menu-item.tsx | 12 ++++++++ .../components/ai-chat-sidebar-item.tsx | 8 +++--- .../src/ee/ai-chat/components/chat-input.tsx | 13 +++++++-- .../src/ee/ai-chat/styles/ai-chat.module.css | 1 - .../group/components/group-members.tsx | 4 ++- .../components/notification-popover.tsx | 6 ++-- .../tree/components/space-tree-node-menu.tsx | 6 ++-- .../page/tree/components/space-tree-row.tsx | 10 ++++--- .../features/page/tree/styles/tree.module.css | 4 +++ .../components/search-spotlight-filters.tsx | 3 +- .../space/components/space-filter-menu.tsx | 5 ++-- .../space/components/space-members.tsx | 4 ++- .../components/members-action-menu.tsx | 9 ++++-- .../components/workspace-members-table.tsx | 1 + 17 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 apps/client/src/components/ui/radio-menu-item.tsx 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 && ( )} From 2ff8720832030b4be6cc16a504c931ac53207fdf Mon Sep 17 00:00:00 2001 From: Felix <66864107+flixz02@users.noreply.github.com> Date: Wed, 17 Jun 2026 15:17:13 +0200 Subject: [PATCH 2/3] fix: slash-menu suggestion search localization (#2280) --- .../components/slash-menu/menu-items.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index 7f8567558..1f05dc62f 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -767,18 +767,34 @@ export const getSuggestionItems = ({ for (const [group, items] of Object.entries(CommandGroups)) { const filteredItems = items.filter((item) => { if (excludeItems?.has(item.title)) return false; + const translatedTitle = i18n.t(item.title); + const translatedDescription = i18n.t(item.description); return ( fuzzyMatch(search, item.title) || + fuzzyMatch(search, translatedTitle) || item.description.toLowerCase().includes(search) || + translatedDescription.toLowerCase().includes(search) || (item.searchTerms && - item.searchTerms.some((term: string) => term.includes(search))) + item.searchTerms.some( + (term: string) => + term.includes(search) || + i18n.t(term).toLowerCase().includes(search), + )) ); }); if (filteredItems.length) { filteredGroups[group] = filteredItems.sort((a, b) => { - const aTitle = a.title.toLowerCase().includes(search) ? 0 : 1; - const bTitle = b.title.toLowerCase().includes(search) ? 0 : 1; + const aTitle = + a.title.toLowerCase().includes(search) || + i18n.t(a.title).toLowerCase().includes(search) + ? 0 + : 1; + const bTitle = + b.title.toLowerCase().includes(search) || + i18n.t(b.title).toLowerCase().includes(search) + ? 0 + : 1; return aTitle - bTitle; }); } From 1867aa8bf6c37a00d0d8c8a2084a2046d3ded06d Mon Sep 17 00:00:00 2001 From: Mayank-2-16 <111053673+Mayank-2-16@users.noreply.github.com> Date: Wed, 17 Jun 2026 19:02:48 +0530 Subject: [PATCH 3/3] fix: pdf table header alignment issue (#2259) * fix: pdf table header alignment issue * cleanup --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com> --- apps/client/src/ee/pdf-export/pdf-render-page.tsx | 1 + .../src/features/editor/readonly-page-editor.tsx | 10 ++++++++-- apps/client/src/features/editor/styles/table.css | 11 ++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/client/src/ee/pdf-export/pdf-render-page.tsx b/apps/client/src/ee/pdf-export/pdf-render-page.tsx index 8705f9b6c..0f13cb71b 100644 --- a/apps/client/src/ee/pdf-export/pdf-render-page.tsx +++ b/apps/client/src/ee/pdf-export/pdf-render-page.tsx @@ -58,6 +58,7 @@ export default function PdfRenderPage() { title={data.title} content={data.content} pageId={data.pageId} + printMode /> ); diff --git a/apps/client/src/features/editor/readonly-page-editor.tsx b/apps/client/src/features/editor/readonly-page-editor.tsx index cd4878a9b..4b28bec9d 100644 --- a/apps/client/src/features/editor/readonly-page-editor.tsx +++ b/apps/client/src/features/editor/readonly-page-editor.tsx @@ -15,6 +15,7 @@ interface PageEditorProps { title: string; content: any; pageId?: string; + printMode?: boolean; /** * When rendering inside a public share, pass the share's id (or key). Lookups * for transclusion content then resolve against the share graph instead of @@ -28,6 +29,7 @@ export default function ReadonlyPageEditor({ title, content, pageId, + printMode = false, shareId, }: PageEditorProps) { const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom); @@ -48,8 +50,12 @@ export default function ReadonlyPageEditor({ }, []); const extensions = useMemo(() => { + const excludedExtensions = new Set([ + "uniqueID", + ...(printMode ? ["tableHeaderPin", "tableReadonlySort"] : []), + ]); const filteredExtensions = mainExtensions.filter( - (ext) => ext.name !== "uniqueID", + (ext) => !excludedExtensions.has(ext.name), ); return [ @@ -59,7 +65,7 @@ export default function ReadonlyPageEditor({ updateDocument: false, }), ]; - }, []); + }, [printMode]); const titleExtensions = [ Document.extend({ diff --git a/apps/client/src/features/editor/styles/table.css b/apps/client/src/features/editor/styles/table.css index ac5b91d4c..32a427936 100644 --- a/apps/client/src/features/editor/styles/table.css +++ b/apps/client/src/features/editor/styles/table.css @@ -163,8 +163,13 @@ @media print { .tableWrapper.tableHeaderPinned table tr:first-child { - position: static; - transform: none; + position: static !important; + top: auto !important; + transform: none !important; + } + + .tableReadonlySortChevron { + display: none !important; } } @@ -268,4 +273,4 @@ .prosemirror-dropcursor-inline { display: none; } -} \ No newline at end of file +}