mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-01 15:32:50 +08:00
style(modal): the designer's three calls from the preview page
Reviewing the live demos on 2026-08-20 turned up three things, two of
which reverse what the spec said:
- The「×」is decided by dialog TYPE, not by whether a 取消 button exists.
A form dialog gets BOTH: 取消 sits by the primary button for someone
who read the whole form and gave up there, the「×」is where a hand
reaches to dismiss a layer. Different places to look — drop either and
half the users hunt for the exit. A confirm dialog still gets neither
beyond its two footer buttons. The old wording ("the「×」is a form
dialog's ONLY exit") effectively banned 取消 on forms.
- A confirm opened on top of a dialog draws its own mask. Without it the
confirm's white card runs into the dialog's white card underneath and
reads as a block embedded in the form rather than a new layer that has
to be answered first. Two 40% masks stack to ~64%; if that lands too
heavy we give the second layer its own lighter value, so
`overlayClassName` stays as an escape hatch instead of being the
standard way to nest.
- The body carries no vertical padding by design, but it is also the
scroll container — and a scroll container clips at its padding box, so
a zero-padding edge shaves the 2px focus ring off whichever field sits
first or last. `py-0.5 -my-0.5` gives the ring its 2px and takes the
same 2px back out of layout; nothing moves.
The component itself only changes in that last line — `closable` already
defaulted to true and 取消 was always the caller's to pass. The demo page
picks up the library's Input/Textarea while it is being rewritten, and
its beforeClose demo now shows the real nested confirm instead of a hint
string.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -93,7 +93,7 @@ BISHENG 前台(client)有很多历史遗留的重复组件——同一种东
|
||||
| 图标规范(Icon) | 🟨 进行中 | lucide 159 文件 / bisheng-icons 39 文件双轨并行(2026-07-16 扫描) | 规范 **v1 已定稿**:bisheng-icons 单一来源(24×24 / 2px),六档尺寸阶梯 + 线宽对照 + 着色/可达性规则;剩:lucide 高频逐批迁移(设计师点名) | [基础-图标规范.mdx](基础-图标规范.mdx) |
|
||||
| 色彩规范(Color) | 🟨 进行中 | 9 套体系并行、裸 hex 2469 处/215 值(2026-07-14 扫描) | Arco 色板规范 **v1 已定稿**;**token 两层 + Tailwind 接线 + 画廊「色彩 Colors」页已落地**(2026-07-15,实际命名见文档 §7.1 落地注:`border` 避让为 `border-base`);剩:逐批迁移(第一优先 = LibreChat 语义 token 重指向 Arco 值) | [基础-色彩规范.md](基础-色彩规范.md) |
|
||||
| 圆角与阴影规范(基础) | 🟨 v1 提案 | 圆角任意值 81 处 12 种、阴影预设 5 档 117 处 + 任意值 94 处 44 种(2026-07-23 扫描,排除 _gallery) | 圆角八档阶梯 + 四条使用原则(尺寸定档 / 同级同档 / 嵌套同心 / 贴边归零,2026-07-31 收拢命名;弹窗定 16px)+ 投影两档 popup/modal(取下拉/弹窗基准组件已落地值);待设计师验收 → 落 token → 逐批归并 | [基础-圆角与阴影规范.mdx](基础-圆角与阴影规范.mdx) |
|
||||
| Modal 弹窗 | 🟨 进行中 | 4(含1个死代码) | 规范 **v1 已定稿**(2026-08-20):尺寸四档 400/600/960/全屏 + 遮罩黑 40% 不加模糊 + 内边距 16(主体只留左右)+ 「×」按有无取消按钮决定 + 手机档一律走全屏档 + 层级四档 1000/1100/1200/1300 在此定稿;**组件库 `@bisheng/ui` 的 `Modal` 已落地**,client 两套旧壳未动、迁移批次待排 | [组件-Modal弹窗.md](组件-Modal弹窗.md) |
|
||||
| Modal 弹窗 | 🟨 进行中 | 4(含1个死代码) | 规范 **v1 已定稿**(2026-08-20):尺寸四档 400/600/960/全屏 + 遮罩黑 40% 不加模糊 + 内边距 16(主体只留左右)+ 「×」按弹窗类型决定(表单类「×」+ 取消都给,确认类都不给) + 手机档一律走全屏档 + 层级四档 1000/1100/1200/1300 在此定稿;**组件库 `@bisheng/ui` 的 `Modal` 已落地**,client 两套旧壳未动、迁移批次待排 | [组件-Modal弹窗.md](组件-Modal弹窗.md) |
|
||||
| Toast 轻提示 | 🟨 进行中 | 组件库 1 套(`@bisheng/ui` Toast,client 全量接线;私有撤销 toast 已删) | 规范 **v1 已定稿** + **基准组件已落地**(2026-08-13):四类型(不做 loading)+ 顶部最多 3 条堆叠去重 + 至多一个操作按钮 + 时长按字数 3~10 秒动态 + 悬停暂停 + 分级播报;404 个调用点经 `showToast` 转接未动;剩:收敛 `severity`/`status` 双入参 → 暗色浅底取值 → z-index 回填 `1200`(层级表已定稿) | [组件-Toast轻提示.md](组件-Toast轻提示.md) |
|
||||
| Drawer 抽屉 | 🟨 v1 已建 | 待扫描(已知 CreateKnowledgeSpaceDrawer / CreateChannelDrawer / VersionHistorySheet 等) | 规范 **v1 已定稿**:宽度三档 400/600/800(不设全屏档)+ 断点逐级降档 + 移动端底部上滑 + 模态/非模态两类;剩:真实页面量宽验收 → 基准组件落地 | [组件-Drawer抽屉.md](组件-Drawer抽屉.md) |
|
||||
| Select / 下拉菜单 | ⬜ 待办 | 多个 | 待定 | 待建 |
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
## 4. 遮罩 Overlay → 已迁出
|
||||
|
||||
2026-08-20 起遮罩规则归 [组件-Modal弹窗.md](组件-Modal弹窗.md) §5:黑色 40% 不透明、不加模糊,同一时间只有一层。旧的「浅黑毛玻璃 vs 灰白毛玻璃」之争就此了结(`bg-gray-500/90` 列为待迁移旧值)。
|
||||
2026-08-20 起遮罩规则归 [组件-Modal弹窗.md](组件-Modal弹窗.md) §5:黑色 40% 不透明、不加模糊;弹窗之上再开二次确认时,第二层遮罩照常叠。旧的「浅黑毛玻璃 vs 灰白毛玻璃」之争就此了结(`bg-gray-500/90` 列为待迁移旧值)。
|
||||
|
||||
## 5. 层级 z-index → 已迁出
|
||||
|
||||
|
||||
@@ -14,17 +14,7 @@ statusNote: 基准壳标准未定稿
|
||||
`title` + 正文 + `footer`。`trigger` 里的元素会自动接上打开事件,也可以不给 `trigger`,改用 `open` / `onOpenChange` 自己控。底部按钮主按钮最右,`ModalClose` 包住的按钮点了就关。
|
||||
|
||||
```tsx
|
||||
import { Button, Modal, ModalClose } from '@bisheng/ui';
|
||||
|
||||
const field = {
|
||||
width: '100%',
|
||||
height: 32,
|
||||
padding: '0 12px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgb(var(--border-base))',
|
||||
background: 'rgb(var(--bg-page))',
|
||||
color: 'rgb(var(--text-1))',
|
||||
};
|
||||
import { Button, Input, Modal, ModalClose } from '@bisheng/ui';
|
||||
|
||||
export default () => (
|
||||
<Modal
|
||||
@@ -35,12 +25,14 @@ export default () => (
|
||||
<ModalClose asChild>
|
||||
<Button color="default" variant="outlined">取消</Button>
|
||||
</ModalClose>
|
||||
<Button>保存</Button>
|
||||
<ModalClose asChild>
|
||||
<Button>保存</Button>
|
||||
</ModalClose>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<label style={{ display: 'block', marginBottom: 8, color: 'rgb(var(--text-2))' }}>名称</label>
|
||||
<input style={field} defaultValue="产品手册" />
|
||||
<Input defaultValue="产品手册" />
|
||||
</Modal>
|
||||
);
|
||||
```
|
||||
@@ -81,7 +73,9 @@ export default () => {
|
||||
<ModalClose asChild>
|
||||
<Button color="default" variant="outlined">取消</Button>
|
||||
</ModalClose>
|
||||
<Button>确定</Button>
|
||||
<ModalClose asChild>
|
||||
<Button>确定</Button>
|
||||
</ModalClose>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -111,7 +105,9 @@ export default () => (
|
||||
<ModalClose asChild>
|
||||
<Button color="default" variant="outlined">暂不同意</Button>
|
||||
</ModalClose>
|
||||
<Button>同意</Button>
|
||||
<ModalClose asChild>
|
||||
<Button>同意</Button>
|
||||
</ModalClose>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -126,7 +122,7 @@ export default () => (
|
||||
|
||||
## 三条关闭路径
|
||||
|
||||
普通弹窗给三条关闭路径,**一条都不能少**:头部的关闭按钮、点击遮罩、按 Esc。关闭「×」按**有没有取消按钮**决定:表单类弹窗必须有(那是它唯一的出口),带取消按钮的确认类用 `closable={false}` 去掉——两个出口说的是同一件事,反而让人犹豫该点哪个。
|
||||
普通弹窗给三条关闭路径,**一条都不能少**:头部的关闭按钮、点击遮罩、按 Esc。关闭「×」按**弹窗类型**决定:**表单类「×」和取消都给**——取消挨着主按钮,是看完整张表后就地放弃的出口,「×」是随手关掉浮层的常规位置;**确认类**用 `closable={false}` 去掉「×」,只留底部两个按钮——问题已经摆在按钮上,右上角再开一个出口只会让人犹豫该点哪个。
|
||||
|
||||
```tsx
|
||||
import { Button, Modal, ModalClose } from '@bisheng/ui';
|
||||
@@ -134,11 +130,20 @@ import { Button, Modal, ModalClose } from '@bisheng/ui';
|
||||
export default () => (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
|
||||
<Modal
|
||||
trigger={<Button color="default" variant="outlined">表单类:有「×」</Button>}
|
||||
trigger={<Button color="default" variant="outlined">表单类:「×」+ 取消</Button>}
|
||||
title="新建频道"
|
||||
footer={<Button>创建</Button>}
|
||||
footer={
|
||||
<>
|
||||
<ModalClose asChild>
|
||||
<Button color="default" variant="outlined">取消</Button>
|
||||
</ModalClose>
|
||||
<ModalClose asChild>
|
||||
<Button>创建</Button>
|
||||
</ModalClose>
|
||||
</>
|
||||
}
|
||||
>
|
||||
表单弹窗的唯一出口就是右上角这个「×」,必须留着。
|
||||
表单类:右上角的「×」和底部的取消都留着——两个出口视线落点不同,缺哪个都会有人找不到。
|
||||
</Modal>
|
||||
<Modal
|
||||
closable={false}
|
||||
@@ -149,7 +154,9 @@ export default () => (
|
||||
<ModalClose asChild>
|
||||
<Button color="default" variant="outlined">继续编辑</Button>
|
||||
</ModalClose>
|
||||
<Button color="danger">放弃</Button>
|
||||
<ModalClose asChild>
|
||||
<Button color="danger">放弃</Button>
|
||||
</ModalClose>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -161,16 +168,17 @@ export default () => (
|
||||
|
||||
## 有未保存内容 / 提交进行中
|
||||
|
||||
填了十分钟的表单,不能因为手滑点到遮罩就没了:`beforeClose` 返回 `false` 就把弹窗留住,业务页自己弹二次确认。提交进行中传 `submitting`,**三条路径全部禁用**,直到接口返回——接口还没回就关掉,用户不知道这一步到底成没成。
|
||||
填了十分钟的表单,不能因为手滑点到遮罩就没了:`beforeClose` 返回 `false` 就把弹窗留住,业务页自己弹二次确认——**打几个字再去点遮罩或按 Esc**,弹窗不会走,跳出来的是「要放弃这次编辑吗」。二次确认带着自己的遮罩叠上去,把下面那个弹窗一起压暗——不叠的话它和下面弹窗的白底连成一片,看着像嵌在表单里的一块(规范 §5)。提交进行中传 `submitting`,**三条路径全部禁用**,直到接口返回——接口还没回就关掉,用户不知道这一步到底成没成。
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
import { Button, Modal, ModalClose } from '@bisheng/ui';
|
||||
import { Button, Modal, ModalClose, Textarea } from '@bisheng/ui';
|
||||
|
||||
export default () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [draft, setDraft] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [discarding, setDiscarding] = useState(false);
|
||||
const [hint, setHint] = useState('');
|
||||
|
||||
return (
|
||||
@@ -184,9 +192,9 @@ export default () => {
|
||||
onOpenChange={setOpen}
|
||||
title="填写反馈"
|
||||
submitting={submitting}
|
||||
beforeClose={(source) => {
|
||||
beforeClose={() => {
|
||||
if (!draft) return true;
|
||||
setHint(`已拦截一次「${source}」关闭:内容还没保存`);
|
||||
setDiscarding(true);
|
||||
return false;
|
||||
}}
|
||||
footer={
|
||||
@@ -198,7 +206,6 @@ export default () => {
|
||||
disabled={submitting}
|
||||
onClick={() => {
|
||||
setSubmitting(true);
|
||||
setHint('提交中,三条出口都锁住了');
|
||||
window.setTimeout(() => { setSubmitting(false); setOpen(false); setHint('已提交'); }, 2000);
|
||||
}}
|
||||
>
|
||||
@@ -207,21 +214,37 @@ export default () => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
<Textarea
|
||||
rows={4}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
placeholder="随便打几个字,再去点遮罩或按 Esc 试试"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 96,
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
border: '1px solid rgb(var(--border-base))',
|
||||
background: 'rgb(var(--bg-page))',
|
||||
color: 'rgb(var(--text-1))',
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* 拦下来之后弹的二次确认:自带遮罩叠在上面,把下面那个弹窗一起压暗 */}
|
||||
<Modal
|
||||
size="small"
|
||||
open={discarding}
|
||||
onOpenChange={setDiscarding}
|
||||
closable={false}
|
||||
title="要放弃这次编辑吗"
|
||||
footer={
|
||||
<>
|
||||
<ModalClose asChild>
|
||||
<Button color="default" variant="outlined">继续编辑</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
color="danger"
|
||||
onClick={() => { setDiscarding(false); setDraft(''); setOpen(false); setHint('已放弃'); }}
|
||||
>
|
||||
放弃
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
已经写了 {draft.length} 个字,关掉就没了。
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -245,7 +268,7 @@ export default () => {
|
||||
| `maskClosable` / `escClosable` | `boolean` | `true` | 关掉其中一条关闭路径 |
|
||||
| `submitting` | `boolean` | `false` | 提交中:三条路径全部禁用 |
|
||||
| `beforeClose` | `(source) => boolean \| void` | — | 返回 `false` 留住弹窗;`source` 为 `'close-button' \| 'overlay' \| 'esc'` |
|
||||
| `className` / `bodyClassName` / `footerClassName` / `overlayClassName` | `string` | — | 局部覆盖;`overlayClassName` 用于叠在别的浮层上时把遮罩置空 |
|
||||
| `className` / `bodyClassName` / `footerClassName` / `overlayClassName` | `string` | — | 局部覆盖;`overlayClassName` 是遮罩的特例口子(如单独调浅某一层),嵌套时不需要动它 |
|
||||
| `container` | `HTMLElement` | `document.body` | portal 挂载点 |
|
||||
|
||||
## 不要用弹窗的场合
|
||||
@@ -257,4 +280,4 @@ export default () => {
|
||||
- 内容有自己的地址、要能分享 → 独立页面。
|
||||
- 纯「确认 / 取消」的破坏性操作 → 二次确认(见「组件 → Confirm 二次确认」),不要用 `Modal` 手拼。
|
||||
|
||||
**一次只开一个弹窗**:弹窗里不能再开弹窗,两层浮层叠着,用户数不清关几次才回得去。弹窗里可以开二次确认,那是一问一答,不算一层——那种情况给它的遮罩传 `overlayClassName="bg-transparent"`,同一时间只压暗一次页面。
|
||||
**一次只开一个弹窗**:弹窗里不能再开弹窗,两层浮层叠着,用户数不清关几次才回得去。弹窗里可以开二次确认,那是一问一答,不算一层——它自带遮罩叠在弹窗之上,把下面那层也压暗。
|
||||
|
||||
@@ -74,7 +74,10 @@
|
||||
|
||||
**头部和底部始终固定,只有主体滚动。** 用户滚到哪里都能看见标题和「保存」,不用滚回去找。
|
||||
|
||||
**关闭「×」按有没有取消按钮决定**:表单类弹窗必须有,那是它唯一的出口;带取消按钮的确认类不给「×」——两个出口说的是同一件事,反而让人犹豫该点哪个。
|
||||
**关闭「×」按弹窗类型决定:**
|
||||
|
||||
- **表单类**(填了才有结果的):**「×」和取消都给**。取消挨着主按钮,是用户看完整张表后就地放弃的出口;「×」在右上角,是随手关掉浮层的常规位置。两个都通向「不保存就走」,但用户的视线落点不同——只留一个,另一头的人就得满弹窗找出口。
|
||||
- **确认类**(一问一答):只有底部两个按钮,**不给「×」**——问题本身已经摆在按钮上了,右上角再开一个出口只会让人犹豫该点哪个。这类走[二次确认](组件-Confirm二次确认.md)。
|
||||
|
||||
底部按钮直接用 Button 组件,排列顺序、间距与最小宽度见 [组件-Button按钮.md](组件-Button按钮.md):主按钮最右,危险场景主位放危险红实心。
|
||||
|
||||
@@ -82,7 +85,9 @@
|
||||
|
||||
**遮罩用黑色 40% 不透明,不加模糊。** 遮罩的作用是把页面压暗、让注意力收到弹窗上;一旦模糊到认不出底下是什么,用户会以为自己跳到了新页面,然后去按返回键。
|
||||
|
||||
**同一时间只有一层遮罩。** 弹窗里再开二次确认,不叠第二层——叠两层等于把页面又压暗一次,用户会以为自己陷得更深了。
|
||||
**弹窗之上再开二次确认,二次确认自带遮罩叠上去。** 那层遮罩压暗的是**下面那个弹窗**:不叠的话,确认框和下面弹窗的白底连成一片,看起来像嵌在表单里的一块,而不是一个必须先回答的新层。叠了之后下面的弹窗一起变暗,确认框才明显是当下唯一能动的东西。
|
||||
|
||||
> 两层 40% 叠起来的实际暗度约 64%。这是目前的取值,若觉得偏重,可单独给第二层定一个更浅的值——改这里即可,壳不写死。
|
||||
|
||||
浮层的前后关系分四档,新增浮层一律从这四档里选,不自造数值:
|
||||
|
||||
@@ -128,9 +133,10 @@
|
||||
**壳已落地为组件库组件 `@bisheng/ui` 的 `Modal`**(`packages/ui/src/components/Modal/Modal.tsx`,实时预览见 components/modal.mdx):本文 §2–§7 的取值——四档宽度与降档表、遮罩、层级、结构与间距、200/160ms 动效、三条关闭路径与提交锁、手机档全屏——全部写死在壳里,业务页只传内容。下列条目是它的实现口径,同时记录尚未收口的部分。
|
||||
|
||||
- **降档断点用 1024 / 1280**,即 Tailwind 的 `lg:` / `xl:`。二者属桌面档(>768)内部的细分排版,不是新增档位断点,符合[基础-多端适配原则.md](基础-多端适配原则.md)「不自造断点」。档位断点仍只有 576 / 768 两个。
|
||||
- 主体区不留上下内边距,但它同时是滚动容器(`overflow-y: auto`),而滚动容器在**内边距盒**处裁切——padding 为 0 时,排在最上 / 最下的输入框那 2px 聚焦环会被削掉半圈。壳里用 `py-0.5 -my-0.5` 解决:内边距给环留出 2px,负外边距把这 2px 从布局里减回去,视觉间距一点不变。
|
||||
- 宽度实现:`width: min(<档位>, calc(100vw - 32px))`,降档表用 `lg:` / `xl:` 前缀覆盖,不写 JS 计算。高度:`max-height: calc(100vh - 64px)`,body 区 `overflow-y: auto` + `overscroll-behavior: contain`。
|
||||
- 遮罩落地:`rgba(0, 0, 0, 0.4)`,**不加模糊**。毛玻璃已于 2026-08-04 随全站 `backdrop-blur` 清除下线(client 33 处 + platform 7 处),现存三套壳都不带模糊,**只剩颜色要迁**:A 套的 `bg-black/40` 数值即最终值,B / C 套的 `bg-gray-500/90` 改过来——二次确认那 16 处随 `ConfirmContext` 一处生效。
|
||||
- 嵌套时不叠遮罩:二次确认开在弹窗之上时,把它的遮罩置为透明(弹窗那层已经在压暗页面)。
|
||||
- 嵌套时照常叠遮罩:二次确认开在弹窗之上时用它自己那层默认遮罩,不传 `overlayClassName`。`overlayClassName` 只留作特例口子(例如要给第二层单独调浅),不再是嵌套的标准写法。
|
||||
- z-index 四档已写成 token(`--z-modal: 1000` / `--z-popover: 1100` / `--z-toast: 1200` / `--z-tooltip: 1300`):`design-token.cjs` 的 `Z_INDEX` 表为名称与取值的 SSOT,两个运行时载体(`packages/ui/src/styles/tokens.css` + `client/src/style.css`)与两份 Tailwind 配置同步落地,类名 `z-modal` / `z-popover` / `z-toast` / `z-tooltip`;组件库 Toaster 容器的临时值 `z-[9999]` 已归并到 1200。**client 现存的 `z-50` / `z-[100]` / `z-[110]` 随两套壳收敛时替换,尚未动。** 本表为层级唯一事实源,[01-设计规范.md](01-设计规范.md) §5 改为指针。
|
||||
- 动效曲线:进出统一 `cubic-bezier(0.2, 0, 0, 1)`;200ms / 160ms。缩放只做入场,出场纯淡出(缩小会让人误以为「收回到某处」)。落地为 `modal-overlay-in/out` + `modal-content-in/out` 四条 keyframes(两份 Tailwind 配置同步)。两个坑:入场缩放写**独立的 `scale` 属性**而不是 `transform: scale()`,否则会和居中用的 `translate(-50%, -50%)` 打架(手机档又是 `inset: 0`,两档不能共用一条 transform);卡片不能再包一层居中 div——`Dialog.Portal` 会给每个子节点各套一个 `Presence`,没有自己动画的那层 div 一关就整棵卸载,出场动画根本来不及播。
|
||||
- 无障碍:`role="dialog"` + `aria-modal="true"` + `aria-labelledby` 指向标题;焦点陷阱在弹窗内,关闭后归还触发元素。全屏档同样带 `aria-modal`。
|
||||
@@ -203,7 +209,7 @@ BISHENG client 有两套弹窗,都包着同一个 Radix `@radix-ui/react-dialo
|
||||
| 遮罩 | 黑色系低不透明度:M3 32% / Fluent 40%(暗色 50%)/ antd 45% / SLDS 50% / Arco 深灰 60%。毛玻璃仅 antd 提供且默认关 | 黑 40% 无模糊。原候选 `bg-gray-500/90` 比七家都重一大截,设计师拍板淘汰 |
|
||||
| 内边距 | 24 最主流(M3 / Fluent / antd 横向);TDesign 32、SLDS 16 | 16,主体只留左右(定稿时为四边 20,设计师 2026-08-20 对着预览页收窄,落在 SLDS 一档)|
|
||||
| 标题 | 中文体系一致 16px(antd / Arco / TDesign);字重 antd 600 / TDesign 600 / Arco 500。西方体系更大(M3 24 / Fluent 20) | 16px / 字重 500,与抽屉、二次确认现状一致 |
|
||||
| 关闭「×」 | antd / Arco / TDesign 默认有;M3 basic dialog 无、Fluent 规定「无取消按钮时才加」、Apple alert 无 | 按有无取消按钮决定 |
|
||||
| 关闭「×」 | antd / Arco / TDesign 默认有;M3 basic dialog 无、Fluent 规定「无取消按钮时才加」、Apple alert 无 | 按弹窗类型决定:表单类「×」+ 取消都给,确认类只留底部两个按钮(2026-08-20 设计师改口径,原为「按有无取消按钮决定」) |
|
||||
| footer 按钮 | 主按钮在右为压倒性共识(antd / Arco / TDesign / M3 / Apple / SLDS),仅 Fluent 在左;间距 8(antd / M3 / Fluent)、Arco 12 | 主按钮最右;间距沿用《Button 按钮》已定的 12px,本文不另立 |
|
||||
| z-index | 基数 + 逐层加:antd 1000 / Arco 1001 / TDesign 2500 / SLDS 9000 | 1000 / 1100 / 1200 / 1300 四档 |
|
||||
| 移动端 | 仅 M3 给显式断点(<600dp 转全屏);Fluent 用 480px / 359px CSS 断点;Apple、SLDS 无公开数值 | 沿用本站 576 断点,转全屏档 |
|
||||
@@ -226,6 +232,8 @@ BISHENG client 有两套弹窗,都包着同一个 Radix `@radix-ui/react-dialo
|
||||
| 2026-08-20 | **v1 规范落地为组件库组件**:`@bisheng/ui` 新增 `Modal`(四档尺寸 + 降档表、黑 40% 无模糊遮罩、结构与间距、三条关闭路径 + `beforeClose` 拦截 + `submitting` 锁、手机档全屏、焦点陷阱与 `aria-modal`),底层沿用 `@radix-ui/react-dialog`;层级四档写成 token(`Z_INDEX` 进 design-token.cjs,两个 CSS 载体 + 两份 Tailwind 配置同步),Toaster 的 `z-[9999]` 归并到 `z-toast`;动效四条 keyframes 落地;文档站 components/modal.mdx 重写为实时预览 + API。**client 两套旧壳未动**(迁移批次见待决策清单) | `packages/ui/src/components/Modal/*`(新)、`packages/ui/src/index.ts`、`packages/ui/design-token.cjs`、`packages/ui/src/styles/tokens.css`、`packages/ui/tailwind-preset.cjs`、`packages/ui/src/components/Toast/Toaster.tsx`、`packages/ui/docs/components/modal.mdx`、`packages/ui/docs/design-token.mdx`、`client/src/style.css`、`client/tailwind.config.cjs`、`packages/ui/package.json` + `pnpm-workspace.yaml`(radix dialog 进 catalog) | 待提交 |
|
||||
|
||||
| 2026-08-20 | 设计师对着预览页调整内边距:**三段左右 20 → 16px**、**底部上下 12 → 16px**(底部即四边 16)、**主体上下内边距去掉**(上下空隙交给头部高度和底部内边距);头部仍靠 56px 定高。§4 表、手机档安全区公式、台账目标值与 §1.4 调研取值同步 | 本文件、`packages/ui/src/components/Modal/Modal.tsx`、`packages/ui/docs/components/modal.mdx` | 待提交 |
|
||||
| 2026-08-20 | 设计师对着预览页改嵌套遮罩口径:**弹窗之上的二次确认要自带遮罩叠上去**,原「同一时间只有一层遮罩、嵌套时把第二层置为透明」作废——不叠时确认框和下面弹窗的白底连成一片,读起来像嵌在表单里的一块。两层 40% 实际约 64%,如需更浅再单独定值。§5 与给实现窗口同步,预览页 demo 去掉 `overlayClassName="bg-transparent"` | 本文件、01-设计规范.md、`packages/ui/docs/components/modal.mdx`、`packages/ui/src/components/Modal/Modal.tsx`(注释) | 待提交 |
|
||||
| 2026-08-20 | 设计师对着预览页改「×」口径:**表单类弹窗「×」和取消按钮都要有**(原写法是「表单类的「×」是它唯一的出口」,等于不给取消)。§4 那条规则从「按有没有取消按钮决定」改成**按弹窗类型决定**——表单类两个都给,确认类只留底部两个按钮。组件不变(`closable` 默认 `true`,取消按钮本就由业务页传),改的是规范与预览页示例 | 本文件、00-总纲.md、`packages/ui/docs/components/modal.mdx`、`packages/ui/src/components/Modal/Modal.tsx`(注释) | 待提交 |
|
||||
| 2026-08-20 | 遮罩现状回填:三套壳的毛玻璃 2026-08-04 已随全站 `backdrop-blur` 清除(client 33 + platform 7),文档里「灰底毛玻璃 / `+ blur`」的旧描述全部作废——**遮罩只剩颜色待迁**(B / C 套 `bg-gray-500/90` → 黑 40%)。二次确认预览页规格表同步 | 本文件、`packages/ui/docs/components/confirm.mdx` | 待提交 |
|
||||
| 2026-08-20 | 设计师对着预览页调整全屏档:**关闭「×」回到右上角、主操作回到底部**,原「左侧关闭 + 右侧主操作」的头部作废——全屏档与普通弹窗共用同一套头尾位置;**底部按钮等宽平铺占满一行**(与手机档同一条规则,卡片档仍是右对齐原宽)。组件同步删掉为它开的 `headerAction` prop | 本文件、`packages/ui/src/components/Modal/Modal.tsx`、`packages/ui/docs/components/modal.mdx` | 待提交 |
|
||||
|
||||
|
||||
@@ -80,8 +80,10 @@ export interface ModalProps {
|
||||
/** Footer actions, primary rightmost (组件-Button按钮.md). Omit and the footer is not rendered. */
|
||||
footer?: React.ReactNode;
|
||||
/**
|
||||
* The header「×」(§4). Keep it for form dialogs — it is their only exit; drop it
|
||||
* when the footer already carries a 取消 button, so there are not two ways to say no.
|
||||
* The header「×」(§4). Decided by dialog TYPE, not by whether a 取消 button
|
||||
* exists: a form dialog keeps both (取消 sits by the primary button, the「×」is
|
||||
* where a hand reaches to dismiss a layer — different places to look); a
|
||||
* confirm dialog drops it, its two footer buttons already are the question.
|
||||
*/
|
||||
closable?: boolean;
|
||||
/** Accessible name of the「×」button. Text comes from the caller (library contract). */
|
||||
@@ -106,7 +108,8 @@ export interface ModalProps {
|
||||
bodyClassName?: string;
|
||||
/** Extra classes on the footer. */
|
||||
footerClassName?: string;
|
||||
/** Escape hatch for the mask, e.g. `bg-transparent` when this opens ON TOP of another overlay (§5). */
|
||||
/** Escape hatch for the mask (§5) — e.g. a lighter second layer. Nesting needs
|
||||
* nothing here: a confirm opened on top of a dialog draws its own mask. */
|
||||
overlayClassName?: string;
|
||||
}
|
||||
|
||||
@@ -187,7 +190,14 @@ export function Modal({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 text-body',
|
||||
// §4 — the body keeps NO vertical padding of its own; the header's
|
||||
// height and the footer's padding already hold that gap open.
|
||||
// `py-0.5 -my-0.5` is not spacing: a scroll container clips at its
|
||||
// padding box, and with zero vertical padding that edge cuts the
|
||||
// 2px focus ring off any field sitting first or last in the body.
|
||||
// The padding gives the ring its 2px, the negative margin takes
|
||||
// the same 2px back out of the layout — nothing moves.
|
||||
'min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-0.5 -my-0.5 text-body',
|
||||
bodyClassName,
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user