fix(chat): keep browser hint expanded and readable
This commit is contained in:
@@ -201,21 +201,21 @@ describe("chat Tailwind components", () => {
|
|||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<BrowserHintOverlay
|
<BrowserHintOverlay
|
||||||
forceShow
|
forceShow
|
||||||
autoCollapseDelayMs={0}
|
|
||||||
title="Open elsewhere"
|
title="Open elsewhere"
|
||||||
description="Better experience"
|
description="Better experience"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain('aria-expanded="true"');
|
expect(html).not.toContain("aria-expanded");
|
||||||
expect(html).toContain('aria-label="Open elsewhere. Better experience"');
|
expect(html).toContain('aria-label="Open elsewhere. Better experience"');
|
||||||
expect(html).toContain("grid-cols-[auto_minmax(0,1fr)]");
|
expect(html).toContain("grid-cols-[auto_minmax(0,1fr)]");
|
||||||
expect(html).toContain(
|
expect(html).toContain(
|
||||||
"right-[calc(var(--spacing-md,12px)+var(--responsive-icon-button-size,42px)+var(--spacing-sm,8px))]",
|
"right-[calc(var(--spacing-md,12px)+var(--responsive-icon-button-size,42px)+var(--spacing-sm,8px))]",
|
||||||
);
|
);
|
||||||
expect(html).toContain("max-w-[min(58vw,220px)]");
|
expect(html).toContain("max-w-[min(58vw,220px)]");
|
||||||
expect(html).toContain("backdrop-blur-md");
|
expect(html).toContain("bg-[#17131f]");
|
||||||
expect(html).toContain("hover:opacity-94");
|
expect(html).toContain("text-[#f3dfe8]");
|
||||||
|
expect(html).not.toContain("Show external browser hint");
|
||||||
expect(html).toContain("Open elsewhere");
|
expect(html).toContain("Open elsewhere");
|
||||||
expect(html).toContain("Better experience");
|
expect(html).toContain("Better experience");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,9 +8,8 @@
|
|||||||
* 行为:
|
* 行为:
|
||||||
* - 开发环境始终显示(测试方便)
|
* - 开发环境始终显示(测试方便)
|
||||||
* - 生产环境仅在应用内浏览器中显示
|
* - 生产环境仅在应用内浏览器中显示
|
||||||
* - 首次完整展示,随后收缩为图标;展开后点击主动打开外部浏览器
|
* - 始终完整展示,点击后主动打开外部浏览器
|
||||||
*/
|
*/
|
||||||
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";
|
||||||
@@ -22,93 +21,49 @@ 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;
|
const OVERLAY_CLASS_NAME =
|
||||||
const OVERLAY_BASE_CLASS_NAME =
|
"pointer-events-auto absolute right-[calc(var(--spacing-md,12px)+var(--responsive-icon-button-size,42px)+var(--spacing-sm,8px))] top-(--spacing-2,8px) z-20 grid min-h-12 w-fit max-w-[min(58vw,220px)] cursor-pointer grid-cols-[auto_minmax(0,1fr)] items-center gap-2 rounded-[18px] border border-[rgba(255,255,255,0.3)] bg-[#17131f] px-2.5 py-2 text-white shadow-[0_12px_28px_rgba(0,0,0,0.34),inset_0_1px_0_rgba(255,255,255,0.12)] transition-[border-color,box-shadow,opacity,transform] duration-180 [-webkit-tap-highlight-color:transparent] hover:-translate-y-px hover:border-[rgba(255,255,255,0.46)] hover:bg-[#211925] hover:shadow-[0_14px_32px_rgba(0,0,0,0.38),inset_0_1px_0_rgba(255,255,255,0.16)] active:translate-y-px active:scale-99 active:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-white";
|
||||||
"pointer-events-auto absolute right-[calc(var(--spacing-md,12px)+var(--responsive-icon-button-size,42px)+var(--spacing-sm,8px))] top-(--spacing-2,8px) z-20 box-border min-h-12 max-w-[min(58vw,220px)] cursor-pointer border border-[rgba(255,255,255,0.18)] bg-[linear-gradient(135deg,rgba(255,255,255,0.14),rgba(255,255,255,0.04)),var(--color-blur-background,rgba(13,11,20,0.86))] text-white shadow-[0_12px_28px_rgba(0,0,0,0.24),inset_0_1px_0_rgba(255,255,255,0.16)] backdrop-blur-md backdrop-saturate-120 transition-[border-color,box-shadow,opacity,padding,transform] duration-180 [-webkit-tap-highlight-color:transparent] hover:-translate-y-px hover:border-[rgba(255,255,255,0.28)] hover:opacity-94 hover:shadow-[0_14px_32px_rgba(0,0,0,0.28),inset_0_1px_0_rgba(255,255,255,0.18)] active:translate-y-px active:scale-99 active:opacity-88 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[rgba(255,255,255,0.88)]";
|
|
||||||
const OVERLAY_EXPANDED_CLASS_NAME =
|
|
||||||
"grid w-fit grid-cols-[auto_minmax(0,1fr)] items-center gap-2 rounded-[18px] px-2.5 py-2";
|
|
||||||
const OVERLAY_COLLAPSED_CLASS_NAME =
|
|
||||||
"inline-flex size-10.5 min-h-10.5 min-w-10.5 items-center justify-center rounded-full p-1.25";
|
|
||||||
|
|
||||||
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) {
|
||||||
const [isExpanded, setIsExpanded] = useState(true);
|
|
||||||
const [hasAutoCollapsed, setHasAutoCollapsed] = useState(false);
|
|
||||||
|
|
||||||
// 展示条件由调用方控制;forceShow 保留给开发/测试入口。
|
// 展示条件由调用方控制;forceShow 保留给开发/测试入口。
|
||||||
const shouldShow = forceShow;
|
const shouldShow = forceShow;
|
||||||
|
|
||||||
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"
|
||||||
data-analytics-key="chat.external_browser_hint"
|
data-analytics-key="chat.external_browser_hint"
|
||||||
data-analytics-label="External browser hint"
|
data-analytics-label="External browser hint"
|
||||||
className={[
|
className={OVERLAY_CLASS_NAME}
|
||||||
OVERLAY_BASE_CLASS_NAME,
|
aria-label={`${title}. ${description}`}
|
||||||
isExpanded ? OVERLAY_EXPANDED_CLASS_NAME : OVERLAY_COLLAPSED_CLASS_NAME,
|
|
||||||
].join(" ")}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="inline-flex size-30 items-center justify-center rounded-full bg-[rgba(255,255,255,0.15)] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.16)] transition-[background,transform] duration-180"
|
className="inline-flex size-30 items-center justify-center rounded-full bg-(--color-accent,#f84d96) text-white shadow-[0_4px_12px_rgba(248,77,150,0.34)]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<ExternalLink size={15} strokeWidth={2.4} />
|
<ExternalLink size={15} strokeWidth={2.4} />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span className="flex min-w-0 max-w-47.5 flex-col gap-0.5 overflow-hidden whitespace-normal text-left">
|
||||||
className={
|
|
||||||
isExpanded
|
|
||||||
? "flex min-w-0 max-w-47.5 translate-x-0 flex-col gap-0.5 overflow-hidden whitespace-normal text-left opacity-100 transition-[max-width,opacity,transform] duration-200"
|
|
||||||
: "hidden translate-x-1.5 pointer-events-none opacity-0 transition-[max-width,opacity,transform] duration-200"
|
|
||||||
}
|
|
||||||
aria-hidden={!isExpanded}
|
|
||||||
>
|
|
||||||
<span className="wrap-break-word text-(length:--font-size-sm,12px) font-extrabold leading-tight text-white">
|
<span className="wrap-break-word text-(length:--font-size-sm,12px) font-extrabold leading-tight text-white">
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
<span className="wrap-break-word text-[11px] font-semibold leading-[1.2] text-[rgba(255,255,255,0.72)]">
|
<span className="wrap-break-word text-[11px] font-semibold leading-[1.25] text-[#f3dfe8]">
|
||||||
{description}
|
{description}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user