fix(chat): place browser hint between header actions
This commit is contained in:
@@ -11,7 +11,6 @@ import { ROUTES } from "@/router/routes";
|
|||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BrowserHintOverlay,
|
|
||||||
ChatArea,
|
ChatArea,
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
ChatInputBar,
|
ChatInputBar,
|
||||||
@@ -216,6 +215,7 @@ export function ChatScreen() {
|
|||||||
{/* isGuest 派生自 auth.loginStatus */}
|
{/* isGuest 派生自 auth.loginStatus */}
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
isGuest={isGuest}
|
isGuest={isGuest}
|
||||||
|
showBrowserHint={shouldShowBrowserHint}
|
||||||
offerBanner={
|
offerBanner={
|
||||||
<FirstRechargeOfferBanner
|
<FirstRechargeOfferBanner
|
||||||
visible={firstRechargeOfferBanner.visible}
|
visible={firstRechargeOfferBanner.visible}
|
||||||
@@ -252,9 +252,6 @@ export function ChatScreen() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 浏览器提示(应用内浏览器检测) */}
|
|
||||||
{shouldShowBrowserHint ? <BrowserHintOverlay forceShow /> : null}
|
|
||||||
|
|
||||||
{/* PWA 安装提示触发器(聊天消息数量达到阈值后才允许弹出) */}
|
{/* PWA 安装提示触发器(聊天消息数量达到阈值后才允许弹出) */}
|
||||||
<PwaInstallOverlay enabled={shouldShowPwaInstall} />
|
<PwaInstallOverlay enabled={shouldShowPwaInstall} />
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ describe("chat Tailwind components", () => {
|
|||||||
const memberHtml = renderToStaticMarkup(
|
const memberHtml = renderToStaticMarkup(
|
||||||
<ChatHeader isGuest={false} offerBanner={<div>Offer</div>} />,
|
<ChatHeader isGuest={false} offerBanner={<div>Offer</div>} />,
|
||||||
);
|
);
|
||||||
|
const memberWithHintHtml = renderToStaticMarkup(
|
||||||
|
<ChatHeader isGuest={false} showBrowserHint />,
|
||||||
|
);
|
||||||
|
const guestWithHintHtml = renderToStaticMarkup(
|
||||||
|
<ChatHeader isGuest={true} showBrowserHint />,
|
||||||
|
);
|
||||||
|
|
||||||
expect(guestHtml).toContain('aria-label="Sign up to unlock more features"');
|
expect(guestHtml).toContain('aria-label="Sign up to unlock more features"');
|
||||||
expect(guestHtml).toContain("bg-accent");
|
expect(guestHtml).toContain("bg-accent");
|
||||||
@@ -173,6 +179,25 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(memberHtml).toContain('data-analytics-key="chat.back_to_home"');
|
expect(memberHtml).toContain('data-analytics-key="chat.back_to_home"');
|
||||||
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
|
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
|
||||||
expect(memberHtml).toContain("size-(--responsive-icon-button-size,42px)");
|
expect(memberHtml).toContain("size-(--responsive-icon-button-size,42px)");
|
||||||
|
expect(memberWithHintHtml).toContain(
|
||||||
|
'data-analytics-key="chat.external_browser_hint"',
|
||||||
|
);
|
||||||
|
expect(memberWithHintHtml).toContain(
|
||||||
|
"grid-cols-[auto_minmax(0,1fr)_auto]",
|
||||||
|
);
|
||||||
|
expect(memberWithHintHtml.indexOf('aria-label="Back to home"')).toBeLessThan(
|
||||||
|
memberWithHintHtml.indexOf(
|
||||||
|
'data-analytics-key="chat.external_browser_hint"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
memberWithHintHtml.indexOf(
|
||||||
|
'data-analytics-key="chat.external_browser_hint"',
|
||||||
|
),
|
||||||
|
).toBeLessThan(memberWithHintHtml.indexOf('aria-label="Menu"'));
|
||||||
|
expect(guestWithHintHtml).not.toContain(
|
||||||
|
'data-analytics-key="chat.external_browser_hint"',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders ChatInputTextField with textarea utilities", () => {
|
it("renders ChatInputTextField with textarea utilities", () => {
|
||||||
@@ -196,11 +221,8 @@ describe("chat Tailwind components", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders BrowserHintOverlay visibility and expanded styles", () => {
|
it("renders BrowserHintOverlay visibility and expanded styles", () => {
|
||||||
expect(renderToStaticMarkup(<BrowserHintOverlay />)).toBe("");
|
|
||||||
|
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<BrowserHintOverlay
|
<BrowserHintOverlay
|
||||||
forceShow
|
|
||||||
title="Open elsewhere"
|
title="Open elsewhere"
|
||||||
description="Better experience"
|
description="Better experience"
|
||||||
/>,
|
/>,
|
||||||
@@ -209,10 +231,9 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(html).not.toContain("aria-expanded");
|
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).not.toContain("absolute");
|
||||||
"right-[calc(var(--spacing-md,12px)+var(--responsive-icon-button-size,42px)+var(--spacing-sm,8px))]",
|
expect(html).toContain("max-w-full");
|
||||||
);
|
expect(html).toContain("size-7.5");
|
||||||
expect(html).toContain("max-w-[min(58vw,220px)]");
|
|
||||||
expect(html).toContain("bg-[#17131f]");
|
expect(html).toContain("bg-[#17131f]");
|
||||||
expect(html).toContain("text-[#f3dfe8]");
|
expect(html).toContain("text-[#f3dfe8]");
|
||||||
expect(html).not.toContain("Show external browser hint");
|
expect(html).not.toContain("Show external browser hint");
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
* 引导用户使用外部浏览器访问。
|
* 引导用户使用外部浏览器访问。
|
||||||
*
|
*
|
||||||
* 行为:
|
* 行为:
|
||||||
* - 开发环境始终显示(测试方便)
|
* - 是否显示由 ChatHeader 的调用方决定
|
||||||
* - 生产环境仅在应用内浏览器中显示
|
|
||||||
* - 始终完整展示,点击后主动打开外部浏览器
|
* - 始终完整展示,点击后主动打开外部浏览器
|
||||||
*/
|
*/
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
@@ -19,25 +18,17 @@ export interface BrowserHintOverlayProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
/** 自定义按钮说明 */
|
/** 自定义按钮说明 */
|
||||||
description?: string;
|
description?: string;
|
||||||
/** 强制显示(开发模式测试用) */
|
|
||||||
forceShow?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 OVERLAY_CLASS_NAME =
|
const OVERLAY_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";
|
"grid min-h-12 min-w-0 w-fit max-w-full 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";
|
||||||
|
|
||||||
export function BrowserHintOverlay({
|
export function BrowserHintOverlay({
|
||||||
title = DEFAULT_TITLE,
|
title = DEFAULT_TITLE,
|
||||||
description = DEFAULT_DESCRIPTION,
|
description = DEFAULT_DESCRIPTION,
|
||||||
forceShow = false,
|
|
||||||
}: BrowserHintOverlayProps) {
|
}: BrowserHintOverlayProps) {
|
||||||
// 展示条件由调用方控制;forceShow 保留给开发/测试入口。
|
|
||||||
const shouldShow = forceShow;
|
|
||||||
|
|
||||||
if (!shouldShow) return null;
|
|
||||||
|
|
||||||
const handleClick = (): void => {
|
const handleClick = (): void => {
|
||||||
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);
|
||||||
@@ -54,7 +45,7 @@ export function BrowserHintOverlay({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
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)]"
|
className="inline-flex size-7.5 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} />
|
||||||
|
|||||||
@@ -12,12 +12,19 @@ import { BackButton } from "@/app/_components";
|
|||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||||
|
|
||||||
|
import { BrowserHintOverlay } from "./browser-hint-overlay";
|
||||||
|
|
||||||
export interface ChatHeaderProps {
|
export interface ChatHeaderProps {
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
offerBanner?: ReactNode;
|
offerBanner?: ReactNode;
|
||||||
|
showBrowserHint?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
|
export function ChatHeader({
|
||||||
|
isGuest,
|
||||||
|
offerBanner,
|
||||||
|
showBrowserHint = false,
|
||||||
|
}: ChatHeaderProps) {
|
||||||
const navigator = useAppNavigator();
|
const navigator = useAppNavigator();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +50,7 @@ export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isGuest ? (
|
{!isGuest ? (
|
||||||
<div className="flex items-center justify-between px-(--spacing-md,12px) py-(--spacing-sm,8px)">
|
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-(--spacing-sm,8px) px-(--spacing-md,12px) py-(--spacing-sm,8px)">
|
||||||
<BackButton
|
<BackButton
|
||||||
href={ROUTES.splash}
|
href={ROUTES.splash}
|
||||||
variant="dark"
|
variant="dark"
|
||||||
@@ -51,6 +58,10 @@ export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
|
|||||||
analyticsKey="chat.back_to_home"
|
analyticsKey="chat.back_to_home"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 justify-center">
|
||||||
|
{showBrowserHint ? <BrowserHintOverlay /> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-analytics-key="chat.open_menu"
|
data-analytics-key="chat.open_menu"
|
||||||
|
|||||||
Reference in New Issue
Block a user