feat(chat): 消息列表回到底部与贴底滚动优化
All checks were successful
CI / build (push) Successful in 2m7s

- 非底部时显示悬浮回到底部按钮,点击平滑滚底并隐藏
- 仅在已处于底部附近时随新消息自动贴底,避免打断上翻阅读
- 按钮使用原生圆钮并相对输入区 max-w-4xl 定位,避免 AntD 按钮宽条问题
- 同步 lucide-react 依赖及流式代码块、全局样式等小改动

Made-with: Cursor
This commit is contained in:
2026-04-24 04:39:24 +08:00
parent 3b0b6eac50
commit 891f09aa0d
5 changed files with 312 additions and 246 deletions

View File

@@ -30,6 +30,7 @@
"axios": "^1.15.0",
"highlight.js": "^11.11.1",
"katex": "^0.16.45",
"lucide-react": "^1.9.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
import { Button, message } from "antd";
import { Check, Copy } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
@@ -47,7 +47,7 @@ function MarkdownPreBlock(props: { children: ReactNode }) {
<Button
type="text"
size="small"
icon={copied ? <CheckOutlined /> : <CopyOutlined />}
icon={copied ? <Check size={16} /> : <Copy size={16} />}
className="text-neutral-600 hover:bg-black/5! hover:text-neutral-800!"
onClick={() => {
void onCopy();

View File

@@ -83,3 +83,9 @@ pre code.hljs {
.ant-dropdown-menu-item-danger {
border-radius: 12px !important;
}
/* 修正 lucide 图标在 AntD Button 中的基线偏移 */
.ant-btn .ant-btn-icon > .lucide {
display: block;
transform: translateY(1px);
}

View File

@@ -1,26 +1,27 @@
import type { CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import {
ArrowUpOutlined,
CopyOutlined,
DeleteOutlined,
EditOutlined,
LogoutOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
MobileOutlined,
MoreOutlined,
PaperClipOutlined,
PlusOutlined,
QuestionCircleOutlined,
RedoOutlined,
SearchOutlined,
ShareAltOutlined,
SettingOutlined,
ThunderboltOutlined,
VerticalAlignTopOutlined,
UserOutlined,
} from "@ant-design/icons";
ArrowUp,
ChevronDown,
Copy,
Ellipsis,
LogOut,
MessageCirclePlus,
Paperclip,
PanelLeftClose,
PanelLeftOpen,
Pencil,
Pin,
RotateCcw,
Search,
Settings,
Share2,
Smartphone,
Trash2,
User,
Zap,
CircleHelp,
} from "lucide-react";
import {
Avatar,
Button,
@@ -88,13 +89,7 @@ function subscribeUserProfile(onChange: () => void) {
};
}
const INITIAL_MESSAGES: UiMessage[] = [
{
id: "init-assistant",
role: "assistant",
content: "你好,我是 ChatOne 助手,有什么可以帮你?",
},
];
const INITIAL_MESSAGES: UiMessage[] = [];
const STREAM_TIMEOUT_MS = 90000;
const MAX_STREAM_RETRY = 1;
@@ -155,7 +150,9 @@ export default function HomePage() {
}
});
const messageListRef = useRef<HTMLDivElement | null>(null);
const shouldStickToBottomRef = useRef(true);
const abortRef = useRef<AbortController | null>(null);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [sessionsLoading, setSessionsLoading] = useState(false);
@@ -200,18 +197,18 @@ export default function HomePage() {
const userMenuDividerStyle: CSSProperties = { margin: "10px 0" };
const userMenuItems: MenuProps["items"] = [
{ key: "app", icon: <MobileOutlined />, label: "下载手机应用", style: userMenuItemStyle },
{ key: "settings", icon: <SettingOutlined />, label: "系统设置", style: userMenuItemStyle },
{ key: "app", icon: <Smartphone size={16} />, label: "下载手机应用", style: userMenuItemStyle },
{ key: "settings", icon: <Settings size={16} />, label: "系统设置", style: userMenuItemStyle },
{
key: "help",
icon: <QuestionCircleOutlined />,
icon: <CircleHelp size={16} />,
label: "帮助与反馈",
style: userMenuItemStyle,
},
{ type: "divider", style: userMenuDividerStyle },
{
key: "logout",
icon: <LogoutOutlined />,
icon: <LogOut size={16} />,
label: "退出登录",
danger: true,
style: userMenuItemStyle,
@@ -258,11 +255,42 @@ export default function HomePage() {
};
}, []);
const isMessageListAtBottom = useCallback(() => {
const el = messageListRef.current;
if (!el) return true;
// 允许少量像素误差,避免小数像素造成闪烁。
return el.scrollHeight - el.scrollTop - el.clientHeight <= 24;
}, []);
const scrollMessageListToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
const el = messageListRef.current;
if (!el) return;
el.scrollTo({ top: el.scrollHeight, behavior });
}, []);
useEffect(() => {
const el = messageListRef.current;
if (!el) return;
el.scrollTop = el.scrollHeight;
}, [messages]);
const onScroll = () => {
const atBottom = isMessageListAtBottom();
shouldStickToBottomRef.current = atBottom;
setShowScrollToBottom(!atBottom);
};
onScroll();
el.addEventListener("scroll", onScroll, { passive: true });
return () => {
el.removeEventListener("scroll", onScroll);
};
}, [isMessageListAtBottom]);
useEffect(() => {
if (shouldStickToBottomRef.current) {
scrollMessageListToBottom("auto");
setShowScrollToBottom(false);
} else {
setShowScrollToBottom(true);
}
}, [messages, scrollMessageListToBottom]);
const refreshSessionList = useCallback(async (): Promise<ChatSession[]> => {
if (!window.localStorage.getItem(ACCESS_TOKEN_KEY)) return [];
@@ -567,7 +595,7 @@ export default function HomePage() {
loading={sessionMutationBusy}
disabled={sessionsLoading}
className="h-10! rounded-xl! border-[var(--ds-border)]! bg-[var(--ds-bg-main)]! text-[13px]! font-medium shadow-none hover:border-sky-300! hover:text-sky-600!"
icon={<PlusOutlined />}
icon={<MessageCirclePlus size={14} strokeWidth={2} />}
onClick={() => {
void handleNewSession();
}}
@@ -600,9 +628,9 @@ export default function HomePage() {
const active = sid === activeSessionId;
const itemMenu: MenuProps = {
items: [
{ key: "rename", icon: <EditOutlined />, label: "重命名" },
{ key: "pin", icon: <VerticalAlignTopOutlined />, label: "置顶" },
{ key: "delete", icon: <DeleteOutlined />, danger: true, label: "删除" },
{ key: "rename", icon: <Pencil size={16} />, label: "重命名" },
{ key: "pin", icon: <Pin size={16} />, label: "置顶" },
{ key: "delete", icon: <Trash2 size={16} />, danger: true, label: "删除" },
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
@@ -614,7 +642,7 @@ export default function HomePage() {
return (
<div
key={sid}
className="mb-1 flex w-full items-center gap-1 rounded-xl pr-1 text-[14px] text-neutral-700 transition-colors hover:bg-black/5"
className="group mb-1.5 flex w-full items-center gap-1 rounded-2xl pr-1 text-[14px] text-neutral-700 transition-colors hover:bg-black/5"
style={{
background: active ? "var(--ds-active-item)" : undefined,
color: active ? "var(--ds-accent)" : undefined,
@@ -625,23 +653,29 @@ export default function HomePage() {
onClick={() => {
void openSession(sid);
}}
className="flex min-h-12 min-w-0 flex-1 items-center rounded-xl px-3 py-3 text-left"
className="flex h-11 min-w-0 flex-1 cursor-pointer items-center rounded-2xl px-4 text-left"
>
{(!collapsed || isMobile) && <span className="truncate">{title}</span>}
{(!collapsed || isMobile) && (
<span className="min-w-0 flex-1 truncate text-[14px] leading-none">
{title}
</span>
)}
</button>
{(!collapsed || isMobile) && (
<Dropdown menu={itemMenu} trigger={["click"]} placement="bottomRight">
<Button
type="text"
size="small"
icon={<MoreOutlined />}
loading={sessionActionBusyId === sid}
className="text-neutral-500 hover:bg-black/5!"
onClick={(event) => {
event.stopPropagation();
}}
/>
</Dropdown>
<div className={`${active ? "flex" : "hidden group-hover:flex"}`}>
<Dropdown menu={itemMenu} trigger={["click"]} placement="bottomRight">
<Button
type="text"
size="small"
icon={<Ellipsis size={16} />}
loading={sessionActionBusyId === sid}
className="!flex !h-8 !w-8 !items-center !justify-center rounded-xl text-neutral-500 hover:bg-black/5!"
onClick={(event) => {
event.stopPropagation();
}}
/>
</Dropdown>
</div>
)}
</div>
);
@@ -650,7 +684,7 @@ export default function HomePage() {
)}
</div>
<div className="shrink-0 border-t border-[var(--ds-border)] p-3">
<div className="shrink-0 p-3">
<Dropdown
menu={{ items: userMenuItems, onClick: onUserMenuClick }}
placement="topLeft"
@@ -658,14 +692,14 @@ export default function HomePage() {
>
<button
type="button"
className={`flex w-full items-center gap-2 rounded-xl bg-neutral-100 px-2 py-2 text-left transition-colors hover:bg-neutral-200/90 ${
className={`flex w-full cursor-pointer items-center gap-2 rounded-xl bg-neutral-100 px-2 py-2 text-left transition-colors hover:bg-neutral-200/90 ${
collapsed && !isMobile ? "justify-center py-2.5" : ""
}`}
>
<Avatar
size={36}
src={userProfile.avatar}
icon={!userProfile.avatar ? <UserOutlined /> : undefined}
icon={!userProfile.avatar ? <User size={18} /> : undefined}
className="shrink-0 bg-neutral-300"
/>
{(!collapsed || isMobile) && (
@@ -673,7 +707,7 @@ export default function HomePage() {
<span className="min-w-0 flex-1 truncate text-[13px] font-medium text-neutral-800">
{userProfile.displayName}
</span>
<MoreOutlined className="shrink-0 text-neutral-500" />
<Ellipsis size={16} className="shrink-0 text-neutral-500" />
</>
)}
</button>
@@ -696,10 +730,10 @@ export default function HomePage() {
width={260}
collapsedWidth={72}
theme="light"
className="!min-h-0 !h-full !overflow-hidden !bg-[var(--ds-bg-sider)] !border-r !border-[var(--ds-border)]"
className="!min-h-0 !h-full !overflow-hidden !bg-[var(--ds-bg-sider)]"
>
<div className="flex h-full min-h-0 min-w-0 flex-col">
<div className="flex h-14 shrink-0 items-center gap-2 border-b border-[var(--ds-border)] px-3">
<div className="flex h-14 shrink-0 items-center gap-2 px-3">
<img
src="/logo.png"
alt=""
@@ -724,7 +758,7 @@ export default function HomePage() {
style={{ background: "var(--ds-bg-main)" }}
>
<Header
className="!flex !h-14 !shrink-0 !items-center !justify-between !border-b !border-[var(--ds-border)] !bg-[var(--ds-bg-main)] !px-5"
className="!flex !h-14 !shrink-0 !items-center !justify-between !bg-[var(--ds-bg-main)] !px-5"
style={{ lineHeight: "56px" }}
>
<div className="flex items-center gap-3">
@@ -733,11 +767,11 @@ export default function HomePage() {
className="text-neutral-500 hover:bg-black/5!"
icon={
isMobile ? (
<MenuUnfoldOutlined />
<PanelLeftOpen size={16} />
) : collapsed ? (
<MenuUnfoldOutlined />
<PanelLeftOpen size={16} />
) : (
<MenuFoldOutlined />
<PanelLeftClose size={16} />
)
}
onClick={() => {
@@ -770,203 +804,223 @@ export default function HomePage() {
</Header>
<Content className="flex min-h-0 flex-1 flex-col overflow-hidden !bg-[var(--ds-bg-main)]">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div
ref={messageListRef}
className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 pt-6 pb-0 md:px-8 md:pt-6 md:pb-0"
>
<div className="mx-auto flex max-w-4xl flex-col gap-5">
{messages.map((item) => (
<div
key={item.id}
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
>
{!activeSessionId && messages.length === 0 ? (
<p className="px-4 text-[18px] text-neutral-800"> ChatOne</p>
) : (
messages.map((item) => (
<div
className={
item.role === "user" ? "group max-w-full px-4 " : "group w-full space-y-2"
}
key={item.id}
className={`flex ${item.role === "user" ? "justify-end" : "justify-start"}`}
>
{item.role === "user" && (
<div
className="rounded-2xl px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800"
style={{ background: "var(--ds-user-bubble)" }}
>
{item.content}
</div>
)}
{item.role === "assistant" && item.thinking && (
<Collapse
bordered={false}
size="small"
className="!mb-2 !rounded-xl !bg-transparent"
items={[
{
key: "think",
label: (
<span className="text-[13px] text-neutral-600">
<ThunderboltOutlined className="mr-1.5" />
2
</span>
),
children: (
<Typography.Text type="secondary" className="text-[13px]">
{item.thinking}
</Typography.Text>
),
},
]}
/>
)}
{item.role === "assistant" &&
(item.content ? (
<StreamMessage content={item.content} />
) : (
<div className="rounded-2xl px-4 py-3 text-[14px] text-neutral-500">
<span className="inline-flex items-center gap-1">
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.3s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.15s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400" />
</span>
<div
className={
item.role === "user" ? "group max-w-full px-4 " : "group w-full space-y-2"
}
>
{item.role === "user" && (
<div
className="rounded-2xl px-4 py-2.5 text-[15px] leading-relaxed text-neutral-800"
style={{ background: "var(--ds-user-bubble)" }}
>
{item.content}
</div>
))}
{item.role === "assistant" && item.content && (
<>
<p className="mt-3 px-4 text-[12px] text-neutral-500 italic">
AI
</p>
<div className="mt-2 flex h-8 items-center gap-1 px-4 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
<Tooltip title="复制">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="复制回复"
onClick={() => {
void handleCopyAssistantMessage(item.content);
}}
/>
</Tooltip>
<Tooltip title="重新生成">
<Button
type="text"
size="small"
icon={<RedoOutlined />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="重新生成"
onClick={() => {
message.info("重新生成功能开发中");
}}
/>
</Tooltip>
<Tooltip title="分享">
<Button
type="text"
size="small"
icon={<ShareAltOutlined />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="分享"
onClick={() => {
message.info("分享功能开发中");
}}
/>
</Tooltip>
)}
{item.role === "assistant" && item.thinking && (
<Collapse
bordered={false}
size="small"
className="!mb-2 !rounded-xl !bg-transparent"
items={[
{
key: "think",
label: (
<span className="text-[13px] text-neutral-600">
<Zap size={14} className="mr-1.5" />
2
</span>
),
children: (
<Typography.Text type="secondary" className="text-[13px]">
{item.thinking}
</Typography.Text>
),
},
]}
/>
)}
{item.role === "assistant" &&
(item.content ? (
<StreamMessage content={item.content} />
) : (
<div className="rounded-2xl px-4 py-3 text-[14px] text-neutral-500">
<span className="inline-flex items-center gap-1">
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.3s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400 [animation-delay:-0.15s]" />
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-neutral-400" />
</span>
</div>
))}
{item.role === "assistant" && item.content && (
<>
<p className="mt-3 px-4 text-[12px] text-neutral-500 italic">
AI
</p>
<div className="mt-2 flex h-8 items-center gap-1 px-4 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
<Tooltip title="复制">
<Button
type="text"
size="small"
icon={<Copy size={16} />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="复制回复"
onClick={() => {
void handleCopyAssistantMessage(item.content);
}}
/>
</Tooltip>
<Tooltip title="重新生成">
<Button
type="text"
size="small"
icon={<RotateCcw size={16} />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="重新生成"
onClick={() => {
message.info("重新生成功能开发中");
}}
/>
</Tooltip>
<Tooltip title="分享">
<Button
type="text"
size="small"
icon={<Share2 size={16} />}
className="!px-1 text-neutral-500 hover:bg-black/5!"
aria-label="分享"
onClick={() => {
message.info("分享功能开发中");
}}
/>
</Tooltip>
</div>
</>
)}
{item.role === "user" && (
<div className="mt-2 flex h-7 items-center justify-end gap-1 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
<Button
type="text"
size="small"
icon={<Copy size={16} />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="复制消息"
onClick={() => {
void handleCopyUserMessage(item.content);
}}
/>
<Button
type="text"
size="small"
icon={<Pencil size={16} />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="编辑消息"
onClick={() => {
message.info("编辑功能开发中");
}}
/>
</div>
</>
)}
{item.role === "user" && (
<div className="mt-2 flex h-7 items-center justify-end gap-1 text-neutral-500 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto">
<Button
type="text"
size="small"
icon={<CopyOutlined />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="复制消息"
onClick={() => {
void handleCopyUserMessage(item.content);
}}
/>
<Button
type="text"
size="small"
icon={<EditOutlined />}
className="text-neutral-500 hover:bg-black/5!"
aria-label="编辑消息"
onClick={() => {
message.info("编辑功能开发中");
}}
/>
</div>
)}
)}
</div>
</div>
</div>
))}
))
)}
</div>
</div>
<div className="shrink-0 bg-[var(--ds-bg-main)] px-4 pt-0 md:px-8 md:pt-0">
<div className="mx-auto max-w-4xl">
<div className="rounded-3xl border border-[var(--ds-border)] bg-neutral-50/50 p-3 shadow-sm">
<Input.TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onPressEnter={(e) => {
if (e.shiftKey) return;
e.preventDefault();
void sendMessage();
}}
placeholder="给 ChatOne 发送消息"
variant="borderless"
autoSize={{ minRows: 2, maxRows: 8 }}
className="!px-1 !text-[15px] placeholder:text-neutral-400"
/>
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setDeepThink((v) => !v)}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
deepThink
? "border-sky-300 bg-sky-50 text-sky-700"
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
}`}
>
<ThunderboltOutlined />
</button>
<button
type="button"
onClick={() => setSmartSearch((v) => !v)}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
smartSearch
? "border-sky-300 bg-sky-50 text-sky-700"
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
}`}
>
<SearchOutlined />
</button>
</div>
<div className="flex items-center gap-2">
<Button
type="text"
icon={<PaperClipOutlined className="text-neutral-400" />}
className="text-neutral-400"
/>
<Button
type="primary"
shape="circle"
icon={<ArrowUpOutlined />}
loading={isSending}
disabled={isSending || !inputValue.trim()}
onClick={() => {
void sendMessage();
}}
className="!flex !h-9 !w-9 !items-center !justify-center !border-0 !shadow-none"
style={{
background: "var(--ds-send)",
}}
/>
<div className="relative">
{showScrollToBottom && (
<button
type="button"
aria-label="滚动到底部"
className="absolute -top-14 right-3 z-10 flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-neutral-200/90 bg-white text-neutral-900 shadow-md transition-colors hover:bg-neutral-50"
onClick={() => {
shouldStickToBottomRef.current = true;
setShowScrollToBottom(false);
scrollMessageListToBottom("smooth");
}}
>
<ChevronDown color="#666" size={18} strokeWidth={2} aria-hidden />
</button>
)}
<div className="rounded-3xl border border-[var(--ds-border)] bg-neutral-50/50 p-3 shadow-sm">
<Input.TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onPressEnter={(e) => {
if (e.shiftKey) return;
e.preventDefault();
void sendMessage();
}}
placeholder="给 ChatOne 发送消息"
variant="borderless"
autoSize={{ minRows: 2, maxRows: 8 }}
className="!px-1 !text-[15px] placeholder:text-neutral-400"
/>
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setDeepThink((v) => !v)}
className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
deepThink
? "border-sky-300 bg-sky-50 text-sky-700"
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
}`}
>
<Zap size={14} />
</button>
<button
type="button"
onClick={() => setSmartSearch((v) => !v)}
className={`inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs transition-colors ${
smartSearch
? "border-sky-300 bg-sky-50 text-sky-700"
: "border-[var(--ds-border)] bg-[var(--ds-bg-main)] text-neutral-600 hover:border-neutral-300"
}`}
>
<Search size={14} />
</button>
</div>
<div className="flex items-center gap-2">
<Button
type="text"
icon={<Paperclip size={16} className="text-neutral-400" />}
className="text-neutral-400"
/>
<Button
type="primary"
shape="circle"
icon={<ArrowUp size={16} />}
loading={isSending}
disabled={isSending || !inputValue.trim()}
onClick={() => {
void sendMessage();
}}
className="!flex !h-9 !w-9 !items-center !justify-center !border-0 !shadow-none"
style={{
background: "var(--ds-send)",
}}
/>
</div>
</div>
</div>
</div>

View File

@@ -2932,6 +2932,11 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
lucide-react@^1.9.0:
version "1.9.0"
resolved "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.9.0.tgz#3c8324e6b574131624c1869cbd38829d9c659627"
integrity sha512-6qVAmbgCjcJz7sAGSPSSJ++RAwjlK2XCbRrZKv63Ciko1KT8jX0//CXxgI3jg2HlJu8tADqdYlNDebmYjeoruA==
magic-string@^0.30.21:
version "0.30.21"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"