refactor(chat): migrate overlay styles to tailwind
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { BrowserHintOverlay } from "../browser-hint-overlay";
|
||||||
import { ChatHeader } from "../chat-header";
|
import { ChatHeader } from "../chat-header";
|
||||||
import { ChatInsufficientCreditsBanner } from "../chat-insufficient-credits-banner";
|
import { ChatInsufficientCreditsBanner } from "../chat-insufficient-credits-banner";
|
||||||
import { ChatInputTextField } from "../chat-input-text-field";
|
import { ChatInputTextField } from "../chat-input-text-field";
|
||||||
@@ -8,6 +9,7 @@ import { ChatSendButton } from "../chat-send-button";
|
|||||||
import { ImageBubble } from "../image-bubble";
|
import { ImageBubble } from "../image-bubble";
|
||||||
import { MessageAvatar } from "../message-avatar";
|
import { MessageAvatar } from "../message-avatar";
|
||||||
import { PrivateMessageCard } from "../private-message-card";
|
import { PrivateMessageCard } from "../private-message-card";
|
||||||
|
import { PwaInstallDialog } from "../pwa-install-dialog";
|
||||||
import { TextBubble } from "../text-bubble";
|
import { TextBubble } from "../text-bubble";
|
||||||
|
|
||||||
vi.mock("@/router/use-app-navigator", () => ({
|
vi.mock("@/router/use-app-navigator", () => ({
|
||||||
@@ -181,4 +183,42 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(html).toContain("Message Elio");
|
expect(html).toContain("Message Elio");
|
||||||
expect(html).toContain("Hello");
|
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 { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser";
|
||||||
|
|
||||||
import styles from "./browser-hint-overlay.module.css";
|
|
||||||
|
|
||||||
export interface BrowserHintOverlayProps {
|
export interface BrowserHintOverlayProps {
|
||||||
/** 自定义按钮标题 */
|
/** 自定义按钮标题 */
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -31,6 +29,12 @@ export interface BrowserHintOverlayProps {
|
|||||||
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 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({
|
export function BrowserHintOverlay({
|
||||||
title = DEFAULT_TITLE,
|
title = DEFAULT_TITLE,
|
||||||
@@ -77,19 +81,34 @@ export function BrowserHintOverlay({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.overlay} ${
|
className={[
|
||||||
isExpanded ? styles.expanded : styles.collapsed
|
OVERLAY_BASE_CLASS_NAME,
|
||||||
}`}
|
isExpanded ? OVERLAY_EXPANDED_CLASS_NAME : OVERLAY_COLLAPSED_CLASS_NAME,
|
||||||
|
].join(" ")}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
aria-expanded={isExpanded}
|
aria-expanded={isExpanded}
|
||||||
onClick={handleClick}
|
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} />
|
<ExternalLink size={15} strokeWidth={2.4} />
|
||||||
</span>
|
</span>
|
||||||
<span className={styles.copy} aria-hidden={!isExpanded}>
|
<span
|
||||||
<span className={styles.title}>{title}</span>
|
className={
|
||||||
<span className={styles.description}>{description}</span>
|
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>
|
</span>
|
||||||
</button>
|
</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 Image from "next/image";
|
||||||
|
|
||||||
import styles from "./pwa-install-dialog.module.css";
|
|
||||||
|
|
||||||
export interface PwaInstallDialogProps {
|
export interface PwaInstallDialogProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onInstall?: () => void | Promise<void>;
|
onInstall?: () => void | Promise<void>;
|
||||||
@@ -31,37 +29,40 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="pwa-dialog-title"
|
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
|
<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"
|
src="/images/icons/icon-add-to-home.png"
|
||||||
alt=""
|
alt=""
|
||||||
width={86}
|
width={86}
|
||||||
height={86}
|
height={86}
|
||||||
priority
|
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
|
Add to Home Screen
|
||||||
</h2>
|
</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
|
Add CozSweet to Home Screen{"\n"}for the best experience
|
||||||
</p>
|
</p>
|
||||||
<div className={styles.actions}>
|
<div className="flex w-full gap-[var(--spacing-md,12px)]">
|
||||||
<button
|
<button
|
||||||
type="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}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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}
|
onClick={handleInstall}
|
||||||
>
|
>
|
||||||
OK
|
OK
|
||||||
|
|||||||
Reference in New Issue
Block a user