refactor(app): remove scaffold and migrate input styles

This commit is contained in:
2026-07-13 10:43:18 +08:00
parent 181e0aee19
commit 8e0c4ef95f
6 changed files with 23 additions and 140 deletions
-1
View File
@@ -9,5 +9,4 @@ export * from "./dialog";
export * from "./loading-indicator";
export * from "./mobile-shell";
export * from "./page-loading-fallback";
export * from "./page-scaffold";
export * from "./responsive-mobile-shell";
@@ -1,50 +0,0 @@
.root {
position: relative;
display: flex;
flex: 1 1 auto;
min-height: var(--app-viewport-height, 100dvh);
min-width: 0;
flex-direction: column;
box-sizing: border-box;
color: var(--color-text-foreground, #171717);
overflow: hidden;
}
.root[data-scrollable="true"] {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.padded {
padding:
calc(var(--page-padding-y, 18px) + var(--app-safe-top, 0px))
calc(var(--page-padding-x, 20px) + var(--app-safe-right, 0px))
calc(var(--page-padding-y, 18px) + var(--app-safe-bottom, 0px))
calc(var(--page-padding-x, 20px) + var(--app-safe-left, 0px));
}
.header,
.footer {
position: relative;
z-index: 1;
flex: 0 0 auto;
}
.body {
position: relative;
z-index: 1;
display: flex;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
flex-direction: column;
}
.body[data-scrollable="true"] {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.footer {
padding-bottom: var(--app-safe-bottom, 0px);
}
@@ -1,49 +0,0 @@
import type { ReactNode } from "react";
import styles from "./page-scaffold.module.css";
export interface PageScaffoldProps {
children: ReactNode;
header?: ReactNode;
footer?: ReactNode;
className?: string;
bodyClassName?: string;
padded?: boolean;
scrollable?: boolean;
bodyScrollable?: boolean;
}
/**
* Shared page layout primitive for mobile-first screens.
*
* Use it when migrating feature pages away from hand-written shell/padding
* styles. It keeps safe-area padding and scroll ownership in one place.
*/
export function PageScaffold({
children,
header,
footer,
className,
bodyClassName,
padded = true,
scrollable = false,
bodyScrollable = false,
}: PageScaffoldProps) {
return (
<main
className={[styles.root, padded ? styles.padded : "", className]
.filter(Boolean)
.join(" ")}
data-scrollable={scrollable}
>
{header ? <div className={styles.header}>{header}</div> : null}
<div
className={[styles.body, bodyClassName].filter(Boolean).join(" ")}
data-scrollable={bodyScrollable}
>
{children}
</div>
{footer ? <div className={styles.footer}>{footer}</div> : null}
</main>
);
}
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest";
import { ChatHeader } from "../chat-header";
import { ChatInsufficientCreditsBanner } from "../chat-insufficient-credits-banner";
import { ChatInputTextField } from "../chat-input-text-field";
import { ChatSendButton } from "../chat-send-button";
import { ImageBubble } from "../image-bubble";
import { MessageAvatar } from "../message-avatar";
@@ -160,4 +161,24 @@ describe("chat Tailwind components", () => {
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
expect(memberHtml).toContain("size-[var(--icon-size-xl,32px)]");
});
it("renders ChatInputTextField with textarea utilities", () => {
const html = renderToStaticMarkup(
<ChatInputTextField
value="Hello"
onChange={() => undefined}
onSubmit={() => undefined}
placeholder="Message Elio"
/>,
);
expect(html).toContain("<textarea");
expect(html).toContain('aria-label="Message"');
expect(html).toContain("min-h-[var(--chat-send-button-size,40px)]");
expect(html).toContain("max-h-[min(30vh,120px)]");
expect(html).toContain("caret-[var(--color-accent,#f84d96)]");
expect(html).toContain("placeholder:text-[var(--color-text-hint,#757575)]");
expect(html).toContain("Message Elio");
expect(html).toContain("Hello");
});
});
@@ -1,36 +0,0 @@
/* ChatInputTextField 输入框样式(外层白底圆角容器 + 内部 textarea */
/* 外层圆角容器(与 Dart `Container(borderRadius: AppRadius.xxxl ≈ 32)` 一致) */
.wrap {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
padding: 0 var(--spacing-lg, 16px); /* Dart: horizontal AppSpacing.lg */
background: #fff; /* Dart: Colors.white */
border-radius: var(--radius-full, 999px);
}
/* textarea 本身(裸元素,背景透明让外层白底透出) */
.textField {
flex: 1 1 auto;
min-width: 0;
width: 100%;
min-height: var(--chat-send-button-size, 40px);
max-height: min(30vh, 120px); /* 约 5 行 × 24px line-height */
padding: clamp(5px, 1.111vw, 6px) 0 0 0; /* 让文字视觉下移,避开正中位置 */
border: 0;
outline: none;
background: transparent;
color: var(--color-text-foreground, #000); /* Dart: Colors.black */
caret-color: var(--color-accent, #f84d96); /* Dart: AppColors.accentColor */
/* iOS Facebook IAB/Safari auto-zooms focused text controls below 16px. */
font-size: 16px;
line-height: clamp(22px, 4.444vw, 24px);
resize: none;
font-family: inherit;
}
.textField::placeholder {
color: var(--color-text-hint, #757575);
}
@@ -22,8 +22,6 @@ import {
useRef,
} from "react";
import styles from "./chat-input-text-field.module.css";
export interface ChatInputTextFieldProps {
value: string;
onChange: (value: string) => void;
@@ -62,10 +60,10 @@ export const ChatInputTextField = forwardRef<
};
return (
<div className={styles.wrap}>
<div className="flex min-w-0 flex-auto items-center rounded-full bg-white px-(--spacing-lg,16px)">
<textarea
ref={innerRef}
className={styles.textField}
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)]"
value={value}
onChange={(e: FormEvent<HTMLTextAreaElement>) =>
onChange(e.currentTarget.value)