From 723a1ea0860ef50f0bbe9b581a5c830660a17c3e Mon Sep 17 00:00:00 2001 From: david_bai Date: Tue, 25 Nov 2025 12:24:28 +0800 Subject: [PATCH] feat(ux): cached roomId auto-join + theme toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Receiver: auto-fill and join on Retrieve tab when input empty, not in room, no URL roomId, and cachedId exists (ClipboardApp + roomIdCache) - Sender: “Use cached ID” now immediately joins the room (add onUseCached + disabled to CachedIdActionButton; wire in SendTabPanel) - UI: add ThemeToggle and integrate into Header (desktop and mobile) - Styles: replace hardcoded white with design tokens in Retrieve panel (bg-card/text-card-foreground) for dark mode - Docs: update AI playbook flows and code-map --- docs/ai-playbook/code-map.zh-CN.md | 3 ++ docs/ai-playbook/flows.zh-CN.md | 3 ++ frontend/components/ClipboardApp.tsx | 30 +++++++++++++++++++ .../ClipboardApp/CachedIdActionButton.tsx | 13 +++++++- .../ClipboardApp/RetrieveTabPanel.tsx | 2 +- .../components/ClipboardApp/SendTabPanel.tsx | 5 ++++ frontend/components/web/Header.tsx | 3 ++ frontend/components/web/ThemeToggle.tsx | 30 +++++++++++++++++++ 8 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 frontend/components/web/ThemeToggle.tsx diff --git a/docs/ai-playbook/code-map.zh-CN.md b/docs/ai-playbook/code-map.zh-CN.md index b41b1e9..ca338ae 100644 --- a/docs/ai-playbook/code-map.zh-CN.md +++ b/docs/ai-playbook/code-map.zh-CN.md @@ -23,7 +23,9 @@ - `frontend/components/` — UI,包括协调器与子组件。 - `frontend/components/ClipboardApp.tsx` — 顶层 UI 协调器,集成 5 个业务 hooks(useWebRTCConnection/useFileTransferHandler/useRoomManager/usePageSetup/useClipboardAppMessages),处理全局拖拽事件和双标签页(发送/接收)管理。 + - 体验增强:切到接收端(retrieve)且满足“未在房间、URL 无 roomId、输入为空、存在缓存ID”时自动填充并加入房间(读取 `frontend/lib/roomIdCache.ts`)。 - `frontend/components/ClipboardApp/SendTabPanel.tsx` — 发送面板,集成富文本编辑器、文件上传、房间 ID 生成(支持 4 位数字/UUID 两种模式)、分享链接生成。 + - 体验增强:点击“使用缓存ID”将立即触发加入房间(sender 侧),减少一次手动点击。 - `frontend/components/ClipboardApp/RetrieveTabPanel.tsx` — 接收面板,处理房间加入、文件接收、目录选择(File System Access API)、富文本内容显示。 - `frontend/components/ClipboardApp/FileListDisplay.tsx` — 文件列表显示组件,支持文件/文件夹分组显示、进度跟踪、多浏览器下载策略(Chrome 自动下载/其他浏览器手动保存)、下载计数统计。 - `frontend/components/ClipboardApp/FullScreenDropZone.tsx` — 全屏拖拽提示组件,文件拖拽时的视觉反馈。 @@ -32,6 +34,7 @@ - `frontend/components/blog/` — 博客相关组件,包含 TableOfContents(支持中文目录生成和滚动跟踪)、Mermaid 图表渲染、MDXComponents、ArticleListItem 文章列表。 - `frontend/components/common/` — 通用组件,包含 clipboard_btn(读写剪贴板按钮)、AutoPopupDialog(自动弹出对话框)、LazyLoadWrapper(懒加载包装器)、YouTubePlayer(YouTube 播放器)。 - `frontend/components/web/` — 网站页面组件,包含 Header(响应式导航和多语言支持)、Footer(版权和语言链接)、FAQSection(可配置 FAQ 展示)、HowItWorks(步骤说明和视频演示)、SystemDiagram(系统架构图)、KeyFeatures(功能特性展示)、theme-provider 主题提供者。 + - `frontend/components/web/ThemeToggle.tsx` — 主题切换按钮(单按钮 Light/Dark 切换),集成于 Header(桌面与移动)。 - `frontend/components/seo/JsonLd.tsx` — SEO 结构化数据组件,支持多类型 JSON-LD 数据生成。 - `frontend/components/LanguageSwitcher.tsx` — 语言切换器。 - `frontend/components/ui/*` — 基础 UI 原子组件(基于 Radix UI 和 shadcn/ui),包含 Button(多变体按钮)、Accordion(手风琴)、Dialog(模态对话框)、Card(卡片)、Tooltip(工具提示)、Select、Input、Textarea、Checkbox、DropdownMenu、Toast 通知系统和 AnimatedButton 动画按钮。 diff --git a/docs/ai-playbook/flows.zh-CN.md b/docs/ai-playbook/flows.zh-CN.md index 7098432..5412f1e 100644 --- a/docs/ai-playbook/flows.zh-CN.md +++ b/docs/ai-playbook/flows.zh-CN.md @@ -248,6 +248,9 @@ Core Services (webrtcService) + Store (fileTransferStore) 7. **剪贴板兼容性**:useClipboardActions 支持现代 navigator.clipboard API 和 document.execCommand 降级方案 8. **富文本安全处理**:useRichTextToPlainText 服务端渲染安全,客户端 DOM 转换处理块级元素 9. **站内导航不中断(同一标签页)**:依赖 `frontend/stores/fileTransferStore.ts`(Zustand 单例)与 `frontend/lib/webrtcService.ts`(服务单例)。App Router 页面切换不打断传输且保留已选择/已接收内容。注意不要在路由切换副作用中调用 `webrtcService.leaveRoom()` 或重置 Store;刷新/新标签不在保证范围内。 +10. **切到接收端自动加入(缓存ID)**:当用户切换到接收端、未在房间、URL 无 `roomId`、输入框为空且本地存在缓存 ID 时,自动填充并直接调用加入房间以提升体验。入口:`frontend/components/ClipboardApp.tsx`(监听 `activeTab` 变化,读取 `frontend/lib/roomIdCache.ts`)。 +11. **发送端“使用缓存ID”即刻加入**:发送端在 `SendTabPanel` 点击“使用缓存ID”后会立即调用加入房间(而非仅填充输入框)。入口:`frontend/components/ClipboardApp/CachedIdActionButton.tsx`(`onUseCached` 回调)+ `frontend/components/ClipboardApp/SendTabPanel.tsx`。 +12. **深色主题切换**:提供单按钮 Light/Dark 切换,入口:`frontend/components/web/ThemeToggle.tsx`;集成位置:`frontend/components/web/Header.tsx`(桌面与移动);局部样式从硬编码颜色迁移为设计令牌(例如接收面板使用 `bg-card text-card-foreground`)。 ### 前端组件架构特化 diff --git a/frontend/components/ClipboardApp.tsx b/frontend/components/ClipboardApp.tsx index a6ae5bf..1966cb4 100644 --- a/frontend/components/ClipboardApp.tsx +++ b/frontend/components/ClipboardApp.tsx @@ -14,6 +14,7 @@ import { RetrieveTabPanel } from "./ClipboardApp/RetrieveTabPanel"; import FullScreenDropZone from "./ClipboardApp/FullScreenDropZone"; import { traverseFileTree } from "@/lib/fileUtils"; import { useFileTransferStore } from "@/stores/fileTransferStore"; +import { getCachedId } from "@/lib/roomIdCache"; const ClipboardApp = () => { const { shareMessage, retrieveMessage, putMessageInMs } = @@ -37,6 +38,9 @@ const ClipboardApp = () => { setIsDragging, setRetrieveRoomIdInput, setActiveTab, + // for auto-join on receiver side + isReceiverInRoom, + retrieveRoomIdInput, } = useFileTransferStore(); const richTextToPlainText = useRichTextToPlainText(); @@ -144,6 +148,32 @@ const ClipboardApp = () => { }; }, [activeTab, handleFileDrop, setIsDragging]); + // Auto-join on switching to receiver tab when cached ID exists + useEffect(() => { + if (activeTab !== "retrieve") return; + if (isReceiverInRoom) return; + + // Do not auto-join if URL already specifies a roomId (URL 优先) + const params = new URLSearchParams(window.location.search); + if (params.get("roomId")) return; + + // Do not override user's existing input + if ((retrieveRoomIdInput || "").trim().length > 0) return; + + const cached = getCachedId(); + if (!cached || cached.trim().length === 0) return; + + // Fill input then join directly to improve UX + setRetrieveRoomIdInput(cached); + joinRoom(false, cached); + }, [ + activeTab, + isReceiverInRoom, + retrieveRoomIdInput, + setRetrieveRoomIdInput, + joinRoom, + ]); + if (isLoadingMessages || !messages) { return (
diff --git a/frontend/components/ClipboardApp/CachedIdActionButton.tsx b/frontend/components/ClipboardApp/CachedIdActionButton.tsx index 5a3af57..5b424cd 100644 --- a/frontend/components/ClipboardApp/CachedIdActionButton.tsx +++ b/frontend/components/ClipboardApp/CachedIdActionButton.tsx @@ -62,6 +62,10 @@ type Props = { size?: "default" | "sm" | "lg" | "icon"; dblClickWindowMs?: number; // default 400ms saveModeDurationMs?: number; // default 3000ms + // Optional: called after a cached ID is applied (single-click "Use cached ID") + onUseCached?: (cachedId: string) => void; + // Optional: external disabled flag + disabled?: boolean; }; export default function CachedIdActionButton({ @@ -75,6 +79,8 @@ export default function CachedIdActionButton({ size = "default", dblClickWindowMs = 400, saveModeDurationMs = 3000, + onUseCached, + disabled = false, }: Props) { const [hasCachedId, setHasCachedId] = useState(false); const [showSaveOverride, setShowSaveOverride] = useState(false); @@ -128,6 +134,8 @@ export default function CachedIdActionButton({ const cached = getCachedId(); if (cached) { setInputValue(cached); + // Notify caller after applying cached value + onUseCached?.(cached); } } clickCountRef.current = 0; @@ -161,6 +169,7 @@ export default function CachedIdActionButton({ isShareEnd, dblClickWindowMs, saveModeDurationMs, + onUseCached, ]); return ( @@ -177,7 +186,9 @@ export default function CachedIdActionButton({ variant={variant} size={size} onClick={handleClick} - disabled={isSaveMode ? !isSaveEnabled : !hasCachedId} + disabled={ + disabled || (isSaveMode ? !isSaveEnabled : !hasCachedId) + } > {isSaveMode ? messages.text.ClipboardApp.html.saveId_dis diff --git a/frontend/components/ClipboardApp/RetrieveTabPanel.tsx b/frontend/components/ClipboardApp/RetrieveTabPanel.tsx index df9ba38..24a4e77 100644 --- a/frontend/components/ClipboardApp/RetrieveTabPanel.tsx +++ b/frontend/components/ClipboardApp/RetrieveTabPanel.tsx @@ -156,7 +156,7 @@ export function RetrieveTabPanel({
{retrievedContent && (
-
+
diff --git a/frontend/components/ClipboardApp/SendTabPanel.tsx b/frontend/components/ClipboardApp/SendTabPanel.tsx index 8ef9241..f52d63e 100644 --- a/frontend/components/ClipboardApp/SendTabPanel.tsx +++ b/frontend/components/ClipboardApp/SendTabPanel.tsx @@ -197,6 +197,11 @@ export function SendTabPanel({ setInputValue={setInputFieldValue} putMessageInMs={putMessageInMs} isShareEnd={true} + disabled={isSenderInRoom} + onUseCached={(id) => { + // Immediately join as sender after applying cached ID + joinRoom(true, id.trim()); + }} /> +
@@ -98,6 +100,7 @@ const Header = ({ messages, lang }: HeaderProps) => { {/* Mobile menu controls */}
+ + ); +} +