refactor: canonicalize TypeScript and Tailwind syntax

This commit is contained in:
2026-07-14 19:06:47 +08:00
parent 52bbc363cf
commit 836ae95940
23 changed files with 58 additions and 58 deletions
+2 -2
View File
@@ -24,11 +24,11 @@ type BackButtonActionProps = BackButtonBaseProps & {
export type BackButtonProps = BackButtonLinkProps | BackButtonActionProps;
const BASE_CLASS_NAME =
"inline-flex cursor-pointer items-center justify-center rounded-full border-0 p-0 text-(--color-auth-text-primary) no-underline transition-[background,box-shadow,transform] duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]";
"inline-flex cursor-pointer items-center justify-center rounded-full border-0 p-0 text-auth-text-primary no-underline transition-[background,box-shadow,transform] duration-150 ease-out focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]";
const VARIANT_CLASS_NAMES = {
floating:
"absolute left-[calc(var(--spacing-xl,20px)+var(--app-safe-left,0px))] top-[calc(var(--spacing-xl,20px)+var(--app-safe-top,0px))] z-2 size-(--back-button-size) bg-(--color-auth-surface) pr-[2px] shadow-[0_2px_8px_rgba(0,0,0,0.1)] hover:bg-[rgba(0,0,0,0.04)] active:scale-96",
"absolute left-[calc(var(--spacing-xl,20px)+var(--app-safe-left,0))] top-[calc(var(--spacing-xl,20px)+var(--app-safe-top,0))] z-2 size-(--back-button-size) bg-(--color-auth-surface) pr-0.5 shadow-[0_2px_8px_rgba(0,0,0,0.1)] hover:bg-[rgba(0,0,0,0.04)] active:scale-96",
soft:
"size-(--responsive-icon-button-size,42px) border border-[rgba(25,19,22,0.08)] bg-[rgba(255,255,255,0.82)] text-[#21191d] shadow-[0_10px_22px_rgba(50,30,40,0.08)] hover:-translate-y-px hover:bg-white hover:shadow-[0_14px_26px_rgba(50,30,40,0.12)] active:translate-y-px",
ghost:
+2 -2
View File
@@ -29,7 +29,7 @@ export function Checkbox({
ariaLabel,
}: CheckboxProps) {
const checkboxClassName = [
"mt-0.5 inline-flex size-(--auth-legal-checkbox-size) shrink-0 cursor-pointer items-center justify-center rounded-full border border-(--color-auth-surface) bg-(--color-auth-surface) p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.06)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--color-accent) disabled:cursor-not-allowed disabled:opacity-50",
"mt-0.5 inline-flex size-(--auth-legal-checkbox-size) shrink-0 cursor-pointer items-center justify-center rounded-full border border-(--color-auth-surface) bg-(--color-auth-surface) p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.06)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50",
].join(" ");
const button = (
<button
@@ -65,7 +65,7 @@ export function Checkbox({
.join(" ")}
>
{button}
<span className="flex-auto text-left text-(length:--font-size-sm) leading-[1.4] text-(--color-auth-text-primary)">
<span className="flex-auto text-left text-(length:--font-size-sm) leading-[1.4] text-auth-text-primary">
{label}
</span>
</label>
@@ -1,19 +1,19 @@
export const stripePaymentDialogStyles = {
overlay:
"fixed inset-0 z-70 flex items-center justify-center bg-[rgba(0,0,0,0.5)] pb-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-bottom,0px))] pl-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-left,0px))] pr-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-right,0px))] pt-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-top,0px))]",
"fixed inset-0 z-70 flex items-center justify-center bg-[rgba(0,0,0,0.5)] pb-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-bottom,0))] pl-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-left,0))] pr-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-right,0))] pt-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-top,0))]",
dialog:
"max-h-[calc(var(--app-visible-height,100dvh)-calc(var(--dialog-safe-margin,20px)*2))] w-full max-w-(--dialog-wide-max-width,420px) overflow-y-auto rounded-(--responsive-card-radius,32px) bg-(--color-page-background,#ffffff) px-(--responsive-card-padding,18px) pb-(--responsive-card-padding,18px) pt-(--responsive-card-padding-lg,24px) shadow-[0_18px_40px_rgba(0,0,0,0.16)]",
header: "mb-(--page-section-gap,18px) text-left",
title:
"m-0 mb-[clamp(7px,1.481vw,8px)] text-(length:--responsive-page-title,22px) font-bold leading-[1.2] text-(--color-text-foreground,#171717)",
"m-0 mb-[clamp(7px,1.481vw,8px)] text-(length:--responsive-page-title,22px) font-bold leading-[1.2] text-text-foreground",
content:
"m-0 text-(length:--responsive-body,14px) leading-[1.5] text-[#393939]",
"m-0 text-(length:--responsive-body,14px) leading-normal text-[#393939]",
form: "flex flex-col gap-(--page-section-gap,18px)",
error:
"m-0 text-(length:--responsive-caption,13px) leading-[1.45] text-[#c0392b]",
actions: "flex w-full gap-(--spacing-md,12px)",
button:
"min-h-(--pwa-button-height,44px) flex-auto cursor-pointer rounded-(--radius-bottom-sheet,28px) border-0 text-(length:--responsive-body,16px) font-semibold disabled:cursor-not-allowed disabled:opacity-55",
"min-h-(--pwa-button-height,44px) flex-auto cursor-pointer rounded-bottom-sheet border-0 text-(length:--responsive-body,16px) font-semibold disabled:cursor-not-allowed disabled:opacity-55",
secondary: "bg-(--color-text-secondary,#9e9e9e) text-(--color-page-background,#ffffff)",
primary:
"bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-(--color-page-background,#ffffff)",
@@ -43,7 +43,7 @@ export function AuthOtherOptionsDialog({
style={{ paddingBottom: "calc(var(--spacing-xxl) + var(--app-safe-bottom, 0px))" }}
>
<div
className="mb-sm h-(--auth-drag-handle-height) w-(--auth-drag-handle-width) self-center rounded-[2px] bg-auth-drag-handle"
className="mb-sm h-(--auth-drag-handle-height) w-(--auth-drag-handle-width) self-center rounded-xs bg-auth-drag-handle"
aria-hidden="true"
/>
<h2 className="my-sm text-center text-(length:--responsive-page-title) font-bold text-auth-text-primary">
+1 -1
View File
@@ -212,7 +212,7 @@ export function ChatScreen() {
/>
</div>
<div className="relative z-[1] flex min-h-0 flex-[1_1_auto] flex-col">
<div className="relative z-1 flex min-h-0 flex-[1_1_auto] flex-col">
{/* isGuest 派生自 auth.loginStatus */}
<ChatHeader
isGuest={isGuest}
@@ -160,7 +160,7 @@ describe("chat Tailwind components", () => {
);
expect(guestHtml).toContain('aria-label="Sign up to unlock more features"');
expect(guestHtml).toContain("bg-(--color-accent,#f84d96)");
expect(guestHtml).toContain("bg-accent");
expect(guestHtml).toContain("size-(--icon-size-sm,16px)");
expect(guestHtml).not.toContain('href="/splash"');
expect(guestHtml).not.toContain('aria-label="Back to home"');
@@ -189,7 +189,7 @@ describe("chat Tailwind components", () => {
expect(html).toContain('aria-label="Message"');
expect(html).toContain("min-h-(--chat-send-button-size,40px)");
expect(html).toContain("max-h-[min(30vh,120px)]");
expect(html).toContain("caret-(--color-accent,#f84d96)");
expect(html).toContain("caret-accent");
expect(html).toContain("placeholder:text-(--color-text-hint,#757575)");
expect(html).toContain("Message Elio");
expect(html).toContain("Hello");
@@ -34,7 +34,7 @@ const OVERLAY_BASE_CLASS_NAME =
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]";
"inline-flex size-10.5 min-h-10.5 min-w-10.5 items-center justify-center rounded-full p-1.25";
export function BrowserHintOverlay({
title = DEFAULT_TITLE,
@@ -92,7 +92,7 @@ export function BrowserHintOverlay({
onClick={handleClick}
>
<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-180"
className="inline-flex size-30 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-180"
aria-hidden="true"
>
<ExternalLink size={15} strokeWidth={2.4} />
@@ -100,15 +100,15 @@ export function BrowserHintOverlay({
<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"
? "flex min-w-0 max-w-47.5 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-(length:--font-size-sm,12px) font-extrabold leading-[1.25] text-white">
<span className="wrap-break-word text-(length:--font-size-sm,12px) font-extrabold leading-tight text-white">
{title}
</span>
<span className="break-words text-[11px] font-semibold leading-[1.2] text-[rgba(255,255,255,0.72)]">
<span className="wrap-break-word text-[11px] font-semibold leading-[1.2] text-[rgba(255,255,255,0.72)]">
{description}
</span>
</span>
+2 -2
View File
@@ -27,7 +27,7 @@ export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
type="button"
data-analytics-key="chat.open_signup"
data-analytics-label="Open sign up"
className="flex w-full cursor-pointer items-center justify-center gap-(--spacing-sm,8px) border-0 bg-(--color-accent,#f84d96) px-(--spacing-md,12px) py-(--spacing-sm,8px) text-center text-(length:--responsive-caption,var(--font-size-sm,12px)) font-medium text-white"
className="flex w-full cursor-pointer items-center justify-center gap-(--spacing-sm,8px) border-0 bg-accent px-(--spacing-md,12px) py-(--spacing-sm,8px) text-center text-(length:--responsive-caption,var(--font-size-sm,12px)) font-medium text-white"
onClick={() => navigator.openAuth(ROUTES.chat)}
aria-label="Sign up to unlock more features"
>
@@ -55,7 +55,7 @@ export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
type="button"
data-analytics-key="chat.open_menu"
data-analytics-label="Open chat menu"
className="flex size-(--responsive-icon-button-size,42px) cursor-pointer items-center justify-center rounded-(--radius-full,999px) border border-[rgba(255,255,255,0.12)] bg-[rgba(13,11,20,0.7)] text-(--color-text-primary,#fff) shadow-[0_10px_24px_rgba(0,0,0,0.2)] backdrop-blur-md transition-[background,transform] duration-150 hover:bg-[rgba(28,24,39,0.82)] active:scale-96 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
className="flex size-(--responsive-icon-button-size,42px) cursor-pointer items-center justify-center rounded-full border border-[rgba(255,255,255,0.12)] bg-[rgba(13,11,20,0.7)] text-(--color-text-primary,#fff) shadow-[0_10px_24px_rgba(0,0,0,0.2)] backdrop-blur-md transition-[background,transform] duration-150 hover:bg-[rgba(28,24,39,0.82)] active:scale-96 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
onClick={() => navigator.push(ROUTES.sidebar)}
aria-label="Menu"
>
@@ -63,7 +63,7 @@ export const ChatInputTextField = forwardRef<
<div className="flex min-w-0 flex-auto items-center rounded-full bg-white px-(--spacing-lg,16px)">
<textarea
ref={innerRef}
className="min-h-(--chat-send-button-size,40px) max-h-[min(30vh,120px)] w-full min-w-0 flex-auto resize-none border-0 bg-transparent pb-0 pl-0 pr-0 pt-[clamp(5px,1.111vw,6px)] font-[inherit] text-[16px] leading-[clamp(22px,4.444vw,24px)] text-(--color-text-foreground,#000) caret-(--color-accent,#f84d96) outline-none placeholder:text-(--color-text-hint,#757575)"
className="min-h-(--chat-send-button-size,40px) max-h-[min(30vh,120px)] w-full min-w-0 flex-auto resize-none border-0 bg-transparent pb-0 pl-0 pr-0 pt-[clamp(5px,1.111vw,6px)] font-[inherit] text-[16px] leading-[clamp(22px,4.444vw,24px)] text-(--color-text-foreground,#000) caret-accent outline-none placeholder:text-(--color-text-hint,#757575)"
value={value}
onChange={(e: FormEvent<HTMLTextAreaElement>) =>
onChange(e.currentTarget.value)
@@ -21,7 +21,7 @@ export function HistoryUnlockDialog({
return (
<div
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))]"
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,0))] pl-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-left,0))] pr-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-right,0))] pt-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-top,0))]"
role="dialog"
aria-modal="true"
aria-labelledby="history-unlock-title"
@@ -29,11 +29,11 @@ export function HistoryUnlockDialog({
<div className="w-full max-w-(--pwa-install-dialog-max-width,360px) rounded-(--responsive-card-radius,40px) bg-(--color-page-background,#ffffff) px-(--responsive-card-padding,16px) pb-(--responsive-card-padding,16px) pt-(--responsive-card-padding-lg,24px) text-center shadow-[0_10px_20px_rgba(0,0,0,0.1)]">
<h2
id="history-unlock-title"
className="m-0 mb-2.5 text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-(--color-text-foreground,#171717)"
className="m-0 mb-2.5 text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-text-foreground"
>
Unlock previous messages?
</h2>
<p className="mx-(--responsive-card-padding-lg,24px) mb-(--spacing-lg,16px) mt-0 text-left text-(length:--responsive-body,var(--font-size-lg,16px)) leading-[1.5] text-[#393939]">
<p className="mx-(--responsive-card-padding-lg,24px) mb-(--spacing-lg,16px) mt-0 text-left text-(length:--responsive-body,var(--font-size-lg,16px)) leading-normal text-[#393939]">
We found {lockedCount} locked messages in your chat history. You can
unlock them now to continue the conversation with full context.
</p>
@@ -45,7 +45,7 @@ export function HistoryUnlockDialog({
<div className="flex w-full gap-(--spacing-md,12px)">
<button
type="button"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-(--radius-bottom-sheet,28px) border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff) disabled:cursor-not-allowed disabled:opacity-72"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-bottom-sheet border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff) disabled:cursor-not-allowed disabled:opacity-72"
onClick={onClose}
disabled={isLoading}
>
@@ -55,7 +55,7 @@ export function HistoryUnlockDialog({
type="button"
data-analytics-key="chat.unlock_history"
data-analytics-label="Unlock chat history"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-(--radius-bottom-sheet,28px) border-0 bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff) disabled:cursor-not-allowed disabled:opacity-72"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-bottom-sheet border-0 bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff) disabled:cursor-not-allowed disabled:opacity-72"
onClick={onConfirm}
disabled={isLoading}
>
+1 -1
View File
@@ -38,7 +38,7 @@ export function ImageBubble({
return (
<div
className="relative max-h-(--chat-media-size,220px) max-w-(--chat-media-size,220px) cursor-pointer overflow-hidden rounded-(--radius-lg,12px) rounded-tl-none bg-(--color-bubble-background,#fff)"
className="relative max-h-(--chat-media-size,220px) max-w-(--chat-media-size,220px) cursor-pointer overflow-hidden rounded-lg rounded-tl-none bg-(--color-bubble-background,#fff)"
onClick={openImage}
role={canOpen ? "button" : undefined}
tabIndex={canOpen ? 0 : undefined}
@@ -23,7 +23,7 @@ export function InsufficientCreditsDialog({
return (
<div
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))]"
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,0))] pl-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-left,0))] pr-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-right,0))] pt-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-top,0))]"
role="dialog"
aria-modal="true"
aria-labelledby="insufficient-credits-title"
@@ -31,11 +31,11 @@ export function InsufficientCreditsDialog({
<div className="w-full max-w-(--pwa-install-dialog-max-width,360px) rounded-(--responsive-card-radius,36px) border border-[rgba(246,87,160,0.16)] bg-(--color-page-background,#ffffff) px-(--responsive-card-padding,18px) pb-(--responsive-card-padding,18px) pt-(--responsive-card-padding-lg,24px) text-center shadow-[0_18px_46px_rgba(38,18,31,0.18)]">
<h2
id="insufficient-credits-title"
className="m-0 mb-2.5 text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-(--color-text-foreground,#171717)"
className="m-0 mb-2.5 text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-text-foreground"
>
Not enough credits
</h2>
<p className="mx-(--spacing-md,12px) mb-(--page-section-gap,18px) mt-0 text-(length:--responsive-body,var(--font-size-lg,16px)) leading-[1.5] text-[#393939]">
<p className="mx-(--spacing-md,12px) mb-(--page-section-gap,18px) mt-0 text-(length:--responsive-body,var(--font-size-lg,16px)) leading-normal text-[#393939]">
Top up your credits to unlock this message and keep the moment going.
</p>
@@ -65,7 +65,7 @@ export function InsufficientCreditsDialog({
<div className="flex w-full gap-(--spacing-md,12px)">
<button
type="button"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-(--radius-bottom-sheet,28px) border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--responsive-body,var(--font-size-lg,16px)) font-bold text-(--color-pwa-button-text,#ffffff) focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-bottom-sheet border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--responsive-body,var(--font-size-lg,16px)) font-bold text-(--color-pwa-button-text,#ffffff) focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
onClick={onClose}
>
Later
@@ -74,7 +74,7 @@ export function InsufficientCreditsDialog({
type="button"
data-analytics-key="chat.topup"
data-analytics-label="Top up chat credits"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-(--radius-bottom-sheet,28px) border-0 bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)] text-(length:--responsive-body,var(--font-size-lg,16px)) font-bold text-(--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]"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-bottom-sheet border-0 bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)] text-(length:--responsive-body,var(--font-size-lg,16px)) font-bold text-(--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}
>
Top up now
@@ -29,7 +29,7 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
return (
<div
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))]"
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,0))] pl-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-left,0))] pr-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-right,0))] pt-[calc(var(--dialog-safe-margin,16px)+var(--app-safe-top,0))]"
role="dialog"
aria-modal="true"
aria-labelledby="pwa-dialog-title"
@@ -45,24 +45,24 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
/>
<h2
id="pwa-dialog-title"
className="m-0 mb-(--spacing-sm,8px) text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-(--color-text-foreground,#171717)"
className="m-0 mb-(--spacing-sm,8px) text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-text-foreground"
>
Add to Home Screen
</h2>
<p className="mx-(--responsive-card-padding-lg,36px) mb-(--page-section-gap-lg,24px) mt-0 text-(length:--responsive-body,var(--font-size-lg,16px)) leading-[1.5] text-[#393939]">
<p className="mx-(--responsive-card-padding-lg,36px) mb-(--page-section-gap-lg,24px) mt-0 text-(length:--responsive-body,var(--font-size-lg,16px)) leading-normal text-[#393939]">
Add CozSweet to Home Screen{"\n"}for the best experience
</p>
<div className="flex w-full gap-(--spacing-md,12px)">
<button
type="button"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-(--radius-bottom-sheet,28px) border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff)"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-bottom-sheet border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff)"
onClick={onClose}
>
Cancel
</button>
<button
type="button"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-(--radius-bottom-sheet,28px) border-0 bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff)"
className="flex min-h-(--pwa-button-height,44px) flex-auto cursor-pointer items-center justify-center rounded-bottom-sheet border-0 bg-[linear-gradient(var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-(length:--responsive-body,var(--font-size-lg,16px)) font-semibold text-(--color-pwa-button-text,#ffffff)"
onClick={handleInstall}
>
OK
+1 -1
View File
@@ -15,7 +15,7 @@ export interface TextBubbleProps {
}
const BASE_CLASS_NAME =
"w-full self-stretch whitespace-pre-wrap break-words rounded-(--radius-xxl,24px) px-(--spacing-4,16px) py-(--spacing-3,12px) text-(length:--chat-message-font-size,var(--responsive-body,16px)) leading-(--chat-message-line-height,1.5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))";
"w-full self-stretch whitespace-pre-wrap wrap-break-word rounded-(--radius-xxl,24px) px-(--spacing-4,16px) py-(--spacing-3,12px) text-(length:--chat-message-font-size,var(--responsive-body,16px)) leading-(--chat-message-line-height,1.5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))";
const AI_CLASS_NAME =
"rounded-tl-none bg-white text-(--color-text-foreground,#000)";
+7 -7
View File
@@ -36,11 +36,11 @@ export function UserHeader({
if (isLoading) {
return (
<div className="flex w-full items-center gap-(--spacing-md,12px)" aria-hidden="true">
<div className="flex size-(--sidebar-avatar-size,56px) shrink-0 items-center justify-center overflow-hidden rounded-full bg-(--color-accent,#f84d96) text-white" />
<div className="flex size-(--sidebar-avatar-size,56px) shrink-0 items-center justify-center overflow-hidden rounded-full bg-accent text-white" />
<div className="flex min-w-0 flex-auto flex-col gap-(--sidebar-card-gap-y,4px)">
<span className="block h-[clamp(16px,3.333vw,18px)] w-[60%] rounded-(--radius-md,8px) bg-(--color-dark-gray,#e5e5e5)" />
<span className="block h-[clamp(12px,2.593vw,14px)] w-[80%] rounded-(--radius-md,8px) bg-(--color-dark-gray,#e5e5e5)" />
<span className="block h-[clamp(12px,2.593vw,14px)] w-[40%] rounded-(--radius-md,8px) bg-(--color-dark-gray,#e5e5e5)" />
<span className="block h-[clamp(16px,3.333vw,18px)] w-[60%] rounded-md bg-(--color-dark-gray,#e5e5e5)" />
<span className="block h-[clamp(12px,2.593vw,14px)] w-[80%] rounded-md bg-(--color-dark-gray,#e5e5e5)" />
<span className="block h-[clamp(12px,2.593vw,14px)] w-[40%] rounded-md bg-(--color-dark-gray,#e5e5e5)" />
</div>
</div>
);
@@ -50,7 +50,7 @@ export function UserHeader({
<div className="flex w-full items-center gap-(--spacing-md,12px)">
<UserMessageAvatar
avatarUrl={avatarUrl}
className="flex size-(--sidebar-avatar-size,56px) shrink-0 items-center justify-center overflow-hidden rounded-full bg-(--color-accent,#f84d96) text-white"
className="flex size-(--sidebar-avatar-size,56px) shrink-0 items-center justify-center overflow-hidden rounded-full bg-accent text-white"
size="var(--sidebar-avatar-size, 56px)"
/>
@@ -64,7 +64,7 @@ export function UserHeader({
</p>
)}
{state === "vip" && (
<span className="inline-flex self-start items-center gap-(--spacing-xs,4px) rounded-full bg-(--color-pill-bg,rgba(248,77,150,0.10)) px-[clamp(7px,1.667vw,9px)] py-[clamp(3px,0.741vw,4px)] text-(length:--font-size-xs,10px) font-semibold leading-none text-(--color-accent,#f84d96)">
<span className="inline-flex self-start items-center gap-(--spacing-xs,4px) rounded-full bg-(--color-pill-bg,rgba(248,77,150,0.10)) px-[clamp(7px,1.667vw,9px)] py-[clamp(3px,0.741vw,4px)] text-(length:--font-size-xs,10px) font-semibold leading-none text-accent">
<Image
src="/images/sidebar/ic_user_vip.png"
alt=""
@@ -82,7 +82,7 @@ export function UserHeader({
type="button"
data-analytics-key="sidebar.login"
data-analytics-label="Open login"
className="inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-[linear-gradient(90deg,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] px-(--spacing-lg,16px) py-[clamp(5px,1.111vw,6px)] text-(length:clamp(12px,2.593vw,14px)) font-semibold text-(--color-text-primary,#ffffff) hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--color-accent,#f84d96)"
className="inline-flex shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-[linear-gradient(90deg,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] px-(--spacing-lg,16px) py-[clamp(5px,1.111vw,6px)] text-(length:clamp(12px,2.593vw,14px)) font-semibold text-(--color-text-primary,#ffffff) hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
onClick={onLoginClick}
aria-label="Log in"
>
+1 -1
View File
@@ -14,7 +14,7 @@ export function SplashButton({ onStartChat }: SplashButtonProps) {
data-analytics-key="splash.start_chat"
data-analytics-label="Start chatting"
onClick={onStartChat}
className="inline-flex min-h-(--responsive-control-height,48px) w-full max-w-120 touch-manipulation flex-auto cursor-pointer items-center justify-center rounded-(--radius-full,999px) border-0 bg-[linear-gradient(to_right,var(--color-button-gradient-start),var(--color-button-gradient-end))] px-[clamp(var(--spacing-lg,16px),5.556vw,30px)] font-bold italic text-white shadow-[0_0_10px_rgba(248,89,168,0.3)] transition-[filter,box-shadow,transform] duration-150 ease-out [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-white enabled:hover:-translate-y-px enabled:hover:brightness-105 enabled:hover:shadow-[0_8px_22px_rgba(248,89,168,0.42)] active:enabled:translate-y-0.5 active:enabled:scale-96 active:enabled:brightness-90 active:enabled:shadow-[0_2px_6px_rgba(248,89,168,0.24)] motion-reduce:transition-none motion-reduce:active:enabled:transform-none disabled:cursor-not-allowed disabled:opacity-70"
className="inline-flex min-h-(--responsive-control-height,48px) w-full max-w-120 touch-manipulation flex-auto cursor-pointer items-center justify-center rounded-full border-0 bg-[linear-gradient(to_right,var(--color-button-gradient-start),var(--color-button-gradient-end))] px-[clamp(var(--spacing-lg,16px),5.556vw,30px)] font-bold italic text-white shadow-[0_0_10px_rgba(248,89,168,0.3)] transition-[filter,box-shadow,transform] duration-150 ease-out [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-white enabled:hover:-translate-y-px enabled:hover:brightness-105 enabled:hover:shadow-[0_8px_22px_rgba(248,89,168,0.42)] active:enabled:translate-y-0.5 active:enabled:scale-96 active:enabled:brightness-90 active:enabled:shadow-[0_2px_6px_rgba(248,89,168,0.24)] motion-reduce:transition-none motion-reduce:active:enabled:transform-none disabled:cursor-not-allowed disabled:opacity-70"
>
<span className="whitespace-nowrap text-(length:--responsive-section-title,var(--font-size-xxl)) font-bold text-white">
Start Chatting
@@ -16,7 +16,7 @@ export function SubscriptionCtaButton({
...rest
}: SubscriptionCtaButtonProps) {
const classes = [
"inline-flex min-h-[clamp(48px,9.63vw,52px)] w-full cursor-pointer items-center justify-center rounded-(--radius-full,999px) border-0 bg-[linear-gradient(269deg,#ff67e0_0%,rgba(254,104,224,0.5)_20%,rgba(252,105,223,0.79)_66%,#f96ade_100%),linear-gradient(#f657a0,#f657a0)] px-(--responsive-card-padding,var(--spacing-lg)) text-(length:--responsive-body,var(--font-size-lg)) font-semibold text-white opacity-85 shadow-[0_5px_7px_0_rgba(247,89,168,0.31)] transition-[filter,transform,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--color-accent) active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-50",
"inline-flex min-h-[clamp(48px,9.63vw,52px)] w-full cursor-pointer items-center justify-center rounded-full border-0 bg-[linear-gradient(269deg,#ff67e0_0%,rgba(254,104,224,0.5)_20%,rgba(252,105,223,0.79)_66%,#f96ade_100%),linear-gradient(#f657a0,#f657a0)] px-(--responsive-card-padding,var(--spacing-lg)) text-(length:--responsive-body,var(--font-size-lg)) font-semibold text-white opacity-85 shadow-[0_5px_7px_0_rgba(247,89,168,0.31)] transition-[filter,transform,opacity] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-50",
className,
]
.filter(Boolean)
@@ -4,11 +4,11 @@ import Image from "next/image";
import type { PayChannel } from "@/data/dto/payment";
const PAYMENT_METHODS: Array<{
const PAYMENT_METHODS: {
channel: PayChannel;
title: string;
logoSrc?: string;
}> = [
}[] = [
{
channel: "stripe",
title: "Stripe",
@@ -63,7 +63,7 @@ export function SubscriptionPaymentMethod({
{paymentMethods.map((method) => {
const selected = method.channel === value;
const classes = [
"flex min-h-[clamp(54px,11.481vw,62px)] cursor-pointer items-center justify-center rounded-(--responsive-card-radius-sm,18px) px-[clamp(12px,2.593vw,14px)] py-[clamp(9px,1.852vw,10px)] text-center font-[inherit] text-[#3c3b3b] transition-[border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--color-accent) active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-72",
"flex min-h-[clamp(54px,11.481vw,62px)] cursor-pointer items-center justify-center rounded-(--responsive-card-radius-sm,18px) px-[clamp(12px,2.593vw,14px)] py-[clamp(9px,1.852vw,10px)] text-center font-[inherit] text-[#3c3b3b] transition-[border-color,box-shadow,transform] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-72",
selected
? "border-2 border-[#f657a0] bg-[#fff4f9] shadow-[0_8px_18px_rgba(217,47,127,0.24),0_0_0_3px_rgba(217,47,127,0.18)] -translate-y-0.5"
: "border border-[rgba(246,87,160,0.18)] bg-white shadow-[0_5px_7px_0_rgba(247,89,168,0.08)]",
@@ -90,7 +90,7 @@ export function SubscriptionPaymentMethod({
aria-hidden="true"
width={122}
height={29}
className="block max-h-[34px] w-[min(100%,92px)] object-contain"
className="block max-h-8.5 w-[min(100%,92px)] object-contain"
/>
) : (
<span className="text-(length:--responsive-body,16px) font-bold leading-[1.1] text-[#1e1e1e]">
@@ -20,7 +20,7 @@ export function SubscriptionPaymentSuccessDialog({
return (
<div
className="fixed inset-0 z-80 flex items-center justify-center bg-[rgba(0,0,0,0.45)] pb-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-bottom,0px))] pl-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-left,0px))] pr-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-right,0px))] pt-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-top,0px))]"
className="fixed inset-0 z-80 flex items-center justify-center bg-[rgba(0,0,0,0.45)] pb-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-bottom,0))] pl-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-left,0))] pr-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-right,0))] pt-[calc(var(--dialog-safe-margin,20px)+var(--app-safe-top,0))]"
role="alertdialog"
aria-modal="true"
aria-labelledby="subscription-payment-success-title"
@@ -34,11 +34,11 @@ export function SubscriptionPaymentSuccessDialog({
</div>
<h2
id="subscription-payment-success-title"
className="m-0 mb-[clamp(7px,1.481vw,8px)] text-(length:--responsive-page-title,22px) font-bold leading-[1.2] text-(--color-text-foreground,#171717)"
className="m-0 mb-[clamp(7px,1.481vw,8px)] text-(length:--responsive-page-title,22px) font-bold leading-[1.2] text-text-foreground"
>
Payment successful
</h2>
<p className="m-0 text-(length:--responsive-body,14px) leading-[1.5] text-[#393939]">
<p className="m-0 text-(length:--responsive-body,14px) leading-normal text-[#393939]">
{content}
</p>
<button
@@ -39,7 +39,7 @@ describe("createHttpClient", () => {
await authStorage.setLoginToken("old-login-token");
await authStorage.setRefreshToken("refresh-token");
const requestAuthHeaders: Array<string | null> = [];
const requestAuthHeaders: (string | null)[] = [];
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
const url = new URL(input instanceof Request ? input.url : String(input));
const headers =
+1 -1
View File
@@ -106,7 +106,7 @@ export async function persistExternalEntryPayload({
}: ExternalEntryPayload): Promise<void> {
const authStorage = AuthStorage.getInstance();
const userStorage = UserStorage.getInstance();
const tasks: Array<Promise<unknown>> = [];
const tasks: Promise<unknown>[] = [];
if (hasValue(deviceId)) {
tasks.push(authStorage.setDeviceId(deviceId));
+2 -2
View File
@@ -7,7 +7,7 @@ import { todayString } from "@/utils/date";
* Business fact: `role === "assistant"` means the message is from AI.
*/
export function localMessagesToUi(
records: ReadonlyArray<{
records: readonly {
id?: string;
type?: string;
content: string;
@@ -16,7 +16,7 @@ export function localMessagesToUi(
audioUrl?: string | null;
image?: { type: string | null; url: string | null };
lockDetail?: ChatLockDetailData;
}>,
}[],
): UiMessage[] {
return records.map((m) => ({
...(m.id ? { id: m.id } : {}),
@@ -97,7 +97,7 @@ function createTestPaymentMachine(
createOrderSpy: CreateOrderSpy;
createOrderError: Error;
orderStatus: "pending" | "paid" | "failed";
orderStatuses: Array<"pending" | "paid" | "failed">;
orderStatuses: ("pending" | "paid" | "failed")[];
}> = {},
) {
const createOrderSpy = overrides.createOrderSpy ?? vi.fn<CreateOrderSpy>();