refactor(chat): migrate overlay styles to tailwind
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { BrowserHintOverlay } from "../browser-hint-overlay";
|
||||
import { ChatHeader } from "../chat-header";
|
||||
import { ChatInsufficientCreditsBanner } from "../chat-insufficient-credits-banner";
|
||||
import { ChatInputTextField } from "../chat-input-text-field";
|
||||
@@ -8,6 +9,7 @@ import { ChatSendButton } from "../chat-send-button";
|
||||
import { ImageBubble } from "../image-bubble";
|
||||
import { MessageAvatar } from "../message-avatar";
|
||||
import { PrivateMessageCard } from "../private-message-card";
|
||||
import { PwaInstallDialog } from "../pwa-install-dialog";
|
||||
import { TextBubble } from "../text-bubble";
|
||||
|
||||
vi.mock("@/router/use-app-navigator", () => ({
|
||||
@@ -181,4 +183,42 @@ describe("chat Tailwind components", () => {
|
||||
expect(html).toContain("Message Elio");
|
||||
expect(html).toContain("Hello");
|
||||
});
|
||||
|
||||
it("renders BrowserHintOverlay visibility and expanded styles", () => {
|
||||
expect(renderToStaticMarkup(<BrowserHintOverlay />)).toBe("");
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<BrowserHintOverlay
|
||||
forceShow
|
||||
autoCollapseDelayMs={0}
|
||||
title="Open elsewhere"
|
||||
description="Better experience"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain('aria-expanded="true"');
|
||||
expect(html).toContain('aria-label="Open elsewhere. Better experience"');
|
||||
expect(html).toContain("grid-cols-[auto_minmax(0,1fr)]");
|
||||
expect(html).toContain("backdrop-blur-[12px]");
|
||||
expect(html).toContain("hover:opacity-[0.94]");
|
||||
expect(html).toContain("Open elsewhere");
|
||||
expect(html).toContain("Better experience");
|
||||
});
|
||||
|
||||
it("renders PwaInstallDialog with Tailwind dialog layout", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<PwaInstallDialog onClose={() => undefined} onInstall={() => undefined} />,
|
||||
);
|
||||
|
||||
expect(html).toContain('role="dialog"');
|
||||
expect(html).toContain('aria-modal="true"');
|
||||
expect(html).toContain('aria-labelledby="pwa-dialog-title"');
|
||||
expect(html).toContain("fixed inset-0");
|
||||
expect(html).toContain("max-w-[var(--pwa-install-dialog-max-width,360px)]");
|
||||
expect(html).toContain("size-[var(--icon-size-86,86px)]");
|
||||
expect(html).toContain("%2Fimages%2Ficons%2Ficon-add-to-home.png");
|
||||
expect(html).toContain("Add to Home Screen");
|
||||
expect(html).toContain("Cancel");
|
||||
expect(html).toContain("OK");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/* BrowserHintOverlay 浏览器提示样式 */
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: var(--spacing-2, 8px);
|
||||
right: var(--spacing-2, 8px);
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 20;
|
||||
width: fit-content;
|
||||
max-width: min(74vw, 260px);
|
||||
min-height: 48px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
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));
|
||||
box-shadow:
|
||||
0 12px 28px rgba(0, 0, 0, 0.24),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(12px);
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease,
|
||||
opacity 0.16s ease, padding 0.18s ease, transform 0.16s ease;
|
||||
-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 {
|
||||
display: inline-flex;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||
color: #fff;
|
||||
transition: background 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.collapsed .iconWrap {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
max-width: 190px;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
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 {
|
||||
color: #fff;
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
line-height: 1.2;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.overlay:hover {
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
box-shadow:
|
||||
0 14px 32px rgba(0, 0, 0, 0.28),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||||
opacity: 0.94;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.overlay:active {
|
||||
opacity: 0.88;
|
||||
transform: translateY(1px) scale(0.99);
|
||||
}
|
||||
|
||||
.overlay:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.88);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
@@ -15,8 +15,6 @@ import { ExternalLink } from "lucide-react";
|
||||
|
||||
import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser";
|
||||
|
||||
import styles from "./browser-hint-overlay.module.css";
|
||||
|
||||
export interface BrowserHintOverlayProps {
|
||||
/** 自定义按钮标题 */
|
||||
title?: string;
|
||||
@@ -31,6 +29,12 @@ export interface BrowserHintOverlayProps {
|
||||
const DEFAULT_TITLE = "Open in external browser";
|
||||
const DEFAULT_DESCRIPTION = "Find me more easily";
|
||||
const DEFAULT_AUTO_COLLAPSE_DELAY_MS = 3500;
|
||||
const OVERLAY_BASE_CLASS_NAME =
|
||||
"pointer-events-auto absolute right-[var(--spacing-2,8px)] top-[var(--spacing-2,8px)] z-20 box-border min-h-12 max-w-[min(74vw,260px)] 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-[12px] backdrop-saturate-[1.2] transition-[border-color,box-shadow,opacity,padding,transform] duration-[180ms] [-webkit-tap-highlight-color:transparent] hover:-translate-y-px hover:border-[rgba(255,255,255,0.28)] hover:opacity-[0.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-[0.99] active:opacity-[0.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-[42px] min-h-[42px] min-w-[42px] items-center justify-center rounded-full p-[5px]";
|
||||
|
||||
export function BrowserHintOverlay({
|
||||
title = DEFAULT_TITLE,
|
||||
@@ -77,19 +81,34 @@ export function BrowserHintOverlay({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.overlay} ${
|
||||
isExpanded ? styles.expanded : styles.collapsed
|
||||
}`}
|
||||
className={[
|
||||
OVERLAY_BASE_CLASS_NAME,
|
||||
isExpanded ? OVERLAY_EXPANDED_CLASS_NAME : OVERLAY_COLLAPSED_CLASS_NAME,
|
||||
].join(" ")}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={isExpanded}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<span className={styles.iconWrap} aria-hidden="true">
|
||||
<span
|
||||
className="inline-flex size-[30px] 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-[180ms]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ExternalLink size={15} strokeWidth={2.4} />
|
||||
</span>
|
||||
<span className={styles.copy} aria-hidden={!isExpanded}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<span className={styles.description}>{description}</span>
|
||||
<span
|
||||
className={
|
||||
isExpanded
|
||||
? "flex min-w-0 max-w-[190px] 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="break-words text-[var(--font-size-sm,12px)] font-extrabold leading-[1.25] text-white">
|
||||
{title}
|
||||
</span>
|
||||
<span className="break-words text-[11px] font-semibold leading-[1.2] text-[rgba(255,255,255,0.72)]">
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/* PwaInstallDialog PWA 安装弹窗样式
|
||||
*
|
||||
* 视觉规格:白底 + 大圆角 + 渐变按钮 + 86×86 图标
|
||||
*/
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 60;
|
||||
padding:
|
||||
calc(var(--dialog-safe-margin, 16px) + var(--app-safe-top, 0px))
|
||||
calc(var(--dialog-safe-margin, 16px) + var(--app-safe-right, 0px))
|
||||
calc(var(--dialog-safe-margin, 16px) + var(--app-safe-bottom, 0px))
|
||||
calc(var(--dialog-safe-margin, 16px) + var(--app-safe-left, 0px));
|
||||
}
|
||||
|
||||
.dialog {
|
||||
width: 100%;
|
||||
max-width: var(--pwa-install-dialog-max-width, 360px);
|
||||
background: var(--color-page-background, #ffffff);
|
||||
border-radius: var(--responsive-card-radius, 40px);
|
||||
padding:
|
||||
var(--responsive-card-padding-lg, 20px)
|
||||
var(--responsive-card-padding, 16px)
|
||||
var(--responsive-card-padding, 16px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: var(--icon-size-86, 86px);
|
||||
height: var(--icon-size-86, 86px);
|
||||
margin-bottom: var(--spacing-lg, 16px);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--responsive-page-title, var(--font-size-22, 22px));
|
||||
font-weight: 700;
|
||||
margin: 0 0 var(--spacing-sm, 8px);
|
||||
color: var(--color-text-foreground, #171717);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: var(--responsive-body, var(--font-size-lg, 16px));
|
||||
line-height: 1.5;
|
||||
color: #393939;
|
||||
margin:
|
||||
0
|
||||
var(--responsive-card-padding-lg, 36px)
|
||||
var(--page-section-gap-lg, 24px);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md, 12px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1 1 auto;
|
||||
min-height: var(--pwa-button-height, 44px);
|
||||
border: 0;
|
||||
border-radius: var(--radius-bottom-sheet, 28px);
|
||||
font-size: var(--responsive-body, var(--font-size-lg, 16px));
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
color: var(--color-page-background, #ffffff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btnSecondary {
|
||||
background: var(--color-text-secondary, #9e9e9e);
|
||||
}
|
||||
|
||||
.btnPrimary {
|
||||
background: linear-gradient(
|
||||
var(--color-button-gradient-start, #ff67e0),
|
||||
var(--color-button-gradient-end, #ff52a2)
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
*/
|
||||
import Image from "next/image";
|
||||
|
||||
import styles from "./pwa-install-dialog.module.css";
|
||||
|
||||
export interface PwaInstallDialogProps {
|
||||
onClose: () => void;
|
||||
onInstall?: () => void | Promise<void>;
|
||||
@@ -31,37 +29,40 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.overlay}
|
||||
className="fixed inset-0 z-[60] flex items-center justify-center bg-[rgba(0,0,0,0.5)] pb-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-bottom,0px))] pl-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-left,0px))] pr-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-right,0px))] pt-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-top,0px))]"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="pwa-dialog-title"
|
||||
>
|
||||
<div className={styles.dialog}>
|
||||
<div className="flex w-full max-w-[var(--pwa-install-dialog-max-width,360px)] flex-col items-center rounded-[var(--responsive-card-radius,40px)] bg-[var(--color-page-background,#ffffff)] px-[var(--responsive-card-padding,16px)] pb-[var(--responsive-card-padding,16px)] pt-[var(--responsive-card-padding-lg,20px)] text-center shadow-[0_10px_20px_rgba(0,0,0,0.1)]">
|
||||
<Image
|
||||
className={styles.icon}
|
||||
className="mb-[var(--spacing-lg,16px)] size-[var(--icon-size-86,86px)] object-contain"
|
||||
src="/images/icons/icon-add-to-home.png"
|
||||
alt=""
|
||||
width={86}
|
||||
height={86}
|
||||
priority
|
||||
/>
|
||||
<h2 id="pwa-dialog-title" className={styles.title}>
|
||||
<h2
|
||||
id="pwa-dialog-title"
|
||||
className="m-0 mb-[var(--spacing-sm,8px)] text-[var(--responsive-page-title,var(--font-size-22,22px))] font-bold leading-[1.2] text-[var(--color-text-foreground,#171717)]"
|
||||
>
|
||||
Add to Home Screen
|
||||
</h2>
|
||||
<p className={styles.content}>
|
||||
<p className="mx-[var(--responsive-card-padding-lg,36px)] mb-[var(--page-section-gap-lg,24px)] mt-0 text-[var(--responsive-body,var(--font-size-lg,16px))] leading-[1.5] text-[#393939]">
|
||||
Add CozSweet to Home Screen{"\n"}for the best experience
|
||||
</p>
|
||||
<div className={styles.actions}>
|
||||
<div className="flex w-full gap-[var(--spacing-md,12px)]">
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.btn} ${styles.btnSecondary}`}
|
||||
className="flex min-h-[var(--pwa-button-height,44px)] flex-auto cursor-pointer items-center justify-center rounded-[var(--radius-bottom-sheet,28px)] border-0 bg-[var(--color-text-secondary,#9e9e9e)] text-[var(--responsive-body,var(--font-size-lg,16px))] font-semibold text-[var(--color-page-background,#ffffff)]"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.btn} ${styles.btnPrimary}`}
|
||||
className="flex min-h-[var(--pwa-button-height,44px)] flex-auto cursor-pointer items-center justify-center rounded-[var(--radius-bottom-sheet,28px)] border-0 bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-[var(--responsive-body,var(--font-size-lg,16px))] font-semibold text-[var(--color-page-background,#ffffff)]"
|
||||
onClick={handleInstall}
|
||||
>
|
||||
OK
|
||||
|
||||
Reference in New Issue
Block a user