mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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
This commit is contained in:
@@ -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(
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="gap-2"
|
||||
aria-label={isSending ? "Sending" : "Send message"}
|
||||
className="rounded-full"
|
||||
data-testid="send-message"
|
||||
disabled={sendDisabled || isSending}
|
||||
title="Send (Enter)"
|
||||
title={isSending ? "Sending…" : "Send (Enter)"}
|
||||
type="submit"
|
||||
size="icon"
|
||||
>
|
||||
<SendHorizontal className="h-4 w-4" />
|
||||
{isSending ? "Sending" : "Send"}
|
||||
{isSending ? (
|
||||
<span
|
||||
aria-hidden
|
||||
className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
|
||||
/>
|
||||
) : (
|
||||
<ArrowUp className="h-4 w-4" aria-hidden />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -228,61 +228,65 @@ export const MessageRow = React.memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
{message.pubkey ? (
|
||||
<UserProfilePopover pubkey={message.pubkey}>
|
||||
<button
|
||||
className="truncate rounded text-sm font-semibold tracking-tight hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
>
|
||||
<div className="relative min-w-0 flex-1 space-y-0">
|
||||
{/* Name + role only in flow — action bar + time are absolute so flex-wrap
|
||||
cannot insert a full-width row between the username and the body. */}
|
||||
<div className="flex min-w-0 flex-nowrap items-start gap-x-2 pr-[9.5rem] sm:pr-40">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-x-2">
|
||||
{message.pubkey ? (
|
||||
<UserProfilePopover pubkey={message.pubkey}>
|
||||
<button
|
||||
className="min-w-0 truncate rounded pt-px text-left text-sm font-semibold leading-tight tracking-tight hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
type="button"
|
||||
>
|
||||
{message.author}
|
||||
</button>
|
||||
</UserProfilePopover>
|
||||
) : (
|
||||
<h3 className="min-w-0 truncate pt-px text-sm font-semibold leading-tight tracking-tight">
|
||||
{message.author}
|
||||
</button>
|
||||
</UserProfilePopover>
|
||||
) : (
|
||||
<h3 className="truncate text-sm font-semibold tracking-tight">
|
||||
{message.author}
|
||||
</h3>
|
||||
)}
|
||||
{message.role ? (
|
||||
<p className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{message.role}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="ml-auto flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<MessageActionBar
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
message={message}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onReactionSelect={
|
||||
canToggleReactions ? handleReactionSelect : undefined
|
||||
}
|
||||
onReply={onReply}
|
||||
reactionErrorMessage={reactionErrorMessage}
|
||||
reactionPending={reactionPending}
|
||||
reactions={reactions}
|
||||
/>
|
||||
{message.pending ? (
|
||||
<p className="font-medium uppercase tracking-[0.14em] text-primary/80">
|
||||
Sending
|
||||
</h3>
|
||||
)}
|
||||
{message.role ? (
|
||||
<p className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{message.role}
|
||||
</p>
|
||||
) : null}
|
||||
{message.edited ? (
|
||||
<p
|
||||
className="text-muted-foreground/70"
|
||||
title="This message has been edited"
|
||||
>
|
||||
(edited)
|
||||
</p>
|
||||
) : null}
|
||||
<MessageTimestamp
|
||||
createdAt={message.createdAt}
|
||||
time={message.time}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{renderBody()}
|
||||
<div className="absolute right-0 top-0 z-10 flex items-start justify-end gap-2 pt-px text-xs text-muted-foreground">
|
||||
<MessageActionBar
|
||||
activeReplyTargetId={activeReplyTargetId}
|
||||
message={message}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
onReactionSelect={
|
||||
canToggleReactions ? handleReactionSelect : undefined
|
||||
}
|
||||
onReply={onReply}
|
||||
reactionErrorMessage={reactionErrorMessage}
|
||||
reactionPending={reactionPending}
|
||||
reactions={reactions}
|
||||
/>
|
||||
{message.pending ? (
|
||||
<p className="shrink-0 font-medium uppercase tracking-[0.14em] text-primary/80">
|
||||
Sending
|
||||
</p>
|
||||
) : null}
|
||||
{message.edited ? (
|
||||
<p
|
||||
className="shrink-0 text-muted-foreground/70"
|
||||
title="This message has been edited"
|
||||
>
|
||||
(edited)
|
||||
</p>
|
||||
) : null}
|
||||
<MessageTimestamp
|
||||
createdAt={message.createdAt}
|
||||
time={message.time}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-1">{renderBody()}</div>
|
||||
<MessageReactions
|
||||
messageId={message.id}
|
||||
reactions={reactions}
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarSeparator,
|
||||
} from "@/shared/ui/sidebar";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -610,8 +609,6 @@ export function AppSidebar({
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarSeparator className="mx-0 w-full" />
|
||||
|
||||
<SidebarContent>
|
||||
{isLoading ? (
|
||||
<SidebarGroup>
|
||||
@@ -710,8 +707,6 @@ export function AppSidebar({
|
||||
) : null}
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarSeparator className="mx-0 w-full" />
|
||||
|
||||
<SidebarFooter>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
|
||||
@@ -237,7 +237,7 @@ function MarkdownInner({
|
||||
<div
|
||||
className={cn(
|
||||
tight
|
||||
? "max-w-none break-words text-sm leading-5 text-foreground/90 [&>*: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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user