mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Tune chat text sizing (#891)
This commit is contained in:
@@ -164,10 +164,16 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
}: ChannelPaneProps) {
|
||||
const timelineScrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const composerWrapperRef = React.useRef<HTMLDivElement>(null);
|
||||
const isNonMemberView =
|
||||
activeChannel !== null &&
|
||||
!activeChannel.isMember &&
|
||||
activeChannel.visibility === "open" &&
|
||||
!activeChannel.archivedAt;
|
||||
const hasMainComposerOverlay = !isNonMemberView;
|
||||
useComposerHeightPadding(
|
||||
timelineScrollRef,
|
||||
composerWrapperRef,
|
||||
isSinglePanelView,
|
||||
`${isSinglePanelView}:${hasMainComposerOverlay}`,
|
||||
);
|
||||
|
||||
// Scope the edit target to the correct composer: if the message being edited
|
||||
@@ -225,12 +231,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
return true;
|
||||
}, [findLastOwnEditable, onEdit, threadHeadMessage, threadMessages]);
|
||||
|
||||
const isNonMemberView =
|
||||
activeChannel !== null &&
|
||||
!activeChannel.isMember &&
|
||||
activeChannel.visibility === "open" &&
|
||||
!activeChannel.archivedAt;
|
||||
|
||||
const isComposerDisabled =
|
||||
!activeChannel?.isMember ||
|
||||
activeChannel.archivedAt !== null ||
|
||||
@@ -309,6 +309,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
currentPubkey={currentPubkey}
|
||||
fetchOlder={fetchOlder}
|
||||
followThreadById={followThreadById}
|
||||
hasComposerOverlay={hasMainComposerOverlay}
|
||||
hasOlderMessages={hasOlderMessages}
|
||||
isFetchingOlder={isFetchingOlder}
|
||||
isFollowingThreadById={isFollowingThreadById}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
formatDayHeading,
|
||||
formatShortMonthDayOrdinal,
|
||||
formatThreadSummaryLastReplyTime,
|
||||
} from "./dateFormatters.ts";
|
||||
|
||||
function localUnixSeconds(year, monthIndex, day) {
|
||||
return new Date(year, monthIndex, day, 12).getTime() / 1_000;
|
||||
}
|
||||
|
||||
function weekday(date) {
|
||||
return new Intl.DateTimeFormat("en-US", { weekday: "long" }).format(date);
|
||||
}
|
||||
|
||||
function month(date) {
|
||||
return new Intl.DateTimeFormat("en-US", { month: "long" }).format(date);
|
||||
}
|
||||
|
||||
test("formatShortMonthDayOrdinal formats month before ordinal day", () => {
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 19)),
|
||||
"May 19th",
|
||||
);
|
||||
});
|
||||
|
||||
test("formatShortMonthDayOrdinal handles ordinal suffixes", () => {
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 1)),
|
||||
"May 1st",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 2)),
|
||||
"May 2nd",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 3)),
|
||||
"May 3rd",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 4)),
|
||||
"May 4th",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 11)),
|
||||
"May 11th",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 12)),
|
||||
"May 12th",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 13)),
|
||||
"May 13th",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 21)),
|
||||
"May 21st",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 22)),
|
||||
"May 22nd",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 23)),
|
||||
"May 23rd",
|
||||
);
|
||||
assert.equal(
|
||||
formatShortMonthDayOrdinal(localUnixSeconds(2026, 4, 31)),
|
||||
"May 31st",
|
||||
);
|
||||
});
|
||||
|
||||
test("formatThreadSummaryLastReplyTime expands relative units", () => {
|
||||
const now = localUnixSeconds(2026, 4, 19);
|
||||
|
||||
assert.equal(formatThreadSummaryLastReplyTime(now - 30, now), "just now");
|
||||
assert.equal(formatThreadSummaryLastReplyTime(now - 60, now), "1 minute ago");
|
||||
assert.equal(
|
||||
formatThreadSummaryLastReplyTime(now - 180, now),
|
||||
"3 minutes ago",
|
||||
);
|
||||
assert.equal(
|
||||
formatThreadSummaryLastReplyTime(now - 3_600, now),
|
||||
"1 hour ago",
|
||||
);
|
||||
assert.equal(
|
||||
formatThreadSummaryLastReplyTime(now - 10_800, now),
|
||||
"3 hours ago",
|
||||
);
|
||||
assert.equal(
|
||||
formatThreadSummaryLastReplyTime(now - 86_400, now),
|
||||
"1 day ago",
|
||||
);
|
||||
assert.equal(
|
||||
formatThreadSummaryLastReplyTime(now - 345_600, now),
|
||||
"4 days ago",
|
||||
);
|
||||
});
|
||||
|
||||
test("formatThreadSummaryLastReplyTime uses ordinal dates for older replies", () => {
|
||||
const now = localUnixSeconds(2026, 5, 15);
|
||||
const replyAt = localUnixSeconds(2026, 4, 19);
|
||||
|
||||
assert.equal(formatThreadSummaryLastReplyTime(replyAt, now), "on May 19th");
|
||||
});
|
||||
|
||||
test("formatDayHeading omits the year for current-year dates", () => {
|
||||
const now = new Date();
|
||||
const date = new Date(now.getFullYear(), (now.getMonth() + 6) % 12, 19, 12);
|
||||
|
||||
assert.equal(
|
||||
formatDayHeading(date.getTime() / 1_000),
|
||||
`${weekday(date)}, ${month(date)} 19th`,
|
||||
);
|
||||
});
|
||||
|
||||
test("formatDayHeading includes the year for other years", () => {
|
||||
const year = new Date().getFullYear() - 1;
|
||||
const date = new Date(year, 4, 19, 12);
|
||||
|
||||
assert.equal(
|
||||
formatDayHeading(date.getTime() / 1_000),
|
||||
`${weekday(date)}, May 19th, ${year}`,
|
||||
);
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
* - `formatFullDateTime` — verbose string for tooltips
|
||||
* ("Wednesday, April 2, 2026 at 2:34 PM").
|
||||
* - `formatDayHeading` — label for day dividers / sticky headers.
|
||||
* Returns "Today", "Yesterday", or a date like "Monday, March 31, 2026".
|
||||
* Returns "Today", "Yesterday", or a date like "Monday, March 31st".
|
||||
* - `isSameDay` — compare two unix-second timestamps.
|
||||
*/
|
||||
|
||||
@@ -23,11 +23,16 @@ const FULL_DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
const DAY_HEADING_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
const WEEKDAY_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
const LONG_MONTH_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const SHORT_MONTH_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
});
|
||||
|
||||
/** Short clock time, e.g. "2:34 PM". */
|
||||
@@ -42,7 +47,8 @@ export function formatFullDateTime(unixSeconds: number): string {
|
||||
|
||||
/**
|
||||
* Human-friendly day label for dividers and sticky headers.
|
||||
* Returns "Today", "Yesterday", or a full date like "Monday, March 31, 2026".
|
||||
* Returns "Today", "Yesterday", a current-year date like "Monday, March 31st",
|
||||
* or a prior-year date like "Monday, March 31st, 2025".
|
||||
*/
|
||||
export function formatDayHeading(unixSeconds: number): string {
|
||||
const date = new Date(unixSeconds * 1_000);
|
||||
@@ -58,7 +64,13 @@ export function formatDayHeading(unixSeconds: number): string {
|
||||
return "Yesterday";
|
||||
}
|
||||
|
||||
return DAY_HEADING_FORMATTER.format(date);
|
||||
const dateLabel = `${WEEKDAY_FORMATTER.format(date)}, ${formatMonthDayOrdinal(
|
||||
date,
|
||||
LONG_MONTH_FORMATTER,
|
||||
)}`;
|
||||
return date.getFullYear() === now.getFullYear()
|
||||
? dateLabel
|
||||
: `${dateLabel}, ${date.getFullYear()}`;
|
||||
}
|
||||
|
||||
/** True when two unix-second timestamps fall on the same calendar day (local time). */
|
||||
@@ -66,6 +78,32 @@ export function isSameDay(a: number, b: number): boolean {
|
||||
return isSameDayDate(new Date(a * 1_000), new Date(b * 1_000));
|
||||
}
|
||||
|
||||
/** Short month + ordinal day, e.g. "May 19th". */
|
||||
export function formatShortMonthDayOrdinal(unixSeconds: number): string {
|
||||
return formatMonthDayOrdinal(
|
||||
new Date(unixSeconds * 1_000),
|
||||
SHORT_MONTH_FORMATTER,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relative thread-summary timestamp with expanded units, e.g. "3 hours ago",
|
||||
* falling back to "on May 19th" for older replies.
|
||||
*/
|
||||
export function formatThreadSummaryLastReplyTime(
|
||||
unixSeconds: number,
|
||||
nowSeconds = Date.now() / 1_000,
|
||||
): string {
|
||||
const diff = Math.max(0, nowSeconds - unixSeconds);
|
||||
|
||||
if (diff < 60) return "just now";
|
||||
if (diff < 3_600) return formatAgo(Math.floor(diff / 60), "minute");
|
||||
if (diff < 86_400) return formatAgo(Math.floor(diff / 3_600), "hour");
|
||||
if (diff < 604_800) return formatAgo(Math.floor(diff / 86_400), "day");
|
||||
|
||||
return `on ${formatShortMonthDayOrdinal(unixSeconds)}`;
|
||||
}
|
||||
|
||||
function isSameDayDate(a: Date, b: Date): boolean {
|
||||
return (
|
||||
a.getFullYear() === b.getFullYear() &&
|
||||
@@ -73,3 +111,34 @@ function isSameDayDate(a: Date, b: Date): boolean {
|
||||
a.getDate() === b.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function formatMonthDayOrdinal(
|
||||
date: Date,
|
||||
monthFormatter: Intl.DateTimeFormat,
|
||||
): string {
|
||||
return `${monthFormatter.format(date)} ${date.getDate()}${ordinalSuffix(
|
||||
date.getDate(),
|
||||
)}`;
|
||||
}
|
||||
|
||||
function formatAgo(value: number, unit: string): string {
|
||||
return `${value} ${unit}${value === 1 ? "" : "s"} ago`;
|
||||
}
|
||||
|
||||
function ordinalSuffix(day: number): string {
|
||||
const lastTwoDigits = day % 100;
|
||||
if (lastTwoDigits >= 11 && lastTwoDigits <= 13) {
|
||||
return "th";
|
||||
}
|
||||
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
return "st";
|
||||
case 2:
|
||||
return "nd";
|
||||
case 3:
|
||||
return "rd";
|
||||
default:
|
||||
return "th";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ export const MessageRow = React.memo(
|
||||
return (
|
||||
<Markdown
|
||||
channelNames={channelNames}
|
||||
className="max-w-full"
|
||||
className="max-w-full text-[15px] leading-6"
|
||||
content={message.body}
|
||||
customEmoji={customEmoji}
|
||||
imetaByUrl={imetaByUrl}
|
||||
@@ -200,11 +200,11 @@ export const MessageRow = React.memo(
|
||||
);
|
||||
|
||||
const authorNode = message.pubkey ? (
|
||||
<span className="truncate text-sm font-semibold leading-none tracking-tight hover:underline">
|
||||
<span className="truncate text-[15px] font-semibold leading-none tracking-tight hover:underline">
|
||||
{message.author}
|
||||
</span>
|
||||
) : (
|
||||
<h3 className="truncate text-sm font-semibold leading-none tracking-tight">
|
||||
<h3 className="truncate text-[15px] font-semibold leading-none tracking-tight">
|
||||
{message.author}
|
||||
</h3>
|
||||
);
|
||||
|
||||
@@ -3,28 +3,13 @@ import type {
|
||||
TimelineThreadSummaryParticipant,
|
||||
} from "@/features/messages/lib/threadPanel";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import { formatThreadSummaryLastReplyTime } from "@/features/messages/lib/dateFormatters";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
|
||||
const MESSAGE_TEXT_OFFSET_PX = 54;
|
||||
const MESSAGE_BODY_OFFSET_PX = MESSAGE_TEXT_OFFSET_PX + 4;
|
||||
const NESTED_REPLY_OFFSET_PX = 28;
|
||||
|
||||
function formatLastReplyTime(unixSeconds: number): string {
|
||||
const now = Date.now() / 1_000;
|
||||
const diff = now - unixSeconds;
|
||||
|
||||
if (diff < 60) return "just now";
|
||||
if (diff < 3_600) return `${Math.floor(diff / 60)}m ago`;
|
||||
if (diff < 86_400) return `${Math.floor(diff / 3_600)}h ago`;
|
||||
if (diff < 604_800) return `${Math.floor(diff / 86_400)}d ago`;
|
||||
|
||||
const date = new Date(unixSeconds * 1_000).toLocaleDateString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
return `on ${date}`;
|
||||
}
|
||||
|
||||
function ParticipantAvatar({
|
||||
hasNextParticipant,
|
||||
participant,
|
||||
@@ -69,7 +54,7 @@ export function MessageThreadSummaryRow({
|
||||
const marginLeftPx = indentPx + MESSAGE_BODY_OFFSET_PX;
|
||||
const replyLabel = summary.replyCount === 1 ? "reply" : "replies";
|
||||
const summaryAriaLabel = summary.lastReplyAt
|
||||
? `View thread with ${summary.replyCount} ${replyLabel}, last reply ${formatLastReplyTime(summary.lastReplyAt)}`
|
||||
? `View thread with ${summary.replyCount} ${replyLabel}, last reply ${formatThreadSummaryLastReplyTime(summary.lastReplyAt)}`
|
||||
: `View thread with ${summary.replyCount} ${replyLabel}`;
|
||||
const depthGuideOffsets =
|
||||
visibleDepth === 0
|
||||
@@ -137,7 +122,8 @@ export function MessageThreadSummaryRow({
|
||||
className="col-start-1 row-start-1 transition-opacity group-hover:opacity-0 group-focus-visible:opacity-0"
|
||||
data-testid="message-thread-summary-last-reply"
|
||||
>
|
||||
last reply {formatLastReplyTime(summary.lastReplyAt)}
|
||||
last reply{" "}
|
||||
{formatThreadSummaryLastReplyTime(summary.lastReplyAt)}
|
||||
</span>
|
||||
<span
|
||||
className="col-start-1 row-start-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ArrowDown } from "lucide-react";
|
||||
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Separator } from "@/shared/ui/separator";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
@@ -24,6 +25,8 @@ type MessageTimelineProps = {
|
||||
/** Optional external ref to the scroll container — used by the parent to
|
||||
* observe scroll position or adjust padding dynamically. */
|
||||
scrollContainerRef?: React.RefObject<HTMLDivElement | null>;
|
||||
/** True when the timeline has the composer overlay below it. */
|
||||
hasComposerOverlay?: boolean;
|
||||
isFetchingOlder?: boolean;
|
||||
messageFooters?: Record<string, React.ReactNode>;
|
||||
/** Map from lowercase pubkey → persona display name for bot members. */
|
||||
@@ -59,6 +62,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
emptyDescription = "Send the first message to start the thread.",
|
||||
currentPubkey,
|
||||
fetchOlder,
|
||||
hasComposerOverlay = true,
|
||||
hasOlderMessages = true,
|
||||
isFetchingOlder = false,
|
||||
followThreadById,
|
||||
@@ -141,7 +145,10 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pb-24 pt-1 [overflow-anchor:none] sm:px-6"
|
||||
className={cn(
|
||||
"absolute inset-0 overflow-y-auto overflow-x-hidden overscroll-contain px-4 pt-1 [overflow-anchor:none] sm:px-6",
|
||||
hasComposerOverlay ? "pb-24" : "pb-4",
|
||||
)}
|
||||
data-scroll-restoration-id={scrollRestorationId}
|
||||
data-testid="message-timeline"
|
||||
onScroll={syncScrollState}
|
||||
@@ -216,7 +223,12 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
</div>
|
||||
|
||||
{!isAtBottom ? (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-36 z-20 flex justify-center px-4">
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 z-20 flex justify-center px-4",
|
||||
hasComposerOverlay ? "bottom-36" : "bottom-4",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className="pointer-events-auto h-7 min-h-7 gap-1.5 rounded-full border-border/50 bg-background/85 px-2.5 text-[11px] font-medium text-muted-foreground shadow-xs backdrop-blur-sm hover:bg-muted/70 hover:text-foreground [&_svg]:size-3.5"
|
||||
data-testid="message-scroll-to-latest"
|
||||
|
||||
@@ -10,6 +10,10 @@ import { resolveUserLabel } from "@/features/profile/lib/identity";
|
||||
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
MENTION_CHIP_BASE_CLASSES,
|
||||
MENTION_CHIP_HOVER_CLASSES,
|
||||
} from "@/shared/ui/mentionChip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
@@ -78,10 +82,10 @@ function ProfileName({
|
||||
const node = (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-xs transition-colors hover:text-foreground",
|
||||
pubkey && "cursor-pointer",
|
||||
highlight &&
|
||||
"rounded-md bg-primary/15 px-1 py-0.5 font-medium text-primary hover:bg-primary/25 hover:text-primary/90",
|
||||
highlight
|
||||
? cn(MENTION_CHIP_BASE_CLASSES, MENTION_CHIP_HOVER_CLASSES)
|
||||
: "rounded-xs transition-colors hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{highlight ? "@" : null}
|
||||
@@ -222,12 +226,7 @@ function describeSystemEvent(
|
||||
}
|
||||
return {
|
||||
title: actorName,
|
||||
action: (
|
||||
<>
|
||||
added <span className="font-medium">{targetName}</span> to the
|
||||
channel
|
||||
</>
|
||||
),
|
||||
action: <>added {targetName} to the channel</>,
|
||||
};
|
||||
}
|
||||
case "member_left":
|
||||
@@ -238,12 +237,7 @@ function describeSystemEvent(
|
||||
case "member_removed":
|
||||
return {
|
||||
title: actorName,
|
||||
action: (
|
||||
<>
|
||||
removed <span className="font-medium">{targetName}</span> from the
|
||||
channel
|
||||
</>
|
||||
),
|
||||
action: <>removed {targetName} from the channel</>,
|
||||
};
|
||||
case "topic_changed":
|
||||
return {
|
||||
|
||||
@@ -114,11 +114,14 @@
|
||||
}
|
||||
|
||||
.rich-text-composer .tiptap .mention-highlight {
|
||||
border-radius: 0.375rem;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
padding: 0.125rem 0.375rem;
|
||||
padding: 3px 4px 2px;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.agent-activity-shimmer {
|
||||
|
||||
@@ -40,6 +40,10 @@ import remarkCustomEmoji, {
|
||||
import remarkMentions from "@/shared/lib/remarkMentions";
|
||||
import remarkMessageLinks from "@/features/messages/lib/remarkMessageLinks";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
MENTION_CHIP_BASE_CLASSES,
|
||||
MENTION_CHIP_HOVER_CLASSES,
|
||||
} from "@/shared/ui/mentionChip";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
|
||||
@@ -908,7 +912,11 @@ function createMarkdownComponents(
|
||||
const mentionNode = (
|
||||
<span
|
||||
data-mention=""
|
||||
className="cursor-pointer rounded-md bg-primary/15 px-1 py-0.5 text-sm font-semibold text-primary transition-colors hover:bg-primary/25 hover:text-primary/90"
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
MENTION_CHIP_BASE_CLASSES,
|
||||
MENTION_CHIP_HOVER_CLASSES,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const MENTION_CHIP_BASE_CLASSES =
|
||||
"inline-block rounded-[4px] bg-primary/15 px-1 pt-[3px] pb-[2px] text-[15px] font-medium leading-none text-primary";
|
||||
|
||||
export const MENTION_CHIP_HOVER_CLASSES =
|
||||
"transition-colors hover:bg-primary/25 hover:text-primary/90";
|
||||
Reference in New Issue
Block a user