refactor(app): remove scaffold and migrate input styles
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user