refactor(chat): migrate unlock dialog styles to tailwind

This commit is contained in:
2026-07-13 12:59:31 +08:00
parent 3752b3b729
commit bc5e80330f
5 changed files with 110 additions and 232 deletions
@@ -6,7 +6,9 @@ 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";
import { ChatSendButton } from "../chat-send-button"; import { ChatSendButton } from "../chat-send-button";
import { HistoryUnlockDialog } from "../history-unlock-dialog";
import { ImageBubble } from "../image-bubble"; import { ImageBubble } from "../image-bubble";
import { InsufficientCreditsDialog } from "../insufficient-credits-dialog";
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 { PwaInstallDialog } from "../pwa-install-dialog";
@@ -223,4 +225,73 @@ describe("chat Tailwind components", () => {
expect(html).toContain("Cancel"); expect(html).toContain("Cancel");
expect(html).toContain("OK"); expect(html).toContain("OK");
}); });
it("renders HistoryUnlockDialog open and loading states", () => {
expect(
renderToStaticMarkup(
<HistoryUnlockDialog
open={false}
lockedCount={2}
onClose={() => undefined}
onConfirm={() => undefined}
/>,
),
).toBe("");
const html = renderToStaticMarkup(
<HistoryUnlockDialog
open
lockedCount={3}
isLoading
errorMessage="Could not unlock history."
onClose={() => undefined}
onConfirm={() => undefined}
/>,
);
expect(html).toContain('aria-labelledby="history-unlock-title"');
expect(html).toContain("z-[200]");
expect(html).toContain("Unlock previous messages?");
expect(html).toContain("We found 3 locked messages");
expect(html).toContain("Could not unlock history.");
expect(html).toContain("Unlocking...");
expect(html).toContain("text-[var(--color-pwa-button-text,#ffffff)]");
expect(html).toContain("disabled");
});
it("renders InsufficientCreditsDialog with credit details", () => {
expect(
renderToStaticMarkup(
<InsufficientCreditsDialog
open={false}
creditBalance={0}
requiredCredits={0}
shortfallCredits={0}
onClose={() => undefined}
onConfirm={() => undefined}
/>,
),
).toBe("");
const html = renderToStaticMarkup(
<InsufficientCreditsDialog
open
creditBalance={2}
requiredCredits={5}
shortfallCredits={3}
onClose={() => undefined}
onConfirm={() => undefined}
/>,
);
expect(html).toContain('aria-labelledby="insufficient-credits-title"');
expect(html).toContain("z-[220]");
expect(html).toContain("Not enough credits");
expect(html).toContain("Balance");
expect(html).toContain("Required");
expect(html).toContain("Still needed");
expect(html).toContain("text-[var(--color-pwa-button-text,#ffffff)]");
expect(html).toContain("bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)]");
expect(html).toContain("Top up now");
});
}); });
@@ -1,94 +0,0 @@
.overlay {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
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);
padding:
var(--responsive-card-padding-lg, 24px)
var(--responsive-card-padding, 16px)
var(--responsive-card-padding, 16px);
text-align: center;
background: var(--color-page-background, #ffffff);
border-radius: var(--responsive-card-radius, 40px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.title {
margin: 0 0 10px;
font-size: var(--responsive-page-title, var(--font-size-22, 22px));
font-weight: 700;
line-height: 1.2;
color: var(--color-text-foreground, #171717);
}
.content {
margin:
0
var(--responsive-card-padding-lg, 24px)
var(--spacing-lg, 16px);
font-size: var(--responsive-body, var(--font-size-lg, 16px));
line-height: 1.5;
text-align: left;
color: #393939;
}
.error {
margin:
0
var(--responsive-card-padding-lg, 24px)
var(--spacing-lg, 16px);
font-size: var(--responsive-caption, var(--font-size-md, 14px));
line-height: 1.4;
text-align: left;
color: #c0364c;
}
.actions {
display: flex;
gap: var(--spacing-md, 12px);
width: 100%;
}
.button {
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;
display: flex;
align-items: center;
justify-content: center;
}
.button:disabled {
cursor: not-allowed;
opacity: 0.72;
}
.secondary {
color: var(--color-page-background, #ffffff);
background: var(--color-text-secondary, #9e9e9e);
}
.primary {
color: var(--color-page-background, #ffffff);
background: linear-gradient(
var(--color-button-gradient-start, #ff67e0),
var(--color-button-gradient-end, #ff52a2)
);
}
@@ -1,7 +1,5 @@
"use client"; "use client";
import styles from "./history-unlock-dialog.module.css";
export interface HistoryUnlockDialogProps { export interface HistoryUnlockDialogProps {
open: boolean; open: boolean;
lockedCount: number; lockedCount: number;
@@ -23,26 +21,31 @@ export function HistoryUnlockDialog({
return ( return (
<div <div
className={styles.overlay} className="fixed inset-0 z-[200] 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="history-unlock-title" aria-labelledby="history-unlock-title"
> >
<div className={styles.dialog}> <div className="w-full max-w-[var(--pwa-install-dialog-max-width,360px)] 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,24px)] text-center shadow-[0_10px_20px_rgba(0,0,0,0.1)]">
<h2 id="history-unlock-title" className={styles.title}> <h2
id="history-unlock-title"
className="m-0 mb-2.5 text-[var(--responsive-page-title,var(--font-size-22,22px))] font-bold leading-[1.2] text-[var(--color-text-foreground,#171717)]"
>
Unlock previous messages? Unlock previous messages?
</h2> </h2>
<p className={styles.content}> <p className="mx-[var(--responsive-card-padding-lg,24px)] mb-[var(--spacing-lg,16px)] mt-0 text-left text-[var(--responsive-body,var(--font-size-lg,16px))] leading-[1.5] text-[#393939]">
We found {lockedCount} locked messages in your chat history. You can We found {lockedCount} locked messages in your chat history. You can
unlock them now to continue the conversation with full context. unlock them now to continue the conversation with full context.
</p> </p>
{errorMessage ? ( {errorMessage ? (
<p className={styles.error}>{errorMessage}</p> <p className="mx-[var(--responsive-card-padding-lg,24px)] mb-[var(--spacing-lg,16px)] mt-0 text-left text-[var(--responsive-caption,var(--font-size-md,14px))] leading-[1.4] text-[#c0364c]">
{errorMessage}
</p>
) : null} ) : null}
<div className={styles.actions}> <div className="flex w-full gap-[var(--spacing-md,12px)]">
<button <button
type="button" type="button"
className={`${styles.button} ${styles.secondary}`} 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-pwa-button-text,#ffffff)] disabled:cursor-not-allowed disabled:opacity-72"
onClick={onClose} onClick={onClose}
disabled={isLoading} disabled={isLoading}
> >
@@ -50,7 +53,7 @@ export function HistoryUnlockDialog({
</button> </button>
<button <button
type="button" type="button"
className={`${styles.button} ${styles.primary}`} 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-pwa-button-text,#ffffff)] disabled:cursor-not-allowed disabled:opacity-72"
onClick={onConfirm} onClick={onConfirm}
disabled={isLoading} disabled={isLoading}
> >
@@ -1,109 +0,0 @@
.overlay {
position: fixed;
inset: 0;
z-index: 220;
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.52);
}
.dialog {
width: 100%;
max-width: var(--pwa-install-dialog-max-width, 360px);
padding:
var(--responsive-card-padding-lg, 24px)
var(--responsive-card-padding, 18px)
var(--responsive-card-padding, 18px);
text-align: center;
background: var(--color-page-background, #ffffff);
border: 1px solid rgba(246, 87, 160, 0.16);
border-radius: var(--responsive-card-radius, 36px);
box-shadow: 0 18px 46px rgba(38, 18, 31, 0.18);
}
.title {
margin: 0 0 10px;
color: var(--color-text-foreground, #171717);
font-size: var(--responsive-page-title, var(--font-size-22, 22px));
font-weight: 700;
line-height: 1.2;
}
.content {
margin: 0 var(--spacing-md, 12px) var(--page-section-gap, 18px);
color: #393939;
font-size: var(--responsive-body, var(--font-size-lg, 16px));
line-height: 1.5;
}
.creditList {
display: grid;
gap: var(--spacing-sm, 8px);
margin: 0 0 var(--page-section-gap, 18px);
padding: var(--spacing-md, 12px);
background: #fff4f9;
border-radius: var(--responsive-card-radius-sm, 22px);
}
.creditItem {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md, 12px);
color: #4a3b43;
font-size: var(--responsive-caption, var(--font-size-md, 14px));
line-height: 1.3;
}
.creditItem dt {
margin: 0;
font-weight: 500;
}
.creditItem dd {
margin: 0;
color: #f657a0;
font-size: var(--responsive-body, var(--font-size-lg, 16px));
font-weight: 800;
}
.actions {
display: flex;
gap: var(--spacing-md, 12px);
width: 100%;
}
.button {
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: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.secondary {
color: var(--color-page-background, #ffffff);
background: var(--color-text-secondary, #9e9e9e);
}
.primary {
color: var(--color-page-background, #ffffff);
background: linear-gradient(90deg, #ff67e0 0%, #ff52a2 100%);
box-shadow: 0 6px 14px rgba(246, 87, 160, 0.28);
}
.button:focus-visible {
outline: 2px solid #f657a0;
outline-offset: 3px;
}
@@ -1,7 +1,5 @@
"use client"; "use client";
import styles from "./insufficient-credits-dialog.module.css";
export interface InsufficientCreditsDialogProps { export interface InsufficientCreditsDialogProps {
open: boolean; open: boolean;
creditBalance: number; creditBalance: number;
@@ -25,47 +23,56 @@ export function InsufficientCreditsDialog({
return ( return (
<div <div
className={styles.overlay} className="fixed inset-0 z-[220] flex items-center justify-center bg-[rgba(0,0,0,0.52)] 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="insufficient-credits-title" aria-labelledby="insufficient-credits-title"
> >
<div className={styles.dialog}> <div className="w-full max-w-[var(--pwa-install-dialog-max-width,360px)] rounded-[var(--responsive-card-radius,36px)] border border-[rgba(246,87,160,0.16)] 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)] text-center shadow-[0_18px_46px_rgba(38,18,31,0.18)]">
<h2 id="insufficient-credits-title" className={styles.title}> <h2
id="insufficient-credits-title"
className="m-0 mb-2.5 text-[var(--responsive-page-title,var(--font-size-22,22px))] font-bold leading-[1.2] text-[var(--color-text-foreground,#171717)]"
>
Not enough credits Not enough credits
</h2> </h2>
<p className={styles.content}> <p className="mx-[var(--spacing-md,12px)] mb-[var(--page-section-gap,18px)] mt-0 text-[var(--responsive-body,var(--font-size-lg,16px))] leading-[1.5] text-[#393939]">
Top up your credits to unlock this message and keep the moment going. Top up your credits to unlock this message and keep the moment going.
</p> </p>
{showCreditDetail ? ( {showCreditDetail ? (
<dl className={styles.creditList}> <dl className="mb-[var(--page-section-gap,18px)] grid gap-[var(--spacing-sm,8px)] rounded-[var(--responsive-card-radius-sm,22px)] bg-[#fff4f9] p-[var(--spacing-md,12px)]">
<div className={styles.creditItem}> <div className="flex items-center justify-between gap-[var(--spacing-md,12px)] text-[var(--responsive-caption,var(--font-size-md,14px))] leading-[1.3] text-[#4a3b43]">
<dt>Balance</dt> <dt className="m-0 font-medium">Balance</dt>
<dd>{creditBalance}</dd> <dd className="m-0 text-[var(--responsive-body,var(--font-size-lg,16px))] font-extrabold text-[#f657a0]">
{creditBalance}
</dd>
</div> </div>
<div className={styles.creditItem}> <div className="flex items-center justify-between gap-[var(--spacing-md,12px)] text-[var(--responsive-caption,var(--font-size-md,14px))] leading-[1.3] text-[#4a3b43]">
<dt>Required</dt> <dt className="m-0 font-medium">Required</dt>
<dd>{requiredCredits}</dd> <dd className="m-0 text-[var(--responsive-body,var(--font-size-lg,16px))] font-extrabold text-[#f657a0]">
{requiredCredits}
</dd>
</div> </div>
<div className={styles.creditItem}> <div className="flex items-center justify-between gap-[var(--spacing-md,12px)] text-[var(--responsive-caption,var(--font-size-md,14px))] leading-[1.3] text-[#4a3b43]">
<dt>Still needed</dt> <dt className="m-0 font-medium">Still needed</dt>
<dd>{shortfallCredits}</dd> <dd className="m-0 text-[var(--responsive-body,var(--font-size-lg,16px))] font-extrabold text-[#f657a0]">
{shortfallCredits}
</dd>
</div> </div>
</dl> </dl>
) : null} ) : null}
<div className={styles.actions}> <div className="flex w-full gap-[var(--spacing-md,12px)]">
<button <button
type="button" type="button"
className={`${styles.button} ${styles.secondary}`} 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-bold text-[var(--color-pwa-button-text,#ffffff)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
onClick={onClose} onClick={onClose}
> >
Later Later
</button> </button>
<button <button
type="button" type="button"
className={`${styles.button} ${styles.primary}`} 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(90deg,#ff67e0_0%,#ff52a2_100%)] text-[var(--responsive-body,var(--font-size-lg,16px))] font-bold text-[var(--color-pwa-button-text,#ffffff)] shadow-[0_6px_14px_rgba(246,87,160,0.28)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
onClick={onConfirm} onClick={onConfirm}
> >
Top up now Top up now