From dcd722b8c3c8d5d5c3e75b007c7264d6a05f2bde Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Tue, 7 Apr 2026 11:39:58 +0200 Subject: [PATCH] desktop: message layout, circular send, cleaner sidebar Tighten message row typography and spacing; keep actions in an absolute header row so the action bar cannot wrap and gap the body; add light padding before message content. Adjust tight Markdown block margins so sibling blocks stack without extra first-block gap. Replace the composer send control with a circular primary button and an up-arrow icon; update smoke e2e to assert title instead of label text. Remove sidebar rail and submenu vertical lines, drop header/content/footer SidebarSeparators, and trim SidebarMenuSub border alignment offsets. Made-with: Cursor --- .../messages/ui/MessageComposerToolbar.tsx | 18 ++- .../src/features/messages/ui/MessageRow.tsx | 104 +++++++++--------- .../src/features/sidebar/ui/AppSidebar.tsx | 5 - desktop/src/shared/ui/markdown.tsx | 2 +- desktop/src/shared/ui/sidebar.tsx | 8 +- desktop/tests/e2e/smoke.spec.ts | 5 +- 6 files changed, 76 insertions(+), 66 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageComposerToolbar.tsx b/desktop/src/features/messages/ui/MessageComposerToolbar.tsx index ef7758dc9..5847c0c38 100644 --- a/desktop/src/features/messages/ui/MessageComposerToolbar.tsx +++ b/desktop/src/features/messages/ui/MessageComposerToolbar.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { AtSign, Paperclip, SendHorizontal } from "lucide-react"; +import { ArrowUp, AtSign, Paperclip } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { ComposerEmojiPicker } from "./ComposerEmojiPicker"; @@ -67,14 +67,22 @@ export const MessageComposerToolbar = React.memo( ); diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 01f1110f9..cd12db5cb 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -228,61 +228,65 @@ export const MessageRow = React.memo( )} -
-
- {message.pubkey ? ( - - + + ) : ( +

{message.author} - - - ) : ( -

- {message.author} -

- )} - {message.role ? ( -

- {message.role} -

- ) : null} -
- - {message.pending ? ( -

- Sending + + )} + {message.role ? ( +

+ {message.role}

) : null} - {message.edited ? ( -

- (edited) -

- ) : null} -
- {renderBody()} +
+ + {message.pending ? ( +

+ Sending +

+ ) : null} + {message.edited ? ( +

+ (edited) +

+ ) : null} + +
+
{renderBody()}
- - {isLoading ? ( @@ -710,8 +707,6 @@ export function AppSidebar({ ) : null} - - diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx index 1956a8ada..b0bbafb9d 100644 --- a/desktop/src/shared/ui/markdown.tsx +++ b/desktop/src/shared/ui/markdown.tsx @@ -237,7 +237,7 @@ function MarkdownInner({
*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-1" + ? "max-w-none break-words text-sm leading-5 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-0 [&>*+*]:mt-1" : compact ? "max-w-none break-words text-[15px] leading-6 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-1.5" : "max-w-none break-words text-sm leading-7 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-3", diff --git a/desktop/src/shared/ui/sidebar.tsx b/desktop/src/shared/ui/sidebar.tsx index 18707f2b9..999b80061 100644 --- a/desktop/src/shared/ui/sidebar.tsx +++ b/desktop/src/shared/ui/sidebar.tsx @@ -308,10 +308,10 @@ const SidebarRail = React.forwardRef< onClick={toggleSidebar} title="Toggle Sidebar" className={cn( - "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", + "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex", "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", - "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar", + "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className, @@ -698,7 +698,7 @@ const SidebarMenuSub = React.forwardRef< ref={ref} data-sidebar="menu-sub" className={cn( - "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5", + "mx-3.5 flex min-w-0 flex-col gap-1 px-2.5 py-0.5", "group-data-[collapsible=icon]:hidden", className, )} @@ -730,7 +730,7 @@ const SidebarMenuSubButton = React.forwardRef< data-size={size} data-active={isActive} className={cn( - "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary-foreground", + "flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground data-[active=true]:[&>svg]:text-sidebar-primary-foreground", "data-[active=true]:bg-sidebar-primary data-[active=true]:font-semibold data-[active=true]:text-sidebar-primary-foreground data-[active=true]:hover:bg-sidebar-primary data-[active=true]:hover:text-sidebar-primary-foreground", size === "sm" && "text-xs", size === "md" && "text-sm", diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts index 21c958cae..bd159aa37 100644 --- a/desktop/tests/e2e/smoke.spec.ts +++ b/desktop/tests/e2e/smoke.spec.ts @@ -262,7 +262,10 @@ test("supports multiline drafts with Ctrl+Enter and sends with Enter", async ({ await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); - await expect(page.getByTestId("send-message")).toContainText("Send"); + await expect(page.getByTestId("send-message")).toHaveAttribute( + "title", + "Send (Enter)", + ); const initialInputHeight = await input.evaluate( (element) => (element as HTMLTextAreaElement).clientHeight, );