@deernova/style 0.3.0 → 0.3.2
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/README.md +12 -1
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
- package/skill/assets/DeernovaTokens.swift +32 -2
- package/skill/assets/tokens.css +55 -4
- package/skill/assets/tokens.json +136 -6
- package/skill/manifest.json +2 -2
- package/skill/references/components/button.md +2 -1
- package/skill/references/components/menu.md +223 -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/review.md +1 -1
- package/skill/references/scenes/web-marketing.md +3 -1
- package/skill/references/scenes/web.md +2 -1
- package/skill/references/tokens.md +30 -2
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Install the Deernova design skill into an existing repository, so AI agents follow Deernova when they build or review interfaces there. Requires Node.js 22+. No runtime dependencies.
|
|
4
4
|
|
|
5
|
+
In a project (run inside it, or pass `--repo <path>`):
|
|
6
|
+
|
|
5
7
|
```sh
|
|
6
8
|
npx @deernova/style@latest install # writes .agents/skills/deernova-style/
|
|
7
9
|
npx @deernova/style@latest install --agent claude # writes .claude/skills/deernova-style/
|
|
@@ -9,7 +11,16 @@ npx @deernova/style@latest update
|
|
|
9
11
|
npx @deernova/style@latest doctor
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
For every project on your machine, install into your user directory:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx @deernova/style@latest install --global --agent claude # writes ~/.claude/skills/deernova-style/
|
|
18
|
+
npx @deernova/style@latest install --global # Codex: writes $CODEX_HOME/skills/deernova-style/
|
|
19
|
+
npx @deernova/style@latest update --global --agent claude
|
|
20
|
+
npx @deernova/style@latest doctor --global --agent claude
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The CLI writes only the skill directory. It does not add application code, CSS imports or package dependencies.
|
|
13
24
|
|
|
14
25
|
The skill is guidance, not a component library: `SKILL.md` picks the scene (macOS, iOS, iPadOS, web app, website) and routes to `references/` (`foundations/`, `components/` with size tables and reference CSS/SwiftUI, `scenes/`, the token table and a review checklist). `assets/` holds `tokens.json` and the files generated from it — `tokens.css` (themes × densities), `tailwind.css` (Tailwind v4) and `DeernovaTokens.swift` — plus reference screenshots. Agents apply it through the project's existing framework and components.
|
|
15
26
|
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: deernova-style
|
|
3
|
-
description: Deernova 的界面设计语言:只用黑白灰,内容放在不透明纸面上,原生端的操作层用系统 Liquid Glass,Web 应用用不透明平面加 1px 线;界面和 AI 回答用无衬线,衬线只给大标题和长文;按场景(Web 应用、macOS、iOS、iPadOS、官网与文档)切换密度。在 Deernova
|
|
3
|
+
description: Deernova 的界面设计语言:只用黑白灰,内容放在不透明纸面上,原生端的操作层用系统 Liquid Glass,Web 应用用不透明平面加 1px 线;界面和 AI 回答用无衬线,衬线只给大标题和长文;按场景(Web 应用、macOS、iOS、iPadOS、官网与文档)切换密度。在 Deernova 的应用或网站里新建、修改或审查界面时使用,包括颜色、字体、间距、圆角、线条、阴影、按钮、输入框、菜单与下拉选择、侧边栏、顶栏、列表、卡片、弹窗、空状态、AI 对话和任务清单,以及对应的 CSS、Tailwind 或 SwiftUI 实现。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Deernova 设计语言
|
|
@@ -76,7 +76,7 @@ description: Deernova 的界面设计语言:只用黑白灰,内容放在不
|
|
|
76
76
|
|
|
77
77
|
### 组件
|
|
78
78
|
|
|
79
|
-
[按钮](references/components/button.md) · [顶栏与侧面板](references/components/toolbar.md) · [玻璃操作层](references/components/glass-controls.md) · [分段控件](references/components/segmented.md) · [文本框](references/components/text-field.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) · [
|
|
79
|
+
[按钮](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) · [消息](references/components/message.md) · [工具调用与执行步骤](references/components/step-row.md)
|
|
80
80
|
|
|
81
81
|
### 场景
|
|
82
82
|
|
|
@@ -168,6 +168,12 @@ public enum Deernova {
|
|
|
168
168
|
public static let opacitySolidHover: Double = 0.88
|
|
169
169
|
/// 对话栏和长文阅读栏的最大宽度。
|
|
170
170
|
public static let contentMax: CGFloat = 680
|
|
171
|
+
/// 菜单最大宽度;放不下的文字用省略号。
|
|
172
|
+
public static let menuMaxWidth: CGFloat = 320
|
|
173
|
+
/// 底部弹窗顶部拖拽条的宽度。
|
|
174
|
+
public static let sheetGrabberWidth: CGFloat = 36
|
|
175
|
+
/// 拖拽条的高度。
|
|
176
|
+
public static let sheetGrabberHeight: CGFloat = 5
|
|
171
177
|
|
|
172
178
|
// 按密度取值的尺寸:macOS 是 desktop(只用于自己画的内容区),iOS 和 iPadOS 是 touch。
|
|
173
179
|
#if os(macOS)
|
|
@@ -226,8 +232,20 @@ public enum Deernova {
|
|
|
226
232
|
public static let emptyIconSize: CGFloat = 32
|
|
227
233
|
public static let toastHeight: CGFloat = 36
|
|
228
234
|
public static let bannerRadius: CGFloat = 10
|
|
229
|
-
public static let
|
|
235
|
+
public static let menuItemHeight: CGFloat = 28
|
|
236
|
+
public static let menuItemPaddingX: CGFloat = 8
|
|
237
|
+
public static let menuFontSize: CGFloat = 14
|
|
238
|
+
public static let menuPadding: CGFloat = 4
|
|
230
239
|
public static let menuRadius: CGFloat = 8
|
|
240
|
+
public static let menuOffset: CGFloat = 4
|
|
241
|
+
public static let menuMinWidth: CGFloat = 160
|
|
242
|
+
public static let sheetWidthSm: CGFloat = 360
|
|
243
|
+
public static let sheetWidthMd: CGFloat = 480
|
|
244
|
+
public static let sheetWidthLg: CGFloat = 720
|
|
245
|
+
public static let sheetPadding: CGFloat = 20
|
|
246
|
+
public static let sheetMargin: CGFloat = 32
|
|
247
|
+
public static let sheetRadius: CGFloat = 12
|
|
248
|
+
public static let popoverPadding: CGFloat = 12
|
|
231
249
|
public static let messageFontSize: CGFloat = 15
|
|
232
250
|
public static let messageActionSize: CGFloat = 24
|
|
233
251
|
public static let iconSizeSm: CGFloat = 14
|
|
@@ -289,8 +307,20 @@ public enum Deernova {
|
|
|
289
307
|
public static let emptyIconSize: CGFloat = 36
|
|
290
308
|
public static let toastHeight: CGFloat = 40
|
|
291
309
|
public static let bannerRadius: CGFloat = 12
|
|
292
|
-
public static let
|
|
310
|
+
public static let menuItemHeight: CGFloat = 44
|
|
311
|
+
public static let menuItemPaddingX: CGFloat = 10
|
|
312
|
+
public static let menuFontSize: CGFloat = 17
|
|
313
|
+
public static let menuPadding: CGFloat = 6
|
|
293
314
|
public static let menuRadius: CGFloat = 12
|
|
315
|
+
public static let menuOffset: CGFloat = 8
|
|
316
|
+
public static let menuMinWidth: CGFloat = 220
|
|
317
|
+
public static let sheetWidthSm: CGFloat = 360
|
|
318
|
+
public static let sheetWidthMd: CGFloat = 540
|
|
319
|
+
public static let sheetWidthLg: CGFloat = 720
|
|
320
|
+
public static let sheetPadding: CGFloat = 20
|
|
321
|
+
public static let sheetMargin: CGFloat = 8
|
|
322
|
+
public static let sheetRadius: CGFloat = 24
|
|
323
|
+
public static let popoverPadding: CGFloat = 16
|
|
294
324
|
public static let messageFontSize: CGFloat = 15
|
|
295
325
|
public static let messageActionSize: CGFloat = 32
|
|
296
326
|
public static let iconSizeSm: CGFloat = 17
|
package/skill/assets/tokens.css
CHANGED
|
@@ -157,6 +157,9 @@
|
|
|
157
157
|
--glass-blur: 16px;
|
|
158
158
|
--glass-saturate: 1.4;
|
|
159
159
|
--content-max: 680px;
|
|
160
|
+
--menu-max-width: 320px;
|
|
161
|
+
--sheet-grabber-width: 36px;
|
|
162
|
+
--sheet-grabber-height: 5px;
|
|
160
163
|
--icon-stroke: 1.75px;
|
|
161
164
|
}
|
|
162
165
|
|
|
@@ -244,8 +247,20 @@
|
|
|
244
247
|
--empty-icon-size: 32px;
|
|
245
248
|
--toast-height: 36px;
|
|
246
249
|
--banner-radius: 10px;
|
|
247
|
-
--
|
|
250
|
+
--menu-item-height: 28px;
|
|
251
|
+
--menu-item-padding-x: 8px;
|
|
252
|
+
--menu-font-size: 14px;
|
|
253
|
+
--menu-padding: 4px;
|
|
248
254
|
--menu-radius: 8px;
|
|
255
|
+
--menu-offset: 4px;
|
|
256
|
+
--menu-min-width: 160px;
|
|
257
|
+
--sheet-width-sm: 360px;
|
|
258
|
+
--sheet-width-md: 480px;
|
|
259
|
+
--sheet-width-lg: 720px;
|
|
260
|
+
--sheet-padding: 20px;
|
|
261
|
+
--sheet-margin: 32px;
|
|
262
|
+
--sheet-radius: 12px;
|
|
263
|
+
--popover-padding: 12px;
|
|
249
264
|
--message-font-size: 15px;
|
|
250
265
|
--message-action-size: 24px;
|
|
251
266
|
--icon-size-sm: 14px;
|
|
@@ -337,8 +352,20 @@
|
|
|
337
352
|
--empty-icon-size: 36px;
|
|
338
353
|
--toast-height: 40px;
|
|
339
354
|
--banner-radius: 12px;
|
|
340
|
-
--
|
|
355
|
+
--menu-item-height: 44px;
|
|
356
|
+
--menu-item-padding-x: 10px;
|
|
357
|
+
--menu-font-size: 17px;
|
|
358
|
+
--menu-padding: 6px;
|
|
341
359
|
--menu-radius: 12px;
|
|
360
|
+
--menu-offset: 8px;
|
|
361
|
+
--menu-min-width: 220px;
|
|
362
|
+
--sheet-width-sm: 360px;
|
|
363
|
+
--sheet-width-md: 540px;
|
|
364
|
+
--sheet-width-lg: 720px;
|
|
365
|
+
--sheet-padding: 20px;
|
|
366
|
+
--sheet-margin: 8px;
|
|
367
|
+
--sheet-radius: 24px;
|
|
368
|
+
--popover-padding: 16px;
|
|
342
369
|
--message-font-size: 15px;
|
|
343
370
|
--message-action-size: 32px;
|
|
344
371
|
--icon-size-sm: 17px;
|
|
@@ -430,8 +457,20 @@
|
|
|
430
457
|
--empty-icon-size: 36px;
|
|
431
458
|
--toast-height: 40px;
|
|
432
459
|
--banner-radius: 12px;
|
|
433
|
-
--
|
|
460
|
+
--menu-item-height: 44px;
|
|
461
|
+
--menu-item-padding-x: 10px;
|
|
462
|
+
--menu-font-size: 17px;
|
|
463
|
+
--menu-padding: 6px;
|
|
434
464
|
--menu-radius: 12px;
|
|
465
|
+
--menu-offset: 8px;
|
|
466
|
+
--menu-min-width: 220px;
|
|
467
|
+
--sheet-width-sm: 360px;
|
|
468
|
+
--sheet-width-md: 540px;
|
|
469
|
+
--sheet-width-lg: 720px;
|
|
470
|
+
--sheet-padding: 20px;
|
|
471
|
+
--sheet-margin: 8px;
|
|
472
|
+
--sheet-radius: 24px;
|
|
473
|
+
--popover-padding: 16px;
|
|
435
474
|
--message-font-size: 15px;
|
|
436
475
|
--message-action-size: 32px;
|
|
437
476
|
--icon-size-sm: 17px;
|
|
@@ -522,8 +561,20 @@
|
|
|
522
561
|
--empty-icon-size: 40px;
|
|
523
562
|
--toast-height: 40px;
|
|
524
563
|
--banner-radius: 12px;
|
|
525
|
-
--
|
|
564
|
+
--menu-item-height: 36px;
|
|
565
|
+
--menu-item-padding-x: 10px;
|
|
566
|
+
--menu-font-size: 15px;
|
|
567
|
+
--menu-padding: 6px;
|
|
526
568
|
--menu-radius: 12px;
|
|
569
|
+
--menu-offset: 8px;
|
|
570
|
+
--menu-min-width: 200px;
|
|
571
|
+
--sheet-width-sm: 400px;
|
|
572
|
+
--sheet-width-md: 560px;
|
|
573
|
+
--sheet-width-lg: 800px;
|
|
574
|
+
--sheet-padding: 24px;
|
|
575
|
+
--sheet-margin: 32px;
|
|
576
|
+
--sheet-radius: 16px;
|
|
577
|
+
--popover-padding: 16px;
|
|
527
578
|
--message-font-size: 15px;
|
|
528
579
|
--message-action-size: 28px;
|
|
529
580
|
--icon-size-sm: 16px;
|
package/skill/assets/tokens.json
CHANGED
|
@@ -1590,11 +1590,131 @@
|
|
|
1590
1590
|
}
|
|
1591
1591
|
]
|
|
1592
1592
|
},
|
|
1593
|
+
{
|
|
1594
|
+
"id": "menu",
|
|
1595
|
+
"title": "菜单与下拉",
|
|
1596
|
+
"reference": "references/components/menu.md",
|
|
1597
|
+
"tokens": [
|
|
1598
|
+
{
|
|
1599
|
+
"name": "menu-item-height",
|
|
1600
|
+
"values": {
|
|
1601
|
+
"desktop": 28,
|
|
1602
|
+
"touch": 44,
|
|
1603
|
+
"comfortable": 36
|
|
1604
|
+
},
|
|
1605
|
+
"usage": "菜单项和下拉选项的高度。"
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
"name": "menu-item-padding-x",
|
|
1609
|
+
"values": {
|
|
1610
|
+
"desktop": 8,
|
|
1611
|
+
"touch": 10,
|
|
1612
|
+
"comfortable": 10
|
|
1613
|
+
},
|
|
1614
|
+
"usage": "菜单项左右内边距。"
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
"name": "menu-font-size",
|
|
1618
|
+
"values": {
|
|
1619
|
+
"desktop": 14,
|
|
1620
|
+
"touch": 17,
|
|
1621
|
+
"comfortable": 15
|
|
1622
|
+
},
|
|
1623
|
+
"usage": "菜单项字号。"
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
"name": "menu-padding",
|
|
1627
|
+
"values": {
|
|
1628
|
+
"desktop": 4,
|
|
1629
|
+
"touch": 6,
|
|
1630
|
+
"comfortable": 6
|
|
1631
|
+
},
|
|
1632
|
+
"usage": "菜单面板的内边距,也是分隔线上下的间距。"
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
"name": "menu-radius",
|
|
1636
|
+
"values": {
|
|
1637
|
+
"desktop": 8,
|
|
1638
|
+
"touch": 12,
|
|
1639
|
+
"comfortable": 12
|
|
1640
|
+
},
|
|
1641
|
+
"usage": "菜单和 popover 的圆角;菜单项的圆角 = 它减去 menu-padding。"
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
"name": "menu-offset",
|
|
1645
|
+
"values": {
|
|
1646
|
+
"desktop": 4,
|
|
1647
|
+
"touch": 8,
|
|
1648
|
+
"comfortable": 8
|
|
1649
|
+
},
|
|
1650
|
+
"usage": "菜单和触发它的控件之间的距离。"
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
"name": "menu-min-width",
|
|
1654
|
+
"values": {
|
|
1655
|
+
"desktop": 160,
|
|
1656
|
+
"touch": 220,
|
|
1657
|
+
"comfortable": 200
|
|
1658
|
+
},
|
|
1659
|
+
"usage": "菜单最小宽度;同时不窄于触发器。"
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
"name": "menu-max-width",
|
|
1663
|
+
"value": "320px",
|
|
1664
|
+
"usage": "菜单最大宽度;放不下的文字用省略号。"
|
|
1665
|
+
}
|
|
1666
|
+
]
|
|
1667
|
+
},
|
|
1593
1668
|
{
|
|
1594
1669
|
"id": "sheet",
|
|
1595
|
-
"title": "
|
|
1670
|
+
"title": "弹窗、底部弹窗与 popover",
|
|
1596
1671
|
"reference": "references/components/sheet.md",
|
|
1597
1672
|
"tokens": [
|
|
1673
|
+
{
|
|
1674
|
+
"name": "sheet-width-sm",
|
|
1675
|
+
"values": {
|
|
1676
|
+
"desktop": 360,
|
|
1677
|
+
"touch": 360,
|
|
1678
|
+
"comfortable": 400
|
|
1679
|
+
},
|
|
1680
|
+
"usage": "确认弹窗、只有一两句话的提示。"
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
"name": "sheet-width-md",
|
|
1684
|
+
"values": {
|
|
1685
|
+
"desktop": 480,
|
|
1686
|
+
"touch": 540,
|
|
1687
|
+
"comfortable": 560
|
|
1688
|
+
},
|
|
1689
|
+
"usage": "默认宽度:短表单、选择分享方式这类短任务。"
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
"name": "sheet-width-lg",
|
|
1693
|
+
"values": {
|
|
1694
|
+
"desktop": 720,
|
|
1695
|
+
"touch": 720,
|
|
1696
|
+
"comfortable": 800
|
|
1697
|
+
},
|
|
1698
|
+
"usage": "预览、较长的设置,或分两栏的内容。"
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
"name": "sheet-padding",
|
|
1702
|
+
"values": {
|
|
1703
|
+
"desktop": 20,
|
|
1704
|
+
"touch": 20,
|
|
1705
|
+
"comfortable": 24
|
|
1706
|
+
},
|
|
1707
|
+
"usage": "弹窗内边距;标题、内容和按钮区都和它对齐。"
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
"name": "sheet-margin",
|
|
1711
|
+
"values": {
|
|
1712
|
+
"desktop": 32,
|
|
1713
|
+
"touch": 8,
|
|
1714
|
+
"comfortable": 32
|
|
1715
|
+
},
|
|
1716
|
+
"usage": "弹窗离窗口边缘的最小距离;触控端底部弹窗悬浮时离屏幕边缘的距离。"
|
|
1717
|
+
},
|
|
1598
1718
|
{
|
|
1599
1719
|
"name": "sheet-radius",
|
|
1600
1720
|
"values": {
|
|
@@ -1605,13 +1725,23 @@
|
|
|
1605
1725
|
"usage": "弹窗和底部弹窗的圆角。"
|
|
1606
1726
|
},
|
|
1607
1727
|
{
|
|
1608
|
-
"name": "
|
|
1728
|
+
"name": "sheet-grabber-width",
|
|
1729
|
+
"value": "36px",
|
|
1730
|
+
"usage": "底部弹窗顶部拖拽条的宽度。"
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
"name": "sheet-grabber-height",
|
|
1734
|
+
"value": "5px",
|
|
1735
|
+
"usage": "拖拽条的高度。"
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
"name": "popover-padding",
|
|
1609
1739
|
"values": {
|
|
1610
|
-
"desktop":
|
|
1611
|
-
"touch":
|
|
1612
|
-
"comfortable":
|
|
1740
|
+
"desktop": 12,
|
|
1741
|
+
"touch": 16,
|
|
1742
|
+
"comfortable": 16
|
|
1613
1743
|
},
|
|
1614
|
-
"usage": "
|
|
1744
|
+
"usage": "popover 的内边距;圆角用 menu-radius,宽度不超过 sheet-width-sm。"
|
|
1615
1745
|
}
|
|
1616
1746
|
]
|
|
1617
1747
|
},
|
package/skill/manifest.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- **主按钮** `is-primary`:`accent` 底、`on-accent` 字。保存、发送、购买这类关键动作,一屏最多一个。
|
|
10
10
|
- **次按钮**(默认):`fill-selected` 灰底、无描边、无阴影、`ink` 字。半透明的灰底放在任何纸面上都成立。
|
|
11
11
|
- **文字按钮** `is-plain`:无底,`ink-secondary` 字,悬停时出现 `fill-hover`。工具栏和卡片里的次要操作。
|
|
12
|
-
- **危险按钮** `is-danger`:`danger-soft` 底、`danger`
|
|
12
|
+
- **危险按钮** `is-danger`:`danger-soft` 底、`danger` 字。只有在最终的[危险确认弹窗](sheet.md)里,才用实色的危险样式 `is-primary is-danger`:`danger` 底、`on-accent` 字(SwiftUI 的 `role: .destructive`)。
|
|
13
13
|
- **图标按钮** `dn-icon-button`:只有图标,无底,`ink-secondary`,必须有无障碍标签。
|
|
14
14
|
|
|
15
15
|
## 尺寸
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
.dn-button.is-plain { background: transparent; color: var(--ink-secondary); }
|
|
83
83
|
.dn-button.is-plain:hover:not(:disabled) { background: var(--fill-hover); color: var(--ink); }
|
|
84
84
|
.dn-button.is-danger { background: var(--danger-soft); color: var(--danger); }
|
|
85
|
+
.dn-button.is-primary.is-danger { background: var(--danger); color: var(--on-accent); }
|
|
85
86
|
|
|
86
87
|
.dn-button.is-sm { height: var(--button-height-sm); padding: 0 var(--button-padding-x-sm); border-radius: var(--button-radius-sm); font-size: var(--button-font-size-sm); }
|
|
87
88
|
.dn-button.is-lg { height: var(--button-height-lg); padding: 0 var(--button-padding-x-lg); font-size: var(--button-font-size-lg); }
|
|
@@ -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 应用的菜单上用玻璃或模糊效果(官网页眉展开的菜单除外)。
|
|
@@ -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
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- **字体**:界面文字和 AI 回答是无衬线;衬线只出现在大标题、空状态标题、官网和文章类长文;代码是等宽;字号来自角色变量(`--type-*`、`.deernovaX`);iOS 用文本样式,没有写死字号;对齐的数字用等宽数字。
|
|
15
15
|
- **字重**:粗体只出现在真正的标题上;分组标题、会话名、顶栏标题、说明是常规字重,靠灰度区分。
|
|
16
16
|
- **间距与圆角**:间距在 4pt 网格上;组与组之间至少是组内的 2 倍;圆角来自组件的 `*-radius` token,嵌套圆角同心;阅读栏宽不超过 `content-max`。
|
|
17
|
-
-
|
|
17
|
+
- **交互**:一屏最多一个主按钮;按钮文字以动词开头;有悬停、按下、选中、禁用、加载、空、错误这些需要的状态;禁用和加载时布局不跳动;键盘焦点清楚;菜单和下拉点击才打开,Esc、点外面、选中一项都能关闭,焦点回到触发器,方向键能走完所有项;弹窗打开时背景不能操作也不滚动,关闭后焦点回到原处,危险确认默认聚焦「取消」。
|
|
18
18
|
- **动效**:时长来自 `duration-*`;动效说明因果;没有无目的的循环;减弱动态效果时只剩淡入淡出;切换内容时阅读位置不跳。
|
|
19
19
|
- **无障碍**:两套主题都看过;最大字号或 200% 缩放下不截断;只有图标的按钮有标签;降低透明度时玻璃变成实色。
|
|
20
20
|
- **文案**:用“你”;没有感叹号和 emoji;完成态用「已保存」;错误说明原因和解决办法;中文与英文、数字之间有半角空格。
|
|
@@ -25,8 +25,10 @@
|
|
|
25
25
|
|
|
26
26
|
品牌展示页可以有叙事动效(首页的麋鹿),但要能暂停、不妨碍阅读;减弱动态效果时保持静止。打字效果、滚动视差这类装饰只用一次,不要每个区块都动。
|
|
27
27
|
|
|
28
|
+
插画默认是黑白的。颜色只能作为用户触发、几秒后自行褪回黑白的插画效果出现(首页点鹿后场景上色);页眉、文字、按钮和链接始终用黑白设计变量,不跟着变色。
|
|
29
|
+
|
|
28
30
|
## 不要
|
|
29
31
|
|
|
30
32
|
- 把这里的 44px 胶囊按钮和大字号搬进应用界面。
|
|
31
|
-
-
|
|
33
|
+
- 为了“高级感”加渐变、常驻的彩色插画或大面积阴影;让颜色进入页眉、文字和按钮。
|
|
32
34
|
- 把给 agent 或开发者看的安装说明放在首屏;放到页面末尾或单独的资源页。
|
|
@@ -71,7 +71,8 @@ Web 上按钮“上下特别宽”几乎都是同一个原因:用内边距和
|
|
|
71
71
|
- 焦点环:`outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset);`,只在 `:focus-visible` 时显示。
|
|
72
72
|
- 细线:`border: var(--stroke-line) solid var(--line);`。
|
|
73
73
|
- 分段控件、开关这类有状态的控件,用 `aria-pressed`、`role="switch"` 或 `role="tab"` 标出当前状态。
|
|
74
|
-
- 弹窗用 `<dialog>` 的 `showModal()`,自带焦点管理和 Esc
|
|
74
|
+
- 弹窗用 `<dialog>` 的 `showModal()`,自带焦点管理和 Esc 关闭;popover 用 `popover` 属性。打开、关闭和焦点规则见 [弹窗与 popover](../components/sheet.md)。
|
|
75
|
+
- 菜单和下拉:选值优先用原生 `<select>`,命令菜单和链接列表按 [菜单与下拉](../components/menu.md) 选语义、处理键盘。
|
|
75
76
|
- 悬停效果放在 `@media (hover: hover)` 里,触屏上不会卡在悬停状态。
|
|
76
77
|
|
|
77
78
|
## 验证
|
|
@@ -408,14 +408,42 @@
|
|
|
408
408
|
| `toast-height` | 36 | 40 | 40 | 轻提示胶囊的高度。 |
|
|
409
409
|
| `banner-radius` | 10 | 12 | 12 | 横幅和贴在输入区上方的提示条的圆角。 |
|
|
410
410
|
|
|
411
|
-
###
|
|
411
|
+
### 菜单与下拉
|
|
412
|
+
|
|
413
|
+
见 [components/menu.md](components/menu.md)。
|
|
414
|
+
|
|
415
|
+
| Token | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 | 用途 |
|
|
416
|
+
| --- | --- | --- | --- | --- |
|
|
417
|
+
| `menu-item-height` | 28 | 44 | 36 | 菜单项和下拉选项的高度。 |
|
|
418
|
+
| `menu-item-padding-x` | 8 | 10 | 10 | 菜单项左右内边距。 |
|
|
419
|
+
| `menu-font-size` | 14 | 17 | 15 | 菜单项字号。 |
|
|
420
|
+
| `menu-padding` | 4 | 6 | 6 | 菜单面板的内边距,也是分隔线上下的间距。 |
|
|
421
|
+
| `menu-radius` | 8 | 12 | 12 | 菜单和 popover 的圆角;菜单项的圆角 = 它减去 menu-padding。 |
|
|
422
|
+
| `menu-offset` | 4 | 8 | 8 | 菜单和触发它的控件之间的距离。 |
|
|
423
|
+
| `menu-min-width` | 160 | 220 | 200 | 菜单最小宽度;同时不窄于触发器。 |
|
|
424
|
+
|
|
425
|
+
| Token | 值 | 用途 |
|
|
426
|
+
| --- | --- | --- |
|
|
427
|
+
| `menu-max-width` | `320px` | 菜单最大宽度;放不下的文字用省略号。 |
|
|
428
|
+
|
|
429
|
+
### 弹窗、底部弹窗与 popover
|
|
412
430
|
|
|
413
431
|
见 [components/sheet.md](components/sheet.md)。
|
|
414
432
|
|
|
415
433
|
| Token | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 | 用途 |
|
|
416
434
|
| --- | --- | --- | --- | --- |
|
|
435
|
+
| `sheet-width-sm` | 360 | 360 | 400 | 确认弹窗、只有一两句话的提示。 |
|
|
436
|
+
| `sheet-width-md` | 480 | 540 | 560 | 默认宽度:短表单、选择分享方式这类短任务。 |
|
|
437
|
+
| `sheet-width-lg` | 720 | 720 | 800 | 预览、较长的设置,或分两栏的内容。 |
|
|
438
|
+
| `sheet-padding` | 20 | 20 | 24 | 弹窗内边距;标题、内容和按钮区都和它对齐。 |
|
|
439
|
+
| `sheet-margin` | 32 | 8 | 32 | 弹窗离窗口边缘的最小距离;触控端底部弹窗悬浮时离屏幕边缘的距离。 |
|
|
417
440
|
| `sheet-radius` | 12 | 24 | 16 | 弹窗和底部弹窗的圆角。 |
|
|
418
|
-
| `
|
|
441
|
+
| `popover-padding` | 12 | 16 | 16 | popover 的内边距;圆角用 menu-radius,宽度不超过 sheet-width-sm。 |
|
|
442
|
+
|
|
443
|
+
| Token | 值 | 用途 |
|
|
444
|
+
| --- | --- | --- |
|
|
445
|
+
| `sheet-grabber-width` | `36px` | 底部弹窗顶部拖拽条的宽度。 |
|
|
446
|
+
| `sheet-grabber-height` | `5px` | 拖拽条的高度。 |
|
|
419
447
|
|
|
420
448
|
### 消息
|
|
421
449
|
|