@deernova/style 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
- package/skill/assets/DeernovaTokens.swift +3 -3
- package/skill/assets/tokens.css +3 -0
- package/skill/assets/tokens.json +24 -6
- package/skill/manifest.json +2 -2
- package/skill/references/components/banner.md +8 -1
- package/skill/references/components/menu.md +8 -5
- package/skill/references/components/sheet.md +63 -12
- package/skill/references/components/sidebar.md +4 -0
- package/skill/references/components/step-row.md +5 -2
- package/skill/references/components/toolbar.md +1 -1
- package/skill/references/components/tooltip.md +3 -2
- package/skill/references/foundations/motion.md +111 -9
- package/skill/references/review.md +1 -1
- package/skill/references/scenes/web.md +2 -2
- package/skill/references/tokens.md +9 -6
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -78,7 +78,7 @@ description: Deernova 的界面设计语言:只用黑白灰,内容放在不
|
|
|
78
78
|
|
|
79
79
|
### 组件
|
|
80
80
|
|
|
81
|
-
[按钮](references/components/button.md) · [顶栏与侧面板](references/components/toolbar.md) · [玻璃操作层](references/components/glass-controls.md) · [分段控件](references/components/segmented.md) · [文本框](references/components/text-field.md) · [菜单与下拉](references/components/menu.md) · [Composer](references/components/composer.md) · [侧边栏](references/components/sidebar.md) · [分组列表](references/components/list.md) · [卡片](references/components/card.md) · [标签](references/components/tag.md) · [代码块](references/components/code-block.md) · [任务清单](references/components/task-list.md) · [空状态](references/components/empty-state.md) · [横幅与轻提示](references/components/banner.md) · [
|
|
81
|
+
[按钮](references/components/button.md) · [顶栏与侧面板](references/components/toolbar.md) · [玻璃操作层](references/components/glass-controls.md) · [分段控件](references/components/segmented.md) · [文本框](references/components/text-field.md) · [菜单与下拉](references/components/menu.md) · [Composer](references/components/composer.md) · [侧边栏](references/components/sidebar.md) · [分组列表](references/components/list.md) · [卡片](references/components/card.md) · [标签](references/components/tag.md) · [代码块](references/components/code-block.md) · [任务清单](references/components/task-list.md) · [空状态](references/components/empty-state.md) · [横幅与轻提示](references/components/banner.md) · [弹窗、抽屉与 popover](references/components/sheet.md) · [Tooltip 与悬停浮层](references/components/tooltip.md) · [消息](references/components/message.md) · [工具调用与执行步骤](references/components/step-row.md)
|
|
82
82
|
|
|
83
83
|
### 场景
|
|
84
84
|
|
|
@@ -98,6 +98,7 @@ description: Deernova 的界面设计语言:只用黑白灰,内容放在不
|
|
|
98
98
|
- 在 SwiftUI 端给系统工具栏、侧边栏加实色背景盖掉玻璃;在 Web 应用里用模糊效果模仿玻璃。
|
|
99
99
|
- 给卡片加阴影;在玻璃上放正文或列表。
|
|
100
100
|
- 给界面文字、AI 回答或按钮用衬线;到处用粗体。
|
|
101
|
+
- 浮层只有出现动画、关闭时一下子消失;用 `@keyframes` 做开合。
|
|
101
102
|
- 用上下内边距撑出按钮高度;为了凑 44 点击区域把可见的按钮做大;用 `padding-top`、`top: 1px` 或等高的 `line-height` 凑垂直居中。
|
|
102
103
|
- 把官网的大按钮、大字号搬进应用界面;把桌面端的尺寸直接搬到触屏上。
|
|
103
104
|
- 一屏出现两个主按钮;用「确定」「OK」当按钮文字。
|
|
@@ -156,11 +156,11 @@ public enum Deernova {
|
|
|
156
156
|
public static let strokeFocus: CGFloat = 2
|
|
157
157
|
/// 焦点环和控件边缘之间的距离。
|
|
158
158
|
public static let focusOffset: CGFloat = 2
|
|
159
|
-
///
|
|
159
|
+
/// 悬停、按压、选中这类即时反馈;tooltip 的进出;菜单、popover 的退出。
|
|
160
160
|
public static let durationInstant: Double = 0.12
|
|
161
|
-
///
|
|
161
|
+
/// 菜单、popover、轻提示的进入;展开收起;弹窗和抽屉的退出。
|
|
162
162
|
public static let durationBase: Double = 0.2
|
|
163
|
-
///
|
|
163
|
+
/// 弹窗、底部弹窗、侧边抽屉的进入,页面级转场。
|
|
164
164
|
public static let durationSlow: Double = 0.32
|
|
165
165
|
/// 禁用的控件。禁用时保持原位,不隐藏。
|
|
166
166
|
public static let opacityDisabled: Double = 0.35
|
package/skill/assets/tokens.css
CHANGED
|
@@ -153,6 +153,9 @@
|
|
|
153
153
|
--duration-base: 200ms;
|
|
154
154
|
--duration-slow: 320ms;
|
|
155
155
|
--ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
156
|
+
--ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
|
|
157
|
+
--ease-exit: cubic-bezier(0.4, 0, 0.2, 1);
|
|
158
|
+
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
156
159
|
--opacity-disabled: 0.35;
|
|
157
160
|
--opacity-solid-hover: 0.88;
|
|
158
161
|
--glass-blur: 16px;
|
package/skill/assets/tokens.json
CHANGED
|
@@ -948,28 +948,46 @@
|
|
|
948
948
|
]
|
|
949
949
|
},
|
|
950
950
|
"motion": {
|
|
951
|
-
"note": "
|
|
951
|
+
"note": "动效时长和缓动。进入用 ease-enter,退出用 ease-exit 并降一档时长,从屏幕边缘滑入的面板用 ease-drawer。SwiftUI 里默认用 .smooth(等同 .spring(response: 0.35, dampingFraction: 0.85) 的手感)。",
|
|
952
952
|
"tokens": [
|
|
953
953
|
{
|
|
954
954
|
"name": "duration-instant",
|
|
955
955
|
"value": "120ms",
|
|
956
|
-
"usage": "
|
|
956
|
+
"usage": "悬停、按压、选中这类即时反馈;tooltip 的进出;菜单、popover 的退出。"
|
|
957
957
|
},
|
|
958
958
|
{
|
|
959
959
|
"name": "duration-base",
|
|
960
960
|
"value": "200ms",
|
|
961
|
-
"usage": "
|
|
961
|
+
"usage": "菜单、popover、轻提示的进入;展开收起;弹窗和抽屉的退出。"
|
|
962
962
|
},
|
|
963
963
|
{
|
|
964
964
|
"name": "duration-slow",
|
|
965
965
|
"value": "320ms",
|
|
966
|
-
"usage": "
|
|
966
|
+
"usage": "弹窗、底部弹窗、侧边抽屉的进入,页面级转场。"
|
|
967
967
|
},
|
|
968
968
|
{
|
|
969
969
|
"name": "ease-standard",
|
|
970
970
|
"value": "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
|
971
971
|
"swift": false,
|
|
972
|
-
"usage": "
|
|
972
|
+
"usage": "状态变化和展开收起:颜色、高度、箭头旋转。快进、缓停。"
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
"name": "ease-enter",
|
|
976
|
+
"value": "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
977
|
+
"swift": false,
|
|
978
|
+
"usage": "出现:菜单、popover、弹窗、轻提示。起步快、停得很柔,前一半时间就完成大部分位移。"
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
"name": "ease-exit",
|
|
982
|
+
"value": "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
983
|
+
"swift": false,
|
|
984
|
+
"usage": "消失:配比进入低一档的时长,不拖泥带水。"
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
"name": "ease-drawer",
|
|
988
|
+
"value": "cubic-bezier(0.32, 0.72, 0, 1)",
|
|
989
|
+
"swift": false,
|
|
990
|
+
"usage": "从屏幕边缘滑入滑出的面板:侧边抽屉、底部弹窗,进出都用它。"
|
|
973
991
|
}
|
|
974
992
|
]
|
|
975
993
|
},
|
|
@@ -1735,7 +1753,7 @@
|
|
|
1735
1753
|
},
|
|
1736
1754
|
{
|
|
1737
1755
|
"id": "sheet",
|
|
1738
|
-
"title": "
|
|
1756
|
+
"title": "弹窗、抽屉与 popover",
|
|
1739
1757
|
"reference": "references/components/sheet.md",
|
|
1740
1758
|
"tokens": [
|
|
1741
1759
|
{
|
package/skill/manifest.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- 圆角 `banner-radius`,内边距上下 `space-2`、左右 `space-3`,图标 `icon-size-md`;标题 `type-body` 半粗,正文 `callout`。状态图标见 [图标](../foundations/iconography.md)。
|
|
18
18
|
- 动作按钮用小号次按钮,放在右侧,一个横幅最多一个。
|
|
19
19
|
- 和输入区有关的提示(额度将用完、网络断开)贴在 Composer 正上方,宽度和 Composer 一致,见 [Composer](composer.md)。
|
|
20
|
-
- 轻提示:底部居中的玻璃胶囊,高 `toast-height`,2
|
|
20
|
+
- 轻提示:底部居中的玻璃胶囊,高 `toast-height`,2 秒后自动消失(出现时淡入并上移 8px,消失时淡出,见 [动效](../foundations/motion.md)),只用来确认刚刚完成的操作(「已复制」「已保存」),文字用完成态,不放按钮。读屏用 `role="status"`。
|
|
21
21
|
|
|
22
22
|
## Web
|
|
23
23
|
|
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
.dn-banner.is-danger > .dn-icon { color: var(--danger); }
|
|
45
45
|
|
|
46
46
|
.dn-toast { display: inline-flex; align-items: center; gap: var(--space-2); height: var(--toast-height); padding: 0 var(--space-4) 0 var(--space-3); color: var(--ink); font: var(--weight-medium) var(--type-body-size)/1 var(--font-sans); }
|
|
47
|
+
.dn-toast { transition: opacity var(--duration-base) var(--ease-enter), transform var(--duration-base) var(--ease-enter), display var(--duration-base) allow-discrete; }
|
|
48
|
+
.dn-toast[hidden] { display: none; opacity: 0; transform: translateY(8px); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-exit); }
|
|
49
|
+
@starting-style { .dn-toast:not([hidden]) { opacity: 0; transform: translateY(8px); } }
|
|
50
|
+
@media (prefers-reduced-motion: reduce) {
|
|
51
|
+
.dn-toast[hidden] { transform: none; }
|
|
52
|
+
@starting-style { .dn-toast:not([hidden]) { transform: none; } }
|
|
53
|
+
}
|
|
47
54
|
.dn-toast .dn-icon { width: var(--icon-size-md); height: var(--icon-size-md); color: var(--success); }
|
|
48
55
|
```
|
|
49
56
|
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
- 默认在触发器下方 `menu-offset` 处展开,左边对齐;从靠右的按钮弹出时,改成右边对齐。下方放不下就翻到上方;上下都放不下时限制最大高度,在面板内部滚动,并用 `overscroll-behavior: contain` 防止带动页面滚动。面板离窗口边缘至少留 `space-2`。
|
|
55
55
|
- 点击触发器打开,再点一次关闭;不要在悬停时打开。选中一项、按 Esc、点击菜单外面、焦点移出菜单,都会关闭菜单。按 Esc 关闭或选中一项后,焦点回到触发器。
|
|
56
56
|
- 右键菜单在指针位置展开,其他规则相同。
|
|
57
|
-
-
|
|
57
|
+
- 打开时淡入并从 0.96 放大到 1,用时 `duration-base`、缓动 `ease-enter`,从触发器那一侧展开;关闭时淡出并缩回 0.96,用时 `duration-instant`、缓动 `ease-exit`。中途反向(刚打开就关闭)时从当前位置退回。开启「减弱动态效果」后只保留淡入淡出。见 [动效](../foundations/motion.md)。
|
|
58
58
|
|
|
59
59
|
## 键盘
|
|
60
60
|
|
|
@@ -91,10 +91,10 @@
|
|
|
91
91
|
color: var(--ink);
|
|
92
92
|
box-shadow: var(--shadow-float);
|
|
93
93
|
transform-origin: top left;
|
|
94
|
-
transition: opacity var(--duration-
|
|
94
|
+
transition: opacity var(--duration-base) var(--ease-enter), transform var(--duration-base) var(--ease-enter), display var(--duration-base) allow-discrete;
|
|
95
95
|
}
|
|
96
|
-
.dn-menu[hidden] { display: none; }
|
|
97
|
-
@starting-style { .dn-menu { opacity: 0; transform: scale(0.
|
|
96
|
+
.dn-menu[hidden] { display: none; opacity: 0; transform: scale(0.96); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-exit); }
|
|
97
|
+
@starting-style { .dn-menu:not([hidden]) { opacity: 0; transform: scale(0.96); } }
|
|
98
98
|
.dn-menu.is-glass { border-color: var(--glass-stroke); background: var(--glass-fill); -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); }
|
|
99
99
|
.dn-menu-item {
|
|
100
100
|
box-sizing: border-box;
|
|
@@ -146,7 +146,10 @@
|
|
|
146
146
|
}
|
|
147
147
|
.dn-select > :is(select, button):focus-visible { outline: none; border-color: var(--focus-ring); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
148
148
|
.dn-select > .dn-select-chevron { position: absolute; top: 50%; right: var(--field-padding-x); width: var(--icon-size-sm); height: var(--icon-size-sm); color: var(--ink-tertiary); translate: 0 -50%; pointer-events: none; }
|
|
149
|
-
@media (prefers-reduced-motion: reduce) {
|
|
149
|
+
@media (prefers-reduced-motion: reduce) {
|
|
150
|
+
.dn-menu[hidden] { transform: none; }
|
|
151
|
+
@starting-style { .dn-menu:not([hidden]) { transform: none; } }
|
|
152
|
+
}
|
|
150
153
|
@media (prefers-reduced-transparency: reduce) { .dn-menu.is-glass { background: var(--surface-raised); -webkit-backdrop-filter: none; backdrop-filter: none; } }
|
|
151
154
|
```
|
|
152
155
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 弹窗、抽屉与 popover
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
弹窗用于一个短小、独立的任务:确认删除、编辑一个字段、选择分享方式。它会打断当前页面,背景压暗,完成或取消之后回到原处。指针端在窗口中央弹出,触控端从底部弹出。侧边抽屉从屏幕左右边缘滑入,窄屏时用来放收起的侧边栏和侧面板。popover 附着在一个控件上,放少量说明或短表单,不打断页面。
|
|
4
4
|
|
|
5
5
|
**需要提供**:标题、一两句说明、内容(短表单或选项)、主操作和取消操作;底部弹窗还要提供支持的高度档位。危险操作要写清后果。
|
|
6
6
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
| 必须马上知道的错误 | 确认弹窗;SwiftUI 用 `.alert` |
|
|
15
15
|
| 一个短小、独立的任务 | 弹窗;触控端是底部弹窗 |
|
|
16
16
|
| 附着在某个控件上的少量说明、设置或短表单 | popover;手机上改成底部弹窗 |
|
|
17
|
+
| 窄屏时收起的侧边栏、侧面板 | 侧边抽屉 |
|
|
17
18
|
| 一列命令或选项 | [菜单](menu.md) |
|
|
18
19
|
| 长内容、多步流程、需要长时间阅读 | 推入新页面,或者全高弹窗 |
|
|
19
20
|
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
- 有未保存的输入时,Esc、取消、下拉关闭都先问「放弃修改?」,按钮是「继续编辑」和「放弃」。这是唯一可以叠在弹窗上的确认。
|
|
59
60
|
- 关闭后焦点回到打开它的控件。
|
|
60
61
|
- 表单弹窗里,回车执行主操作;危险确认除外。
|
|
61
|
-
-
|
|
62
|
+
- 动效:居中的弹窗淡入并从 0.96 放大到 1,用时 `duration-slow`、缓动 `ease-enter`;关闭时淡出并缩回 0.96,用时 `duration-base`、缓动 `ease-exit`。底部弹窗和侧边抽屉整块滑入滑出,进入 `duration-slow`、退出 `duration-base`,都用 `ease-drawer`。遮罩和面板同步淡入淡出。popover 的开合和菜单相同。开启「减弱动态效果」后只保留淡入淡出。见 [动效](../foundations/motion.md)。
|
|
62
63
|
|
|
63
64
|
## 触控端
|
|
64
65
|
|
|
@@ -67,6 +68,13 @@
|
|
|
67
68
|
- 输入框聚焦、键盘弹起时,输入框要留在可见区域,不能被键盘挡住。
|
|
68
69
|
- 手机上不用 popover,改成底部弹窗;iPad 和平板的大屏上不要弹出撑满全屏的底部弹窗。
|
|
69
70
|
|
|
71
|
+
## 侧边抽屉
|
|
72
|
+
|
|
73
|
+
- 从面板平时所在的那一边滑入:侧边栏从左边,侧面板从右边。宽度分别是 `sidebar-width` 和 `sheet-width-md`,右侧或左侧至少露出 48px 的页面,让人知道点那里能关掉。
|
|
74
|
+
- `surface` 底,靠页面一侧一条 1px `line`,阴影 `shadow-float`,背后用 `scrim` 压暗;打开时页面不能滚动。
|
|
75
|
+
- 关闭方式:点遮罩、Esc、抽屉里的关闭按钮、选中一个导航项后自动关闭。打开后焦点移进抽屉,关闭后回到打开它的按钮。
|
|
76
|
+
- 触屏上可以跟手拖动关闭:拖动时去掉过渡,让抽屉跟着手指走;松手时拖过宽度的三分之一,或者向外甩得够快,就滑出关闭,否则用 `ease-drawer` 弹回原位。
|
|
77
|
+
|
|
70
78
|
## Web
|
|
71
79
|
|
|
72
80
|
- 弹窗用 `<dialog>` 和 `showModal()`:自带顶层叠放、焦点限制在弹窗内、Esc 关闭和背景遮罩,关闭后焦点回到原来的控件。用 `aria-labelledby` 指向标题,`aria-describedby` 指向说明;危险确认用 `role="alertdialog"`。初始焦点用 `autofocus` 标出。
|
|
@@ -89,17 +97,48 @@
|
|
|
89
97
|
background: var(--surface-raised);
|
|
90
98
|
color: var(--ink);
|
|
91
99
|
box-shadow: var(--shadow-float);
|
|
92
|
-
transition: opacity var(--duration-
|
|
100
|
+
transition: opacity var(--duration-slow) var(--ease-enter), transform var(--duration-slow) var(--ease-enter), overlay var(--duration-slow) allow-discrete, display var(--duration-slow) allow-discrete;
|
|
93
101
|
}
|
|
94
|
-
dialog.dn-sheet:not([open]) { display: none; }
|
|
102
|
+
dialog.dn-sheet:not([open]) { display: none; opacity: 0; transform: scale(0.96); transition-duration: var(--duration-base); transition-timing-function: var(--ease-exit); }
|
|
95
103
|
.dn-sheet.is-sm { width: min(100% - 2 * var(--sheet-margin), var(--sheet-width-sm)); }
|
|
96
104
|
.dn-sheet.is-lg { width: min(100% - 2 * var(--sheet-margin), var(--sheet-width-lg)); }
|
|
97
|
-
.dn-sheet::backdrop { background: var(--scrim); transition: opacity var(--duration-
|
|
105
|
+
.dn-sheet::backdrop { background: var(--scrim); transition: opacity var(--duration-slow) var(--ease-enter), overlay var(--duration-slow) allow-discrete, display var(--duration-slow) allow-discrete; }
|
|
106
|
+
dialog.dn-sheet:not([open])::backdrop { opacity: 0; transition-duration: var(--duration-base); transition-timing-function: var(--ease-exit); }
|
|
98
107
|
@starting-style {
|
|
99
108
|
.dn-sheet[open] { opacity: 0; transform: scale(0.96); }
|
|
100
109
|
.dn-sheet[open]::backdrop { opacity: 0; }
|
|
101
110
|
}
|
|
102
|
-
:root:has(.dn-sheet:modal) { overflow: hidden; }
|
|
111
|
+
:root:has(:is(.dn-sheet, .dn-drawer):modal) { overflow: hidden; }
|
|
112
|
+
.dn-drawer {
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
position: fixed;
|
|
115
|
+
inset: 0 auto 0 0;
|
|
116
|
+
width: min(var(--sidebar-width), 100vw - var(--space-12));
|
|
117
|
+
max-width: none;
|
|
118
|
+
height: 100dvh;
|
|
119
|
+
max-height: none;
|
|
120
|
+
margin: 0;
|
|
121
|
+
padding: 0;
|
|
122
|
+
overflow-y: auto;
|
|
123
|
+
overscroll-behavior: contain;
|
|
124
|
+
border: 0;
|
|
125
|
+
border-right: var(--stroke-line) solid var(--line);
|
|
126
|
+
background: var(--surface);
|
|
127
|
+
color: var(--ink);
|
|
128
|
+
box-shadow: var(--shadow-float);
|
|
129
|
+
transition: transform var(--duration-slow) var(--ease-drawer), opacity var(--duration-slow) var(--ease-drawer), overlay var(--duration-slow) allow-discrete, display var(--duration-slow) allow-discrete;
|
|
130
|
+
}
|
|
131
|
+
.dn-drawer.is-end { inset: 0 0 0 auto; width: min(var(--sheet-width-md), 100vw - var(--space-12)); border-right: 0; border-left: var(--stroke-line) solid var(--line); }
|
|
132
|
+
dialog.dn-drawer:not([open]) { display: none; transform: translateX(-100%); transition-duration: var(--duration-base); }
|
|
133
|
+
dialog.dn-drawer.is-end:not([open]) { transform: translateX(100%); }
|
|
134
|
+
.dn-drawer::backdrop { background: var(--scrim); transition: opacity var(--duration-slow) var(--ease-drawer), overlay var(--duration-slow) allow-discrete, display var(--duration-slow) allow-discrete; }
|
|
135
|
+
dialog.dn-drawer:not([open])::backdrop { opacity: 0; transition-duration: var(--duration-base); }
|
|
136
|
+
@starting-style {
|
|
137
|
+
.dn-drawer[open] { transform: translateX(-100%); }
|
|
138
|
+
.dn-drawer.is-end[open] { transform: translateX(100%); }
|
|
139
|
+
.dn-drawer[open]::backdrop { opacity: 0; }
|
|
140
|
+
}
|
|
141
|
+
.dn-drawer.is-dragging { transition: none; }
|
|
103
142
|
.dn-sheet-grabber { flex: none; width: var(--sheet-grabber-width); height: var(--sheet-grabber-height); margin: var(--space-2) auto 0; border-radius: var(--radius-full); background: var(--line-control); }
|
|
104
143
|
.dn-sheet-head { flex: none; display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--sheet-padding) var(--sheet-padding) 0; }
|
|
105
144
|
.dn-sheet-heading { flex: 1; min-width: 0; }
|
|
@@ -121,23 +160,33 @@ dialog.dn-sheet:not([open]) { display: none; }
|
|
|
121
160
|
background: var(--surface-raised);
|
|
122
161
|
color: var(--ink);
|
|
123
162
|
box-shadow: var(--shadow-float);
|
|
124
|
-
|
|
163
|
+
transform-origin: top left;
|
|
164
|
+
transition: opacity var(--duration-base) var(--ease-enter), transform var(--duration-base) var(--ease-enter), overlay var(--duration-base) allow-discrete, display var(--duration-base) allow-discrete;
|
|
125
165
|
}
|
|
126
166
|
.dn-popover[popover] { inset: auto; margin: 0; }
|
|
127
|
-
|
|
167
|
+
.dn-popover[popover]:not(:popover-open) { opacity: 0; transform: scale(0.96); transition-duration: var(--duration-instant); transition-timing-function: var(--ease-exit); }
|
|
168
|
+
@starting-style { .dn-popover:popover-open { opacity: 0; transform: scale(0.96); } }
|
|
128
169
|
.dn-popover-title { margin: 0 0 var(--space-1); font: var(--type-headline); }
|
|
129
170
|
.dn-popover-text { margin: 0 0 var(--space-3); color: var(--ink-secondary); font: var(--type-callout); }
|
|
130
171
|
.dn-popover-row { display: flex; gap: var(--gap-inline); }
|
|
131
172
|
.dn-popover-row > .dn-button { flex: none; }
|
|
132
173
|
@media (pointer: coarse) {
|
|
133
|
-
.dn-sheet { transition-
|
|
174
|
+
.dn-sheet { transition-timing-function: var(--ease-drawer); }
|
|
134
175
|
.dn-sheet[open] { margin: auto auto calc(var(--sheet-margin) + env(safe-area-inset-bottom)); }
|
|
135
|
-
|
|
176
|
+
dialog.dn-sheet:not([open]) { opacity: 1; transform: translateY(calc(100% + var(--sheet-margin) + env(safe-area-inset-bottom))); transition-timing-function: var(--ease-drawer); }
|
|
177
|
+
@starting-style { .dn-sheet[open] { opacity: 1; transform: translateY(calc(100% + var(--sheet-margin) + env(safe-area-inset-bottom))); } }
|
|
136
178
|
.dn-sheet-actions { flex-direction: column-reverse; gap: var(--space-1); }
|
|
137
179
|
.dn-sheet-actions .dn-button { width: 100%; height: var(--button-height-lg); }
|
|
138
180
|
}
|
|
139
181
|
@media (pointer: fine) { .dn-sheet-grabber { display: none; } }
|
|
140
|
-
@media (prefers-reduced-motion: reduce) {
|
|
182
|
+
@media (prefers-reduced-motion: reduce) {
|
|
183
|
+
dialog.dn-sheet:not([open]), dialog.dn-drawer:not([open]) { opacity: 0; transform: none; }
|
|
184
|
+
.dn-popover[popover]:not(:popover-open) { transform: none; }
|
|
185
|
+
@starting-style {
|
|
186
|
+
.dn-sheet[open], .dn-drawer[open] { opacity: 0; transform: none; }
|
|
187
|
+
.dn-popover:popover-open { transform: none; }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
141
190
|
```
|
|
142
191
|
|
|
143
192
|
```html
|
|
@@ -197,6 +246,8 @@ popover 附着在打开它的按钮下方:
|
|
|
197
246
|
|
|
198
247
|
真实页面里,popover 用 `<div class="dn-popover" popover id="share">`,打开它的按钮加 `popovertarget="share"`。
|
|
199
248
|
|
|
249
|
+
侧边抽屉用 `<dialog class="dn-drawer">`(侧面板加 `is-end`),打开时调用 `showModal()`。跟手拖动时给它加 `is-dragging`,用 `transform` 跟随手指,松手后去掉 `is-dragging`,再决定 `close()` 还是弹回。
|
|
250
|
+
|
|
200
251
|
## SwiftUI
|
|
201
252
|
|
|
202
253
|
在 iOS 26 上,中等高度的弹窗由系统渲染成悬浮的玻璃面板,拉到全高后变成不透明;Mac 上对应的是附着在窗口上的 sheet 和系统 Alert。直接用系统样式,不要用 `.presentationBackground` 盖掉它。
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
background: var(--surface);
|
|
40
40
|
}
|
|
41
41
|
.dn-sidebar-section, .dn-sidebar-group { display: flex; flex-direction: column; gap: var(--row-gap); }
|
|
42
|
+
.dn-sidebar-group { interpolate-size: allow-keywords; }
|
|
43
|
+
.dn-sidebar-group::details-content { display: flex; flex-direction: column; gap: var(--row-gap); block-size: 0; overflow: clip; transition: block-size var(--duration-base) var(--ease-standard), content-visibility var(--duration-base) allow-discrete; }
|
|
44
|
+
.dn-sidebar-group[open]::details-content { block-size: auto; }
|
|
45
|
+
@media (prefers-reduced-motion: reduce) { .dn-sidebar-group::details-content { transition: none; } }
|
|
42
46
|
.dn-sidebar-heading {
|
|
43
47
|
display: flex;
|
|
44
48
|
align-items: center;
|
|
@@ -15,10 +15,13 @@ AI 在对话里做了什么。分两层:平时只显示一行灰色的**摘要
|
|
|
15
15
|
|
|
16
16
|
## Web
|
|
17
17
|
|
|
18
|
-
用 `<details>` 和 `<summary>`
|
|
18
|
+
用 `<details>` 和 `<summary>` 实现展开,键盘和读屏都能用;内容高度和箭头一起用 `duration-base` 过渡,见 [动效](../foundations/motion.md)。
|
|
19
19
|
|
|
20
20
|
```css
|
|
21
|
-
.dn-activity { min-width: 0; }
|
|
21
|
+
.dn-activity { min-width: 0; interpolate-size: allow-keywords; }
|
|
22
|
+
.dn-activity::details-content { block-size: 0; overflow: clip; transition: block-size var(--duration-base) var(--ease-standard), content-visibility var(--duration-base) allow-discrete; }
|
|
23
|
+
.dn-activity[open]::details-content { block-size: auto; }
|
|
24
|
+
@media (prefers-reduced-motion: reduce) { .dn-activity::details-content { transition: none; } }
|
|
22
25
|
.dn-activity > summary {
|
|
23
26
|
display: flex;
|
|
24
27
|
align-items: center;
|
|
@@ -16,7 +16,7 @@ Web 应用的页面骨架:左边是[侧边栏](sidebar.md),中间是内容
|
|
|
16
16
|
- **顶栏**:高 `toolbar-height`,`canvas` 底,不透明,底部一条 1px `line`,吸顶(`z-sticky`)。左边是标题(`toolbar-title-size`、常规字重、`ink`,后面一个小箭头,点开是菜单);右边是一排[图标按钮](button.md),间隔 2,打开状态的按钮用 `fill-selected` 底。
|
|
17
17
|
- **内容区**:对话栏宽度不超过 `content-max`,左右至少留 `page-margin` + `space-3`;Composer 左右留 `page-margin`。
|
|
18
18
|
- **侧面板**:左边一条 1px `line`,`canvas` 底,默认占内容区的 40%,可以拖动。顶部是标签头,和顶栏同高、底部同样一条线;标签高 `panel-tab-height`,当前标签 `fill-selected` 底、`ink` 字,其他 `ink-secondary`;最右边是关闭、放大按钮。
|
|
19
|
-
- 窄于 `breakpoint-md`(960
|
|
19
|
+
- 窄于 `breakpoint-md`(960)时侧面板改成覆盖在内容上的[抽屉](sheet.md);窄于 `breakpoint-sm`(760)时侧边栏也收起成抽屉。
|
|
20
20
|
|
|
21
21
|
## Web
|
|
22
22
|
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
- **键盘**:控件获得键盘焦点(`:focus-visible`)时立即显示,失去焦点时关闭。
|
|
52
52
|
- **关闭**:指针离开控件和 tooltip、按 Esc、按下或点击控件、页面滚动时关闭。指针可以从控件移到 tooltip 上而不关闭(离开后留 100ms 的余地),方便放大镜用户阅读。
|
|
53
53
|
- 同一时间最多显示一个 tooltip。
|
|
54
|
-
-
|
|
54
|
+
- 动效:淡入淡出,都用 `duration-instant`(进入 `ease-enter`,退出 `ease-exit`),不位移、不缩放。
|
|
55
55
|
- 悬停卡片:同样停留 `tooltip-delay` 后显示;指针移到卡片上不关闭,离开控件和卡片 300ms 后关闭。卡片里的内容只是预览,点击链接本身就能看到全部,所以键盘和触屏用户不会因为看不到卡片而缺少信息。
|
|
56
56
|
|
|
57
57
|
## 触控端
|
|
@@ -91,9 +91,10 @@
|
|
|
91
91
|
box-shadow: var(--shadow-float);
|
|
92
92
|
font: var(--weight-regular) var(--tooltip-font-size)/var(--type-caption-line) var(--font-sans);
|
|
93
93
|
overflow-wrap: anywhere;
|
|
94
|
-
transition: opacity var(--duration-instant) var(--ease-
|
|
94
|
+
transition: opacity var(--duration-instant) var(--ease-enter), overlay var(--duration-instant) allow-discrete, display var(--duration-instant) allow-discrete;
|
|
95
95
|
}
|
|
96
96
|
.dn-tooltip[popover] { position: fixed; inset: auto; }
|
|
97
|
+
.dn-tooltip[popover]:not(:popover-open) { display: none; opacity: 0; transition-timing-function: var(--ease-exit); }
|
|
97
98
|
@starting-style { .dn-tooltip:popover-open { opacity: 0; } }
|
|
98
99
|
.dn-tooltip-shortcut { margin-inline-start: var(--space-2); color: color-mix(in srgb, var(--on-accent) 70%, transparent); font-variant-numeric: tabular-nums; }
|
|
99
100
|
.dn-tooltip-anchor { position: relative; display: inline-flex; }
|
|
@@ -1,22 +1,124 @@
|
|
|
1
1
|
# 动效
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
动效用来说明因果:东西从哪里来、到哪里去。打开有过程,关闭也有过程;起步要快,停下要柔。数值见 [设计变量](../tokens.md) 的「动效」。
|
|
4
4
|
|
|
5
5
|
| Token | 值 | 用途 |
|
|
6
6
|
| --- | --- | --- |
|
|
7
|
-
| `duration-instant` | 120ms |
|
|
8
|
-
| `duration-base` | 200ms |
|
|
9
|
-
| `duration-slow` | 320ms |
|
|
10
|
-
| `ease-standard` | cubic-bezier(0.2, 0.8, 0.2, 1) |
|
|
7
|
+
| `duration-instant` | 120ms | 悬停、按压、选中;tooltip 的进出;菜单、popover 的退出 |
|
|
8
|
+
| `duration-base` | 200ms | 菜单、popover、轻提示的进入;展开收起;弹窗和抽屉的退出 |
|
|
9
|
+
| `duration-slow` | 320ms | 弹窗、底部弹窗、侧边抽屉的进入,页面级转场 |
|
|
10
|
+
| `ease-standard` | cubic-bezier(0.2, 0.8, 0.2, 1) | 状态变化和展开收起:颜色、高度、箭头旋转 |
|
|
11
|
+
| `ease-enter` | cubic-bezier(0.16, 1, 0.3, 1) | 出现:起步快、停得很柔 |
|
|
12
|
+
| `ease-exit` | cubic-bezier(0.4, 0, 0.2, 1) | 消失:配比进入低一档的时长 |
|
|
13
|
+
| `ease-drawer` | cubic-bezier(0.32, 0.72, 0, 1) | 从屏幕边缘滑入滑出的面板,进出都用它 |
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
- Web 上 `transition: background-color var(--duration-instant) var(--ease-standard)`。只过渡颜色、不透明度和 transform,不过渡 width、height。
|
|
15
|
+
## 进入和退出
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
| 元素 | 进入 | 退出 | 动什么 |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| Tooltip | `duration-instant` · `ease-enter` | `duration-instant` · `ease-exit` | 透明度 |
|
|
20
|
+
| 菜单、下拉选择、popover、悬停卡片 | `duration-base` · `ease-enter` | `duration-instant` · `ease-exit` | 透明度,加上从触发器那一侧 0.96 → 1 的缩放 |
|
|
21
|
+
| 轻提示 | `duration-base` · `ease-enter` | `duration-instant` · `ease-exit` | 透明度,加上向上 8px 的位移 |
|
|
22
|
+
| 居中弹窗 | `duration-slow` · `ease-enter` | `duration-base` · `ease-exit` | 面板的透明度和 0.96 → 1 的缩放;遮罩的透明度 |
|
|
23
|
+
| 底部弹窗 | `duration-slow` · `ease-drawer` | `duration-base` · `ease-drawer` | 从底边整块滑入;遮罩的透明度 |
|
|
24
|
+
| 侧边抽屉 | `duration-slow` · `ease-drawer` | `duration-base` · `ease-drawer` | 从左边或右边整块滑入;遮罩的透明度 |
|
|
25
|
+
| 展开收起(侧边栏分组、执行步骤、目录) | `duration-base` · `ease-standard` | 同进入 | 内容高度,箭头同步旋转 |
|
|
26
|
+
| 悬停、按下、选中 | `duration-instant` · `ease-standard` | 同进入 | 颜色,见 [交互状态](states.md) |
|
|
27
|
+
|
|
28
|
+
## 原则
|
|
29
|
+
|
|
30
|
+
- **一开一关都要有过程。** 只有出现动画、消失时一下子不见,是最常见的生硬感来源。
|
|
31
|
+
- **退出比进入快一档。** 用户要关掉的东西不该拖住他:进入 `duration-slow` 的,退出用 `duration-base`;进入 `duration-base` 的,退出用 `duration-instant`。
|
|
32
|
+
- **进入用 `ease-enter`。** 前一半时间就完成大部分位移,看起来立即响应,结尾又很柔。抽屉和底部弹窗用 `ease-drawer`,整块面板像被推出来一样顺。
|
|
33
|
+
- **距离要短。** 浮层从触发器旁边长出来:缩放不小于 0.96,位移不超过 8px;只有抽屉和底部弹窗走完整的屏幕边距离。不要从远处飞入,不要回弹、晃动。
|
|
34
|
+
- **一起动的东西同步。** 遮罩和面板用同样的时长和曲线;面板里的内容跟着面板一起出现,应用里不做逐个错开的入场。
|
|
35
|
+
- **可以中途反向。** 刚打开就关闭、刚关闭又打开时,从当前位置平滑退回,不跳到起点。
|
|
36
|
+
- **保持 60fps。** 浮层只动 `transform` 和 `opacity`;展开收起是唯一允许动高度的情况。不在动画里改宽度、位置、阴影、模糊。内容超过一屏的区域不做高度动画,直接展开。
|
|
37
|
+
|
|
38
|
+
## Web 写法
|
|
39
|
+
|
|
40
|
+
- 开合用 CSS transition,不用 `@keyframes`:transition 中途反向时会从当前值退回,keyframes 会跳。
|
|
41
|
+
- 打开的样子写在元素默认样式里,关闭的样子写在关闭状态的选择器上(`[hidden]`、`dialog:not([open])`、`[popover]:not(:popover-open)`),并在那里换成退出的时长和曲线。浏览器按「变成哪个状态」选用 transition,所以进出可以各用各的。
|
|
42
|
+
- 出现前的样子写在 `@starting-style` 里,元素从 `display: none` 出来时从这里开始过渡。
|
|
43
|
+
- 关闭时元素要变回 `display: none`、离开顶层:给 `display` 加 `allow-discrete`(`<dialog>` 和 popover 还要给 `overlay` 加),退出动画才能播完。不支持的浏览器退化为直接消失,功能不受影响。
|
|
44
|
+
- 展开收起:`<details>` 用 `::details-content` 加 `interpolate-size: allow-keywords` 过渡高度;其他容器用 `grid-template-rows` 从 `0fr` 到 `1fr`。
|
|
45
|
+
- 不用 `setTimeout` 按时长删除元素;要等退出结束时监听 `transitionend`。`will-change` 只在动画进行时加。
|
|
46
|
+
- 检查:在浏览器性能面板里录一次开合,没有掉帧;在动画中途反向操作,能平滑退回。
|
|
47
|
+
|
|
48
|
+
各组件的完整写法见 [菜单](../components/menu.md)、[弹窗、抽屉与 popover](../components/sheet.md)、[Tooltip](../components/tooltip.md)、[横幅与轻提示](../components/banner.md)。下面是展开收起的通用写法:
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
.dn-collapse { interpolate-size: allow-keywords; }
|
|
52
|
+
.dn-collapse::details-content { block-size: 0; overflow: clip; transition: block-size var(--duration-base) var(--ease-standard), content-visibility var(--duration-base) allow-discrete; }
|
|
53
|
+
.dn-collapse[open]::details-content { block-size: auto; }
|
|
54
|
+
.dn-collapse > summary { list-style: none; }
|
|
55
|
+
.dn-collapse > summary::-webkit-details-marker { display: none; }
|
|
56
|
+
.dn-collapse-grid { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--duration-base) var(--ease-standard); }
|
|
57
|
+
.dn-collapse-grid.is-open { grid-template-rows: 1fr; }
|
|
58
|
+
.dn-collapse-grid > * { min-height: 0; overflow: hidden; }
|
|
59
|
+
@media (prefers-reduced-motion: reduce) { .dn-collapse::details-content, .dn-collapse-grid { transition: none; } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<details class="dn-collapse">
|
|
64
|
+
<summary class="dn-state dn-state-row">本周的会话(点开试试)</summary>
|
|
65
|
+
<div class="dn-state dn-state-row">整理周会记录</div>
|
|
66
|
+
<div class="dn-state dn-state-row">修复登录页的焦点顺序</div>
|
|
67
|
+
<div class="dn-state dn-state-row">起草发布说明</div>
|
|
68
|
+
</details>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 减弱动态效果
|
|
72
|
+
|
|
73
|
+
- 用户开启「减弱动态效果」后,去掉位移和缩放,只保留透明度的淡入淡出,时长不超过 `duration-base`;抽屉和底部弹窗也改成淡入淡出。
|
|
74
|
+
- 展开收起直接切换;转圈这类循环放慢,不再闪烁。
|
|
75
|
+
- 完整信息不能依赖动画才看得到。
|
|
76
|
+
|
|
77
|
+
## 其他规则
|
|
16
78
|
|
|
17
79
|
- 业务界面里不做无目的的循环动画。品牌展示页可以有叙事动效,但要能暂停,也不能妨碍阅读。
|
|
18
|
-
- 用户开启「减弱动态效果」后,位移和缩放换成淡入淡出,完整信息不能依赖动画才看得到。
|
|
19
80
|
- **布局保持稳定**:给会变化的标题、状态和按钮文字预留空间,切换时阅读位置和按钮位置不跳动。禁用的按钮保持原位,不隐藏。加载时按钮宽度不变,把文字换成转圈。
|
|
20
81
|
- **状态不丢失**:重绘、切换主题、窗口缩放时,焦点、选中项和用户输入都要保留。
|
|
21
82
|
- **中断可恢复**:装饰性动画在暂停、离开视野或页面隐藏时冻结,回来后从原处继续,不追赶错过的时间;和真实时间绑定的功能(倒计时等)仍以真实时间为准。
|
|
22
83
|
- 连续的装饰动画在 Web 上用 `requestAnimationFrame` 按时间推进,`visibilitychange` 时暂停。
|
|
84
|
+
|
|
85
|
+
## SwiftUI
|
|
86
|
+
|
|
87
|
+
系统的 `.sheet`、`.popover`、`Menu`、`NavigationStack`、`DisclosureGroup` 自带进出动画,不要另加。自己画的浮层和面板:出现和消失都给 `transition`,动画挂在状态上。默认用 `.smooth`,小浮层用 `.snappy`。
|
|
88
|
+
|
|
89
|
+
```swift
|
|
90
|
+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
91
|
+
|
|
92
|
+
// 小浮层:从触发器一侧长出来
|
|
93
|
+
ZStack(alignment: .topLeading) {
|
|
94
|
+
if isOpen {
|
|
95
|
+
FilterPanel()
|
|
96
|
+
.transition(reduceMotion ? .opacity : .opacity.combined(with: .scale(scale: 0.96, anchor: .topLeading)))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
.animation(.snappy(duration: Deernova.durationBase), value: isOpen)
|
|
100
|
+
|
|
101
|
+
// 侧边抽屉:遮罩和面板一起动
|
|
102
|
+
ZStack(alignment: .leading) {
|
|
103
|
+
if showDrawer {
|
|
104
|
+
Color.scrim.ignoresSafeArea()
|
|
105
|
+
.onTapGesture { showDrawer = false }
|
|
106
|
+
.transition(.opacity)
|
|
107
|
+
DrawerView()
|
|
108
|
+
.frame(width: Deernova.sidebarWidth)
|
|
109
|
+
.transition(reduceMotion ? .opacity : .move(edge: .leading))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
.animation(.smooth(duration: Deernova.durationSlow), value: showDrawer)
|
|
113
|
+
|
|
114
|
+
// 展开收起
|
|
115
|
+
withAnimation(.smooth(duration: Deernova.durationBase)) { isExpanded.toggle() }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 不要
|
|
119
|
+
|
|
120
|
+
- 只做出现动画,关闭时一下子消失;退出比进入还慢。
|
|
121
|
+
- 用 `@keyframes` 做开合,中途反向时跳回起点。
|
|
122
|
+
- 在动画里改宽度、位置、阴影或模糊;对超过一屏的内容做高度动画。
|
|
123
|
+
- 从远处飞入、缩放小于 0.96、回弹或晃动;应用里让列表项逐个错开入场。
|
|
124
|
+
- 用 `setTimeout` 按时长删除元素;遮罩和面板各用一套时长。
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- **对齐**:单行控件在固定高度里垂直居中,没有用内边距、`top` 或等高行高去凑;图标和文字按中线,不同字号按基线;三行以上的内容图标对齐第一行;表格数字右对齐;空状态略高于正中;内容栏限宽居中,页面没有横向滚动,每一栏各自滚动。
|
|
17
17
|
- **间距与圆角**:间距在 4pt 网格上;组与组之间至少是组内的 2 倍;圆角来自组件的 `*-radius` token,嵌套圆角同心;阅读栏宽不超过 `content-max`。
|
|
18
18
|
- **交互**:一屏最多一个主按钮;按钮文字以动词开头;有悬停、按下、选中、禁用、加载、空、错误这些需要的状态,悬停底色是 `fill-hover`、选中和按下是 `fill-selected`,选中项悬停不加深,悬停没有阴影和位移,`:hover` 都在 `@media (hover: hover)` 里;禁用和加载时布局不跳动;键盘焦点清楚;菜单和下拉点击才打开,Esc、点外面、选中一项都能关闭,焦点回到触发器,方向键能走完所有项;弹窗打开时背景不能操作也不滚动,关闭后焦点回到原处,危险确认默认聚焦「取消」。
|
|
19
|
-
-
|
|
19
|
+
- **动效**:打开和关闭都有过程,退出比进入快一档,抽屉和底部弹窗用 `ease-drawer`,中途反向能平滑退回;时长来自 `duration-*`;动效说明因果;没有无目的的循环;减弱动态效果时只剩淡入淡出;切换内容时阅读位置不跳。
|
|
20
20
|
- **无障碍**:两套主题都看过;最大字号或 200% 缩放下不截断;只有图标的按钮有标签,指针端有 tooltip(不是 `title` 属性),键盘聚焦时也显示;降低透明度时玻璃变成实色。
|
|
21
21
|
- **文案**:用“你”;没有感叹号和 emoji;完成态用「已保存」;错误说明原因和解决办法;中文与英文、数字之间有半角空格。
|
|
22
22
|
|
|
@@ -59,7 +59,7 @@ Web 上按钮“上下特别宽”几乎都是同一个原因:用内边距和
|
|
|
59
59
|
- **内容区**:`canvas` 底;对话栏宽度不超过 `content-max`,在内容区居中;段落、工具调用行、卡片、任务清单之间 `space-4`。AI 回答用 `type-body`,不用衬线。
|
|
60
60
|
- **底部**:[Composer](../components/composer.md) 悬浮在内容之上,上面可以贴一条提示,下面是模型和权限的信息行;内容区底部留出它的高度。
|
|
61
61
|
- **侧面板**:右边可以并排打开终端、预览等面板,标签头和顶栏同高。
|
|
62
|
-
- 窄于 `breakpoint-md`(960
|
|
62
|
+
- 窄于 `breakpoint-md`(960)时侧面板变成[抽屉](../components/sheet.md);窄于 `breakpoint-sm`(760)时侧边栏也收起成抽屉,顶栏左侧放菜单按钮。不能出现横向滚动。
|
|
63
63
|
- 页面本身不滚动,侧边栏、内容区、侧面板各自滚动。其他页面类型(列表加详情、设置页、表格)的宽度,以及对齐规则,见 [布局与对齐](../foundations/layout.md)。
|
|
64
64
|
|
|
65
65
|
## 字体
|
|
@@ -72,7 +72,7 @@ Web 上按钮“上下特别宽”几乎都是同一个原因:用内边距和
|
|
|
72
72
|
- 焦点环:`outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset);`,只在 `:focus-visible` 时显示。
|
|
73
73
|
- 细线:`border: var(--stroke-line) solid var(--line);`。
|
|
74
74
|
- 分段控件、开关这类有状态的控件,用 `aria-pressed`、`role="switch"` 或 `role="tab"` 标出当前状态。
|
|
75
|
-
- 弹窗用 `<dialog>` 的 `showModal()`,自带焦点管理和 Esc 关闭;popover 用 `popover` 属性。打开、关闭和焦点规则见 [
|
|
75
|
+
- 弹窗用 `<dialog>` 的 `showModal()`,自带焦点管理和 Esc 关闭;popover 用 `popover` 属性。打开、关闭和焦点规则见 [弹窗、抽屉与 popover](../components/sheet.md)。
|
|
76
76
|
- 菜单和下拉:选值优先用原生 `<select>`,命令菜单和链接列表按 [菜单与下拉](../components/menu.md) 选语义、处理键盘。
|
|
77
77
|
- 悬停效果放在 `@media (hover: hover)` 里,触屏上不会卡在悬停状态。各元素的悬停、按下、选中见 [交互状态](../foundations/states.md)。
|
|
78
78
|
|
|
@@ -215,14 +215,17 @@
|
|
|
215
215
|
|
|
216
216
|
## 动效
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
动效时长和缓动。进入用 ease-enter,退出用 ease-exit 并降一档时长,从屏幕边缘滑入的面板用 ease-drawer。SwiftUI 里默认用 .smooth(等同 .spring(response: 0.35, dampingFraction: 0.85) 的手感)。
|
|
219
219
|
|
|
220
220
|
| Token | 值 | 用途 |
|
|
221
221
|
| --- | --- | --- |
|
|
222
|
-
| `duration-instant` | `120ms` |
|
|
223
|
-
| `duration-base` | `200ms` |
|
|
224
|
-
| `duration-slow` | `320ms` |
|
|
225
|
-
| `ease-standard` | `cubic-bezier(0.2, 0.8, 0.2, 1)` |
|
|
222
|
+
| `duration-instant` | `120ms` | 悬停、按压、选中这类即时反馈;tooltip 的进出;菜单、popover 的退出。 |
|
|
223
|
+
| `duration-base` | `200ms` | 菜单、popover、轻提示的进入;展开收起;弹窗和抽屉的退出。 |
|
|
224
|
+
| `duration-slow` | `320ms` | 弹窗、底部弹窗、侧边抽屉的进入,页面级转场。 |
|
|
225
|
+
| `ease-standard` | `cubic-bezier(0.2, 0.8, 0.2, 1)` | 状态变化和展开收起:颜色、高度、箭头旋转。快进、缓停。 |
|
|
226
|
+
| `ease-enter` | `cubic-bezier(0.16, 1, 0.3, 1)` | 出现:菜单、popover、弹窗、轻提示。起步快、停得很柔,前一半时间就完成大部分位移。 |
|
|
227
|
+
| `ease-exit` | `cubic-bezier(0.4, 0, 0.2, 1)` | 消失:配比进入低一档的时长,不拖泥带水。 |
|
|
228
|
+
| `ease-drawer` | `cubic-bezier(0.32, 0.72, 0, 1)` | 从屏幕边缘滑入滑出的面板:侧边抽屉、底部弹窗,进出都用它。 |
|
|
226
229
|
|
|
227
230
|
## 不透明度
|
|
228
231
|
|
|
@@ -435,7 +438,7 @@
|
|
|
435
438
|
| --- | --- | --- |
|
|
436
439
|
| `menu-max-width` | `320px` | 菜单最大宽度;放不下的文字用省略号。 |
|
|
437
440
|
|
|
438
|
-
###
|
|
441
|
+
### 弹窗、抽屉与 popover
|
|
439
442
|
|
|
440
443
|
见 [components/sheet.md](components/sheet.md)。
|
|
441
444
|
|