feat(chat): collapse browser hint after delay
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
width: fit-content;
|
||||||
max-width: min(74vw, 260px);
|
max-width: min(74vw, 260px);
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
@@ -25,10 +26,26 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
transition: border-color 0.16s ease, box-shadow 0.16s ease,
|
transition: border-color 0.16s ease, box-shadow 0.16s ease,
|
||||||
opacity 0.16s ease, transform 0.16s ease;
|
opacity 0.16s ease, padding 0.18s ease, transform 0.16s ease;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expanded {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
min-width: 42px;
|
||||||
|
min-height: 42px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
.iconWrap {
|
.iconWrap {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
@@ -39,14 +56,34 @@
|
|||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
transition: background 0.18s ease, transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed .iconWrap {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy {
|
.copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
max-width: 190px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
opacity: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: max-width 0.2s ease, opacity 0.14s ease,
|
||||||
|
transform 0.18s ease;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed .copy {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
* 行为:
|
* 行为:
|
||||||
* - 开发环境始终显示(测试方便)
|
* - 开发环境始终显示(测试方便)
|
||||||
* - 生产环境仅在应用内浏览器中显示
|
* - 生产环境仅在应用内浏览器中显示
|
||||||
* - 点击后主动打开外部浏览器
|
* - 首次完整展示,随后收缩为图标;展开后点击主动打开外部浏览器
|
||||||
*/
|
*/
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
|
||||||
import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser";
|
import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser";
|
||||||
@@ -25,40 +25,72 @@ export interface BrowserHintOverlayProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
/** 强制显示(开发模式测试用) */
|
/** 强制显示(开发模式测试用) */
|
||||||
forceShow?: boolean;
|
forceShow?: boolean;
|
||||||
|
/** 自动收缩延迟,设为 0 可禁用自动收缩 */
|
||||||
|
autoCollapseDelayMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TITLE = "Open in external browser";
|
const DEFAULT_TITLE = "Open in external browser";
|
||||||
const DEFAULT_DESCRIPTION = "Find me more easily";
|
const DEFAULT_DESCRIPTION = "Find me more easily";
|
||||||
|
const DEFAULT_AUTO_COLLAPSE_DELAY_MS = 3500;
|
||||||
|
|
||||||
export function BrowserHintOverlay({
|
export function BrowserHintOverlay({
|
||||||
title = DEFAULT_TITLE,
|
title = DEFAULT_TITLE,
|
||||||
description = DEFAULT_DESCRIPTION,
|
description = DEFAULT_DESCRIPTION,
|
||||||
forceShow = false,
|
forceShow = false,
|
||||||
|
autoCollapseDelayMs = DEFAULT_AUTO_COLLAPSE_DELAY_MS,
|
||||||
}: BrowserHintOverlayProps) {
|
}: BrowserHintOverlayProps) {
|
||||||
// lazy initializer:首次渲染即计算(避免 useEffect 中调 setState 的 lint 错)
|
// lazy initializer:首次渲染即计算(避免 useEffect 中调 setState 的 lint 错)
|
||||||
const [isInAppBrowser] = useState(BrowserDetector.isInAppBrowser);
|
const [isInAppBrowser] = useState(BrowserDetector.isInAppBrowser);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
|
||||||
|
|
||||||
// 开发模式或应用内浏览器才显示
|
// 开发模式或应用内浏览器才显示
|
||||||
const shouldShow = forceShow || isInAppBrowser;
|
const shouldShow = forceShow || isInAppBrowser;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldShow || hasAutoCollapsed || autoCollapseDelayMs <= 0) return;
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
setIsExpanded(false);
|
||||||
|
setHasAutoCollapsed(true);
|
||||||
|
}, autoCollapseDelayMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [autoCollapseDelayMs, hasAutoCollapsed, shouldShow]);
|
||||||
|
|
||||||
if (!shouldShow) return null;
|
if (!shouldShow) return null;
|
||||||
|
|
||||||
const handleClick = (): void => {
|
const handleClick = (): void => {
|
||||||
|
if (!isExpanded) {
|
||||||
|
setIsExpanded(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void openChatInExternalBrowser().catch((error) => {
|
void openChatInExternalBrowser().catch((error) => {
|
||||||
console.warn("[chat] Failed to open external browser", error);
|
console.warn("[chat] Failed to open external browser", error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ariaLabel = isExpanded
|
||||||
|
? `${title}. ${description}`
|
||||||
|
: "Show external browser hint";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.overlay}
|
className={`${styles.overlay} ${
|
||||||
aria-label={`${title}. ${description}`}
|
isExpanded ? styles.expanded : styles.collapsed
|
||||||
|
}`}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<span className={styles.iconWrap} aria-hidden="true">
|
<span className={styles.iconWrap} aria-hidden="true">
|
||||||
<ExternalLink size={15} strokeWidth={2.4} />
|
<ExternalLink size={15} strokeWidth={2.4} />
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.copy}>
|
<span className={styles.copy} aria-hidden={!isExpanded}>
|
||||||
<span className={styles.title}>{title}</span>
|
<span className={styles.title}>{title}</span>
|
||||||
<span className={styles.description}>{description}</span>
|
<span className={styles.description}>{description}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user