@deernova/style 0.3.2 → 0.3.4
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 +10 -8
- package/skill/assets/DeernovaTokens.swift +24 -0
- package/skill/assets/tokens.css +41 -0
- package/skill/assets/tokens.json +137 -0
- package/skill/manifest.json +2 -2
- package/skill/references/components/button.md +7 -5
- package/skill/references/components/card.md +1 -1
- package/skill/references/components/composer.md +2 -1
- package/skill/references/components/empty-state.md +1 -1
- package/skill/references/components/menu.md +1 -0
- package/skill/references/components/message.md +1 -0
- package/skill/references/components/segmented.md +1 -1
- package/skill/references/components/sidebar.md +3 -2
- package/skill/references/components/step-row.md +3 -2
- package/skill/references/components/toolbar.md +4 -3
- package/skill/references/components/tooltip.md +145 -0
- package/skill/references/foundations/accessibility.md +1 -1
- package/skill/references/foundations/color.md +1 -1
- package/skill/references/foundations/elevation.md +1 -0
- package/skill/references/foundations/layout.md +245 -0
- package/skill/references/foundations/spacing.md +3 -8
- package/skill/references/foundations/states.md +98 -0
- package/skill/references/review.md +3 -2
- package/skill/references/scenes/ipados.md +1 -1
- package/skill/references/scenes/macos.md +1 -1
- package/skill/references/scenes/web-marketing.md +1 -1
- package/skill/references/scenes/web.md +3 -2
- package/skill/references/tokens.md +27 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Tooltip 与悬停浮层
|
|
2
|
+
|
|
3
|
+
指针停在一个控件上时浮出的一小段文字,用来说出只有图标的按钮叫什么、快捷键是什么,或者显示被截断的完整文字。悬停卡片是它的大号版本,用来预览链接、人名这类对象。
|
|
4
|
+
|
|
5
|
+
**需要提供**:一段很短的文字(通常就是按钮的名称),可选的快捷键;悬停卡片需要标题、一两行摘要。
|
|
6
|
+
|
|
7
|
+
## 什么时候用
|
|
8
|
+
|
|
9
|
+
| 情况 | 用 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 只有图标的按钮,说出它的名称和快捷键 | tooltip |
|
|
12
|
+
| 文字被截断,显示完整内容 | tooltip |
|
|
13
|
+
| 解释一个概念、给出操作说明 | 写在界面上的说明文字,或者 [popover](sheet.md) |
|
|
14
|
+
| 里面要放链接、按钮、输入框 | [popover](sheet.md) |
|
|
15
|
+
| 预览一个链接、文件或人 | 悬停卡片 |
|
|
16
|
+
| 必须让用户看到的信息(错误、限制、价格) | 直接写在界面上,不要藏进 tooltip |
|
|
17
|
+
|
|
18
|
+
## 尺寸
|
|
19
|
+
|
|
20
|
+
<!-- tokens:tooltip -->
|
|
21
|
+
| Token | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 | 用途 |
|
|
22
|
+
| --- | --- | --- | --- | --- |
|
|
23
|
+
| `tooltip-height` | 24 | 28 | 28 | 单行 tooltip 的高度;折成两行时随内容长高。 |
|
|
24
|
+
| `tooltip-padding-x` | 8 | 10 | 10 | tooltip 左右内边距。 |
|
|
25
|
+
| `tooltip-font-size` | 12 | 13 | 13 | tooltip 字号。 |
|
|
26
|
+
| `tooltip-radius` | 6 | 8 | 8 | tooltip 圆角。 |
|
|
27
|
+
|
|
28
|
+
| Token | 值 | 用途 |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| `tooltip-offset` | `8px` | tooltip 和触发它的控件之间的距离。 |
|
|
31
|
+
| `tooltip-max-width` | `240px` | tooltip 最大宽度,超过时折行,最多两行。 |
|
|
32
|
+
| `tooltip-delay` | `500ms` | 指针停留多久后显示 tooltip 和悬停卡片。SwiftUI 的 .help() 用系统的时间。(仅网页) |
|
|
33
|
+
| `tooltip-skip-window` | `300ms` | 一个 tooltip 刚关闭的这段时间里,移到相邻控件立即显示,不再等待。(仅网页) |
|
|
34
|
+
<!-- /tokens -->
|
|
35
|
+
|
|
36
|
+
- 外观:反色的小块,`accent` 底、`on-accent` 字(浅色主题黑底白字,深色主题白底黑字),圆角 `tooltip-radius`,阴影 `shadow-float`,叠放层级 `z-tooltip`。不加箭头,不加描边。
|
|
37
|
+
- 单行高 `tooltip-height`,左右内边距 `tooltip-padding-x`,字号 `tooltip-font-size`、常规字重。超过 `tooltip-max-width` 时折行,最多两行;两行还放不下,说明内容太多,改用 popover。
|
|
38
|
+
- 快捷键跟在名称后面,间距 `space-2`,颜色是 `on-accent` 的 70%,用等宽数字。
|
|
39
|
+
- 文案:名词或动词短语,和按钮的无障碍名称一致,不加句号,不写「点击这里……」。
|
|
40
|
+
- 悬停卡片:外观和 [popover](sheet.md) 相同(`surface-raised` 底、`line` 描边、`menu-radius`、`shadow-float`、`popover-padding`),宽度不超过 `sheet-width-sm`。
|
|
41
|
+
|
|
42
|
+
## 位置
|
|
43
|
+
|
|
44
|
+
- 默认在控件正下方,水平居中,间距 `tooltip-offset`;下方放不下时翻到上方。窗口底部的控件(例如 Composer 里的按钮)默认放在上方。
|
|
45
|
+
- 靠近窗口边缘时往里推,离边缘至少 `space-2`,不要被裁掉。
|
|
46
|
+
- 盖住旁边的控件没有关系,但不能盖住触发它的控件本身。
|
|
47
|
+
|
|
48
|
+
## 显示与关闭
|
|
49
|
+
|
|
50
|
+
- **指针**:停留 `tooltip-delay` 后显示。一个 tooltip 正在显示,或者刚关闭不到 `tooltip-skip-window` 时,移到相邻的控件立即显示,不再等待,这样扫过一排工具栏按钮时不会一个个卡顿。
|
|
51
|
+
- **键盘**:控件获得键盘焦点(`:focus-visible`)时立即显示,失去焦点时关闭。
|
|
52
|
+
- **关闭**:指针离开控件和 tooltip、按 Esc、按下或点击控件、页面滚动时关闭。指针可以从控件移到 tooltip 上而不关闭(离开后留 100ms 的余地),方便放大镜用户阅读。
|
|
53
|
+
- 同一时间最多显示一个 tooltip。
|
|
54
|
+
- 动效:淡入,用时 `duration-instant`;关闭时直接消失。不位移、不缩放。
|
|
55
|
+
- 悬停卡片:同样停留 `tooltip-delay` 后显示;指针移到卡片上不关闭,离开控件和卡片 300ms 后关闭。卡片里的内容只是预览,点击链接本身就能看到全部,所以键盘和触屏用户不会因为看不到卡片而缺少信息。
|
|
56
|
+
|
|
57
|
+
## 触控端
|
|
58
|
+
|
|
59
|
+
- 触屏没有悬停,不显示 tooltip 和悬停卡片,也不要把长按改成显示 tooltip。只有图标的按钮靠清晰的图标和无障碍名称;含义不明显的操作直接带文字标签。
|
|
60
|
+
- iPad 接触控板或鼠标时有指针,SwiftUI 的 `.help()` 会显示系统 tooltip。
|
|
61
|
+
|
|
62
|
+
## 悬停才出现的操作
|
|
63
|
+
|
|
64
|
+
列表行、消息上悬停才出现的操作按钮(复制、更多)也属于悬停浮层,规则一样:
|
|
65
|
+
|
|
66
|
+
- 放在 `@media (hover: hover) and (pointer: fine)` 里才隐藏;触屏上一直显示,或者收进「更多」菜单。
|
|
67
|
+
- 键盘焦点进入这一行时也要显示(`:focus-within`),隐藏时用 `opacity`,按钮仍然能被 Tab 到。
|
|
68
|
+
- 显示和隐藏不改变布局,不让旁边的内容跳动。
|
|
69
|
+
|
|
70
|
+
## Web
|
|
71
|
+
|
|
72
|
+
- 只有图标的按钮用 `aria-label` 命名,快捷键用 `aria-keyshortcuts`;这时 tooltip 只是把名称显示出来,加 `aria-hidden="true"`,避免读屏读两遍。tooltip 补充了名称以外的信息(被截断的全文、禁用的原因)时,用 `role="tooltip"`,并在控件上用 `aria-describedby` 指向它。
|
|
73
|
+
- 禁用的按钮收不到指针事件。要用 tooltip 解释禁用原因时,用 `aria-disabled="true"` 代替 `disabled`,保留焦点和悬停。
|
|
74
|
+
- 不要用 `title` 属性:显示时间不能控制,键盘聚焦时不显示,样式也和界面不一致。
|
|
75
|
+
- tooltip 用脚本显示和定位。要盖在弹窗之上时,给它加 `popover="manual"`(浏览器支持时用 `popover="hint"`)放到顶层。只在工具栏这类简单场景里,可以用下面的 `.dn-tooltip-anchor` 直接定位。
|
|
76
|
+
|
|
77
|
+
```css
|
|
78
|
+
.dn-tooltip {
|
|
79
|
+
box-sizing: border-box;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
width: max-content;
|
|
83
|
+
max-width: min(var(--tooltip-max-width), 100vw - 2 * var(--space-2));
|
|
84
|
+
min-height: var(--tooltip-height);
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: var(--space-1) var(--tooltip-padding-x);
|
|
87
|
+
border: 0;
|
|
88
|
+
border-radius: var(--tooltip-radius);
|
|
89
|
+
background: var(--accent);
|
|
90
|
+
color: var(--on-accent);
|
|
91
|
+
box-shadow: var(--shadow-float);
|
|
92
|
+
font: var(--weight-regular) var(--tooltip-font-size)/var(--type-caption-line) var(--font-sans);
|
|
93
|
+
overflow-wrap: anywhere;
|
|
94
|
+
transition: opacity var(--duration-instant) var(--ease-standard);
|
|
95
|
+
}
|
|
96
|
+
.dn-tooltip[popover] { position: fixed; inset: auto; }
|
|
97
|
+
@starting-style { .dn-tooltip:popover-open { opacity: 0; } }
|
|
98
|
+
.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
|
+
.dn-tooltip-anchor { position: relative; display: inline-flex; }
|
|
100
|
+
.dn-tooltip-anchor > .dn-tooltip { position: absolute; top: calc(100% + var(--tooltip-offset)); left: 50%; z-index: var(--z-tooltip); translate: -50% 0; }
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<div class="dn-row">
|
|
105
|
+
<button class="dn-icon-button" type="button" aria-label="新建笔记" aria-keyshortcuts="Meta+N"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
|
|
106
|
+
<span class="dn-tooltip-anchor">
|
|
107
|
+
<button class="dn-icon-button" type="button" aria-label="搜索" aria-keyshortcuts="Meta+K"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="M20 20l-4.5-4.5"/></svg></button>
|
|
108
|
+
<span class="dn-tooltip" aria-hidden="true">搜索<span class="dn-tooltip-shortcut">⌘K</span></span>
|
|
109
|
+
</span>
|
|
110
|
+
<button class="dn-icon-button" type="button" aria-label="设置"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/></svg></button>
|
|
111
|
+
</div>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
示例里的 tooltip 画成显示状态。真实页面里由脚本按上面的时间显示和关闭。
|
|
115
|
+
|
|
116
|
+
## SwiftUI
|
|
117
|
+
|
|
118
|
+
在 macOS 和接了指针的 iPadOS 上,`.help()` 显示系统 tooltip,时间和位置由系统决定。iPhone 上不显示。
|
|
119
|
+
|
|
120
|
+
```swift
|
|
121
|
+
Button {
|
|
122
|
+
search()
|
|
123
|
+
} label: {
|
|
124
|
+
Image(systemName: "magnifyingglass")
|
|
125
|
+
}
|
|
126
|
+
.keyboardShortcut("k")
|
|
127
|
+
.accessibilityLabel("搜索")
|
|
128
|
+
.help("搜索")
|
|
129
|
+
|
|
130
|
+
// 被截断的文字:给出完整内容
|
|
131
|
+
Text(note.title)
|
|
132
|
+
.lineLimit(1)
|
|
133
|
+
.help(note.title)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
工具栏按钮用 `Label("搜索", systemImage: "magnifyingglass")` 写,标题会成为无障碍名称,再加 `.help()` 显示 tooltip。SwiftUI 没有悬停卡片的系统组件,不要自己做;需要预览时,用 `.contextMenu(menuItems:preview:)`(右键或长按显示)。
|
|
137
|
+
|
|
138
|
+
## 不要
|
|
139
|
+
|
|
140
|
+
- 用 tooltip 放必须看到的信息、长段说明,或者链接和按钮。
|
|
141
|
+
- 给已经有文字标签的按钮再加一个重复的 tooltip(只补快捷键时除外)。
|
|
142
|
+
- 用 `title` 属性代替 tooltip;只在悬停时显示,键盘聚焦时不显示。
|
|
143
|
+
- 在触屏上用长按显示 tooltip。
|
|
144
|
+
- 给 tooltip 加箭头、彩色底色或描边;一次显示多个 tooltip。
|
|
145
|
+
- 指针一碰到就立即显示(扫过工具栏时满屏闪烁);指针移到 tooltip 上它就消失。
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
## 其他
|
|
23
23
|
|
|
24
|
-
-
|
|
24
|
+
- 只有图标的按钮必须有无障碍标签;指针端的 [tooltip](../components/tooltip.md) 在键盘聚焦时也要显示,能用 Esc 关闭,指针移上去不消失。
|
|
25
25
|
- 只靠颜色传达的信息,一律补上图标或文字。
|
|
26
26
|
- 键盘焦点环清楚可见(`focus-ring`,只在 `:focus-visible` 时显示);Tab 顺序合理,Esc 能关闭弹层。
|
|
27
27
|
- 开启「减弱动态效果」时只保留淡入淡出;开启「降低透明度」时玻璃换成实色纸面。
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
## 交互底色
|
|
47
47
|
|
|
48
|
-
悬停用 `fill-hover`,选中用 `fill-selected`。它们是半透明的黑或白,叠在任何纸面上都成立,所以列表行、侧边栏项、次按钮、文字按钮都用它们,而不是另选一个灰色。选中要一眼看得出来:`fill-selected`
|
|
48
|
+
悬停用 `fill-hover`,选中用 `fill-selected`。它们是半透明的黑或白,叠在任何纸面上都成立,所以列表行、侧边栏项、次按钮、文字按钮都用它们,而不是另选一个灰色。选中要一眼看得出来:`fill-selected` 比悬停明显得多。每种元素在悬停、按下、选中时怎么变,见 [交互状态](states.md)。
|
|
49
49
|
|
|
50
50
|
## 仅网页
|
|
51
51
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| 2 浮层 | Composer、弹出菜单、弹窗、浮动面板 | `shadow-float` | `z-dropdown`、`z-overlay` |
|
|
10
10
|
| 玻璃 | SwiftUI 端的工具栏、标签栏、浮动按钮;官网页眉 | 系统玻璃自带;官网页眉用 `shadow-glass` | `z-sticky` |
|
|
11
11
|
| 提示 | 轻提示 | 跟随玻璃 | `z-toast` |
|
|
12
|
+
| Tooltip | tooltip、悬停卡片 | `shadow-float` | `z-tooltip` |
|
|
12
13
|
|
|
13
14
|
- 卡片不加阴影。卡片和纸面的区分靠 `line` 线和亮度差。
|
|
14
15
|
- Web 应用的顶栏是不透明的纸面,吸顶时用 `z-sticky`,底部一条 `stroke-line`,不加阴影。
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# 布局与对齐
|
|
2
|
+
|
|
3
|
+
页面怎么分栏、内容多宽、元素怎么对齐、哪里滚动。间距刻度见 [间距](spacing.md),数值见 [设计变量](../tokens.md) 的「布局」。
|
|
4
|
+
|
|
5
|
+
## 页面结构
|
|
6
|
+
|
|
7
|
+
| 页面 | 结构 | 内容宽度 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| 对话、编辑器 | 侧边栏、顶栏、居中的内容栏、底部 Composer,右边可以开侧面板 | 内容栏不超过 `content-max` |
|
|
10
|
+
| 列表加详情(笔记、邮件、任务) | 侧边栏、列表栏(`pane-width`)、详情 | 详情里的正文不超过 `content-max` |
|
|
11
|
+
| 设置页、表单页 | 单栏居中,分组列表或表单 | 不超过 `form-max` |
|
|
12
|
+
| 表格、看板、数据页 | 顶部一行筛选和操作,下面是表格或卡片网格 | 不超过 `wide-max`,表格比它宽时在表格内部横向滚动 |
|
|
13
|
+
| 文档、长文 | 左侧目录(`sidebar-width`),正文栏居中 | 正文不超过 `content-max` |
|
|
14
|
+
| 官网 | 12 栏网格,栏间距 `grid-gutter` | 不超过 `wide-max` |
|
|
15
|
+
|
|
16
|
+
- 对话栏:消息左右各留 44,Composer 左右各留 32,宽度不超过 `content-max`。
|
|
17
|
+
- 一屏只有一个主内容区。侧边栏、列表栏、侧面板是辅助栏,宽度固定或可以拖动,主内容区占剩下的空间。
|
|
18
|
+
- 侧边栏可以在 `sidebar-width-min` 和 `sidebar-width-max` 之间拖动,双击分隔线恢复默认宽度 `sidebar-width`。
|
|
19
|
+
- 内容比可用宽度窄时,在主内容区里水平居中,两边留白;不要把文字拉满宽屏。左右至少留 `page-margin`。
|
|
20
|
+
- 各平台的具体骨架见场景说明:[Web 应用](../scenes/web.md)、[官网与文档](../scenes/web-marketing.md)、[macOS](../scenes/macos.md)、[iPadOS](../scenes/ipados.md)。
|
|
21
|
+
|
|
22
|
+
## 网格
|
|
23
|
+
|
|
24
|
+
- 官网和文档用 12 栏网格,栏间距 `grid-gutter`,左右边距 `page-margin`;窄于 `breakpoint-md` 时变成 6 栏,窄于 `breakpoint-sm` 时变成单栏。
|
|
25
|
+
- 应用界面不用栏网格,用 flex 和 grid 加间距 token 排版。卡片网格按可用宽度自动增减列数,见 [卡片](../components/card.md)。
|
|
26
|
+
|
|
27
|
+
## 水平对齐
|
|
28
|
+
|
|
29
|
+
- 文字左对齐,标题也是。只有空状态、加载状态、官网首屏标题居中;系统 Alert 的排法交给系统。
|
|
30
|
+
- 同一栏里的标题、正文、表单和列表共用一条左边线,也就是容器的内边距。分组标题和它下面的行左边缘对齐。
|
|
31
|
+
- 行尾的元素(计数、快捷键、开关、箭头、按钮)用 `margin-left: auto` 推到最右边,不要用空格或固定宽度去凑。
|
|
32
|
+
- 表格里的数字、金额、时间右对齐,并用等宽数字(`tabular-nums`);文字列左对齐,表头和本列的内容对齐方式一致。
|
|
33
|
+
- 弹窗和表单底部的按钮靠右,主按钮在最右边;页面级的操作放在顶栏右侧,或者内容区顶部的右侧。
|
|
34
|
+
|
|
35
|
+
## 垂直对齐
|
|
36
|
+
|
|
37
|
+
按内容选一种,不要混用:
|
|
38
|
+
|
|
39
|
+
1. **单行控件和行:在固定高度里垂直居中。** 按钮、输入框、列表行、菜单项、标签、顶栏都用 `height`(或 `min-height`)加 `display: flex; align-items: center`,文字行高为 1。不要用上下内边距撑,也不要用 `padding-top`、`top: 1px` 微调。
|
|
40
|
+
2. **图标和文字:按中线对齐。** 同一行的图标和文字用 `align-items: center`,不按基线,也不用 `vertical-align: middle`。图标尺寸跟着字号:12–13 号字配 `icon-size-sm`,14–15 号配 `icon-size-md`,更大的配 `icon-size-lg`。
|
|
41
|
+
3. **不同字号的文字并排:按基线对齐。** 标题旁边的计数、单位、时间(例如 18 号标题旁边 13 号的「128 条」)用 `align-items: baseline`;按中线对齐时小字看起来会往上飘。
|
|
42
|
+
4. **两行条目(标题加一行说明):整行居中。** 设置项、列表行、横幅里,前导图标和尾部的开关、箭头、按钮都在整行里垂直居中。
|
|
43
|
+
5. **长内容(三行以上,或者高度不固定):对齐第一行。** 消息、任务说明、多行横幅里,前导的图标、头像、复选框和第一行文字的中线对齐,不跟着整块内容居中。写法是 `align-items: flex-start`,图标的上外边距等于(行高 − 图标高)÷ 2。尾部只有一个短按钮时可以在整块里居中(横幅);内容很长时,尾部操作也对齐第一行,或者放到内容下方。
|
|
44
|
+
6. **标签和控件左右并排:中线对齐。** 设置项、筛选条里的标签和控件中线对齐;控件是多行文本框时,标签对齐它的第一行。
|
|
45
|
+
7. **表格:** 单行的单元格垂直居中;有单元格会折行时,整行改成顶部对齐。
|
|
46
|
+
8. **卡片网格:** 同一行的卡片顶部对齐,高度由内容决定。
|
|
47
|
+
9. **空状态、加载、整页提示:视觉居中。** 在可用区域里水平居中,垂直方向放在略高于正中的位置:上方留白和下方留白大约是 2 : 3。正好放在几何中心看起来会偏下,贴顶又像没加载完。
|
|
48
|
+
10. **光学修正:** 播放、发送这类不对称的图标,在圆形按钮里按视觉重心居中,可以偏移 1px;这是唯一允许手动微调的情况。
|
|
49
|
+
|
|
50
|
+
同一行的控件用同一个高度档位;必须混用时(输入框旁边一个小按钮),按中线对齐。大号标题和图标并排时,支持 `text-box: trim-both cap alphabetic` 的浏览器可以用它去掉字体上下的留白,让字的可见部分居中。
|
|
51
|
+
|
|
52
|
+
## 滚动与固定
|
|
53
|
+
|
|
54
|
+
- 应用里页面本身不滚动:侧边栏、列表栏、内容区、侧面板各自是一个纵向滚动区域,互不带动。文档和官网整页滚动,页眉吸顶。
|
|
55
|
+
- 顶栏、侧面板的标签头、表头固定,内容从下面滚过去。Composer 固定在内容区底部,内容区底部留出它的高度。
|
|
56
|
+
- 同一方向不要嵌套两个滚动区域。表格横向滚动只发生在表格内部,页面不能横向滚动。
|
|
57
|
+
- 弹出层和面板内部滚动时加 `overscroll-behavior: contain`,不带动后面的页面。
|
|
58
|
+
- 跳到页面锚点时,用 `scroll-padding-top` 留出吸顶元素的高度。
|
|
59
|
+
|
|
60
|
+
## 滚动条
|
|
61
|
+
|
|
62
|
+
- **Web 统一用一种细滚动条。** 所有滚动区域,包括页面本身,都用同一种:占 `scrollbar-size`(10px),中间是 `scrollbar-thumb-size`(6px)粗的胶囊滑块,`line-control` 色,悬停和拖动时变 `ink-tertiary`;轨道透明,没有两端的箭头按钮。滑块颜色来自 token,深色主题自动跟着换。
|
|
63
|
+
- **在全局样式里写一次。** 用下面的 CSS:Chrome、Edge、Safari 用 `::-webkit-scrollbar` 画,Firefox 用 `scrollbar-width: thin` 加 `scrollbar-color`。Chrome 里一个元素只要设置了 `scrollbar-width` 或 `scrollbar-color`,`::-webkit-scrollbar` 对它就失效,所以除了隐藏滚动条,组件里不要再写这两个属性,也不要给某个组件单独写一套滚动条或用脚本模拟。
|
|
64
|
+
- **SwiftUI 用系统滚动条。** 原生端的滚动指示条跟随用户的系统设置,不要改。
|
|
65
|
+
- **预留位置,不让内容跳动。** 整页滚动的页面(文档、官网)在 `html` 上设 `scrollbar-gutter: stable`;内容居中的主滚动区域(对话栏)也一样。这样内容从不满一屏变成超出一屏、或者弹窗打开锁住页面滚动时,滚动条出现和消失不会让内容左右跳。应用里页面本身不滚动,不要在 `html` 上预留。
|
|
66
|
+
- **内容别被压住。** 滚动区域的内边距写在滚动容器里面,右侧至少留 `space-2`,滚动时才浮现的滚动条不会盖住文字。
|
|
67
|
+
- **能隐藏的只有横向滑动条。** 标签、筛选项、卡片排成一行横向滑动时,可以隐藏滚动条(`scrollbar-width: none`),但必须有提示:末端渐隐、露出半张卡片,或者左右翻页按钮;滚到底时去掉渐隐。纵向的滚动区域一律不隐藏滚动条。
|
|
68
|
+
- **键盘也能滚。** 滚动区域里没有可以聚焦的内容时,给容器加 `tabindex="0"`、`role="region"` 和 `aria-label`,键盘用户才能用方向键滚动它,读屏也能读出它是什么。
|
|
69
|
+
- **把宽度算进去。** 细滚动条在 Chrome、Edge、Safari 里一直显示,占 `scrollbar-size`,在 Mac 上也不再是滚动时才浮现;Firefox 跟随系统设置。栏宽、居中和右侧留白按占位的情况检查:内容不被挤窄,没有多出来的横向滚动条。
|
|
70
|
+
- **滚动不加戏。** 不接管滚轮和触控板,不改滚动速度和惯性;`scroll-behavior: smooth` 只用于页内锚点跳转,开启「减弱动态效果」时去掉;吸附滚动(`scroll-snap`)只用于横向卡片条。
|
|
71
|
+
- **提示还有内容,用线不用阴影。** 顶栏、表头本来就有 1px 底线;弹窗、面板这类平时没有分隔线的区域,内容能滚动时才加上(见[弹窗](../components/sheet.md))。不在内容边缘加阴影或渐变遮罩,横向滑动条的末端渐隐除外。
|
|
72
|
+
|
|
73
|
+
## 响应式
|
|
74
|
+
|
|
75
|
+
- `breakpoint-sm`(760)以下侧边栏收起成抽屉,多栏变单栏;`breakpoint-md`(960)以下侧面板变成抽屉,目录这类次要栏收起;`breakpoint-lg`(1280)以上内容不再变宽,两侧留白。断点只能写在媒体查询里,没有对应的 CSS 变量。
|
|
76
|
+
- 页面骨架跟着窗口宽度变(媒体查询);组件跟着自己容器的宽度变(`@container`),同一张卡片放在主内容区和侧面板里都能排好。
|
|
77
|
+
- 任何宽度、200% 缩放下都不能横向滚动。
|
|
78
|
+
- 手机网页里固定在边缘的元素(底部操作条、轻提示)要让出安全区:`env(safe-area-inset-bottom)`,页面的 viewport 加 `viewport-fit=cover`。
|
|
79
|
+
|
|
80
|
+
## Web
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
.dn-column { box-sizing: border-box; width: min(100%, var(--content-max) + 2 * var(--page-margin)); margin-inline: auto; padding-inline: var(--page-margin); }
|
|
84
|
+
.dn-column.is-form { width: min(100%, var(--form-max) + 2 * var(--page-margin)); }
|
|
85
|
+
.dn-column.is-wide { width: min(100%, var(--wide-max) + 2 * var(--page-margin)); }
|
|
86
|
+
.dn-scroll { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
|
|
87
|
+
.dn-scroll.is-stable { scrollbar-gutter: stable; }
|
|
88
|
+
.dn-scroll-x { display: flex; gap: var(--gap-inline); overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; scroll-snap-type: x proximity; }
|
|
89
|
+
.dn-scroll-x::-webkit-scrollbar { display: none; }
|
|
90
|
+
.dn-scroll-x > * { flex: none; scroll-snap-align: start; }
|
|
91
|
+
.dn-scroll-x:not(.is-end) { mask-image: linear-gradient(to left, transparent, var(--ink) var(--space-8)); }
|
|
92
|
+
.dn-baseline { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
|
|
93
|
+
.dn-baseline-title { margin: 0; font: var(--type-title); }
|
|
94
|
+
.dn-baseline-meta { color: var(--ink-tertiary); font: var(--type-footnote); font-variant-numeric: tabular-nums; }
|
|
95
|
+
.dn-item { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
|
|
96
|
+
.dn-item > .dn-icon { color: var(--ink-secondary); }
|
|
97
|
+
.dn-item-text { flex: 1; min-width: 0; }
|
|
98
|
+
.dn-item-title { margin: 0; color: var(--ink); font: var(--weight-medium) var(--type-body-size)/var(--type-body-line) var(--font-sans); }
|
|
99
|
+
.dn-item-detail { margin: 0; color: var(--ink-secondary); font: var(--type-callout); }
|
|
100
|
+
.dn-item-end { flex: none; margin-left: auto; }
|
|
101
|
+
.dn-item.is-top { align-items: flex-start; }
|
|
102
|
+
.dn-item.is-top > .dn-icon { margin-top: calc((var(--type-body-line) - var(--icon-size-md)) / 2); }
|
|
103
|
+
.dn-item.is-top > .dn-item-end { margin-top: calc((var(--type-body-line) - var(--button-height-sm)) / 2); }
|
|
104
|
+
.dn-centered { display: grid; grid-template-rows: 2fr auto 3fr; justify-items: center; min-height: 100%; text-align: center; }
|
|
105
|
+
.dn-centered > * { grid-row: 2; }
|
|
106
|
+
@supports selector(::-webkit-scrollbar) {
|
|
107
|
+
::-webkit-scrollbar { width: var(--scrollbar-size); height: var(--scrollbar-size); }
|
|
108
|
+
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent; }
|
|
109
|
+
::-webkit-scrollbar-thumb { border: calc((var(--scrollbar-size) - var(--scrollbar-thumb-size)) / 2) solid transparent; border-radius: var(--radius-full); background: var(--line-control) padding-box; }
|
|
110
|
+
::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active { background-color: var(--ink-tertiary); }
|
|
111
|
+
::-webkit-scrollbar-button { display: none; }
|
|
112
|
+
}
|
|
113
|
+
@supports not selector(::-webkit-scrollbar) {
|
|
114
|
+
* { scrollbar-width: thin; scrollbar-color: var(--line-control) transparent; }
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
不同字号并排,按基线对齐:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<div class="dn-baseline">
|
|
122
|
+
<h3 class="dn-baseline-title">笔记</h3>
|
|
123
|
+
<span class="dn-baseline-meta">128 条</span>
|
|
124
|
+
</div>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
两行条目,图标和尾部按钮在整行里居中:
|
|
128
|
+
|
|
129
|
+
```html
|
|
130
|
+
<div class="dn-item">
|
|
131
|
+
<svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 18h10a4 4 0 0 0 .5-8 6 6 0 0 0-11.5 1.5A3.5 3.5 0 0 0 7 18z"/></svg>
|
|
132
|
+
<div class="dn-item-text">
|
|
133
|
+
<p class="dn-item-title">iCloud 同步</p>
|
|
134
|
+
<p class="dn-item-detail">在登录同一账户的设备之间保持一致。</p>
|
|
135
|
+
</div>
|
|
136
|
+
<button class="dn-button is-sm dn-item-end" type="button">管理</button>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
长内容,图标和按钮对齐第一行:
|
|
141
|
+
|
|
142
|
+
```html
|
|
143
|
+
<div class="dn-item is-top">
|
|
144
|
+
<svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg>
|
|
145
|
+
<div class="dn-item-text">
|
|
146
|
+
<p class="dn-item-title">会议纪要已整理</p>
|
|
147
|
+
<p class="dn-item-detail">按议题分成了三段,待办事项单独列在最后,负责人和截止日期都已经补上。原始录音的转写保留在附件里,需要核对时可以打开对照。</p>
|
|
148
|
+
</div>
|
|
149
|
+
<button class="dn-button is-sm dn-item-end" type="button">打开</button>
|
|
150
|
+
</div>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
空状态在可用区域里视觉居中(上下留白约 2 : 3):
|
|
154
|
+
|
|
155
|
+
```html
|
|
156
|
+
<div class="dn-centered">
|
|
157
|
+
<div class="dn-empty">
|
|
158
|
+
<svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5"/></svg>
|
|
159
|
+
<p class="dn-empty-title">还没有笔记</p>
|
|
160
|
+
<p class="dn-empty-body">点右上角的加号新建一条。</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
滚动区域用细滚动条;横向滑动条隐藏滚动条、末端渐隐:
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<div class="dn-scroll" tabindex="0" role="region" aria-label="最近的会话">
|
|
169
|
+
<p class="dn-item-title">整理周会记录</p>
|
|
170
|
+
<p class="dn-item-title">修复登录页的焦点顺序</p>
|
|
171
|
+
<p class="dn-item-title">起草发布说明</p>
|
|
172
|
+
<p class="dn-item-title">对比三种排版方案</p>
|
|
173
|
+
<p class="dn-item-title">检查深色主题的对比度</p>
|
|
174
|
+
<p class="dn-item-title">补全弹窗的键盘操作</p>
|
|
175
|
+
<p class="dn-item-title">写 tooltip 的测试</p>
|
|
176
|
+
<p class="dn-item-title">更新安装说明</p>
|
|
177
|
+
<p class="dn-item-title">导出图标</p>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="dn-scroll-x" tabindex="0" role="region" aria-label="筛选">
|
|
180
|
+
<span class="dn-tag">全部</span><span class="dn-tag">今天</span><span class="dn-tag">本周</span><span class="dn-tag">已完成</span><span class="dn-tag">进行中</span><span class="dn-tag">有附件</span><span class="dn-tag">我创建的</span><span class="dn-tag">共享给我</span><span class="dn-tag">已归档</span><span class="dn-tag">草稿</span>
|
|
181
|
+
</div>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## SwiftUI
|
|
185
|
+
|
|
186
|
+
`Form`、`List`、`NavigationSplitView` 自带对齐和滚动,不要改;滚动指示条保持系统默认,跟随用户的系统设置。只有横向卡片条可以用 `.scrollIndicators(.hidden)`,并露出半张卡片提示还能滑动。自己排的内容按下面写:
|
|
187
|
+
|
|
188
|
+
```swift
|
|
189
|
+
// 图标和文字:用 Label,图标会对齐到第一行文字
|
|
190
|
+
Label("同步", systemImage: "arrow.triangle.2.circlepath")
|
|
191
|
+
|
|
192
|
+
// 不同字号并排:按基线
|
|
193
|
+
HStack(alignment: .firstTextBaseline, spacing: Deernova.space2) {
|
|
194
|
+
Text("笔记").font(.deernovaTitle)
|
|
195
|
+
Text("128 条").font(.footnote).monospacedDigit().foregroundStyle(.inkTertiary)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 两行条目:整行居中
|
|
199
|
+
HStack(spacing: Deernova.space3) {
|
|
200
|
+
Label {
|
|
201
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
202
|
+
Text("iCloud 同步")
|
|
203
|
+
Text("在登录同一账户的设备之间保持一致。").font(.footnote).foregroundStyle(.inkSecondary)
|
|
204
|
+
}
|
|
205
|
+
} icon: {
|
|
206
|
+
Image(systemName: "icloud")
|
|
207
|
+
}
|
|
208
|
+
Spacer()
|
|
209
|
+
Toggle("iCloud 同步", isOn: $sync).labelsHidden()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 内容栏限宽并居中
|
|
213
|
+
ScrollView {
|
|
214
|
+
content
|
|
215
|
+
.frame(maxWidth: Deernova.contentMax, alignment: .leading)
|
|
216
|
+
.padding(.horizontal, Deernova.pageMargin)
|
|
217
|
+
.frame(maxWidth: .infinity)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 横向卡片条:隐藏滚动条,按卡片吸附,露出下一张的一部分
|
|
221
|
+
ScrollView(.horizontal) {
|
|
222
|
+
LazyHStack(spacing: Deernova.cardGap) {
|
|
223
|
+
ForEach(notes) { NoteCard(note: $0).frame(width: 240) }
|
|
224
|
+
}
|
|
225
|
+
.scrollTargetLayout()
|
|
226
|
+
}
|
|
227
|
+
.scrollIndicators(.hidden)
|
|
228
|
+
.scrollTargetBehavior(.viewAligned)
|
|
229
|
+
.contentMargins(.horizontal, Deernova.pageMargin)
|
|
230
|
+
|
|
231
|
+
// 空状态:系统组件已经处理了视觉居中
|
|
232
|
+
ContentUnavailableView("还没有笔记", systemImage: "doc", description: Text("点右上角的加号新建一条。"))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## 不要
|
|
236
|
+
|
|
237
|
+
- 用 `padding-top`、`margin-top`、`position: relative; top: 1px` 去凑垂直居中(第一行对齐的公式和图标的 1px 光学修正除外)。
|
|
238
|
+
- 用和高度相等的 `line-height` 让单行文字居中:文字一折行就会溢出。用 flex 居中。
|
|
239
|
+
- 用 `vertical-align: middle` 对齐图标和文字。
|
|
240
|
+
- 不同字号并排时按中线对齐;多行内容里让图标、头像跟着整块内容居中。
|
|
241
|
+
- 把空状态贴在顶部,或者正好放在几何中心。
|
|
242
|
+
- 把正文拉满宽屏;正文和普通标题居中排版。
|
|
243
|
+
- 页面和里面的栏同时滚动;同一方向嵌套滚动;页面出现横向滚动。
|
|
244
|
+
- 给单个组件另写一套滚动条,或者用脚本模拟滚动条;隐藏纵向滚动区域的滚动条;接管滚轮或改滚动速度;在应用的 `html` 上预留滚动条位置。
|
|
245
|
+
- 按设备型号写死布局;用可用宽度和尺寸类别判断。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 间距
|
|
2
2
|
|
|
3
|
-
4pt 网格,整体偏紧凑。层次靠间距建立,不靠加更多的线和框。数值见 [设计变量](../tokens.md)
|
|
3
|
+
4pt 网格,整体偏紧凑。层次靠间距建立,不靠加更多的线和框。数值见 [设计变量](../tokens.md) 的「间距」。
|
|
4
4
|
|
|
5
5
|
## 刻度和语义间距
|
|
6
6
|
|
|
@@ -26,12 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
## 布局
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
- 侧边栏默认宽 `sidebar-width`(桌面 288),用户可以拖动调整。iPhone 没有侧边栏。
|
|
31
|
-
- Web 应用的顶栏高 `toolbar-height`(44),侧面板的标签头和它对齐。
|
|
32
|
-
- 对话栏:内容左右各留 44,Composer 左右各留 32,宽度不超过 `content-max`。
|
|
33
|
-
- 断点:`breakpoint-sm`(760)以下侧边栏收起、多栏变单栏;`breakpoint-md`(960)以下收起目录这类次要栏;`breakpoint-lg`(1280)以上内容不再变宽。断点只能写在媒体查询里,所以没有对应的 CSS 变量。
|
|
34
|
-
- 窄屏下重新排列,不能出现横向滚动。
|
|
29
|
+
分栏、内容宽度、对齐(包括垂直居中)、滚动和断点,见 [布局与对齐](layout.md)。
|
|
35
30
|
|
|
36
31
|
## 触控端比指针端大是有意的
|
|
37
32
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 交互状态
|
|
2
|
+
|
|
3
|
+
悬停、按下、选中、焦点、禁用分别怎么表现。颜色只有两个半透明的底色:悬停 `fill-hover`,按下和选中 `fill-selected`,数值见 [设计变量](../tokens.md) 的「颜色」。
|
|
4
|
+
|
|
5
|
+
## 各元素的状态
|
|
6
|
+
|
|
7
|
+
| 元素 | 悬停 | 按下 | 选中(当前项) |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| 文字按钮、图标按钮、工具栏按钮 | `fill-hover` 底,文字和图标从 `ink-secondary` 变 `ink` | `fill-selected` 底 | `fill-selected` 底、`ink`(`aria-pressed`) |
|
|
10
|
+
| 次按钮(灰底) | 在 `fill-selected` 上再叠一层 `fill-hover` | 再叠一层 `fill-selected` | — |
|
|
11
|
+
| 主按钮、实色危险按钮 | 不透明度 `opacity-solid-hover` | 同悬停 | — |
|
|
12
|
+
| 浅底危险按钮 | 在 `danger-soft` 上叠一层 `fill-hover` | 叠一层 `fill-selected` | — |
|
|
13
|
+
| 列表行、侧边栏项、菜单项、步骤行 | `fill-hover` 底 | `fill-selected` 底 | `fill-selected` 底,字重不变;选中项悬停时保持 `fill-selected`,不再叠加 |
|
|
14
|
+
| 可点的卡片 | 描边从 `line` 变 `line-control`,底色不变 | 同悬停 | 描边 `ink`,宽 `stroke-control` |
|
|
15
|
+
| 分段控件、标签页、折叠标题 | 文字从 `ink-secondary` 变 `ink`,不加底色 | — | 分段控件的选中块;标签页 `fill-selected` 底 |
|
|
16
|
+
| 本身是 `ink` 的文字链接(字标、页眉的产品名) | 变 `ink-secondary` | — | — |
|
|
17
|
+
| 正文里的链接 | 下划线从 `line-control` 变成文字颜色 | — | — |
|
|
18
|
+
| 输入框 | 不变 | — | 聚焦:边框 `focus-ring`,外圈 3px `accent-soft` |
|
|
19
|
+
| 玻璃按钮 | 比命中区小一圈的 `fill-hover` | `fill-selected` | `fill-selected`,图标 `ink` |
|
|
20
|
+
|
|
21
|
+
## 规则
|
|
22
|
+
|
|
23
|
+
- **悬停底色只用 `fill-hover`。** 它是半透明的黑(深色主题是白),叠在 `canvas`、`surface`、`surface-raised` 和玻璃上都成立。不要另选一个实色灰(比如 `#f2f2f2`),换一张纸面或换到深色主题就不对了。
|
|
24
|
+
- **三档,不叠加。** 悬停最轻,按下和选中一样深。选中项被悬停时保持选中的底色;按下时用 `fill-selected` 换掉 `fill-hover`,不是两层叠在一起。次按钮本身就是灰底,悬停和按下在它上面叠一层,这是唯一的叠加。
|
|
25
|
+
- **反馈只改颜色。** 悬停和按下只改底色、文字颜色或描边颜色(三者可以一起变,比如图标按钮底色和图标颜色一起变),不加阴影,不上浮,不缩放,不改字重和尺寸。
|
|
26
|
+
- **底色的形状和元素一样。** 悬停底色和元素同样大、同样的圆角(`row-radius`、`button-radius`、`menu-radius` 减内边距)。列表行在容器里左右缩进,不铺满整栏;命中区比可见尺寸大时,底色只画在可见范围里。
|
|
27
|
+
- **只在能悬停的设备上。** `:hover` 一律写在 `@media (hover: hover)` 里,触屏上不会停在悬停状态。触屏没有悬停,按下时(`:active`)立即给 `fill-selected` 反馈。
|
|
28
|
+
- **过渡要快。** 底色、文字颜色、描边在 `duration-instant` 内变化,进入和离开一样快,不加延迟。
|
|
29
|
+
- **焦点不靠悬停底色。** 键盘焦点用焦点环(`stroke-focus` 宽的 `focus-ring`,只在 `:focus-visible` 时出现)。列表和菜单里用方向键移动的高亮,用 `fill-hover` 加内描边焦点环,见 [菜单](../components/menu.md)。
|
|
30
|
+
- **禁用没有反馈。** 禁用的元素整体降到 `opacity-disabled`,没有悬停和按下的变化,光标恢复默认;不要隐藏,保持在原位。
|
|
31
|
+
- **光标。** 可以点的元素(按钮、行、标签、卡片、链接)用 `cursor: pointer`;文字输入用 `text`;可以拖动的用 `grab`,拖动中用 `grabbing`。
|
|
32
|
+
- **拖放。** 拖起来的元素用 `shadow-float` 浮起;可以放下的位置用 `fill-selected` 底,或者 `line-style-dropzone` 的虚线框。
|
|
33
|
+
|
|
34
|
+
## Web
|
|
35
|
+
|
|
36
|
+
下面的 `.dn-state` 是通用写法,适合行、文字按钮这类「透明底、悬停出底色」的元素。按钮、卡片、分段控件的写法见各自的组件说明。示例里的 `.is-hover`、`.is-active` 只用来在文档里把状态画出来。
|
|
37
|
+
|
|
38
|
+
```css
|
|
39
|
+
.dn-state { cursor: pointer; transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-instant) var(--ease-standard); }
|
|
40
|
+
@media (hover: hover) { .dn-state:hover { background-color: var(--fill-hover); color: var(--ink); } }
|
|
41
|
+
.dn-state:active, .dn-state.is-active { background-color: var(--fill-selected); }
|
|
42
|
+
.dn-state.is-hover { background-color: var(--fill-hover); color: var(--ink); }
|
|
43
|
+
.dn-state:is([aria-selected="true"], [aria-current="page"], [aria-current="true"], [aria-pressed="true"]) { background-color: var(--fill-selected); color: var(--ink); }
|
|
44
|
+
.dn-state:is(:disabled, [aria-disabled="true"]) { background-color: transparent; opacity: var(--opacity-disabled); cursor: default; }
|
|
45
|
+
.dn-state-row { display: flex; align-items: center; height: var(--row-height); padding: 0 var(--row-padding-x); border-radius: var(--row-radius); color: var(--ink-secondary); font: var(--weight-regular) var(--type-callout-size)/1 var(--font-sans); }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
同一组状态放在 `canvas` 和卡片的 `surface` 上,半透明的底色在两种纸面上都成立:
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<div class="dn-row is-top">
|
|
52
|
+
<div class="dn-state-list">
|
|
53
|
+
<div class="dn-state dn-state-row">默认</div>
|
|
54
|
+
<div class="dn-state dn-state-row is-hover">悬停 · fill-hover</div>
|
|
55
|
+
<div class="dn-state dn-state-row is-active">按下 · fill-selected</div>
|
|
56
|
+
<div class="dn-state dn-state-row" aria-current="true">选中 · fill-selected</div>
|
|
57
|
+
<div class="dn-state dn-state-row" aria-disabled="true">禁用 · opacity-disabled</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="dn-card dn-state-list">
|
|
60
|
+
<div class="dn-state dn-state-row">默认</div>
|
|
61
|
+
<div class="dn-state dn-state-row is-hover">悬停 · fill-hover</div>
|
|
62
|
+
<div class="dn-state dn-state-row is-active">按下 · fill-selected</div>
|
|
63
|
+
<div class="dn-state dn-state-row" aria-current="true">选中 · fill-selected</div>
|
|
64
|
+
<div class="dn-state dn-state-row" aria-disabled="true">禁用 · opacity-disabled</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## SwiftUI
|
|
70
|
+
|
|
71
|
+
系统控件(`Button`、`List`、`Menu`、工具栏)自带悬停、按下和选中的效果,不要覆盖。自己画的可点区域:
|
|
72
|
+
|
|
73
|
+
```swift
|
|
74
|
+
struct RowButtonStyle: ButtonStyle {
|
|
75
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
76
|
+
configuration.label
|
|
77
|
+
.frame(maxWidth: .infinity, minHeight: Deernova.rowHeight, alignment: .leading)
|
|
78
|
+
.padding(.horizontal, Deernova.rowPaddingX)
|
|
79
|
+
.background(configuration.isPressed ? Color.fillSelected : .clear,
|
|
80
|
+
in: .rect(cornerRadius: Deernova.rowRadius))
|
|
81
|
+
.contentShape(.rect)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Button(action: open) { Label("周会记录", systemImage: "doc") }
|
|
86
|
+
.buttonStyle(RowButtonStyle())
|
|
87
|
+
.hoverEffect(.highlight) // iPadOS 接触控板时和系统一致
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
macOS 上自己画的行需要悬停底色时,用 `.onHover` 记下状态,再铺 `fillHover`;iPhone 没有悬停。
|
|
91
|
+
|
|
92
|
+
## 不要
|
|
93
|
+
|
|
94
|
+
- 用实色灰做悬停底色;悬停和选中用同一个底色,分不出当前在哪。
|
|
95
|
+
- 悬停时加阴影、上浮、放大、加粗,或者改变尺寸让布局跳动。
|
|
96
|
+
- 把 `:hover` 写在 `@media (hover: hover)` 外面,让触屏停在悬停状态。
|
|
97
|
+
- 选中项悬停时再叠一层变得更深;一个元素同时出现两种底色。
|
|
98
|
+
- 用悬停底色代替键盘焦点环;禁用的元素还有悬停效果。
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
- **线条与阴影**:分组靠 1px 线和间距;卡片没有阴影;阴影只出现在菜单、弹窗、浮动面板和 Composer 上;虚线只用在拖放区和占位块上。
|
|
14
14
|
- **字体**:界面文字和 AI 回答是无衬线;衬线只出现在大标题、空状态标题、官网和文章类长文;代码是等宽;字号来自角色变量(`--type-*`、`.deernovaX`);iOS 用文本样式,没有写死字号;对齐的数字用等宽数字。
|
|
15
15
|
- **字重**:粗体只出现在真正的标题上;分组标题、会话名、顶栏标题、说明是常规字重,靠灰度区分。
|
|
16
|
+
- **对齐**:单行控件在固定高度里垂直居中,没有用内边距、`top` 或等高行高去凑;图标和文字按中线,不同字号按基线;三行以上的内容图标对齐第一行;表格数字右对齐;空状态略高于正中;内容栏限宽居中,页面没有横向滚动,每一栏各自滚动。
|
|
16
17
|
- **间距与圆角**:间距在 4pt 网格上;组与组之间至少是组内的 2 倍;圆角来自组件的 `*-radius` token,嵌套圆角同心;阅读栏宽不超过 `content-max`。
|
|
17
|
-
-
|
|
18
|
+
- **交互**:一屏最多一个主按钮;按钮文字以动词开头;有悬停、按下、选中、禁用、加载、空、错误这些需要的状态,悬停底色是 `fill-hover`、选中和按下是 `fill-selected`,选中项悬停不加深,悬停没有阴影和位移,`:hover` 都在 `@media (hover: hover)` 里;禁用和加载时布局不跳动;键盘焦点清楚;菜单和下拉点击才打开,Esc、点外面、选中一项都能关闭,焦点回到触发器,方向键能走完所有项;弹窗打开时背景不能操作也不滚动,关闭后焦点回到原处,危险确认默认聚焦「取消」。
|
|
18
19
|
- **动效**:时长来自 `duration-*`;动效说明因果;没有无目的的循环;减弱动态效果时只剩淡入淡出;切换内容时阅读位置不跳。
|
|
19
|
-
- **无障碍**:两套主题都看过;最大字号或 200%
|
|
20
|
+
- **无障碍**:两套主题都看过;最大字号或 200% 缩放下不截断;只有图标的按钮有标签,指针端有 tooltip(不是 `title` 属性),键盘聚焦时也显示;降低透明度时玻璃变成实色。
|
|
20
21
|
- **文案**:用“你”;没有感叹号和 emoji;完成态用「已保存」;错误说明原因和解决办法;中文与英文、数字之间有半角空格。
|
|
21
22
|
|
|
22
23
|
## 怎么汇报
|
|
@@ -15,7 +15,7 @@ iPad 应用和 iPhone 一样用**触控**密度(`touch`),导航结构用
|
|
|
15
15
|
```swift
|
|
16
16
|
NavigationSplitView {
|
|
17
17
|
SidebarView()
|
|
18
|
-
.navigationSplitViewColumnWidth(min:
|
|
18
|
+
.navigationSplitViewColumnWidth(min: Deernova.sidebarWidthMin, ideal: Deernova.sidebarWidth, max: Deernova.sidebarWidthMax)
|
|
19
19
|
} detail: {
|
|
20
20
|
ScrollView { ThreadView().frame(maxWidth: Deernova.contentMax) }
|
|
21
21
|
.safeAreaInset(edge: .bottom) { Composer().frame(maxWidth: Deernova.contentMax).padding(Deernova.pageMargin) }
|
|
@@ -19,7 +19,7 @@ SwiftUI 的通用写法见 [SwiftUI 共用](swiftui.md)。
|
|
|
19
19
|
```swift
|
|
20
20
|
NavigationSplitView {
|
|
21
21
|
SidebarView()
|
|
22
|
-
.navigationSplitViewColumnWidth(min:
|
|
22
|
+
.navigationSplitViewColumnWidth(min: Deernova.sidebarWidthMin, ideal: Deernova.sidebarWidth, max: Deernova.sidebarWidthMax)
|
|
23
23
|
} detail: {
|
|
24
24
|
ScrollView { ThreadView().frame(maxWidth: Deernova.contentMax) }
|
|
25
25
|
.safeAreaInset(edge: .bottom) { Composer().padding(Deernova.pageMargin) }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- 大标题用 `type-display`(衬线)。首屏标题可以再放大,用 `clamp()` 跟随视口,例如 `font-size: clamp(40px, 6vw, 88px)`,但每页只有一个。
|
|
14
14
|
- 正文 `type-body`,长文 `type-prose`,栏宽不超过 `content-max`。
|
|
15
15
|
- 章节之间 `gap-group`(48),首屏上下留白 `space-16`。
|
|
16
|
-
-
|
|
16
|
+
- 一页只用一套 12 栏网格,栏间距 `grid-gutter`;左右边距 `page-margin`,内容不超过 `wide-max`,两侧留白。见 [布局与对齐](../foundations/layout.md)。
|
|
17
17
|
|
|
18
18
|
## 控件
|
|
19
19
|
|
|
@@ -60,6 +60,7 @@ Web 上按钮“上下特别宽”几乎都是同一个原因:用内边距和
|
|
|
60
60
|
- **底部**:[Composer](../components/composer.md) 悬浮在内容之上,上面可以贴一条提示,下面是模型和权限的信息行;内容区底部留出它的高度。
|
|
61
61
|
- **侧面板**:右边可以并排打开终端、预览等面板,标签头和顶栏同高。
|
|
62
62
|
- 窄于 `breakpoint-md`(960)时侧面板变成抽屉;窄于 `breakpoint-sm`(760)时侧边栏也收起成抽屉,顶栏左侧放菜单按钮。不能出现横向滚动。
|
|
63
|
+
- 页面本身不滚动,侧边栏、内容区、侧面板各自滚动。其他页面类型(列表加详情、设置页、表格)的宽度,以及对齐规则,见 [布局与对齐](../foundations/layout.md)。
|
|
63
64
|
|
|
64
65
|
## 字体
|
|
65
66
|
|
|
@@ -67,13 +68,13 @@ Web 上按钮“上下特别宽”几乎都是同一个原因:用内边距和
|
|
|
67
68
|
|
|
68
69
|
## 控件
|
|
69
70
|
|
|
70
|
-
- 用原生元素:`<button>`、`<a>`、`<input>`、`<select>`、`<dialog>`、`<details>`。只有图标的按钮加 `aria-label
|
|
71
|
+
- 用原生元素:`<button>`、`<a>`、`<input>`、`<select>`、`<dialog>`、`<details>`。只有图标的按钮加 `aria-label`,并用 [tooltip](../components/tooltip.md) 显示名称,不用 `title` 属性。
|
|
71
72
|
- 焦点环:`outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset);`,只在 `:focus-visible` 时显示。
|
|
72
73
|
- 细线:`border: var(--stroke-line) solid var(--line);`。
|
|
73
74
|
- 分段控件、开关这类有状态的控件,用 `aria-pressed`、`role="switch"` 或 `role="tab"` 标出当前状态。
|
|
74
75
|
- 弹窗用 `<dialog>` 的 `showModal()`,自带焦点管理和 Esc 关闭;popover 用 `popover` 属性。打开、关闭和焦点规则见 [弹窗与 popover](../components/sheet.md)。
|
|
75
76
|
- 菜单和下拉:选值优先用原生 `<select>`,命令菜单和链接列表按 [菜单与下拉](../components/menu.md) 选语义、处理键盘。
|
|
76
|
-
- 悬停效果放在 `@media (hover: hover)`
|
|
77
|
+
- 悬停效果放在 `@media (hover: hover)` 里,触屏上不会卡在悬停状态。各元素的悬停、按下、选中见 [交互状态](../foundations/states.md)。
|
|
77
78
|
|
|
78
79
|
## 验证
|
|
79
80
|
|