feat(ui): add PasswordInput(锁图标 + 显隐切换)
- Input + IconButton 组合,type 切换、aria-pressed、尺寸对齐内建 - 展示页补「密码框」示例与 props 表;COMPONENTS.md / README 同步 - 版本 0.3.0
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) | **组件目录与契约**:现有 41 个组件的 API 与边界、组合模式、候选组件、图标新增流程 | 想知道「用哪个 / 有没有 / 缺什么」 |
|
| [docs/COMPONENTS.md](docs/COMPONENTS.md) | **组件目录与契约**:现有 42 个组件的 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) | **外部风格接入**:截图 → 组件/令牌映射表、决策矩阵、验收清单 | 新项目给了外部设计稿、和现有风格不一致时 |
|
||||||
@@ -121,6 +121,7 @@ stash-ui/
|
|||||||
| `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` |
|
| `Textarea` | 多行文本(Markdown / 简介) | `rows` `resize` `invalid` |
|
||||||
|
| `PasswordInput` | 密码输入(锁图标 + 显隐切换) | `icon` `size` `toggle` |
|
||||||
| `Notice` | 提示条 | `tone` `title` `action` |
|
| `Notice` | 提示条 | `tone` `title` `action` |
|
||||||
| `Empty` | 空态 | `text` `action` |
|
| `Empty` | 空态 | `text` `action` |
|
||||||
| `Result` | 整块结果反馈 | `status` `title` `description` `actions` |
|
| `Result` | 整块结果反馈 | `status` `title` `description` `actions` |
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
| `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` |
|
| `Textarea` | L2 | ✅ | 多行文本(Markdown / 简介) | `rows` `resize` `invalid` |
|
||||||
|
| `PasswordInput` | L2 | ✅ | 密码输入(锁图标 + 显隐切换) | `icon` `size` `toggle` |
|
||||||
| `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` |
|
||||||
@@ -77,6 +78,7 @@
|
|||||||
| 主操作 | `Button`(一页一个 primary) | 多个 primary 并列 |
|
| 主操作 | `Button`(一页一个 primary) | 多个 primary 并列 |
|
||||||
| 登录 / 筛选这类主操作 | `Button shape="pill"` | 圆角按钮凑合 |
|
| 登录 / 筛选这类主操作 | `Button shape="pill"` | 圆角按钮凑合 |
|
||||||
| 只有图标的操作 | `IconButton label="…"` | `<button><Icon /></button>` |
|
| 只有图标的操作 | `IconButton label="…"` | `<button><Icon /></button>` |
|
||||||
|
| 密码输入(登录 / 改密) | `PasswordInput`(锁图标 + 显隐切换) | 自己拼 `Input` + 眼睛按钮(容易漏掉切换) |
|
||||||
| 分隔内容块 | `Divider` | `border-top` 手写 |
|
| 分隔内容块 | `Divider` | `border-top` 手写 |
|
||||||
| 长内容回到顶部 | `ScrollTop` | 自己写 fixed 按钮 |
|
| 长内容回到顶部 | `ScrollTop` | 自己写 fixed 按钮 |
|
||||||
|
|
||||||
@@ -578,6 +580,24 @@
|
|||||||
- **规则**:焦点只用边框变色,不叠 outline(与 `Input` 一致);`invalid` 要和错误文案一起用。
|
- **规则**:焦点只用边框变色,不叠 outline(与 `Input` 一致);`invalid` 要和错误文案一起用。
|
||||||
- **展示页**:`#inputs`
|
- **展示页**:`#inputs`
|
||||||
|
|
||||||
|
### 3.42 PasswordInput ✅
|
||||||
|
|
||||||
|
- **用途**:登录、改密这类密码输入;固定「锁图标 + 眼睛按钮」,显隐切换与无障碍名称由组件负责。
|
||||||
|
- **不该用在哪**:普通文本输入(用 `Input`);需要自己控制 `type` 的场景(直接用 `Input`)。
|
||||||
|
- **解剖**:外层 flex(`Input` 撑满 + 右侧 `IconButton`),`type` 在 `password` / `text` 间切换。
|
||||||
|
- **Props**:
|
||||||
|
|
||||||
|
| prop | 类型 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `icon` | `IconName \| null` | `'lock_line'` | 左侧图标;`null` 去掉 |
|
||||||
|
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | 32 / 36 / 44,与 `Input` 一致 |
|
||||||
|
| `toggle` | `boolean` | `true` | 是否显示右侧显隐按钮 |
|
||||||
|
| `className` / `style` | `string` / `CSSProperties` | — | 作用在最外层 |
|
||||||
|
| `…rest` | `InputHTMLAttributes` | — | 透传原生属性(`type` / `size` / `icon` 由组件接管) |
|
||||||
|
|
||||||
|
- **规则**:`onChange` 是原生事件(与 `Input` 一致),Enter 提交用 `onKeyDown`;切换按钮必须带 `aria-pressed` 与「显示/隐藏密码」名称,焦点环不得移除。
|
||||||
|
- **展示页**:`#inputs`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 组合模式(不进库,但必须照这个拼)
|
## 4. 组合模式(不进库,但必须照这个拼)
|
||||||
@@ -677,3 +697,4 @@
|
|||||||
| 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` 共用边框与焦点样式。兼容性:纯新增 |
|
| 2026-10-29 | 新增 `Textarea`(`rows` / `resize` / `invalid`),与 `Input` 共用边框与焦点样式。兼容性:纯新增 |
|
||||||
|
| 2026-10-29 | 新增 `PasswordInput`(`icon` / `size` / `toggle`):锁图标 + 显隐切换 + `aria-pressed`,事件语义与 `Input` 一致。兼容性:纯新增 |
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@stash/ui",
|
"name": "@stash/ui",
|
||||||
"version": "0.1.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Stash 设计系统:设计令牌 + 基础组件 + 组件展示站",
|
"description": "Stash 设计系统:设计令牌 + 基础组件 + 组件展示站",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+2
-2
@@ -147,7 +147,7 @@ export function App() {
|
|||||||
<span className="doc-brand-mark">S</span>
|
<span className="doc-brand-mark">S</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>Stash UI</strong>
|
<strong>Stash UI</strong>
|
||||||
<em>v0.1.0</em>
|
<em>v0.3.0</em>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ export function App() {
|
|||||||
组件只依赖令牌,不依赖任何页面数据,所以换页面、换应用都能直接拼。
|
组件只依赖令牌,不依赖任何页面数据,所以换页面、换应用都能直接拼。
|
||||||
</p>
|
</p>
|
||||||
<div className="doc-hero-meta">
|
<div className="doc-hero-meta">
|
||||||
<span>40 个基础组件</span>
|
<span>42 个基础组件</span>
|
||||||
<span>零运行时依赖</span>
|
<span>零运行时依赖</span>
|
||||||
<span>CSS Modules + 设计令牌</span>
|
<span>CSS Modules + 设计令牌</span>
|
||||||
<span>对齐 Alright 参考站</span>
|
<span>对齐 Alright 参考站</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Button, FormField, Icon, Input, Space, Text, Textarea } from '../../src'
|
import { Button, FormField, Icon, Input, PasswordInput, 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'
|
||||||
@@ -8,6 +8,7 @@ export function Inputs() {
|
|||||||
const [keyword, setKeyword] = useState('')
|
const [keyword, setKeyword] = useState('')
|
||||||
const [host, setHost] = useState('127.0.0.1:8080')
|
const [host, setHost] = useState('127.0.0.1:8080')
|
||||||
const [email, setEmail] = useState('not-an-email')
|
const [email, setEmail] = useState('not-an-email')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section
|
<Section
|
||||||
@@ -133,6 +134,31 @@ export function Inputs() {
|
|||||||
</div>
|
</div>
|
||||||
</Demo>
|
</Demo>
|
||||||
|
|
||||||
|
<Demo
|
||||||
|
title="密码框"
|
||||||
|
desc="PasswordInput 在 Input 右侧固定一个眼睛按钮,显隐切换与无障碍名称都封装在组件里;登录、改密这类页面直接用它,不要漏掉切换按钮。"
|
||||||
|
layout="column"
|
||||||
|
code={`<FormField label="密码" htmlFor="demo-password" required hint="至少 8 位">
|
||||||
|
<PasswordInput
|
||||||
|
id="demo-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
placeholder="输入密码"
|
||||||
|
/>
|
||||||
|
</FormField>`}
|
||||||
|
>
|
||||||
|
<div style={{ width: 320 }}>
|
||||||
|
<FormField label="密码" htmlFor="demo-password" required hint="至少 8 位">
|
||||||
|
<PasswordInput
|
||||||
|
id="demo-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
|
placeholder="输入密码"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</Demo>
|
||||||
|
|
||||||
<PropsTable
|
<PropsTable
|
||||||
rows={[
|
rows={[
|
||||||
{ name: 'icon', type: 'IconName', desc: '左侧前缀图标' },
|
{ name: 'icon', type: 'IconName', desc: '左侧前缀图标' },
|
||||||
@@ -157,6 +183,19 @@ export function Inputs() {
|
|||||||
{ name: '…rest', type: 'TextareaHTMLAttributes', desc: '透传原生属性' },
|
{ name: '…rest', type: 'TextareaHTMLAttributes', desc: '透传原生属性' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Text size="sm" tone="muted" style={{ marginTop: 24 }}>
|
||||||
|
PasswordInput
|
||||||
|
</Text>
|
||||||
|
<PropsTable
|
||||||
|
rows={[
|
||||||
|
{ name: 'icon', type: 'IconName | null', defaultValue: "'lock_line'", desc: '左侧图标;null 表示不显示' },
|
||||||
|
{ name: 'size', type: "'sm' | 'md' | 'lg'", defaultValue: "'md'", desc: '32 / 36 / 44px,与 Input 一致' },
|
||||||
|
{ name: 'toggle', type: 'boolean', defaultValue: 'true', desc: '是否显示右侧显隐按钮' },
|
||||||
|
{ name: 'className / style', type: 'string / CSSProperties', desc: '作用在最外层容器上' },
|
||||||
|
{ name: '…rest', type: 'InputHTMLAttributes', desc: '透传原生属性(type / size / icon 由组件接管)' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--su-space-2);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框吃掉剩余宽度,眼睛按钮保持自身尺寸 */
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState, type CSSProperties, type InputHTMLAttributes } from 'react'
|
||||||
|
import { Icon, type IconName } from './Icon'
|
||||||
|
import { IconButton } from './IconButton'
|
||||||
|
import { Input, type InputSize } from './Input'
|
||||||
|
import styles from './PasswordInput.module.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码输入框:`Input` + 右侧显隐切换。
|
||||||
|
*
|
||||||
|
* 为什么单独成组件而不是每个页面拼一遍:密码框的「锁图标 + 眼睛按钮 +
|
||||||
|
* aria 名称 + type 切换」是固定搭配,手拼时很容易漏掉显隐按钮(Stash 的
|
||||||
|
* 系统改密页就漏过一次),而漏掉之后用户无法确认自己输的是什么。
|
||||||
|
*
|
||||||
|
* 事件语义与 `Input` 一致:`onChange` 收到的是原生事件,Enter 等键盘逻辑
|
||||||
|
* 用 `onKeyDown`。输入框本身不知道「密码是否正确」这类业务状态。
|
||||||
|
*/
|
||||||
|
export type PasswordInputProps = {
|
||||||
|
/** 左侧图标,默认 `lock_line`;传 `null` 去掉 */
|
||||||
|
icon?: IconName | null
|
||||||
|
/** 尺寸 32 / 36 / 44,与 `Input` 一致 */
|
||||||
|
size?: InputSize
|
||||||
|
/** 是否显示右侧显隐按钮,默认显示 */
|
||||||
|
toggle?: boolean
|
||||||
|
/** `className` / `style` 作用在最外层(负责宽度与间距) */
|
||||||
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
} & Omit<
|
||||||
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
'type' | 'size' | 'className' | 'style' | 'icon'
|
||||||
|
>
|
||||||
|
|
||||||
|
export function PasswordInput({
|
||||||
|
icon = 'lock_line',
|
||||||
|
size = 'md',
|
||||||
|
toggle = true,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
...rest
|
||||||
|
}: PasswordInputProps) {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
|
// 眼睛按钮的高度跟随输入框,避免并排时上下错位
|
||||||
|
const buttonSize = size === 'lg' ? 'md' : size === 'sm' ? 'xs' : 'sm'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={[styles.root, className].filter(Boolean).join(' ')} style={style}>
|
||||||
|
<Input
|
||||||
|
{...rest}
|
||||||
|
className={styles.input}
|
||||||
|
size={size}
|
||||||
|
type={visible ? 'text' : 'password'}
|
||||||
|
icon={icon ?? undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{toggle && (
|
||||||
|
<IconButton
|
||||||
|
label={visible ? '隐藏密码' : '显示密码'}
|
||||||
|
aria-pressed={visible}
|
||||||
|
size={buttonSize}
|
||||||
|
icon={<Icon name={visible ? 'eye_close_line' : 'eye_line'} />}
|
||||||
|
onClick={() => setVisible((current) => !current)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -64,6 +64,9 @@ export type { IconButtonProps, IconButtonSize } from './components/IconButton'
|
|||||||
export { Input } from './components/Input'
|
export { Input } from './components/Input'
|
||||||
export type { InputProps } from './components/Input'
|
export type { InputProps } from './components/Input'
|
||||||
|
|
||||||
|
export { PasswordInput } from './components/PasswordInput'
|
||||||
|
export type { PasswordInputProps } from './components/PasswordInput'
|
||||||
|
|
||||||
export { Layout } from './components/Layout'
|
export { Layout } from './components/Layout'
|
||||||
export type { LayoutProps } from './components/Layout'
|
export type { LayoutProps } from './components/Layout'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user