refactor(chat): remove language dialog
This commit is contained in:
@@ -13,6 +13,7 @@ import { MessageAvatar } from "../message-avatar";
|
|||||||
import { PrivateMessageCard } from "../private-message-card";
|
import { PrivateMessageCard } from "../private-message-card";
|
||||||
import { PwaInstallDialog } from "../pwa-install-dialog";
|
import { PwaInstallDialog } from "../pwa-install-dialog";
|
||||||
import { TextBubble } from "../text-bubble";
|
import { TextBubble } from "../text-bubble";
|
||||||
|
import { VoiceUnlockOptionsDialog } from "../voice-unlock-options-dialog";
|
||||||
|
|
||||||
vi.mock("@/router/use-app-navigator", () => ({
|
vi.mock("@/router/use-app-navigator", () => ({
|
||||||
useAppNavigator: () => ({
|
useAppNavigator: () => ({
|
||||||
@@ -294,4 +295,38 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(html).toContain("bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)]");
|
expect(html).toContain("bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)]");
|
||||||
expect(html).toContain("Top up now");
|
expect(html).toContain("Top up now");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders VoiceUnlockOptionsDialog action choices", () => {
|
||||||
|
expect(
|
||||||
|
renderToStaticMarkup(
|
||||||
|
<VoiceUnlockOptionsDialog
|
||||||
|
open={false}
|
||||||
|
onClose={() => undefined}
|
||||||
|
onBuyVoicePackage={() => undefined}
|
||||||
|
onActivateVip={() => undefined}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
).toBe("");
|
||||||
|
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<VoiceUnlockOptionsDialog
|
||||||
|
open
|
||||||
|
onClose={() => undefined}
|
||||||
|
onBuyVoicePackage={() => undefined}
|
||||||
|
onActivateVip={() => undefined}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(html).toContain('aria-labelledby="voice-unlock-options-title"');
|
||||||
|
expect(html).toContain("z-[72]");
|
||||||
|
expect(html).toContain("Unlock voice message");
|
||||||
|
expect(html).toContain("购买语音包");
|
||||||
|
expect(html).toContain("开通 VIP");
|
||||||
|
expect(html).toContain("免费10条语言消息+无限畅聊");
|
||||||
|
expect(html).toContain("Cancel");
|
||||||
|
expect(html).toContain("bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)]");
|
||||||
|
expect(html).toContain(
|
||||||
|
"bg-[linear-gradient(#ffffff,#ffffff)_padding-box,linear-gradient(90deg,#ff67e0,#ff52a2)_border-box]",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export * from "./fullscreen-image-viewer";
|
|||||||
export * from "./history-unlock-dialog";
|
export * from "./history-unlock-dialog";
|
||||||
export * from "./image-bubble";
|
export * from "./image-bubble";
|
||||||
export * from "./insufficient-credits-dialog";
|
export * from "./insufficient-credits-dialog";
|
||||||
export * from "./language-dialog";
|
|
||||||
export * from "./lottie-message-bubble";
|
export * from "./lottie-message-bubble";
|
||||||
export * from "./message-avatar";
|
export * from "./message-avatar";
|
||||||
export * from "./message-bubble";
|
export * from "./message-bubble";
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
/* LanguageDialog 语言切换弹窗样式 */
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 50;
|
|
||||||
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(--dialog-max-width, 360px);
|
|
||||||
background: var(--color-surface, #1f1a2e);
|
|
||||||
border-radius: var(--responsive-card-radius-sm, var(--radius-lg, 12px));
|
|
||||||
padding: var(--responsive-card-padding-lg, var(--spacing-5, 20px));
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-3, 12px);
|
|
||||||
max-height: calc(
|
|
||||||
var(--app-visible-height, 100dvh) -
|
|
||||||
calc(var(--dialog-safe-margin, 16px) * 2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--responsive-section-title, var(--font-size-lg, 18px));
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-text-primary, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-1, 4px);
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: calc(
|
|
||||||
var(--app-visible-height, 100dvh) -
|
|
||||||
calc(var(--dialog-safe-margin, 16px) * 2) -
|
|
||||||
88px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: var(--spacing-3, 12px) var(--spacing-4, 16px);
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius-md, 8px);
|
|
||||||
color: var(--color-text-primary, #fff);
|
|
||||||
font-size: var(--responsive-body, var(--font-size-base, 14px));
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover,
|
|
||||||
.item:focus-visible {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemActive {
|
|
||||||
background: rgba(248, 77, 150, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
color: var(--color-accent, #f84d96);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
"use client";
|
|
||||||
/**
|
|
||||||
* LanguageDialog 语言切换弹窗
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* 简化版:单语种列表选择(保留可扩展性)
|
|
||||||
* - 支持未来添加更多语言
|
|
||||||
* - 当前选中状态高亮
|
|
||||||
* - ESC / 点击外部 → 关闭
|
|
||||||
*/
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
import styles from "./language-dialog.module.css";
|
|
||||||
|
|
||||||
export interface LanguageItem {
|
|
||||||
code: string;
|
|
||||||
label: string;
|
|
||||||
nativeLabel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_LANGUAGES: readonly LanguageItem[] = [
|
|
||||||
{ code: "en", label: "English", nativeLabel: "English" },
|
|
||||||
{ code: "zh", label: "Chinese (Simplified)", nativeLabel: "简体中文" },
|
|
||||||
{ code: "zh-TW", label: "Chinese (Traditional)", nativeLabel: "繁體中文" },
|
|
||||||
{ code: "ja", label: "Japanese", nativeLabel: "日本語" },
|
|
||||||
{ code: "ko", label: "Korean", nativeLabel: "한국어" },
|
|
||||||
{ code: "es", label: "Spanish", nativeLabel: "Español" },
|
|
||||||
{ code: "fr", label: "French", nativeLabel: "Français" },
|
|
||||||
{ code: "de", label: "German", nativeLabel: "Deutsch" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface LanguageDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
currentLanguageCode?: string;
|
|
||||||
languages?: readonly LanguageItem[];
|
|
||||||
onSelect: (code: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LanguageDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
currentLanguageCode = "en",
|
|
||||||
languages = DEFAULT_LANGUAGES,
|
|
||||||
onSelect,
|
|
||||||
}: LanguageDialogProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") onClose();
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", onKey);
|
|
||||||
return () => document.removeEventListener("keydown", onKey);
|
|
||||||
}, [open, onClose]);
|
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.overlay}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<div className={styles.dialog} onClick={(e) => e.stopPropagation()}>
|
|
||||||
<h2 className={styles.title}>Select Language</h2>
|
|
||||||
<div className={styles.list} role="listbox">
|
|
||||||
{languages.map((lang) => {
|
|
||||||
const isActive = currentLanguageCode === lang.code;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={lang.code}
|
|
||||||
type="button"
|
|
||||||
role="option"
|
|
||||||
aria-selected={isActive}
|
|
||||||
className={`${styles.item} ${isActive ? styles.itemActive : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(lang.code);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{lang.nativeLabel}{" "}
|
|
||||||
<span style={{ opacity: 0.5 }}>({lang.label})</span>
|
|
||||||
</span>
|
|
||||||
{isActive && <span className={styles.check}>✓</span>}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 72;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
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));
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
width: 100%;
|
|
||||||
max-width: var(--dialog-max-width, 380px);
|
|
||||||
padding:
|
|
||||||
var(--responsive-card-padding-lg, 24px)
|
|
||||||
var(--responsive-card-padding, 18px)
|
|
||||||
var(--responsive-card-padding, 18px);
|
|
||||||
border-radius: var(--responsive-card-radius, 36px);
|
|
||||||
background: var(--color-page-background, #ffffff);
|
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0 0 clamp(7px, 1.481vw, 8px);
|
|
||||||
color: var(--color-text-foreground, #171717);
|
|
||||||
font-size: var(--responsive-page-title, var(--font-size-22, 22px));
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
margin: 0 var(--spacing-md, 12px) var(--page-section-gap-lg, 20px);
|
|
||||||
color: #393939;
|
|
||||||
font-size: var(--responsive-body, var(--font-size-md, 14px));
|
|
||||||
line-height: 1.5;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing-md, 12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.optionButton,
|
|
||||||
.cancel {
|
|
||||||
width: 100%;
|
|
||||||
min-height: var(--responsive-control-height, 48px);
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius-bottom-sheet, 28px);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--responsive-body, var(--font-size-lg, 16px));
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optionButton {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: var(--responsive-inline-gap-xs, 4px);
|
|
||||||
padding:
|
|
||||||
clamp(9px, 1.852vw, 10px)
|
|
||||||
var(--responsive-card-padding, 16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
color: #ffffff;
|
|
||||||
background: linear-gradient(90deg, #ff67e0 0%, #ff52a2 100%);
|
|
||||||
box-shadow: 0 5px 7px rgba(247, 89, 168, 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondaryOption {
|
|
||||||
color: #f657a0;
|
|
||||||
background:
|
|
||||||
linear-gradient(#ffffff, #ffffff) padding-box,
|
|
||||||
linear-gradient(90deg, #ff67e0, #ff52a2) border-box;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optionTitle {
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optionDescription {
|
|
||||||
color: #3c3b3b;
|
|
||||||
font-size: var(--responsive-caption, 13px);
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel {
|
|
||||||
margin-top: clamp(12px, 2.593vw, 14px);
|
|
||||||
color: #ffffff;
|
|
||||||
background: var(--color-text-secondary, #9e9e9e);
|
|
||||||
}
|
|
||||||
|
|
||||||
.optionButton:focus-visible,
|
|
||||||
.cancel:focus-visible {
|
|
||||||
outline: 2px solid #f657a0;
|
|
||||||
outline-offset: 3px;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import styles from "./voice-unlock-options-dialog.module.css";
|
|
||||||
|
|
||||||
export interface VoiceUnlockOptionsDialogProps {
|
export interface VoiceUnlockOptionsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -19,40 +17,47 @@ export function VoiceUnlockOptionsDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.overlay}
|
className="fixed inset-0 z-[72] 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="voice-unlock-options-title"
|
aria-labelledby="voice-unlock-options-title"
|
||||||
>
|
>
|
||||||
<div className={styles.dialog}>
|
<div className="w-full max-w-[var(--dialog-max-width,380px)] rounded-[var(--responsive-card-radius,36px)] bg-[var(--color-page-background,#ffffff)] px-[var(--responsive-card-padding,18px)] pb-[var(--responsive-card-padding,18px)] pt-[var(--responsive-card-padding-lg,24px)] shadow-[0_18px_40px_rgba(0,0,0,0.16)]">
|
||||||
<h2 id="voice-unlock-options-title" className={styles.title}>
|
<h2
|
||||||
|
id="voice-unlock-options-title"
|
||||||
|
className="m-0 mb-[clamp(7px,1.481vw,8px)] text-center text-[var(--responsive-page-title,var(--font-size-22,22px))] font-bold leading-[1.2] text-[var(--color-text-foreground,#171717)]"
|
||||||
|
>
|
||||||
Unlock voice message
|
Unlock voice message
|
||||||
</h2>
|
</h2>
|
||||||
<p className={styles.content}>
|
<p className="mx-[var(--spacing-md,12px)] mb-[var(--page-section-gap-lg,20px)] mt-0 text-center text-[var(--responsive-body,var(--font-size-md,14px))] leading-[1.5] text-[#393939]">
|
||||||
Choose how you would like to unlock this voice message.
|
Choose how you would like to unlock this voice message.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className={styles.options}>
|
<div className="flex flex-col gap-[var(--spacing-md,12px)]">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.optionButton} ${styles.primary}`}
|
className="flex min-h-[var(--responsive-control-height,48px)] w-full cursor-pointer flex-col items-center justify-center gap-[var(--responsive-inline-gap-xs,4px)] rounded-[var(--radius-bottom-sheet,28px)] border-0 bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)] px-[var(--responsive-card-padding,16px)] py-[clamp(9px,1.852vw,10px)] text-[var(--responsive-body,var(--font-size-lg,16px))] font-bold text-white shadow-[0_5px_7px_rgba(247,89,168,0.24)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
||||||
onClick={onBuyVoicePackage}
|
onClick={onBuyVoicePackage}
|
||||||
>
|
>
|
||||||
购买语音包
|
购买语音包
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.optionButton} ${styles.secondaryOption}`}
|
className="flex min-h-[var(--responsive-control-height,48px)] w-full cursor-pointer flex-col items-center justify-center gap-[var(--responsive-inline-gap-xs,4px)] rounded-[var(--radius-bottom-sheet,28px)] border border-transparent bg-[linear-gradient(#ffffff,#ffffff)_padding-box,linear-gradient(90deg,#ff67e0,#ff52a2)_border-box] px-[var(--responsive-card-padding,16px)] py-[clamp(9px,1.852vw,10px)] text-[var(--responsive-body,var(--font-size-lg,16px))] font-bold text-[#f657a0] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
||||||
onClick={onActivateVip}
|
onClick={onActivateVip}
|
||||||
>
|
>
|
||||||
<span className={styles.optionTitle}>开通 VIP</span>
|
<span className="leading-[1.2]">开通 VIP</span>
|
||||||
<span className={styles.optionDescription}>
|
<span className="text-[var(--responsive-caption,13px)] font-medium leading-[1.25] text-[#3c3b3b]">
|
||||||
免费10条语言消息+无限畅聊
|
免费10条语言消息+无限畅聊
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" className={styles.cancel} onClick={onClose}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-[clamp(12px,2.593vw,14px)] min-h-[var(--responsive-control-height,48px)] w-full cursor-pointer rounded-[var(--radius-bottom-sheet,28px)] border-0 bg-[var(--color-text-secondary,#9e9e9e)] text-[var(--responsive-body,var(--font-size-lg,16px))] font-bold text-white focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user