feat(textarea): 新增多行文本组件
- rows / resize / invalid,样式与 Input 一致(焦点只换边框) - 契约、展示页(#inputs)、README 计数 40 → 41、变更记录同步
This commit is contained in:
@@ -34,7 +34,7 @@ node scripts/screenshot.mjs http://127.0.0.1:5199/ shots/cards.png '#cards' #
|
|||||||
| 文档 | 内容 | 什么时候看 |
|
| 文档 | 内容 | 什么时候看 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| [DESIGN.md](DESIGN.md) | **规格总纲**:分层结构、设计令牌、视觉规则、代码约定、验收清单、决策与已知坑 | 动手前、评审时 |
|
| [DESIGN.md](DESIGN.md) | **规格总纲**:分层结构、设计令牌、视觉规则、代码约定、验收清单、决策与已知坑 | 动手前、评审时 |
|
||||||
| [docs/COMPONENTS.md](docs/COMPONENTS.md) | **组件目录与契约**:现有 40 个组件的 API 与边界、组合模式、候选组件、图标新增流程 | 想知道「用哪个 / 有没有 / 缺什么」 |
|
| [docs/COMPONENTS.md](docs/COMPONENTS.md) | **组件目录与契约**:现有 41 个组件的 API 与边界、组合模式、候选组件、图标新增流程 | 想知道「用哪个 / 有没有 / 缺什么」 |
|
||||||
| [docs/ADDING-A-COMPONENT.md](docs/ADDING-A-COMPONENT.md) | **新增组件手册**:12 步流程、代码模板、`Spinner` 完整示例、常见错误 | 要补一个组件时照着做 |
|
| [docs/ADDING-A-COMPONENT.md](docs/ADDING-A-COMPONENT.md) | **新增组件手册**:12 步流程、代码模板、`Spinner` 完整示例、常见错误 | 要补一个组件时照着做 |
|
||||||
| [docs/ALRIGHT-DECOMPOSITION.md](docs/ALRIGHT-DECOMPOSITION.md) | **参考站拆解**:实测令牌、组件尺寸、交互节奏、暗色映射、保留差异 | 想核对「这个值对不对」 |
|
| [docs/ALRIGHT-DECOMPOSITION.md](docs/ALRIGHT-DECOMPOSITION.md) | **参考站拆解**:实测令牌、组件尺寸、交互节奏、暗色映射、保留差异 | 想核对「这个值对不对」 |
|
||||||
| [docs/STYLE-INTAKE.md](docs/STYLE-INTAKE.md) | **外部风格接入**:截图 → 组件/令牌映射表、决策矩阵、验收清单 | 新项目给了外部设计稿、和现有风格不一致时 |
|
| [docs/STYLE-INTAKE.md](docs/STYLE-INTAKE.md) | **外部风格接入**:截图 → 组件/令牌映射表、决策矩阵、验收清单 | 新项目给了外部设计稿、和现有风格不一致时 |
|
||||||
@@ -120,6 +120,7 @@ stash-ui/
|
|||||||
| `Space` | 元素间距 | `direction` `gap` `wrap` `align` `justify` |
|
| `Space` | 元素间距 | `direction` `gap` `wrap` `align` `justify` |
|
||||||
| `Divider` | 分隔线 | `orientation` `variant` `spacing` |
|
| `Divider` | 分隔线 | `orientation` `variant` `spacing` |
|
||||||
| `Input` | 输入框 | `icon` `clearable` `onClear` `shortcut` `inputRef` |
|
| `Input` | 输入框 | `icon` `clearable` `onClear` `shortcut` `inputRef` |
|
||||||
|
| `Textarea` | 多行文本(Markdown / 简介) | `rows` `resize` `invalid` |
|
||||||
| `Notice` | 提示条 | `tone` `title` `action` |
|
| `Notice` | 提示条 | `tone` `title` `action` |
|
||||||
| `Empty` | 空态 | `text` `action` |
|
| `Empty` | 空态 | `text` `action` |
|
||||||
| `Result` | 整块结果反馈 | `status` `title` `description` `actions` |
|
| `Result` | 整块结果反馈 | `status` `title` `description` `actions` |
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
| `Button` | L2 | ✅ | 操作按钮(圆角 / 胶囊) | `variant` `size` `shape` `block` `loading` `icon` |
|
| `Button` | L2 | ✅ | 操作按钮(圆角 / 胶囊) | `variant` `size` `shape` `block` `loading` `icon` |
|
||||||
| `IconButton` | L2 | ✅ | 圆形图标按钮(28 / 36 / 44) | `label`(必填) `icon` `variant` `size` `spinning` |
|
| `IconButton` | L2 | ✅ | 圆形图标按钮(28 / 36 / 44) | `label`(必填) `icon` `variant` `size` `spinning` |
|
||||||
| `Input` | L2 | ✅ | 组合式输入框 | `icon` `clearable` `onClear` `shortcut` `inputRef` |
|
| `Input` | L2 | ✅ | 组合式输入框 | `icon` `clearable` `onClear` `shortcut` `inputRef` |
|
||||||
|
| `Textarea` | L2 | ✅ | 多行文本(Markdown / 简介) | `rows` `resize` `invalid` |
|
||||||
| `Switch` | L2 | ✅ | 即时生效的开关 | `checked` `onChange` `size` `label` |
|
| `Switch` | L2 | ✅ | 即时生效的开关 | `checked` `onChange` `size` `label` |
|
||||||
| `Checkbox` | L2 | ✅ | 复选框(多选 / 确认) | `checked` `onChange` `label` |
|
| `Checkbox` | L2 | ✅ | 复选框(多选 / 确认) | `checked` `onChange` `label` |
|
||||||
| `RadioGroup` | L2 | ✅ | 互斥单选组 | `options` `value` `onChange` `direction` |
|
| `RadioGroup` | L2 | ✅ | 互斥单选组 | `options` `value` `onChange` `direction` |
|
||||||
@@ -569,6 +570,14 @@
|
|||||||
- **规则**:错误用文案而不是只换颜色(颜色不足以表达);`defaultValue` / `value` 等仍由控件自己管。
|
- **规则**:错误用文案而不是只换颜色(颜色不足以表达);`defaultValue` / `value` 等仍由控件自己管。
|
||||||
- **展示页**:`#inputs`
|
- **展示页**:`#inputs`
|
||||||
|
|
||||||
|
### 3.41 Textarea ✅
|
||||||
|
|
||||||
|
- **用途**:多行文本——Markdown 正文、简介、备注。
|
||||||
|
- **不该用在哪**:单行短文本(用 `Input`);需要工具栏的富文本(页面自己拼)。
|
||||||
|
- **Props**:透传 `<textarea>`,另加 `rows`(默认 4)、`resize: 'none' | 'vertical'`(默认 `vertical`)、`invalid`(红边框 + `aria-invalid`)。
|
||||||
|
- **规则**:焦点只用边框变色,不叠 outline(与 `Input` 一致);`invalid` 要和错误文案一起用。
|
||||||
|
- **展示页**:`#inputs`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 组合模式(不进库,但必须照这个拼)
|
## 4. 组合模式(不进库,但必须照这个拼)
|
||||||
@@ -667,3 +676,4 @@
|
|||||||
| 2026-10-28 | **修复**:`Input` 聚焦不再叠 outline(文本框点击会命中 `:focus-visible`,导致边框 + 外环「双框」;现在只用边框变主色)。`Layout` 增加 `hideScrollbar`(内部滚动条可隐藏)。影响面:所有 `Input` 页面视觉(去掉了多余外环)、`Layout screen` 页面可选用 |
|
| 2026-10-28 | **修复**:`Input` 聚焦不再叠 outline(文本框点击会命中 `:focus-visible`,导致边框 + 外环「双框」;现在只用边框变主色)。`Layout` 增加 `hideScrollbar`(内部滚动条可隐藏)。影响面:所有 `Input` 页面视觉(去掉了多余外环)、`Layout screen` 页面可选用 |
|
||||||
| 2026-10-28 | **补齐剩余能力**:新增 `Image`(比例 / 填充 / 失败降级)与 `FormField`(标签 + 提示 + 错误,不做校验);`Button` 增加 `size="lg"`(44);展示页补 `Title weight`、`Avatar xl/tone/icon`、`Cover lift`、`Menu itemHeight`、`Layout screen/hideScrollbar`、`Input size`、`FormField` 示例;新增 `themes/example.css` 主题示例(只改变量) |
|
| 2026-10-28 | **补齐剩余能力**:新增 `Image`(比例 / 填充 / 失败降级)与 `FormField`(标签 + 提示 + 错误,不做校验);`Button` 增加 `size="lg"`(44);展示页补 `Title weight`、`Avatar xl/tone/icon`、`Cover lift`、`Menu itemHeight`、`Layout screen/hideScrollbar`、`Input size`、`FormField` 示例;新增 `themes/example.css` 主题示例(只改变量) |
|
||||||
| 2026-10-29 | `Breadcrumb` 增加 `onNavigate`(可选):传了就用 `preventDefault` 接管点击,交给 SPA 路由;不传保持普通链接。兼容性:新增可选 prop,无破坏性 |
|
| 2026-10-29 | `Breadcrumb` 增加 `onNavigate`(可选):传了就用 `preventDefault` 接管点击,交给 SPA 路由;不传保持普通链接。兼容性:新增可选 prop,无破坏性 |
|
||||||
|
| 2026-10-29 | 新增 `Textarea`(`rows` / `resize` / `invalid`),与 `Input` 共用边框与焦点样式。兼容性:纯新增 |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button, FormField, Icon, Input, Space, Text } from '../../src'
|
import { Button, FormField, Icon, Input, Space, Text, Textarea } from '../../src'
|
||||||
import { Demo } from '../components/Demo'
|
import { Demo } from '../components/Demo'
|
||||||
import { PropsTable } from '../components/PropsTable'
|
import { PropsTable } from '../components/PropsTable'
|
||||||
import { Section } from '../components/Section'
|
import { Section } from '../components/Section'
|
||||||
@@ -120,6 +120,19 @@ export function Inputs() {
|
|||||||
</div>
|
</div>
|
||||||
</Demo>
|
</Demo>
|
||||||
|
|
||||||
|
<Demo
|
||||||
|
title="多行文本"
|
||||||
|
desc="Textarea 用于 Markdown 正文、简介这类长文本;行数可调,默认只允许竖向拖拽。"
|
||||||
|
layout="column"
|
||||||
|
code={`<Textarea rows={5} placeholder="支持 Markdown…" />
|
||||||
|
<Textarea rows={3} resize="none" invalid />`}
|
||||||
|
>
|
||||||
|
<div style={{ width: 420, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
<Textarea rows={5} placeholder="写点什么,支持 Markdown:## 标题、**粗体**、[链接](url)" />
|
||||||
|
<Textarea rows={3} resize="none" invalid placeholder="校验失败时的样子(invalid)" />
|
||||||
|
</div>
|
||||||
|
</Demo>
|
||||||
|
|
||||||
<PropsTable
|
<PropsTable
|
||||||
rows={[
|
rows={[
|
||||||
{ name: 'icon', type: 'IconName', desc: '左侧前缀图标' },
|
{ name: 'icon', type: 'IconName', desc: '左侧前缀图标' },
|
||||||
@@ -132,6 +145,18 @@ export function Inputs() {
|
|||||||
{ name: '…rest', type: 'InputHTMLAttributes', desc: '透传原生属性,onChange 是原生事件' },
|
{ name: '…rest', type: 'InputHTMLAttributes', desc: '透传原生属性,onChange 是原生事件' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Text size="sm" tone="muted" style={{ marginTop: 24 }}>
|
||||||
|
Textarea
|
||||||
|
</Text>
|
||||||
|
<PropsTable
|
||||||
|
rows={[
|
||||||
|
{ name: 'rows', type: 'number', defaultValue: '4', desc: '初始可见行数' },
|
||||||
|
{ name: 'resize', type: "'none' | 'vertical'", defaultValue: "'vertical'", desc: '是否允许拖拽改高' },
|
||||||
|
{ name: 'invalid', type: 'boolean', defaultValue: 'false', desc: '红边框 + aria-invalid' },
|
||||||
|
{ name: '…rest', type: 'TextareaHTMLAttributes', desc: '透传原生属性' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.root {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 96px;
|
||||||
|
padding: var(--su-space-2) var(--su-space-3);
|
||||||
|
border: 1px solid var(--su-border-strong);
|
||||||
|
border-radius: var(--su-radius-sm);
|
||||||
|
background-color: var(--su-bg-card);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--su-font-size-md);
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--su-text-main);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color var(--su-duration-base) var(--su-ease-in-out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 与 Input 一致:焦点只换边框颜色,不叠 outline */
|
||||||
|
.root:focus {
|
||||||
|
border-color: var(--su-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root::placeholder {
|
||||||
|
color: var(--su-text-placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--su-bg-subtle);
|
||||||
|
color: var(--su-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
border-color: var(--su-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.none {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { TextareaHTMLAttributes } from 'react'
|
||||||
|
import styles from './Textarea.module.css'
|
||||||
|
|
||||||
|
/** 多行文本:Markdown 正文、简介、备注。样式与 `Input` 保持一致,只有高度是自适应的。 */
|
||||||
|
export type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||||
|
/** 初始可见行数,默认 4 */
|
||||||
|
rows?: number
|
||||||
|
/** 禁止手动拖拽改高度(需要固定版式时用) */
|
||||||
|
resize?: 'none' | 'vertical'
|
||||||
|
/** 校验失败态:红边框 */
|
||||||
|
invalid?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Textarea({
|
||||||
|
rows = 4,
|
||||||
|
resize = 'vertical',
|
||||||
|
invalid = false,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: TextareaProps) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
{...rest}
|
||||||
|
rows={rows}
|
||||||
|
aria-invalid={invalid || undefined}
|
||||||
|
className={[styles.root, styles[resize], invalid ? styles.invalid : '', className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,6 +51,8 @@ export type { ImageFit, ImageProps, ImageRatio } from './components/Image'
|
|||||||
|
|
||||||
export { FormField } from './components/FormField'
|
export { FormField } from './components/FormField'
|
||||||
export type { FormFieldProps } from './components/FormField'
|
export type { FormFieldProps } from './components/FormField'
|
||||||
|
export { Textarea } from './components/Textarea'
|
||||||
|
export type { TextareaProps } from './components/Textarea'
|
||||||
|
|
||||||
export { Icon } from './components/Icon'
|
export { Icon } from './components/Icon'
|
||||||
export type { IconName, IconProps } from './components/Icon'
|
export type { IconName, IconProps } from './components/Icon'
|
||||||
|
|||||||
Reference in New Issue
Block a user