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 "./loading-indicator";
|
||||||
export * from "./mobile-shell";
|
export * from "./mobile-shell";
|
||||||
export * from "./page-loading-fallback";
|
export * from "./page-loading-fallback";
|
||||||
export * from "./page-scaffold";
|
|
||||||
export * from "./responsive-mobile-shell";
|
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 { ChatHeader } from "../chat-header";
|
||||||
import { ChatInsufficientCreditsBanner } from "../chat-insufficient-credits-banner";
|
import { ChatInsufficientCreditsBanner } from "../chat-insufficient-credits-banner";
|
||||||
|
import { ChatInputTextField } from "../chat-input-text-field";
|
||||||
import { ChatSendButton } from "../chat-send-button";
|
import { ChatSendButton } from "../chat-send-button";
|
||||||
import { ImageBubble } from "../image-bubble";
|
import { ImageBubble } from "../image-bubble";
|
||||||
import { MessageAvatar } from "../message-avatar";
|
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("bg-[rgba(13,11,20,0.7)]");
|
||||||
expect(memberHtml).toContain("size-[var(--icon-size-xl,32px)]");
|
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,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import styles from "./chat-input-text-field.module.css";
|
|
||||||
|
|
||||||
export interface ChatInputTextFieldProps {
|
export interface ChatInputTextFieldProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
@@ -62,10 +60,10 @@ export const ChatInputTextField = forwardRef<
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrap}>
|
<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={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}
|
value={value}
|
||||||
onChange={(e: FormEvent<HTMLTextAreaElement>) =>
|
onChange={(e: FormEvent<HTMLTextAreaElement>) =>
|
||||||
onChange(e.currentTarget.value)
|
onChange(e.currentTarget.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user