style(app): normalize tailwind utility syntax

This commit is contained in:
2026-07-13 13:17:16 +08:00
parent aff7f80206
commit b89bde1b55
37 changed files with 143 additions and 143 deletions
@@ -17,7 +17,7 @@ describe("shared Tailwind components", () => {
expect(linkHtml).toContain('href="/chat"'); expect(linkHtml).toContain('href="/chat"');
expect(linkHtml).toContain('aria-label="Back to chat"'); expect(linkHtml).toContain('aria-label="Back to chat"');
expect(linkHtml).toContain("inline-flex"); expect(linkHtml).toContain("inline-flex");
expect(linkHtml).toContain("size-[var(--responsive-icon-button-size,42px)]"); expect(linkHtml).toContain("size-(--responsive-icon-button-size,42px)");
expect(linkHtml).toContain("hover:bg-white"); expect(linkHtml).toContain("hover:bg-white");
expect(actionHtml).toContain("<button"); expect(actionHtml).toContain("<button");
expect(actionHtml).toContain("size-8"); expect(actionHtml).toContain("size-8");
+4 -4
View File
@@ -23,15 +23,15 @@ type BackButtonActionProps = BackButtonBaseProps & {
export type BackButtonProps = BackButtonLinkProps | BackButtonActionProps; export type BackButtonProps = BackButtonLinkProps | BackButtonActionProps;
const BASE_CLASS_NAME = const BASE_CLASS_NAME =
"inline-flex cursor-pointer items-center justify-center rounded-full border-0 p-0 text-[var(--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-(--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]";
const VARIANT_CLASS_NAMES = { const VARIANT_CLASS_NAMES = {
floating: 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-[var(--back-button-size)] bg-[var(--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-[0.96]", "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",
soft: soft:
"size-[var(--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", "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: ghost:
"size-8 bg-transparent text-black hover:opacity-70 active:scale-[0.96]", "size-8 bg-transparent text-black hover:opacity-70 active:scale-96",
} satisfies Record<NonNullable<BackButtonBaseProps["variant"]>, string>; } satisfies Record<NonNullable<BackButtonBaseProps["variant"]>, string>;
export function BackButton({ export function BackButton({
+1 -1
View File
@@ -30,7 +30,7 @@ export function CharacterAvatar({
return ( return (
<span <span
className={[ className={[
"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-[var(--color-avatar-border,#fbf3f5)]", "inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-(--color-avatar-border,#fbf3f5)",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
@@ -45,13 +45,13 @@ describe("core Tailwind components", () => {
expect(checkedHtml).toContain('role="checkbox"'); expect(checkedHtml).toContain('role="checkbox"');
expect(checkedHtml).toContain('aria-checked="true"'); expect(checkedHtml).toContain('aria-checked="true"');
expect(checkedHtml).toContain('aria-label="Accept terms"'); expect(checkedHtml).toContain('aria-label="Accept terms"');
expect(checkedHtml).toContain("size-[var(--auth-legal-checkbox-size)]"); expect(checkedHtml).toContain("size-(--auth-legal-checkbox-size)");
expect(checkedHtml).toContain( expect(checkedHtml).toContain(
"size-[var(--auth-legal-checkbox-inner-size)]", "size-(--auth-legal-checkbox-inner-size)",
); );
expect(labeledHtml).toContain("<label"); expect(labeledHtml).toContain("<label");
expect(labeledHtml).toContain("Keep me signed in"); expect(labeledHtml).toContain("Keep me signed in");
expect(labeledHtml).toContain("gap-[var(--spacing-sm)]"); expect(labeledHtml).toContain("gap-(--spacing-sm)");
}); });
}); });
+4 -4
View File
@@ -29,7 +29,7 @@ export function Checkbox({
ariaLabel, ariaLabel,
}: CheckboxProps) { }: CheckboxProps) {
const checkboxClassName = [ const checkboxClassName = [
"mt-0.5 inline-flex size-[var(--auth-legal-checkbox-size)] shrink-0 cursor-pointer items-center justify-center rounded-full border border-[var(--color-auth-surface)] bg-[var(--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-[var(--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-(--color-accent) disabled:cursor-not-allowed disabled:opacity-50",
].join(" "); ].join(" ");
const button = ( const button = (
<button <button
@@ -42,7 +42,7 @@ export function Checkbox({
className={checkboxClassName} className={checkboxClassName}
> >
{checked ? ( {checked ? (
<span className="block size-[var(--auth-legal-checkbox-inner-size)] rounded-full bg-[var(--color-auth-legal-check)]" /> <span className="block size-(--auth-legal-checkbox-inner-size) rounded-full bg-(--color-auth-legal-check)" />
) : null} ) : null}
</button> </button>
); );
@@ -58,14 +58,14 @@ export function Checkbox({
return ( return (
<label <label
className={[ className={[
"flex w-full cursor-pointer items-start gap-[var(--spacing-sm)]", "flex w-full cursor-pointer items-start gap-(--spacing-sm)",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
.join(" ")} .join(" ")}
> >
{button} {button}
<span className="flex-auto text-left text-[var(--font-size-sm)] leading-[1.4] text-[var(--color-auth-text-primary)]"> <span className="flex-auto text-left text-(length:--font-size-sm) leading-[1.4] text-(--color-auth-text-primary)">
{label} {label}
</span> </span>
</label> </label>
@@ -43,7 +43,7 @@ export function LoadingIndicator({
style={style} style={style}
/> />
{label ? ( {label ? (
<span className="text-[var(--font-size-sm)] text-text-secondary"> <span className="text-(length:--font-size-sm) text-text-secondary">
{label} {label}
</span> </span>
) : null} ) : null}
+1 -1
View File
@@ -14,7 +14,7 @@ export function UserMessageAvatar({
size = 43, size = 43,
}: UserMessageAvatarProps) { }: UserMessageAvatarProps) {
const avatarClassName = [ const avatarClassName = [
"flex shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-[var(--color-avatar-border,#fbf3f5)] bg-[var(--color-avatar-border,#fbf3f5)] shadow-[var(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))]", "flex shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-(--color-avatar-border,#fbf3f5) bg-(--color-avatar-border,#fbf3f5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
+2 -2
View File
@@ -50,10 +50,10 @@ export function AuthScreen() {
return ( return (
<MobileShell> <MobileShell>
<div className="relative flex min-h-[var(--app-viewport-height,100dvh)] w-full flex-1 flex-col overflow-hidden bg-[#fbf1f2]"> <div className="relative flex min-h-(--app-viewport-height,100dvh) w-full flex-1 flex-col overflow-hidden bg-[#fbf1f2]">
<AuthBackground /> <AuthBackground />
<div <div
className="relative z-[2] flex flex-auto flex-col justify-center" className="relative z-2 flex flex-auto flex-col justify-center"
style={{ style={{
padding: padding:
"calc(var(--page-padding-y, var(--spacing-lg)) + var(--app-safe-top, 0px)) calc(var(--page-padding-x, var(--spacing-lg)) + var(--app-safe-right, 0px)) calc(var(--page-padding-y, var(--spacing-lg)) + var(--app-safe-bottom, 0px)) calc(var(--page-padding-x, var(--spacing-lg)) + var(--app-safe-left, 0px))", "calc(var(--page-padding-y, var(--spacing-lg)) + var(--app-safe-top, 0px)) calc(var(--page-padding-x, var(--spacing-lg)) + var(--app-safe-right, 0px)) calc(var(--page-padding-y, var(--spacing-lg)) + var(--app-safe-bottom, 0px)) calc(var(--page-padding-x, var(--spacing-lg)) + var(--app-safe-left, 0px))",
@@ -39,7 +39,7 @@ describe("auth Tailwind components", () => {
<AuthPrimaryButton isLoading>Continue</AuthPrimaryButton>, <AuthPrimaryButton isLoading>Continue</AuthPrimaryButton>,
); );
expect(html).toContain("min-h-[var(--auth-field-height)]"); expect(html).toContain("min-h-(--auth-field-height)");
expect(html).toContain("bg-[linear-gradient"); expect(html).toContain("bg-[linear-gradient");
expect(html).toContain("animate-spin"); expect(html).toContain("animate-spin");
expect(html).toContain("Continue"); expect(html).toContain("Continue");
@@ -80,7 +80,7 @@ describe("auth Tailwind components", () => {
<AuthSocialButton icon={<span>G</span>}>Continue</AuthSocialButton>, <AuthSocialButton icon={<span>G</span>}>Continue</AuthSocialButton>,
); );
expect(html).toContain("min-h-[var(--auth-social-button-height)]"); expect(html).toContain("min-h-(--auth-social-button-height)");
expect(html).toContain("border-auth-border"); expect(html).toContain("border-auth-border");
expect(html).toContain("text-auth-text-primary"); expect(html).toContain("text-auth-text-primary");
expect(html).toContain("Continue"); expect(html).toContain("Continue");
@@ -108,7 +108,7 @@ describe("auth Tailwind components", () => {
const html = renderToStaticMarkup(<AuthLegalText />); const html = renderToStaticMarkup(<AuthLegalText />);
expect(html).toContain('aria-label="Agree to terms"'); expect(html).toContain('aria-label="Agree to terms"');
expect(html).toContain("size-[var(--auth-legal-checkbox-size)]"); expect(html).toContain("size-(--auth-legal-checkbox-size)");
expect(html).toContain("bg-auth-legal-check"); expect(html).toContain("bg-auth-legal-check");
expect(html).toContain("Privacy Agreement"); expect(html).toContain("Privacy Agreement");
expect(html).toContain("Terms of User"); expect(html).toContain("Terms of User");
@@ -125,7 +125,7 @@ describe("auth Tailwind components", () => {
/>, />,
); );
expect(html).toContain("min-h-[var(--auth-field-height)]"); expect(html).toContain("min-h-(--auth-field-height)");
expect(html).toContain("placeholder:text-auth-input-placeholder"); expect(html).toContain("placeholder:text-auth-input-placeholder");
expect(html).toContain("text-auth-text-primary"); expect(html).toContain("text-auth-text-primary");
expect(html).toContain("Invalid email"); expect(html).toContain("Invalid email");
+3 -3
View File
@@ -83,7 +83,7 @@ export function AuthEmailPanel({
<button <button
type="button" type="button"
className="self-center border-0 bg-transparent pt-md text-[var(--responsive-body)] text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80" className="self-center border-0 bg-transparent pt-md text-(length:--responsive-body) text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80"
onClick={() => setMode((m) => (m === "login" ? "register" : "login"))} onClick={() => setMode((m) => (m === "login" ? "register" : "login"))}
> >
{mode === "login" {mode === "login"
@@ -93,13 +93,13 @@ export function AuthEmailPanel({
<button <button
type="button" type="button"
className="self-center border-0 bg-transparent py-md text-[var(--responsive-body)] text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80" className="self-center border-0 bg-transparent py-md text-(length:--responsive-body) text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80"
onClick={() => setShowOptions(true)} onClick={() => setShowOptions(true)}
> >
Other Sign In Options Other Sign In Options
</button> </button>
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" /> <div className="min-h-(--page-section-gap,var(--spacing-lg)) flex-[1_1_0]" />
<AuthLegalText /> <AuthLegalText />
<AuthOtherOptionsDialog <AuthOtherOptionsDialog
@@ -8,7 +8,7 @@ export function AuthErrorMessage({ message }: { message: string | null }) {
if (!message) return null; if (!message) return null;
return ( return (
<div <div
className="w-full bg-transparent p-0 text-center text-[var(--responsive-caption)] leading-[1.4] text-error" className="w-full bg-transparent p-0 text-center text-(length:--responsive-caption) leading-[1.4] text-error"
role="alert" role="alert"
> >
{message} {message}
@@ -49,7 +49,7 @@ export function AuthFacebookPanel({
return ( return (
<div className="flex min-h-0 w-full flex-auto flex-col items-stretch"> <div className="flex min-h-0 w-full flex-auto flex-col items-stretch">
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" /> <div className="min-h-(--page-section-gap,var(--spacing-lg)) flex-[1_1_0]" />
<Image <Image
src="/images/auth/ic-logo-login.png" src="/images/auth/ic-logo-login.png"
alt="Cozsweet" alt="Cozsweet"
@@ -59,17 +59,17 @@ export function AuthFacebookPanel({
style={{ width: "auto", height: "auto" }} style={{ width: "auto", height: "auto" }}
priority priority
/> />
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" /> <div className="min-h-(--page-section-gap,var(--spacing-lg)) flex-[1_1_0]" />
<button <button
type="button" type="button"
className="mt-md inline-flex min-h-[var(--auth-field-height)] w-full cursor-pointer items-center justify-center gap-md rounded-full border-0 bg-auth-surface px-lg text-[var(--responsive-body)] font-semibold text-auth-text-primary shadow-[0_2px_6px_rgba(0,0,0,0.06)] transition-[filter,transform,opacity] duration-150 enabled:hover:brightness-[0.97] enabled:active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60" className="mt-md inline-flex min-h-(--auth-field-height) w-full cursor-pointer items-center justify-center gap-md rounded-full border-0 bg-auth-surface px-lg text-(length:--responsive-body) font-semibold text-auth-text-primary shadow-[0_2px_6px_rgba(0,0,0,0.06)] transition-[filter,transform,opacity] duration-150 enabled:hover:brightness-[0.97] enabled:active:scale-98 disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => handleFacebook()} onClick={() => handleFacebook()}
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? ( {isLoading ? (
<span <span
className="inline-block size-[var(--responsive-spinner-size,18px)] flex-none animate-spin rounded-full border-2 border-solid border-facebook-blue border-r-transparent border-b-transparent border-l-transparent" className="inline-block size-(--responsive-spinner-size,18px) flex-none animate-spin rounded-full border-2 border-solid border-facebook-blue border-r-transparent border-b-transparent border-l-transparent"
aria-hidden="true" aria-hidden="true"
/> />
) : ( ) : (
@@ -87,13 +87,13 @@ export function AuthFacebookPanel({
<button <button
type="button" type="button"
className="self-center border-0 bg-transparent py-md text-[var(--responsive-body)] text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80" className="self-center border-0 bg-transparent py-md text-(length:--responsive-body) text-auth-text-primary underline underline-offset-[3px] transition-opacity duration-150 hover:opacity-80"
onClick={() => setShowOptions(true)} onClick={() => setShowOptions(true)}
> >
Other Sign In Options Other Sign In Options
</button> </button>
<div className="min-h-[var(--page-section-gap,var(--spacing-lg))] flex-[1_1_0]" /> <div className="min-h-(--page-section-gap,var(--spacing-lg)) flex-[1_1_0]" />
<AuthErrorMessage message={errorMessage} /> <AuthErrorMessage message={errorMessage} />
<AuthLegalText /> <AuthLegalText />
+3 -3
View File
@@ -22,16 +22,16 @@ export function AuthLegalText() {
<div className="flex w-full items-start gap-sm"> <div className="flex w-full items-start gap-sm">
<button <button
type="button" type="button"
className="mt-0.5 inline-flex size-[var(--auth-legal-checkbox-size)] flex-none cursor-pointer items-center justify-center rounded-full border border-auth-surface bg-auth-surface p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.06)]" className="mt-0.5 inline-flex size-(--auth-legal-checkbox-size) flex-none cursor-pointer items-center justify-center rounded-full border border-auth-surface bg-auth-surface p-0 shadow-[0_0_0_1px_rgba(0,0,0,0.06)]"
onClick={() => setChecked((c) => !c)} onClick={() => setChecked((c) => !c)}
aria-label="Agree to terms" aria-label="Agree to terms"
aria-pressed={checked} aria-pressed={checked}
> >
{checked ? ( {checked ? (
<span className="block size-[var(--auth-legal-checkbox-inner-size)] rounded-full bg-auth-legal-check" /> <span className="block size-(--auth-legal-checkbox-inner-size) rounded-full bg-auth-legal-check" />
) : null} ) : null}
</button> </button>
<p className="m-0 flex-auto text-left text-[var(--responsive-caption)] leading-[1.4] text-auth-text-primary"> <p className="m-0 flex-auto text-left text-(length:--responsive-caption) leading-[1.4] text-auth-text-primary">
I agree to the Cozsweet {" "} I agree to the Cozsweet {" "}
<a <a
href={AppConstants.privacyPolicyUrl} href={AppConstants.privacyPolicyUrl}
@@ -43,10 +43,10 @@ export function AuthOtherOptionsDialog({
style={{ paddingBottom: "calc(var(--spacing-xxl) + var(--app-safe-bottom, 0px))" }} style={{ paddingBottom: "calc(var(--spacing-xxl) + var(--app-safe-bottom, 0px))" }}
> >
<div <div
className="mb-sm h-[var(--auth-drag-handle-height)] w-[var(--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-[2px] bg-auth-drag-handle"
aria-hidden="true" aria-hidden="true"
/> />
<h2 className="my-sm text-center text-[var(--responsive-page-title)] font-bold text-auth-text-primary"> <h2 className="my-sm text-center text-(length:--responsive-page-title) font-bold text-auth-text-primary">
Other Sign-in Options Other Sign-in Options
</h2> </h2>
@@ -90,7 +90,7 @@ export function AuthOtherOptionsDialog({
<button <button
type="button" type="button"
className="self-center border-0 bg-transparent py-md text-[var(--responsive-body)] font-medium text-auth-text-primary no-underline hover:underline" className="self-center border-0 bg-transparent py-md text-(length:--responsive-body) font-medium text-auth-text-primary no-underline hover:underline"
onClick={onClose} onClick={onClose}
> >
Cancel Cancel
@@ -32,7 +32,7 @@ export function AuthPrimaryButton({
type={type} type={type}
disabled={disabled || isLoading} disabled={disabled || isLoading}
className={[ className={[
"inline-flex min-h-[var(--auth-field-height)] w-full cursor-pointer items-center justify-center gap-sm rounded-full border-0 bg-[linear-gradient(90deg,var(--color-auth-primary-gradient-start),var(--color-auth-primary-gradient-end))] px-lg text-[var(--responsive-body)] font-bold text-white shadow-[0_3px_5px_var(--color-auth-primary-button-shadow)] transition-[filter,transform,opacity] duration-150 active:enabled:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70", "inline-flex min-h-(--auth-field-height) w-full cursor-pointer items-center justify-center gap-sm rounded-full border-0 bg-[linear-gradient(90deg,var(--color-auth-primary-gradient-start),var(--color-auth-primary-gradient-end))] px-lg text-(length:--responsive-body) font-bold text-white shadow-[0_3px_5px_var(--color-auth-primary-button-shadow)] transition-[filter,transform,opacity] duration-150 active:enabled:scale-98 disabled:cursor-not-allowed disabled:opacity-70",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
@@ -41,7 +41,7 @@ export function AuthPrimaryButton({
> >
{isLoading ? ( {isLoading ? (
<span <span
className="inline-block size-[var(--responsive-spinner-size,18px)] animate-spin rounded-full border-2 border-solid border-white border-r-transparent border-b-transparent border-l-transparent" className="inline-block size-(--responsive-spinner-size,18px) animate-spin rounded-full border-2 border-solid border-white border-r-transparent border-b-transparent border-l-transparent"
aria-hidden="true" aria-hidden="true"
/> />
) : null} ) : null}
@@ -36,7 +36,7 @@ export function AuthSocialButton({
type={type} type={type}
disabled={disabled} disabled={disabled}
className={[ className={[
"inline-flex min-h-[var(--auth-social-button-height)] w-full cursor-pointer items-center justify-center gap-md rounded-full border border-auth-border bg-auth-surface px-lg text-[var(--responsive-body)] font-medium text-auth-text-primary transition-[background,opacity] duration-150 enabled:hover:bg-[rgba(0,0,0,0.02)] disabled:cursor-not-allowed disabled:opacity-50", "inline-flex min-h-(--auth-social-button-height) w-full cursor-pointer items-center justify-center gap-md rounded-full border border-auth-border bg-auth-surface px-lg text-(length:--responsive-body) font-medium text-auth-text-primary transition-[background,opacity] duration-150 enabled:hover:bg-[rgba(0,0,0,0.02)] disabled:cursor-not-allowed disabled:opacity-50",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
@@ -29,7 +29,7 @@ export function AuthSocialButtons({
googleSlot, googleSlot,
}: AuthSocialButtonsProps) { }: AuthSocialButtonsProps) {
const buttonClassName = const buttonClassName =
"inline-flex min-h-[var(--button-height)] w-full cursor-pointer items-center justify-center gap-sm rounded-full border border-chat-input-border bg-[rgba(255,255,255,0.04)] px-lg text-[var(--responsive-body)] font-medium text-text-primary transition-colors duration-150 enabled:hover:bg-[rgba(255,255,255,0.08)] disabled:cursor-not-allowed disabled:opacity-50"; "inline-flex min-h-(--button-height) w-full cursor-pointer items-center justify-center gap-sm rounded-full border border-chat-input-border bg-[rgba(255,255,255,0.04)] px-lg text-(length:--responsive-body) font-medium text-text-primary transition-colors duration-150 enabled:hover:bg-[rgba(255,255,255,0.08)] disabled:cursor-not-allowed disabled:opacity-50";
const iconClassName = "inline-flex size-5 items-center justify-center"; const iconClassName = "inline-flex size-5 items-center justify-center";
return ( return (
@@ -47,7 +47,7 @@ export function AuthSocialButtons({
</button> </button>
{googleSlot ? ( {googleSlot ? (
<div className="flex min-h-[var(--button-height)] w-full justify-center"> <div className="flex min-h-(--button-height) w-full justify-center">
{googleSlot} {googleSlot}
</div> </div>
) : ( ) : (
+3 -3
View File
@@ -43,7 +43,7 @@ export const AuthTextField = forwardRef<HTMLInputElement, AuthTextFieldProps>(
ref, ref,
) { ) {
const inputClassName = [ const inputClassName = [
"min-h-[var(--auth-field-height)] w-full rounded-full border-0 bg-auth-surface py-0 pl-xxl pr-lg text-[var(--responsive-body)] text-auth-text-primary shadow-[0_3px_5px_var(--color-auth-input-shadow)] outline-none transition-shadow duration-150 placeholder:text-auth-input-placeholder focus:shadow-[0_3px_5px_var(--color-auth-input-shadow),0_0_0_2px_var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-70", "min-h-(--auth-field-height) w-full rounded-full border-0 bg-auth-surface py-0 pl-xxl pr-lg text-(length:--responsive-body) text-auth-text-primary shadow-[0_3px_5px_var(--color-auth-input-shadow)] outline-none transition-shadow duration-150 placeholder:text-auth-input-placeholder focus:shadow-[0_3px_5px_var(--color-auth-input-shadow),0_0_0_2px_var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-70",
errorMessage errorMessage
? "shadow-[0_3px_5px_rgba(255,77,79,0.18),0_0_0_1px_var(--color-error)] focus:shadow-[0_3px_5px_rgba(255,77,79,0.18),0_0_0_2px_var(--color-error)]" ? "shadow-[0_3px_5px_rgba(255,77,79,0.18),0_0_0_1px_var(--color-error)] focus:shadow-[0_3px_5px_rgba(255,77,79,0.18),0_0_0_2px_var(--color-error)]"
: "", : "",
@@ -54,7 +54,7 @@ export const AuthTextField = forwardRef<HTMLInputElement, AuthTextFieldProps>(
return ( return (
<label className="flex w-full flex-col gap-xs"> <label className="flex w-full flex-col gap-xs">
{label ? ( {label ? (
<span className="text-[var(--responsive-caption)] text-text-secondary"> <span className="text-(length:--responsive-caption) text-text-secondary">
{label} {label}
</span> </span>
) : null} ) : null}
@@ -79,7 +79,7 @@ export const AuthTextField = forwardRef<HTMLInputElement, AuthTextFieldProps>(
}} }}
/> />
{errorMessage ? ( {errorMessage ? (
<span className="text-[var(--responsive-caption)] text-error"> <span className="text-(length:--responsive-caption) text-error">
{errorMessage} {errorMessage}
</span> </span>
) : null} ) : null}
@@ -29,7 +29,7 @@ describe("chat Tailwind components", () => {
); );
expect(aiHtml).toContain('aria-label="AI avatar"'); expect(aiHtml).toContain('aria-label="AI avatar"');
expect(aiHtml).toContain("size-[var(--chat-avatar-size,43px)]"); expect(aiHtml).toContain("size-(--chat-avatar-size,43px)");
expect(aiHtml).toContain("size-full object-cover"); expect(aiHtml).toContain("size-full object-cover");
expect(aiHtml).toContain("%2Fimages%2Fchat%2Fpic-chat-elio.png"); expect(aiHtml).toContain("%2Fimages%2Fchat%2Fpic-chat-elio.png");
expect(userHtml).toContain('aria-label="User avatar"'); expect(userHtml).toContain('aria-label="User avatar"');
@@ -97,7 +97,7 @@ describe("chat Tailwind components", () => {
); );
expect(activeHtml).toContain('aria-label="Send message"'); expect(activeHtml).toContain('aria-label="Send message"');
expect(activeHtml).toContain("size-[var(--chat-send-button-size,40px)]"); expect(activeHtml).toContain("size-(--chat-send-button-size,40px)");
expect(activeHtml).toContain( expect(activeHtml).toContain(
"bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]", "bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]",
); );
@@ -124,7 +124,7 @@ describe("chat Tailwind components", () => {
expect(openableHtml).toContain("rounded-tl-none"); expect(openableHtml).toContain("rounded-tl-none");
expect(openableHtml).toContain("block h-auto w-full object-cover"); expect(openableHtml).toContain("block h-auto w-full object-cover");
expect(openableHtml).toContain("data:image/png;base64,/chat-image.png"); expect(openableHtml).toContain("data:image/png;base64,/chat-image.png");
expect(paywalledHtml).toContain("scale-[1.04] blur-[8px]"); expect(paywalledHtml).toContain("scale-104 blur-sm");
expect(paywalledHtml).toContain( expect(paywalledHtml).toContain(
"bg-[linear-gradient(180deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.14)_100%)]", "bg-[linear-gradient(180deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.14)_100%)]",
); );
@@ -149,7 +149,7 @@ describe("chat Tailwind components", () => {
expect(html).toContain("Credits keep the conversation going."); expect(html).toContain("Credits keep the conversation going.");
expect(html).toContain('aria-label="Top up"'); expect(html).toContain('aria-label="Top up"');
expect(html).toContain("bg-[linear-gradient(135deg,#f96ade_0%,#f657a0_100%)]"); expect(html).toContain("bg-[linear-gradient(135deg,#f96ade_0%,#f657a0_100%)]");
expect(html).toContain("active:scale-[0.98]"); expect(html).toContain("active:scale-98");
}); });
it("renders ChatHeader guest and member branches", () => { it("renders ChatHeader guest and member branches", () => {
@@ -159,12 +159,12 @@ describe("chat Tailwind components", () => {
); );
expect(guestHtml).toContain('aria-label="Sign up to unlock more features"'); expect(guestHtml).toContain('aria-label="Sign up to unlock more features"');
expect(guestHtml).toContain("bg-[var(--color-accent,#f84d96)]"); expect(guestHtml).toContain("bg-(--color-accent,#f84d96)");
expect(guestHtml).toContain("size-[var(--icon-size-sm,16px)]"); expect(guestHtml).toContain("size-(--icon-size-sm,16px)");
expect(memberHtml).toContain("Offer"); expect(memberHtml).toContain("Offer");
expect(memberHtml).toContain('aria-label="Menu"'); expect(memberHtml).toContain('aria-label="Menu"');
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]"); expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
expect(memberHtml).toContain("size-[var(--icon-size-xl,32px)]"); expect(memberHtml).toContain("size-(--icon-size-xl,32px)");
}); });
it("renders ChatInputTextField with textarea utilities", () => { it("renders ChatInputTextField with textarea utilities", () => {
@@ -179,10 +179,10 @@ describe("chat Tailwind components", () => {
expect(html).toContain("<textarea"); expect(html).toContain("<textarea");
expect(html).toContain('aria-label="Message"'); expect(html).toContain('aria-label="Message"');
expect(html).toContain("min-h-[var(--chat-send-button-size,40px)]"); expect(html).toContain("min-h-(--chat-send-button-size,40px)");
expect(html).toContain("max-h-[min(30vh,120px)]"); expect(html).toContain("max-h-[min(30vh,120px)]");
expect(html).toContain("caret-[var(--color-accent,#f84d96)]"); expect(html).toContain("caret-(--color-accent,#f84d96)");
expect(html).toContain("placeholder:text-[var(--color-text-hint,#757575)]"); expect(html).toContain("placeholder:text-(--color-text-hint,#757575)");
expect(html).toContain("Message Elio"); expect(html).toContain("Message Elio");
expect(html).toContain("Hello"); expect(html).toContain("Hello");
}); });
@@ -202,8 +202,8 @@ describe("chat Tailwind components", () => {
expect(html).toContain('aria-expanded="true"'); expect(html).toContain('aria-expanded="true"');
expect(html).toContain('aria-label="Open elsewhere. Better experience"'); expect(html).toContain('aria-label="Open elsewhere. Better experience"');
expect(html).toContain("grid-cols-[auto_minmax(0,1fr)]"); expect(html).toContain("grid-cols-[auto_minmax(0,1fr)]");
expect(html).toContain("backdrop-blur-[12px]"); expect(html).toContain("backdrop-blur-md");
expect(html).toContain("hover:opacity-[0.94]"); expect(html).toContain("hover:opacity-94");
expect(html).toContain("Open elsewhere"); expect(html).toContain("Open elsewhere");
expect(html).toContain("Better experience"); expect(html).toContain("Better experience");
}); });
@@ -217,10 +217,10 @@ describe("chat Tailwind components", () => {
expect(html).toContain('aria-modal="true"'); expect(html).toContain('aria-modal="true"');
expect(html).toContain('aria-labelledby="pwa-dialog-title"'); expect(html).toContain('aria-labelledby="pwa-dialog-title"');
expect(html).toContain("fixed inset-0"); expect(html).toContain("fixed inset-0");
expect(html).toContain("max-w-[var(--pwa-install-dialog-max-width,360px)]"); expect(html).toContain("max-w-(--pwa-install-dialog-max-width,360px)");
expect(html).toContain("size-[var(--icon-size-86,86px)]"); expect(html).toContain("size-(--icon-size-86,86px)");
expect(html).toContain("min-h-[var(--pwa-button-height,44px)]"); expect(html).toContain("min-h-(--pwa-button-height,44px)");
expect(html).toContain("text-[var(--color-pwa-button-text,#ffffff)]"); expect(html).toContain("text-(--color-pwa-button-text,#ffffff)");
expect(html).toContain("%2Fimages%2Ficons%2Ficon-add-to-home.png"); expect(html).toContain("%2Fimages%2Ficons%2Ficon-add-to-home.png");
expect(html).toContain("Add to Home Screen"); expect(html).toContain("Add to Home Screen");
expect(html).toContain("Cancel"); expect(html).toContain("Cancel");
@@ -251,12 +251,12 @@ describe("chat Tailwind components", () => {
); );
expect(html).toContain('aria-labelledby="history-unlock-title"'); expect(html).toContain('aria-labelledby="history-unlock-title"');
expect(html).toContain("z-[200]"); expect(html).toContain("z-200");
expect(html).toContain("Unlock previous messages?"); expect(html).toContain("Unlock previous messages?");
expect(html).toContain("We found 3 locked messages"); expect(html).toContain("We found 3 locked messages");
expect(html).toContain("Could not unlock history."); expect(html).toContain("Could not unlock history.");
expect(html).toContain("Unlocking..."); expect(html).toContain("Unlocking...");
expect(html).toContain("text-[var(--color-pwa-button-text,#ffffff)]"); expect(html).toContain("text-(--color-pwa-button-text,#ffffff)");
expect(html).toContain("disabled"); expect(html).toContain("disabled");
}); });
@@ -286,12 +286,12 @@ describe("chat Tailwind components", () => {
); );
expect(html).toContain('aria-labelledby="insufficient-credits-title"'); expect(html).toContain('aria-labelledby="insufficient-credits-title"');
expect(html).toContain("z-[220]"); expect(html).toContain("z-220");
expect(html).toContain("Not enough credits"); expect(html).toContain("Not enough credits");
expect(html).toContain("Balance"); expect(html).toContain("Balance");
expect(html).toContain("Required"); expect(html).toContain("Required");
expect(html).toContain("Still needed"); expect(html).toContain("Still needed");
expect(html).toContain("text-[var(--color-pwa-button-text,#ffffff)]"); expect(html).toContain("text-(--color-pwa-button-text,#ffffff)");
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");
}); });
@@ -318,7 +318,7 @@ describe("chat Tailwind components", () => {
); );
expect(html).toContain('aria-labelledby="voice-unlock-options-title"'); expect(html).toContain('aria-labelledby="voice-unlock-options-title"');
expect(html).toContain("z-[72]"); expect(html).toContain("z-72");
expect(html).toContain("Unlock voice message"); expect(html).toContain("Unlock voice message");
expect(html).toContain("购买语音包"); expect(html).toContain("购买语音包");
expect(html).toContain("开通 VIP"); expect(html).toContain("开通 VIP");
@@ -30,7 +30,7 @@ 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 = 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)]"; "pointer-events-auto absolute right-(--spacing-2,8px) top-(--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-md backdrop-saturate-120 transition-[border-color,box-shadow,opacity,padding,transform] duration-180 [-webkit-tap-highlight-color:transparent] hover:-translate-y-px hover:border-[rgba(255,255,255,0.28)] hover:opacity-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-99 active:opacity-88 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[rgba(255,255,255,0.88)]";
const OVERLAY_EXPANDED_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"; "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 = const OVERLAY_COLLAPSED_CLASS_NAME =
@@ -90,7 +90,7 @@ export function BrowserHintOverlay({
onClick={handleClick} onClick={handleClick}
> >
<span <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]" 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"
aria-hidden="true" aria-hidden="true"
> >
<ExternalLink size={15} strokeWidth={2.4} /> <ExternalLink size={15} strokeWidth={2.4} />
@@ -103,7 +103,7 @@ export function BrowserHintOverlay({
} }
aria-hidden={!isExpanded} aria-hidden={!isExpanded}
> >
<span className="break-words text-[var(--font-size-sm,12px)] font-extrabold leading-[1.25] text-white"> <span className="break-words text-(length:--font-size-sm,12px) font-extrabold leading-[1.25] text-white">
{title} {title}
</span> </span>
<span className="break-words text-[11px] font-semibold leading-[1.2] text-[rgba(255,255,255,0.72)]"> <span className="break-words text-[11px] font-semibold leading-[1.2] text-[rgba(255,255,255,0.72)]">
+5 -5
View File
@@ -24,12 +24,12 @@ export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
<header className="flex shrink-0 flex-col items-stretch bg-transparent p-0"> <header className="flex shrink-0 flex-col items-stretch bg-transparent p-0">
<button <button
type="button" type="button"
className="flex w-full cursor-pointer items-center justify-center gap-[var(--spacing-sm,8px)] border-0 bg-[var(--color-accent,#f84d96)] px-[var(--spacing-md,12px)] py-[var(--spacing-sm,8px)] text-center text-[var(--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-(--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"
onClick={() => navigator.openAuth(ROUTES.chat)} onClick={() => navigator.openAuth(ROUTES.chat)}
aria-label="Sign up to unlock more features" aria-label="Sign up to unlock more features"
> >
<Lock <Lock
className="size-[var(--icon-size-sm,16px)]" className="size-(--icon-size-sm,16px)"
size={16} size={16}
aria-hidden="true" aria-hidden="true"
/> />
@@ -43,16 +43,16 @@ export function ChatHeader({ isGuest, offerBanner }: ChatHeaderProps) {
<header className="flex shrink-0 flex-col items-stretch bg-transparent p-0"> <header className="flex shrink-0 flex-col items-stretch bg-transparent p-0">
{offerBanner} {offerBanner}
<div className="ml-[var(--spacing-md,12px)] flex items-center justify-between px-[var(--spacing-md,12px)] py-[var(--spacing-sm,8px)]"> <div className="ml-(--spacing-md,12px) flex items-center justify-between px-(--spacing-md,12px) py-(--spacing-sm,8px)">
{/* 菜单按钮(点击 → push 到 /sidebar,与 Dart MenuButton 一致) */} {/* 菜单按钮(点击 → push 到 /sidebar,与 Dart MenuButton 一致) */}
<button <button
type="button" type="button"
className="flex cursor-pointer items-center justify-center rounded-[var(--radius-full,999px)] border-0 bg-[rgba(13,11,20,0.7)] p-[var(--spacing-sm,8px)] text-[var(--color-text-primary,#fff)]" className="flex cursor-pointer items-center justify-center rounded-(--radius-full,999px) border-0 bg-[rgba(13,11,20,0.7)] p-(--spacing-sm,8px) text-(--color-text-primary,#fff)"
onClick={() => navigator.push(ROUTES.sidebar)} onClick={() => navigator.push(ROUTES.sidebar)}
aria-label="Menu" aria-label="Menu"
> >
<Menu <Menu
className="size-[var(--icon-size-xl,32px)]" className="size-(--icon-size-xl,32px)"
size={24} size={24}
aria-hidden="true" aria-hidden="true"
/> />
@@ -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)"> <div className="flex min-w-0 flex-auto items-center rounded-full bg-white px-(--spacing-lg,16px)">
<textarea <textarea
ref={innerRef} ref={innerRef}
className="min-h-[var(--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-[var(--color-text-foreground,#000)] caret-[var(--color-accent,#f84d96)] outline-none placeholder:text-[var(--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-(--color-accent,#f84d96) outline-none placeholder:text-(--color-text-hint,#757575)"
value={value} value={value}
onChange={(e: FormEvent<HTMLTextAreaElement>) => onChange={(e: FormEvent<HTMLTextAreaElement>) =>
onChange(e.currentTarget.value) onChange(e.currentTarget.value)
@@ -49,12 +49,12 @@ export function ChatInsufficientCreditsBanner({
return ( return (
<div <div
className="mx-[var(--chat-inline-padding,16px)] mb-[var(--page-section-gap,16px)] shrink-0 rounded-[var(--responsive-card-radius-sm,24px)] bg-[linear-gradient(135deg,#f96ade_0%,#f657a0_100%)] px-[var(--responsive-card-padding,16px)] py-[var(--page-section-gap-lg,24px)] text-center shadow-[0_4px_16px_rgba(248,77,150,0.25)]" className="mx-(--chat-inline-padding,16px) mb-(--page-section-gap,16px) shrink-0 rounded-(--responsive-card-radius-sm,24px) bg-[linear-gradient(135deg,#f96ade_0%,#f657a0_100%)] px-(--responsive-card-padding,16px) py-(--page-section-gap-lg,24px) text-center shadow-[0_4px_16px_rgba(248,77,150,0.25)]"
role="status" role="status"
aria-live="polite" aria-live="polite"
data-testid="chat-insufficient-credits-banner" data-testid="chat-insufficient-credits-banner"
> >
<p className="m-0 mb-[var(--spacing-sm,8px)] text-[var(--responsive-card-title,18px)] font-semibold leading-[1.4] text-white opacity-95"> <p className="m-0 mb-(--spacing-sm,8px) text-(length:--responsive-card-title,18px) font-semibold leading-[1.4] text-white opacity-95">
{titleLines.map((line, index) => ( {titleLines.map((line, index) => (
<span key={`${line}-${index}`}> <span key={`${line}-${index}`}>
{line} {line}
@@ -63,13 +63,13 @@ export function ChatInsufficientCreditsBanner({
))} ))}
</p> </p>
{description ? ( {description ? (
<p className="mx-auto mb-[var(--spacing-lg,16px)] mt-0 max-w-[min(100%,360px)] text-[var(--responsive-caption,14px)] font-semibold leading-[1.4] text-[rgba(255,255,255,0.88)]"> <p className="mx-auto mb-(--spacing-lg,16px) mt-0 max-w-[min(100%,360px)] text-(length:--responsive-caption,14px) font-semibold leading-[1.4] text-[rgba(255,255,255,0.88)]">
{description} {description}
</p> </p>
) : null} ) : null}
<button <button
type="button" type="button"
className="min-h-[var(--responsive-control-height,48px)] w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(135deg,#ffb8e0_0%,#f57ec0_100%)] px-[var(--responsive-card-padding-lg,24px)] text-[var(--responsive-card-title,18px)] font-bold text-white shadow-[0_2px_8px_rgba(0,0,0,0.15)] transition-[box-shadow,transform] duration-150 hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] active:scale-[0.98]" className="min-h-(--responsive-control-height,48px) w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(135deg,#ffb8e0_0%,#f57ec0_100%)] px-(--responsive-card-padding-lg,24px) text-(length:--responsive-card-title,18px) font-bold text-white shadow-[0_2px_8px_rgba(0,0,0,0.15)] transition-[box-shadow,transform] duration-150 hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)] active:scale-98"
onClick={handleClick} onClick={handleClick}
aria-label={ctaLabel} aria-label={ctaLabel}
> >
+2 -2
View File
@@ -30,7 +30,7 @@ export function ChatSendButton({
<button <button
type="button" type="button"
className={[ className={[
"flex aspect-square size-[var(--chat-send-button-size,40px)] shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-[var(--color-button-gradient-end,#fc69df)] text-white transition-[background,transform] duration-200 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]", "flex aspect-square size-(--chat-send-button-size,40px) shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-(--color-button-gradient-end,#fc69df) text-white transition-[background,transform] duration-200 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]",
isActive isActive
? "bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]" ? "bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))]"
: "bg-[#f8a8ce] text-[rgba(255,255,255,0.88)] shadow-none", : "bg-[#f8a8ce] text-[rgba(255,255,255,0.88)] shadow-none",
@@ -49,7 +49,7 @@ export function ChatSendButton({
aria-label="Send message" aria-label="Send message"
> >
<ArrowUp <ArrowUp
className="size-[var(--icon-size-xl,24px)] text-[length:var(--icon-size-xl,24px)] leading-none" className="size-(--icon-size-xl,24px) text-(length:--icon-size-xl,24px) leading-none"
size={24} size={24}
aria-hidden="true" aria-hidden="true"
/> />
@@ -21,31 +21,31 @@ export function HistoryUnlockDialog({
return ( return (
<div <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,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="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)]"> <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 <h2
id="history-unlock-title" 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)]" 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)"
> >
Unlock previous messages? Unlock previous messages?
</h2> </h2>
<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]"> <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]">
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="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]"> <p className="mx-(--responsive-card-padding-lg,24px) mb-(--spacing-lg,16px) mt-0 text-left text-(length:--responsive-caption,var(--font-size-md,14px)) leading-[1.4] text-[#c0364c]">
{errorMessage} {errorMessage}
</p> </p>
) : null} ) : null}
<div className="flex w-full gap-[var(--spacing-md,12px)]"> <div className="flex w-full gap-(--spacing-md,12px)">
<button <button
type="button" type="button"
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" 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"
onClick={onClose} onClick={onClose}
disabled={isLoading} disabled={isLoading}
> >
@@ -53,7 +53,7 @@ export function HistoryUnlockDialog({
</button> </button>
<button <button
type="button" type="button"
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" 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"
onClick={onConfirm} onClick={onConfirm}
disabled={isLoading} disabled={isLoading}
> >
+3 -3
View File
@@ -26,7 +26,7 @@ export function ImageBubble({
const canOpen = Boolean(messageId && onOpenImage); const canOpen = Boolean(messageId && onOpenImage);
const imageClassName = [ const imageClassName = [
"block h-auto w-full object-cover", "block h-auto w-full object-cover",
imagePaywalled ? "scale-[1.04] blur-[8px]" : "", imagePaywalled ? "scale-104 blur-sm" : "",
] ]
.filter(Boolean) .filter(Boolean)
.join(" "); .join(" ");
@@ -38,7 +38,7 @@ export function ImageBubble({
return ( return (
<div <div
className="relative max-h-[var(--chat-media-size,220px)] max-w-[var(--chat-media-size,220px)] cursor-pointer overflow-hidden rounded-[var(--radius-lg,12px)] rounded-tl-none bg-[var(--color-bubble-background,#fff)]" 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)"
onClick={openImage} onClick={openImage}
role={canOpen ? "button" : undefined} role={canOpen ? "button" : undefined}
tabIndex={canOpen ? 0 : undefined} tabIndex={canOpen ? 0 : undefined}
@@ -55,7 +55,7 @@ export function ImageBubble({
messageId={messageId} messageId={messageId}
remoteUrl={imageUrl} remoteUrl={imageUrl}
className={imageClassName} className={imageClassName}
errorClassName="flex size-[var(--chat-media-size,220px)] items-center justify-center bg-[var(--color-bubble-background,#fff)] text-[var(--icon-size-xl,24px)] text-[var(--color-text-secondary,#9e9e9e)]" errorClassName="flex size-(--chat-media-size,220px) items-center justify-center bg-(--color-bubble-background,#fff) text-(length:--icon-size-xl,24px) text-(--color-text-secondary,#9e9e9e)"
width={240} width={240}
height={240} height={240}
> >
@@ -23,56 +23,56 @@ export function InsufficientCreditsDialog({
return ( return (
<div <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,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="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)]"> <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 <h2
id="insufficient-credits-title" 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)]" 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)"
> >
Not enough credits Not enough credits
</h2> </h2>
<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]"> <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]">
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="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)]"> <dl className="mb-(--page-section-gap,18px) grid gap-(--spacing-sm,8px) rounded-(--responsive-card-radius-sm,22px) bg-[#fff4f9] p-(--spacing-md,12px)">
<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]"> <div className="flex items-center justify-between gap-(--spacing-md,12px) text-(length:--responsive-caption,var(--font-size-md,14px)) leading-[1.3] text-[#4a3b43]">
<dt className="m-0 font-medium">Balance</dt> <dt className="m-0 font-medium">Balance</dt>
<dd className="m-0 text-[var(--responsive-body,var(--font-size-lg,16px))] font-extrabold text-[#f657a0]"> <dd className="m-0 text-(length:--responsive-body,var(--font-size-lg,16px)) font-extrabold text-[#f657a0]">
{creditBalance} {creditBalance}
</dd> </dd>
</div> </div>
<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]"> <div className="flex items-center justify-between gap-(--spacing-md,12px) text-(length:--responsive-caption,var(--font-size-md,14px)) leading-[1.3] text-[#4a3b43]">
<dt className="m-0 font-medium">Required</dt> <dt className="m-0 font-medium">Required</dt>
<dd className="m-0 text-[var(--responsive-body,var(--font-size-lg,16px))] font-extrabold text-[#f657a0]"> <dd className="m-0 text-(length:--responsive-body,var(--font-size-lg,16px)) font-extrabold text-[#f657a0]">
{requiredCredits} {requiredCredits}
</dd> </dd>
</div> </div>
<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]"> <div className="flex items-center justify-between gap-(--spacing-md,12px) text-(length:--responsive-caption,var(--font-size-md,14px)) leading-[1.3] text-[#4a3b43]">
<dt className="m-0 font-medium">Still needed</dt> <dt className="m-0 font-medium">Still needed</dt>
<dd className="m-0 text-[var(--responsive-body,var(--font-size-lg,16px))] font-extrabold text-[#f657a0]"> <dd className="m-0 text-(length:--responsive-body,var(--font-size-lg,16px)) font-extrabold text-[#f657a0]">
{shortfallCredits} {shortfallCredits}
</dd> </dd>
</div> </div>
</dl> </dl>
) : null} ) : null}
<div className="flex w-full gap-[var(--spacing-md,12px)]"> <div className="flex w-full gap-(--spacing-md,12px)">
<button <button
type="button" type="button"
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]" 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]"
onClick={onClose} onClick={onClose}
> >
Later Later
</button> </button>
<button <button
type="button" type="button"
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]" 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]"
onClick={onConfirm} onClick={onConfirm}
> >
Top up now Top up now
@@ -15,7 +15,7 @@ export function LockedImageMessageCard({
}: LockedImageMessageCardProps) { }: LockedImageMessageCardProps) {
return ( return (
<div <div
className="w-[min(72vw,280px)] overflow-hidden rounded-[var(--responsive-card-radius-sm,18px)] rounded-tl-none border border-[rgba(246,87,160,0.2)] bg-white shadow-[0_4px_14px_rgba(246,87,160,0.14)]" className="w-[min(72vw,280px)] overflow-hidden rounded-(--responsive-card-radius-sm,18px) rounded-tl-none border border-[rgba(246,87,160,0.2)] bg-white shadow-[0_4px_14px_rgba(246,87,160,0.14)]"
role="group" role="group"
aria-label="Locked private image" aria-label="Locked private image"
> >
@@ -25,15 +25,15 @@ export function LockedImageMessageCard({
<LockKeyhole size={19} aria-hidden="true" /> <LockKeyhole size={19} aria-hidden="true" />
</span> </span>
</div> </div>
<div className="p-[var(--responsive-card-padding,14px)]"> <div className="p-(--responsive-card-padding,14px)">
<p className="m-0 text-[var(--responsive-body,14px)] leading-[1.4] text-[#3c3b3b]"> <p className="m-0 text-(length:--responsive-body,14px) leading-[1.4] text-[#3c3b3b]">
{hint && hint.length > 0 {hint && hint.length > 0
? hint ? hint
: "Elio sent you a locked image."} : "Elio sent you a locked image."}
</p> </p>
<button <button
type="button" type="button"
className="mt-[var(--spacing-md,12px)] w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(90deg,#ff67e0,#ff52a2)] px-[var(--spacing-md,12px)] py-[clamp(9px,1.852vw,10px)] text-[var(--responsive-body,14px)] font-bold text-white disabled:cursor-not-allowed disabled:opacity-65 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]" className="mt-(--spacing-md,12px) w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(90deg,#ff67e0,#ff52a2)] px-(--spacing-md,12px) py-[clamp(9px,1.852vw,10px)] text-(length:--responsive-body,14px) font-bold text-white disabled:cursor-not-allowed disabled:opacity-65 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
disabled={isUnlocking || !onUnlock} disabled={isUnlocking || !onUnlock}
onClick={onUnlock} onClick={onUnlock}
> >
+1 -1
View File
@@ -9,7 +9,7 @@ export interface MessageAvatarProps {
} }
const AVATAR_CLASS_NAME = const AVATAR_CLASS_NAME =
"flex size-[var(--chat-avatar-size,43px)] shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-[var(--color-avatar-border,#fbf3f5)] bg-[var(--color-avatar-border,#fbf3f5)] shadow-[var(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))]"; "flex size-(--chat-avatar-size,43px) shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-(--color-avatar-border,#fbf3f5) bg-(--color-avatar-border,#fbf3f5) shadow-(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))";
export function MessageAvatar({ isFromAI, userAvatarUrl }: MessageAvatarProps) { export function MessageAvatar({ isFromAI, userAvatarUrl }: MessageAvatarProps) {
if (isFromAI) { if (isFromAI) {
@@ -15,24 +15,24 @@ export function PrivateMessageCard({
}: PrivateMessageCardProps) { }: PrivateMessageCardProps) {
return ( return (
<div <div
className="max-w-[min(72vw,280px)] rounded-[var(--responsive-card-radius-sm,18px)] rounded-tl-none border border-[rgba(246,87,160,0.2)] bg-[linear-gradient(180deg,rgba(255,244,248,0.95),rgba(255,255,255,0.95)),#ffffff] p-[var(--responsive-card-padding,14px)] text-[#3c3b3b] shadow-[0_4px_12px_rgba(246,87,160,0.12)]" className="max-w-[min(72vw,280px)] rounded-(--responsive-card-radius-sm,18px) rounded-tl-none border border-[rgba(246,87,160,0.2)] bg-[linear-gradient(180deg,rgba(255,244,248,0.95),rgba(255,255,255,0.95)),#ffffff] p-(--responsive-card-padding,14px) text-[#3c3b3b] shadow-[0_4px_12px_rgba(246,87,160,0.12)]"
role="group" role="group"
aria-label="Locked private message" aria-label="Locked private message"
> >
<div <div
className="mb-[clamp(8px,1.852vw,10px)] flex size-[var(--responsive-icon-button-size,42px)] items-center justify-center rounded-full bg-[linear-gradient(135deg,#ff8fc7_0%,#f657a0_100%)] text-white" className="mb-[clamp(8px,1.852vw,10px)] flex size-(--responsive-icon-button-size,42px) items-center justify-center rounded-full bg-[linear-gradient(135deg,#ff8fc7_0%,#f657a0_100%)] text-white"
aria-hidden="true" aria-hidden="true"
> >
<LockKeyhole className="block" size={22} /> <LockKeyhole className="block" size={22} />
</div> </div>
<p className="m-0 text-[var(--responsive-body,14px)] leading-[1.4] text-[#3c3b3b]"> <p className="m-0 text-(length:--responsive-body,14px) leading-[1.4] text-[#3c3b3b]">
{hint && hint.length > 0 {hint && hint.length > 0
? hint ? hint
: "Elio has a private message for you."} : "Elio has a private message for you."}
</p> </p>
<button <button
type="button" type="button"
className="mt-[var(--spacing-md,12px)] w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(90deg,#ff67e0,#ff52a2)] px-[var(--spacing-md,12px)] py-[clamp(9px,1.852vw,10px)] text-[var(--responsive-body,14px)] font-bold text-white disabled:cursor-not-allowed disabled:opacity-65 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]" className="mt-(--spacing-md,12px) w-full cursor-pointer rounded-full border-0 bg-[linear-gradient(90deg,#ff67e0,#ff52a2)] px-(--spacing-md,12px) py-[clamp(9px,1.852vw,10px)] text-(length:--responsive-body,14px) font-bold text-white disabled:cursor-not-allowed disabled:opacity-65 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
disabled={isUnlocking || !onUnlock} disabled={isUnlocking || !onUnlock}
onClick={onUnlock} onClick={onUnlock}
> >
@@ -34,9 +34,9 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
aria-modal="true" aria-modal="true"
aria-labelledby="pwa-dialog-title" aria-labelledby="pwa-dialog-title"
> >
<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)]"> <div className="flex w-full max-w-(--pwa-install-dialog-max-width,360px) flex-col items-center 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,20px) text-center shadow-[0_10px_20px_rgba(0,0,0,0.1)]">
<Image <Image
className="mb-[var(--spacing-lg,16px)] size-[var(--icon-size-86,86px)] object-contain" className="mb-(--spacing-lg,16px) size-(--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}
@@ -45,24 +45,24 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
/> />
<h2 <h2
id="pwa-dialog-title" 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)]" 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)"
> >
Add to Home Screen Add to Home Screen
</h2> </h2>
<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]"> <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]">
Add CozSweet to Home Screen{"\n"}for the best experience Add CozSweet to Home Screen{"\n"}for the best experience
</p> </p>
<div className="flex w-full gap-[var(--spacing-md,12px)]"> <div className="flex w-full gap-(--spacing-md,12px)">
<button <button
type="button" type="button"
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)]" 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)"
onClick={onClose} onClick={onClose}
> >
Cancel Cancel
</button> </button>
<button <button
type="button" type="button"
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)]" 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)"
onClick={handleInstall} onClick={handleInstall}
> >
OK OK
+2 -2
View File
@@ -15,10 +15,10 @@ export interface TextBubbleProps {
} }
const BASE_CLASS_NAME = const BASE_CLASS_NAME =
"w-full self-stretch whitespace-pre-wrap break-words rounded-[var(--radius-xxl,24px)] px-[var(--spacing-4,16px)] py-[var(--spacing-3,12px)] text-[length:var(--chat-message-font-size,var(--responsive-body,16px))] leading-[var(--chat-message-line-height,1.5)] shadow-[var(--shadow-input-box,0_1px_2px_rgba(0,0,0,0.1))]"; "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))";
const AI_CLASS_NAME = const AI_CLASS_NAME =
"rounded-tl-none bg-white text-[var(--color-text-foreground,#000)]"; "rounded-tl-none bg-white text-(--color-text-foreground,#000)";
const USER_CLASS_NAME = const USER_CLASS_NAME =
"rounded-tr-none bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-white"; "rounded-tr-none bg-[linear-gradient(to_right,var(--color-button-gradient-start,#ff67e0),var(--color-button-gradient-end,#ff52a2))] text-white";
@@ -17,37 +17,37 @@ export function VoiceUnlockOptionsDialog({
return ( return (
<div <div
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))]" 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="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)]"> <div className="w-full max-w-(--dialog-max-width,380px) rounded-(--responsive-card-radius,36px) 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)]">
<h2 <h2
id="voice-unlock-options-title" 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)]" className="m-0 mb-[clamp(7px,1.481vw,8px)] text-center text-(length:--responsive-page-title,var(--font-size-22,22px)) font-bold leading-[1.2] text-(--color-text-foreground,#171717)"
> >
Unlock voice message Unlock voice message
</h2> </h2>
<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]"> <p className="mx-(--spacing-md,12px) mb-(--page-section-gap-lg,20px) mt-0 text-center text-(length:--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="flex flex-col gap-[var(--spacing-md,12px)]"> <div className="flex flex-col gap-(--spacing-md,12px)">
<button <button
type="button" type="button"
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]" className="flex min-h-(--responsive-control-height,48px) w-full cursor-pointer flex-col items-center justify-center gap-(--responsive-inline-gap-xs,4px) rounded-(--radius-bottom-sheet,28px) border-0 bg-[linear-gradient(90deg,#ff67e0_0%,#ff52a2_100%)] px-(--responsive-card-padding,16px) py-[clamp(9px,1.852vw,10px)] text-(length:--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="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]" className="flex min-h-(--responsive-control-height,48px) w-full cursor-pointer flex-col items-center justify-center gap-(--responsive-inline-gap-xs,4px) rounded-(--radius-bottom-sheet,28px) border border-transparent bg-[linear-gradient(#ffffff,#ffffff)_padding-box,linear-gradient(90deg,#ff67e0,#ff52a2)_border-box] px-(--responsive-card-padding,16px) py-[clamp(9px,1.852vw,10px)] text-(length:--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="leading-[1.2]"> VIP</span> <span className="leading-[1.2]"> VIP</span>
<span className="text-[var(--responsive-caption,13px)] font-medium leading-[1.25] text-[#3c3b3b]"> <span className="text-(length:--responsive-caption,13px) font-medium leading-[1.25] text-[#3c3b3b]">
10+ 10+
</span> </span>
</button> </button>
@@ -55,7 +55,7 @@ export function VoiceUnlockOptionsDialog({
<button <button
type="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]" className="mt-[clamp(12px,2.593vw,14px)] min-h-(--responsive-control-height,48px) w-full cursor-pointer rounded-(--radius-bottom-sheet,28px) border-0 bg-(--color-text-secondary,#9e9e9e) text-(length:--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} onClick={onClose}
> >
Cancel Cancel
@@ -24,7 +24,7 @@ describe("splash Tailwind components", () => {
it("renders SplashLogo with Tailwind wrapper and image classes", () => { it("renders SplashLogo with Tailwind wrapper and image classes", () => {
const html = renderToStaticMarkup(<SplashLogo />); const html = renderToStaticMarkup(<SplashLogo />);
expect(html).toContain("z-[2]"); expect(html).toContain("z-2");
expect(html).toContain("inline-flex"); expect(html).toContain("inline-flex");
expect(html).toContain("block h-auto w-auto"); expect(html).toContain("block h-auto w-auto");
expect(html).toContain("%2Fimages%2Fsplash%2Fic-logo-home.png"); expect(html).toContain("%2Fimages%2Fsplash%2Fic-logo-home.png");
@@ -43,7 +43,7 @@ describe("splash Tailwind components", () => {
const html = renderToStaticMarkup(<SplashContent />); const html = renderToStaticMarkup(<SplashContent />);
expect(html).toContain("flex flex-col gap-md"); expect(html).toContain("flex flex-col gap-md");
expect(html).toContain("font-[var(--font-athelas)]"); expect(html).toContain("font-(family-name:--font-athelas)");
expect(html).toContain("whitespace-pre-line"); expect(html).toContain("whitespace-pre-line");
expect(html).toContain("Welcome to my secret hideout"); expect(html).toContain("Welcome to my secret hideout");
}); });
@@ -67,8 +67,8 @@ describe("splash Tailwind components", () => {
<SplashButton onStartChat={() => undefined} />, <SplashButton onStartChat={() => undefined} />,
); );
expect(readyHtml).toContain("z-[2]"); expect(readyHtml).toContain("z-2");
expect(readyHtml).toContain("max-w-[480px]"); expect(readyHtml).toContain("max-w-120");
expect(readyHtml).toContain("bg-[linear-gradient(to_right"); expect(readyHtml).toContain("bg-[linear-gradient(to_right");
expect(readyHtml).toContain("Start Chatting"); expect(readyHtml).toContain("Start Chatting");
expect(loadingHtml).toContain("disabled"); expect(loadingHtml).toContain("disabled");
+3 -3
View File
@@ -14,17 +14,17 @@ export function SplashButton({ onStartChat }: SplashButtonProps) {
const isLoading = !state.hasInitialized || state.isLoading; const isLoading = !state.hasInitialized || state.isLoading;
return ( return (
<div className="z-[2] flex w-full flex-row items-center justify-center gap-[clamp(var(--spacing-md,12px),4.815vw,var(--spacing-26,26px))] px-[clamp(var(--spacing-sm,8px),2.963vw,var(--spacing-lg,16px))]"> <div className="z-2 flex w-full flex-row items-center justify-center gap-[clamp(var(--spacing-md,12px),4.815vw,var(--spacing-26,26px))] px-[clamp(var(--spacing-sm,8px),2.963vw,var(--spacing-lg,16px))]">
<button <button
type="button" type="button"
onClick={onStartChat} onClick={onStartChat}
disabled={isLoading} disabled={isLoading}
className="inline-flex min-h-[var(--responsive-control-height,48px)] w-full max-w-[480px] flex-auto cursor-pointer items-center justify-center rounded-[var(--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)] disabled:cursor-not-allowed disabled:opacity-70" className="inline-flex min-h-(--responsive-control-height,48px) w-full max-w-120 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)] disabled:cursor-not-allowed disabled:opacity-70"
> >
{isLoading ? ( {isLoading ? (
<LoadingIndicator color="#ffffff" /> <LoadingIndicator color="#ffffff" />
) : ( ) : (
<span className="whitespace-nowrap text-[var(--responsive-section-title,var(--font-size-xxl))] font-bold text-white"> <span className="whitespace-nowrap text-(length:--responsive-section-title,var(--font-size-xxl)) font-bold text-white">
Start Chatting Start Chatting
</span> </span>
)} )}
+2 -2
View File
@@ -6,8 +6,8 @@ const APOSTROPHE = "'";
export function SplashContent() { export function SplashContent() {
return ( return (
<div className="z-[2] flex flex-col gap-md"> <div className="z-2 flex flex-col gap-md">
<h1 className="m-0 whitespace-pre-line text-left font-[var(--font-athelas)] text-[clamp(28px,7.8vw,30px)] font-bold italic leading-[1.28] text-white"> <h1 className="m-0 whitespace-pre-line text-left font-(family-name:--font-athelas) text-[clamp(28px,7.8vw,30px)] font-bold italic leading-[1.28] text-white">
Welcome to my secret hideout~ Welcome to my secret hideout~
{"\n"}It{APOSTROPHE}s just the two of us now. {"\n"}It{APOSTROPHE}s just the two of us now.
{"\n"}Feel free to whisper your {"\n"}Feel free to whisper your
+1 -1
View File
@@ -2,7 +2,7 @@ import Image from "next/image";
export function SplashLogo() { export function SplashLogo() {
return ( return (
<div className="z-[2] inline-flex"> <div className="z-2 inline-flex">
<Image <Image
src="/images/splash/ic-logo-home.png" src="/images/splash/ic-logo-home.png"
alt="cozsweet" alt="cozsweet"