feat(chat): rewrite chat page with chatscope UI and custom hooks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-04-11 13:27:46 +02:00
parent db59316dc3
commit bb0aa5e28e
6 changed files with 361 additions and 69 deletions

View file

@ -75,6 +75,8 @@
"@blocknote/core": "^0.41.1",
"@blocknote/mantine": "^0.41.1",
"@blocknote/react": "^0.41.1",
"@chatscope/chat-ui-kit-react": "^2.1.1",
"@chatscope/chat-ui-kit-styles": "^1.4.0",
"@datadog/browser-rum": "^6.13.0",
"@datadog/browser-rum-react": "^6.13.0",
"@hookform/resolvers": "^5.2.2",

View file

@ -0,0 +1,90 @@
import { ChannelBadge } from "@ui/components/ChannelBadge";
import type { UserTablo } from "@xtablo/shared/types/tablos.types";
import { Badge } from "@xtablo/ui/components/badge";
import { twMerge } from "tailwind-merge";
interface ChatChannelPreviewProps {
tablo: UserTablo;
isActive: boolean;
onClick: () => void;
unreadCount: number;
lastMessage?: string;
lastMessageTime?: string;
isOnline: boolean;
}
function formatTimestamp(timestamp: string | Date): string {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return "now";
if (minutes < 60) return `${minutes}m`;
if (hours < 24) return `${hours}h`;
if (days < 7) return `${days}d`;
return date.toLocaleDateString();
}
export function ChatChannelPreview({
tablo,
isActive,
onClick,
unreadCount,
lastMessage,
lastMessageTime,
isOnline,
}: ChatChannelPreviewProps) {
return (
<div
className={twMerge(
"group relative flex items-center gap-3 p-3 cursor-pointer transition-all duration-200 hover:bg-gray-50 dark:hover:bg-gray-800/50 border-b border-gray-100 dark:border-gray-800",
isActive && "bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800"
)}
onClick={onClick}
>
<ChannelBadge tablo={tablo} displayTitle={tablo.name} isOnline={isOnline} />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h3
className={twMerge(
"font-medium text-gray-900 dark:text-gray-100 truncate",
isActive && "text-[#804EEC] dark:text-purple-400"
)}
>
{tablo.name}
</h3>
{lastMessageTime && (
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2 shrink-0">
{formatTimestamp(lastMessageTime)}
</span>
)}
</div>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 max-h-10 overflow-hidden">
{lastMessage ?? "No messages yet"}
</p>
{unreadCount > 0 && (
<div className="ml-2 shrink-0">
<Badge
color="indigo"
className="text-xs min-w-[20px] h-5 px-2 py-0 flex items-center justify-center"
>
{unreadCount > 99 ? "99+" : unreadCount}
</Badge>
</div>
)}
</div>
</div>
{isActive && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[#804EEC] dark:bg-purple-400 rounded-r-full" />
)}
</div>
);
}

View file

@ -0,0 +1,54 @@
import { ChannelBadge } from "@ui/components/ChannelBadge";
import type { UserTablo } from "@xtablo/shared/types/tablos.types";
interface ChatHeaderProps {
tablo: UserTablo | null;
onToggleChannelList?: () => void;
isChannelListExpanded?: boolean;
onlineUsers: string[];
}
export function ChatHeader({
tablo,
onToggleChannelList,
isChannelListExpanded = false,
onlineUsers,
}: ChatHeaderProps) {
const memberCount = onlineUsers.length;
return (
<div className="flex items-center px-4 py-3 border-b border-gray-200 dark:border-gray-600/50 bg-white dark:bg-gray-800/40">
{onToggleChannelList && (
<button
onClick={onToggleChannelList}
className="mr-2 p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
aria-label="Toggle channel list"
>
<svg
className={`w-5 h-5 transition-transform duration-200 ${
isChannelListExpanded ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
)}
{tablo && (
<>
<ChannelBadge tablo={tablo} displayTitle={tablo.name} isOnline={memberCount > 0} />
<div className="ml-3">
<h2 className="font-semibold text-gray-900 dark:text-gray-100">{tablo.name}</h2>
{memberCount > 0 && (
<p className="text-xs text-gray-500 dark:text-gray-400">
{memberCount} online
</p>
)}
</div>
</>
)}
</div>
);
}

View file

@ -28,7 +28,6 @@ import { TabloDetailsPage } from "../pages/tablo-details";
import { TablosPage } from "../pages/tablos";
import { TasksPage } from "../pages/tasks";
import { UpdatePasswordPage } from "../pages/update-password";
import ChatProvider from "../providers/ChatProvider";
export const routes: RouteObject[] = [
// Protected routes
@ -75,11 +74,7 @@ export const routes: RouteObject[] = [
},
{
path: "chat",
element: (
<ChatProvider>
<ChatPage />
</ChatProvider>
),
element: <ChatPage />,
children: [{ index: true }, { path: ":channelId" }],
},
// Notes feature temporarily hidden

View file

@ -1,39 +1,58 @@
import { ChannelPreview } from "@ui/components/ChannelPreview";
import { CustomChannelHeader } from "@ui/components/CustomChannelHeader";
import { useEffect, useState } from "react";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import {
Channel,
ChannelList,
MessageInput,
ChatContainer,
MessageList,
useChatContext,
Window,
} from "stream-chat-react";
import { useChannelFromUrl } from "../hooks/channel";
Message,
MessageInput,
TypingIndicator,
} from "@chatscope/chat-ui-kit-react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ChatChannelPreview } from "../components/ChatChannelPreview";
import { ChatHeader } from "../components/ChatHeader";
import { useChat } from "../hooks/useChat";
import { useChatUnread } from "../hooks/useChatUnread";
import { useTablosList } from "../hooks/tablos";
import { useUser } from "../providers/UserStoreProvider";
export function ChatPage() {
const user = useUser();
const filters = { members: { $in: [user.id] }, type: "messaging" };
const { client, channel, setActiveChannel } = useChatContext();
const { channel: channelFromUrl, isChannelInUrl } = useChannelFromUrl(client);
const { channelId } = useParams();
const navigate = useNavigate();
const { data: tablos } = useTablosList();
const [isChannelListExpanded, setIsChannelListExpanded] = useState(false);
const { getUnreadCount } = useChatUnread();
const [isChannelListExpanded, setIsChannelListExpanded] = useState(!channelId);
const toggleChannelList = () => {
setIsChannelListExpanded(!isChannelListExpanded);
const {
messages,
sendMessage,
sendTyping,
isConnected,
typingUsers,
onlineUsers,
loadMoreMessages,
hasMoreMessages,
markAsRead,
} = useChat(channelId);
const activeTablo = tablos?.find((t) => t.id === channelId) ?? null;
// Mark as read when channel is focused
useEffect(() => {
if (channelId && messages.length > 0) {
markAsRead();
}
}, [channelId, messages.length, markAsRead]);
const handleSend = (innerHtml: string, textContent: string) => {
const text = textContent.trim();
if (!text) return;
sendMessage(text);
};
useEffect(() => {
if (channelFromUrl) {
setActiveChannel(channelFromUrl);
} else {
setIsChannelListExpanded(true);
}
}, [channelFromUrl]);
const handleChannelSelect = (tabloId: string) => {
navigate(`/chat/${tabloId}`);
};
return (
<div className="flex flex-col h-[calc(100vh-75px)] bg-gray-50 dark:bg-background">
@ -41,46 +60,72 @@ export function ChatPage() {
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Discussions</h1>
</div>
<div className="flex flex-1 overflow-hidden">
{/* Channel list sidebar */}
<div
className={`border-r border-gray-200 dark:border-gray-600/50 bg-white dark:bg-gray-700/40 transition-all duration-300 ease-in-out overflow-hidden ${
isChannelListExpanded ? "w-80" : "w-0"
}`}
>
<ChannelList
filters={filters}
setActiveChannelOnMount={isChannelInUrl ? false : true}
Preview={({
displayTitle,
channel,
activeChannel,
setActiveChannel,
unread,
latestMessagePreview,
}) => (
<ChannelPreview
displayTitle={displayTitle}
channel={channel}
tablo={tablos?.find((t) => t.id === channel.id) ?? null}
activeChannel={activeChannel}
setActiveChannel={setActiveChannel}
unreadCount={unread}
latestMessagePreview={latestMessagePreview}
<div className="overflow-y-auto h-full">
{tablos?.map((tablo) => (
<ChatChannelPreview
key={tablo.id}
tablo={tablo}
isActive={channelId === tablo.id}
onClick={() => handleChannelSelect(tablo.id)}
unreadCount={getUnreadCount(tablo.id)}
isOnline={onlineUsers.some((uid) => uid !== user.id)}
/>
)}
/>
))}
</div>
</div>
<div className="flex-1 bg-white dark:bg-gray-700/40">
<Channel channel={channel}>
<Window>
<CustomChannelHeader
tablos={tablos ?? []}
onToggleChannelList={toggleChannelList}
{/* Chat area */}
<div className="flex-1 flex flex-col bg-white dark:bg-gray-700/40">
{channelId && activeTablo ? (
<>
<ChatHeader
tablo={activeTablo}
onToggleChannelList={() => setIsChannelListExpanded(!isChannelListExpanded)}
isChannelListExpanded={isChannelListExpanded}
onlineUsers={onlineUsers}
/>
<MessageList />
<MessageInput />
</Window>
</Channel>
<div className="flex-1 overflow-hidden">
<ChatContainer>
<MessageList
typingIndicator={
typingUsers.length > 0 ? (
<TypingIndicator content="typing..." />
) : undefined
}
>
{messages.map((msg) => (
<Message
key={msg.id}
model={{
message: msg.text,
sentTime: msg.createdAt,
sender: msg.userId,
direction: msg.userId === user.id ? "outgoing" : "incoming",
position: "single",
}}
/>
))}
</MessageList>
<MessageInput
placeholder="Type a message..."
onSend={handleSend}
onChange={() => sendTyping()}
attachButton={false}
/>
</ChatContainer>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-gray-500 dark:text-gray-400">
Select a conversation to start chatting
</div>
)}
</div>
</div>
</div>

View file

@ -124,6 +124,25 @@ importers:
specifier: ^4.0.8
version: 4.0.8(@types/debug@4.1.12)(@types/node@20.19.23)(@vitest/ui@4.0.8)(happy-dom@20.0.7)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6)
apps/chat-worker:
dependencies:
hono:
specifier: ^4.7.7
version: 4.10.4
jose:
specifier: ^6.0.0
version: 6.2.2
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20250410.0
version: 4.20260411.1
typescript:
specifier: ^5.8.3
version: 5.9.3
wrangler:
specifier: ^4.14.0
version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
apps/external:
dependencies:
'@tanstack/react-query':
@ -174,7 +193,7 @@ importers:
version: 2.2.5
'@cloudflare/vite-plugin':
specifier: ^1.9.4
version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0)
version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1))
'@tailwindcss/vite':
specifier: ^4.0.14
version: 4.1.15(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))
@ -204,7 +223,7 @@ importers:
version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))
wrangler:
specifier: ^4.24.3
version: 4.44.0
version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
apps/main:
dependencies:
@ -217,6 +236,12 @@ importers:
'@blocknote/react':
specifier: ^0.41.1
version: 0.41.1(@floating-ui/dom@1.7.4)(@tiptap/extensions@3.8.0(@tiptap/core@3.8.0(@tiptap/pm@3.8.0))(@tiptap/pm@3.8.0))(@types/hast@3.0.4)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@chatscope/chat-ui-kit-react':
specifier: ^2.1.1
version: 2.1.1(prop-types@15.8.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@chatscope/chat-ui-kit-styles':
specifier: ^1.4.0
version: 1.4.0
'@datadog/browser-rum':
specifier: ^6.13.0
version: 6.22.0
@ -373,7 +398,7 @@ importers:
version: 2.2.5
'@cloudflare/vite-plugin':
specifier: ^1.9.4
version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0)
version: 1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1))
'@esbuild-plugins/node-globals-polyfill':
specifier: ^0.2.3
version: 0.2.3(esbuild@0.25.11)
@ -511,7 +536,7 @@ importers:
version: 7.4.0
wrangler:
specifier: ^4.24.3
version: 4.44.0
version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
packages/shared:
dependencies:
@ -1623,6 +1648,16 @@ packages:
'@braintree/sanitize-url@6.0.4':
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
'@chatscope/chat-ui-kit-react@2.1.1':
resolution: {integrity: sha512-rCtE9abdmAbBDkAAUYBC1TDTBMZHquqFIZhADptAfHcJ8z8W3XH/z/ZuwBSJXtzi6h1mwCNc3tBmm1A2NLGhNg==}
peerDependencies:
prop-types: ^15.7.2
react: ^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0
react-dom: ^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0
'@chatscope/chat-ui-kit-styles@1.4.0':
resolution: {integrity: sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q==}
'@cloudflare/kv-asset-handler@0.4.0':
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
engines: {node: '>=18.0.0'}
@ -1672,6 +1707,9 @@ packages:
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260411.1':
resolution: {integrity: sha512-SsntcTanLz+LmgJC8yB7sGCtpC8HxboVDmwrOH1hp1SHZwuKnhfmhUfeiwy7O/cE3iVN1cxe1E17stxP5DJXDw==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@ -2180,6 +2218,29 @@ packages:
'@formatjs/intl-localematcher@0.6.2':
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
'@fortawesome/fontawesome-common-types@6.7.2':
resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-free@6.7.2':
resolution: {integrity: sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==}
engines: {node: '>=6'}
'@fortawesome/fontawesome-svg-core@6.7.2':
resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==}
engines: {node: '>=6'}
'@fortawesome/free-solid-svg-icons@6.7.2':
resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==}
engines: {node: '>=6'}
'@fortawesome/react-fontawesome@0.2.6':
resolution: {integrity: sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==}
deprecated: v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater.
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7
react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@google-cloud/common@5.0.2':
resolution: {integrity: sha512-V7bmBKYQyu0eVG2BFejuUjlBt+zrya6vtsKdY+JxMM/dNntPF41vZ9+LhOshEUH01zOHEqBSvI7Dad7ZS6aUeA==}
engines: {node: '>=14.0.0'}
@ -5480,6 +5541,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@ -6936,6 +7000,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -11231,6 +11298,20 @@ snapshots:
'@braintree/sanitize-url@6.0.4': {}
'@chatscope/chat-ui-kit-react@2.1.1(prop-types@15.8.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@chatscope/chat-ui-kit-styles': 1.4.0
'@fortawesome/fontawesome-free': 6.7.2
'@fortawesome/fontawesome-svg-core': 6.7.2
'@fortawesome/free-solid-svg-icons': 6.7.2
'@fortawesome/react-fontawesome': 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.0.0)
classnames: 2.5.1
prop-types: 15.8.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
'@chatscope/chat-ui-kit-styles@1.4.0': {}
'@cloudflare/kv-asset-handler@0.4.0':
dependencies:
mime: 3.0.0
@ -11241,7 +11322,7 @@ snapshots:
optionalDependencies:
workerd: 1.20251011.0
'@cloudflare/vite-plugin@1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0)':
'@cloudflare/vite-plugin@1.13.14(vite@6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6))(workerd@1.20251011.0)(wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1))':
dependencies:
'@cloudflare/unenv-preset': 2.7.8(unenv@2.0.0-rc.21)(workerd@1.20251011.0)
'@remix-run/node-fetch-server': 0.8.1
@ -11251,7 +11332,7 @@ snapshots:
tinyglobby: 0.2.15
unenv: 2.0.0-rc.21
vite: 6.4.1(@types/node@22.18.12)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.1)(tsx@4.20.6)
wrangler: 4.44.0
wrangler: 4.44.0(@cloudflare/workers-types@4.20260411.1)
ws: 8.18.0
transitivePeerDependencies:
- bufferutil
@ -11273,6 +11354,8 @@ snapshots:
'@cloudflare/workerd-windows-64@1.20251011.0':
optional: true
'@cloudflare/workers-types@4.20260411.1': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@ -11651,6 +11734,24 @@ snapshots:
dependencies:
tslib: 2.8.1
'@fortawesome/fontawesome-common-types@6.7.2': {}
'@fortawesome/fontawesome-free@6.7.2': {}
'@fortawesome/fontawesome-svg-core@6.7.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.2
'@fortawesome/free-solid-svg-icons@6.7.2':
dependencies:
'@fortawesome/fontawesome-common-types': 6.7.2
'@fortawesome/react-fontawesome@0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.0.0)':
dependencies:
'@fortawesome/fontawesome-svg-core': 6.7.2
prop-types: 15.8.1
react: 19.0.0
'@google-cloud/common@5.0.2':
dependencies:
'@google-cloud/projectify': 4.0.0
@ -15697,6 +15798,8 @@ snapshots:
dependencies:
clsx: 2.1.1
classnames@2.5.1: {}
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
@ -17687,6 +17790,8 @@ snapshots:
jiti@2.6.1: {}
jose@6.2.2: {}
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
@ -20926,7 +21031,7 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20251011.0
'@cloudflare/workerd-windows-64': 1.20251011.0
wrangler@4.44.0:
wrangler@4.44.0(@cloudflare/workers-types@4.20260411.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@cloudflare/unenv-preset': 2.7.8(unenv@2.0.0-rc.21)(workerd@1.20251011.0)
@ -20937,6 +21042,7 @@ snapshots:
unenv: 2.0.0-rc.21
workerd: 1.20251011.0
optionalDependencies:
'@cloudflare/workers-types': 4.20260411.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil