@deernova/style 0.3.1 → 0.3.3
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 +5 -4
- package/skill/assets/DeernovaTokens.swift +56 -2
- package/skill/assets/tokens.css +94 -4
- package/skill/assets/tokens.json +256 -5
- package/skill/manifest.json +2 -2
- package/skill/references/components/button.md +3 -2
- package/skill/references/components/empty-state.md +1 -1
- package/skill/references/components/menu.md +223 -0
- package/skill/references/components/message.md +1 -0
- package/skill/references/components/segmented.md +1 -1
- package/skill/references/components/sheet.md +193 -31
- package/skill/references/components/text-field.md +1 -1
- package/skill/references/components/tooltip.md +145 -0
- package/skill/references/foundations/accessibility.md +1 -1
- package/skill/references/foundations/elevation.md +1 -0
- package/skill/references/foundations/layout.md +186 -0
- package/skill/references/foundations/spacing.md +3 -8
- 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 +4 -2
- package/skill/references/tokens.md +55 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# 菜单与下拉
|
|
2
|
+
|
|
3
|
+
从一个按钮或下拉框弹出的一列选项。它有三种用途:对当前对象执行的命令(⋯ 按钮、右键),从一组值里选一个(下拉选择),一组跳转链接(产品切换、账户)。
|
|
4
|
+
|
|
5
|
+
**需要提供**:触发它的控件(按钮、图标按钮或下拉框)、每一项的文字,以及可选的图标、快捷键和当前值;危险操作要单独标出。
|
|
6
|
+
|
|
7
|
+
## 先选对控件
|
|
8
|
+
|
|
9
|
+
| 需要 | 用 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 2–4 个互斥选项,需要一直看得见 | [分段控件](segmented.md) |
|
|
12
|
+
| 表单里从一组值中选一个 | 下拉选择:Web 用原生 `select`,SwiftUI 用 `Picker` |
|
|
13
|
+
| 对当前对象执行的一组命令 | 菜单:⋯ 图标按钮、右键、长按 |
|
|
14
|
+
| 一组跳转链接(产品切换、账户) | 展开式链接列表,外观和菜单相同 |
|
|
15
|
+
| 选项多到需要搜索,或者要放表单、说明文字 | 可搜索的下拉框(combobox)、[popover 或弹窗](sheet.md) |
|
|
16
|
+
|
|
17
|
+
## 尺寸
|
|
18
|
+
|
|
19
|
+
<!-- tokens:menu -->
|
|
20
|
+
| Token | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 | 用途 |
|
|
21
|
+
| --- | --- | --- | --- | --- |
|
|
22
|
+
| `menu-item-height` | 28 | 44 | 36 | 菜单项和下拉选项的高度。 |
|
|
23
|
+
| `menu-item-padding-x` | 8 | 10 | 10 | 菜单项左右内边距。 |
|
|
24
|
+
| `menu-font-size` | 14 | 17 | 15 | 菜单项字号。 |
|
|
25
|
+
| `menu-padding` | 4 | 6 | 6 | 菜单面板的内边距,也是分隔线上下的间距。 |
|
|
26
|
+
| `menu-radius` | 8 | 12 | 12 | 菜单和 popover 的圆角;菜单项的圆角 = 它减去 menu-padding。 |
|
|
27
|
+
| `menu-offset` | 4 | 8 | 8 | 菜单和触发它的控件之间的距离。 |
|
|
28
|
+
| `menu-min-width` | 160 | 220 | 200 | 菜单最小宽度;同时不窄于触发器。 |
|
|
29
|
+
|
|
30
|
+
| Token | 值 | 用途 |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `menu-max-width` | `320px` | 菜单最大宽度;放不下的文字用省略号。 |
|
|
33
|
+
<!-- /tokens -->
|
|
34
|
+
|
|
35
|
+
- 面板:`surface-raised` 底,`stroke-line` 宽的 `line` 色描边,圆角 `menu-radius`,阴影 `shadow-float`,叠放层级 `z-dropdown`,内边距 `menu-padding`。
|
|
36
|
+
- 菜单项:高度 `menu-item-height`,上下内边距为 0,文字行高 1,常规字重,`ink` 色。圆角等于 `menu-radius` 减 `menu-padding`,和面板同心。
|
|
37
|
+
- 宽度:跟随内容,不窄于 `menu-min-width`,不宽于 `menu-max-width`;从按钮或下拉框展开时,也不窄于它。文字只占一行,放不下时用省略号,不换行。
|
|
38
|
+
- 图标用 `icon-size-md`,和文字间距 `space-2`。产品图标、头像这类图像可以用 `icon-size-lg`。同一个菜单里,要么每项都有图标,要么都没有。
|
|
39
|
+
- 快捷键靠右放,`caption` 字号、`ink-tertiary` 色,和文字之间至少 `space-6`。
|
|
40
|
+
- 分组之间放一条 1px 的 `line` 色分隔线,左右缩进 `menu-item-padding-x`,上下各留 `menu-padding`。分组标题用 `caption` 字号、常规字重、`ink-tertiary` 色。
|
|
41
|
+
- 有子菜单的项,在右端放一个 `icon-size-sm` 的右箭头,用 `ink-tertiary` 色。子菜单最多一层,从父项右侧展开,顶部和父项对齐。
|
|
42
|
+
- 文案:动词开头,不加句号。点了之后还要再输入或确认的命令,在文字后加「…」,例如「导出…」。
|
|
43
|
+
- 官网吸顶的玻璃页眉展开的菜单,可以沿用页眉的玻璃(`.dn-menu.is-glass`),用户开启「降低透明度」后换回 `surface-raised`;整条页眉都是触发器时,菜单从页眉左边缘展开,宽度取页眉的一半,不受 `menu-max-width` 限制。Web 应用里的菜单不用玻璃。
|
|
44
|
+
|
|
45
|
+
## 状态
|
|
46
|
+
|
|
47
|
+
- **高亮**:指针悬停或键盘移到的那一项铺 `fill-hover`。同一时间只高亮一项:指针移到哪一项,焦点就跟到哪一项。键盘操作时再加一圈内描边焦点环(`focus-ring`),只在 `:focus-visible` 时出现。
|
|
48
|
+
- **当前值**:下拉选项和可勾选的命令,在最左侧留一列 `icon-size-sm` 宽的对勾位,当前选中的项显示对勾,其余项留空,保证文字对齐。不要用 `fill-selected` 底色表示当前值。
|
|
49
|
+
- **禁用**:整项降到 `opacity-disabled`,指针点不了,方向键会跳过它。暂时不能用的命令留在原处并禁用,不要隐藏,否则菜单的布局每次都会变。
|
|
50
|
+
- **危险操作**:文字用 `danger` 色,放在最后,前面加一条分隔线;高亮时的底色不变红。执行前用 [弹窗](sheet.md) 确认,能撤销的操作除外。
|
|
51
|
+
|
|
52
|
+
## 位置与开合
|
|
53
|
+
|
|
54
|
+
- 默认在触发器下方 `menu-offset` 处展开,左边对齐;从靠右的按钮弹出时,改成右边对齐。下方放不下就翻到上方;上下都放不下时限制最大高度,在面板内部滚动,并用 `overscroll-behavior: contain` 防止带动页面滚动。面板离窗口边缘至少留 `space-2`。
|
|
55
|
+
- 点击触发器打开,再点一次关闭;不要在悬停时打开。选中一项、按 Esc、点击菜单外面、焦点移出菜单,都会关闭菜单。按 Esc 关闭或选中一项后,焦点回到触发器。
|
|
56
|
+
- 右键菜单在指针位置展开,其他规则相同。
|
|
57
|
+
- 打开时淡入,同时从 0.98 放大到 1,用时 `duration-instant`、缓动 `ease-standard`,从触发器那一侧展开;关闭时直接消失。开启「减弱动态效果」后只保留淡入。
|
|
58
|
+
|
|
59
|
+
## 键盘
|
|
60
|
+
|
|
61
|
+
- **在触发器上**:Enter 或 Space 打开菜单;↓ 打开并移到第一项,↑ 打开并移到最后一项。
|
|
62
|
+
- **在菜单里**:↑↓ 逐项移动,到头后从另一端继续;Home、End 跳到第一项、最后一项;输入字母跳到以它开头的项;Enter 执行(命令菜单里 Space 也可以);→ 打开子菜单,← 关闭子菜单并回到父项;Esc 关闭菜单。
|
|
63
|
+
- Tab 关闭菜单,焦点按正常顺序移到下一个控件,不要把焦点困在菜单里。
|
|
64
|
+
|
|
65
|
+
## Web
|
|
66
|
+
|
|
67
|
+
按用途选择语义:
|
|
68
|
+
|
|
69
|
+
- **跳转链接**(产品切换、文档目录):用展开式(disclosure)。触发器是带 `aria-expanded`、`aria-controls` 的 `<button>`,面板是 `<nav>` 里的链接列表,不加 `role="menu"`。方向键只是额外的便利。
|
|
70
|
+
- **命令**:触发器加 `aria-haspopup="menu"` 和 `aria-expanded`;面板用 `role="menu"`,项用 `role="menuitem"`,可勾选的项用 `menuitemcheckbox` 或 `menuitemradio` 加 `aria-checked`。打开后焦点移进菜单,菜单里只有当前项能被 Tab 到(roving tabindex)。快捷键用 `aria-keyshortcuts` 标出。
|
|
71
|
+
- **选一个值**:先用原生 `<select>`,外面包一层 `.dn-select`,就能得到和输入框一样的外观;它弹出的列表由系统绘制,不要改。只有选项需要图标或两行文字时才自己画:触发器用 `<button aria-haspopup="listbox">`,面板用 `role="listbox"`,项用 `role="option"` 加 `aria-selected`。需要搜索时用 `role="combobox"`。
|
|
72
|
+
- 祖先元素有 `overflow: hidden`、会把菜单裁掉时,给面板加 `popover` 属性放到顶层,或者把面板挂到 `body` 下面再定位。
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
.dn-menu-anchor { position: relative; display: inline-block; }
|
|
76
|
+
.dn-menu-anchor > .dn-menu { position: absolute; top: calc(100% + var(--menu-offset)); left: 0; z-index: var(--z-dropdown); min-width: max(var(--menu-min-width), 100%); }
|
|
77
|
+
.dn-menu-anchor > .dn-menu.is-end { left: auto; right: 0; transform-origin: top right; }
|
|
78
|
+
.dn-menu-anchor > .dn-menu.is-above { top: auto; bottom: calc(100% + var(--menu-offset)); transform-origin: bottom left; }
|
|
79
|
+
.dn-menu-anchor > .dn-menu.is-above.is-end { transform-origin: bottom right; }
|
|
80
|
+
.dn-menu {
|
|
81
|
+
box-sizing: border-box;
|
|
82
|
+
min-width: var(--menu-min-width);
|
|
83
|
+
max-width: min(var(--menu-max-width), 100vw - 2 * var(--space-2));
|
|
84
|
+
max-height: min(24rem, 100dvh - 2 * var(--space-2));
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
overscroll-behavior: contain;
|
|
87
|
+
padding: var(--menu-padding);
|
|
88
|
+
border: var(--stroke-line) solid var(--line);
|
|
89
|
+
border-radius: var(--menu-radius);
|
|
90
|
+
background: var(--surface-raised);
|
|
91
|
+
color: var(--ink);
|
|
92
|
+
box-shadow: var(--shadow-float);
|
|
93
|
+
transform-origin: top left;
|
|
94
|
+
transition: opacity var(--duration-instant) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
95
|
+
}
|
|
96
|
+
.dn-menu[hidden] { display: none; }
|
|
97
|
+
@starting-style { .dn-menu { opacity: 0; transform: scale(0.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
|
+
.dn-menu-item {
|
|
100
|
+
box-sizing: border-box;
|
|
101
|
+
display: flex;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: var(--space-2);
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: var(--menu-item-height);
|
|
106
|
+
padding: 0 var(--menu-item-padding-x);
|
|
107
|
+
border: 0;
|
|
108
|
+
border-radius: calc(var(--menu-radius) - var(--menu-padding));
|
|
109
|
+
background: none;
|
|
110
|
+
color: var(--ink);
|
|
111
|
+
font: var(--weight-regular) var(--menu-font-size)/1 var(--font-sans);
|
|
112
|
+
text-align: start;
|
|
113
|
+
text-decoration: none;
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
}
|
|
116
|
+
.dn-menu-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
117
|
+
.dn-menu-shortcut { margin-inline-start: var(--space-6); color: var(--ink-tertiary); font: var(--weight-regular) var(--type-caption-size)/1 var(--font-sans); font-variant-numeric: tabular-nums; }
|
|
118
|
+
.dn-menu-item > .dn-menu-check { width: var(--icon-size-sm); height: var(--icon-size-sm); visibility: hidden; }
|
|
119
|
+
.dn-menu-item:is([aria-checked="true"], [aria-selected="true"]) > .dn-menu-check { visibility: visible; }
|
|
120
|
+
.dn-menu-item > .dn-menu-more { width: var(--icon-size-sm); height: var(--icon-size-sm); color: var(--ink-tertiary); }
|
|
121
|
+
@media (hover: hover) { .dn-menu-item:hover { background: var(--fill-hover); } }
|
|
122
|
+
.dn-menu-item:focus { outline: none; }
|
|
123
|
+
.dn-menu-item:focus-visible, .dn-menu-item[aria-expanded="true"] { background: var(--fill-hover); }
|
|
124
|
+
.dn-menu-item:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: calc(var(--stroke-focus) * -1); }
|
|
125
|
+
.dn-menu-item.is-danger { color: var(--danger); }
|
|
126
|
+
.dn-menu-item[aria-disabled="true"] { opacity: var(--opacity-disabled); pointer-events: none; }
|
|
127
|
+
.dn-menu-separator { height: var(--stroke-line); margin: var(--menu-padding) var(--menu-item-padding-x); border: 0; background: var(--line); }
|
|
128
|
+
.dn-menu-heading { padding: var(--space-2) var(--menu-item-padding-x) var(--space-1); color: var(--ink-tertiary); font: var(--weight-regular) var(--type-caption-size)/var(--type-caption-line) var(--font-sans); }
|
|
129
|
+
.dn-select { position: relative; display: grid; min-width: 0; }
|
|
130
|
+
.dn-select > :is(select, button) {
|
|
131
|
+
box-sizing: border-box;
|
|
132
|
+
width: 100%;
|
|
133
|
+
height: var(--field-height);
|
|
134
|
+
padding: 0 calc(var(--field-padding-x) + var(--icon-size-sm) + var(--space-1)) 0 var(--field-padding-x);
|
|
135
|
+
border: var(--stroke-control) var(--line-style-divider) var(--line-control);
|
|
136
|
+
border-radius: var(--field-radius);
|
|
137
|
+
background: var(--surface-raised);
|
|
138
|
+
color: var(--ink);
|
|
139
|
+
font: var(--weight-regular) var(--field-font-size)/1.2 var(--font-sans);
|
|
140
|
+
text-align: start;
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
text-overflow: ellipsis;
|
|
144
|
+
appearance: none;
|
|
145
|
+
}
|
|
146
|
+
.dn-select > :is(select, button):focus-visible { outline: none; border-color: var(--focus-ring); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
147
|
+
.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; }
|
|
148
|
+
@media (prefers-reduced-motion: reduce) { .dn-menu { transition-property: opacity; } }
|
|
149
|
+
@media (prefers-reduced-transparency: reduce) { .dn-menu.is-glass { background: var(--surface-raised); -webkit-backdrop-filter: none; backdrop-filter: none; } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<div class="dn-row is-top">
|
|
154
|
+
<div class="dn-field">
|
|
155
|
+
<span class="dn-field-label" id="sort-label">排序</span>
|
|
156
|
+
<div class="dn-select">
|
|
157
|
+
<button type="button" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="sort-label sort-value"><span id="sort-value">最近编辑</span></button>
|
|
158
|
+
<svg class="dn-icon dn-select-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="dn-menu" role="listbox" aria-labelledby="sort-label">
|
|
161
|
+
<div class="dn-menu-item" role="option" aria-selected="true"><svg class="dn-icon dn-menu-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg><span class="dn-menu-label">最近编辑</span></div>
|
|
162
|
+
<div class="dn-menu-item" role="option" aria-selected="false"><svg class="dn-icon dn-menu-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg><span class="dn-menu-label">创建时间</span></div>
|
|
163
|
+
<div class="dn-menu-item" role="option" aria-selected="false"><svg class="dn-icon dn-menu-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12l5 5 9-10"/></svg><span class="dn-menu-label">标题</span></div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="dn-menu" role="menu" aria-label="笔记">
|
|
167
|
+
<button class="dn-menu-item" type="button" role="menuitem" aria-keyshortcuts="Meta+R"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16z"/></svg><span class="dn-menu-label">重命名</span><span class="dn-menu-shortcut">⌘R</span></button>
|
|
168
|
+
<button class="dn-menu-item" type="button" role="menuitem" aria-keyshortcuts="Shift+Meta+C"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg><span class="dn-menu-label">复制链接</span><span class="dn-menu-shortcut">⇧⌘C</span></button>
|
|
169
|
+
<button class="dn-menu-item" type="button" role="menuitem" aria-haspopup="menu" aria-expanded="false"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="dn-menu-label">移到</span><svg class="dn-icon dn-menu-more" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
|
|
170
|
+
<button class="dn-menu-item" type="button" role="menuitem" aria-disabled="true"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 9l5-5 5 5M5 20h14"/></svg><span class="dn-menu-label">导出…</span></button>
|
|
171
|
+
<div class="dn-menu-separator" role="separator"></div>
|
|
172
|
+
<button class="dn-menu-item is-danger" type="button" role="menuitem"><svg class="dn-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg><span class="dn-menu-label">删除</span></button>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
示例里的两个面板都画成展开状态。真实页面里,把触发器和面板一起放进 `.dn-menu-anchor`,用 `hidden` 控制开合;空间不够时,由脚本给面板加 `is-above` 或 `is-end`。
|
|
178
|
+
|
|
179
|
+
## SwiftUI
|
|
180
|
+
|
|
181
|
+
在 iOS 26 和 macOS 26 上,系统菜单自动渲染成 Liquid Glass,尺寸、键盘操作和无障碍也都由系统处理。直接用 `Menu`、`Picker`、`.contextMenu`,不要自己画菜单。
|
|
182
|
+
|
|
183
|
+
```swift
|
|
184
|
+
// 命令:⋯ 按钮
|
|
185
|
+
Menu {
|
|
186
|
+
Button("重命名", systemImage: "pencil") { rename() }
|
|
187
|
+
.keyboardShortcut("r")
|
|
188
|
+
Button("复制链接", systemImage: "link") { copyLink() }
|
|
189
|
+
Menu("移到", systemImage: "folder") {
|
|
190
|
+
ForEach(folders) { folder in
|
|
191
|
+
Button(folder.name) { move(to: folder) }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
Divider()
|
|
195
|
+
Button("删除", systemImage: "trash", role: .destructive) { confirmDelete = true }
|
|
196
|
+
} label: {
|
|
197
|
+
Label("更多", systemImage: "ellipsis")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 选一个值:系统会给当前值打上对勾
|
|
201
|
+
Picker("排序", selection: $sort) {
|
|
202
|
+
ForEach(SortOrder.allCases) { Text($0.title).tag($0) }
|
|
203
|
+
}
|
|
204
|
+
.pickerStyle(.menu)
|
|
205
|
+
|
|
206
|
+
// 右键(Mac)和长按(iOS)
|
|
207
|
+
.contextMenu {
|
|
208
|
+
Button("复制链接", systemImage: "link") { copyLink() }
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
iOS 上选项很多(例如选择国家)或者需要搜索时,推入一个新页面(`Picker` 加 `.pickerStyle(.navigationLink)`),或者用 [底部弹窗](sheet.md)。
|
|
213
|
+
|
|
214
|
+
## 不要
|
|
215
|
+
|
|
216
|
+
- 把一屏的主要操作藏进菜单;主要操作放在按钮上。
|
|
217
|
+
- 悬停时就打开菜单;子菜单超过一层。
|
|
218
|
+
- 只有 2–4 个选项还用下拉(改用分段控件);十几个以上的选项不提供搜索。
|
|
219
|
+
- 给导航链接加 `role="menu"`;原生 `select` 能完成的选择,也自己画。
|
|
220
|
+
- 在菜单里放段落、输入框或开关;需要这些时改用 [popover 或弹窗](sheet.md)。
|
|
221
|
+
- 用彩色图标或彩色底色区分选项;颜色只留给危险操作的文字。
|
|
222
|
+
- 在 macOS、iOS 上自己画菜单,或者给系统菜单加实色背景。
|
|
223
|
+
- 在 Web 应用的菜单上用玻璃或模糊效果(官网页眉展开的菜单除外)。
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- 消息、工具调用行、卡片之间统一 `space-4`。
|
|
19
19
|
- 操作按钮放在 AI 消息下方,`message-action-size` 的图标按钮,`ink-tertiary` 色;指针端只在悬停时显示,触控端一直显示。
|
|
20
20
|
- 链接用 `ink` 色,加 1px 的 `line-control` 色下划线,下偏移 3px。
|
|
21
|
+
- 操作按钮悬停才出现时,键盘聚焦和触屏上仍然可见,规则见 [悬停才出现的操作](tooltip.md#悬停才出现的操作)。
|
|
21
22
|
- 生成中:回答末尾显示「正在运行」状态(见 [工具调用与执行步骤](step-row.md)),Composer 的发送按钮变成「停止」。
|
|
22
23
|
|
|
23
24
|
## Web
|
|
@@ -1,84 +1,246 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 弹窗、底部弹窗与 popover
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
弹窗用于一个短小、独立的任务:确认删除、编辑一个字段、选择分享方式。它会打断当前页面,背景压暗,完成或取消之后回到原处。指针端在窗口中央弹出,触控端从底部弹出。popover 附着在一个控件上,放少量说明或短表单,不打断页面。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**需要提供**:标题、一两句说明、内容(短表单或选项)、主操作和取消操作;底部弹窗还要提供支持的高度档位。危险操作要写清后果。
|
|
6
|
+
|
|
7
|
+
## 先选对控件
|
|
8
|
+
|
|
9
|
+
| 情况 | 用 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 告诉用户一件事,不需要回应 | [轻提示](banner.md) |
|
|
12
|
+
| 需要处理,但不必马上打断 | [横幅](banner.md) |
|
|
13
|
+
| 执行不可撤销的操作前确认 | 确认弹窗(`is-sm`);SwiftUI 用 `.confirmationDialog` |
|
|
14
|
+
| 必须马上知道的错误 | 确认弹窗;SwiftUI 用 `.alert` |
|
|
15
|
+
| 一个短小、独立的任务 | 弹窗;触控端是底部弹窗 |
|
|
16
|
+
| 附着在某个控件上的少量说明、设置或短表单 | popover;手机上改成底部弹窗 |
|
|
17
|
+
| 一列命令或选项 | [菜单](menu.md) |
|
|
18
|
+
| 长内容、多步流程、需要长时间阅读 | 推入新页面,或者全高弹窗 |
|
|
6
19
|
|
|
7
20
|
## 尺寸
|
|
8
21
|
|
|
9
22
|
<!-- tokens:sheet -->
|
|
10
23
|
| Token | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 | 用途 |
|
|
11
24
|
| --- | --- | --- | --- | --- |
|
|
25
|
+
| `sheet-width-sm` | 360 | 360 | 400 | 确认弹窗、只有一两句话的提示。 |
|
|
26
|
+
| `sheet-width-md` | 480 | 540 | 560 | 默认宽度:短表单、选择分享方式这类短任务。 |
|
|
27
|
+
| `sheet-width-lg` | 720 | 720 | 800 | 预览、较长的设置,或分两栏的内容。 |
|
|
28
|
+
| `sheet-padding` | 20 | 20 | 24 | 弹窗内边距;标题、内容和按钮区都和它对齐。 |
|
|
29
|
+
| `sheet-margin` | 32 | 8 | 32 | 弹窗离窗口边缘的最小距离;触控端底部弹窗悬浮时离屏幕边缘的距离。 |
|
|
12
30
|
| `sheet-radius` | 12 | 24 | 16 | 弹窗和底部弹窗的圆角。 |
|
|
13
|
-
| `
|
|
31
|
+
| `popover-padding` | 12 | 16 | 16 | popover 的内边距;圆角用 menu-radius,宽度不超过 sheet-width-sm。 |
|
|
32
|
+
|
|
33
|
+
| Token | 值 | 用途 |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `sheet-grabber-width` | `36px` | 底部弹窗顶部拖拽条的宽度。 |
|
|
36
|
+
| `sheet-grabber-height` | `5px` | 拖拽条的高度。 |
|
|
14
37
|
<!-- /tokens -->
|
|
15
38
|
|
|
16
|
-
- 弹窗:`surface-raised`
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
39
|
+
- 弹窗:`surface-raised` 底,`stroke-line` 宽的 `line` 色描边,圆角 `sheet-radius`,阴影 `shadow-float`,背后用 `scrim` 压暗,叠放层级 `z-overlay`(Web 的 `<dialog>` 自动放在顶层)。
|
|
40
|
+
- 宽度三档:确认用 `sheet-width-sm`,默认 `sheet-width-md`,预览和较长的设置用 `sheet-width-lg`;窗口太窄时,离两边各留 `sheet-margin`。最大高度是窗口高度减去上下两个 `sheet-margin`。
|
|
41
|
+
- 结构从上到下是标题区、内容区、按钮区,左右都是 `sheet-padding`;标题区和按钮区不随内容滚动。标题和内容之间 `space-4`,内容和按钮之间 `space-4`。
|
|
42
|
+
- 内容超过最大高度时,只有内容区滚动,同时在标题区下方和按钮区上方各加一条 1px 的 `line` 线,提示还有内容。内容长到要滚好几屏,就改成推入新页面。
|
|
43
|
+
- popover:外观和[菜单](menu.md)的面板相同(`menu-radius`、`shadow-float`、`z-dropdown`),内边距 `popover-padding`,宽度跟随内容,不超过 `sheet-width-sm`;不压暗背景。
|
|
44
|
+
|
|
45
|
+
## 内容与按钮
|
|
46
|
+
|
|
47
|
+
- 标题用 `type-title`(衬线),一句话说清这个弹窗要做什么;说明用 `callout`、`ink-secondary`。popover 的标题用 `headline`。
|
|
48
|
+
- 按钮文字以动词开头,说清点了会发生什么:「保存」「删除」「开启」,不用「确定」。
|
|
49
|
+
- 指针端:按钮靠右排成一行,主按钮在最右边,取消在它左边。
|
|
50
|
+
- 触控端:主操作是大号通栏按钮,下面是同样大小的文字按钮「取消」或「暂不需要」,两者间距 `space-1`。
|
|
51
|
+
- 有「取消」按钮时,不在右上角放关闭按钮。只读、没有按钮的弹窗(预览、说明)在标题区右端放一个关闭图标按钮,`aria-label="关闭"`。
|
|
52
|
+
- **危险确认**:标题写成问句并说出对象,例如「删除“周会记录”?」;说明写后果,例如「删除后无法恢复」。主按钮用实色的危险样式(`is-primary is-danger`),文字重复动词。默认焦点放在「取消」上,回车不会直接删除。能撤销的操作不要确认,执行后给一个带「撤销」的轻提示。
|
|
53
|
+
|
|
54
|
+
## 打开与关闭
|
|
55
|
+
|
|
56
|
+
- 打开后,焦点移到第一个输入框;没有输入框的,移到主按钮;危险确认移到「取消」。弹窗以外的内容不能操作,页面也不能滚动。
|
|
57
|
+
- 关闭方式:取消按钮、Esc、完成主操作。只有选项类、没有输入的弹窗,点遮罩才关闭;有输入的弹窗,点遮罩不关闭,避免误点丢掉内容。
|
|
58
|
+
- 有未保存的输入时,Esc、取消、下拉关闭都先问「放弃修改?」,按钮是「继续编辑」和「放弃」。这是唯一可以叠在弹窗上的确认。
|
|
59
|
+
- 关闭后焦点回到打开它的控件。
|
|
60
|
+
- 表单弹窗里,回车执行主操作;危险确认除外。
|
|
61
|
+
- 动效:居中的弹窗淡入,同时从 0.96 放大到 1,用时 `duration-base`;底部弹窗从下方滑入,用时 `duration-slow`;遮罩同时淡入,缓动都是 `ease-standard`。关闭时直接消失。开启「减弱动态效果」后只保留淡入。popover 的开合和菜单相同。
|
|
62
|
+
|
|
63
|
+
## 触控端
|
|
64
|
+
|
|
65
|
+
- 底部弹窗悬浮在屏幕底部,左、右、下各留 `sheet-margin`,下方再加上安全区(`env(safe-area-inset-bottom)`)。顶部有拖拽条,`sheet-grabber-width` × `sheet-grabber-height`,`line-control` 色。
|
|
66
|
+
- 高度档位:中等(跟随内容,最多半屏左右)和全高。可以支持向下拖动关闭,但仍要保留取消按钮。
|
|
67
|
+
- 输入框聚焦、键盘弹起时,输入框要留在可见区域,不能被键盘挡住。
|
|
68
|
+
- 手机上不用 popover,改成底部弹窗;iPad 和平板的大屏上不要弹出撑满全屏的底部弹窗。
|
|
22
69
|
|
|
23
70
|
## Web
|
|
24
71
|
|
|
25
|
-
|
|
72
|
+
- 弹窗用 `<dialog>` 和 `showModal()`:自带顶层叠放、焦点限制在弹窗内、Esc 关闭和背景遮罩,关闭后焦点回到原来的控件。用 `aria-labelledby` 指向标题,`aria-describedby` 指向说明;危险确认用 `role="alertdialog"`。初始焦点用 `autofocus` 标出。
|
|
73
|
+
- 表单放在 `<form>` 里,主按钮是 `type="submit"`,回车就能提交。
|
|
74
|
+
- 点遮罩关闭:在 `<dialog>` 的 `click` 事件里判断点击位置在面板外,再调用 `close()`。
|
|
75
|
+
- popover 用 `popover` 属性和 `popovertarget`:自带顶层叠放、点外面关闭和 Esc 关闭。位置规则同菜单,支持 CSS anchor positioning 的浏览器可以直接用它,否则由脚本计算。
|
|
26
76
|
|
|
27
77
|
```css
|
|
28
78
|
.dn-sheet {
|
|
29
79
|
box-sizing: border-box;
|
|
30
|
-
|
|
31
|
-
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
width: min(100% - 2 * var(--sheet-margin), var(--sheet-width-md));
|
|
83
|
+
max-width: none;
|
|
84
|
+
max-height: calc(100dvh - 2 * var(--sheet-margin));
|
|
85
|
+
padding: 0;
|
|
86
|
+
overflow: hidden;
|
|
32
87
|
border: var(--stroke-line) solid var(--line);
|
|
33
88
|
border-radius: var(--sheet-radius);
|
|
34
89
|
background: var(--surface-raised);
|
|
35
90
|
color: var(--ink);
|
|
36
91
|
box-shadow: var(--shadow-float);
|
|
92
|
+
transition: opacity var(--duration-base) var(--ease-standard), transform var(--duration-base) var(--ease-standard);
|
|
93
|
+
}
|
|
94
|
+
dialog.dn-sheet:not([open]) { display: none; }
|
|
95
|
+
.dn-sheet.is-sm { width: min(100% - 2 * var(--sheet-margin), var(--sheet-width-sm)); }
|
|
96
|
+
.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-base) var(--ease-standard); }
|
|
98
|
+
@starting-style {
|
|
99
|
+
.dn-sheet[open] { opacity: 0; transform: scale(0.96); }
|
|
100
|
+
.dn-sheet[open]::backdrop { opacity: 0; }
|
|
101
|
+
}
|
|
102
|
+
:root:has(.dn-sheet:modal) { overflow: hidden; }
|
|
103
|
+
.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
|
+
.dn-sheet-head { flex: none; display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--sheet-padding) var(--sheet-padding) 0; }
|
|
105
|
+
.dn-sheet-heading { flex: 1; min-width: 0; }
|
|
106
|
+
.dn-sheet-title { margin: 0; font: var(--type-title); }
|
|
107
|
+
.dn-sheet-description { margin: var(--space-1) 0 0; color: var(--ink-secondary); font: var(--type-callout); }
|
|
108
|
+
.dn-sheet-close { flex: none; margin: calc(var(--space-1) * -1) calc(var(--space-2) * -1) 0 0; }
|
|
109
|
+
.dn-sheet-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-4) var(--sheet-padding) 0; }
|
|
110
|
+
.dn-sheet-actions { flex: none; display: flex; justify-content: flex-end; gap: var(--gap-stack); padding: var(--space-4) var(--sheet-padding) var(--sheet-padding); }
|
|
111
|
+
.dn-sheet.is-scrollable .dn-sheet-head { padding-bottom: var(--space-4); border-bottom: var(--stroke-line) solid var(--line); }
|
|
112
|
+
.dn-sheet.is-scrollable .dn-sheet-content { padding-bottom: var(--space-4); }
|
|
113
|
+
.dn-sheet.is-scrollable .dn-sheet-actions { border-top: var(--stroke-line) solid var(--line); }
|
|
114
|
+
.dn-popover {
|
|
115
|
+
box-sizing: border-box;
|
|
116
|
+
width: max-content;
|
|
117
|
+
max-width: min(var(--sheet-width-sm), 100vw - 2 * var(--space-2));
|
|
118
|
+
padding: var(--popover-padding);
|
|
119
|
+
border: var(--stroke-line) solid var(--line);
|
|
120
|
+
border-radius: var(--menu-radius);
|
|
121
|
+
background: var(--surface-raised);
|
|
122
|
+
color: var(--ink);
|
|
123
|
+
box-shadow: var(--shadow-float);
|
|
124
|
+
transition: opacity var(--duration-instant) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
37
125
|
}
|
|
38
|
-
.dn-
|
|
39
|
-
|
|
40
|
-
.dn-
|
|
41
|
-
.dn-
|
|
42
|
-
.dn-
|
|
126
|
+
.dn-popover[popover] { inset: auto; margin: 0; }
|
|
127
|
+
@starting-style { .dn-popover:popover-open { opacity: 0; transform: scale(0.98); } }
|
|
128
|
+
.dn-popover-title { margin: 0 0 var(--space-1); font: var(--type-headline); }
|
|
129
|
+
.dn-popover-text { margin: 0 0 var(--space-3); color: var(--ink-secondary); font: var(--type-callout); }
|
|
130
|
+
.dn-popover-row { display: flex; gap: var(--gap-inline); }
|
|
131
|
+
.dn-popover-row > .dn-button { flex: none; }
|
|
43
132
|
@media (pointer: coarse) {
|
|
44
|
-
.dn-sheet
|
|
45
|
-
.dn-sheet
|
|
133
|
+
.dn-sheet { transition-duration: var(--duration-slow); }
|
|
134
|
+
.dn-sheet[open] { margin: auto auto calc(var(--sheet-margin) + env(safe-area-inset-bottom)); }
|
|
135
|
+
@starting-style { .dn-sheet[open] { opacity: 0; transform: translateY(100%); } }
|
|
136
|
+
.dn-sheet-actions { flex-direction: column-reverse; gap: var(--space-1); }
|
|
46
137
|
.dn-sheet-actions .dn-button { width: 100%; height: var(--button-height-lg); }
|
|
47
138
|
}
|
|
48
139
|
@media (pointer: fine) { .dn-sheet-grabber { display: none; } }
|
|
140
|
+
@media (prefers-reduced-motion: reduce) { .dn-sheet, .dn-popover { transition-property: opacity; } }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<div class="dn-sheet" role="dialog" aria-modal="true" aria-labelledby="rename-title" aria-describedby="rename-desc">
|
|
145
|
+
<div class="dn-sheet-grabber" aria-hidden="true"></div>
|
|
146
|
+
<div class="dn-sheet-head">
|
|
147
|
+
<div class="dn-sheet-heading">
|
|
148
|
+
<h3 class="dn-sheet-title" id="rename-title">重命名笔记</h3>
|
|
149
|
+
<p class="dn-sheet-description" id="rename-desc">新名称会同步到你的所有设备。</p>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="dn-sheet-content">
|
|
153
|
+
<div class="dn-field">
|
|
154
|
+
<label class="dn-field-label" for="note-name">名称</label>
|
|
155
|
+
<input class="dn-input" id="note-name" value="周会记录">
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="dn-sheet-actions">
|
|
159
|
+
<button class="dn-button is-plain" type="button">取消</button>
|
|
160
|
+
<button class="dn-button is-primary" type="submit">保存</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
49
163
|
```
|
|
50
164
|
|
|
165
|
+
真实页面里外层用 `<dialog class="dn-sheet">`,内容和按钮放在 `<form>` 里,输入框加 `autofocus`,打开时调用 `dialog.showModal()`。内容能滚动时,由脚本给弹窗加上 `is-scrollable`。
|
|
166
|
+
|
|
167
|
+
危险确认用窄的一档,没有内容区:
|
|
168
|
+
|
|
51
169
|
```html
|
|
52
|
-
<div class="dn-sheet" role="
|
|
170
|
+
<div class="dn-sheet is-sm" role="alertdialog" aria-modal="true" aria-labelledby="delete-title" aria-describedby="delete-desc">
|
|
53
171
|
<div class="dn-sheet-grabber" aria-hidden="true"></div>
|
|
54
|
-
<
|
|
55
|
-
|
|
172
|
+
<div class="dn-sheet-head">
|
|
173
|
+
<div class="dn-sheet-heading">
|
|
174
|
+
<h3 class="dn-sheet-title" id="delete-title">删除“周会记录”?</h3>
|
|
175
|
+
<p class="dn-sheet-description" id="delete-desc">删除后无法恢复,共享链接也会失效。</p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
56
178
|
<div class="dn-sheet-actions">
|
|
57
|
-
<button class="dn-button is-plain" type="button"
|
|
58
|
-
<button class="dn-button is-primary" type="button"
|
|
179
|
+
<button class="dn-button is-plain" type="button">取消</button>
|
|
180
|
+
<button class="dn-button is-primary is-danger" type="button">删除</button>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
popover 附着在打开它的按钮下方:
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<div class="dn-popover" role="dialog" aria-labelledby="share-title">
|
|
189
|
+
<h3 class="dn-popover-title" id="share-title">共享链接</h3>
|
|
190
|
+
<p class="dn-popover-text">拿到链接的人都可以查看这条笔记。</p>
|
|
191
|
+
<div class="dn-popover-row">
|
|
192
|
+
<input class="dn-input" value="deernova.app/n/8f3k" readonly aria-label="共享链接">
|
|
193
|
+
<button class="dn-button" type="button">复制</button>
|
|
59
194
|
</div>
|
|
60
195
|
</div>
|
|
61
196
|
```
|
|
62
197
|
|
|
63
|
-
|
|
198
|
+
真实页面里,popover 用 `<div class="dn-popover" popover id="share">`,打开它的按钮加 `popovertarget="share"`。
|
|
64
199
|
|
|
65
200
|
## SwiftUI
|
|
66
201
|
|
|
202
|
+
在 iOS 26 上,中等高度的弹窗由系统渲染成悬浮的玻璃面板,拉到全高后变成不透明;Mac 上对应的是附着在窗口上的 sheet 和系统 Alert。直接用系统样式,不要用 `.presentationBackground` 盖掉它。
|
|
203
|
+
|
|
67
204
|
```swift
|
|
68
|
-
|
|
69
|
-
|
|
205
|
+
// 短任务:iPhone 上是底部弹窗,iPad 和 Mac 上是居中的表单弹窗
|
|
206
|
+
.sheet(isPresented: $showRename) {
|
|
207
|
+
RenameView()
|
|
70
208
|
.presentationDetents([.medium, .large])
|
|
71
209
|
.presentationDragIndicator(.visible)
|
|
210
|
+
.presentationSizing(.form)
|
|
211
|
+
.interactiveDismissDisabled(hasChanges) // 有未保存的输入时,不能下拉关闭
|
|
72
212
|
}
|
|
73
213
|
|
|
74
|
-
//
|
|
75
|
-
.confirmationDialog("
|
|
214
|
+
// 危险确认:iPhone 上从底部弹出,iPad 和 Mac 上附着在按钮上
|
|
215
|
+
.confirmationDialog("删除“周会记录”?", isPresented: $confirmDelete, titleVisibility: .visible) {
|
|
76
216
|
Button("删除", role: .destructive) { delete() }
|
|
217
|
+
} message: {
|
|
218
|
+
Text("删除后无法恢复,共享链接也会失效。")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 必须马上知道的错误
|
|
222
|
+
.alert("无法同步", isPresented: $syncFailed) {
|
|
223
|
+
Button("重试") { sync() }
|
|
224
|
+
Button("取消", role: .cancel) {}
|
|
225
|
+
} message: {
|
|
226
|
+
Text("网络连接已断开。")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 附着在控件上的小面板;iPhone 上系统会自动改成底部弹窗
|
|
230
|
+
.popover(isPresented: $showShare) {
|
|
231
|
+
ShareLinkView()
|
|
232
|
+
.padding(Deernova.popoverPadding)
|
|
77
233
|
}
|
|
78
234
|
```
|
|
79
235
|
|
|
236
|
+
只在 iOS 上、需要占满整屏的流程(新手引导、拍照)用 `.fullScreenCover`。
|
|
237
|
+
|
|
80
238
|
## 不要
|
|
81
239
|
|
|
82
|
-
-
|
|
240
|
+
- 在弹窗里再弹一个弹窗(「放弃修改?」的确认除外);在弹窗里做多步流程。
|
|
83
241
|
- 把长篇文章放进玻璃弹窗。
|
|
84
242
|
- 没有取消方式;把价格之类的关键信息藏在小字里。
|
|
243
|
+
- 按钮写「确定」「OK」;危险确认把默认焦点放在危险按钮上。
|
|
244
|
+
- 用弹窗告诉用户“已保存”这类不需要回应的消息(用轻提示)。
|
|
245
|
+
- 有输入的弹窗点遮罩就关闭,丢掉用户填的内容。
|
|
246
|
+
- 在手机上弹 popover,或者在 iPad 大屏上弹撑满全屏的底部弹窗。
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- 出错时边框变 `danger`,说明文字换成错误信息,同样用 `danger` 色。错误信息要说清楚怎么改。
|
|
21
21
|
- 标签在输入框上方,`callout` 字号、中粗、`ink-secondary`;说明在下方,`caption` 字号、常规字重、`ink-tertiary`。标签、输入框、说明之间 `space-1`。
|
|
22
22
|
- 触控端输入文字不小于 16px,避免浏览器在聚焦时放大页面(`field-font-size` 已经保证)。
|
|
23
|
-
- 下拉选择(`select
|
|
23
|
+
- 下拉选择(`select`)和输入框同高、同边框,写法见 [菜单与下拉](menu.md)。
|
|
24
24
|
|
|
25
25
|
## Web
|
|
26
26
|
|