@deernova/style 0.3.3 → 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 +7 -6
- package/skill/assets/tokens.css +2 -0
- package/skill/assets/tokens.json +16 -0
- package/skill/manifest.json +2 -2
- package/skill/references/components/button.md +6 -4
- package/skill/references/components/card.md +1 -1
- package/skill/references/components/composer.md +2 -1
- package/skill/references/components/menu.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/foundations/color.md +1 -1
- package/skill/references/foundations/layout.md +60 -1
- package/skill/references/foundations/states.md +98 -0
- package/skill/references/review.md +1 -1
- package/skill/references/scenes/web.md +1 -1
- package/skill/references/tokens.md +2 -0
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -53,11 +53,12 @@ description: Deernova 的界面设计语言:只用黑白灰,内容放在不
|
|
|
53
53
|
2. **黑白灰,强调靠对比。** `accent` 就是墨色 `ink`:主按钮在浅色主题是黑底白字,在深色主题是白底黑字,文字用 `on-accent`,不要写死白色。唯一的彩色是状态色 `success`、`warning`、`danger`、`info`,只用来表达状态,而且必须同时带图标或文字。
|
|
54
54
|
3. **线条代替阴影。** 区块之间用 `line` 色的 1px 线分隔(`stroke-line`)。卡片不加阴影;只有菜单、弹窗、浮动面板和 Composer 这类真正浮起来的东西才用 `shadow-float`。
|
|
55
55
|
4. **无衬线做界面,衬线点题。** 界面文字、AI 回答、用户消息、标题栏都用系统无衬线(SF Pro);代码和需要对齐的数字用等宽(SF Mono)。衬线(New York)只用在页面大标题和空状态标题(`display`、`title`)、官网与文档,以及文章类的长文阅读(`prose`)。中文统一落到苹方。
|
|
56
|
-
5.
|
|
57
|
-
6.
|
|
58
|
-
7.
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
5. **状态只有两种底色。** 悬停 `fill-hover`,按下和选中 `fill-selected`,都是半透明的,叠在任何纸面上都成立;选中项悬停时不再加深。悬停只改颜色,不加阴影、不位移,只写在 `@media (hover: hover)` 里,见 [交互状态](references/foundations/states.md)。
|
|
57
|
+
6. **层次靠字号和灰度,少用粗体。** 600 只给真正的标题(`display`、`title`、`headline`)。分组标题、会话名、说明、时间戳用常规字重,靠 `ink-secondary`、`ink-tertiary` 和小一号的字区分层级。
|
|
58
|
+
7. **控件高度由 token 决定,点击区域单独补。** 按钮、输入框、列表行的可见高度直接用 `button-height-*`、`field-height`、`row-height`,上下内边距为 0,不让页面行高把它撑高。可见尺寸小于 `hit-min` 时,用透明的命中区补足,不加大可见的控件。
|
|
59
|
+
8. **间距成组。** 间距走 4pt 网格。组内用 `gap-stack`,组与组之间用 `gap-group`(至少 2 倍);内容区里段落、工具调用、卡片之间 `space-4`。用细线和少量留白分组,不靠大块空白和外框。
|
|
60
|
+
9. **原生端用系统控件,Web 端用同一套自绘组件。** SwiftUI 端的导航结构、侧边栏、工具栏、菜单、表单、弹窗直接用系统控件和系统尺寸,只通过颜色和字体让它们融入;自己画的内容区(对话、卡片、输入区、任务清单)用 token。Web 应用没有系统控件可借,按组件说明里的规格画,骨架见 [Web 应用](references/scenes/web.md)。
|
|
61
|
+
10. **对齐按内容选。** 单行的控件和行在固定高度里垂直居中;图标和文字按中线,不同字号的文字按基线;三行以上的内容,图标和头像对齐第一行;空状态放在略高于正中的位置。不用内边距或 `top` 微调去凑居中。内容栏限宽并在主内容区里居中,见 [布局与对齐](references/foundations/layout.md)。
|
|
61
62
|
|
|
62
63
|
## 读什么
|
|
63
64
|
|
|
@@ -73,7 +74,7 @@ description: Deernova 的界面设计语言:只用黑白灰,内容放在不
|
|
|
73
74
|
|
|
74
75
|
### 基础
|
|
75
76
|
|
|
76
|
-
[颜色](references/foundations/color.md) · [字体](references/foundations/typography.md) · [间距](references/foundations/spacing.md) · [布局与对齐](references/foundations/layout.md) · [圆角](references/foundations/radius.md) · [线条](references/foundations/stroke.md) · [层级与阴影](references/foundations/elevation.md) · [玻璃](references/foundations/glass.md) · [动效](references/foundations/motion.md) · [图标](references/foundations/iconography.md) · [文案](references/foundations/writing.md) · [无障碍](references/foundations/accessibility.md)
|
|
77
|
+
[颜色](references/foundations/color.md) · [交互状态](references/foundations/states.md) · [字体](references/foundations/typography.md) · [间距](references/foundations/spacing.md) · [布局与对齐](references/foundations/layout.md) · [圆角](references/foundations/radius.md) · [线条](references/foundations/stroke.md) · [层级与阴影](references/foundations/elevation.md) · [玻璃](references/foundations/glass.md) · [动效](references/foundations/motion.md) · [图标](references/foundations/iconography.md) · [文案](references/foundations/writing.md) · [无障碍](references/foundations/accessibility.md)
|
|
77
78
|
|
|
78
79
|
### 组件
|
|
79
80
|
|
package/skill/assets/tokens.css
CHANGED
package/skill/assets/tokens.json
CHANGED
|
@@ -1107,6 +1107,22 @@
|
|
|
1107
1107
|
"comfortable": 32
|
|
1108
1108
|
},
|
|
1109
1109
|
"usage": "官网和文档 12 栏网格的栏间距。应用里的卡片网格用 card-gap。"
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
"name": "scrollbar-size",
|
|
1113
|
+
"value": "10px",
|
|
1114
|
+
"platforms": [
|
|
1115
|
+
"web"
|
|
1116
|
+
],
|
|
1117
|
+
"usage": "网页滚动条占的宽度(横向滚动条是高度)。"
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
"name": "scrollbar-thumb-size",
|
|
1121
|
+
"value": "6px",
|
|
1122
|
+
"platforms": [
|
|
1123
|
+
"web"
|
|
1124
|
+
],
|
|
1125
|
+
"usage": "滚动条里胶囊滑块的粗细,两边各留 (scrollbar-size − 它) ÷ 2。"
|
|
1110
1126
|
}
|
|
1111
1127
|
]
|
|
1112
1128
|
},
|
package/skill/manifest.json
CHANGED
|
@@ -72,15 +72,16 @@
|
|
|
72
72
|
cursor: pointer;
|
|
73
73
|
transition: background-color var(--duration-instant) var(--ease-standard), opacity var(--duration-instant) var(--ease-standard);
|
|
74
74
|
}
|
|
75
|
-
.dn-button:hover:not(:disabled) { background-image: linear-gradient(var(--fill-hover), var(--fill-hover)); }
|
|
75
|
+
@media (hover: hover) { .dn-button:hover:not(:disabled) { background-image: linear-gradient(var(--fill-hover), var(--fill-hover)); } }
|
|
76
76
|
.dn-button:active:not(:disabled) { background-image: linear-gradient(var(--fill-selected), var(--fill-selected)); }
|
|
77
77
|
.dn-button:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset); }
|
|
78
78
|
.dn-button:disabled { opacity: var(--opacity-disabled); cursor: default; }
|
|
79
79
|
|
|
80
80
|
.dn-button.is-primary { background: var(--accent); color: var(--on-accent); }
|
|
81
|
-
.dn-button.is-primary:hover:not(:disabled)
|
|
81
|
+
@media (hover: hover) { .dn-button.is-primary:hover:not(:disabled) { background-image: none; opacity: var(--opacity-solid-hover); } }
|
|
82
|
+
.dn-button.is-primary:active:not(:disabled) { background-image: none; opacity: var(--opacity-solid-hover); }
|
|
82
83
|
.dn-button.is-plain { background: transparent; color: var(--ink-secondary); }
|
|
83
|
-
.dn-button.is-plain:hover:not(:disabled) { background: var(--fill-hover); color: var(--ink); }
|
|
84
|
+
@media (hover: hover) { .dn-button.is-plain:hover:not(:disabled) { background: var(--fill-hover); color: var(--ink); } }
|
|
84
85
|
.dn-button.is-danger { background: var(--danger-soft); color: var(--danger); }
|
|
85
86
|
.dn-button.is-primary.is-danger { background: var(--danger); color: var(--on-accent); }
|
|
86
87
|
|
|
@@ -104,7 +105,8 @@
|
|
|
104
105
|
cursor: pointer;
|
|
105
106
|
transition: background-color var(--duration-instant) var(--ease-standard);
|
|
106
107
|
}
|
|
107
|
-
.dn-icon-button:hover { background: var(--fill-hover); color: var(--ink); }
|
|
108
|
+
@media (hover: hover) { .dn-icon-button:hover { background: var(--fill-hover); color: var(--ink); } }
|
|
109
|
+
.dn-icon-button:active { background: var(--fill-selected); color: var(--ink); }
|
|
108
110
|
.dn-icon-button:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset); }
|
|
109
111
|
|
|
110
112
|
/* 点击区域:可见尺寸不变,用透明的伪元素把可点范围撑到 hit-min。
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
text-decoration: none;
|
|
40
40
|
transition: border-color var(--duration-instant) var(--ease-standard);
|
|
41
41
|
}
|
|
42
|
-
a.dn-card:hover, button.dn-card:hover { border-color: var(--line-control); }
|
|
42
|
+
@media (hover: hover) { a.dn-card:hover, button.dn-card:hover { border-color: var(--line-control); } }
|
|
43
43
|
.dn-card:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset); }
|
|
44
44
|
.dn-card[aria-current="true"], .dn-card.is-selected { border-color: var(--ink); }
|
|
45
45
|
.dn-card-title { margin: 0; font: var(--type-headline); }
|
|
@@ -124,7 +124,8 @@
|
|
|
124
124
|
font: var(--weight-regular) var(--type-callout-size)/1 var(--font-sans);
|
|
125
125
|
cursor: pointer;
|
|
126
126
|
}
|
|
127
|
-
.dn-meta-button:hover { background: var(--fill-hover); }
|
|
127
|
+
@media (hover: hover) { .dn-meta-button:hover { background: var(--fill-hover); } }
|
|
128
|
+
.dn-meta-button:active { background: var(--fill-selected); }
|
|
128
129
|
.dn-meta-button b { color: var(--ink); font-weight: var(--weight-regular); }
|
|
129
130
|
.dn-context-ring {
|
|
130
131
|
width: var(--icon-size-sm);
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
.dn-menu-item:is([aria-checked="true"], [aria-selected="true"]) > .dn-menu-check { visibility: visible; }
|
|
120
120
|
.dn-menu-item > .dn-menu-more { width: var(--icon-size-sm); height: var(--icon-size-sm); color: var(--ink-tertiary); }
|
|
121
121
|
@media (hover: hover) { .dn-menu-item:hover { background: var(--fill-hover); } }
|
|
122
|
+
.dn-menu-item:active { background: var(--fill-selected); }
|
|
122
123
|
.dn-menu-item:focus { outline: none; }
|
|
123
124
|
.dn-menu-item:focus-visible, .dn-menu-item[aria-expanded="true"] { background: var(--fill-hover); }
|
|
124
125
|
.dn-menu-item:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: calc(var(--stroke-focus) * -1); }
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
cursor: pointer;
|
|
46
46
|
transition: background-color var(--duration-instant) var(--ease-standard), color var(--duration-instant) var(--ease-standard);
|
|
47
47
|
}
|
|
48
|
-
.dn-segmented button:hover { color: var(--ink); }
|
|
48
|
+
@media (hover: hover) { .dn-segmented button:hover { color: var(--ink); } }
|
|
49
49
|
.dn-segmented button[aria-pressed="true"], .dn-segmented button[aria-selected="true"] { background: var(--surface-raised); color: var(--ink); box-shadow: var(--shadow-control); }
|
|
50
50
|
.dn-segmented button:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: var(--focus-offset); }
|
|
51
51
|
```
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
.dn-sidebar-heading::-webkit-details-marker { display: none; }
|
|
55
55
|
.dn-sidebar-heading .dn-icon { width: var(--icon-size-sm); height: var(--icon-size-sm); transition: transform var(--duration-base) var(--ease-standard); }
|
|
56
56
|
.dn-sidebar-group:not([open]) > .dn-sidebar-heading .dn-icon { transform: rotate(-90deg); }
|
|
57
|
-
.dn-sidebar-heading:hover { color: var(--ink-secondary); }
|
|
57
|
+
@media (hover: hover) { .dn-sidebar-heading:hover { color: var(--ink-secondary); } }
|
|
58
58
|
.dn-sidebar-item {
|
|
59
59
|
display: flex;
|
|
60
60
|
align-items: center;
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
}
|
|
71
71
|
.dn-sidebar-group .dn-sidebar-item { font: var(--type-callout); }
|
|
72
72
|
.dn-sidebar-item .dn-icon { width: var(--icon-size-md); height: var(--icon-size-md); color: var(--ink-secondary); }
|
|
73
|
-
.dn-sidebar-item:hover { background: var(--fill-hover); }
|
|
73
|
+
@media (hover: hover) { .dn-sidebar-item:hover { background: var(--fill-hover); } }
|
|
74
|
+
.dn-sidebar-item:active { background: var(--fill-selected); }
|
|
74
75
|
.dn-sidebar-item[aria-current="page"] { background: var(--fill-selected); }
|
|
75
76
|
.dn-sidebar-item:focus-visible { outline: var(--stroke-focus) solid var(--focus-ring); outline-offset: calc(var(--focus-offset) * -1); }
|
|
76
77
|
.dn-sidebar-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -30,7 +30,7 @@ AI 在对话里做了什么。分两层:平时只显示一行灰色的**摘要
|
|
|
30
30
|
cursor: pointer;
|
|
31
31
|
}
|
|
32
32
|
.dn-activity > summary::-webkit-details-marker { display: none; }
|
|
33
|
-
.dn-activity > summary:hover { color: var(--ink); }
|
|
33
|
+
@media (hover: hover) { .dn-activity > summary:hover { color: var(--ink); } }
|
|
34
34
|
.dn-activity > summary .dn-icon { width: var(--icon-size-sm); height: var(--icon-size-sm); transition: transform var(--duration-base) var(--ease-standard); }
|
|
35
35
|
.dn-activity[open] > summary .dn-icon { transform: rotate(90deg); }
|
|
36
36
|
.dn-activity > .dn-steps { margin: var(--space-1) 0 0 6px; padding-left: var(--space-3); border-left: var(--stroke-line) solid var(--line); }
|
|
@@ -50,7 +50,8 @@ AI 在对话里做了什么。分两层:平时只显示一行灰色的**摘要
|
|
|
50
50
|
}
|
|
51
51
|
.dn-step::-webkit-details-marker { display: none; }
|
|
52
52
|
details > .dn-step { cursor: pointer; }
|
|
53
|
-
.dn-step:hover { background: var(--fill-hover); }
|
|
53
|
+
@media (hover: hover) { .dn-step:hover { background: var(--fill-hover); } }
|
|
54
|
+
.dn-step:active { background: var(--fill-selected); }
|
|
54
55
|
.dn-step .dn-icon { width: var(--icon-size-md); height: var(--icon-size-md); }
|
|
55
56
|
.dn-step-name { color: var(--ink); font-weight: var(--weight-semibold); white-space: nowrap; }
|
|
56
57
|
.dn-step-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--weight-regular) var(--type-caption-size)/1 var(--font-mono); }
|
|
@@ -23,7 +23,7 @@ Web 应用的页面骨架:左边是[侧边栏](sidebar.md),中间是内容
|
|
|
23
23
|
```css
|
|
24
24
|
.dn-app { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; height: 100dvh; background: var(--canvas); color: var(--ink); }
|
|
25
25
|
.dn-app-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
26
|
-
.dn-app-content { flex: 1; min-height: 0; overflow: auto; padding: var(--space-6) calc(var(--page-margin) + var(--space-3)); }
|
|
26
|
+
.dn-app-content { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: var(--space-6) calc(var(--page-margin) + var(--space-3)); }
|
|
27
27
|
|
|
28
28
|
.dn-toolbar {
|
|
29
29
|
position: sticky;
|
|
@@ -53,7 +53,8 @@ Web 应用的页面骨架:左边是[侧边栏](sidebar.md),中间是内容
|
|
|
53
53
|
}
|
|
54
54
|
.dn-toolbar-title span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
55
55
|
.dn-toolbar-title .dn-icon { width: var(--icon-size-sm); height: var(--icon-size-sm); color: var(--ink-secondary); }
|
|
56
|
-
.dn-toolbar-title:hover { background: var(--fill-hover); }
|
|
56
|
+
@media (hover: hover) { .dn-toolbar-title:hover { background: var(--fill-hover); } }
|
|
57
|
+
.dn-toolbar-title:active { background: var(--fill-selected); }
|
|
57
58
|
.dn-toolbar-actions { display: flex; align-items: center; gap: 2px; margin-left: auto; }
|
|
58
59
|
.dn-toolbar .dn-icon-button[aria-pressed="true"], .dn-panel-tabs .dn-icon-button[aria-pressed="true"] { background: var(--fill-selected); color: var(--ink); }
|
|
59
60
|
|
|
@@ -77,7 +78,7 @@ Web 应用的页面骨架:左边是[侧边栏](sidebar.md),中间是内容
|
|
|
77
78
|
white-space: nowrap;
|
|
78
79
|
cursor: pointer;
|
|
79
80
|
}
|
|
80
|
-
.dn-panel-tabs [role="tab"]:hover { color: var(--ink); }
|
|
81
|
+
@media (hover: hover) { .dn-panel-tabs [role="tab"]:hover { color: var(--ink); } }
|
|
81
82
|
.dn-panel-tabs [role="tab"][aria-selected="true"] { background: var(--fill-selected); color: var(--ink); }
|
|
82
83
|
.dn-panel-tabs .dn-panel-actions { display: flex; gap: 2px; margin-left: auto; }
|
|
83
84
|
.dn-panel-body { flex: 1; min-height: 0; overflow: auto; padding: var(--space-3) var(--space-4); font: var(--type-code); color: var(--ink-secondary); }
|
|
@@ -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
|
|
|
@@ -57,6 +57,19 @@
|
|
|
57
57
|
- 弹出层和面板内部滚动时加 `overscroll-behavior: contain`,不带动后面的页面。
|
|
58
58
|
- 跳到页面锚点时,用 `scroll-padding-top` 留出吸顶元素的高度。
|
|
59
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
|
+
|
|
60
73
|
## 响应式
|
|
61
74
|
|
|
62
75
|
- `breakpoint-sm`(760)以下侧边栏收起成抽屉,多栏变单栏;`breakpoint-md`(960)以下侧面板变成抽屉,目录这类次要栏收起;`breakpoint-lg`(1280)以上内容不再变宽,两侧留白。断点只能写在媒体查询里,没有对应的 CSS 变量。
|
|
@@ -71,6 +84,11 @@
|
|
|
71
84
|
.dn-column.is-form { width: min(100%, var(--form-max) + 2 * var(--page-margin)); }
|
|
72
85
|
.dn-column.is-wide { width: min(100%, var(--wide-max) + 2 * var(--page-margin)); }
|
|
73
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)); }
|
|
74
92
|
.dn-baseline { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
|
|
75
93
|
.dn-baseline-title { margin: 0; font: var(--type-title); }
|
|
76
94
|
.dn-baseline-meta { color: var(--ink-tertiary); font: var(--type-footnote); font-variant-numeric: tabular-nums; }
|
|
@@ -85,6 +103,16 @@
|
|
|
85
103
|
.dn-item.is-top > .dn-item-end { margin-top: calc((var(--type-body-line) - var(--button-height-sm)) / 2); }
|
|
86
104
|
.dn-centered { display: grid; grid-template-rows: 2fr auto 3fr; justify-items: center; min-height: 100%; text-align: center; }
|
|
87
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
|
+
}
|
|
88
116
|
```
|
|
89
117
|
|
|
90
118
|
不同字号并排,按基线对齐:
|
|
@@ -134,9 +162,28 @@
|
|
|
134
162
|
</div>
|
|
135
163
|
```
|
|
136
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
|
+
|
|
137
184
|
## SwiftUI
|
|
138
185
|
|
|
139
|
-
`Form`、`List`、`NavigationSplitView`
|
|
186
|
+
`Form`、`List`、`NavigationSplitView` 自带对齐和滚动,不要改;滚动指示条保持系统默认,跟随用户的系统设置。只有横向卡片条可以用 `.scrollIndicators(.hidden)`,并露出半张卡片提示还能滑动。自己排的内容按下面写:
|
|
140
187
|
|
|
141
188
|
```swift
|
|
142
189
|
// 图标和文字:用 Label,图标会对齐到第一行文字
|
|
@@ -170,6 +217,17 @@ ScrollView {
|
|
|
170
217
|
.frame(maxWidth: .infinity)
|
|
171
218
|
}
|
|
172
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
|
+
|
|
173
231
|
// 空状态:系统组件已经处理了视觉居中
|
|
174
232
|
ContentUnavailableView("还没有笔记", systemImage: "doc", description: Text("点右上角的加号新建一条。"))
|
|
175
233
|
```
|
|
@@ -183,4 +241,5 @@ ContentUnavailableView("还没有笔记", systemImage: "doc", description: Text(
|
|
|
183
241
|
- 把空状态贴在顶部,或者正好放在几何中心。
|
|
184
242
|
- 把正文拉满宽屏;正文和普通标题居中排版。
|
|
185
243
|
- 页面和里面的栏同时滚动;同一方向嵌套滚动;页面出现横向滚动。
|
|
244
|
+
- 给单个组件另写一套滚动条,或者用脚本模拟滚动条;隐藏纵向滚动区域的滚动条;接管滚轮或改滚动速度;在应用的 `html` 上预留滚动条位置。
|
|
186
245
|
- 按设备型号写死布局;用可用宽度和尺寸类别判断。
|
|
@@ -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
|
+
- 用悬停底色代替键盘焦点环;禁用的元素还有悬停效果。
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- **字重**:粗体只出现在真正的标题上;分组标题、会话名、顶栏标题、说明是常规字重,靠灰度区分。
|
|
16
16
|
- **对齐**:单行控件在固定高度里垂直居中,没有用内边距、`top` 或等高行高去凑;图标和文字按中线,不同字号按基线;三行以上的内容图标对齐第一行;表格数字右对齐;空状态略高于正中;内容栏限宽居中,页面没有横向滚动,每一栏各自滚动。
|
|
17
17
|
- **间距与圆角**:间距在 4pt 网格上;组与组之间至少是组内的 2 倍;圆角来自组件的 `*-radius` token,嵌套圆角同心;阅读栏宽不超过 `content-max`。
|
|
18
|
-
-
|
|
18
|
+
- **交互**:一屏最多一个主按钮;按钮文字以动词开头;有悬停、按下、选中、禁用、加载、空、错误这些需要的状态,悬停底色是 `fill-hover`、选中和按下是 `fill-selected`,选中项悬停不加深,悬停没有阴影和位移,`:hover` 都在 `@media (hover: hover)` 里;禁用和加载时布局不跳动;键盘焦点清楚;菜单和下拉点击才打开,Esc、点外面、选中一项都能关闭,焦点回到触发器,方向键能走完所有项;弹窗打开时背景不能操作也不滚动,关闭后焦点回到原处,危险确认默认聚焦「取消」。
|
|
19
19
|
- **动效**:时长来自 `duration-*`;动效说明因果;没有无目的的循环;减弱动态效果时只剩淡入淡出;切换内容时阅读位置不跳。
|
|
20
20
|
- **无障碍**:两套主题都看过;最大字号或 200% 缩放下不截断;只有图标的按钮有标签,指针端有 tooltip(不是 `title` 属性),键盘聚焦时也显示;降低透明度时玻璃变成实色。
|
|
21
21
|
- **文案**:用“你”;没有感叹号和 emoji;完成态用「已保存」;错误说明原因和解决办法;中文与英文、数字之间有半角空格。
|
|
@@ -74,7 +74,7 @@ Web 上按钮“上下特别宽”几乎都是同一个原因:用内边距和
|
|
|
74
74
|
- 分段控件、开关这类有状态的控件,用 `aria-pressed`、`role="switch"` 或 `role="tab"` 标出当前状态。
|
|
75
75
|
- 弹窗用 `<dialog>` 的 `showModal()`,自带焦点管理和 Esc 关闭;popover 用 `popover` 属性。打开、关闭和焦点规则见 [弹窗与 popover](../components/sheet.md)。
|
|
76
76
|
- 菜单和下拉:选值优先用原生 `<select>`,命令菜单和链接列表按 [菜单与下拉](../components/menu.md) 选语义、处理键盘。
|
|
77
|
-
- 悬停效果放在 `@media (hover: hover)`
|
|
77
|
+
- 悬停效果放在 `@media (hover: hover)` 里,触屏上不会卡在悬停状态。各元素的悬停、按下、选中见 [交互状态](../foundations/states.md)。
|
|
78
78
|
|
|
79
79
|
## 验证
|
|
80
80
|
|
|
@@ -254,6 +254,8 @@
|
|
|
254
254
|
| `breakpoint-sm` | `760px` | 窄屏:侧边栏收起,多栏变单栏。 |
|
|
255
255
|
| `breakpoint-md` | `960px` | 中等宽度:目录和次要栏收起。 |
|
|
256
256
|
| `breakpoint-lg` | `1280px` | 宽屏:内容不再继续变宽,两侧留白。 |
|
|
257
|
+
| `scrollbar-size` | `10px` | 网页滚动条占的宽度(横向滚动条是高度)。(仅网页) |
|
|
258
|
+
| `scrollbar-thumb-size` | `6px` | 滚动条里胶囊滑块的粗细,两边各留 (scrollbar-size − 它) ÷ 2。(仅网页) |
|
|
257
259
|
|
|
258
260
|
| Token | 桌面<br>Web 应用 · macOS 内容区 | 触控<br>iOS · iPadOS · Web 触屏 | 舒展<br>官网与文档 | 用途 |
|
|
259
261
|
| --- | --- | --- | --- | --- |
|