Fix desktop DM and sidebar UI polish (#908)

This commit is contained in:
klopez4212
2026-06-08 21:04:20 +01:00
committed by GitHub
parent dd08f988de
commit da80c7340f
14 changed files with 214 additions and 47 deletions
+40 -3
View File
@@ -2,6 +2,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
import type { Channel, SearchHit } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { SidebarTrigger, useSidebar } from "@/shared/ui/sidebar";
@@ -30,6 +31,43 @@ function GlobalTopDivider() {
);
}
function CenterColumnTopbarSearch({
channels,
currentPubkey,
onOpenChannel,
onOpenResult,
searchFocusRequest,
}: Pick<
AppTopChromeProps,
| "channels"
| "currentPubkey"
| "onOpenChannel"
| "onOpenResult"
| "searchFocusRequest"
>) {
const { isResizing, state } = useSidebar();
return (
<div
className={cn(
"pointer-events-none fixed right-0 top-[7px] z-[45] flex justify-center px-24",
!isResizing && "transition-[left] duration-200 ease-linear",
)}
data-testid="topbar-search-column"
style={{ left: state === "expanded" ? "var(--sidebar-width)" : 0 }}
>
<TopbarSearch
channels={channels}
className="pointer-events-auto w-[220px] max-w-full md:w-[300px] lg:w-[360px] xl:w-[420px] 2xl:w-[480px]"
currentPubkey={currentPubkey}
focusRequest={searchFocusRequest}
onOpenChannel={onOpenChannel}
onOpenResult={onOpenResult}
/>
</div>
);
}
const TOP_CHROME_ICON_BUTTON_CLASS =
"size-6 rounded-[4px] p-1 text-muted-foreground/70 hover:bg-border/45 hover:text-foreground";
@@ -79,13 +117,12 @@ export function AppTopChrome({
</Button>
</div>
{searchHidden ? null : (
<TopbarSearch
<CenterColumnTopbarSearch
channels={channels}
className="fixed left-1/2 top-[7px] z-[45] block w-[220px] max-w-[calc(100vw-11rem)] -translate-x-1/2 md:w-[300px] md:max-w-[34vw] lg:w-[360px] lg:max-w-[38vw] xl:w-[420px] xl:max-w-[42vw] 2xl:w-[480px] 2xl:max-w-[44vw]"
currentPubkey={currentPubkey}
focusRequest={searchFocusRequest}
onOpenChannel={onOpenChannel}
onOpenResult={onOpenResult}
searchFocusRequest={searchFocusRequest}
/>
)}
</>
@@ -20,7 +20,10 @@ import { Button } from "@/shared/ui/button";
import type { useChannelFind } from "@/features/search/useChannelFind";
import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel";
import type { TimelineMessage } from "@/features/messages/types";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import {
resolveUserLabel,
type UserProfileLookup,
} from "@/features/profile/lib/identity";
import type { Channel } from "@/shared/api/types";
type ChannelPaneProps = {
@@ -278,6 +281,43 @@ export const ChannelPane = React.memo(function ChannelPane({
}, [botTypingEntries, openThreadHeadId]);
const hasThreadComposerBotActivity =
threadComposerBotTypingPubkeys.length > 0;
const directMessageIntro = React.useMemo(() => {
if (activeChannel?.channelType !== "dm") {
return null;
}
const participants = activeChannel.participantPubkeys.map(
(pubkey, index) => ({
fallbackName: activeChannel.participants[index] ?? null,
pubkey,
}),
);
const otherParticipants = currentPubkey
? participants.filter(
(participant) =>
participant.pubkey.toLowerCase() !== currentPubkey.toLowerCase(),
)
: participants;
const [participant] =
otherParticipants.length > 0 ? otherParticipants : participants;
if (!participant) {
return null;
}
const profile = profiles?.[participant.pubkey.toLowerCase()] ?? null;
const displayName = resolveUserLabel({
currentPubkey,
fallbackName: participant.fallbackName,
profiles,
pubkey: participant.pubkey,
});
return {
avatarUrl: profile?.avatarUrl ?? null,
displayName,
};
}, [activeChannel, currentPubkey, profiles]);
const selectedAgent = React.useMemo(
() =>
@@ -305,6 +345,7 @@ export const ChannelPane = React.memo(function ChannelPane({
) : null}
<MessageTimeline
channelId={activeChannel?.id}
directMessageIntro={directMessageIntro}
scrollContainerRef={timelineScrollRef}
currentPubkey={currentPubkey}
fetchOlder={fetchOlder}
@@ -133,6 +133,7 @@ export function ChannelScreen({
}, [activeChannel?.isMember, activeChannelId, activeReadAt, markChannelRead]);
const {
activeChannelTitle,
activeDmAvatarUrl,
activeDmPresenceStatus,
activeChannelEphemeralDisplay,
} = useActiveChannelHeader(activeChannel, currentPubkey);
@@ -164,14 +165,22 @@ export function ChannelScreen({
currentPubkey,
latestMessageEvent,
);
const activeDmParticipantPubkeys = React.useMemo(
() =>
activeChannel?.channelType === "dm"
? activeChannel.participantPubkeys
: [],
[activeChannel],
);
const messageProfilePubkeys = React.useMemo(
() => [
...new Set([
...messageAuthorPubkeys,
...activeDmParticipantPubkeys,
...typingEntries.map((entry) => entry.pubkey),
]),
],
[messageAuthorPubkeys, typingEntries],
[activeDmParticipantPubkeys, messageAuthorPubkeys, typingEntries],
);
const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, {
enabled: messageProfilePubkeys.length > 0,
@@ -473,6 +482,7 @@ export function ChannelScreen({
activeChannelTitle={activeChannelTitle}
actionsRightInset={headerActionsRightInset}
actionsVariant={shouldCompactHeaderActions ? "compact" : "inline"}
activeDmAvatarUrl={activeDmAvatarUrl}
activeDmPresenceStatus={activeDmPresenceStatus}
currentPubkey={currentPubkey}
isJoining={joinChannelMutation.isPending}
@@ -5,6 +5,7 @@ import type { EphemeralChannelDisplay } from "@/features/channels/lib/ephemeralC
import { getChannelDescription } from "@/features/channels/lib/channelDescription";
import { ChannelHeaderStatusBadge } from "@/features/channels/ui/ChannelHeaderStatusBadge";
import { ChannelMembersBar } from "@/features/channels/ui/ChannelMembersBar";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { Button } from "@/shared/ui/button";
import type { Channel, PresenceStatus } from "@/shared/api/types";
@@ -14,6 +15,7 @@ type ChannelScreenHeaderProps = {
activeChannelTitle: string;
actionsRightInset?: string;
actionsVariant?: "inline" | "compact";
activeDmAvatarUrl: string | null;
activeDmPresenceStatus: PresenceStatus | null;
currentPubkey?: string;
isJoining?: boolean;
@@ -29,6 +31,7 @@ export function ChannelScreenHeader({
activeChannelTitle,
actionsRightInset,
actionsVariant = "inline",
activeDmAvatarUrl,
activeDmPresenceStatus,
currentPubkey,
isJoining = false,
@@ -78,6 +81,17 @@ export function ChannelScreenHeader({
actionsRightInset={actionsRightInset}
channelType={activeChannel?.channelType}
description={getChannelDescription(activeChannel)}
leadingContent={
activeChannel?.channelType === "dm" ? (
<ProfileAvatar
avatarUrl={activeDmAvatarUrl}
className="h-6 w-6 rounded-md text-[10px]"
iconClassName="h-3.5 w-3.5"
label={activeChannelTitle}
testId="chat-header-dm-avatar"
/>
) : undefined
}
statusBadge={
<ChannelHeaderStatusBadge
channelType={activeChannel?.channelType}
@@ -33,6 +33,12 @@ export function useActiveChannelHeader(
activeDmParticipantPubkeys[0]?.toLowerCase()
] ?? null)
: null;
const activeDmAvatarUrl =
activeDmParticipantPubkeys.length > 0
? (activeDmProfilesQuery.data?.profiles?.[
activeDmParticipantPubkeys[0]?.toLowerCase()
]?.avatarUrl ?? null)
: null;
return {
activeChannelTitle: activeChannel
@@ -42,6 +48,7 @@ export function useActiveChannelHeader(
activeDmProfilesQuery.data?.profiles,
)
: "Channels",
activeDmAvatarUrl,
activeDmPresenceStatus,
activeChannelEphemeralDisplay,
};
+13 -7
View File
@@ -26,6 +26,7 @@ type ChatHeaderProps = {
description?: string;
channelType?: ChannelType;
visibility?: ChannelVisibility;
leadingContent?: React.ReactNode;
mode?: "home" | "channel" | "agents" | "workflows" | "pulse" | "projects";
overlaysContent?: boolean;
statusBadge?: React.ReactNode;
@@ -88,6 +89,7 @@ export function ChatHeader({
description,
channelType,
visibility,
leadingContent,
mode = "channel",
overlaysContent = false,
statusBadge,
@@ -103,8 +105,10 @@ export function ChatHeader({
const header = (
<header
className={cn(
"pointer-events-auto relative z-30 flex min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-transparent pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pl-[24px] sm:pr-[12px]",
density === "compact" ? "h-[32px] py-0" : "min-h-[44px] py-[6px]",
"pointer-events-auto relative z-30 flex min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-transparent pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pr-[12px]",
density === "compact"
? "h-[32px] py-0"
: "min-h-[44px] py-[6px] sm:pl-[24px]",
overlaysContent && !belowSystemChrome && "-mb-[44px]",
)}
data-testid="chat-header"
@@ -112,11 +116,13 @@ export function ChatHeader({
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-[4px]">
<ChannelIcon
channelType={channelType}
mode={mode}
visibility={visibility}
/>
{leadingContent ?? (
<ChannelIcon
channelType={channelType}
mode={mode}
visibility={visibility}
/>
)}
<h1
className="min-w-0 translate-y-px truncate text-base font-semibold leading-6 tracking-tight"
data-testid="chat-title"
@@ -141,15 +141,15 @@ export function CustomEmojiSettingsCard() {
}}
>
<SettingsOptionGroup>
<div className="flex flex-col gap-3 px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3 text-sm">
<div className="min-w-0 flex-[1_1_22rem]">
<h4 className="text-sm font-medium">Upload an image</h4>
<p className="text-sm font-normal text-muted-foreground">
Square images work best. GIF, PNG, JPEG, and WebP files are
supported.
</p>
</div>
<div className="flex min-w-0 items-center gap-3">
<div className="flex min-w-0 flex-[1_1_16rem] items-center gap-3">
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-md border bg-background">
{pendingUpload ? (
<img
@@ -163,9 +163,11 @@ export function CustomEmojiSettingsCard() {
)}
</div>
<div className="min-w-0 space-y-2">
<p className="max-w-48 truncate text-sm font-normal text-muted-foreground">
{pendingUpload?.filename ?? "No image selected"}
</p>
{pendingUpload?.filename ? (
<p className="max-w-full truncate text-sm font-normal text-muted-foreground">
{pendingUpload.filename}
</p>
) : null}
<Button
type="button"
data-testid="custom-emoji-upload"
@@ -183,15 +185,15 @@ export function CustomEmojiSettingsCard() {
</div>
</div>
<div className="flex flex-col gap-3 px-4 py-3 text-sm sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 px-4 py-3 text-sm">
<div className="min-w-0 flex-[1_1_22rem]">
<h4 className="text-sm font-medium">Give it a name</h4>
<p className="text-sm font-normal text-muted-foreground">
This is what youll type to add this emoji to messages and
reactions.
</p>
</div>
<div className="w-full min-w-0 max-w-sm space-y-2">
<div className="w-full min-w-0 max-w-sm flex-[1_1_20rem] space-y-2">
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
:
@@ -4,7 +4,7 @@ export function HomeLoadingState() {
return (
<div className="flex-1 overflow-hidden">
<div className="grid h-full min-h-0 w-full lg:grid-cols-[320px_minmax(0,1fr)]">
<div className="overflow-hidden border-r border-border/70 bg-background/60">
<div className="relative overflow-hidden bg-background/60 after:absolute after:bottom-0 after:right-0 after:top-10 after:w-px after:bg-border/70 after:content-['']">
<div className="border-b border-border/70 px-4 pb-4 pt-14">
<Skeleton className="h-4 w-20" />
<Skeleton className="mt-2 h-4 w-28" />
@@ -52,7 +52,7 @@ export function InboxListPane({
className={cn(
"relative flex min-h-0 min-w-0 flex-col overflow-hidden bg-background/60",
showRightDivider &&
"after:pointer-events-none after:absolute after:inset-y-0 after:right-0 after:z-40 after:w-px after:bg-border/35 after:content-['']",
"after:pointer-events-none after:absolute after:bottom-0 after:right-0 after:top-10 after:z-40 after:w-px after:bg-border/35 after:content-['']",
)}
>
<TopChromeBackdrop className="h-[76px]" />
@@ -3,9 +3,10 @@ import { ArrowDown } from "lucide-react";
import type { TimelineMessage } from "@/features/messages/types";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Separator } from "@/shared/ui/separator";
import { Spinner } from "@/shared/ui/spinner";
import { TooltipProvider } from "@/shared/ui/tooltip";
import { TimelineSkeleton } from "./TimelineSkeleton";
@@ -16,6 +17,10 @@ import { useTimelineScrollManager } from "./useTimelineScrollManager";
type MessageTimelineProps = {
channelId?: string | null;
messages: TimelineMessage[];
directMessageIntro?: {
avatarUrl: string | null;
displayName: string;
} | null;
isLoading?: boolean;
emptyTitle?: string;
emptyDescription?: string;
@@ -56,6 +61,7 @@ type MessageTimelineProps = {
export const MessageTimeline = React.memo(function MessageTimeline({
channelId,
directMessageIntro = null,
messages,
isLoading = false,
emptyTitle = "No messages yet",
@@ -141,6 +147,16 @@ export const MessageTimeline = React.memo(function MessageTimeline({
sentinelRef: topSentinelRef,
});
const hasConversationMessage = messages.some(
(message) => message.kind !== KIND_SYSTEM_MESSAGE,
);
const showDirectMessageIntro =
!isLoading && directMessageIntro !== null && !hasConversationMessage;
const showGenericEmpty =
!isLoading && messages.length === 0 && directMessageIntro === null;
const showMessageList =
!isLoading && messages.length > 0 && !showDirectMessageIntro;
return (
<TooltipProvider delayDuration={200}>
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
@@ -155,7 +171,10 @@ export const MessageTimeline = React.memo(function MessageTimeline({
ref={scrollContainerRef}
>
<div
className="flex w-full flex-col gap-2 pt-[92px]"
className={cn(
"flex w-full flex-col gap-2 pt-[92px]",
(showDirectMessageIntro || showGenericEmpty) && "min-h-full",
)}
ref={contentRef}
>
<div ref={topSentinelRef} aria-hidden className="h-px" />
@@ -166,22 +185,34 @@ export const MessageTimeline = React.memo(function MessageTimeline({
</div>
) : null}
{!hasOlderMessages && !isLoading && messages.length > 0 ? (
{isLoading ? <TimelineSkeleton /> : null}
{showDirectMessageIntro ? (
<div
className="flex items-center gap-3 py-2"
data-testid="message-timeline-beginning"
className="mb-10 mt-auto flex w-full flex-col items-start text-left sm:-ml-2"
data-testid="message-dm-intro"
>
<Separator className="flex-1" />
<p className="text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground/75">
Beginning of conversation
<ProfileAvatar
avatarUrl={directMessageIntro.avatarUrl}
className="h-[60px] w-[60px] text-base"
iconClassName="h-6 w-6"
label={directMessageIntro.displayName}
testId="message-dm-intro-avatar"
/>
<p className="mt-4 max-w-full truncate text-xl font-semibold leading-7 tracking-tight text-foreground">
{directMessageIntro.displayName}
</p>
<p className="mt-1 max-w-full truncate whitespace-nowrap text-sm leading-5 text-muted-foreground">
This is the beginning of your direct message with{" "}
<span className="font-medium text-foreground">
{directMessageIntro.displayName}
</span>
.
</p>
<Separator className="flex-1" />
</div>
) : null}
{isLoading ? <TimelineSkeleton /> : null}
{!isLoading && messages.length === 0 ? (
{showGenericEmpty ? (
<div
className="rounded-3xl border border-dashed border-border/80 bg-card/70 px-6 py-10 text-center shadow-xs"
data-testid="message-empty"
@@ -195,7 +226,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
</div>
) : null}
{!isLoading && messages.length > 0 ? (
{showMessageList ? (
<TimelineMessageList
channelId={channelId}
currentPubkey={currentPubkey}
@@ -33,11 +33,11 @@ import { SidebarSection } from "@/features/sidebar/ui/SidebarSection";
import {
ChannelGroupSection,
CustomChannelSection,
SECTION_ACTION_VISIBILITY_CLASS,
} from "@/features/sidebar/ui/CustomChannelSection";
import { CreateChannelDialog } from "@/features/sidebar/ui/CreateChannelDialog";
import { NewDirectMessageDialog } from "@/features/sidebar/ui/NewDirectMessageDialog";
import { SidebarProfileCard } from "@/features/sidebar/ui/SidebarProfileCard";
import { SECTION_ACTION_VISIBILITY_CLASS } from "@/features/sidebar/ui/sidebarSectionStyles";
import type {
Channel,
ChannelVisibility,
@@ -41,6 +41,10 @@ import {
DroppableUngroupedBody,
SortableSectionShell,
} from "@/features/sidebar/ui/SidebarDnd";
import {
SECTION_ACTION_VISIBILITY_CLASS,
SECTION_ICON_BUTTON_CLASS,
} from "@/features/sidebar/ui/sidebarSectionStyles";
import type { ChannelSection } from "@/features/sidebar/lib/useChannelSections";
import type { Channel } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
@@ -49,10 +53,6 @@ import { cn } from "@/shared/lib/cn";
// Shared styles
// ---------------------------------------------------------------------------
const SECTION_ICON_BUTTON_CLASS =
"flex size-6 items-center justify-center rounded-[4px] p-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-border/35 hover:text-sidebar-foreground focus-visible:bg-sidebar-border/35 focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
export const SECTION_ACTION_VISIBILITY_CLASS =
"opacity-0 transition-opacity group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100";
const SECTION_LABEL_BUTTON_CLASS =
"group/section-label flex w-fit max-w-[calc(100%-3rem)] cursor-pointer appearance-none items-center gap-1 text-left transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground";
const SECTION_LABEL_CHEVRON_CLASS =
@@ -26,7 +26,6 @@ import {
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
} from "@/shared/ui/sidebar";
@@ -37,6 +36,10 @@ const SECTION_LABEL_BUTTON_CLASS =
"group/section-label flex w-fit max-w-[calc(100%-3rem)] cursor-pointer appearance-none items-center gap-1 text-left transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground";
const SECTION_LABEL_CHEVRON_CLASS =
"h-2.5 w-2.5 shrink-0 opacity-0 text-sidebar-foreground/45 transition-[color,opacity,transform] group-hover/section-label:opacity-100 group-hover/section-label:text-sidebar-foreground group-focus-visible/section-label:opacity-100 group-focus-visible/section-label:text-sidebar-foreground";
const SIDEBAR_ROW_ACTION_VISIBILITY_CLASS =
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 md:opacity-0";
const SIDEBAR_ROW_ICON_ACTION_CLASS =
"flex size-6 items-center justify-center p-1 text-sidebar-foreground/45 transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring peer-data-[active=true]/menu-button:text-sidebar-active-foreground/75 peer-data-[active=true]/menu-button:hover:text-sidebar-active-foreground [&>svg]:size-4 [&>svg]:shrink-0";
export type SidebarDmParticipant = {
avatarUrl: string | null;
@@ -168,6 +171,9 @@ export function ChannelMenuButton({
return (
<SidebarMenuButton
className={cn(
isActive
? "group-hover/menu-item:bg-sidebar-active group-hover/menu-item:text-sidebar-active-foreground"
: "group-hover/menu-item:bg-sidebar-accent group-hover/menu-item:text-sidebar-accent-foreground",
!isActive &&
hasUnread &&
"font-semibold text-sidebar-foreground hover:text-sidebar-foreground",
@@ -326,15 +332,23 @@ export function SidebarSection({
/>
) : null}
{channel.channelType === "dm" && onHideDm ? (
<SidebarMenuAction
<button
aria-label="Close direct message"
className="right-0 top-1/2 h-8 w-8 -translate-y-1/2 rounded-lg border border-border/40 [&>svg]:size-5 peer-data-[size=default]/menu-button:top-1/2 peer-data-[size=lg]/menu-button:top-1/2 peer-data-[size=sm]/menu-button:top-1/2"
className={cn(
"absolute right-1 top-1/2 z-10 -translate-y-1/2 after:absolute after:-inset-2 after:md:hidden group-data-[collapsible=icon]:hidden",
SIDEBAR_ROW_ICON_ACTION_CLASS,
SIDEBAR_ROW_ACTION_VISIBILITY_CLASS,
)}
data-sidebar="menu-action"
data-testid={`hide-dm-${channel.name}`}
onClick={() => onHideDm(channel.id)}
showOnHover
onClick={(event) => {
event.stopPropagation();
onHideDm(channel.id);
}}
type="button"
>
<X />
</SidebarMenuAction>
</button>
) : null}
</SidebarMenuItem>
);
@@ -0,0 +1,5 @@
export const SECTION_ICON_BUTTON_CLASS =
"flex size-6 items-center justify-center rounded-[4px] p-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-border/35 hover:text-sidebar-foreground focus-visible:bg-sidebar-border/35 focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
export const SECTION_ACTION_VISIBILITY_CLASS =
"opacity-0 transition-opacity group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100";